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.
package/dist/cli/score.js CHANGED
@@ -1,68 +1,78 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli/score.ts
4
- import { resolve as resolve4, join as join4 } from "path";
5
- import { mkdirSync, writeFileSync, readFileSync } from "fs";
6
-
7
- // src/engine/analyzer.ts
8
- import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
9
- import { join as join2, extname, relative as relative2, resolve as resolve2, basename as basename2 } from "path";
10
- import { createHash } from "crypto";
11
-
12
- // src/types/engine.ts
13
- var DEFAULT_RISK_WEIGHTS = {
14
- hub: 30,
15
- typeProvider: 25,
16
- complexity: 15,
17
- recency: 15,
18
- config: 10,
19
- churn: 5
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
20
10
  };
21
11
 
22
- // src/types/config.ts
23
- var DEFAULT_CONFIG = {
24
- version: "2.0",
25
- analysis: {
26
- extensions: {
27
- code: ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "kt", "rb", "php", "c", "cpp", "h", "hpp", "cs"],
28
- config: ["json", "yml", "yaml", "toml"],
29
- docs: ["md", "txt", "rst"]
30
- },
31
- ignore: {
32
- dirs: ["node_modules", "dist", "build", ".git", "coverage", "__pycache__", ".next", "vendor", ".cto"],
33
- patterns: ["*.min.js", "*.map", "*.lock", "*.generated.*"]
34
- },
35
- maxDepth: 20
36
- },
37
- risk: {
38
- weights: {
12
+ // src/types/engine.ts
13
+ var DEFAULT_RISK_WEIGHTS;
14
+ var init_engine = __esm({
15
+ "src/types/engine.ts"() {
16
+ "use strict";
17
+ DEFAULT_RISK_WEIGHTS = {
39
18
  hub: 30,
40
19
  typeProvider: 25,
41
20
  complexity: 15,
42
21
  recency: 15,
43
22
  config: 10,
44
23
  churn: 5
45
- }
46
- },
47
- interaction: {
48
- defaultBudget: 5e4,
49
- defaultModel: "claude-sonnet-4"
50
- },
51
- tokens: {
52
- method: "chars4"
53
- },
54
- governance: {
55
- auditEnabled: true,
56
- secretDetection: true,
57
- retentionDays: 90
24
+ };
58
25
  }
59
- };
26
+ });
27
+
28
+ // src/types/config.ts
29
+ var DEFAULT_CONFIG;
30
+ var init_config = __esm({
31
+ "src/types/config.ts"() {
32
+ "use strict";
33
+ DEFAULT_CONFIG = {
34
+ version: "2.0",
35
+ analysis: {
36
+ extensions: {
37
+ code: ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "kt", "rb", "php", "c", "cpp", "h", "hpp", "cs"],
38
+ config: ["json", "yml", "yaml", "toml"],
39
+ docs: ["md", "txt", "rst"]
40
+ },
41
+ ignore: {
42
+ dirs: ["node_modules", "dist", "build", ".git", "coverage", "__pycache__", ".next", "vendor", ".cto"],
43
+ patterns: ["*.min.js", "*.map", "*.lock", "*.generated.*"]
44
+ },
45
+ maxDepth: 20
46
+ },
47
+ risk: {
48
+ weights: {
49
+ hub: 30,
50
+ typeProvider: 25,
51
+ complexity: 15,
52
+ recency: 15,
53
+ config: 10,
54
+ churn: 5
55
+ }
56
+ },
57
+ interaction: {
58
+ defaultBudget: 5e4,
59
+ defaultModel: "claude-sonnet-4"
60
+ },
61
+ tokens: {
62
+ method: "chars4"
63
+ },
64
+ governance: {
65
+ auditEnabled: true,
66
+ secretDetection: true,
67
+ retentionDays: 90
68
+ }
69
+ };
70
+ }
71
+ });
60
72
 
61
73
  // src/engine/tokenizer.ts
62
74
  import { encodingForModel } from "js-tiktoken";
63
75
  import { readFile, stat } from "fs/promises";
