cto-ai-cli 5.2.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -316
- package/dist/cli/index.js +6306 -5691
- package/dist/engine/index.d.ts +690 -730
- package/dist/engine/index.js +2415 -4721
- package/dist/mcp/index.js +3313 -15036
- package/package.json +9 -43
- package/DOCS.md +0 -902
- package/dist/action/index.js +0 -26395
- package/dist/api/dashboard.js +0 -2276
- package/dist/api/dashboard.js.map +0 -1
- package/dist/api/server.js +0 -3663
- package/dist/api/server.js.map +0 -1
- package/dist/cli/gateway.js +0 -3054
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/score.js +0 -6352
- package/dist/cli/v2/index.d.ts +0 -2
- package/dist/cli/v2/index.js +0 -3695
- package/dist/cli/v2/index.js.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/fsevents-X6WP4TKM.node +0 -0
- package/dist/gateway/index.d.ts +0 -281
- package/dist/gateway/index.js +0 -2932
- package/dist/gateway/index.js.map +0 -1
- package/dist/govern/index.d.ts +0 -325
- package/dist/govern/index.js +0 -1101
- package/dist/govern/index.js.map +0 -1
- package/dist/interact/index.d.ts +0 -234
- package/dist/interact/index.js +0 -1542
- package/dist/interact/index.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -2
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/v2.d.ts +0 -2
- package/dist/mcp/v2.js +0 -18492
- package/dist/mcp/v2.js.map +0 -1
package/dist/interact/index.js
DELETED
|
@@ -1,1542 +0,0 @@
|
|
|
1
|
-
// src/interact/orchestrator.ts
|
|
2
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3
|
-
|
|
4
|
-
// src/engine/selector.ts
|
|
5
|
-
import { createHash as createHash2 } from "crypto";
|
|
6
|
-
|
|
7
|
-
// src/govern/secrets.ts
|
|
8
|
-
import { readFile } from "fs/promises";
|
|
9
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
10
|
-
import { resolve, relative, join, dirname } from "path";
|
|
11
|
-
import { createHash } from "crypto";
|
|
12
|
-
var BUILTIN_PATTERNS = [
|
|
13
|
-
// API Keys
|
|
14
|
-
{ type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
|
|
15
|
-
{ type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
|
|
16
|
-
{ type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
|
|
17
|
-
// AWS
|
|
18
|
-
{ type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
|
|
19
|
-
{ 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" },
|
|
20
|
-
// Private Keys
|
|
21
|
-
{ type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
|
|
22
|
-
{ type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
|
|
23
|
-
// Passwords
|
|
24
|
-
{ type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
|
|
25
|
-
{ type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
|
|
26
|
-
// Tokens
|
|
27
|
-
{ 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" },
|
|
28
|
-
{ type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
|
|
29
|
-
{ type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
|
|
30
|
-
{ type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
|
|
31
|
-
{ type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
|
|
32
|
-
// Connection strings
|
|
33
|
-
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
34
|
-
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
35
|
-
// Environment variables with secrets
|
|
36
|
-
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
|
|
37
|
-
// Stripe
|
|
38
|
-
{ type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
|
|
39
|
-
{ type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
|
|
40
|
-
{ type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
|
|
41
|
-
// Slack
|
|
42
|
-
{ type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
|
|
43
|
-
{ type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
|
|
44
|
-
{ 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" },
|
|
45
|
-
// Google
|
|
46
|
-
{ type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
|
|
47
|
-
{ type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
|
|
48
|
-
// Azure
|
|
49
|
-
{ type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
|
|
50
|
-
// Twilio
|
|
51
|
-
{ type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
|
|
52
|
-
// SendGrid
|
|
53
|
-
{ type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
|
|
54
|
-
// JWT
|
|
55
|
-
{ 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" },
|
|
56
|
-
// Datadog
|
|
57
|
-
{ type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
|
|
58
|
-
{ type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
|
|
59
|
-
// Sentry
|
|
60
|
-
{ type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
|
|
61
|
-
// Firebase
|
|
62
|
-
{ type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
|
|
63
|
-
{ type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
|
|
64
|
-
// Supabase
|
|
65
|
-
{ type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
|
|
66
|
-
{ type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
|
|
67
|
-
// Vercel
|
|
68
|
-
{ type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
|
|
69
|
-
// Heroku
|
|
70
|
-
{ type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
|
|
71
|
-
// DigitalOcean
|
|
72
|
-
{ type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
|
|
73
|
-
{ type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
|
|
74
|
-
// Mailgun
|
|
75
|
-
{ type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
|
|
76
|
-
// PII
|
|
77
|
-
{ 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)" },
|
|
78
|
-
{ 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)" }
|
|
79
|
-
];
|
|
80
|
-
var _cachedBuiltinPatterns = null;
|
|
81
|
-
function getBuiltinPatterns() {
|
|
82
|
-
if (!_cachedBuiltinPatterns) {
|
|
83
|
-
_cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
|
|
84
|
-
type: def.type,
|
|
85
|
-
pattern: new RegExp(def.source, def.flags),
|
|
86
|
-
severity: def.severity,
|
|
87
|
-
description: def.description
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
return _cachedBuiltinPatterns;
|
|
91
|
-
}
|
|
92
|
-
function buildPatterns(customPatterns = []) {
|
|
93
|
-
const builtins = getBuiltinPatterns();
|
|
94
|
-
if (customPatterns.length === 0) return builtins;
|
|
95
|
-
const patterns = [...builtins];
|
|
96
|
-
for (const custom of customPatterns) {
|
|
97
|
-
try {
|
|
98
|
-
patterns.push({
|
|
99
|
-
type: "custom",
|
|
100
|
-
pattern: new RegExp(custom, "gi"),
|
|
101
|
-
severity: "medium",
|
|
102
|
-
description: `Custom pattern: ${custom}`
|
|
103
|
-
});
|
|
104
|
-
} catch {
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return patterns;
|
|
108
|
-
}
|
|
109
|
-
function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
|
|
110
|
-
const findings = [];
|
|
111
|
-
const lines = content.split("\n");
|
|
112
|
-
const allPatterns = buildPatterns(customPatterns);
|
|
113
|
-
for (const secretPattern of allPatterns) {
|
|
114
|
-
for (let i = 0; i < lines.length; i++) {
|
|
115
|
-
const line = lines[i];
|
|
116
|
-
secretPattern.pattern.lastIndex = 0;
|
|
117
|
-
let match;
|
|
118
|
-
while ((match = secretPattern.pattern.exec(line)) !== null) {
|
|
119
|
-
const matchText = match[0];
|
|
120
|
-
if (isTemplateOrPlaceholder(matchText)) continue;
|
|
121
|
-
if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
|
|
122
|
-
findings.push({
|
|
123
|
-
type: secretPattern.type,
|
|
124
|
-
file: filePath,
|
|
125
|
-
line: i + 1,
|
|
126
|
-
match: matchText,
|
|
127
|
-
redacted: redactSecret(matchText),
|
|
128
|
-
severity: secretPattern.severity
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return deduplicateFindings(findings);
|
|
134
|
-
}
|
|
135
|
-
async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
|
|
136
|
-
try {
|
|
137
|
-
const content = await readFile(filePath, "utf-8");
|
|
138
|
-
const relPath = relative(resolve(projectPath), resolve(filePath));
|
|
139
|
-
return scanContentForSecrets(content, relPath, customPatterns);
|
|
140
|
-
} catch {
|
|
141
|
-
return [];
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function redactSecret(value) {
|
|
145
|
-
if (value.length <= 8) return "***REDACTED***";
|
|
146
|
-
const prefix = value.substring(0, 4);
|
|
147
|
-
const suffix = value.substring(value.length - 2);
|
|
148
|
-
return `${prefix}${"*".repeat(Math.min(value.length - 6, 20))}${suffix}`;
|
|
149
|
-
}
|
|
150
|
-
function isTemplateOrPlaceholder(value) {
|
|
151
|
-
const placeholders = [
|
|
152
|
-
/\$\{.*\}/,
|
|
153
|
-
/\{\{.*\}\}/,
|
|
154
|
-
/%[sd]/,
|
|
155
|
-
/<[A-Z_]+>/,
|
|
156
|
-
/YOUR_.*_HERE/i,
|
|
157
|
-
/\bCHANGE_ME\b/i,
|
|
158
|
-
/\bPLACEHOLDER\b/i,
|
|
159
|
-
/\bexample\b/i,
|
|
160
|
-
/\bTODO\b/i,
|
|
161
|
-
/xxx+/i,
|
|
162
|
-
/\breplace.?me\b/i,
|
|
163
|
-
/\bdummy\b/i,
|
|
164
|
-
/\btest_?key\b/i,
|
|
165
|
-
/\bsample\b/i
|
|
166
|
-
];
|
|
167
|
-
return placeholders.some((p) => p.test(value));
|
|
168
|
-
}
|
|
169
|
-
var PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
170
|
-
"example.com",
|
|
171
|
-
"example.org",
|
|
172
|
-
"example.net",
|
|
173
|
-
"test.com",
|
|
174
|
-
"test.org",
|
|
175
|
-
"test.net",
|
|
176
|
-
"localhost",
|
|
177
|
-
"localhost.localdomain",
|
|
178
|
-
"email.com",
|
|
179
|
-
"mail.com",
|
|
180
|
-
"foo.com",
|
|
181
|
-
"bar.com",
|
|
182
|
-
"baz.com",
|
|
183
|
-
"acme.com",
|
|
184
|
-
"company.com",
|
|
185
|
-
"corp.com",
|
|
186
|
-
"noreply.com",
|
|
187
|
-
"no-reply.com",
|
|
188
|
-
"users.noreply.github.com",
|
|
189
|
-
"placeholder.com"
|
|
190
|
-
]);
|
|
191
|
-
function isSafeEmail(value, extraDomains) {
|
|
192
|
-
const match = value.match(/@([a-zA-Z0-9.-]+)$/);
|
|
193
|
-
if (!match) return false;
|
|
194
|
-
const domain = match[1].toLowerCase();
|
|
195
|
-
if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
|
|
196
|
-
if (extraDomains && extraDomains.has(domain)) return true;
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
function deduplicateFindings(findings) {
|
|
200
|
-
const seen = /* @__PURE__ */ new Set();
|
|
201
|
-
return findings.filter((f) => {
|
|
202
|
-
const key = `${f.file}:${f.line}:${f.type}:${f.match}`;
|
|
203
|
-
if (seen.has(key)) return false;
|
|
204
|
-
seen.add(key);
|
|
205
|
-
return true;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/engine/pruner.ts
|
|
210
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
211
|
-
|
|
212
|
-
// src/engine/tokenizer.ts
|
|
213
|
-
import { encodingForModel } from "js-tiktoken";
|
|
214
|
-
import { readFile as readFile2, stat } from "fs/promises";
|
|
215
|
-
var CHARS_PER_TOKEN = 4;
|
|
216
|
-
function countTokensChars4(sizeInBytes) {
|
|
217
|
-
return Math.ceil(sizeInBytes / CHARS_PER_TOKEN);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// src/engine/pruner.ts
|
|
221
|
-
var TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
222
|
-
async function pruneFile(file, level) {
|
|
223
|
-
if (level === "excluded") {
|
|
224
|
-
return emptyResult(file, "excluded");
|
|
225
|
-
}
|
|
226
|
-
if (level === "full") {
|
|
227
|
-
return fullContent(file);
|
|
228
|
-
}
|
|
229
|
-
const ext = file.extension.toLowerCase();
|
|
230
|
-
const isTS = TS_EXTENSIONS.has(ext);
|
|
231
|
-
if (isTS) {
|
|
232
|
-
return pruneTypeScript(file, level);
|
|
233
|
-
}
|
|
234
|
-
return pruneGeneric(file, level);
|
|
235
|
-
}
|
|
236
|
-
async function pruneTypeScript(file, level) {
|
|
237
|
-
let content;
|
|
238
|
-
try {
|
|
239
|
-
content = await readFile3(file.path, "utf-8");
|
|
240
|
-
} catch {
|
|
241
|
-
return emptyResult(file, level);
|
|
242
|
-
}
|
|
243
|
-
const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
|
|
244
|
-
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
245
|
-
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
246
|
-
return {
|
|
247
|
-
relativePath: file.relativePath,
|
|
248
|
-
originalTokens: file.tokens,
|
|
249
|
-
prunedTokens,
|
|
250
|
-
pruneLevel: level,
|
|
251
|
-
content: prunedContent,
|
|
252
|
-
savingsPercent: Math.max(0, savingsPercent)
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
function extractSignaturesRegex(content) {
|
|
256
|
-
const lines = content.split("\n");
|
|
257
|
-
const parts = [];
|
|
258
|
-
let i = 0;
|
|
259
|
-
while (i < lines.length) {
|
|
260
|
-
const line = lines[i];
|
|
261
|
-
const trimmed = line.trim();
|
|
262
|
-
if (trimmed === "") {
|
|
263
|
-
i++;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (trimmed.startsWith("/**")) {
|
|
267
|
-
const docLines = [];
|
|
268
|
-
while (i < lines.length) {
|
|
269
|
-
docLines.push(lines[i]);
|
|
270
|
-
if (lines[i].includes("*/")) {
|
|
271
|
-
i++;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
i++;
|
|
275
|
-
}
|
|
276
|
-
parts.push(docLines.join("\n"));
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (trimmed.startsWith("//")) {
|
|
280
|
-
parts.push(line);
|
|
281
|
-
i++;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
|
|
285
|
-
const block = collectBracedLine(lines, i);
|
|
286
|
-
parts.push(block.text);
|
|
287
|
-
i = block.nextIndex;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
|
|
291
|
-
const block = collectBracedLine(lines, i);
|
|
292
|
-
parts.push(block.text);
|
|
293
|
-
i = block.nextIndex;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
|
|
297
|
-
const block = collectBalanced(lines, i);
|
|
298
|
-
parts.push(block.text);
|
|
299
|
-
i = block.nextIndex;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
|
|
303
|
-
const block = collectBalanced(lines, i);
|
|
304
|
-
parts.push(block.text);
|
|
305
|
-
i = block.nextIndex;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
309
|
-
const block = collectBalanced(lines, i);
|
|
310
|
-
parts.push(block.text);
|
|
311
|
-
i = block.nextIndex;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
|
|
315
|
-
if (fnMatch) {
|
|
316
|
-
const sig = extractFnSignature(lines, i);
|
|
317
|
-
parts.push(`${sig} { /* ... */ }`);
|
|
318
|
-
i = skipBlock(lines, i);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
|
|
322
|
-
if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
|
|
323
|
-
const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
|
|
324
|
-
if (prefix) {
|
|
325
|
-
parts.push(`${prefix} /* ... */;`);
|
|
326
|
-
}
|
|
327
|
-
i = skipBlock(lines, i);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
if (arrowMatch) {
|
|
331
|
-
const block = collectStatement(lines, i);
|
|
332
|
-
parts.push(block.text);
|
|
333
|
-
i = block.nextIndex;
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
|
|
337
|
-
const classOutline = extractClassOutline(lines, i);
|
|
338
|
-
parts.push(classOutline.text);
|
|
339
|
-
i = classOutline.nextIndex;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
i++;
|
|
343
|
-
}
|
|
344
|
-
return parts.join("\n");
|
|
345
|
-
}
|
|
346
|
-
function extractSkeletonRegex(content) {
|
|
347
|
-
const lines = content.split("\n");
|
|
348
|
-
const parts = [];
|
|
349
|
-
let i = 0;
|
|
350
|
-
while (i < lines.length) {
|
|
351
|
-
const trimmed = lines[i].trim();
|
|
352
|
-
if (/^import\s/.test(trimmed)) {
|
|
353
|
-
const block = collectBracedLine(lines, i);
|
|
354
|
-
parts.push(block.text);
|
|
355
|
-
i = block.nextIndex;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
|
|
359
|
-
const block = collectBalanced(lines, i);
|
|
360
|
-
parts.push(block.text);
|
|
361
|
-
i = block.nextIndex;
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
365
|
-
const block = collectBalanced(lines, i);
|
|
366
|
-
parts.push(block.text);
|
|
367
|
-
i = block.nextIndex;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
|
|
371
|
-
const sig = extractFnSignature(lines, i);
|
|
372
|
-
parts.push(`${sig};`);
|
|
373
|
-
i = skipBlock(lines, i);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
|
|
377
|
-
const nameMatch = trimmed.match(/class\s+(\w+)/);
|
|
378
|
-
const name = nameMatch?.[1] ?? "Unknown";
|
|
379
|
-
const end = skipBlock(lines, i);
|
|
380
|
-
const methods = [];
|
|
381
|
-
for (let j = i + 1; j < end; j++) {
|
|
382
|
-
const mt = lines[j].trim();
|
|
383
|
-
const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
|
|
384
|
-
if (mm && mm[1] !== "constructor") methods.push(mm[1]);
|
|
385
|
-
}
|
|
386
|
-
parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
|
|
387
|
-
i = end;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (/^export\s*(\{|\*)/.test(trimmed)) {
|
|
391
|
-
const block = collectBracedLine(lines, i);
|
|
392
|
-
parts.push(block.text);
|
|
393
|
-
i = block.nextIndex;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
i++;
|
|
397
|
-
}
|
|
398
|
-
return parts.join("\n");
|
|
399
|
-
}
|
|
400
|
-
function collectBracedLine(lines, start) {
|
|
401
|
-
let text = lines[start];
|
|
402
|
-
let i = start + 1;
|
|
403
|
-
while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
|
|
404
|
-
text += "\n" + lines[i];
|
|
405
|
-
i++;
|
|
406
|
-
}
|
|
407
|
-
return { text, nextIndex: i };
|
|
408
|
-
}
|
|
409
|
-
function collectBalanced(lines, start) {
|
|
410
|
-
let depth = 0;
|
|
411
|
-
let text = "";
|
|
412
|
-
let i = start;
|
|
413
|
-
let started = false;
|
|
414
|
-
while (i < lines.length) {
|
|
415
|
-
const line = lines[i];
|
|
416
|
-
text += (text ? "\n" : "") + line;
|
|
417
|
-
for (const ch of line) {
|
|
418
|
-
if (ch === "{" || ch === "(") {
|
|
419
|
-
depth++;
|
|
420
|
-
started = true;
|
|
421
|
-
}
|
|
422
|
-
if (ch === "}" || ch === ")") depth--;
|
|
423
|
-
}
|
|
424
|
-
i++;
|
|
425
|
-
if (started && depth <= 0) break;
|
|
426
|
-
if (!started && line.includes(";")) break;
|
|
427
|
-
}
|
|
428
|
-
return { text, nextIndex: i };
|
|
429
|
-
}
|
|
430
|
-
function collectStatement(lines, start) {
|
|
431
|
-
let text = lines[start];
|
|
432
|
-
let i = start + 1;
|
|
433
|
-
if (text.includes(";")) return { text, nextIndex: i };
|
|
434
|
-
let depth = 0;
|
|
435
|
-
for (const ch of text) {
|
|
436
|
-
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
437
|
-
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
438
|
-
}
|
|
439
|
-
while (i < lines.length && depth > 0) {
|
|
440
|
-
text += "\n" + lines[i];
|
|
441
|
-
for (const ch of lines[i]) {
|
|
442
|
-
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
443
|
-
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
444
|
-
}
|
|
445
|
-
i++;
|
|
446
|
-
}
|
|
447
|
-
return { text, nextIndex: i };
|
|
448
|
-
}
|
|
449
|
-
function extractFnSignature(lines, start) {
|
|
450
|
-
let sig = "";
|
|
451
|
-
let i = start;
|
|
452
|
-
while (i < lines.length) {
|
|
453
|
-
const line = lines[i].trim();
|
|
454
|
-
sig += (sig ? " " : "") + line;
|
|
455
|
-
if (line.includes("{")) {
|
|
456
|
-
sig = sig.replace(/\s*\{[^]*$/, "").trim();
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
i++;
|
|
460
|
-
}
|
|
461
|
-
return sig;
|
|
462
|
-
}
|
|
463
|
-
function skipBlock(lines, start) {
|
|
464
|
-
let depth = 0;
|
|
465
|
-
let i = start;
|
|
466
|
-
let foundBrace = false;
|
|
467
|
-
while (i < lines.length) {
|
|
468
|
-
for (const ch of lines[i]) {
|
|
469
|
-
if (ch === "{") {
|
|
470
|
-
depth++;
|
|
471
|
-
foundBrace = true;
|
|
472
|
-
}
|
|
473
|
-
if (ch === "}") depth--;
|
|
474
|
-
}
|
|
475
|
-
i++;
|
|
476
|
-
if (foundBrace && depth <= 0) break;
|
|
477
|
-
if (!foundBrace && lines[i - 1].includes(";")) break;
|
|
478
|
-
}
|
|
479
|
-
return i;
|
|
480
|
-
}
|
|
481
|
-
function looksLikeFunctionDecl(lines, start) {
|
|
482
|
-
const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
|
|
483
|
-
return /=>/.test(chunk) || /=\s*function/.test(chunk);
|
|
484
|
-
}
|
|
485
|
-
function extractClassOutline(lines, start) {
|
|
486
|
-
const header = lines[start].trim();
|
|
487
|
-
let headerText = header;
|
|
488
|
-
let i = start + 1;
|
|
489
|
-
if (!header.includes("{")) {
|
|
490
|
-
while (i < lines.length) {
|
|
491
|
-
headerText += " " + lines[i].trim();
|
|
492
|
-
if (lines[i].includes("{")) {
|
|
493
|
-
i++;
|
|
494
|
-
break;
|
|
495
|
-
}
|
|
496
|
-
i++;
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
i = start + 1;
|
|
500
|
-
}
|
|
501
|
-
const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
|
|
502
|
-
let depth = 1;
|
|
503
|
-
while (i < lines.length && depth > 0) {
|
|
504
|
-
const line = lines[i];
|
|
505
|
-
const trimmed = line.trim();
|
|
506
|
-
for (const ch of line) {
|
|
507
|
-
if (ch === "{") depth++;
|
|
508
|
-
if (ch === "}") depth--;
|
|
509
|
-
}
|
|
510
|
-
if (depth <= 0) {
|
|
511
|
-
i++;
|
|
512
|
-
break;
|
|
513
|
-
}
|
|
514
|
-
if (depth === 1) {
|
|
515
|
-
if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
|
|
516
|
-
bodyParts.push(` ${trimmed}`);
|
|
517
|
-
} else if (/^constructor\s*\(/.test(trimmed)) {
|
|
518
|
-
const sig = extractFnSignature(lines, i);
|
|
519
|
-
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
520
|
-
} else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
521
|
-
const sig = extractFnSignature(lines, i);
|
|
522
|
-
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
i++;
|
|
526
|
-
}
|
|
527
|
-
bodyParts.push("}");
|
|
528
|
-
return { text: bodyParts.join("\n"), nextIndex: i };
|
|
529
|
-
}
|
|
530
|
-
async function pruneGeneric(file, level) {
|
|
531
|
-
let content;
|
|
532
|
-
try {
|
|
533
|
-
content = await readFile3(file.path, "utf-8");
|
|
534
|
-
} catch {
|
|
535
|
-
return emptyResult(file, level);
|
|
536
|
-
}
|
|
537
|
-
return pruneGenericFromContent(file, content, level);
|
|
538
|
-
}
|
|
539
|
-
function pruneGenericFromContent(file, content, level) {
|
|
540
|
-
const lines = content.split("\n");
|
|
541
|
-
let result;
|
|
542
|
-
if (level === "signatures") {
|
|
543
|
-
result = lines.filter((line) => {
|
|
544
|
-
const t = line.trim();
|
|
545
|
-
return t === "" || t.startsWith("#") || t.startsWith("//") || t.startsWith("import ") || t.startsWith("from ") || t.startsWith("export ") || t.startsWith("def ") || t.startsWith("async def ") || t.startsWith("class ") || t.startsWith("function ") || t.startsWith("const ") || t.startsWith("let ") || t.startsWith("var ") || /^(pub |fn |struct |enum |impl |mod |use )/.test(t);
|
|
546
|
-
});
|
|
547
|
-
} else {
|
|
548
|
-
result = lines.filter((line) => {
|
|
549
|
-
const t = line.trim();
|
|
550
|
-
return t.startsWith("import ") || t.startsWith("from ") || t.startsWith("export ") || t.startsWith("def ") || t.startsWith("class ") || t.startsWith("function ") || /^(pub |fn |struct |enum |mod |use )/.test(t);
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
const prunedContent = result.join("\n");
|
|
554
|
-
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
555
|
-
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
556
|
-
return {
|
|
557
|
-
relativePath: file.relativePath,
|
|
558
|
-
originalTokens: file.tokens,
|
|
559
|
-
prunedTokens,
|
|
560
|
-
pruneLevel: level,
|
|
561
|
-
content: prunedContent,
|
|
562
|
-
savingsPercent: Math.max(0, savingsPercent)
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
async function fullContent(file) {
|
|
566
|
-
let content = "";
|
|
567
|
-
try {
|
|
568
|
-
content = await readFile3(file.path, "utf-8");
|
|
569
|
-
} catch {
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
relativePath: file.relativePath,
|
|
573
|
-
originalTokens: file.tokens,
|
|
574
|
-
prunedTokens: file.tokens,
|
|
575
|
-
pruneLevel: "full",
|
|
576
|
-
content,
|
|
577
|
-
savingsPercent: 0
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
function emptyResult(file, level) {
|
|
581
|
-
return {
|
|
582
|
-
relativePath: file.relativePath,
|
|
583
|
-
originalTokens: file.tokens,
|
|
584
|
-
prunedTokens: 0,
|
|
585
|
-
pruneLevel: level,
|
|
586
|
-
content: "",
|
|
587
|
-
savingsPercent: 100
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// src/engine/graph-utils.ts
|
|
592
|
-
function buildAdjacencyList(edges) {
|
|
593
|
-
const forward = /* @__PURE__ */ new Map();
|
|
594
|
-
const reverse = /* @__PURE__ */ new Map();
|
|
595
|
-
for (const edge of edges) {
|
|
596
|
-
if (!forward.has(edge.from)) forward.set(edge.from, []);
|
|
597
|
-
forward.get(edge.from).push(edge.to);
|
|
598
|
-
if (!reverse.has(edge.to)) reverse.set(edge.to, []);
|
|
599
|
-
reverse.get(edge.to).push(edge.from);
|
|
600
|
-
}
|
|
601
|
-
return { forward, reverse };
|
|
602
|
-
}
|
|
603
|
-
function bfsBidirectional(seeds, adj, depth) {
|
|
604
|
-
const result = new Set(seeds);
|
|
605
|
-
let frontier = [...seeds];
|
|
606
|
-
const visited = /* @__PURE__ */ new Set();
|
|
607
|
-
for (let d = 0; d < depth; d++) {
|
|
608
|
-
const nextFrontier = [];
|
|
609
|
-
for (const node of frontier) {
|
|
610
|
-
if (visited.has(node)) continue;
|
|
611
|
-
visited.add(node);
|
|
612
|
-
const fwd = adj.forward.get(node);
|
|
613
|
-
if (fwd) {
|
|
614
|
-
for (const neighbor of fwd) {
|
|
615
|
-
if (!visited.has(neighbor)) {
|
|
616
|
-
result.add(neighbor);
|
|
617
|
-
nextFrontier.push(neighbor);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const rev = adj.reverse.get(node);
|
|
622
|
-
if (rev) {
|
|
623
|
-
for (const neighbor of rev) {
|
|
624
|
-
if (!visited.has(neighbor)) {
|
|
625
|
-
result.add(neighbor);
|
|
626
|
-
nextFrontier.push(neighbor);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
frontier = nextFrontier;
|
|
632
|
-
}
|
|
633
|
-
return result;
|
|
634
|
-
}
|
|
635
|
-
function matchGlob(path, pattern) {
|
|
636
|
-
const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\xA7\xA7").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, ".");
|
|
637
|
-
try {
|
|
638
|
-
return new RegExp(`^${regexStr}$`).test(path);
|
|
639
|
-
} catch {
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// src/engine/coverage.ts
|
|
645
|
-
function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
|
|
646
|
-
const adj = buildAdjacencyList(graph.edges);
|
|
647
|
-
const relevantSet = targetPaths.length > 0 ? bfsBidirectional(targetPaths, adj, depth) : /* @__PURE__ */ new Set();
|
|
648
|
-
const includedSet = new Set(includedPaths);
|
|
649
|
-
const tempFileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
|
|
650
|
-
for (const path of includedPaths) {
|
|
651
|
-
const file = tempFileMap.get(path);
|
|
652
|
-
if (!file) continue;
|
|
653
|
-
for (const imp of file.imports) {
|
|
654
|
-
const impFile = tempFileMap.get(imp);
|
|
655
|
-
if (impFile && impFile.kind === "type") {
|
|
656
|
-
relevantSet.add(imp);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const relevantFiles = Array.from(relevantSet);
|
|
661
|
-
const includedRelevant = relevantFiles.filter((f) => includedSet.has(f));
|
|
662
|
-
const missingRelevant = relevantFiles.filter((f) => !includedSet.has(f));
|
|
663
|
-
const missingCritical = missingRelevant.filter((f) => {
|
|
664
|
-
const file = tempFileMap.get(f);
|
|
665
|
-
return file && (file.exclusionImpact === "critical" || file.exclusionImpact === "high");
|
|
666
|
-
});
|
|
667
|
-
const fileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
|
|
668
|
-
let totalRelevantRisk = 0;
|
|
669
|
-
let includedRelevantRisk = 0;
|
|
670
|
-
for (const f of relevantFiles) {
|
|
671
|
-
const risk = fileMap.get(f)?.riskScore ?? 1;
|
|
672
|
-
totalRelevantRisk += risk;
|
|
673
|
-
if (includedSet.has(f)) {
|
|
674
|
-
includedRelevantRisk += risk;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
const score = totalRelevantRisk > 0 ? Math.round(includedRelevantRisk / totalRelevantRisk * 100) : relevantFiles.length > 0 ? Math.round(includedRelevant.length / relevantFiles.length * 100) : 100;
|
|
678
|
-
let explanation;
|
|
679
|
-
if (score >= 90) {
|
|
680
|
-
explanation = `Excellent coverage (${score}%): AI has nearly all relevant context.`;
|
|
681
|
-
} else if (score >= 70) {
|
|
682
|
-
explanation = `Good coverage (${score}%): Most relevant files included.`;
|
|
683
|
-
if (missingCritical.length > 0) {
|
|
684
|
-
explanation += ` Warning: ${missingCritical.length} critical file(s) missing.`;
|
|
685
|
-
}
|
|
686
|
-
} else if (score >= 50) {
|
|
687
|
-
explanation = `Partial coverage (${score}%): Significant context is missing.`;
|
|
688
|
-
if (missingCritical.length > 0) {
|
|
689
|
-
explanation += ` ${missingCritical.length} critical file(s) not included \u2014 AI quality will degrade.`;
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
explanation = `Low coverage (${score}%): Most relevant files are excluded. AI response quality will be poor.`;
|
|
693
|
-
}
|
|
694
|
-
return {
|
|
695
|
-
score,
|
|
696
|
-
relevantFiles,
|
|
697
|
-
includedRelevant,
|
|
698
|
-
missingRelevant,
|
|
699
|
-
missingCritical,
|
|
700
|
-
explanation
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/engine/budget.ts
|
|
705
|
-
function getPruneLevelForRisk(riskScore) {
|
|
706
|
-
if (riskScore >= 80) return "full";
|
|
707
|
-
if (riskScore >= 60) return "full";
|
|
708
|
-
if (riskScore >= 30) return "signatures";
|
|
709
|
-
return "skeleton";
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// src/engine/selector.ts
|
|
713
|
-
async function selectContext(input) {
|
|
714
|
-
const { task, analysis, budget, policies, depth = 2 } = input;
|
|
715
|
-
const decisions = [];
|
|
716
|
-
const targetPaths = identifyTargetFiles(task, analysis.files);
|
|
717
|
-
if (targetPaths.length > 0) {
|
|
718
|
-
decisions.push({
|
|
719
|
-
file: targetPaths.join(", "),
|
|
720
|
-
action: "include-full",
|
|
721
|
-
reason: `Target file(s) identified from task description`
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
725
|
-
const expandedPaths = targetPaths.length > 0 ? Array.from(bfsBidirectional(targetPaths, adj, depth)) : [];
|
|
726
|
-
const expansionCount = expandedPaths.length - targetPaths.length;
|
|
727
|
-
if (expansionCount > 0) {
|
|
728
|
-
decisions.push({
|
|
729
|
-
file: `${expansionCount} dependencies`,
|
|
730
|
-
action: "include-full",
|
|
731
|
-
reason: `Expanded ${targetPaths.length} target(s) to ${expandedPaths.length} files via dependency graph (depth ${depth})`
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
const allFileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
735
|
-
if (targetPaths.length > 0) {
|
|
736
|
-
for (const path of expandedPaths) {
|
|
737
|
-
const file = allFileMap.get(path);
|
|
738
|
-
if (!file) continue;
|
|
739
|
-
for (const imp of file.imports) {
|
|
740
|
-
const impFile = allFileMap.get(imp);
|
|
741
|
-
if (impFile && impFile.kind === "type") {
|
|
742
|
-
expandedPaths.push(imp);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
const { mustInclude, mustExclude } = applyPolicies(analysis.files, policies);
|
|
748
|
-
const candidateSet = /* @__PURE__ */ new Set([...expandedPaths, ...mustInclude]);
|
|
749
|
-
if (targetPaths.length === 0) {
|
|
750
|
-
for (const f of analysis.files) {
|
|
751
|
-
candidateSet.add(f.relativePath);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
for (const ex of mustExclude) {
|
|
755
|
-
candidateSet.delete(ex);
|
|
756
|
-
decisions.push({
|
|
757
|
-
file: ex,
|
|
758
|
-
action: "exclude",
|
|
759
|
-
reason: "Excluded by policy"
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
const hasSecretBlock = policies?.rules.some(
|
|
763
|
-
(r) => r.type === "secret-block" && r.enabled
|
|
764
|
-
);
|
|
765
|
-
if (hasSecretBlock) {
|
|
766
|
-
for (const path of Array.from(candidateSet)) {
|
|
767
|
-
const file = allFileMap.get(path);
|
|
768
|
-
if (!file) continue;
|
|
769
|
-
const findings = await scanFileForSecrets(
|
|
770
|
-
file.path,
|
|
771
|
-
analysis.projectPath
|
|
772
|
-
);
|
|
773
|
-
if (findings.length > 0) {
|
|
774
|
-
candidateSet.delete(path);
|
|
775
|
-
decisions.push({
|
|
776
|
-
file: path,
|
|
777
|
-
action: "exclude",
|
|
778
|
-
reason: `Blocked: ${findings.length} secret(s) detected (${findings.map((f) => f.type).join(", ")})`
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
const candidates = Array.from(candidateSet).map((p) => allFileMap.get(p)).filter((f) => f !== void 0).sort((a, b) => {
|
|
784
|
-
const aIsTarget = targetPaths.includes(a.relativePath) ? 0 : 1;
|
|
785
|
-
const bIsTarget = targetPaths.includes(b.relativePath) ? 0 : 1;
|
|
786
|
-
if (aIsTarget !== bIsTarget) return aIsTarget - bIsTarget;
|
|
787
|
-
const aIsMust = mustInclude.has(a.relativePath) ? 0 : 1;
|
|
788
|
-
const bIsMust = mustInclude.has(b.relativePath) ? 0 : 1;
|
|
789
|
-
if (aIsMust !== bIsMust) return aIsMust - bIsMust;
|
|
790
|
-
return b.riskScore - a.riskScore;
|
|
791
|
-
});
|
|
792
|
-
const selectedFiles = [];
|
|
793
|
-
let usedTokens = 0;
|
|
794
|
-
for (const file of candidates) {
|
|
795
|
-
const isTarget = targetPaths.includes(file.relativePath);
|
|
796
|
-
const isMustInclude = mustInclude.has(file.relativePath);
|
|
797
|
-
const defaultLevel = isTarget ? "full" : getPruneLevelForRisk(file.riskScore);
|
|
798
|
-
const levels = getCascadeLevels(defaultLevel);
|
|
799
|
-
let included = false;
|
|
800
|
-
for (const level of levels) {
|
|
801
|
-
if (level === "excluded") break;
|
|
802
|
-
let tokens;
|
|
803
|
-
if (level === "full") {
|
|
804
|
-
tokens = file.tokens;
|
|
805
|
-
} else {
|
|
806
|
-
const pruned = await pruneFile(file, level);
|
|
807
|
-
tokens = pruned.prunedTokens;
|
|
808
|
-
}
|
|
809
|
-
if (usedTokens + tokens <= budget) {
|
|
810
|
-
usedTokens += tokens;
|
|
811
|
-
selectedFiles.push({
|
|
812
|
-
relativePath: file.relativePath,
|
|
813
|
-
tokens,
|
|
814
|
-
originalTokens: file.tokens,
|
|
815
|
-
pruneLevel: level,
|
|
816
|
-
riskScore: file.riskScore,
|
|
817
|
-
reason: buildReason(file, level, isTarget, isMustInclude)
|
|
818
|
-
});
|
|
819
|
-
if (level !== defaultLevel) {
|
|
820
|
-
decisions.push({
|
|
821
|
-
file: file.relativePath,
|
|
822
|
-
action: `include-${level}`,
|
|
823
|
-
reason: `Downgraded from ${defaultLevel} to ${level} due to budget constraint`,
|
|
824
|
-
alternatives: `Would need ${file.tokens - tokens} more tokens for ${defaultLevel}`
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
included = true;
|
|
828
|
-
break;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (!included) {
|
|
832
|
-
decisions.push({
|
|
833
|
-
file: file.relativePath,
|
|
834
|
-
action: "exclude",
|
|
835
|
-
reason: `Budget exhausted (risk: ${file.riskScore}, needs ${file.tokens} tokens)`
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
const includedPaths = selectedFiles.map((f) => f.relativePath);
|
|
840
|
-
const coverage = calculateCoverage(
|
|
841
|
-
targetPaths,
|
|
842
|
-
includedPaths,
|
|
843
|
-
analysis.files,
|
|
844
|
-
analysis.graph,
|
|
845
|
-
depth
|
|
846
|
-
);
|
|
847
|
-
const includedSet = new Set(includedPaths);
|
|
848
|
-
const excludedFiles = analysis.files.filter(
|
|
849
|
-
(f) => !includedSet.has(f.relativePath)
|
|
850
|
-
);
|
|
851
|
-
const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
|
|
852
|
-
const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
|
|
853
|
-
const hash = createHash2("sha256").update(hashInput).digest("hex").substring(0, 16);
|
|
854
|
-
return {
|
|
855
|
-
files: selectedFiles,
|
|
856
|
-
totalTokens: usedTokens,
|
|
857
|
-
budget,
|
|
858
|
-
usedPercent: budget > 0 ? Math.round(usedTokens / budget * 100 * 10) / 10 : 0,
|
|
859
|
-
coverage,
|
|
860
|
-
riskScore: excludedRisk,
|
|
861
|
-
deterministic: true,
|
|
862
|
-
hash,
|
|
863
|
-
decisions
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
function identifyTargetFiles(task, files) {
|
|
867
|
-
const targets = [];
|
|
868
|
-
const pathPattern = /(?:^|\s|["'`])([.\w/-]+\.[a-zA-Z]{1,4})(?:\s|$|["'`]|,|:)/g;
|
|
869
|
-
let match;
|
|
870
|
-
while ((match = pathPattern.exec(task)) !== null) {
|
|
871
|
-
const candidate = match[1];
|
|
872
|
-
const found = files.find(
|
|
873
|
-
(f) => f.relativePath === candidate || f.relativePath.endsWith(candidate)
|
|
874
|
-
);
|
|
875
|
-
if (found && !targets.includes(found.relativePath)) {
|
|
876
|
-
targets.push(found.relativePath);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return targets;
|
|
880
|
-
}
|
|
881
|
-
function applyPolicies(files, policies) {
|
|
882
|
-
const mustInclude = /* @__PURE__ */ new Set();
|
|
883
|
-
const mustExclude = /* @__PURE__ */ new Set();
|
|
884
|
-
if (!policies) return { mustInclude, mustExclude };
|
|
885
|
-
for (const rule of policies.rules) {
|
|
886
|
-
if (!rule.enabled) continue;
|
|
887
|
-
if (rule.type === "include-always" && rule.pattern) {
|
|
888
|
-
for (const file of files) {
|
|
889
|
-
if (matchGlob(file.relativePath, rule.pattern)) {
|
|
890
|
-
mustInclude.add(file.relativePath);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (rule.type === "exclude-always" && rule.pattern) {
|
|
895
|
-
for (const file of files) {
|
|
896
|
-
if (matchGlob(file.relativePath, rule.pattern)) {
|
|
897
|
-
mustExclude.add(file.relativePath);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return { mustInclude, mustExclude };
|
|
903
|
-
}
|
|
904
|
-
function getCascadeLevels(startLevel) {
|
|
905
|
-
const all = ["full", "signatures", "skeleton", "excluded"];
|
|
906
|
-
const startIdx = all.indexOf(startLevel);
|
|
907
|
-
return all.slice(startIdx);
|
|
908
|
-
}
|
|
909
|
-
function buildReason(file, level, isTarget, isMustInclude) {
|
|
910
|
-
if (isTarget) return "Target file";
|
|
911
|
-
if (isMustInclude) return "Required by policy";
|
|
912
|
-
const impact = file.exclusionImpact;
|
|
913
|
-
const levelStr = level === "full" ? "full content" : level;
|
|
914
|
-
if (impact === "critical") return `Critical dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
|
|
915
|
-
if (impact === "high") return `High-risk dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
|
|
916
|
-
if (impact === "medium") return `Medium relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
|
|
917
|
-
return `Low relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// src/interact/router.ts
|
|
921
|
-
var MODEL_REGISTRY = [
|
|
922
|
-
{
|
|
923
|
-
id: "claude-haiku-3.5",
|
|
924
|
-
name: "Claude 3.5 Haiku",
|
|
925
|
-
tier: "fast",
|
|
926
|
-
pricing: { inputPerMillion: 0.8, outputPerMillion: 4, cacheReadPerMillion: 0.08 },
|
|
927
|
-
contextWindow: 2e5,
|
|
928
|
-
strengths: ["speed", "simple-tasks", "low-cost"]
|
|
929
|
-
},
|
|
930
|
-
{
|
|
931
|
-
id: "claude-sonnet-4",
|
|
932
|
-
name: "Claude Sonnet 4",
|
|
933
|
-
tier: "balanced",
|
|
934
|
-
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheReadPerMillion: 0.3 },
|
|
935
|
-
contextWindow: 2e5,
|
|
936
|
-
strengths: ["code-generation", "refactoring", "balanced-reasoning"]
|
|
937
|
-
},
|
|
938
|
-
{
|
|
939
|
-
id: "claude-opus-4",
|
|
940
|
-
name: "Claude Opus 4",
|
|
941
|
-
tier: "reasoning",
|
|
942
|
-
pricing: { inputPerMillion: 15, outputPerMillion: 75, cacheReadPerMillion: 1.5 },
|
|
943
|
-
contextWindow: 2e5,
|
|
944
|
-
strengths: ["deep-reasoning", "architecture", "complex-debugging"]
|
|
945
|
-
}
|
|
946
|
-
];
|
|
947
|
-
var ROUTING_RULES = [
|
|
948
|
-
{
|
|
949
|
-
task: "simple-edit",
|
|
950
|
-
defaultModel: "claude-haiku-3.5",
|
|
951
|
-
upgradeIf: () => false,
|
|
952
|
-
upgradeTo: "claude-haiku-3.5",
|
|
953
|
-
reason: "Simple edits are best handled by fast models",
|
|
954
|
-
upgradeReason: ""
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
task: "docs",
|
|
958
|
-
defaultModel: "claude-haiku-3.5",
|
|
959
|
-
upgradeIf: (a) => a.totalTokens > 1e5,
|
|
960
|
-
upgradeTo: "claude-sonnet-4",
|
|
961
|
-
reason: "Documentation tasks are straightforward",
|
|
962
|
-
upgradeReason: "Large codebase \u2014 Sonnet provides better understanding"
|
|
963
|
-
},
|
|
964
|
-
{
|
|
965
|
-
task: "test",
|
|
966
|
-
defaultModel: "claude-sonnet-4",
|
|
967
|
-
upgradeIf: (a) => a.riskProfile.overallComplexity > 15,
|
|
968
|
-
upgradeTo: "claude-opus-4",
|
|
969
|
-
reason: "Test generation requires good code understanding",
|
|
970
|
-
upgradeReason: "High complexity codebase \u2014 Opus for better test coverage"
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
task: "debug",
|
|
974
|
-
defaultModel: "claude-sonnet-4",
|
|
975
|
-
upgradeIf: (a) => a.riskProfile.distribution.critical > 5,
|
|
976
|
-
upgradeTo: "claude-opus-4",
|
|
977
|
-
reason: "Debugging requires solid reasoning about code flow",
|
|
978
|
-
upgradeReason: "Many critical files involved \u2014 Opus for deeper analysis"
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
task: "refactor",
|
|
982
|
-
defaultModel: "claude-sonnet-4",
|
|
983
|
-
upgradeIf: (a) => a.totalFiles > 50 && a.riskProfile.overallComplexity > 10,
|
|
984
|
-
upgradeTo: "claude-opus-4",
|
|
985
|
-
reason: "Refactoring needs good structural understanding",
|
|
986
|
-
upgradeReason: "Large + complex project \u2014 Opus for safer refactoring"
|
|
987
|
-
},
|
|
988
|
-
{
|
|
989
|
-
task: "review",
|
|
990
|
-
defaultModel: "claude-sonnet-4",
|
|
991
|
-
upgradeIf: (a) => a.riskProfile.distribution.critical > 3,
|
|
992
|
-
upgradeTo: "claude-opus-4",
|
|
993
|
-
reason: "Code review benefits from balanced reasoning",
|
|
994
|
-
upgradeReason: "Critical code under review \u2014 Opus for thorough analysis"
|
|
995
|
-
},
|
|
996
|
-
{
|
|
997
|
-
task: "feature",
|
|
998
|
-
defaultModel: "claude-sonnet-4",
|
|
999
|
-
upgradeIf: (a) => a.totalFiles > 100,
|
|
1000
|
-
upgradeTo: "claude-opus-4",
|
|
1001
|
-
reason: "Feature development needs code generation + understanding",
|
|
1002
|
-
upgradeReason: "Large codebase \u2014 Opus for better integration"
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
task: "architecture",
|
|
1006
|
-
defaultModel: "claude-opus-4",
|
|
1007
|
-
upgradeIf: () => false,
|
|
1008
|
-
upgradeTo: "claude-opus-4",
|
|
1009
|
-
reason: "Architecture decisions require deep reasoning",
|
|
1010
|
-
upgradeReason: ""
|
|
1011
|
-
}
|
|
1012
|
-
];
|
|
1013
|
-
var TASK_KEYWORDS = {
|
|
1014
|
-
debug: ["debug", "fix", "bug", "error", "issue", "broken", "crash", "failing", "wrong"],
|
|
1015
|
-
review: ["review", "check", "assess", "evaluate", "audit", "inspect", "critique"],
|
|
1016
|
-
refactor: ["refactor", "restructure", "reorganize", "clean up", "simplify", "extract", "move"],
|
|
1017
|
-
test: ["test", "spec", "coverage", "unit test", "integration test", "e2e"],
|
|
1018
|
-
docs: ["document", "docs", "readme", "jsdoc", "comment", "explain"],
|
|
1019
|
-
feature: ["add", "implement", "create", "build", "new", "feature", "endpoint"],
|
|
1020
|
-
architecture: ["architecture", "design", "system", "structure", "migrate", "pattern"],
|
|
1021
|
-
"simple-edit": ["rename", "typo", "update", "change", "modify", "tweak", "adjust"]
|
|
1022
|
-
};
|
|
1023
|
-
function classifyTask(taskDescription) {
|
|
1024
|
-
const lower = taskDescription.toLowerCase();
|
|
1025
|
-
let bestType = "simple-edit";
|
|
1026
|
-
let bestScore = 0;
|
|
1027
|
-
for (const [type, keywords] of Object.entries(TASK_KEYWORDS)) {
|
|
1028
|
-
let score = 0;
|
|
1029
|
-
for (const kw of keywords) {
|
|
1030
|
-
if (lower.includes(kw)) score++;
|
|
1031
|
-
}
|
|
1032
|
-
if (score > bestScore) {
|
|
1033
|
-
bestScore = score;
|
|
1034
|
-
bestType = type;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
return bestType;
|
|
1038
|
-
}
|
|
1039
|
-
function routeModel(taskType, analysis, preferredModel) {
|
|
1040
|
-
if (preferredModel) {
|
|
1041
|
-
const spec = MODEL_REGISTRY.find((m) => m.id === preferredModel);
|
|
1042
|
-
if (spec) {
|
|
1043
|
-
return {
|
|
1044
|
-
model: preferredModel,
|
|
1045
|
-
reason: "User-specified model",
|
|
1046
|
-
confidence: 1,
|
|
1047
|
-
alternatives: buildAlternatives(preferredModel, taskType)
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
const rule = ROUTING_RULES.find((r) => r.task === taskType);
|
|
1052
|
-
if (!rule) {
|
|
1053
|
-
return {
|
|
1054
|
-
model: "claude-sonnet-4",
|
|
1055
|
-
reason: "Default model for unrecognized task type",
|
|
1056
|
-
confidence: 0.5,
|
|
1057
|
-
alternatives: buildAlternatives("claude-sonnet-4", taskType)
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
const shouldUpgrade = rule.upgradeIf(analysis);
|
|
1061
|
-
const model = shouldUpgrade ? rule.upgradeTo : rule.defaultModel;
|
|
1062
|
-
const reason = shouldUpgrade ? rule.upgradeReason : rule.reason;
|
|
1063
|
-
return {
|
|
1064
|
-
model,
|
|
1065
|
-
reason,
|
|
1066
|
-
confidence: shouldUpgrade ? 0.8 : 0.9,
|
|
1067
|
-
alternatives: buildAlternatives(model, taskType)
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
function buildAlternatives(chosenModel, taskType) {
|
|
1071
|
-
return MODEL_REGISTRY.filter((m) => m.id !== chosenModel).map((m) => {
|
|
1072
|
-
const chosen = MODEL_REGISTRY.find((r) => r.id === chosenModel);
|
|
1073
|
-
const costDelta = m.pricing.inputPerMillion - chosen.pricing.inputPerMillion;
|
|
1074
|
-
const tradeoff = costDelta > 0 ? `More capable but ${(costDelta / chosen.pricing.inputPerMillion * 100).toFixed(0)}% more expensive` : `${Math.abs(costDelta / chosen.pricing.inputPerMillion * 100).toFixed(0)}% cheaper but less capable`;
|
|
1075
|
-
return { model: m.id, costDelta, tradeoff };
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
function getModelSpec(modelId) {
|
|
1079
|
-
return MODEL_REGISTRY.find((m) => m.id === modelId);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// src/interact/estimator.ts
|
|
1083
|
-
function estimateCost(modelId, inputTokens, totalProjectTokens, estimatedOutputRatio = 0.3) {
|
|
1084
|
-
const spec = getModelSpec(modelId) ?? MODEL_REGISTRY[1];
|
|
1085
|
-
const estimatedOutputTokens = Math.round(inputTokens * estimatedOutputRatio);
|
|
1086
|
-
const inputCost = inputTokens / 1e6 * spec.pricing.inputPerMillion;
|
|
1087
|
-
const outputCost = estimatedOutputTokens / 1e6 * spec.pricing.outputPerMillion;
|
|
1088
|
-
const totalCost = inputCost + outputCost;
|
|
1089
|
-
const woInputCost = totalProjectTokens / 1e6 * spec.pricing.inputPerMillion;
|
|
1090
|
-
const woOutputCost = Math.round(totalProjectTokens * estimatedOutputRatio) / 1e6 * spec.pricing.outputPerMillion;
|
|
1091
|
-
const woTotalCost = woInputCost + woOutputCost;
|
|
1092
|
-
const tokensSaved = totalProjectTokens - inputTokens;
|
|
1093
|
-
const costSaved = woTotalCost - totalCost;
|
|
1094
|
-
const savingsPercent = woTotalCost > 0 ? costSaved / woTotalCost * 100 : 0;
|
|
1095
|
-
return {
|
|
1096
|
-
model: modelId,
|
|
1097
|
-
inputTokens,
|
|
1098
|
-
estimatedOutputTokens,
|
|
1099
|
-
inputCost: round(inputCost),
|
|
1100
|
-
outputCost: round(outputCost),
|
|
1101
|
-
totalCost: round(totalCost),
|
|
1102
|
-
formatted: formatCost(totalCost),
|
|
1103
|
-
withoutOptimization: {
|
|
1104
|
-
inputTokens: totalProjectTokens,
|
|
1105
|
-
totalCost: round(woTotalCost),
|
|
1106
|
-
formatted: formatCost(woTotalCost)
|
|
1107
|
-
},
|
|
1108
|
-
savings: {
|
|
1109
|
-
tokensSaved: Math.max(0, tokensSaved),
|
|
1110
|
-
costSaved: round(Math.max(0, costSaved)),
|
|
1111
|
-
percent: Math.max(0, Math.round(savingsPercent)),
|
|
1112
|
-
formatted: costSaved > 0 ? `saved ${formatCost(costSaved)} (${Math.round(savingsPercent)}%)` : "no savings"
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
function round(n) {
|
|
1117
|
-
return Math.round(n * 1e6) / 1e6;
|
|
1118
|
-
}
|
|
1119
|
-
function formatCost(cost) {
|
|
1120
|
-
if (cost < 1e-3) return "<$0.001";
|
|
1121
|
-
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
1122
|
-
if (cost < 1) return `$${cost.toFixed(3)}`;
|
|
1123
|
-
return `$${cost.toFixed(2)}`;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// src/interact/prompt.ts
|
|
1127
|
-
function buildPrompt(options) {
|
|
1128
|
-
const {
|
|
1129
|
-
task,
|
|
1130
|
-
taskType,
|
|
1131
|
-
analysis,
|
|
1132
|
-
selection,
|
|
1133
|
-
enableCoT = true,
|
|
1134
|
-
enableConstraints = true,
|
|
1135
|
-
enableAntiHallucination = true
|
|
1136
|
-
} = options;
|
|
1137
|
-
const sections = [];
|
|
1138
|
-
sections.push(buildSystemSection(analysis.stack, taskType));
|
|
1139
|
-
sections.push(buildContextSection(analysis, selection));
|
|
1140
|
-
sections.push(buildTaskSection(task, taskType));
|
|
1141
|
-
if (enableConstraints) {
|
|
1142
|
-
sections.push(buildConstraintsSection(analysis.stack, taskType));
|
|
1143
|
-
}
|
|
1144
|
-
if (enableCoT) {
|
|
1145
|
-
sections.push(buildCoTSection(taskType));
|
|
1146
|
-
}
|
|
1147
|
-
if (enableAntiHallucination) {
|
|
1148
|
-
sections.push(buildAntiHallucinationSection());
|
|
1149
|
-
}
|
|
1150
|
-
sections.push(buildFormatSection(taskType));
|
|
1151
|
-
const rendered = sections.map((s) => s.content).join("\n\n---\n\n");
|
|
1152
|
-
const totalTokens = sections.reduce((s, sec) => s + sec.tokens, 0);
|
|
1153
|
-
return { sections, totalTokens, rendered };
|
|
1154
|
-
}
|
|
1155
|
-
function buildSystemSection(stack, taskType) {
|
|
1156
|
-
const stackStr = stack.length > 0 ? stack.join(", ") : "software";
|
|
1157
|
-
const taskRole = TASK_ROLES[taskType] ?? "engineer";
|
|
1158
|
-
const content = [
|
|
1159
|
-
`You are a senior ${stackStr} ${taskRole} with deep expertise in clean architecture, testing, and production-quality code.`,
|
|
1160
|
-
"You prioritize correctness, readability, and maintainability.",
|
|
1161
|
-
"You never make assumptions without evidence from the code."
|
|
1162
|
-
].join(" ");
|
|
1163
|
-
return makeSection("system", "system", content);
|
|
1164
|
-
}
|
|
1165
|
-
function buildContextSection(analysis, selection) {
|
|
1166
|
-
const lines = [];
|
|
1167
|
-
lines.push(`## Project: ${analysis.projectName}`);
|
|
1168
|
-
lines.push(`Stack: ${analysis.stack.join(", ") || "Unknown"}`);
|
|
1169
|
-
lines.push(`Files analyzed: ${analysis.totalFiles} | Tokens: ~${Math.round(analysis.totalTokens / 1e3)}K`);
|
|
1170
|
-
lines.push(`Context coverage: ${selection.coverage.score}% | Risk score: ${selection.riskScore}/100`);
|
|
1171
|
-
lines.push("");
|
|
1172
|
-
lines.push("### Included Files");
|
|
1173
|
-
lines.push("");
|
|
1174
|
-
const fullFiles = selection.files.filter((f) => f.pruneLevel === "full");
|
|
1175
|
-
const sigFiles = selection.files.filter((f) => f.pruneLevel === "signatures");
|
|
1176
|
-
const skelFiles = selection.files.filter((f) => f.pruneLevel === "skeleton");
|
|
1177
|
-
if (fullFiles.length > 0) {
|
|
1178
|
-
lines.push("**Full content (read these first):**");
|
|
1179
|
-
for (const f of fullFiles) {
|
|
1180
|
-
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens) \u2014 ${f.reason}`);
|
|
1181
|
-
}
|
|
1182
|
-
lines.push("");
|
|
1183
|
-
}
|
|
1184
|
-
if (sigFiles.length > 0) {
|
|
1185
|
-
lines.push("**Signatures only (reference as needed):**");
|
|
1186
|
-
for (const f of sigFiles) {
|
|
1187
|
-
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
|
|
1188
|
-
}
|
|
1189
|
-
lines.push("");
|
|
1190
|
-
}
|
|
1191
|
-
if (skelFiles.length > 0) {
|
|
1192
|
-
lines.push("**Skeleton (structure overview):**");
|
|
1193
|
-
for (const f of skelFiles) {
|
|
1194
|
-
lines.push(`- \`${f.relativePath}\``);
|
|
1195
|
-
}
|
|
1196
|
-
lines.push("");
|
|
1197
|
-
}
|
|
1198
|
-
if (selection.coverage.missingCritical.length > 0) {
|
|
1199
|
-
lines.push("\u26A0\uFE0F **Missing critical files** (not included due to budget):");
|
|
1200
|
-
for (const f of selection.coverage.missingCritical) {
|
|
1201
|
-
lines.push(`- \`${f}\``);
|
|
1202
|
-
}
|
|
1203
|
-
lines.push("");
|
|
1204
|
-
}
|
|
1205
|
-
const content = lines.join("\n");
|
|
1206
|
-
return makeSection("context", "context", content);
|
|
1207
|
-
}
|
|
1208
|
-
function buildTaskSection(task, taskType) {
|
|
1209
|
-
const content = [
|
|
1210
|
-
"## Task",
|
|
1211
|
-
"",
|
|
1212
|
-
task,
|
|
1213
|
-
"",
|
|
1214
|
-
`Task type: **${taskType}**`
|
|
1215
|
-
].join("\n");
|
|
1216
|
-
return makeSection("task", "task", content);
|
|
1217
|
-
}
|
|
1218
|
-
function buildConstraintsSection(stack, taskType) {
|
|
1219
|
-
const lines = ["## Constraints", ""];
|
|
1220
|
-
lines.push("- **Do NOT** delete or modify existing tests unless explicitly asked");
|
|
1221
|
-
lines.push("- **Do NOT** change function signatures that are part of the public API");
|
|
1222
|
-
lines.push("- **Do NOT** introduce new dependencies without mentioning it");
|
|
1223
|
-
lines.push("- **Always** handle errors explicitly (no silent catches)");
|
|
1224
|
-
lines.push("- **Always** preserve existing code style and conventions");
|
|
1225
|
-
lines.push("- **Prefer** minimal changes \u2014 smallest diff that solves the problem");
|
|
1226
|
-
if (stack.includes("TypeScript")) {
|
|
1227
|
-
lines.push("- **Always** use strict TypeScript types (no `any` unless unavoidable)");
|
|
1228
|
-
lines.push("- **Always** add explicit return types to exported functions");
|
|
1229
|
-
}
|
|
1230
|
-
if (taskType === "refactor") {
|
|
1231
|
-
lines.push("- **Do NOT** change behavior \u2014 refactoring must be behavior-preserving");
|
|
1232
|
-
lines.push("- **Verify** all existing tests still pass after changes");
|
|
1233
|
-
}
|
|
1234
|
-
if (taskType === "test") {
|
|
1235
|
-
lines.push("- Use AAA pattern: Arrange, Act, Assert");
|
|
1236
|
-
lines.push("- Test boundaries, null/undefined, async errors, type edges");
|
|
1237
|
-
lines.push('- Use descriptive test names: "should [expected] when [condition]"');
|
|
1238
|
-
}
|
|
1239
|
-
return makeSection("constraints", "constraints", lines.join("\n"));
|
|
1240
|
-
}
|
|
1241
|
-
function buildCoTSection(taskType) {
|
|
1242
|
-
const steps = COT_STEPS[taskType] ?? COT_STEPS["simple-edit"];
|
|
1243
|
-
const lines = ["## Thinking Process", "", "Before writing any code:"];
|
|
1244
|
-
steps.forEach((step, i) => {
|
|
1245
|
-
lines.push(`${i + 1}. ${step}`);
|
|
1246
|
-
});
|
|
1247
|
-
return makeSection("cot", "constraints", lines.join("\n"));
|
|
1248
|
-
}
|
|
1249
|
-
function buildAntiHallucinationSection() {
|
|
1250
|
-
const content = [
|
|
1251
|
-
"## Important",
|
|
1252
|
-
"",
|
|
1253
|
-
"- Only reference files, functions, and APIs that exist in the provided context",
|
|
1254
|
-
"- If you are unsure about something, say so explicitly",
|
|
1255
|
-
"- Do NOT invent function signatures, types, or module paths",
|
|
1256
|
-
"- If the context is insufficient to complete the task, explain what is missing"
|
|
1257
|
-
].join("\n");
|
|
1258
|
-
return makeSection("anti-hallucination", "constraints", content);
|
|
1259
|
-
}
|
|
1260
|
-
function buildFormatSection(taskType) {
|
|
1261
|
-
const lines = ["## Output Format", ""];
|
|
1262
|
-
if (taskType === "review") {
|
|
1263
|
-
lines.push("Provide findings in priority order: Critical > Major > Minor > Nitpick");
|
|
1264
|
-
lines.push("For each finding: file, line, issue, and concrete suggestion with code");
|
|
1265
|
-
} else if (taskType === "architecture") {
|
|
1266
|
-
lines.push("Present 2-3 options with trade-offs, then recommend one with justification");
|
|
1267
|
-
} else if (taskType === "debug") {
|
|
1268
|
-
lines.push("1. Root cause analysis");
|
|
1269
|
-
lines.push("2. Minimal fix");
|
|
1270
|
-
lines.push("3. Explanation of why the fix works");
|
|
1271
|
-
lines.push("4. Edge cases to consider");
|
|
1272
|
-
} else {
|
|
1273
|
-
lines.push("Provide clean, production-ready code with brief explanations of key decisions");
|
|
1274
|
-
}
|
|
1275
|
-
return makeSection("format", "format", lines.join("\n"));
|
|
1276
|
-
}
|
|
1277
|
-
var TASK_ROLES = {
|
|
1278
|
-
debug: "debugger",
|
|
1279
|
-
review: "code reviewer",
|
|
1280
|
-
refactor: "architect",
|
|
1281
|
-
test: "test engineer",
|
|
1282
|
-
docs: "technical writer",
|
|
1283
|
-
feature: "engineer",
|
|
1284
|
-
architecture: "systems architect",
|
|
1285
|
-
"simple-edit": "engineer"
|
|
1286
|
-
};
|
|
1287
|
-
var COT_STEPS = {
|
|
1288
|
-
debug: [
|
|
1289
|
-
"**Reproduce** \u2014 Understand the exact symptom and when it occurs",
|
|
1290
|
-
"**Hypothesize** \u2014 List the most likely root causes (max 3)",
|
|
1291
|
-
"**Verify** \u2014 Check each hypothesis against the code",
|
|
1292
|
-
"**Fix** \u2014 Apply the minimal fix that addresses the root cause",
|
|
1293
|
-
"**Validate** \u2014 Explain why the fix works and what edge cases it covers"
|
|
1294
|
-
],
|
|
1295
|
-
review: [
|
|
1296
|
-
"**Understand** \u2014 Read the code and understand its purpose",
|
|
1297
|
-
"**Assess** \u2014 Evaluate correctness, readability, performance, security",
|
|
1298
|
-
"**Prioritize** \u2014 Rank issues by severity (critical > major > minor)",
|
|
1299
|
-
"**Suggest** \u2014 Provide concrete, actionable improvements with code"
|
|
1300
|
-
],
|
|
1301
|
-
refactor: [
|
|
1302
|
-
"**Analyze** \u2014 Identify code smells and structural issues",
|
|
1303
|
-
"**Plan** \u2014 Define the target structure before changing anything",
|
|
1304
|
-
"**Preserve** \u2014 Ensure behavior doesn't change",
|
|
1305
|
-
"**Refactor** \u2014 Apply changes incrementally",
|
|
1306
|
-
"**Verify** \u2014 Confirm all existing tests still pass"
|
|
1307
|
-
],
|
|
1308
|
-
test: [
|
|
1309
|
-
"**Identify** \u2014 What needs testing? (happy path, edge cases, errors)",
|
|
1310
|
-
"**Structure** \u2014 Use AAA pattern: Arrange, Act, Assert",
|
|
1311
|
-
"**Cover** \u2014 Test boundaries, null/undefined, async errors",
|
|
1312
|
-
"**Isolate** \u2014 Mock external dependencies, test units independently"
|
|
1313
|
-
],
|
|
1314
|
-
docs: [
|
|
1315
|
-
"**Read** \u2014 Understand the code before documenting",
|
|
1316
|
-
"**Structure** \u2014 Organize by audience (API users, contributors, operators)",
|
|
1317
|
-
"**Write** \u2014 Clear, concise, with examples"
|
|
1318
|
-
],
|
|
1319
|
-
feature: [
|
|
1320
|
-
"**Clarify** \u2014 Restate the requirement in your own words",
|
|
1321
|
-
"**Design** \u2014 Plan the approach (types, interfaces, flow)",
|
|
1322
|
-
"**Implement** \u2014 Build incrementally, starting with types",
|
|
1323
|
-
"**Test** \u2014 Write tests alongside implementation",
|
|
1324
|
-
"**Integrate** \u2014 Ensure no regressions"
|
|
1325
|
-
],
|
|
1326
|
-
architecture: [
|
|
1327
|
-
"**Context** \u2014 Understand current architecture and constraints",
|
|
1328
|
-
"**Options** \u2014 Present 2-3 viable approaches with trade-offs",
|
|
1329
|
-
"**Recommend** \u2014 Choose the best and explain why",
|
|
1330
|
-
"**Plan** \u2014 Define migration steps",
|
|
1331
|
-
"**Risks** \u2014 Identify risks and mitigation strategies"
|
|
1332
|
-
],
|
|
1333
|
-
"simple-edit": [
|
|
1334
|
-
"**Understand** \u2014 Read the relevant code",
|
|
1335
|
-
"**Plan** \u2014 Think before writing",
|
|
1336
|
-
"**Implement** \u2014 Write clean, well-typed code",
|
|
1337
|
-
"**Verify** \u2014 Check for edge cases"
|
|
1338
|
-
]
|
|
1339
|
-
};
|
|
1340
|
-
function makeSection(id, role, content) {
|
|
1341
|
-
const tokens = countTokensChars4(Buffer.byteLength(content, "utf-8"));
|
|
1342
|
-
return { id, role, content, tokens };
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// src/govern/audit.ts
|
|
1346
|
-
import { randomUUID, createHash as createHash3 } from "crypto";
|
|
1347
|
-
import { readdir, chmod } from "fs/promises";
|
|
1348
|
-
import { join as join2 } from "path";
|
|
1349
|
-
import { userInfo } from "os";
|
|
1350
|
-
import { homedir } from "os";
|
|
1351
|
-
var CTO_DIR = ".cto-ai";
|
|
1352
|
-
var AUDIT_DIR = "audit";
|
|
1353
|
-
var MAX_ENTRIES_PER_FILE = 500;
|
|
1354
|
-
function getAuditDir() {
|
|
1355
|
-
return join2(homedir(), CTO_DIR, AUDIT_DIR);
|
|
1356
|
-
}
|
|
1357
|
-
function getCurrentAuditFile() {
|
|
1358
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
1359
|
-
return join2(getAuditDir(), `audit_${date}.json`);
|
|
1360
|
-
}
|
|
1361
|
-
function computeIntegrityHash(entry) {
|
|
1362
|
-
const payload = JSON.stringify({
|
|
1363
|
-
id: entry.id,
|
|
1364
|
-
timestamp: entry.timestamp,
|
|
1365
|
-
action: entry.action,
|
|
1366
|
-
user: entry.user,
|
|
1367
|
-
projectPath: entry.projectPath,
|
|
1368
|
-
details: entry.details
|
|
1369
|
-
});
|
|
1370
|
-
return createHash3("sha256").update(payload).digest("hex");
|
|
1371
|
-
}
|
|
1372
|
-
async function ensureDir(dirPath) {
|
|
1373
|
-
const { mkdir } = await import("fs/promises");
|
|
1374
|
-
await mkdir(dirPath, { recursive: true });
|
|
1375
|
-
}
|
|
1376
|
-
async function readJSON(filePath) {
|
|
1377
|
-
const { readFile: readFile4 } = await import("fs/promises");
|
|
1378
|
-
try {
|
|
1379
|
-
const content = await readFile4(filePath, "utf-8");
|
|
1380
|
-
return JSON.parse(content);
|
|
1381
|
-
} catch {
|
|
1382
|
-
return null;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
async function writeJSON(filePath, data) {
|
|
1386
|
-
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
1387
|
-
await ensureDir(join2(filePath, ".."));
|
|
1388
|
-
await writeFile2(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
1389
|
-
}
|
|
1390
|
-
async function logAudit(action, projectPath, details = {}) {
|
|
1391
|
-
const auditDir = getAuditDir();
|
|
1392
|
-
await ensureDir(auditDir);
|
|
1393
|
-
let currentUser;
|
|
1394
|
-
try {
|
|
1395
|
-
currentUser = userInfo().username;
|
|
1396
|
-
} catch {
|
|
1397
|
-
currentUser = process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
1398
|
-
}
|
|
1399
|
-
const partialEntry = {
|
|
1400
|
-
id: randomUUID().substring(0, 12),
|
|
1401
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1402
|
-
action,
|
|
1403
|
-
user: currentUser,
|
|
1404
|
-
projectPath,
|
|
1405
|
-
details
|
|
1406
|
-
};
|
|
1407
|
-
const entry = {
|
|
1408
|
-
...partialEntry,
|
|
1409
|
-
integrityHash: computeIntegrityHash(partialEntry)
|
|
1410
|
-
};
|
|
1411
|
-
const auditFile = getCurrentAuditFile();
|
|
1412
|
-
let entries = await readJSON(auditFile) ?? [];
|
|
1413
|
-
entries.push(entry);
|
|
1414
|
-
if (entries.length > MAX_ENTRIES_PER_FILE) {
|
|
1415
|
-
entries = entries.slice(-MAX_ENTRIES_PER_FILE);
|
|
1416
|
-
}
|
|
1417
|
-
await writeJSON(auditFile, entries);
|
|
1418
|
-
try {
|
|
1419
|
-
await chmod(auditFile, 384);
|
|
1420
|
-
} catch {
|
|
1421
|
-
}
|
|
1422
|
-
return entry;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// src/interact/orchestrator.ts
|
|
1426
|
-
async function planInteraction(input) {
|
|
1427
|
-
const {
|
|
1428
|
-
task,
|
|
1429
|
-
analysis,
|
|
1430
|
-
budget = 5e4,
|
|
1431
|
-
model: preferredModel,
|
|
1432
|
-
policies,
|
|
1433
|
-
depth = 2,
|
|
1434
|
-
enableCoT = true,
|
|
1435
|
-
enableConstraints = true,
|
|
1436
|
-
enableAntiHallucination = true
|
|
1437
|
-
} = input;
|
|
1438
|
-
const decisions = [];
|
|
1439
|
-
const taskType = classifyTask(task);
|
|
1440
|
-
decisions.push({
|
|
1441
|
-
step: "classify",
|
|
1442
|
-
decision: taskType,
|
|
1443
|
-
reason: `Task classified as "${taskType}" based on keyword analysis`,
|
|
1444
|
-
data: { task, taskType }
|
|
1445
|
-
});
|
|
1446
|
-
const context = await selectContext({
|
|
1447
|
-
task,
|
|
1448
|
-
analysis,
|
|
1449
|
-
budget,
|
|
1450
|
-
policies,
|
|
1451
|
-
depth
|
|
1452
|
-
});
|
|
1453
|
-
decisions.push({
|
|
1454
|
-
step: "select-context",
|
|
1455
|
-
decision: `${context.files.length} files selected (${context.totalTokens} tokens)`,
|
|
1456
|
-
reason: `Coverage: ${context.coverage.score}%, Risk: ${context.riskScore}/100`,
|
|
1457
|
-
data: {
|
|
1458
|
-
filesIncluded: context.files.length,
|
|
1459
|
-
tokensUsed: context.totalTokens,
|
|
1460
|
-
budget,
|
|
1461
|
-
coverage: context.coverage.score,
|
|
1462
|
-
risk: context.riskScore
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
const modelChoice = routeModel(taskType, analysis, preferredModel);
|
|
1466
|
-
decisions.push({
|
|
1467
|
-
step: "choose-model",
|
|
1468
|
-
decision: modelChoice.model,
|
|
1469
|
-
reason: modelChoice.reason,
|
|
1470
|
-
data: {
|
|
1471
|
-
confidence: modelChoice.confidence,
|
|
1472
|
-
alternatives: modelChoice.alternatives.length
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
const prompt = buildPrompt({
|
|
1476
|
-
task,
|
|
1477
|
-
taskType,
|
|
1478
|
-
analysis,
|
|
1479
|
-
selection: context,
|
|
1480
|
-
enableCoT,
|
|
1481
|
-
enableConstraints,
|
|
1482
|
-
enableAntiHallucination
|
|
1483
|
-
});
|
|
1484
|
-
decisions.push({
|
|
1485
|
-
step: "build-prompt",
|
|
1486
|
-
decision: `${prompt.sections.length} sections, ${prompt.totalTokens} tokens`,
|
|
1487
|
-
reason: `Sections: ${prompt.sections.map((s) => s.id).join(", ")}`
|
|
1488
|
-
});
|
|
1489
|
-
const cost = estimateCost(
|
|
1490
|
-
modelChoice.model,
|
|
1491
|
-
context.totalTokens + prompt.totalTokens,
|
|
1492
|
-
analysis.totalTokens
|
|
1493
|
-
);
|
|
1494
|
-
decisions.push({
|
|
1495
|
-
step: "estimate-cost",
|
|
1496
|
-
decision: cost.formatted,
|
|
1497
|
-
reason: cost.savings.formatted,
|
|
1498
|
-
data: {
|
|
1499
|
-
inputTokens: cost.inputTokens,
|
|
1500
|
-
totalCost: cost.totalCost,
|
|
1501
|
-
savings: cost.savings.percent
|
|
1502
|
-
}
|
|
1503
|
-
});
|
|
1504
|
-
const plan = {
|
|
1505
|
-
id: randomUUID2().substring(0, 8),
|
|
1506
|
-
task,
|
|
1507
|
-
taskType,
|
|
1508
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1509
|
-
context,
|
|
1510
|
-
model: modelChoice,
|
|
1511
|
-
prompt,
|
|
1512
|
-
cost,
|
|
1513
|
-
decisions
|
|
1514
|
-
};
|
|
1515
|
-
try {
|
|
1516
|
-
await logAudit("interact", analysis.projectPath, {
|
|
1517
|
-
interactionId: plan.id,
|
|
1518
|
-
task,
|
|
1519
|
-
taskType,
|
|
1520
|
-
contextHash: context.hash,
|
|
1521
|
-
filesIncluded: context.files.length,
|
|
1522
|
-
filesExcluded: analysis.totalFiles - context.files.length,
|
|
1523
|
-
tokensUsed: context.totalTokens,
|
|
1524
|
-
coverageScore: context.coverage.score,
|
|
1525
|
-
riskScore: context.riskScore,
|
|
1526
|
-
model: modelChoice.model,
|
|
1527
|
-
estimatedCost: cost.totalCost
|
|
1528
|
-
});
|
|
1529
|
-
} catch {
|
|
1530
|
-
}
|
|
1531
|
-
return plan;
|
|
1532
|
-
}
|
|
1533
|
-
export {
|
|
1534
|
-
MODEL_REGISTRY,
|
|
1535
|
-
buildPrompt,
|
|
1536
|
-
classifyTask,
|
|
1537
|
-
estimateCost,
|
|
1538
|
-
getModelSpec,
|
|
1539
|
-
planInteraction,
|
|
1540
|
-
routeModel
|
|
1541
|
-
};
|
|
1542
|
-
//# sourceMappingURL=index.js.map
|