64
- var CHARS_PER_TOKEN = 4;
65
- var encoder = null;
66
76
  function getEncoder() {
67
77
  if (!encoder) {
68
78
  encoder = encodingForModel("claude-3-5-sonnet-20241022");
@@ -87,12 +97,19 @@ function estimateTokens(content, sizeInBytes, method = "chars4") {
87
97
  }
88
98
  return countTokensChars4(sizeInBytes);
89
99
  }
100
+ var CHARS_PER_TOKEN, encoder;
101
+ var init_tokenizer = __esm({
102
+ "src/engine/tokenizer.ts"() {
103
+ "use strict";
104
+ CHARS_PER_TOKEN = 4;
105
+ encoder = null;
106
+ }
107
+ });
90
108
 
91
109
  // src/engine/graph.ts
92
110
  import { Project, SyntaxKind } from "ts-morph";
93
111
  import { resolve, relative, dirname, join } from "path";
94
112
  import { existsSync } from "fs";
95
- var TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs", "cts", "cjs"]);
96
113
  function createProject(projectPath, filePaths) {
97
114
  const tsConfigPath = join(projectPath, "tsconfig.json");
98
115
  const hasTsConfig = existsSync(tsConfigPath);
@@ -191,41 +208,6 @@ function buildProjectGraph(projectPath, files) {
191
208
  enrichComplexity(project, absPath, files);
192
209
  return { nodes, edges, hubs, leaves, orphans, clusters };
193
210
  }
194
- var UnionFind = class {
195
- parent;
196
- rank;
197
- constructor(nodes) {
198
- this.parent = /* @__PURE__ */ new Map();
199
- this.rank = /* @__PURE__ */ new Map();
200
- for (const n of nodes) {
201
- this.parent.set(n, n);
202
- this.rank.set(n, 0);
203
- }
204
- }
205
- find(x) {
206
- const p = this.parent.get(x);
207
- if (p === void 0) return x;
208
- if (p !== x) {
209
- this.parent.set(x, this.find(p));
210
- }
211
- return this.parent.get(x);
212
- }
213
- union(a, b) {
214
- const ra = this.find(a);
215
- const rb = this.find(b);
216
- if (ra === rb) return;
217
- const rankA = this.rank.get(ra) ?? 0;
218
- const rankB = this.rank.get(rb) ?? 0;
219
- if (rankA < rankB) {
220
- this.parent.set(ra, rb);
221
- } else if (rankA > rankB) {
222
- this.parent.set(rb, ra);
223
- } else {
224
- this.parent.set(rb, ra);
225
- this.rank.set(ra, rankA + 1);
226
- }
227
- }
228
- };
229
211
  function detectClusters(nodes, edges, files) {
230
212
  const uf = new UnionFind(nodes);
231
213
  for (const edge of edges) {
@@ -363,6 +345,48 @@ function emptyGraph(files) {
363
345
  clusters: []
364
346
  };
365
347
  }
348
+ var TS_EXTENSIONS, UnionFind;
349
+ var init_graph = __esm({
350
+ "src/engine/graph.ts"() {
351
+ "use strict";
352
+ TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs", "cts", "cjs"]);
353
+ UnionFind = class {
354
+ parent;
355
+ rank;
356
+ constructor(nodes) {
357
+ this.parent = /* @__PURE__ */ new Map();
358
+ this.rank = /* @__PURE__ */ new Map();
359
+ for (const n of nodes) {
360
+ this.parent.set(n, n);
361
+ this.rank.set(n, 0);
362
+ }
363
+ }
364
+ find(x) {
365
+ const p = this.parent.get(x);
366
+ if (p === void 0) return x;
367
+ if (p !== x) {
368
+ this.parent.set(x, this.find(p));
369
+ }
370
+ return this.parent.get(x);
371
+ }
372
+ union(a, b) {
373
+ const ra = this.find(a);
374
+ const rb = this.find(b);
375
+ if (ra === rb) return;
376
+ const rankA = this.rank.get(ra) ?? 0;
377
+ const rankB = this.rank.get(rb) ?? 0;
378
+ if (rankA < rankB) {
379
+ this.parent.set(ra, rb);
380
+ } else if (rankA > rankB) {
381
+ this.parent.set(rb, ra);
382
+ } else {
383
+ this.parent.set(rb, ra);
384
+ this.rank.set(ra, rankA + 1);
385
+ }
386
+ }
387
+ };
388
+ }
389
+ });
366
390
 
367
391
  // src/engine/risk.ts
368
392
  function scoreAllFiles(files, graph, weights = DEFAULT_RISK_WEIGHTS) {
@@ -480,8 +504,17 @@ function computeTypeProviderUsage(files, graph) {
480
504
  }
481
505
  return usage;
482
506
  }
507
+ var init_risk = __esm({
508
+ "src/engine/risk.ts"() {
509
+ "use strict";
510
+ init_engine();
511
+ }
512
+ });
483
513
 
484
514
  // src/engine/analyzer.ts
515
+ import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
516
+ import { join as join2, extname, relative as relative2, resolve as resolve2, basename as basename2 } from "path";
517
+ import { createHash } from "crypto";
485
518
  function matchesPattern(filename, patterns) {
486
519
  for (const pattern of patterns) {
487
520
  if (pattern.startsWith("*.")) {
@@ -544,10 +577,6 @@ async function walkProject(rootPath, options) {
544
577
  await walk(rootPath, 0);
545
578
  return results;
546
579
  }
547
- var TYPE_PATTERNS = [/types?\//i, /\.d\.ts$/, /interfaces?\//i];
548
- var TEST_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//];
549
- var CONFIG_PATTERNS = [/\.config\.[jt]s$/, /rc\.[jt]s$/, /\.env/, /tsconfig/, /package\.json$/, /\.yml$/, /\.yaml$/, /\.toml$/];
550
- var ENTRY_PATTERNS = [/^index\.[jt]sx?$/, /^main\.[jt]sx?$/, /^app\.[jt]sx?$/, /^server\.[jt]sx?$/];
551
580
  function classifyFileKind(relativePath) {
552
581
  const filename = basename2(relativePath);
553
582
  if (TYPE_PATTERNS.some((p) => p.test(relativePath))) return "type";
@@ -706,46 +735,60 @@ function mergeConfig(base, overrides) {
706
735
  }
707
736
  };
708
737
  }
709
-
710
- // src/engine/selector.ts
711
- import { createHash as createHash2 } from "crypto";
738
+ var TYPE_PATTERNS, TEST_PATTERNS, CONFIG_PATTERNS, ENTRY_PATTERNS;
739
+ var init_analyzer = __esm({
740
+ "src/engine/analyzer.ts"() {
741
+ "use strict";
742
+ init_engine();
743
+ init_config();
744
+ init_tokenizer();
745
+ init_graph();
746
+ init_risk();
747
+ TYPE_PATTERNS = [/types?\//i, /\.d\.ts$/, /interfaces?\//i];
748
+ TEST_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//];
749
+ CONFIG_PATTERNS = [/\.config\.[jt]s$/, /rc\.[jt]s$/, /\.env/, /tsconfig/, /package\.json$/, /\.yml$/, /\.yaml$/, /\.toml$/];
750
+ ENTRY_PATTERNS = [/^index\.[jt]sx?$/, /^main\.[jt]sx?$/, /^app\.[jt]sx?$/, /^server\.[jt]sx?$/];
751
+ }
752
+ });
712
753
 
713
754
  // src/govern/secrets.ts
755
+ var secrets_exports = {};
756
+ __export(secrets_exports, {
757
+ DEFAULT_AUDIT_CONFIG: () => DEFAULT_AUDIT_CONFIG,
758
+ addToAllowlist: () => addToAllowlist,
759
+ auditProject: () => auditProject,
760
+ filterByAllowlist: () => filterByAllowlist,
761
+ generatePreCommitHook: () => generatePreCommitHook,
762
+ getChangedFiles: () => getChangedFiles,
763
+ loadAllowlist: () => loadAllowlist,
764
+ loadAuditConfig: () => loadAuditConfig,
765
+ sanitizeContent: () => sanitizeContent,
766
+ saveAllowlist: () => saveAllowlist,
767
+ saveAuditConfig: () => saveAuditConfig,
768
+ scanContentForHighEntropy: () => scanContentForHighEntropy,
769
+ scanContentForSecrets: () => scanContentForSecrets,
770
+ scanFileForSecrets: () => scanFileForSecrets,
771
+ scanProjectForSecrets: () => scanProjectForSecrets
772
+ });
714
773
  import { readFile as readFile3 } from "fs/promises";
715
- import { resolve as resolve3, relative as relative3 } from "path";
716
- var BUILTIN_PATTERNS = [
717
- // API Keys
718
- { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
719
- { type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
720
- { type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
721
- // AWS
722
- { type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
723
- { type: "aws-key", source: `(?:aws_secret_access_key|aws_secret)\\s*[:=]\\s*['"]?([a-zA-Z0-9/+=]{40})['"]?`, flags: "gi", severity: "critical", description: "AWS Secret Key" },
724
- // Private Keys
725
- { type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
726
- { type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
727
- // Passwords
728
- { type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
729
- { type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
730
- // Tokens
731
- { type: "token", source: `(?:bearer|token|auth_token|access_token|refresh_token)\\s*[:=]\\s*['"]([a-zA-Z0-9_\\-.]{20,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Auth Token" },
732
- { type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
733
- { type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
734
- { type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
735
- { type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
736
- // Connection strings
737
- { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
738
- { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
739
- // Environment variables with secrets
740
- { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
741
- ];
774
+ import { readFileSync, existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
775
+ import { resolve as resolve3, relative as relative3, join as join3, dirname as dirname2 } from "path";
776
+ import { createHash as createHash2 } from "crypto";
777
+ function getBuiltinPatterns() {
778
+ if (!_cachedBuiltinPatterns) {
779
+ _cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
780
+ type: def.type,
781
+ pattern: new RegExp(def.source, def.flags),
782
+ severity: def.severity,
783
+ description: def.description
784
+ }));
785
+ }
786
+ return _cachedBuiltinPatterns;
787
+ }
742
788
  function buildPatterns(customPatterns = []) {
743
- const patterns = BUILTIN_PATTERNS.map((def) => ({
744
- type: def.type,
745
- pattern: new RegExp(def.source, def.flags),
746
- severity: def.severity,
747
- description: def.description
748
- }));
789
+ const builtins = getBuiltinPatterns();
790
+ if (customPatterns.length === 0) return builtins;
791
+ const patterns = [...builtins];
749
792
  for (const custom of customPatterns) {
750
793
  try {
751
794
  patterns.push({
@@ -759,7 +802,7 @@ function buildPatterns(customPatterns = []) {
759
802
  }
760
803
  return patterns;
761
804
  }
762
- function scanContentForSecrets(content, filePath, customPatterns = []) {
805
+ function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
763
806
  const findings = [];
764
807
  const lines = content.split("\n");
765
808
  const allPatterns = buildPatterns(customPatterns);
@@ -771,6 +814,7 @@ function scanContentForSecrets(content, filePath, customPatterns = []) {
771
814
  while ((match = secretPattern.pattern.exec(line)) !== null) {
772
815
  const matchText = match[0];
773
816
  if (isTemplateOrPlaceholder(matchText)) continue;
817
+ if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
774
818
  findings.push({
775
819
  type: secretPattern.type,
776
820
  file: filePath,
@@ -793,6 +837,28 @@ async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
793
837
  return [];
794
838
  }
795
839
  }
840
+ async function scanProjectForSecrets(projectPath, filePaths, customPatterns = []) {
841
+ const allFindings = [];
842
+ for (const fp of filePaths) {
843
+ const findings = await scanFileForSecrets(fp, projectPath, customPatterns);
844
+ allFindings.push(...findings);
845
+ }
846
+ return allFindings.sort((a, b) => {
847
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
848
+ return severityOrder[a.severity] - severityOrder[b.severity];
849
+ });
850
+ }
851
+ function sanitizeContent(content, customPatterns = []) {
852
+ let sanitized = content;
853
+ const allPatterns = buildPatterns(customPatterns);
854
+ for (const secretPattern of allPatterns) {
855
+ sanitized = sanitized.replace(secretPattern.pattern, (match) => {
856
+ if (isTemplateOrPlaceholder(match)) return match;
857
+ return redactSecret(match);
858
+ });
859
+ }
860
+ return sanitized;
861
+ }
796
862
  function redactSecret(value) {
797
863
  if (value.length <= 8) return "***REDACTED***";
798
864
  const prefix = value.substring(0, 4);
@@ -818,6 +884,14 @@ function isTemplateOrPlaceholder(value) {
818
884
  ];
819
885
  return placeholders.some((p) => p.test(value));
820
886
  }
887
+ function isSafeEmail(value, extraDomains) {
888
+ const match = value.match(/@([a-zA-Z0-9.-]+)$/);
889
+ if (!match) return false;
890
+ const domain = match[1].toLowerCase();
891
+ if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
892
+ if (extraDomains && extraDomains.has(domain)) return true;
893
+ return false;
894
+ }
821
895
  function deduplicateFindings(findings) {
822
896
  const seen = /* @__PURE__ */ new Set();
823
897
  return findings.filter((f) => {
@@ -827,13 +901,453 @@ function deduplicateFindings(findings) {
827
901
  return true;
828
902
  });
829
903
  }
904
+ function fingerprintFinding(f) {
905
+ return createHash2("sha256").update(`${f.file}:${f.type}:${f.match}`).digest("hex").slice(0, 32);
906
+ }
907
+ function getAllowlistPath(projectPath) {
908
+ return join3(projectPath, ".cto", "audit", "allowlist.json");
909
+ }
910
+ function loadAllowlist(projectPath) {
911
+ const filePath = getAllowlistPath(projectPath);
912
+ if (!existsSync2(filePath)) return [];
913
+ try {
914
+ return JSON.parse(readFileSync(filePath, "utf-8"));
915
+ } catch {
916
+ return [];
917
+ }
918
+ }
919
+ function saveAllowlist(projectPath, entries) {
920
+ const filePath = getAllowlistPath(projectPath);
921
+ mkdirSync(dirname2(filePath), { recursive: true });
922
+ writeFileSync(filePath, JSON.stringify(entries, null, 2) + "\n");
923
+ }
924
+ function addToAllowlist(projectPath, finding, reason, reviewedBy = "manual") {
925
+ const entries = loadAllowlist(projectPath);
926
+ const entry = {
927
+ fingerprint: fingerprintFinding(finding),
928
+ file: finding.file,
929
+ type: finding.type,
930
+ redacted: finding.redacted,
931
+ reason,
932
+ reviewedBy,
933
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
934
+ };
935
+ const existing = entries.findIndex((e) => e.fingerprint === entry.fingerprint);
936
+ if (existing >= 0) {
937
+ entries[existing] = entry;
938
+ } else {
939
+ entries.push(entry);
940
+ }
941
+ saveAllowlist(projectPath, entries);
942
+ return entry;
943
+ }
944
+ function filterByAllowlist(findings, projectPath) {
945
+ const allowlist = loadAllowlist(projectPath);
946
+ if (allowlist.length === 0) return { filtered: findings, allowed: [] };
947
+ const allowedFingerprints = new Set(allowlist.map((e) => e.fingerprint));
948
+ const filtered = [];
949
+ const allowed = [];
950
+ for (const f of findings) {
951
+ if (allowedFingerprints.has(fingerprintFinding(f))) {
952
+ allowed.push(f);
953
+ } else {
954
+ filtered.push(f);
955
+ }
956
+ }
957
+ return { filtered, allowed };
958
+ }
959
+ function getHashCachePath(projectPath) {
960
+ return join3(projectPath, ".cto", "audit", ".hashcache.json");
961
+ }
962
+ function loadHashCache(projectPath) {
963
+ const filePath = getHashCachePath(projectPath);
964
+ if (!existsSync2(filePath)) return {};
965
+ try {
966
+ return JSON.parse(readFileSync(filePath, "utf-8"));
967
+ } catch {
968
+ return {};
969
+ }
970
+ }
971
+ function saveHashCache(projectPath, cache) {
972
+ const filePath = getHashCachePath(projectPath);
973
+ mkdirSync(dirname2(filePath), { recursive: true });
974
+ writeFileSync(filePath, JSON.stringify(cache));
975
+ }
976
+ function hashContent(content) {
977
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
978
+ }
979
+ function getChangedFiles(projectPath, filePaths) {
980
+ const oldCache = loadHashCache(projectPath);
981
+ const newCache = {};
982
+ const changed = [];
983
+ const unchanged = [];
984
+ for (const fp of filePaths) {
985
+ try {
986
+ const content = readFileSync(fp, "utf-8");
987
+ const relPath = relative3(resolve3(projectPath), resolve3(fp));
988
+ const hash = hashContent(content);
989
+ newCache[relPath] = hash;
990
+ if (oldCache[relPath] === hash) {
991
+ unchanged.push(fp);
992
+ } else {
993
+ changed.push(fp);
994
+ }
995
+ } catch {
996
+ changed.push(fp);
997
+ }
998
+ }
999
+ return { changed, unchanged, cache: newCache };
1000
+ }
1001
+ function getAuditConfigPath(projectPath) {
1002
+ return join3(projectPath, ".cto", "audit", "config.json");
1003
+ }
1004
+ function loadAuditConfig(projectPath) {
1005
+ const filePath = getAuditConfigPath(projectPath);
1006
+ if (!existsSync2(filePath)) return { ...DEFAULT_AUDIT_CONFIG };
1007
+ try {
1008
+ const loaded = JSON.parse(readFileSync(filePath, "utf-8"));
1009
+ return { ...DEFAULT_AUDIT_CONFIG, ...loaded };
1010
+ } catch {
1011
+ return { ...DEFAULT_AUDIT_CONFIG };
1012
+ }
1013
+ }
1014
+ function saveAuditConfig(projectPath, config) {
1015
+ const filePath = getAuditConfigPath(projectPath);
1016
+ mkdirSync(dirname2(filePath), { recursive: true });
1017
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
1018
+ }
1019
+ function applySeverityOverrides(findings, overrides) {
1020
+ if (Object.keys(overrides).length === 0) return findings;
1021
+ return findings.map((f) => {
1022
+ const override = overrides[f.type];
1023
+ if (override) return { ...f, severity: override };
1024
+ return f;
1025
+ });
1026
+ }
1027
+ function generatePreCommitHook(projectPath, hookType = "husky") {
1028
+ const hookContent = `#!/bin/sh
1029
+ # CTO Secret Detection \u2014 Pre-commit hook
1030
+ # Auto-generated by: npx cto-ai-cli --audit --init-hook
1031
+ # Scans ONLY staged files for secrets before allowing commit.
1032
+
1033
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
1034
+
1035
+ if [ -z "$STAGED_FILES" ]; then
1036
+ exit 0
1037
+ fi
1038
+
1039
+ echo "\u{1F50D} CTO: Scanning $(echo "$STAGED_FILES" | wc -l | tr -d ' ') staged files for secrets..."
1040
+
1041
+ # Write staged files to temp list
1042
+ TMPFILE=$(mktemp)
1043
+ echo "$STAGED_FILES" > "$TMPFILE"
1044
+
1045
+ # Run audit in CI mode on staged files only
1046
+ CI=true npx cto-ai-cli --audit --files "$TMPFILE"
1047
+ RESULT=$?
1048
+
1049
+ rm -f "$TMPFILE"
1050
+
1051
+ if [ $RESULT -ne 0 ]; then
1052
+ echo ""
1053
+ echo "\u274C Commit blocked: secrets detected in staged files."
1054
+ echo " Run 'npx cto-ai-cli --audit' to see details."
1055
+ echo " Use allowlist to mark reviewed findings as safe."
1056
+ echo ""
1057
+ exit 1
1058
+ fi
1059
+
1060
+ echo "\u2705 No secrets detected. Proceeding with commit."
1061
+ `;
1062
+ let hookPath;
1063
+ if (hookType === "husky") {
1064
+ hookPath = join3(projectPath, ".husky", "pre-commit");
1065
+ } else {
1066
+ hookPath = join3(projectPath, ".git", "hooks", "pre-commit");
1067
+ }
1068
+ mkdirSync(dirname2(hookPath), { recursive: true });
1069
+ writeFileSync(hookPath, hookContent, { mode: 493 });
1070
+ return hookPath;
1071
+ }
1072
+ function shannonEntropy(str) {
1073
+ const freq = /* @__PURE__ */ new Map();
1074
+ for (const ch of str) {
1075
+ freq.set(ch, (freq.get(ch) || 0) + 1);
1076
+ }
1077
+ let entropy = 0;
1078
+ for (const count of freq.values()) {
1079
+ const p = count / str.length;
1080
+ if (p > 0) entropy -= p * Math.log2(p);
1081
+ }
1082
+ return entropy;
1083
+ }
1084
+ function scanContentForHighEntropy(content, filePath, threshold = 5) {
1085
+ const findings = [];
1086
+ const lines = content.split("\n");
1087
+ for (let i = 0; i < lines.length; i++) {
1088
+ const line = lines[i];
1089
+ if (line.trim().startsWith("//") || line.trim().startsWith("#") || line.trim().startsWith("*")) continue;
1090
+ HIGH_ENTROPY_RE.lastIndex = 0;
1091
+ let match;
1092
+ while ((match = HIGH_ENTROPY_RE.exec(line)) !== null) {
1093
+ const value = match[1] || match[2];
1094
+ if (!value || value.length < 40) continue;
1095
+ if (isTemplateOrPlaceholder(value)) continue;
1096
+ if (ENTROPY_SKIP.some((p) => p.test(value))) continue;
1097
+ const entropy = shannonEntropy(value);
1098
+ if (entropy >= threshold) {
1099
+ findings.push({
1100
+ type: "high-entropy",
1101
+ file: filePath,
1102
+ line: i + 1,
1103
+ match: value,
1104
+ redacted: redactSecret(value),
1105
+ severity: entropy >= 5 ? "high" : "medium"
1106
+ });
1107
+ }
1108
+ }
1109
+ }
1110
+ return deduplicateFindings(findings);
1111
+ }
1112
+ async function auditProject(projectPath, filePaths, options = {}) {
1113
+ const savedConfig = loadAuditConfig(projectPath);
1114
+ const customPatterns = options.customPatterns ?? savedConfig.customPatterns;
1115
+ const entropyThreshold = options.entropyThreshold ?? savedConfig.entropyThreshold;
1116
+ const includePII = options.includePII ?? savedConfig.includePII;
1117
+ const useAllowlist = options.useAllowlist ?? true;
1118
+ const incrementalScan = options.incrementalScan ?? savedConfig.incrementalScan;
1119
+ const severityOverrides = options.severityOverrides ?? savedConfig.severityOverrides;
1120
+ let extraPiiDomains;
1121
+ const allExtraDomains = [...options.piiSafeDomains || [], ...savedConfig.piiSafeDomains];
1122
+ if (allExtraDomains.length > 0) {
1123
+ extraPiiDomains = new Set(allExtraDomains.map((d) => d.toLowerCase()));
1124
+ }
1125
+ let filesToScan = filePaths;
1126
+ let unchangedCount = 0;
1127
+ let newCache = null;
1128
+ if (incrementalScan) {
1129
+ const { changed, unchanged, cache } = getChangedFiles(projectPath, filePaths);
1130
+ newCache = cache;
1131
+ if (changed.length < filePaths.length) {
1132
+ filesToScan = changed;
1133
+ unchangedCount = unchanged.length;
1134
+ }
1135
+ }
1136
+ const allFindings = [];
1137
+ const filesWithSecrets = /* @__PURE__ */ new Set();
1138
+ for (const fp of filesToScan) {
1139
+ try {
1140
+ const content = await readFile3(fp, "utf-8");
1141
+ const relPath = relative3(resolve3(projectPath), resolve3(fp));
1142
+ const isTestFile = /\.(test|spec|mock)\.[jt]sx?$/.test(relPath) || relPath.includes("__tests__");
1143
+ const isDtsFile = relPath.endsWith(".d.ts");
1144
+ let findings = scanContentForSecrets(content, relPath, customPatterns, extraPiiDomains);
1145
+ if (!includePII) {
1146
+ findings = findings.filter((f) => f.type !== "pii");
1147
+ }
1148
+ const entropyFindings = isTestFile || isDtsFile ? [] : scanContentForHighEntropy(content, relPath, entropyThreshold);
1149
+ const combined = [...findings, ...entropyFindings];
1150
+ if (combined.length > 0) {
1151
+ filesWithSecrets.add(relPath);
1152
+ allFindings.push(...combined);
1153
+ }
1154
+ } catch {
1155
+ }
1156
+ }
1157
+ let finalFindings = applySeverityOverrides(allFindings, severityOverrides);
1158
+ let allowedCount = 0;
1159
+ if (useAllowlist) {
1160
+ const { filtered, allowed } = filterByAllowlist(finalFindings, projectPath);
1161
+ finalFindings = filtered;
1162
+ allowedCount = allowed.length;
1163
+ }
1164
+ finalFindings.sort((a, b) => {
1165
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
1166
+ return order[a.severity] - order[b.severity];
1167
+ });
1168
+ if (newCache) {
1169
+ saveHashCache(projectPath, newCache);
1170
+ }
1171
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
1172
+ const byType = {};
1173
+ for (const f of finalFindings) {
1174
+ bySeverity[f.severity]++;
1175
+ byType[f.type] = (byType[f.type] || 0) + 1;
1176
+ }
1177
+ const recommendations = [];
1178
+ if (bySeverity.critical > 0) {
1179
+ recommendations.push("CRITICAL: Rotate all detected credentials immediately. They may already be compromised.");
1180
+ }
1181
+ if (byType["password"] > 0) {
1182
+ recommendations.push("Move passwords to environment variables or a secrets manager (AWS Secrets Manager, Vault, etc.).");
1183
+ }
1184
+ if (byType["api-key"] > 0 || byType["aws-key"] > 0) {
1185
+ recommendations.push("Use environment variables for API keys. Never commit them to source control.");
1186
+ }
1187
+ if (byType["connection-string"] > 0) {
1188
+ recommendations.push("Database connection strings should use environment variables, not hardcoded values.");
1189
+ }
1190
+ if (byType["private-key"] > 0) {
1191
+ recommendations.push("Private keys should NEVER be in source code. Use a key management service.");
1192
+ }
1193
+ if (byType["pii"] > 0) {
1194
+ recommendations.push("PII detected. Review for GDPR/CCPA compliance. Consider data anonymization.");
1195
+ }
1196
+ if (byType["high-entropy"] > 0) {
1197
+ recommendations.push("High-entropy strings detected that may be secrets. Review manually.");
1198
+ }
1199
+ if (finalFindings.length > 0) {
1200
+ recommendations.push("Add a .gitignore entry for .env files if not already present.");
1201
+ recommendations.push("Run `npx cto-ai-cli --audit` regularly or add to CI pipeline.");
1202
+ }
1203
+ if (finalFindings.length === 0) {
1204
+ recommendations.push("No secrets detected. Great job keeping your codebase clean!");
1205
+ }
1206
+ if (allowedCount > 0) {
1207
+ recommendations.push(`${allowedCount} finding(s) skipped via allowlist (.cto/audit/allowlist.json).`);
1208
+ }
1209
+ if (unchangedCount > 0) {
1210
+ recommendations.push(`${unchangedCount} unchanged file(s) skipped (incremental scan).`);
1211
+ }
1212
+ return {
1213
+ findings: finalFindings,
1214
+ summary: {
1215
+ totalFiles: filePaths.length,
1216
+ filesScanned: filesToScan.length,
1217
+ filesWithSecrets: filesWithSecrets.size,
1218
+ totalFindings: finalFindings.length,
1219
+ bySeverity,
1220
+ byType
1221
+ },
1222
+ recommendations
1223
+ };
1224
+ }
1225
+ var BUILTIN_PATTERNS, _cachedBuiltinPatterns, PII_SAFE_EMAIL_DOMAINS, DEFAULT_AUDIT_CONFIG, HIGH_ENTROPY_RE, ENTROPY_SKIP;
1226
+ var init_secrets = __esm({
1227
+ "src/govern/secrets.ts"() {
1228
+ "use strict";
1229
+ BUILTIN_PATTERNS = [
1230
+ // API Keys
1231
+ { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
1232
+ { type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
1233
+ { type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
1234
+ // AWS
1235
+ { type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
1236
+ { type: "aws-key", source: `(?:aws_secret_access_key|aws_secret)\\s*[:=]\\s*['"]?([a-zA-Z0-9/+=]{40})['"]?`, flags: "gi", severity: "critical", description: "AWS Secret Key" },
1237
+ // Private Keys
1238
+ { type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
1239
+ { type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
1240
+ // Passwords
1241
+ { type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
1242
+ { type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
1243
+ // Tokens
1244
+ { type: "token", source: `(?:bearer|token|auth_token|access_token|refresh_token)\\s*[:=]\\s*['"]([a-zA-Z0-9_\\-.]{20,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Auth Token" },
1245
+ { type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
1246
+ { type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
1247
+ { type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
1248
+ { type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
1249
+ // Connection strings
1250
+ { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
1251
+ { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
1252
+ // Environment variables with secrets
1253
+ { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
1254
+ // Stripe
1255
+ { type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
1256
+ { type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
1257
+ { type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
1258
+ // Slack
1259
+ { type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
1260
+ { type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
1261
+ { 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" },
1262
+ // Google
1263
+ { type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
1264
+ { type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
1265
+ // Azure
1266
+ { type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
1267
+ // Twilio
1268
+ { type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
1269
+ // SendGrid
1270
+ { type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
1271
+ // JWT
1272
+ { 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" },
1273
+ // Datadog
1274
+ { type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
1275
+ { type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
1276
+ // Sentry
1277
+ { type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
1278
+ // Firebase
1279
+ { type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
1280
+ { type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
1281
+ // Supabase
1282
+ { type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
1283
+ { type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
1284
+ // Vercel
1285
+ { type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
1286
+ // Heroku
1287
+ { type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
1288
+ // DigitalOcean
1289
+ { type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
1290
+ { type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
1291
+ // Mailgun
1292
+ { type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
1293
+ // PII
1294
+ { 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)" },
1295
+ { 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)" }
1296
+ ];
1297
+ _cachedBuiltinPatterns = null;
1298
+ PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
1299
+ "example.com",
1300
+ "example.org",
1301
+ "example.net",
1302
+ "test.com",
1303
+ "test.org",
1304
+ "test.net",
1305
+ "localhost",
1306
+ "localhost.localdomain",
1307
+ "email.com",
1308
+ "mail.com",
1309
+ "foo.com",
1310
+ "bar.com",
1311
+ "baz.com",
1312
+ "acme.com",
1313
+ "company.com",
1314
+ "corp.com",
1315
+ "noreply.com",
1316
+ "no-reply.com",
1317
+ "users.noreply.github.com",
1318
+ "placeholder.com"
1319
+ ]);
1320
+ DEFAULT_AUDIT_CONFIG = {
1321
+ severityOverrides: {},
1322
+ piiSafeDomains: [],
1323
+ customPatterns: [],
1324
+ entropyThreshold: 5,
1325
+ includePII: true,
1326
+ incrementalScan: true
1327
+ };
1328
+ HIGH_ENTROPY_RE = /['"]([a-zA-Z0-9+/=_\-]{30,})['"]|=\s*['"]?([a-zA-Z0-9+/=_\-]{30,})['"]?/g;
1329
+ ENTROPY_SKIP = [
1330
+ /^[a-f0-9]{32,}$/i,
1331
+ // hex hashes
1332
+ /^[A-Z_]{30,}$/,
1333
+ // all-caps constants
1334
+ /^[a-z_]{30,}$/,
1335
+ // all-lowercase identifiers
1336
+ /^[a-zA-Z0-9+/]+=+$/,
1337
+ // base64 padding
1338
+ /^[a-z]+[A-Z][a-zA-Z]+$/,
1339
+ // camelCase identifiers
1340
+ /sha\d+-/i
1341
+ // integrity hashes (sha256-, sha512-)
1342
+ ];
1343
+ }
1344
+ });
830
1345
 
831
1346
  // src/engine/pruner.ts
832
1347
  import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
833
1348
  import { readFile as readFile4 } from "fs/promises";
834
- import { existsSync as existsSync2 } from "fs";
835
- import { join as join3 } from "path";
836
- var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
1349
+ import { existsSync as existsSync3 } from "fs";
1350
+ import { join as join4 } from "path";
837
1351
  async function pruneFile(file, level) {
838
1352
  if (level === "excluded") {
839
1353
  return emptyResult(file, "excluded");
@@ -1078,12 +1592,20 @@ function addJSDoc(node, parts) {
1078
1592
  function findTsConfig(filePath) {
1079
1593
  let dir = filePath;
1080
1594
  for (let i = 0; i < 10; i++) {
1081
- dir = join3(dir, "..");
1082
- const candidate = join3(dir, "tsconfig.json");
1083
- if (existsSync2(candidate)) return candidate;
1595
+ dir = join4(dir, "..");
1596
+ const candidate = join4(dir, "tsconfig.json");
1597
+ if (existsSync3(candidate)) return candidate;
1084
1598
  }
1085
1599
  return void 0;
1086
1600
  }
1601
+ var TS_EXTENSIONS2;
1602
+ var init_pruner = __esm({
1603
+ "src/engine/pruner.ts"() {
1604
+ "use strict";
1605
+ init_tokenizer();
1606
+ TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
1607
+ }
1608
+ });
1087
1609
 
1088
1610
  // src/engine/graph-utils.ts
1089
1611
  function buildAdjacencyList(edges) {
@@ -1137,6 +1659,11 @@ function matchGlob(path, pattern) {
1137
1659
  return false;
1138
1660
  }
1139
1661
  }
1662
+ var init_graph_utils = __esm({
1663
+ "src/engine/graph-utils.ts"() {
1664
+ "use strict";
1665
+ }
1666
+ });
1140
1667
 
1141
1668
  // src/engine/coverage.ts
1142
1669
  function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
@@ -1197,6 +1724,12 @@ function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth =
1197
1724
  explanation
1198
1725
  };
1199
1726
  }
1727
+ var init_coverage = __esm({
1728
+ "src/engine/coverage.ts"() {
1729
+ "use strict";
1730
+ init_graph_utils();
1731
+ }
1732
+ });
1200
1733
 
1201
1734
  // src/engine/budget.ts
1202
1735
  function getPruneLevelForRisk(riskScore) {
@@ -1205,8 +1738,15 @@ function getPruneLevelForRisk(riskScore) {
1205
1738
  if (riskScore >= 30) return "signatures";
1206
1739
  return "skeleton";
1207
1740
  }
1741
+ var init_budget = __esm({
1742
+ "src/engine/budget.ts"() {
1743
+ "use strict";
1744
+ init_pruner();
1745
+ }
1746
+ });
1208
1747
 
1209
1748
  // src/engine/selector.ts
1749
+ import { createHash as createHash3 } from "crypto";
1210
1750
  async function selectContext(input) {
1211
1751
  const { task, analysis, budget, policies, depth = 2 } = input;
1212
1752
  const decisions = [];
@@ -1347,7 +1887,7 @@ async function selectContext(input) {
1347
1887
  );
1348
1888
  const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
1349
1889
  const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
1350
- const hash = createHash2("sha256").update(hashInput).digest("hex").substring(0, 16);
1890
+ const hash = createHash3("sha256").update(hashInput).digest("hex").substring(0, 16);
1351
1891
  return {
1352
1892
  files: selectedFiles,
1353
1893
  totalTokens: usedTokens,
@@ -1413,94 +1953,1443 @@ function buildReason(file, level, isTarget, isMustInclude) {
1413
1953
  if (impact === "medium") return `Medium relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
1414
1954
  return `Low relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
1415
1955
  }
1956
+ var init_selector = __esm({
1957
+ "src/engine/selector.ts"() {
1958
+ "use strict";
1959
+ init_secrets();
1960
+ init_pruner();
1961
+ init_coverage();
1962
+ init_budget();
1963
+ init_graph_utils();
1964
+ }
1965
+ });
1416
1966
 
1417
- // src/engine/score.ts
1418
- async function computeContextScore(analysis, task = "general code review and refactoring", budget = 5e4) {
1419
- const selection = await selectContext({ task, analysis, budget });
1420
- const insights = [];
1421
- const efficiency = scoreEfficiency(analysis, selection, insights);
1422
- const coverage = scoreCoverage(analysis, selection, insights);
1423
- const riskControl = scoreRiskControl(analysis, selection, insights);
1424
- const structure = scoreStructure(analysis, insights);
1425
- const governance = scoreGovernance(analysis, insights);
1426
- const overall = Math.round(
1427
- efficiency.weighted + coverage.weighted + riskControl.weighted + structure.weighted + governance.weighted
1428
- );
1429
- const grade = scoreToGrade(overall);
1430
- const naiveTokens = analysis.totalTokens;
1431
- const optimizedTokens = selection.totalTokens;
1432
- const savedTokens = naiveTokens - optimizedTokens;
1433
- const savedPercent = naiveTokens > 0 ? Math.round(savedTokens / naiveTokens * 100) : 0;
1434
- const interactionsPerMonth = 40 * 20;
1435
- const costPerMToken = 3;
1436
- const naiveMonthlyCost = naiveTokens / 1e6 * costPerMToken * interactionsPerMonth;
1437
- const optimizedMonthlyCost = optimizedTokens / 1e6 * costPerMToken * interactionsPerMonth;
1438
- const monthlySavingsUSD = Math.round((naiveMonthlyCost - optimizedMonthlyCost) * 100) / 100;
1967
+ // src/gateway/types.ts
1968
+ var types_exports = {};
1969
+ __export(types_exports, {
1970
+ DEFAULT_ALLOWED_DOMAINS: () => DEFAULT_ALLOWED_DOMAINS,
1971
+ DEFAULT_GATEWAY_CONFIG: () => DEFAULT_GATEWAY_CONFIG,
1972
+ isAllowedTarget: () => isAllowedTarget,
1973
+ isPrivateIP: () => isPrivateIP
1974
+ });
1975
+ function isPrivateIP(ip) {
1976
+ return PRIVATE_IP_PATTERNS.some((p) => p.test(ip));
1977
+ }
1978
+ function isAllowedTarget(hostname, config) {
1979
+ if (config.allowedTargetDomains.length > 0) {
1980
+ return config.allowedTargetDomains.some(
1981
+ (d) => hostname === d || hostname.endsWith("." + d)
1982
+ );
1983
+ }
1984
+ if (DEFAULT_ALLOWED_DOMAINS.has(hostname)) return true;
1985
+ if (hostname.endsWith(".openai.azure.com")) return true;
1986
+ return false;
1987
+ }
1988
+ var DEFAULT_GATEWAY_CONFIG, DEFAULT_ALLOWED_DOMAINS, PRIVATE_IP_PATTERNS;
1989
+ var init_types = __esm({
1990
+ "src/gateway/types.ts"() {
1991
+ "use strict";
1992
+ DEFAULT_GATEWAY_CONFIG = {
1993
+ port: 8787,
1994
+ host: "127.0.0.1",
1995
+ optimize: true,
1996
+ projectPath: ".",
1997
+ budget: 5e4,
1998
+ redactSecrets: true,
1999
+ blockOnSecrets: false,
2000
+ apiKey: "",
2001
+ allowedTargetDomains: [],
2002
+ // Empty = default LLM provider allowlist
2003
+ maxBodyBytes: 10 * 1024 * 1024,
2004
+ // 10MB
2005
+ upstreamTimeoutMs: 12e4,
2006
+ // 2 minutes (streaming can be slow)
2007
+ costTracking: true,
2008
+ budgetDaily: 0,
2009
+ budgetMonthly: 0,
2010
+ alertThreshold: 0.8,
2011
+ auditLog: true,
2012
+ logDir: ".cto/gateway",
2013
+ dashboard: true,
2014
+ dashboardPath: "/__cto"
2015
+ };
2016
+ DEFAULT_ALLOWED_DOMAINS = /* @__PURE__ */ new Set([
2017
+ "api.openai.com",
2018
+ "api.anthropic.com",
2019
+ "generativelanguage.googleapis.com",
2020
+ "aiplatform.googleapis.com"
2021
+ // Azure uses custom subdomains: *.openai.azure.com
2022
+ ]);
2023
+ PRIVATE_IP_PATTERNS = [
2024
+ /^127\./,
2025
+ // Loopback
2026
+ /^10\./,
2027
+ // Class A private
2028
+ /^172\.(1[6-9]|2\d|3[01])\./,
2029
+ // Class B private
2030
+ /^192\.168\./,
2031
+ // Class C private
2032
+ /^169\.254\./,
2033
+ // Link-local (AWS metadata!)
2034
+ /^0\./,
2035
+ // Current network
2036
+ /^::1$/,
2037
+ // IPv6 loopback
2038
+ /^f[cd]/i,
2039
+ // IPv6 private
2040
+ /^fe80:/i
2041
+ // IPv6 link-local
2042
+ ];
2043
+ }
2044
+ });
2045
+
2046
+ // src/gateway/providers.ts
2047
+ function parseOpenAIRequest(body) {
2048
+ const messages = (body.messages || []).map((m) => ({
2049
+ role: m.role || "user",
2050
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2051
+ }));
1439
2052
  return {
1440
- overall,
1441
- grade,
1442
- dimensions: {
1443
- efficiency,
1444
- coverage,
1445
- riskControl,
1446
- structure,
1447
- governance
1448
- },
1449
- insights: insights.sort((a, b) => {
1450
- const order = { high: 0, medium: 1, low: 2 };
1451
- return order[a.impact] - order[b.impact];
1452
- }),
1453
- comparison: {
1454
- naiveTokens,
1455
- optimizedTokens,
1456
- savedTokens,
1457
- savedPercent,
1458
- monthlySavingsUSD
1459
- },
1460
- meta: {
1461
- projectName: analysis.projectName,
1462
- totalFiles: analysis.totalFiles,
1463
- totalTokens: analysis.totalTokens,
1464
- analyzedAt: analysis.analyzedAt
1465
- }
2053
+ model: body.model || "unknown",
2054
+ messages,
2055
+ stream: body.stream === true,
2056
+ maxTokens: body.max_tokens ?? body.max_completion_tokens,
2057
+ temperature: body.temperature
1466
2058
  };
1467
2059
  }
1468
- function scoreEfficiency(analysis, selection, insights) {
1469
- const weight = 30;
1470
- const ratio = analysis.totalTokens > 0 ? 1 - selection.totalTokens / analysis.totalTokens : 0;
1471
- const selectivity = analysis.totalFiles > 0 ? 1 - selection.files.length / analysis.totalFiles : 0;
1472
- const prunedFiles = selection.files.filter(
1473
- (f) => f.pruneLevel === "signatures" || f.pruneLevel === "skeleton"
1474
- ).length;
1475
- const pruneRatio = selection.files.length > 0 ? prunedFiles / selection.files.length : 0;
1476
- const raw = (ratio * 0.5 + selectivity * 0.3 + pruneRatio * 0.2) * 100;
1477
- const score = Math.min(100, Math.max(0, Math.round(raw)));
1478
- const weighted = score / 100 * weight;
1479
- if (ratio > 0.7) {
1480
- insights.push({
1481
- type: "strength",
1482
- title: "Excellent compression",
1483
- detail: `${Math.round(ratio * 100)}% token reduction while maintaining context quality`,
1484
- impact: "high"
1485
- });
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
+ };
1486
2069
  }
1487
- if (ratio < 0.3 && analysis.totalTokens > 2e4) {
1488
- insights.push({
1489
- type: "weakness",
1490
- title: "Low compression opportunity",
1491
- detail: "Most files are needed. Consider splitting the project into smaller modules.",
1492
- impact: "medium"
2070
+ return {
2071
+ 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"
2076
+ };
2077
+ }
2078
+ function parseAnthropicRequest(body) {
2079
+ const messages = [];
2080
+ if (body.system) {
2081
+ messages.push({ role: "system", content: body.system });
2082
+ }
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") || ""
1493
2087
  });
1494
2088
  }
1495
2089
  return {
1496
- score,
1497
- weight,
1498
- weighted,
1499
- detail: `${Math.round(ratio * 100)}% compression, ${prunedFiles}/${selection.files.length} files pruned`
2090
+ model: body.model || "unknown",
2091
+ messages,
2092
+ stream: body.stream === true,
2093
+ maxTokens: body.max_tokens,
2094
+ temperature: body.temperature
1500
2095
  };
1501
2096
  }
1502
- function scoreCoverage(analysis, selection, insights) {
1503
- const weight = 25;
2097
+ function parseAnthropicResponse(body, _streaming) {
2098
+ return {
2099
+ 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") || "",
2103
+ finishReason: body.stop_reason || "end_turn"
2104
+ };
2105
+ }
2106
+ function parseGoogleRequest(body) {
2107
+ const messages = [];
2108
+ if (body.systemInstruction?.parts) {
2109
+ messages.push({
2110
+ role: "system",
2111
+ content: body.systemInstruction.parts.map((p) => p.text || "").join("\n")
2112
+ });
2113
+ }
2114
+ for (const item of body.contents || []) {
2115
+ const role = item.role === "model" ? "assistant" : "user";
2116
+ const content = item.parts?.map((p) => p.text || "").join("\n") || "";
2117
+ messages.push({ role, content });
2118
+ }
2119
+ const model = body.model || body.modelId || "gemini-2.0-flash";
2120
+ return {
2121
+ model,
2122
+ messages,
2123
+ stream: body.stream === true,
2124
+ maxTokens: body.generationConfig?.maxOutputTokens,
2125
+ temperature: body.generationConfig?.temperature
2126
+ };
2127
+ }
2128
+ function parseGoogleResponse(body, _streaming) {
2129
+ const candidate = body.candidates?.[0];
2130
+ return {
2131
+ 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") || "",
2135
+ finishReason: candidate?.finishReason || "STOP"
2136
+ };
2137
+ }
2138
+ function detectProvider(url, headers) {
2139
+ for (const provider of Object.values(PROVIDERS)) {
2140
+ if (provider.name === "custom") continue;
2141
+ if (provider.detectProvider(url, headers)) return provider;
2142
+ }
2143
+ return PROVIDERS.custom;
2144
+ }
2145
+ function getModelConfig(provider, modelId) {
2146
+ const exact = provider.models.find((m) => m.id === modelId);
2147
+ if (exact) return exact;
2148
+ return provider.models.find((m) => modelId.startsWith(m.id) || m.id.startsWith(modelId));
2149
+ }
2150
+ function estimateCost(provider, modelId, inputTokens, outputTokens) {
2151
+ const model = getModelConfig(provider, modelId);
2152
+ if (!model) return 0;
2153
+ const inputCost = inputTokens / 1e6 * model.costPerMInput;
2154
+ const outputCost = outputTokens / 1e6 * model.costPerMOutput;
2155
+ return Math.round((inputCost + outputCost) * 1e6) / 1e6;
2156
+ }
2157
+ var OPENAI_MODELS, ANTHROPIC_MODELS, GOOGLE_MODELS, PROVIDERS;
2158
+ var init_providers = __esm({
2159
+ "src/gateway/providers.ts"() {
2160
+ "use strict";
2161
+ OPENAI_MODELS = [
2162
+ { id: "gpt-4o", contextWindow: 128e3, costPerMInput: 2.5, costPerMOutput: 10, maxOutput: 16384 },
2163
+ { id: "gpt-4o-mini", contextWindow: 128e3, costPerMInput: 0.15, costPerMOutput: 0.6, maxOutput: 16384 },
2164
+ { id: "gpt-4-turbo", contextWindow: 128e3, costPerMInput: 10, costPerMOutput: 30, maxOutput: 4096 },
2165
+ { id: "gpt-3.5-turbo", contextWindow: 16385, costPerMInput: 0.5, costPerMOutput: 1.5, maxOutput: 4096 },
2166
+ { id: "o1", contextWindow: 2e5, costPerMInput: 15, costPerMOutput: 60, maxOutput: 1e5 },
2167
+ { id: "o1-mini", contextWindow: 128e3, costPerMInput: 3, costPerMOutput: 12, maxOutput: 65536 },
2168
+ { id: "o3-mini", contextWindow: 2e5, costPerMInput: 1.1, costPerMOutput: 4.4, maxOutput: 1e5 }
2169
+ ];
2170
+ ANTHROPIC_MODELS = [
2171
+ { id: "claude-sonnet-4-20250514", contextWindow: 2e5, costPerMInput: 3, costPerMOutput: 15, maxOutput: 64e3 },
2172
+ { id: "claude-3-5-haiku-20241022", contextWindow: 2e5, costPerMInput: 0.8, costPerMOutput: 4, maxOutput: 8192 },
2173
+ { id: "claude-3-opus-20240229", contextWindow: 2e5, costPerMInput: 15, costPerMOutput: 75, maxOutput: 4096 }
2174
+ ];
2175
+ GOOGLE_MODELS = [
2176
+ { id: "gemini-2.5-pro", contextWindow: 1e6, costPerMInput: 1.25, costPerMOutput: 10, maxOutput: 65536 },
2177
+ { id: "gemini-2.0-flash", contextWindow: 1e6, costPerMInput: 0.1, costPerMOutput: 0.4, maxOutput: 8192 },
2178
+ { id: "gemini-1.5-pro", contextWindow: 2e6, costPerMInput: 1.25, costPerMOutput: 5, maxOutput: 8192 }
2179
+ ];
2180
+ PROVIDERS = {
2181
+ openai: {
2182
+ name: "openai",
2183
+ displayName: "OpenAI",
2184
+ baseUrl: "https://api.openai.com",
2185
+ authHeader: "Authorization",
2186
+ chatPath: "/v1/chat/completions",
2187
+ models: OPENAI_MODELS,
2188
+ parseRequest: parseOpenAIRequest,
2189
+ parseResponse: parseOpenAIResponse,
2190
+ detectProvider: (url, _headers) => url.includes("api.openai.com") || url.includes("/v1/chat/completions")
2191
+ },
2192
+ anthropic: {
2193
+ name: "anthropic",
2194
+ displayName: "Anthropic",
2195
+ baseUrl: "https://api.anthropic.com",
2196
+ authHeader: "x-api-key",
2197
+ chatPath: "/v1/messages",
2198
+ models: ANTHROPIC_MODELS,
2199
+ parseRequest: parseAnthropicRequest,
2200
+ parseResponse: parseAnthropicResponse,
2201
+ detectProvider: (url, headers) => url.includes("api.anthropic.com") || url.includes("/v1/messages") || !!headers["x-api-key"] || !!headers["anthropic-version"]
2202
+ },
2203
+ google: {
2204
+ name: "google",
2205
+ displayName: "Google AI",
2206
+ baseUrl: "https://generativelanguage.googleapis.com",
2207
+ authHeader: "x-goog-api-key",
2208
+ chatPath: "/v1beta/models",
2209
+ models: GOOGLE_MODELS,
2210
+ parseRequest: parseGoogleRequest,
2211
+ parseResponse: parseGoogleResponse,
2212
+ detectProvider: (url, _headers) => url.includes("generativelanguage.googleapis.com") || url.includes("aiplatform.googleapis.com")
2213
+ },
2214
+ "azure-openai": {
2215
+ name: "azure-openai",
2216
+ displayName: "Azure OpenAI",
2217
+ baseUrl: "",
2218
+ authHeader: "api-key",
2219
+ chatPath: "/openai/deployments",
2220
+ models: OPENAI_MODELS,
2221
+ // Same models, different hosting
2222
+ parseRequest: parseOpenAIRequest,
2223
+ parseResponse: parseOpenAIResponse,
2224
+ detectProvider: (url, headers) => url.includes(".openai.azure.com") || !!headers["api-key"]
2225
+ },
2226
+ custom: {
2227
+ name: "custom",
2228
+ displayName: "Custom (OpenAI-compatible)",
2229
+ baseUrl: "",
2230
+ authHeader: "Authorization",
2231
+ chatPath: "/v1/chat/completions",
2232
+ models: [],
2233
+ parseRequest: parseOpenAIRequest,
2234
+ parseResponse: parseOpenAIResponse,
2235
+ detectProvider: () => false
2236
+ // Fallback only
2237
+ }
2238
+ };
2239
+ }
2240
+ });
2241
+
2242
+ // src/gateway/interceptor.ts
2243
+ import { readFileSync as readFileSync2 } from "fs";
2244
+ import { resolve as resolve6 } from "path";
2245
+ function estimateTokensFromString(s) {
2246
+ return Math.ceil(Buffer.byteLength(s, "utf-8") / 4);
2247
+ }
2248
+ async function interceptRequest(messages, config, analysis) {
2249
+ const decisions = [];
2250
+ let secretsRedacted = 0;
2251
+ let secretsBlocked = false;
2252
+ let contextInjected = false;
2253
+ const originalTokens = messages.reduce((sum, m) => sum + estimateTokensFromString(m.content), 0);
2254
+ let processedMessages = messages;
2255
+ if (config.redactSecrets || config.blockOnSecrets) {
2256
+ const { messages: scannedMessages, redactedCount, blocked, scanDecisions } = scanMessages(messages, config);
2257
+ processedMessages = scannedMessages;
2258
+ secretsRedacted = redactedCount;
2259
+ secretsBlocked = blocked;
2260
+ decisions.push(...scanDecisions);
2261
+ if (blocked) {
2262
+ return {
2263
+ modified: true,
2264
+ messages: processedMessages,
2265
+ originalTokens,
2266
+ optimizedTokens: 0,
2267
+ secretsRedacted,
2268
+ secretsBlocked: true,
2269
+ contextInjected: false,
2270
+ decisions
2271
+ };
2272
+ }
2273
+ }
2274
+ if (config.optimize && analysis) {
2275
+ const { messages: optimizedMessages, injected, optimizeDecisions } = await optimizeContext(processedMessages, analysis, config);
2276
+ processedMessages = optimizedMessages;
2277
+ contextInjected = injected;
2278
+ decisions.push(...optimizeDecisions);
2279
+ }
2280
+ const optimizedTokens = processedMessages.reduce((sum, m) => sum + estimateTokensFromString(m.content), 0);
2281
+ return {
2282
+ modified: secretsRedacted > 0 || contextInjected,
2283
+ messages: processedMessages,
2284
+ originalTokens,
2285
+ optimizedTokens,
2286
+ secretsRedacted,
2287
+ secretsBlocked,
2288
+ contextInjected,
2289
+ decisions
2290
+ };
2291
+ }
2292
+ function scanMessages(messages, config) {
2293
+ const scanDecisions = [];
2294
+ let redactedCount = 0;
2295
+ let blocked = false;
2296
+ const scannedMessages = messages.map((msg) => {
2297
+ const findings = scanContentForSecrets(msg.content, `message:${msg.role}`);
2298
+ if (findings.length === 0) return msg;
2299
+ const criticalCount = findings.filter((f) => f.severity === "critical").length;
2300
+ if (config.blockOnSecrets && criticalCount > 0) {
2301
+ blocked = true;
2302
+ scanDecisions.push(
2303
+ `BLOCKED: ${criticalCount} critical secret(s) in ${msg.role} message. Types: ${[...new Set(findings.map((f) => f.type))].join(", ")}`
2304
+ );
2305
+ return msg;
2306
+ }
2307
+ const sanitized = sanitizeContent(msg.content);
2308
+ redactedCount += findings.length;
2309
+ scanDecisions.push(
2310
+ `Redacted ${findings.length} secret(s) in ${msg.role} message: ${[...new Set(findings.map((f) => f.type))].join(", ")}`
2311
+ );
2312
+ return { ...msg, content: sanitized };
2313
+ });
2314
+ return { messages: scannedMessages, redactedCount, blocked, scanDecisions };
2315
+ }
2316
+ async function optimizeContext(messages, analysis, config) {
2317
+ const optimizeDecisions = [];
2318
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
2319
+ if (!lastUserMsg) {
2320
+ optimizeDecisions.push("No user message found \u2014 skipping optimization");
2321
+ return { messages, injected: false, optimizeDecisions };
2322
+ }
2323
+ const hasCtxContext = messages.some(
2324
+ (m) => m.role === "system" && m.content.includes("[CTO Context]")
2325
+ );
2326
+ if (hasCtxContext) {
2327
+ optimizeDecisions.push("CTO context already present \u2014 skipping injection");
2328
+ return { messages, injected: false, optimizeDecisions };
2329
+ }
2330
+ try {
2331
+ const selection = await selectContext({
2332
+ task: lastUserMsg.content.slice(0, 500),
2333
+ analysis,
2334
+ budget: config.budget
2335
+ });
2336
+ if (selection.files.length === 0) {
2337
+ optimizeDecisions.push("No relevant files found for task \u2014 skipping injection");
2338
+ return { messages, injected: false, optimizeDecisions };
2339
+ }
2340
+ const contentBudget = Math.floor(config.budget * 0.6);
2341
+ let usedTokens = 0;
2342
+ const contextLines = [
2343
+ "[CTO Context] Optimized project context (auto-injected by CTO Gateway)",
2344
+ "",
2345
+ `Project: ${analysis.projectName} (${analysis.totalFiles} files, ${Math.round(analysis.totalTokens / 1e3)}K tokens)`,
2346
+ `Selected: ${selection.files.length} files, ${selection.totalTokens.toLocaleString()} tokens (${selection.coverage.score}% coverage)`,
2347
+ ""
2348
+ ];
2349
+ const topFiles = selection.files.sort((a, b) => b.riskScore - a.riskScore);
2350
+ const injectedFiles = [];
2351
+ const skippedFiles = [];
2352
+ for (const f of topFiles) {
2353
+ if (usedTokens >= contentBudget) {
2354
+ skippedFiles.push(f.relativePath);
2355
+ continue;
2356
+ }
2357
+ try {
2358
+ const fullPath = resolve6(config.projectPath, f.relativePath);
2359
+ const content = readFileSync2(fullPath, "utf-8");
2360
+ const fileTokens = estimateTokensFromString(content);
2361
+ const remainingBudget = contentBudget - usedTokens;
2362
+ let fileContent;
2363
+ let truncated = false;
2364
+ if (fileTokens > remainingBudget) {
2365
+ const charLimit = remainingBudget * 4;
2366
+ fileContent = content.slice(0, charLimit);
2367
+ truncated = true;
2368
+ } else {
2369
+ fileContent = content;
2370
+ }
2371
+ const ext = f.relativePath.split(".").pop() || "";
2372
+ contextLines.push(`### ${f.relativePath}${truncated ? " [truncated]" : ""}`);
2373
+ contextLines.push("```" + ext);
2374
+ contextLines.push(fileContent);
2375
+ contextLines.push("```");
2376
+ contextLines.push("");
2377
+ usedTokens += estimateTokensFromString(fileContent);
2378
+ injectedFiles.push(f.relativePath);
2379
+ } catch {
2380
+ skippedFiles.push(f.relativePath);
2381
+ }
2382
+ }
2383
+ const typeFiles = analysis.files.filter((f) => f.kind === "type").map((f) => f.relativePath);
2384
+ if (typeFiles.length > 0) {
2385
+ contextLines.push("Type definitions (always import from these):");
2386
+ for (const tf of typeFiles.slice(0, 10)) {
2387
+ contextLines.push(` - ${tf}`);
2388
+ }
2389
+ contextLines.push("");
2390
+ }
2391
+ if (analysis.graph.hubs.length > 0) {
2392
+ contextLines.push("Hub files (central modules with many dependents):");
2393
+ for (const hub of analysis.graph.hubs.slice(0, 5)) {
2394
+ contextLines.push(` - ${hub.relativePath} (${hub.dependents} dependents)`);
2395
+ }
2396
+ contextLines.push("");
2397
+ }
2398
+ if (skippedFiles.length > 0) {
2399
+ contextLines.push(`Additional relevant files (not included due to token budget):`);
2400
+ for (const sf of skippedFiles.slice(0, 15)) {
2401
+ contextLines.push(` - ${sf}`);
2402
+ }
2403
+ if (skippedFiles.length > 15) {
2404
+ contextLines.push(` ... and ${skippedFiles.length - 15} more`);
2405
+ }
2406
+ }
2407
+ const contextBlock = contextLines.join("\n");
2408
+ const systemMsg = {
2409
+ role: "system",
2410
+ content: contextBlock
2411
+ };
2412
+ const existingSystemIdx = messages.findIndex((m) => m.role === "system");
2413
+ let optimizedMessages;
2414
+ if (existingSystemIdx >= 0) {
2415
+ optimizedMessages = [...messages];
2416
+ optimizedMessages[existingSystemIdx] = {
2417
+ ...optimizedMessages[existingSystemIdx],
2418
+ content: optimizedMessages[existingSystemIdx].content + "\n\n" + contextBlock
2419
+ };
2420
+ } else {
2421
+ optimizedMessages = [systemMsg, ...messages];
2422
+ }
2423
+ optimizeDecisions.push(
2424
+ `Injected CTO context: ${injectedFiles.length} files with contents (${usedTokens.toLocaleString()} tokens), ${skippedFiles.length} listed without contents, ${selection.coverage.score}% coverage`
2425
+ );
2426
+ return { messages: optimizedMessages, injected: true, optimizeDecisions };
2427
+ } catch (err) {
2428
+ optimizeDecisions.push(`Context optimization failed: ${err.message}`);
2429
+ return { messages, injected: false, optimizeDecisions };
2430
+ }
2431
+ }
2432
+ var init_interceptor = __esm({
2433
+ "src/gateway/interceptor.ts"() {
2434
+ "use strict";
2435
+ init_secrets();
2436
+ init_selector();
2437
+ }
2438
+ });
2439
+
2440
+ // 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";
2443
+ import { randomUUID } from "crypto";
2444
+ var UsageTracker;
2445
+ var init_tracker = __esm({
2446
+ "src/gateway/tracker.ts"() {
2447
+ "use strict";
2448
+ UsageTracker = class {
2449
+ logDir;
2450
+ config;
2451
+ eventHandlers = [];
2452
+ cache = null;
2453
+ cacheMonth = null;
2454
+ // In-memory cost accumulators — survive async disk writes
2455
+ memRecords = [];
2456
+ constructor(config) {
2457
+ this.config = config;
2458
+ this.logDir = join7(config.logDir, "usage");
2459
+ mkdirSync2(this.logDir, { recursive: true });
2460
+ }
2461
+ // ===== EVENT SYSTEM =====
2462
+ onEvent(handler) {
2463
+ this.eventHandlers.push(handler);
2464
+ }
2465
+ emit(event) {
2466
+ for (const handler of this.eventHandlers) {
2467
+ try {
2468
+ handler(event);
2469
+ } catch {
2470
+ }
2471
+ }
2472
+ }
2473
+ // ===== RECORD =====
2474
+ record(params) {
2475
+ const record = {
2476
+ id: randomUUID().slice(0, 8),
2477
+ timestamp: /* @__PURE__ */ new Date(),
2478
+ ...params
2479
+ };
2480
+ const monthKey = this.getMonthKey(record.timestamp);
2481
+ const logFile = join7(this.logDir, `${monthKey}.jsonl`);
2482
+ const line = JSON.stringify({
2483
+ ...record,
2484
+ timestamp: record.timestamp.toISOString()
2485
+ });
2486
+ appendFileSync(logFile, line + "\n");
2487
+ this.memRecords.push(record);
2488
+ this.cache = null;
2489
+ this.emit({ type: "request", record });
2490
+ this.checkBudget(record.timestamp);
2491
+ return record;
2492
+ }
2493
+ // ===== BUDGET CHECKS =====
2494
+ checkBudget(now) {
2495
+ if (this.config.budgetDaily > 0) {
2496
+ const dailyCost = this.getDailyCost(now);
2497
+ const threshold = this.config.budgetDaily * this.config.alertThreshold;
2498
+ if (dailyCost >= this.config.budgetDaily) {
2499
+ this.emit({
2500
+ type: "budget-exceeded",
2501
+ current: dailyCost,
2502
+ limit: this.config.budgetDaily,
2503
+ period: "daily"
2504
+ });
2505
+ } else if (dailyCost >= threshold) {
2506
+ this.emit({
2507
+ type: "budget-alert",
2508
+ current: dailyCost,
2509
+ limit: this.config.budgetDaily,
2510
+ period: "daily"
2511
+ });
2512
+ }
2513
+ }
2514
+ if (this.config.budgetMonthly > 0) {
2515
+ const monthlyCost = this.getMonthlyCost(now);
2516
+ const threshold = this.config.budgetMonthly * this.config.alertThreshold;
2517
+ if (monthlyCost >= this.config.budgetMonthly) {
2518
+ this.emit({
2519
+ type: "budget-exceeded",
2520
+ current: monthlyCost,
2521
+ limit: this.config.budgetMonthly,
2522
+ period: "monthly"
2523
+ });
2524
+ } else if (monthlyCost >= threshold) {
2525
+ this.emit({
2526
+ type: "budget-alert",
2527
+ current: monthlyCost,
2528
+ limit: this.config.budgetMonthly,
2529
+ period: "monthly"
2530
+ });
2531
+ }
2532
+ }
2533
+ }
2534
+ isDailyBudgetExceeded(now = /* @__PURE__ */ new Date()) {
2535
+ if (this.config.budgetDaily <= 0) return false;
2536
+ return this.getDailyCost(now) >= this.config.budgetDaily;
2537
+ }
2538
+ isMonthlyBudgetExceeded(now = /* @__PURE__ */ new Date()) {
2539
+ if (this.config.budgetMonthly <= 0) return false;
2540
+ return this.getMonthlyCost(now) >= this.config.budgetMonthly;
2541
+ }
2542
+ // ===== QUERIES =====
2543
+ getDailyCost(date = /* @__PURE__ */ new Date()) {
2544
+ const dayStr = date.toISOString().split("T")[0];
2545
+ const diskRecords = this.getMonthRecords(date);
2546
+ const allRecords = this.mergeWithMemRecords(diskRecords);
2547
+ return allRecords.filter((r) => r.timestamp.toISOString().startsWith(dayStr)).reduce((sum, r) => sum + r.costUSD, 0);
2548
+ }
2549
+ getMonthlyCost(date = /* @__PURE__ */ new Date()) {
2550
+ const diskRecords = this.getMonthRecords(date);
2551
+ const allRecords = this.mergeWithMemRecords(diskRecords);
2552
+ return allRecords.reduce((sum, r) => sum + r.costUSD, 0);
2553
+ }
2554
+ mergeWithMemRecords(diskRecords) {
2555
+ if (this.memRecords.length === 0) return diskRecords;
2556
+ const diskIds = new Set(diskRecords.map((r) => r.id));
2557
+ const newRecords = this.memRecords.filter((r) => !diskIds.has(r.id));
2558
+ return [...diskRecords, ...newRecords];
2559
+ }
2560
+ getSummary(period = "month") {
2561
+ const now = /* @__PURE__ */ new Date();
2562
+ let records;
2563
+ switch (period) {
2564
+ case "day": {
2565
+ const dayStr = now.toISOString().split("T")[0];
2566
+ records = this.mergeWithMemRecords(this.getMonthRecords(now)).filter(
2567
+ (r) => r.timestamp.toISOString().startsWith(dayStr)
2568
+ );
2569
+ break;
2570
+ }
2571
+ case "week": {
2572
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
2573
+ const weekAgoKey = this.getMonthKey(weekAgo);
2574
+ const nowKey = this.getMonthKey(now);
2575
+ const baseRecs = weekAgoKey !== nowKey ? [...this.getMonthRecordsByKey(weekAgoKey), ...this.getMonthRecords(now)] : this.getMonthRecords(now);
2576
+ records = this.mergeWithMemRecords(baseRecs).filter((r) => r.timestamp >= weekAgo);
2577
+ break;
2578
+ }
2579
+ case "month":
2580
+ records = this.mergeWithMemRecords(this.getMonthRecords(now));
2581
+ break;
2582
+ case "all":
2583
+ records = this.mergeWithMemRecords(this.getAllRecords());
2584
+ break;
2585
+ }
2586
+ const byModel = {};
2587
+ const byProvider = {};
2588
+ for (const r of records) {
2589
+ if (!byModel[r.model]) byModel[r.model] = { requests: 0, costUSD: 0, tokens: 0 };
2590
+ byModel[r.model].requests++;
2591
+ byModel[r.model].costUSD += r.costUSD;
2592
+ byModel[r.model].tokens += r.inputTokens + r.outputTokens;
2593
+ if (!byProvider[r.provider]) byProvider[r.provider] = { requests: 0, costUSD: 0 };
2594
+ byProvider[r.provider].requests++;
2595
+ byProvider[r.provider].costUSD += r.costUSD;
2596
+ }
2597
+ return {
2598
+ period,
2599
+ totalRequests: records.length,
2600
+ totalInputTokens: records.reduce((s, r) => s + r.inputTokens, 0),
2601
+ totalOutputTokens: records.reduce((s, r) => s + r.outputTokens, 0),
2602
+ totalCostUSD: records.reduce((s, r) => s + r.costUSD, 0),
2603
+ totalSavedTokens: records.reduce((s, r) => s + r.savedTokens, 0),
2604
+ totalSavedUSD: records.reduce((s, r) => s + r.savedUSD, 0),
2605
+ totalSecretsRedacted: records.reduce((s, r) => s + r.secretsRedacted, 0),
2606
+ byModel,
2607
+ byProvider
2608
+ };
2609
+ }
2610
+ // ===== STORAGE =====
2611
+ getMonthKey(date) {
2612
+ return date.toISOString().slice(0, 7);
2613
+ }
2614
+ 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) => {
2618
+ try {
2619
+ const parsed = JSON.parse(line);
2620
+ parsed.timestamp = new Date(parsed.timestamp);
2621
+ return parsed;
2622
+ } catch {
2623
+ return null;
2624
+ }
2625
+ }).filter((r) => r !== null);
2626
+ }
2627
+ getMonthRecords(date) {
2628
+ const monthKey = this.getMonthKey(date);
2629
+ 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) => {
2633
+ try {
2634
+ const parsed = JSON.parse(line);
2635
+ parsed.timestamp = new Date(parsed.timestamp);
2636
+ return parsed;
2637
+ } catch {
2638
+ return null;
2639
+ }
2640
+ }).filter((r) => r !== null);
2641
+ this.cache = records;
2642
+ this.cacheMonth = monthKey;
2643
+ return records;
2644
+ }
2645
+ getAllRecords() {
2646
+ if (!existsSync6(this.logDir)) return [];
2647
+ const files = readdirSync(this.logDir).filter((f) => f.endsWith(".jsonl")).sort();
2648
+ const allRecords = [];
2649
+ for (const file of files) {
2650
+ const content = readFileSync3(join7(this.logDir, file), "utf-8");
2651
+ const records = content.split("\n").filter((line) => line.trim()).map((line) => {
2652
+ try {
2653
+ const parsed = JSON.parse(line);
2654
+ parsed.timestamp = new Date(parsed.timestamp);
2655
+ return parsed;
2656
+ } catch {
2657
+ return null;
2658
+ }
2659
+ }).filter((r) => r !== null);
2660
+ allRecords.push(...records);
2661
+ }
2662
+ return allRecords;
2663
+ }
2664
+ };
2665
+ }
2666
+ });
2667
+
2668
+ // src/gateway/server.ts
2669
+ var server_exports = {};
2670
+ __export(server_exports, {
2671
+ ContextGateway: () => ContextGateway
2672
+ });
2673
+ import { createServer, Agent as HttpAgent } from "http";
2674
+ import { request as httpsRequest, Agent as HttpsAgent } from "https";
2675
+ import { request as httpRequest } from "http";
2676
+ import { URL } from "url";
2677
+ import { lookup } from "dns/promises";
2678
+ function readBody(req, maxBytes = 0) {
2679
+ return new Promise((resolve8, reject) => {
2680
+ const chunks = [];
2681
+ let totalBytes = 0;
2682
+ req.on("data", (chunk) => {
2683
+ totalBytes += chunk.length;
2684
+ if (maxBytes > 0 && totalBytes > maxBytes) {
2685
+ req.destroy();
2686
+ reject(new Error("body-too-large"));
2687
+ return;
2688
+ }
2689
+ chunks.push(chunk);
2690
+ });
2691
+ req.on("end", () => resolve8(Buffer.concat(chunks).toString()));
2692
+ req.on("error", reject);
2693
+ });
2694
+ }
2695
+ function flattenHeaders(headers) {
2696
+ const flat = {};
2697
+ for (const [key, value] of Object.entries(headers)) {
2698
+ if (value) flat[key] = Array.isArray(value) ? value[0] : value;
2699
+ }
2700
+ return flat;
2701
+ }
2702
+ function rebuildRequestBody(original, messages, provider) {
2703
+ const body = { ...original };
2704
+ if (provider === "anthropic") {
2705
+ const systemMsg = messages.find((m) => m.role === "system");
2706
+ const otherMsgs = messages.filter((m) => m.role !== "system");
2707
+ if (systemMsg) body.system = systemMsg.content;
2708
+ body.messages = otherMsgs;
2709
+ } else if (provider === "google") {
2710
+ const systemMsg = messages.find((m) => m.role === "system");
2711
+ const otherMsgs = messages.filter((m) => m.role !== "system");
2712
+ if (systemMsg) {
2713
+ body.systemInstruction = { parts: [{ text: systemMsg.content }] };
2714
+ }
2715
+ body.contents = otherMsgs.map((m) => ({
2716
+ role: m.role === "assistant" ? "model" : "user",
2717
+ parts: [{ text: m.content }]
2718
+ }));
2719
+ } else {
2720
+ body.messages = messages;
2721
+ }
2722
+ return JSON.stringify(body);
2723
+ }
2724
+ function generateDashboardHTML(monthly, daily, config, analysis) {
2725
+ const modelRows = Object.entries(monthly.byModel).sort(([, a], [, b]) => b.costUSD - a.costUSD).map(
2726
+ ([model, data]) => `<tr><td>${model}</td><td>${data.requests}</td><td>${(data.tokens / 1e3).toFixed(1)}K</td><td>$${data.costUSD.toFixed(4)}</td></tr>`
2727
+ ).join("");
2728
+ const providerRows = Object.entries(monthly.byProvider).sort(([, a], [, b]) => b.costUSD - a.costUSD).map(
2729
+ ([provider, data]) => `<tr><td>${provider}</td><td>${data.requests}</td><td>$${data.costUSD.toFixed(4)}</td></tr>`
2730
+ ).join("");
2731
+ return `<!DOCTYPE html>
2732
+ <html lang="en">
2733
+ <head>
2734
+ <meta charset="utf-8">
2735
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2736
+ <title>CTO Gateway Dashboard</title>
2737
+ <style>
2738
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2739
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0f; color: #e0e0e0; padding: 2rem; }
2740
+ h1 { font-size: 1.8rem; margin-bottom: 0.5rem; color: #fff; }
2741
+ h2 { font-size: 1.2rem; margin: 2rem 0 1rem; color: #8b8bff; }
2742
+ .subtitle { color: #666; margin-bottom: 2rem; }
2743
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
2744
+ .card { background: #14141f; border: 1px solid #2a2a3a; border-radius: 12px; padding: 1.5rem; }
2745
+ .card .label { font-size: 0.75rem; text-transform: uppercase; color: #666; letter-spacing: 0.05em; }
2746
+ .card .value { font-size: 2rem; font-weight: 700; color: #fff; margin-top: 0.25rem; }
2747
+ .card .detail { font-size: 0.85rem; color: #888; margin-top: 0.25rem; }
2748
+ .card.green .value { color: #4ade80; }
2749
+ .card.red .value { color: #f87171; }
2750
+ .card.blue .value { color: #60a5fa; }
2751
+ .card.purple .value { color: #a78bfa; }
2752
+ table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; }
2753
+ th { text-align: left; padding: 0.5rem; color: #666; font-size: 0.75rem; text-transform: uppercase; border-bottom: 1px solid #2a2a3a; }
2754
+ td { padding: 0.5rem; border-bottom: 1px solid #1a1a2a; font-size: 0.9rem; }
2755
+ .status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; }
2756
+ .status.on { background: #4ade80; }
2757
+ .status.off { background: #666; }
2758
+ .footer { margin-top: 3rem; color: #444; font-size: 0.75rem; }
2759
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; }
2760
+ .badge.critical { background: #7f1d1d; color: #fca5a5; }
2761
+ .badge.ok { background: #14532d; color: #86efac; }
2762
+ </style>
2763
+ </head>
2764
+ <body>
2765
+ <h1>\u26A1 CTO Context Gateway</h1>
2766
+ <p class="subtitle">Real-time AI proxy with context optimization, secret redaction, and cost tracking</p>
2767
+
2768
+ <h2>Today</h2>
2769
+ <div class="grid">
2770
+ <div class="card blue">
2771
+ <div class="label">Requests</div>
2772
+ <div class="value">${daily.totalRequests}</div>
2773
+ </div>
2774
+ <div class="card">
2775
+ <div class="label">Cost</div>
2776
+ <div class="value">$${daily.totalCostUSD.toFixed(2)}</div>
2777
+ ${config.budgetDaily > 0 ? `<div class="detail">Budget: $${config.budgetDaily}/day</div>` : ""}
2778
+ </div>
2779
+ <div class="card green">
2780
+ <div class="label">Tokens Saved</div>
2781
+ <div class="value">${(daily.totalSavedTokens / 1e3).toFixed(1)}K</div>
2782
+ <div class="detail">$${daily.totalSavedUSD.toFixed(2)} saved</div>
2783
+ </div>
2784
+ <div class="card ${daily.totalSecretsRedacted > 0 ? "red" : ""}">
2785
+ <div class="label">Secrets Redacted</div>
2786
+ <div class="value">${daily.totalSecretsRedacted}</div>
2787
+ </div>
2788
+ </div>
2789
+
2790
+ <h2>This Month</h2>
2791
+ <div class="grid">
2792
+ <div class="card blue">
2793
+ <div class="label">Total Requests</div>
2794
+ <div class="value">${monthly.totalRequests}</div>
2795
+ </div>
2796
+ <div class="card">
2797
+ <div class="label">Total Cost</div>
2798
+ <div class="value">$${monthly.totalCostUSD.toFixed(2)}</div>
2799
+ ${config.budgetMonthly > 0 ? `<div class="detail">Budget: $${config.budgetMonthly}/month</div>` : ""}
2800
+ </div>
2801
+ <div class="card green">
2802
+ <div class="label">Total Saved</div>
2803
+ <div class="value">$${monthly.totalSavedUSD.toFixed(2)}</div>
2804
+ <div class="detail">${(monthly.totalSavedTokens / 1e3).toFixed(0)}K tokens</div>
2805
+ </div>
2806
+ <div class="card purple">
2807
+ <div class="label">Tokens Processed</div>
2808
+ <div class="value">${((monthly.totalInputTokens + monthly.totalOutputTokens) / 1e3).toFixed(0)}K</div>
2809
+ <div class="detail">${(monthly.totalInputTokens / 1e3).toFixed(0)}K in / ${(monthly.totalOutputTokens / 1e3).toFixed(0)}K out</div>
2810
+ </div>
2811
+ </div>
2812
+
2813
+ <h2>Features</h2>
2814
+ <div class="grid">
2815
+ <div class="card">
2816
+ <div class="label">Context Optimization</div>
2817
+ <div class="value"><span class="status ${config.optimize ? "on" : "off"}"></span>${config.optimize ? "ON" : "OFF"}</div>
2818
+ ${analysis ? `<div class="detail">${analysis.totalFiles} files, ${(analysis.totalTokens / 1e3).toFixed(0)}K tokens</div>` : '<div class="detail">Loading analysis...</div>'}
2819
+ </div>
2820
+ <div class="card">
2821
+ <div class="label">Secret Redaction</div>
2822
+ <div class="value"><span class="status ${config.redactSecrets ? "on" : "off"}"></span>${config.redactSecrets ? "ON" : "OFF"}</div>
2823
+ ${config.blockOnSecrets ? '<span class="badge critical">BLOCKING</span>' : ""}
2824
+ </div>
2825
+ <div class="card">
2826
+ <div class="label">Cost Tracking</div>
2827
+ <div class="value"><span class="status ${config.costTracking ? "on" : "off"}"></span>${config.costTracking ? "ON" : "OFF"}</div>
2828
+ </div>
2829
+ <div class="card">
2830
+ <div class="label">Audit Log</div>
2831
+ <div class="value"><span class="status ${config.auditLog ? "on" : "off"}"></span>${config.auditLog ? "ON" : "OFF"}</div>
2832
+ <div class="detail">${config.logDir}</div>
2833
+ </div>
2834
+ </div>
2835
+
2836
+ ${modelRows ? `
2837
+ <h2>By Model</h2>
2838
+ <div class="card">
2839
+ <table>
2840
+ <thead><tr><th>Model</th><th>Requests</th><th>Tokens</th><th>Cost</th></tr></thead>
2841
+ <tbody>${modelRows}</tbody>
2842
+ </table>
2843
+ </div>` : ""}
2844
+
2845
+ ${providerRows ? `
2846
+ <h2>By Provider</h2>
2847
+ <div class="card">
2848
+ <table>
2849
+ <thead><tr><th>Provider</th><th>Requests</th><th>Cost</th></tr></thead>
2850
+ <tbody>${providerRows}</tbody>
2851
+ </table>
2852
+ </div>` : ""}
2853
+
2854
+ <div class="footer">
2855
+ CTO Context Gateway v4.0.0 \xB7 Listening on ${config.host}:${config.port} \xB7 <a href="/health" style="color:#666">Health</a>
2856
+ </div>
2857
+
2858
+ <script>setTimeout(() => location.reload(), 30000);</script>
2859
+ </body>
2860
+ </html>`;
2861
+ }
2862
+ var ContextGateway;
2863
+ var init_server = __esm({
2864
+ "src/gateway/server.ts"() {
2865
+ "use strict";
2866
+ init_types();
2867
+ init_providers();
2868
+ init_interceptor();
2869
+ init_tracker();
2870
+ init_analyzer();
2871
+ ContextGateway = class {
2872
+ config;
2873
+ tracker;
2874
+ analysis = null;
2875
+ analysisPromise = null;
2876
+ eventHandlers = [];
2877
+ server = null;
2878
+ httpAgent;
2879
+ httpsAgent;
2880
+ budgetLock = false;
2881
+ // Simple lock for budget reservation
2882
+ constructor(config = {}) {
2883
+ this.config = { ...DEFAULT_GATEWAY_CONFIG, ...config };
2884
+ this.tracker = new UsageTracker(this.config);
2885
+ this.httpAgent = new HttpAgent({ keepAlive: true, maxSockets: 50 });
2886
+ this.httpsAgent = new HttpsAgent({ keepAlive: true, maxSockets: 50 });
2887
+ this.tracker.onEvent((event) => this.emit(event));
2888
+ }
2889
+ // ===== EVENTS =====
2890
+ onEvent(handler) {
2891
+ this.eventHandlers.push(handler);
2892
+ }
2893
+ emit(event) {
2894
+ for (const handler of this.eventHandlers) {
2895
+ try {
2896
+ handler(event);
2897
+ } catch {
2898
+ }
2899
+ }
2900
+ }
2901
+ // ===== LIFECYCLE =====
2902
+ async start() {
2903
+ if (this.config.optimize) {
2904
+ this.analysisPromise = this.refreshAnalysis();
2905
+ }
2906
+ this.server = createServer((req, res) => this.handleRequest(req, res));
2907
+ return new Promise((resolve8) => {
2908
+ this.server.listen(this.config.port, this.config.host, () => {
2909
+ resolve8();
2910
+ });
2911
+ });
2912
+ }
2913
+ async stop() {
2914
+ return new Promise((resolve8) => {
2915
+ if (this.server) {
2916
+ this.server.close(() => resolve8());
2917
+ } else {
2918
+ resolve8();
2919
+ }
2920
+ });
2921
+ }
2922
+ getTracker() {
2923
+ return this.tracker;
2924
+ }
2925
+ // ===== ANALYSIS =====
2926
+ async refreshAnalysis() {
2927
+ try {
2928
+ const analysis = await analyzeProject(this.config.projectPath);
2929
+ this.analysis = analysis;
2930
+ return analysis;
2931
+ } catch (err) {
2932
+ this.emit({ type: "error", message: `Analysis failed: ${err.message}`, error: err });
2933
+ throw err;
2934
+ }
2935
+ }
2936
+ // ===== REQUEST HANDLER =====
2937
+ async handleRequest(req, res) {
2938
+ const startTime = Date.now();
2939
+ if (this.config.dashboard && req.url?.startsWith(this.config.dashboardPath)) {
2940
+ return this.serveDashboard(req, res);
2941
+ }
2942
+ if (req.url === "/health" || req.url === "/__cto/health") {
2943
+ res.writeHead(200, { "Content-Type": "application/json" });
2944
+ res.end(JSON.stringify({
2945
+ status: "ok",
2946
+ version: "4.0.0",
2947
+ uptime: process.uptime(),
2948
+ analysis: this.analysis ? "ready" : "loading"
2949
+ }));
2950
+ return;
2951
+ }
2952
+ if (this.config.apiKey) {
2953
+ const authHeader = req.headers["x-cto-key"] || req.headers["authorization"]?.replace(/^Bearer\s+/i, "") || "";
2954
+ if (authHeader !== this.config.apiKey) {
2955
+ res.writeHead(401, { "Content-Type": "application/json" });
2956
+ res.end(JSON.stringify({ error: "Unauthorized. Set x-cto-key header or Authorization: Bearer <key>" }));
2957
+ return;
2958
+ }
2959
+ }
2960
+ if (req.method !== "POST") {
2961
+ res.writeHead(405, { "Content-Type": "application/json" });
2962
+ res.end(JSON.stringify({ error: "Method not allowed. Gateway only proxies POST requests." }));
2963
+ return;
2964
+ }
2965
+ let body;
2966
+ try {
2967
+ body = await readBody(req, this.config.maxBodyBytes);
2968
+ } catch (err) {
2969
+ const status = err.message === "body-too-large" ? 413 : 400;
2970
+ res.writeHead(status, { "Content-Type": "application/json" });
2971
+ 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
+ return;
2973
+ }
2974
+ let parsedBody;
2975
+ try {
2976
+ parsedBody = JSON.parse(body);
2977
+ } catch {
2978
+ res.writeHead(400, { "Content-Type": "application/json" });
2979
+ res.end(JSON.stringify({ error: "Invalid JSON in request body" }));
2980
+ return;
2981
+ }
2982
+ const targetUrl = req.headers["x-cto-target"] || req.headers["x-target-url"] || "";
2983
+ if (!targetUrl) {
2984
+ res.writeHead(400, { "Content-Type": "application/json" });
2985
+ res.end(JSON.stringify({
2986
+ error: "Missing target URL. Set x-cto-target header to the provider API URL.",
2987
+ example: "x-cto-target: https://api.openai.com/v1/chat/completions"
2988
+ }));
2989
+ return;
2990
+ }
2991
+ let targetUrlParsed;
2992
+ try {
2993
+ targetUrlParsed = new URL(targetUrl);
2994
+ } catch {
2995
+ res.writeHead(400, { "Content-Type": "application/json" });
2996
+ res.end(JSON.stringify({ error: "Invalid target URL" }));
2997
+ return;
2998
+ }
2999
+ if (targetUrlParsed.protocol !== "https:" && targetUrlParsed.hostname !== "localhost") {
3000
+ res.writeHead(403, { "Content-Type": "application/json" });
3001
+ res.end(JSON.stringify({ error: "Only HTTPS targets allowed (SSRF protection)" }));
3002
+ return;
3003
+ }
3004
+ if (!isAllowedTarget(targetUrlParsed.hostname, this.config)) {
3005
+ res.writeHead(403, { "Content-Type": "application/json" });
3006
+ res.end(JSON.stringify({
3007
+ error: `Target domain not allowed: ${targetUrlParsed.hostname}`,
3008
+ allowed: this.config.allowedTargetDomains.length > 0 ? this.config.allowedTargetDomains : ["api.openai.com", "api.anthropic.com", "*.googleapis.com", "*.openai.azure.com"]
3009
+ }));
3010
+ return;
3011
+ }
3012
+ try {
3013
+ const resolved = await lookup(targetUrlParsed.hostname);
3014
+ if (isPrivateIP(resolved.address)) {
3015
+ res.writeHead(403, { "Content-Type": "application/json" });
3016
+ res.end(JSON.stringify({ error: "Target resolves to private IP (SSRF protection)" }));
3017
+ return;
3018
+ }
3019
+ } catch {
3020
+ }
3021
+ const headers = flattenHeaders(req.headers);
3022
+ const provider = detectProvider(targetUrl || req.url || "", headers);
3023
+ const parsed = provider.parseRequest(parsedBody);
3024
+ const now = /* @__PURE__ */ new Date();
3025
+ if (this.tracker.isDailyBudgetExceeded(now)) {
3026
+ res.writeHead(429, { "Content-Type": "application/json" });
3027
+ res.end(JSON.stringify({
3028
+ error: "Daily budget exceeded",
3029
+ budget: this.config.budgetDaily,
3030
+ current: this.tracker.getDailyCost(now)
3031
+ }));
3032
+ return;
3033
+ }
3034
+ if (this.tracker.isMonthlyBudgetExceeded(now)) {
3035
+ res.writeHead(429, { "Content-Type": "application/json" });
3036
+ res.end(JSON.stringify({
3037
+ error: "Monthly budget exceeded",
3038
+ budget: this.config.budgetMonthly,
3039
+ current: this.tracker.getMonthlyCost(now)
3040
+ }));
3041
+ return;
3042
+ }
3043
+ if (this.analysisPromise && !this.analysis) {
3044
+ try {
3045
+ await this.analysisPromise;
3046
+ } catch {
3047
+ }
3048
+ }
3049
+ const interceptResult = await interceptRequest(parsed.messages, this.config, this.analysis);
3050
+ if (interceptResult.secretsBlocked) {
3051
+ res.writeHead(403, { "Content-Type": "application/json" });
3052
+ res.end(JSON.stringify({
3053
+ error: "Request blocked: secrets detected in message content",
3054
+ decisions: interceptResult.decisions,
3055
+ secretsRedacted: interceptResult.secretsRedacted
3056
+ }));
3057
+ this.tracker.record({
3058
+ provider: provider.name,
3059
+ model: parsed.model,
3060
+ inputTokens: 0,
3061
+ outputTokens: 0,
3062
+ costUSD: 0,
3063
+ originalTokens: interceptResult.originalTokens,
3064
+ optimizedTokens: 0,
3065
+ savedTokens: 0,
3066
+ savedUSD: 0,
3067
+ secretsRedacted: interceptResult.secretsRedacted,
3068
+ secretsBlocked: true,
3069
+ projectPath: this.config.projectPath,
3070
+ latencyMs: Date.now() - startTime,
3071
+ stream: parsed.stream,
3072
+ error: "blocked:secrets"
3073
+ });
3074
+ return;
3075
+ }
3076
+ const modifiedBody = rebuildRequestBody(parsedBody, interceptResult.messages, provider.name);
3077
+ try {
3078
+ await this.proxyRequest(targetUrl, req, res, modifiedBody, provider, parsed, interceptResult, startTime);
3079
+ } catch (err) {
3080
+ if (!res.headersSent) {
3081
+ const status = err.message === "upstream-timeout" ? 504 : 502;
3082
+ res.writeHead(status, { "Content-Type": "application/json" });
3083
+ res.end(JSON.stringify({ error: status === 504 ? "Upstream provider timeout" : `Proxy error: ${err.message}` }));
3084
+ }
3085
+ this.emit({ type: "error", message: `Proxy error: ${err.message}`, error: err });
3086
+ }
3087
+ }
3088
+ // ===== PROXY =====
3089
+ async proxyRequest(targetUrl, clientReq, clientRes, body, provider, parsed, interceptResult, startTime) {
3090
+ const url = new URL(targetUrl);
3091
+ const isHttps = url.protocol === "https:";
3092
+ const requester = isHttps ? httpsRequest : httpRequest;
3093
+ const forwardHeaders = {};
3094
+ const stripHeaders = /* @__PURE__ */ new Set(["host", "content-length", "x-cto-target", "x-target-url", "x-cto-key"]);
3095
+ for (const [key, value] of Object.entries(clientReq.headers)) {
3096
+ if (stripHeaders.has(key)) continue;
3097
+ if (value) forwardHeaders[key] = Array.isArray(value) ? value[0] : value;
3098
+ }
3099
+ forwardHeaders["content-length"] = Buffer.byteLength(body).toString();
3100
+ return new Promise((resolve8, reject) => {
3101
+ const proxyReq = requester(
3102
+ {
3103
+ hostname: url.hostname,
3104
+ port: url.port || (isHttps ? 443 : 80),
3105
+ path: url.pathname + url.search,
3106
+ method: "POST",
3107
+ headers: forwardHeaders,
3108
+ agent: isHttps ? this.httpsAgent : this.httpAgent,
3109
+ // Connection pooling
3110
+ timeout: this.config.upstreamTimeoutMs
3111
+ },
3112
+ (proxyRes) => {
3113
+ if (parsed.stream && proxyRes.headers["content-type"]?.includes("text/event-stream")) {
3114
+ this.handleStreamResponse(
3115
+ proxyRes,
3116
+ clientRes,
3117
+ provider,
3118
+ parsed,
3119
+ interceptResult,
3120
+ startTime
3121
+ ).then(resolve8).catch(reject);
3122
+ } else {
3123
+ this.handleBufferedResponse(
3124
+ proxyRes,
3125
+ clientRes,
3126
+ provider,
3127
+ parsed,
3128
+ interceptResult,
3129
+ startTime
3130
+ ).then(resolve8).catch(reject);
3131
+ }
3132
+ }
3133
+ );
3134
+ proxyReq.on("timeout", () => {
3135
+ proxyReq.destroy();
3136
+ reject(new Error("upstream-timeout"));
3137
+ });
3138
+ proxyReq.on("error", reject);
3139
+ proxyReq.write(body);
3140
+ proxyReq.end();
3141
+ });
3142
+ }
3143
+ // ===== STREAM HANDLER =====
3144
+ async handleStreamResponse(proxyRes, clientRes, provider, parsed, interceptResult, startTime) {
3145
+ clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
3146
+ let fullContent2 = "";
3147
+ let inputTokens = 0;
3148
+ let outputTokens = 0;
3149
+ let sseBuffer = "";
3150
+ return new Promise((resolve8) => {
3151
+ proxyRes.on("data", (chunk) => {
3152
+ clientRes.write(chunk);
3153
+ sseBuffer += chunk.toString();
3154
+ const events = sseBuffer.split("\n\n");
3155
+ sseBuffer = events.pop() || "";
3156
+ for (const event of events) {
3157
+ for (const line of event.split("\n")) {
3158
+ if (!line.startsWith("data: ")) continue;
3159
+ const data = line.slice(6).trim();
3160
+ if (data === "[DONE]") continue;
3161
+ try {
3162
+ const obj = JSON.parse(data);
3163
+ const delta = obj.choices?.[0]?.delta?.content || obj.delta?.text || "";
3164
+ if (delta) fullContent2 += delta;
3165
+ if (obj.usage) {
3166
+ inputTokens = obj.usage.prompt_tokens || obj.usage.input_tokens || 0;
3167
+ outputTokens = obj.usage.completion_tokens || obj.usage.output_tokens || 0;
3168
+ }
3169
+ } catch {
3170
+ }
3171
+ }
3172
+ }
3173
+ });
3174
+ proxyRes.on("end", () => {
3175
+ if (sseBuffer.trim()) {
3176
+ for (const line of sseBuffer.split("\n")) {
3177
+ if (!line.startsWith("data: ")) continue;
3178
+ const data = line.slice(6).trim();
3179
+ if (data === "[DONE]") continue;
3180
+ try {
3181
+ const obj = JSON.parse(data);
3182
+ if (obj.usage) {
3183
+ inputTokens = obj.usage.prompt_tokens || obj.usage.input_tokens || 0;
3184
+ outputTokens = obj.usage.completion_tokens || obj.usage.output_tokens || 0;
3185
+ }
3186
+ } catch {
3187
+ }
3188
+ }
3189
+ }
3190
+ clientRes.end();
3191
+ if (inputTokens === 0) inputTokens = interceptResult.optimizedTokens;
3192
+ if (outputTokens === 0) outputTokens = Math.ceil(fullContent2.length / 4);
3193
+ const costUSD = estimateCost(provider, parsed.model, inputTokens, outputTokens);
3194
+ const originalCost = estimateCost(provider, parsed.model, interceptResult.originalTokens, outputTokens);
3195
+ this.tracker.record({
3196
+ provider: provider.name,
3197
+ model: parsed.model,
3198
+ inputTokens,
3199
+ outputTokens,
3200
+ costUSD,
3201
+ originalTokens: interceptResult.originalTokens,
3202
+ optimizedTokens: interceptResult.optimizedTokens,
3203
+ savedTokens: interceptResult.originalTokens - interceptResult.optimizedTokens,
3204
+ savedUSD: Math.max(0, originalCost - costUSD),
3205
+ secretsRedacted: interceptResult.secretsRedacted,
3206
+ secretsBlocked: false,
3207
+ projectPath: this.config.projectPath,
3208
+ latencyMs: Date.now() - startTime,
3209
+ stream: true
3210
+ });
3211
+ resolve8();
3212
+ });
3213
+ proxyRes.on("error", () => {
3214
+ clientRes.end();
3215
+ resolve8();
3216
+ });
3217
+ });
3218
+ }
3219
+ // ===== BUFFERED HANDLER =====
3220
+ async handleBufferedResponse(proxyRes, clientRes, provider, parsed, interceptResult, startTime) {
3221
+ return new Promise((resolve8) => {
3222
+ const chunks = [];
3223
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
3224
+ proxyRes.on("end", () => {
3225
+ const responseBody = Buffer.concat(chunks).toString();
3226
+ clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
3227
+ clientRes.end(responseBody);
3228
+ try {
3229
+ const responseJson = JSON.parse(responseBody);
3230
+ const parsedResponse = provider.parseResponse(responseJson, false);
3231
+ const costUSD = estimateCost(
3232
+ provider,
3233
+ parsedResponse.model || parsed.model,
3234
+ parsedResponse.inputTokens,
3235
+ parsedResponse.outputTokens
3236
+ );
3237
+ const originalCost = estimateCost(
3238
+ provider,
3239
+ parsed.model,
3240
+ interceptResult.originalTokens,
3241
+ parsedResponse.outputTokens
3242
+ );
3243
+ this.tracker.record({
3244
+ provider: provider.name,
3245
+ model: parsedResponse.model || parsed.model,
3246
+ inputTokens: parsedResponse.inputTokens,
3247
+ outputTokens: parsedResponse.outputTokens,
3248
+ costUSD,
3249
+ originalTokens: interceptResult.originalTokens,
3250
+ optimizedTokens: interceptResult.optimizedTokens,
3251
+ savedTokens: interceptResult.originalTokens - interceptResult.optimizedTokens,
3252
+ savedUSD: Math.max(0, originalCost - costUSD),
3253
+ secretsRedacted: interceptResult.secretsRedacted,
3254
+ secretsBlocked: false,
3255
+ projectPath: this.config.projectPath,
3256
+ latencyMs: Date.now() - startTime,
3257
+ stream: false
3258
+ });
3259
+ } catch {
3260
+ this.tracker.record({
3261
+ provider: provider.name,
3262
+ model: parsed.model,
3263
+ inputTokens: interceptResult.optimizedTokens,
3264
+ outputTokens: 0,
3265
+ costUSD: 0,
3266
+ originalTokens: interceptResult.originalTokens,
3267
+ optimizedTokens: interceptResult.optimizedTokens,
3268
+ savedTokens: interceptResult.originalTokens - interceptResult.optimizedTokens,
3269
+ savedUSD: 0,
3270
+ secretsRedacted: interceptResult.secretsRedacted,
3271
+ secretsBlocked: false,
3272
+ projectPath: this.config.projectPath,
3273
+ latencyMs: Date.now() - startTime,
3274
+ stream: false,
3275
+ error: "response-parse-failed"
3276
+ });
3277
+ }
3278
+ resolve8();
3279
+ });
3280
+ proxyRes.on("error", () => {
3281
+ clientRes.end();
3282
+ resolve8();
3283
+ });
3284
+ });
3285
+ }
3286
+ // ===== DASHBOARD =====
3287
+ serveDashboard(_req, res) {
3288
+ const summary = this.tracker.getSummary("month");
3289
+ const dailySummary = this.tracker.getSummary("day");
3290
+ const html = generateDashboardHTML(summary, dailySummary, this.config, this.analysis);
3291
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3292
+ res.end(html);
3293
+ }
3294
+ };
3295
+ }
3296
+ });
3297
+
3298
+ // src/cli/score.ts
3299
+ 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";
3302
+
3303
+ // src/engine/score.ts
3304
+ init_selector();
3305
+ init_coverage();
3306
+ init_graph_utils();
3307
+ async function computeContextScore(analysis, task = "general code review and refactoring", budget = 5e4) {
3308
+ const selection = await selectContext({ task, analysis, budget });
3309
+ const insights = [];
3310
+ const efficiency = scoreEfficiency(analysis, selection, insights);
3311
+ const coverage = scoreCoverage(analysis, selection, insights);
3312
+ const riskControl = scoreRiskControl(analysis, selection, insights);
3313
+ const structure = scoreStructure(analysis, insights);
3314
+ const governance = scoreGovernance(analysis, insights);
3315
+ const overall = Math.round(
3316
+ efficiency.weighted + coverage.weighted + riskControl.weighted + structure.weighted + governance.weighted
3317
+ );
3318
+ const grade = scoreToGrade(overall);
3319
+ const naiveTokens = analysis.totalTokens;
3320
+ const optimizedTokens = selection.totalTokens;
3321
+ const savedTokens = naiveTokens - optimizedTokens;
3322
+ const savedPercent = naiveTokens > 0 ? Math.round(savedTokens / naiveTokens * 100) : 0;
3323
+ const interactionsPerMonth = 40 * 20;
3324
+ const costPerMToken = 3;
3325
+ const naiveMonthlyCost = naiveTokens / 1e6 * costPerMToken * interactionsPerMonth;
3326
+ const optimizedMonthlyCost = optimizedTokens / 1e6 * costPerMToken * interactionsPerMonth;
3327
+ const monthlySavingsUSD = Math.round((naiveMonthlyCost - optimizedMonthlyCost) * 100) / 100;
3328
+ return {
3329
+ overall,
3330
+ grade,
3331
+ dimensions: {
3332
+ efficiency,
3333
+ coverage,
3334
+ riskControl,
3335
+ structure,
3336
+ governance
3337
+ },
3338
+ insights: insights.sort((a, b) => {
3339
+ const order = { high: 0, medium: 1, low: 2 };
3340
+ return order[a.impact] - order[b.impact];
3341
+ }),
3342
+ comparison: {
3343
+ naiveTokens,
3344
+ optimizedTokens,
3345
+ savedTokens,
3346
+ savedPercent,
3347
+ monthlySavingsUSD
3348
+ },
3349
+ meta: {
3350
+ projectName: analysis.projectName,
3351
+ totalFiles: analysis.totalFiles,
3352
+ totalTokens: analysis.totalTokens,
3353
+ analyzedAt: analysis.analyzedAt
3354
+ }
3355
+ };
3356
+ }
3357
+ function scoreEfficiency(analysis, selection, insights) {
3358
+ const weight = 30;
3359
+ const ratio = analysis.totalTokens > 0 ? 1 - selection.totalTokens / analysis.totalTokens : 0;
3360
+ const selectivity = analysis.totalFiles > 0 ? 1 - selection.files.length / analysis.totalFiles : 0;
3361
+ const prunedFiles = selection.files.filter(
3362
+ (f) => f.pruneLevel === "signatures" || f.pruneLevel === "skeleton"
3363
+ ).length;
3364
+ const pruneRatio = selection.files.length > 0 ? prunedFiles / selection.files.length : 0;
3365
+ const raw = (ratio * 0.5 + selectivity * 0.3 + pruneRatio * 0.2) * 100;
3366
+ const score = Math.min(100, Math.max(0, Math.round(raw)));
3367
+ const weighted = score / 100 * weight;
3368
+ if (ratio > 0.7) {
3369
+ insights.push({
3370
+ type: "strength",
3371
+ title: "Excellent compression",
3372
+ detail: `${Math.round(ratio * 100)}% token reduction while maintaining context quality`,
3373
+ impact: "high"
3374
+ });
3375
+ }
3376
+ if (ratio < 0.3 && analysis.totalTokens > 2e4) {
3377
+ insights.push({
3378
+ type: "weakness",
3379
+ title: "Low compression opportunity",
3380
+ detail: "Most files are needed. Consider splitting the project into smaller modules.",
3381
+ impact: "medium"
3382
+ });
3383
+ }
3384
+ return {
3385
+ score,
3386
+ weight,
3387
+ weighted,
3388
+ detail: `${Math.round(ratio * 100)}% compression, ${prunedFiles}/${selection.files.length} files pruned`
3389
+ };
3390
+ }
3391
+ function scoreCoverage(analysis, selection, insights) {
3392
+ const weight = 25;
1504
3393
  const coverageScore = selection.coverage.score;
1505
3394
  const missingCritical = selection.coverage.missingCritical.length;
1506
3395
  let penalty = 0;
@@ -1727,6 +3616,7 @@ function formatNumber(n) {
1727
3616
  }
1728
3617
 
1729
3618
  // src/engine/benchmark.ts
3619
+ init_selector();
1730
3620
  async function runBenchmark(analysis, task = "general code review and refactoring", budget = 5e4) {
1731
3621
  const criticalFiles = analysis.files.filter((f) => f.riskScore >= 80);
1732
3622
  const highRiskFiles = analysis.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80);
@@ -1879,6 +3769,540 @@ function fmt(n) {
1879
3769
  return n.toString();
1880
3770
  }
1881
3771
 
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
+ }
3815
+ }
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
+ }
3835
+ }
3836
+ return packagePaths;
3837
+ }
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
+ }
3893
+ }
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
+ }
3902
+ }
3903
+ }
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);
3914
+ }
3915
+ }
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: [],
3929
+ sharedPackages: [],
3930
+ crossPackageEdges: [],
3931
+ isolationScore: 100,
3932
+ totalTokens: analysis?.totalTokens ?? 0,
3933
+ packageTokenMap: {}
3934
+ };
3935
+ }
3936
+ const packagePaths = await discoverPackages(rootPath, tool);
3937
+ const packages = [];
3938
+ const packageTokenMap = {};
3939
+ for (const pkgPath of packagePaths) {
3940
+ const pkgJsonPath = join5(pkgPath, "package.json");
3941
+ let name = basename3(pkgPath);
3942
+ let pkgDeps = [];
3943
+ try {
3944
+ const pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf-8"));
3945
+ name = pkgJson.name || name;
3946
+ const allDeps = {
3947
+ ...pkgJson.dependencies || {},
3948
+ ...pkgJson.devDependencies || {},
3949
+ ...pkgJson.peerDependencies || {}
3950
+ };
3951
+ pkgDeps = Object.keys(allDeps);
3952
+ } catch {
3953
+ }
3954
+ const relPath = relative4(rootPath, pkgPath);
3955
+ let fileCount = 0;
3956
+ let tokenCount = 0;
3957
+ const entryPoints = [];
3958
+ if (analysis) {
3959
+ for (const f of analysis.files) {
3960
+ if (f.relativePath.startsWith(relPath + "/") || f.relativePath.startsWith(relPath + "\\")) {
3961
+ fileCount++;
3962
+ tokenCount += f.tokens;
3963
+ if (f.kind === "entry") entryPoints.push(f.relativePath);
3964
+ }
3965
+ }
3966
+ }
3967
+ packageTokenMap[name] = tokenCount;
3968
+ packages.push({
3969
+ name,
3970
+ path: pkgPath,
3971
+ relativePath: relPath,
3972
+ files: fileCount,
3973
+ tokens: tokenCount,
3974
+ dependencies: [],
3975
+ // Filled below after we know all package names
3976
+ dependents: [],
3977
+ isShared: false,
3978
+ entryPoints
3979
+ });
3980
+ }
3981
+ const pkgNames = new Set(packages.map((p) => p.name));
3982
+ for (const pkg of packages) {
3983
+ const pkgJsonPath = join5(pkg.path, "package.json");
3984
+ try {
3985
+ const pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf-8"));
3986
+ const allDeps = {
3987
+ ...pkgJson.dependencies || {},
3988
+ ...pkgJson.devDependencies || {}
3989
+ };
3990
+ pkg.dependencies = Object.keys(allDeps).filter((d) => pkgNames.has(d));
3991
+ } catch {
3992
+ }
3993
+ }
3994
+ for (const pkg of packages) {
3995
+ for (const depName of pkg.dependencies) {
3996
+ const dep = packages.find((p) => p.name === depName);
3997
+ if (dep) dep.dependents.push(pkg.name);
3998
+ }
3999
+ }
4000
+ for (const pkg of packages) {
4001
+ pkg.isShared = pkg.dependents.length >= 2;
4002
+ }
4003
+ const sharedPackages = packages.filter((p) => p.isShared);
4004
+ const crossPackageEdges = analysis ? buildCrossPackageEdges(packages, analysis.files, analysis.graph.edges, rootPath) : [];
4005
+ const maxPossibleEdges = packages.length * (packages.length - 1);
4006
+ const actualEdges = crossPackageEdges.length;
4007
+ const isolationScore = maxPossibleEdges > 0 ? Math.round(100 * (1 - actualEdges / maxPossibleEdges)) : 100;
4008
+ return {
4009
+ detected: true,
4010
+ tool,
4011
+ rootPath,
4012
+ packages,
4013
+ sharedPackages,
4014
+ crossPackageEdges,
4015
+ isolationScore,
4016
+ totalTokens: analysis?.totalTokens ?? packages.reduce((s, p) => s + p.tokens, 0),
4017
+ packageTokenMap
4018
+ };
4019
+ }
4020
+ function selectPackageContext(monorepo, targetPackage) {
4021
+ const target = monorepo.packages.find((p) => p.name === targetPackage || p.relativePath === targetPackage);
4022
+ if (!target) {
4023
+ return {
4024
+ targetPackage,
4025
+ includedPackages: [],
4026
+ excludedPackages: monorepo.packages.map((p) => p.name),
4027
+ originalTokens: monorepo.totalTokens,
4028
+ optimizedTokens: 0,
4029
+ savedTokens: monorepo.totalTokens,
4030
+ savedPercent: 100
4031
+ };
4032
+ }
4033
+ const includedNames = /* @__PURE__ */ new Set([target.name]);
4034
+ for (const depName of target.dependencies) {
4035
+ includedNames.add(depName);
4036
+ const dep = monorepo.packages.find((p) => p.name === depName);
4037
+ if (dep) {
4038
+ for (const transDep of dep.dependencies) {
4039
+ const transDepPkg = monorepo.packages.find((p) => p.name === transDep);
4040
+ if (transDepPkg?.isShared) includedNames.add(transDep);
4041
+ }
4042
+ }
4043
+ }
4044
+ const includedPackages = Array.from(includedNames);
4045
+ const excludedPackages = monorepo.packages.filter((p) => !includedNames.has(p.name)).map((p) => p.name);
4046
+ const optimizedTokens = monorepo.packages.filter((p) => includedNames.has(p.name)).reduce((s, p) => s + p.tokens, 0);
4047
+ const savedTokens = monorepo.totalTokens - optimizedTokens;
4048
+ const savedPercent = monorepo.totalTokens > 0 ? Math.round(savedTokens / monorepo.totalTokens * 100) : 0;
4049
+ return {
4050
+ targetPackage: target.name,
4051
+ includedPackages,
4052
+ excludedPackages,
4053
+ originalTokens: monorepo.totalTokens,
4054
+ optimizedTokens,
4055
+ savedTokens,
4056
+ savedPercent
4057
+ };
4058
+ }
4059
+ function renderMonorepoAnalysis(mono) {
4060
+ if (!mono.detected) {
4061
+ return " \u2139\uFE0F No monorepo detected (single-package project).\n";
4062
+ }
4063
+ const lines = [];
4064
+ lines.push("");
4065
+ 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");
4066
+ lines.push(` \u{1F4E6} Monorepo Analysis \u2014 ${mono.tool}`);
4067
+ 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");
4068
+ lines.push("");
4069
+ lines.push(` Packages: ${mono.packages.length}`);
4070
+ lines.push(` Shared packages: ${mono.sharedPackages.length}`);
4071
+ lines.push(` Isolation score: ${mono.isolationScore}/100`);
4072
+ lines.push(` Total tokens: ${mono.totalTokens.toLocaleString()}`);
4073
+ lines.push("");
4074
+ lines.push(" Package breakdown:");
4075
+ lines.push("");
4076
+ const sorted = [...mono.packages].sort((a, b) => b.tokens - a.tokens);
4077
+ const maxNameLen = Math.max(...sorted.map((p) => p.name.length), 10);
4078
+ for (const pkg of sorted) {
4079
+ const name = pkg.name.padEnd(maxNameLen);
4080
+ const tokens = `${(pkg.tokens / 1e3).toFixed(1)}K`.padStart(8);
4081
+ const files = `${pkg.files} files`.padStart(10);
4082
+ const deps = pkg.dependencies.length > 0 ? ` \u2192 ${pkg.dependencies.join(", ")}` : "";
4083
+ const shared = pkg.isShared ? " [shared]" : "";
4084
+ lines.push(` ${name} ${tokens} ${files}${shared}${deps}`);
4085
+ }
4086
+ if (mono.crossPackageEdges.length > 0) {
4087
+ lines.push("");
4088
+ lines.push(" Cross-package dependencies:");
4089
+ for (const edge of mono.crossPackageEdges.slice(0, 10)) {
4090
+ lines.push(` ${edge.from} \u2192 ${edge.to} (${edge.files} files)`);
4091
+ }
4092
+ if (mono.crossPackageEdges.length > 10) {
4093
+ lines.push(` ... and ${mono.crossPackageEdges.length - 10} more`);
4094
+ }
4095
+ }
4096
+ lines.push("");
4097
+ lines.push(" \u{1F4A1} Use --monorepo --package <name> to see context savings for a specific package.");
4098
+ lines.push("");
4099
+ return lines.join("\n");
4100
+ }
4101
+ function renderPackageContext(result) {
4102
+ const lines = [];
4103
+ lines.push("");
4104
+ 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");
4105
+ lines.push(` \u{1F3AF} Package Context \u2014 ${result.targetPackage}`);
4106
+ 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");
4107
+ lines.push("");
4108
+ lines.push(` Included packages: ${result.includedPackages.length}`);
4109
+ lines.push(` Excluded packages: ${result.excludedPackages.length}`);
4110
+ lines.push("");
4111
+ lines.push(` Original tokens: ${result.originalTokens.toLocaleString()}`);
4112
+ lines.push(` Optimized tokens: ${result.optimizedTokens.toLocaleString()}`);
4113
+ lines.push(` Token savings: ${result.savedTokens.toLocaleString()} (${result.savedPercent}%)`);
4114
+ lines.push("");
4115
+ if (result.includedPackages.length > 0) {
4116
+ lines.push(" \u2705 Included:");
4117
+ for (const p of result.includedPackages) {
4118
+ lines.push(` ${p}`);
4119
+ }
4120
+ }
4121
+ if (result.excludedPackages.length > 0) {
4122
+ lines.push(" \u2B1C Excluded (not needed for this package):");
4123
+ for (const p of result.excludedPackages.slice(0, 10)) {
4124
+ lines.push(` ${p}`);
4125
+ }
4126
+ if (result.excludedPackages.length > 10) {
4127
+ lines.push(` ... and ${result.excludedPackages.length - 10} more`);
4128
+ }
4129
+ }
4130
+ lines.push("");
4131
+ return lines.join("\n");
4132
+ }
4133
+
4134
+ // src/engine/quality-gate.ts
4135
+ import { readFile as readFile6, writeFile as writeFile2, mkdir } from "fs/promises";
4136
+ import { resolve as resolve5 } from "path";
4137
+ import { existsSync as existsSync5 } from "fs";
4138
+ var DEFAULT_GATE_CONFIG = {
4139
+ threshold: 70,
4140
+ failOnSecrets: true,
4141
+ failOnRegression: true,
4142
+ regressionLimit: 5,
4143
+ baselinePath: ".cto/baseline.json",
4144
+ secretSeverities: ["critical", "high"]
4145
+ };
4146
+ async function loadBaseline(projectPath, baselinePath) {
4147
+ const filePath = resolve5(projectPath, baselinePath || ".cto/baseline.json");
4148
+ if (!existsSync5(filePath)) return null;
4149
+ try {
4150
+ const content = await readFile6(filePath, "utf-8");
4151
+ return JSON.parse(content);
4152
+ } catch {
4153
+ return null;
4154
+ }
4155
+ }
4156
+ async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
4157
+ const dir = resolve5(projectPath, ".cto");
4158
+ if (!existsSync5(dir)) await mkdir(dir, { recursive: true });
4159
+ const baseline = {
4160
+ score: score.overall,
4161
+ grade: score.grade,
4162
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4163
+ commit,
4164
+ branch,
4165
+ dimensions: {
4166
+ efficiency: score.dimensions.efficiency.score,
4167
+ coverage: score.dimensions.coverage.score,
4168
+ riskControl: score.dimensions.riskControl.score,
4169
+ structure: score.dimensions.structure.score,
4170
+ governance: score.dimensions.governance.score
4171
+ }
4172
+ };
4173
+ const filePath = resolve5(projectPath, baselinePath || ".cto/baseline.json");
4174
+ await writeFile2(filePath, JSON.stringify(baseline, null, 2));
4175
+ }
4176
+ async function runQualityGate(score, analysis, secretFindings, config = {}) {
4177
+ const cfg = { ...DEFAULT_GATE_CONFIG, ...config };
4178
+ const checks = [];
4179
+ const baseline = await loadBaseline(analysis.projectPath, cfg.baselinePath);
4180
+ const previousScore = baseline?.score ?? null;
4181
+ const delta = previousScore !== null ? score.overall - previousScore : null;
4182
+ const thresholdPassed = score.overall >= cfg.threshold;
4183
+ checks.push({
4184
+ name: "Score threshold",
4185
+ passed: thresholdPassed,
4186
+ detail: thresholdPassed ? `Score ${score.overall} \u2265 threshold ${cfg.threshold}` : `Score ${score.overall} < threshold ${cfg.threshold}`,
4187
+ severity: thresholdPassed ? "info" : "error"
4188
+ });
4189
+ const dangerousSecrets = secretFindings.filter(
4190
+ (f) => cfg.secretSeverities.includes(f.severity)
4191
+ );
4192
+ const secretsPassed = !cfg.failOnSecrets || dangerousSecrets.length === 0;
4193
+ checks.push({
4194
+ name: "No secrets detected",
4195
+ passed: secretsPassed,
4196
+ detail: secretsPassed ? "No critical/high severity secrets found" : `${dangerousSecrets.length} secret(s) with ${cfg.secretSeverities.join("/")} severity`,
4197
+ severity: secretsPassed ? "info" : "error"
4198
+ });
4199
+ let regressionPassed = true;
4200
+ if (cfg.failOnRegression && delta !== null) {
4201
+ regressionPassed = delta >= -cfg.regressionLimit;
4202
+ }
4203
+ checks.push({
4204
+ name: "No score regression",
4205
+ passed: regressionPassed,
4206
+ detail: delta !== null ? regressionPassed ? `Score changed by ${delta >= 0 ? "+" : ""}${delta} (limit: -${cfg.regressionLimit})` : `Score dropped by ${Math.abs(delta)} points (limit: -${cfg.regressionLimit})` : "No baseline found (first run)",
4207
+ severity: regressionPassed ? delta !== null && delta < 0 ? "warning" : "info" : "error"
4208
+ });
4209
+ const weakDimensions = Object.entries(score.dimensions).filter(([_, d]) => d.score < 50).map(([name]) => name);
4210
+ const dimensionsPassed = weakDimensions.length === 0;
4211
+ checks.push({
4212
+ name: "Dimension health",
4213
+ passed: dimensionsPassed,
4214
+ detail: dimensionsPassed ? "All dimensions above 50%" : `Weak dimensions: ${weakDimensions.join(", ")}`,
4215
+ severity: dimensionsPassed ? "info" : "warning"
4216
+ });
4217
+ const passed = checks.filter((c) => c.severity === "error").every((c) => c.passed);
4218
+ const prComment = generatePRComment(score, analysis, checks, baseline, delta);
4219
+ const summary = generateSummary(score, checks, passed);
4220
+ return {
4221
+ passed,
4222
+ score: score.overall,
4223
+ grade: score.grade,
4224
+ previousScore,
4225
+ delta,
4226
+ checks,
4227
+ baseline,
4228
+ prComment,
4229
+ summary
4230
+ };
4231
+ }
4232
+ function generatePRComment(score, analysis, checks, baseline, delta) {
4233
+ const gradeEmoji = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
4234
+ const allPassed = checks.filter((c) => c.severity === "error").every((c) => c.passed);
4235
+ const statusIcon = allPassed ? "\u2705" : "\u274C";
4236
+ const deltaStr = delta !== null ? ` (${delta >= 0 ? "+" : ""}${delta})` : "";
4237
+ const lines = [
4238
+ `## ${statusIcon} CTO Quality Gate ${allPassed ? "Passed" : "Failed"}`,
4239
+ "",
4240
+ `### ${gradeEmoji} Context Score: ${score.overall}/100 (${score.grade})${deltaStr}`,
4241
+ "",
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
+
1882
4306
  // src/cli/score.ts
1883
4307
  async function main() {
1884
4308
  const args = process.argv.slice(2);
@@ -1887,24 +4311,60 @@ async function main() {
1887
4311
  const fixMode = args.includes("--fix");
1888
4312
  const reportMode = args.includes("--report");
1889
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");
1890
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;
1891
4326
  const contextIdx = args.indexOf("--context");
1892
4327
  const contextTask = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
1893
- const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask);
1894
- const projectPath = resolve4(pathArg ?? ".");
4328
+ const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask && a !== targetPackage);
4329
+ const projectPath = resolve7(pathArg ?? ".");
1895
4330
  if (helpMode) {
1896
4331
  console.log(`
1897
4332
  \u26A1 cto-score \u2014 How AI-ready is your codebase?
1898
4333
 
1899
4334
  Usage:
1900
- npx cto-ai-cli Scan current directory
1901
- npx cto-ai-cli ./path Scan a specific project
1902
- npx cto-ai-cli --benchmark CTO vs naive vs random comparison
1903
- npx cto-ai-cli --fix Auto-generate optimized context files
1904
- npx cto-ai-cli --context "your task" Generate task-specific context
1905
- npx cto-ai-cli --report Generate shareable markdown report
1906
- npx cto-ai-cli --compare Compare your score vs popular projects
1907
- npx cto-ai-cli --json Output as JSON (for CI/scripts)
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)
1908
4368
 
1909
4369
  What it does:
1910
4370
  Analyzes your project's structure, dependencies, and risk profile.
@@ -1916,6 +4376,65 @@ async function main() {
1916
4376
  `);
1917
4377
  process.exit(0);
1918
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
+ }
1919
4438
  console.log("");
1920
4439
  console.log(" \u26A1 cto-score \u2014 analyzing your project...");
1921
4440
  console.log("");
@@ -1964,6 +4483,15 @@ async function main() {
1964
4483
  if (compareMode) {
1965
4484
  runCompare(score);
1966
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
+ }
1967
4495
  console.log("");
1968
4496
  console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
1969
4497
  console.log("");
@@ -1993,8 +4521,8 @@ async function main() {
1993
4521
  }
1994
4522
  }
1995
4523
  async function runFix(projectPath, analysis, score) {
1996
- const ctoDir = join4(projectPath, ".cto");
1997
- mkdirSync(ctoDir, { recursive: true });
4524
+ const ctoDir = join8(projectPath, ".cto");
4525
+ mkdirSync3(ctoDir, { recursive: true });
1998
4526
  const selection = await selectContext({
1999
4527
  task: "general code review and refactoring",
2000
4528
  analysis,
@@ -2052,7 +4580,7 @@ async function runFix(projectPath, analysis, score) {
2052
4580
  `;
2053
4581
  contextMd += `## Savings: ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)})
2054
4582
  `;
2055
- writeFileSync(join4(ctoDir, "context.md"), contextMd);
4583
+ writeFileSync2(join8(ctoDir, "context.md"), contextMd);
2056
4584
  const config = {
2057
4585
  version: "3.0",
2058
4586
  project: analysis.projectName,
@@ -2080,7 +4608,7 @@ async function runFix(projectPath, analysis, score) {
2080
4608
  impact: i.impact
2081
4609
  }))
2082
4610
  };
2083
- writeFileSync(join4(ctoDir, "config.json"), JSON.stringify(config, null, 2));
4611
+ writeFileSync2(join8(ctoDir, "config.json"), JSON.stringify(config, null, 2));
2084
4612
  const ignoreContent = [
2085
4613
  "# CTO AI-ignore \u2014 files that add noise to AI context",
2086
4614
  "# Generated by cto-ai-cli",
@@ -2106,7 +4634,7 @@ async function runFix(projectPath, analysis, score) {
2106
4634
  }).slice(0, 20).map((o) => `${o} # orphan, low-risk`),
2107
4635
  ""
2108
4636
  ].join("\n");
2109
- writeFileSync(join4(ctoDir, ".cteignore"), ignoreContent);
4637
+ writeFileSync2(join8(ctoDir, ".cteignore"), ignoreContent);
2110
4638
  console.log("");
2111
4639
  console.log(" \u2705 Auto-fix complete! Generated:");
2112
4640
  console.log("");
@@ -2119,8 +4647,8 @@ async function runFix(projectPath, analysis, score) {
2119
4647
  console.log("");
2120
4648
  }
2121
4649
  async function runContext(projectPath, analysis, task) {
2122
- const ctoDir = join4(projectPath, ".cto");
2123
- mkdirSync(ctoDir, { recursive: true });
4650
+ const ctoDir = join8(projectPath, ".cto");
4651
+ mkdirSync3(ctoDir, { recursive: true });
2124
4652
  const selection = await selectContext({
2125
4653
  task,
2126
4654
  analysis,
@@ -2200,12 +4728,14 @@ async function runContext(projectPath, analysis, task) {
2200
4728
  const fullFile = analysis.files.find((f) => f.relativePath === sf.relativePath);
2201
4729
  if (!fullFile) continue;
2202
4730
  try {
2203
- const content = readFileSync(fullFile.path, "utf-8");
4731
+ const content = readFileSync4(fullFile.path, "utf-8");
2204
4732
  const ext = fullFile.extension.replace(".", "");
2205
4733
  contextMd += `### ${sf.relativePath}
2206
4734
  `;
4735
+ const maxChars = 5e3;
4736
+ const truncated = content.length > maxChars;
2207
4737
  contextMd += `\`\`\`${ext}
2208
- ${content.slice(0, 5e3)}
4738
+ ${content.slice(0, maxChars)}${truncated ? "\n// ... [truncated \u2014 " + (content.length - maxChars) + " chars omitted]" : ""}
2209
4739
  \`\`\`
2210
4740
 
2211
4741
  `;
@@ -2214,7 +4744,7 @@ ${content.slice(0, 5e3)}
2214
4744
  }
2215
4745
  const safeName = task.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 40);
2216
4746
  const filename = `context-${safeName}.md`;
2217
- writeFileSync(join4(ctoDir, filename), contextMd);
4747
+ writeFileSync2(join8(ctoDir, filename), contextMd);
2218
4748
  console.log("");
2219
4749
  console.log(` \u2705 Task context generated!`);
2220
4750
  console.log("");
@@ -2232,9 +4762,10 @@ async function runReport(projectPath, analysis, score) {
2232
4762
  let report = `# CTO Context Score\u2122 Report
2233
4763
 
2234
4764
  `;
4765
+ const safeGrade = encodeURIComponent(score.grade);
2235
4766
  report += `![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor}?style=for-the-badge)
2236
4767
  `;
2237
- report += `![Grade](https://img.shields.io/badge/Grade-${score.grade}-${gradeColor}?style=for-the-badge)
4768
+ report += `![Grade](https://img.shields.io/badge/Grade-${safeGrade}-${gradeColor}?style=for-the-badge)
2238
4769
 
2239
4770
  `;
2240
4771
  report += `> Generated by [cto-ai-cli](https://npmjs.com/package/cto-ai-cli) on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
@@ -2306,9 +4837,9 @@ async function runReport(projectPath, analysis, score) {
2306
4837
  `;
2307
4838
  report += `*Run \`npx cto-ai-cli\` to generate your own report. [Learn more](https://npmjs.com/package/cto-ai-cli)*
2308
4839
  `;
2309
- const ctoDir = join4(projectPath, ".cto");
2310
- mkdirSync(ctoDir, { recursive: true });
2311
- writeFileSync(join4(ctoDir, "report.md"), report);
4840
+ const ctoDir = join8(projectPath, ".cto");
4841
+ mkdirSync3(ctoDir, { recursive: true });
4842
+ writeFileSync2(join8(ctoDir, "report.md"), report);
2312
4843
  console.log("");
2313
4844
  console.log(" \u2705 Report generated!");
2314
4845
  console.log("");
@@ -2365,9 +4896,256 @@ function renderCompareBar(pct) {
2365
4896
  const empty = width - filled;
2366
4897
  return "\u2588".repeat(filled) + "\u2591".repeat(empty);
2367
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.");
4909
+ console.log("");
4910
+ return;
4911
+ }
4912
+ console.log("");
4913
+ console.log(" \u{1F50D} Running security audit...");
4914
+ 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`);
4936
+ }
4937
+ if (summary.bySeverity.high > 0) {
4938
+ console.log(` \u2551 \u{1F7E0} High: ${summary.bySeverity.high.toString().padEnd(33)} \u2551`);
4939
+ }
4940
+ if (summary.bySeverity.medium > 0) {
4941
+ console.log(` \u2551 \u{1F7E1} Medium: ${summary.bySeverity.medium.toString().padEnd(33)} \u2551`);
4942
+ }
4943
+ if (summary.bySeverity.low > 0) {
4944
+ console.log(` \u2551 \u{1F535} Low: ${summary.bySeverity.low.toString().padEnd(33)} \u2551`);
4945
+ }
4946
+ if (summary.totalFindings === 0) {
4947
+ console.log(" \u2551 \u2705 No secrets or PII detected \u2551");
4948
+ }
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`);
4957
+ }
4958
+ console.log(" \u2551 \u2551");
4959
+ }
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}`);
4971
+ }
4972
+ if (findings.length > 15) {
4973
+ console.log(` ... and ${findings.length - 15} more (see .cto/audit/ for full report)`);
4974
+ }
4975
+ }
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}`);
4983
+ }
4984
+ }
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
+ }))
5008
+ };
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
+ `;
5047
+ }
5048
+ report += "\n";
5049
+ }
5050
+ if (recommendations.length > 0) {
5051
+ report += `## Recommendations
5052
+
5053
+ `;
5054
+ for (const rec of recommendations) {
5055
+ report += `- ${rec}
5056
+ `;
5057
+ }
5058
+ }
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);
5072
+ }
5073
+ }
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);
5082
+ }
5083
+ }
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");
5090
+ }
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);
5095
+ }
5096
+ }
2368
5097
  function formatTokens(n) {
2369
5098
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2370
5099
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
2371
5100
  return n.toString();
2372
5101
  }
5102
+ async function runMonorepo(projectPath, analysis, targetPackage, jsonMode) {
5103
+ const mono = await analyzeMonorepo(projectPath, analysis);
5104
+ 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
+ }
5111
+ return;
5112
+ }
5113
+ console.log(renderMonorepoAnalysis(mono));
5114
+ if (targetPackage && mono.detected) {
5115
+ const pkgCtx = selectPackageContext(mono, targetPackage);
5116
+ console.log(renderPackageContext(pkgCtx));
5117
+ }
5118
+ }
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;
5128
+ }
5129
+ 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");
5133
+ 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}`);
5137
+ }
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`);
5142
+ }
5143
+ console.log("");
5144
+ console.log(" \u{1F4CB} Baseline saved to .cto/baseline.json");
5145
+ console.log("");
5146
+ if (!result.passed) {
5147
+ console.log(" \u274C Quality gate failed. Fix the issues above and re-run.");
5148
+ process.exit(1);
5149
+ }
5150
+ }
2373
5151
  main();