cto-ai-cli 1.3.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/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +6331 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +717 -0
- package/dist/core/index.js +4446 -0
- package/dist/core/index.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +15336 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,4446 @@
|
|
|
1
|
+
// src/core/analyzer.ts
|
|
2
|
+
import { resolve as resolve3, basename } from "path";
|
|
3
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
4
|
+
|
|
5
|
+
// src/core/config.ts
|
|
6
|
+
import { parse, stringify } from "yaml";
|
|
7
|
+
|
|
8
|
+
// src/utils/constants.ts
|
|
9
|
+
var CTO_VERSION = "1.3.0";
|
|
10
|
+
var CTO_DIR = ".config/cto";
|
|
11
|
+
var CTO_CONFIG_FILE = "config.yaml";
|
|
12
|
+
var CTO_PROJECTS_DIR = "projects";
|
|
13
|
+
var CTO_BACKUPS_DIR = "backups";
|
|
14
|
+
var CTO_SESSIONS_DIR = "sessions";
|
|
15
|
+
var DEFAULT_EXTENSIONS = {
|
|
16
|
+
code: [
|
|
17
|
+
"ts",
|
|
18
|
+
"tsx",
|
|
19
|
+
"js",
|
|
20
|
+
"jsx",
|
|
21
|
+
"py",
|
|
22
|
+
"rb",
|
|
23
|
+
"go",
|
|
24
|
+
"rs",
|
|
25
|
+
"java",
|
|
26
|
+
"kt",
|
|
27
|
+
"swift",
|
|
28
|
+
"c",
|
|
29
|
+
"cpp",
|
|
30
|
+
"h",
|
|
31
|
+
"hpp",
|
|
32
|
+
"cs",
|
|
33
|
+
"php",
|
|
34
|
+
"vue",
|
|
35
|
+
"svelte",
|
|
36
|
+
"astro",
|
|
37
|
+
"lua",
|
|
38
|
+
"zig",
|
|
39
|
+
"ex",
|
|
40
|
+
"exs",
|
|
41
|
+
"clj",
|
|
42
|
+
"scala",
|
|
43
|
+
"dart"
|
|
44
|
+
],
|
|
45
|
+
config: [
|
|
46
|
+
"json",
|
|
47
|
+
"yaml",
|
|
48
|
+
"yml",
|
|
49
|
+
"toml",
|
|
50
|
+
"ini",
|
|
51
|
+
"env",
|
|
52
|
+
"cfg",
|
|
53
|
+
"xml",
|
|
54
|
+
"plist",
|
|
55
|
+
"properties"
|
|
56
|
+
],
|
|
57
|
+
docs: ["md", "mdx", "txt", "rst", "adoc"],
|
|
58
|
+
ignore: [
|
|
59
|
+
"lock",
|
|
60
|
+
"map",
|
|
61
|
+
"min.js",
|
|
62
|
+
"min.css",
|
|
63
|
+
"bundle.js",
|
|
64
|
+
"chunk.js",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"CHANGELOG"
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
var DEFAULT_TIER_THRESHOLDS = {
|
|
70
|
+
hotDays: 3,
|
|
71
|
+
warmDays: 14,
|
|
72
|
+
hotTokenLimit: 5e4,
|
|
73
|
+
warmTokenLimit: 2e5
|
|
74
|
+
};
|
|
75
|
+
var DEFAULT_IGNORE_DIRS = [
|
|
76
|
+
"node_modules",
|
|
77
|
+
".git",
|
|
78
|
+
"dist",
|
|
79
|
+
"build",
|
|
80
|
+
"out",
|
|
81
|
+
".next",
|
|
82
|
+
".nuxt",
|
|
83
|
+
".svelte-kit",
|
|
84
|
+
"coverage",
|
|
85
|
+
"__pycache__",
|
|
86
|
+
".pytest_cache",
|
|
87
|
+
"venv",
|
|
88
|
+
".venv",
|
|
89
|
+
"env",
|
|
90
|
+
".env",
|
|
91
|
+
"vendor",
|
|
92
|
+
"target",
|
|
93
|
+
"bin",
|
|
94
|
+
"obj",
|
|
95
|
+
".idea",
|
|
96
|
+
".vscode",
|
|
97
|
+
".DS_Store",
|
|
98
|
+
".turbo",
|
|
99
|
+
".vercel",
|
|
100
|
+
".output",
|
|
101
|
+
"public/assets",
|
|
102
|
+
"static/assets",
|
|
103
|
+
".cache",
|
|
104
|
+
"tmp",
|
|
105
|
+
".tmp"
|
|
106
|
+
];
|
|
107
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
108
|
+
"*.min.js",
|
|
109
|
+
"*.min.css",
|
|
110
|
+
"*.map",
|
|
111
|
+
"*.lock",
|
|
112
|
+
"*.log",
|
|
113
|
+
"*.bundle.js",
|
|
114
|
+
"*.chunk.js",
|
|
115
|
+
"package-lock.json",
|
|
116
|
+
"yarn.lock",
|
|
117
|
+
"pnpm-lock.yaml",
|
|
118
|
+
"composer.lock",
|
|
119
|
+
"Gemfile.lock",
|
|
120
|
+
"Cargo.lock",
|
|
121
|
+
"poetry.lock"
|
|
122
|
+
];
|
|
123
|
+
var DEFAULT_CONFIG = {
|
|
124
|
+
version: CTO_VERSION,
|
|
125
|
+
extensions: DEFAULT_EXTENSIONS,
|
|
126
|
+
tiering: DEFAULT_TIER_THRESHOLDS,
|
|
127
|
+
ignoreDirs: DEFAULT_IGNORE_DIRS,
|
|
128
|
+
ignorePatterns: DEFAULT_IGNORE_PATTERNS,
|
|
129
|
+
model: "sonnet",
|
|
130
|
+
tokenEstimation: "chars4"
|
|
131
|
+
};
|
|
132
|
+
var PROMPT_TEMPLATES = [
|
|
133
|
+
{
|
|
134
|
+
id: "task-focused",
|
|
135
|
+
name: "Focused Task",
|
|
136
|
+
description: "Optimized prompt for a specific task, includes only hot files",
|
|
137
|
+
category: "task",
|
|
138
|
+
template: `Project context: {projectName}
|
|
139
|
+
Stack: {stack}
|
|
140
|
+
|
|
141
|
+
Main files (hot tier \u2014 read these first):
|
|
142
|
+
{hotFiles}
|
|
143
|
+
|
|
144
|
+
Task: {taskDescription}
|
|
145
|
+
|
|
146
|
+
Instructions:
|
|
147
|
+
- Focus ONLY on the files listed above
|
|
148
|
+
- If you need other files, ask before reading them
|
|
149
|
+
- Use /compact if the conversation gets long`
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "code-review",
|
|
153
|
+
name: "Code Review",
|
|
154
|
+
description: "Efficient code review prompt",
|
|
155
|
+
category: "review",
|
|
156
|
+
template: `Project context: {projectName}
|
|
157
|
+
|
|
158
|
+
Files to review:
|
|
159
|
+
{hotFiles}
|
|
160
|
+
|
|
161
|
+
Review these files focusing on:
|
|
162
|
+
1. Potential bugs
|
|
163
|
+
2. Performance
|
|
164
|
+
3. Security
|
|
165
|
+
4. Best practices
|
|
166
|
+
|
|
167
|
+
Do NOT read additional files unless strictly necessary.`
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "debug-minimal",
|
|
171
|
+
name: "Minimal Debug",
|
|
172
|
+
description: "Debugging prompt with minimal context",
|
|
173
|
+
category: "debug",
|
|
174
|
+
template: `Project: {projectName}
|
|
175
|
+
Stack: {stack}
|
|
176
|
+
|
|
177
|
+
Error/Problem: {taskDescription}
|
|
178
|
+
|
|
179
|
+
Relevant files (hot):
|
|
180
|
+
{hotFiles}
|
|
181
|
+
|
|
182
|
+
Debugging instructions:
|
|
183
|
+
1. Analyze the error with the hot files
|
|
184
|
+
2. If you need more files, ask for them one at a time
|
|
185
|
+
3. Propose a minimal fix
|
|
186
|
+
4. Do NOT refactor unrelated code`
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "refactor-safe",
|
|
190
|
+
name: "Safe Refactor",
|
|
191
|
+
description: "Refactoring prompt with full context",
|
|
192
|
+
category: "refactor",
|
|
193
|
+
template: `Project: {projectName}
|
|
194
|
+
Stack: {stack}
|
|
195
|
+
|
|
196
|
+
Hot files (modify):
|
|
197
|
+
{hotFiles}
|
|
198
|
+
|
|
199
|
+
Warm files (context only, do not modify unless necessary):
|
|
200
|
+
{warmFiles}
|
|
201
|
+
|
|
202
|
+
Refactor goal: {taskDescription}
|
|
203
|
+
|
|
204
|
+
Rules:
|
|
205
|
+
- Change only what is necessary
|
|
206
|
+
- Keep the public interface intact
|
|
207
|
+
- Verify you don't break imports`
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "test-gen",
|
|
211
|
+
name: "Generate Tests",
|
|
212
|
+
description: "Test generation prompt",
|
|
213
|
+
category: "test",
|
|
214
|
+
template: `Project: {projectName}
|
|
215
|
+
Stack: {stack}
|
|
216
|
+
|
|
217
|
+
Files to test:
|
|
218
|
+
{hotFiles}
|
|
219
|
+
|
|
220
|
+
Generate tests for the listed files:
|
|
221
|
+
- Project test framework: {testFramework}
|
|
222
|
+
- Cover happy path and edge cases
|
|
223
|
+
- Do NOT read files you don't need
|
|
224
|
+
- Use mocks for external dependencies`
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
var CHARS_PER_TOKEN = 4;
|
|
228
|
+
|
|
229
|
+
// src/utils/paths.ts
|
|
230
|
+
import { homedir } from "os";
|
|
231
|
+
import { join, resolve } from "path";
|
|
232
|
+
import { createHash } from "crypto";
|
|
233
|
+
function getCTORoot() {
|
|
234
|
+
return join(homedir(), CTO_DIR);
|
|
235
|
+
}
|
|
236
|
+
function getCTOConfigPath() {
|
|
237
|
+
return join(getCTORoot(), CTO_CONFIG_FILE);
|
|
238
|
+
}
|
|
239
|
+
function getProjectHash(projectPath) {
|
|
240
|
+
const normalized = resolve(projectPath);
|
|
241
|
+
return createHash("sha256").update(normalized).digest("hex").substring(0, 12);
|
|
242
|
+
}
|
|
243
|
+
function getProjectDir(projectPath) {
|
|
244
|
+
const hash = getProjectHash(projectPath);
|
|
245
|
+
return join(getCTORoot(), CTO_PROJECTS_DIR, hash);
|
|
246
|
+
}
|
|
247
|
+
function getProjectConfigPath(projectPath) {
|
|
248
|
+
return join(getProjectDir(projectPath), CTO_CONFIG_FILE);
|
|
249
|
+
}
|
|
250
|
+
function getProjectAnalysisPath(projectPath) {
|
|
251
|
+
return join(getProjectDir(projectPath), "analysis.json");
|
|
252
|
+
}
|
|
253
|
+
function getBackupsDir(projectPath) {
|
|
254
|
+
return join(getProjectDir(projectPath), CTO_BACKUPS_DIR);
|
|
255
|
+
}
|
|
256
|
+
function getSessionsDir(projectPath) {
|
|
257
|
+
return join(getProjectDir(projectPath), CTO_SESSIONS_DIR);
|
|
258
|
+
}
|
|
259
|
+
function getArtifactsDir(projectPath) {
|
|
260
|
+
return join(getProjectDir(projectPath), "artifacts");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/utils/fs.ts
|
|
264
|
+
import { readFile, writeFile, mkdir, stat, readdir, copyFile, unlink, rm } from "fs/promises";
|
|
265
|
+
import { join as join2, extname, relative } from "path";
|
|
266
|
+
import { existsSync } from "fs";
|
|
267
|
+
async function ensureDir(dirPath) {
|
|
268
|
+
await mkdir(dirPath, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
async function readJSON(filePath) {
|
|
271
|
+
try {
|
|
272
|
+
const content = await readFile(filePath, "utf-8");
|
|
273
|
+
return JSON.parse(content);
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function writeJSON(filePath, data) {
|
|
279
|
+
const dir = join2(filePath, "..");
|
|
280
|
+
await ensureDir(dir);
|
|
281
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
282
|
+
}
|
|
283
|
+
async function readText(filePath) {
|
|
284
|
+
try {
|
|
285
|
+
return await readFile(filePath, "utf-8");
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function writeText(filePath, content) {
|
|
291
|
+
const dir = join2(filePath, "..");
|
|
292
|
+
await ensureDir(dir);
|
|
293
|
+
await writeFile(filePath, content, "utf-8");
|
|
294
|
+
}
|
|
295
|
+
function fileExists(filePath) {
|
|
296
|
+
return existsSync(filePath);
|
|
297
|
+
}
|
|
298
|
+
async function getFileStat(filePath) {
|
|
299
|
+
try {
|
|
300
|
+
return await stat(filePath);
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function copyFileWithBackup(src, dest) {
|
|
306
|
+
await ensureDir(join2(dest, ".."));
|
|
307
|
+
await copyFile(src, dest);
|
|
308
|
+
}
|
|
309
|
+
async function removeFile(filePath) {
|
|
310
|
+
try {
|
|
311
|
+
await unlink(filePath);
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function removeDir(dirPath) {
|
|
318
|
+
try {
|
|
319
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
320
|
+
return true;
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function matchesPattern(filename, patterns) {
|
|
326
|
+
for (const pattern of patterns) {
|
|
327
|
+
if (pattern.startsWith("*.")) {
|
|
328
|
+
const ext = pattern.slice(1);
|
|
329
|
+
if (filename.endsWith(ext)) return true;
|
|
330
|
+
} else if (filename === pattern) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
async function walkProject(rootPath, options) {
|
|
337
|
+
const results = [];
|
|
338
|
+
const { ignoreDirs, ignorePatterns, extensions, maxDepth = 20 } = options;
|
|
339
|
+
const ignoreDirSet = new Set(ignoreDirs);
|
|
340
|
+
async function walk(dir, depth) {
|
|
341
|
+
if (depth > maxDepth) return;
|
|
342
|
+
let entries;
|
|
343
|
+
try {
|
|
344
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
345
|
+
} catch {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const promises = [];
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const fullPath = join2(dir, entry.name);
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
if (!ignoreDirSet.has(entry.name) && !entry.name.startsWith(".")) {
|
|
353
|
+
promises.push(walk(fullPath, depth + 1));
|
|
354
|
+
}
|
|
355
|
+
} else if (entry.isFile()) {
|
|
356
|
+
const ext = extname(entry.name).slice(1).toLowerCase();
|
|
357
|
+
if (ext && extensions.includes(ext) && !matchesPattern(entry.name, ignorePatterns)) {
|
|
358
|
+
promises.push(
|
|
359
|
+
(async () => {
|
|
360
|
+
const fileStat = await getFileStat(fullPath);
|
|
361
|
+
if (!fileStat) return;
|
|
362
|
+
let lines = 0;
|
|
363
|
+
try {
|
|
364
|
+
const content = await readFile(fullPath, "utf-8");
|
|
365
|
+
lines = content.split("\n").length;
|
|
366
|
+
} catch {
|
|
367
|
+
lines = 0;
|
|
368
|
+
}
|
|
369
|
+
results.push({
|
|
370
|
+
path: fullPath,
|
|
371
|
+
relativePath: relative(rootPath, fullPath),
|
|
372
|
+
extension: ext,
|
|
373
|
+
size: fileStat.size,
|
|
374
|
+
lastModified: fileStat.mtime,
|
|
375
|
+
lastAccessed: fileStat.atime,
|
|
376
|
+
lines
|
|
377
|
+
});
|
|
378
|
+
})()
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await Promise.all(promises);
|
|
384
|
+
}
|
|
385
|
+
await walk(rootPath, 0);
|
|
386
|
+
return results;
|
|
387
|
+
}
|
|
388
|
+
function getAllExtensions(config) {
|
|
389
|
+
return [
|
|
390
|
+
...config.extensions.code,
|
|
391
|
+
...config.extensions.config,
|
|
392
|
+
...config.extensions.docs
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/core/config.ts
|
|
397
|
+
async function initCTODir() {
|
|
398
|
+
await ensureDir(getCTORoot());
|
|
399
|
+
}
|
|
400
|
+
async function loadGlobalConfig() {
|
|
401
|
+
const configPath = getCTOConfigPath();
|
|
402
|
+
if (!fileExists(configPath)) {
|
|
403
|
+
return { ...DEFAULT_CONFIG };
|
|
404
|
+
}
|
|
405
|
+
const content = await readText(configPath);
|
|
406
|
+
if (!content) return { ...DEFAULT_CONFIG };
|
|
407
|
+
try {
|
|
408
|
+
const parsed = parse(content);
|
|
409
|
+
return mergeConfig(DEFAULT_CONFIG, parsed);
|
|
410
|
+
} catch {
|
|
411
|
+
return { ...DEFAULT_CONFIG };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function saveGlobalConfig(config) {
|
|
415
|
+
await initCTODir();
|
|
416
|
+
const configPath = getCTOConfigPath();
|
|
417
|
+
await writeText(configPath, stringify(config, { indent: 2 }));
|
|
418
|
+
}
|
|
419
|
+
async function loadProjectConfig(projectPath) {
|
|
420
|
+
const globalConfig = await loadGlobalConfig();
|
|
421
|
+
const projectConfigPath = getProjectConfigPath(projectPath);
|
|
422
|
+
if (!fileExists(projectConfigPath)) {
|
|
423
|
+
return globalConfig;
|
|
424
|
+
}
|
|
425
|
+
const content = await readText(projectConfigPath);
|
|
426
|
+
if (!content) return globalConfig;
|
|
427
|
+
try {
|
|
428
|
+
const parsed = parse(content);
|
|
429
|
+
return mergeConfig(globalConfig, parsed);
|
|
430
|
+
} catch {
|
|
431
|
+
return globalConfig;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function saveProjectConfig(projectPath, config) {
|
|
435
|
+
const projectConfigPath = getProjectConfigPath(projectPath);
|
|
436
|
+
const existing = await loadProjectConfig(projectPath);
|
|
437
|
+
const merged = { ...existing, ...config, projectPath };
|
|
438
|
+
await writeText(projectConfigPath, stringify(merged, { indent: 2 }));
|
|
439
|
+
}
|
|
440
|
+
function mergeConfig(base, override) {
|
|
441
|
+
return {
|
|
442
|
+
version: override.version ?? base.version,
|
|
443
|
+
extensions: override.extensions ? {
|
|
444
|
+
code: override.extensions.code ?? base.extensions.code,
|
|
445
|
+
config: override.extensions.config ?? base.extensions.config,
|
|
446
|
+
docs: override.extensions.docs ?? base.extensions.docs,
|
|
447
|
+
ignore: override.extensions.ignore ?? base.extensions.ignore
|
|
448
|
+
} : base.extensions,
|
|
449
|
+
tiering: override.tiering ? { ...base.tiering, ...override.tiering } : base.tiering,
|
|
450
|
+
ignoreDirs: override.ignoreDirs ?? base.ignoreDirs,
|
|
451
|
+
ignorePatterns: override.ignorePatterns ?? base.ignorePatterns,
|
|
452
|
+
model: override.model ?? base.model,
|
|
453
|
+
tokenEstimation: override.tokenEstimation ?? base.tokenEstimation
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function hasGlobalConfig() {
|
|
457
|
+
return fileExists(getCTOConfigPath());
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/core/tiering.ts
|
|
461
|
+
function classifyFile(entry, thresholds) {
|
|
462
|
+
const now = /* @__PURE__ */ new Date();
|
|
463
|
+
const diffMs = now.getTime() - entry.lastModified.getTime();
|
|
464
|
+
const diffDays = diffMs / (1e3 * 60 * 60 * 24);
|
|
465
|
+
if (diffDays <= thresholds.hotDays) {
|
|
466
|
+
return "hot";
|
|
467
|
+
}
|
|
468
|
+
if (diffDays <= thresholds.warmDays) {
|
|
469
|
+
return "warm";
|
|
470
|
+
}
|
|
471
|
+
return "cold";
|
|
472
|
+
}
|
|
473
|
+
function classifyFileWithAST(file, thresholds) {
|
|
474
|
+
const now = /* @__PURE__ */ new Date();
|
|
475
|
+
const modDate = file.lastModified instanceof Date ? file.lastModified : new Date(file.lastModified);
|
|
476
|
+
const diffMs = now.getTime() - modDate.getTime();
|
|
477
|
+
const diffDays = diffMs / (1e3 * 60 * 60 * 24);
|
|
478
|
+
let baseTier;
|
|
479
|
+
if (diffDays <= thresholds.hotDays) {
|
|
480
|
+
baseTier = "hot";
|
|
481
|
+
} else if (diffDays <= thresholds.warmDays) {
|
|
482
|
+
baseTier = "warm";
|
|
483
|
+
} else {
|
|
484
|
+
baseTier = "cold";
|
|
485
|
+
}
|
|
486
|
+
if (file.isHub && (file.importedByCount ?? 0) >= 3) {
|
|
487
|
+
if (baseTier === "cold") return "warm";
|
|
488
|
+
if (baseTier === "warm") return "hot";
|
|
489
|
+
}
|
|
490
|
+
if ((file.complexity ?? 0) > 30 && baseTier === "warm") {
|
|
491
|
+
return "hot";
|
|
492
|
+
}
|
|
493
|
+
return baseTier;
|
|
494
|
+
}
|
|
495
|
+
function buildTierSummary(files, totalTokens) {
|
|
496
|
+
const hot = files.filter((f) => f.tier === "hot");
|
|
497
|
+
const warm = files.filter((f) => f.tier === "warm");
|
|
498
|
+
const cold = files.filter((f) => f.tier === "cold");
|
|
499
|
+
return {
|
|
500
|
+
hot: buildTierDetail(hot, totalTokens),
|
|
501
|
+
warm: buildTierDetail(warm, totalTokens),
|
|
502
|
+
cold: buildTierDetail(cold, totalTokens)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function buildTierDetail(files, totalTokens) {
|
|
506
|
+
const tierTokens = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
507
|
+
const tierSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
508
|
+
return {
|
|
509
|
+
files,
|
|
510
|
+
totalTokens: tierTokens,
|
|
511
|
+
totalSize: tierSize,
|
|
512
|
+
count: files.length,
|
|
513
|
+
percentage: totalTokens > 0 ? tierTokens / totalTokens * 100 : 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function getTierFiles(files, tier) {
|
|
517
|
+
return files.filter((f) => f.tier === tier).sort((a, b) => b.tokens - a.tokens);
|
|
518
|
+
}
|
|
519
|
+
function getTokenSavings(files) {
|
|
520
|
+
const withoutOptimization = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
521
|
+
const withOptimization = files.filter((f) => f.tier === "hot").reduce((sum, f) => sum + f.tokens, 0);
|
|
522
|
+
const saved = withoutOptimization - withOptimization;
|
|
523
|
+
const savingsPercent = withoutOptimization > 0 ? saved / withoutOptimization * 100 : 0;
|
|
524
|
+
return {
|
|
525
|
+
withOptimization,
|
|
526
|
+
withoutOptimization,
|
|
527
|
+
saved,
|
|
528
|
+
savingsPercent
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/core/tokenizer.ts
|
|
533
|
+
import { encodingForModel } from "js-tiktoken";
|
|
534
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
535
|
+
var encoder = null;
|
|
536
|
+
function getEncoder() {
|
|
537
|
+
if (!encoder) {
|
|
538
|
+
encoder = encodingForModel("claude-3-5-sonnet-20241022");
|
|
539
|
+
}
|
|
540
|
+
return encoder;
|
|
541
|
+
}
|
|
542
|
+
function countTokensTiktoken(text) {
|
|
543
|
+
try {
|
|
544
|
+
const enc = getEncoder();
|
|
545
|
+
const tokens = enc.encode(text);
|
|
546
|
+
return tokens.length;
|
|
547
|
+
} catch {
|
|
548
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function countTokensChars4(sizeInBytes) {
|
|
552
|
+
return Math.ceil(sizeInBytes / CHARS_PER_TOKEN);
|
|
553
|
+
}
|
|
554
|
+
function estimateTokens(content, sizeInBytes, method = "chars4") {
|
|
555
|
+
if (method === "tiktoken") {
|
|
556
|
+
return countTokensTiktoken(content);
|
|
557
|
+
}
|
|
558
|
+
return countTokensChars4(sizeInBytes);
|
|
559
|
+
}
|
|
560
|
+
async function estimateFileTokens(filePath, method = "chars4") {
|
|
561
|
+
if (method === "chars4") {
|
|
562
|
+
const { stat: stat3 } = await import("fs/promises");
|
|
563
|
+
const s = await stat3(filePath);
|
|
564
|
+
return countTokensChars4(s.size);
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const content = await readFile2(filePath, "utf-8");
|
|
568
|
+
return countTokensTiktoken(content);
|
|
569
|
+
} catch {
|
|
570
|
+
const { stat: stat3 } = await import("fs/promises");
|
|
571
|
+
const s = await stat3(filePath);
|
|
572
|
+
return countTokensChars4(s.size);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function freeEncoder() {
|
|
576
|
+
encoder = null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/core/ast.ts
|
|
580
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
581
|
+
import { resolve as resolve2, relative as relative2, dirname, join as join3 } from "path";
|
|
582
|
+
import { existsSync as existsSync2 } from "fs";
|
|
583
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs", "cts", "cjs"]);
|
|
584
|
+
function createProject(projectPath, filePaths) {
|
|
585
|
+
const tsConfigPath = join3(projectPath, "tsconfig.json");
|
|
586
|
+
const hasTsConfig = existsSync2(tsConfigPath);
|
|
587
|
+
const project = new Project({
|
|
588
|
+
tsConfigFilePath: hasTsConfig ? tsConfigPath : void 0,
|
|
589
|
+
skipAddingFilesFromTsConfig: true,
|
|
590
|
+
compilerOptions: hasTsConfig ? void 0 : {
|
|
591
|
+
allowJs: true,
|
|
592
|
+
jsx: 4,
|
|
593
|
+
// JsxEmit.ReactJSX
|
|
594
|
+
esModuleInterop: true,
|
|
595
|
+
moduleResolution: 100
|
|
596
|
+
// Bundler
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
const tsFiles = filePaths.filter((f) => {
|
|
600
|
+
const ext = f.split(".").pop()?.toLowerCase() ?? "";
|
|
601
|
+
return TS_EXTENSIONS.has(ext);
|
|
602
|
+
});
|
|
603
|
+
for (const filePath of tsFiles) {
|
|
604
|
+
try {
|
|
605
|
+
project.addSourceFileAtPath(filePath);
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return project;
|
|
610
|
+
}
|
|
611
|
+
function buildDependencyGraph(project, projectPath) {
|
|
612
|
+
const absPath = resolve2(projectPath);
|
|
613
|
+
const edges = [];
|
|
614
|
+
const nodeSet = /* @__PURE__ */ new Set();
|
|
615
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
616
|
+
const fromRel = relative2(absPath, sourceFile.getFilePath());
|
|
617
|
+
if (fromRel.startsWith("..") || fromRel.includes("node_modules")) continue;
|
|
618
|
+
nodeSet.add(fromRel);
|
|
619
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
620
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
621
|
+
const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
|
|
622
|
+
if (resolved) {
|
|
623
|
+
nodeSet.add(resolved);
|
|
624
|
+
edges.push({ from: fromRel, to: resolved, type: "import" });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const exp of sourceFile.getExportDeclarations()) {
|
|
628
|
+
const moduleSpecifier = exp.getModuleSpecifierValue();
|
|
629
|
+
if (moduleSpecifier) {
|
|
630
|
+
const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
|
|
631
|
+
if (resolved) {
|
|
632
|
+
nodeSet.add(resolved);
|
|
633
|
+
edges.push({ from: fromRel, to: resolved, type: "re-export" });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const nodes = Array.from(nodeSet);
|
|
639
|
+
const importedByCount = /* @__PURE__ */ new Map();
|
|
640
|
+
const importCount = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const edge of edges) {
|
|
642
|
+
importedByCount.set(edge.to, (importedByCount.get(edge.to) ?? 0) + 1);
|
|
643
|
+
importCount.set(edge.from, (importCount.get(edge.from) ?? 0) + 1);
|
|
644
|
+
}
|
|
645
|
+
const hubs = nodes.map((node) => ({
|
|
646
|
+
relativePath: node,
|
|
647
|
+
importedByCount: importedByCount.get(node) ?? 0,
|
|
648
|
+
importCount: importCount.get(node) ?? 0,
|
|
649
|
+
score: (importedByCount.get(node) ?? 0) * 2 + (importCount.get(node) ?? 0)
|
|
650
|
+
})).filter((h) => h.importedByCount >= 2 || h.score >= 4).sort((a, b) => b.score - a.score);
|
|
651
|
+
const leaves = nodes.filter(
|
|
652
|
+
(node) => (importedByCount.get(node) ?? 0) === 0 && (importCount.get(node) ?? 0) > 0
|
|
653
|
+
);
|
|
654
|
+
return { nodes, edges, hubs, leaves };
|
|
655
|
+
}
|
|
656
|
+
function analyzeFileComplexity(project, projectPath, filePath) {
|
|
657
|
+
const absPath = resolve2(projectPath);
|
|
658
|
+
const sourceFile = project.getSourceFile(filePath);
|
|
659
|
+
if (!sourceFile) return null;
|
|
660
|
+
const relPath = relative2(absPath, sourceFile.getFilePath());
|
|
661
|
+
const functions = [];
|
|
662
|
+
for (const func of sourceFile.getFunctions()) {
|
|
663
|
+
const name = func.getName() ?? "<anonymous>";
|
|
664
|
+
const complexity = calculateCyclomaticComplexity(func);
|
|
665
|
+
functions.push({
|
|
666
|
+
name,
|
|
667
|
+
complexity,
|
|
668
|
+
startLine: func.getStartLineNumber(),
|
|
669
|
+
endLine: func.getEndLineNumber()
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
for (const cls of sourceFile.getClasses()) {
|
|
673
|
+
for (const method of cls.getMethods()) {
|
|
674
|
+
const name = `${cls.getName() ?? "<class>"}.${method.getName()}`;
|
|
675
|
+
const complexity = calculateCyclomaticComplexity(method);
|
|
676
|
+
functions.push({
|
|
677
|
+
name,
|
|
678
|
+
complexity,
|
|
679
|
+
startLine: method.getStartLineNumber(),
|
|
680
|
+
endLine: method.getEndLineNumber()
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const varDecl of sourceFile.getVariableDeclarations()) {
|
|
685
|
+
const init = varDecl.getInitializer();
|
|
686
|
+
if (init && (init.getKind() === SyntaxKind.ArrowFunction || init.getKind() === SyntaxKind.FunctionExpression)) {
|
|
687
|
+
const name = varDecl.getName();
|
|
688
|
+
const complexity = calculateCyclomaticComplexity(init);
|
|
689
|
+
functions.push({
|
|
690
|
+
name,
|
|
691
|
+
complexity,
|
|
692
|
+
startLine: varDecl.getStartLineNumber(),
|
|
693
|
+
endLine: varDecl.getEndLineNumber()
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const totalComplexity = functions.length > 0 ? functions.reduce((sum, f) => sum + f.complexity, 0) : 1;
|
|
698
|
+
const avgComplexity = functions.length > 0 ? totalComplexity / functions.length : 1;
|
|
699
|
+
return {
|
|
700
|
+
relativePath: relPath,
|
|
701
|
+
cyclomaticComplexity: totalComplexity,
|
|
702
|
+
functions: functions.sort((a, b) => b.complexity - a.complexity),
|
|
703
|
+
modelSuggestion: suggestModel(avgComplexity, totalComplexity)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function analyzeAllComplexity(project, projectPath) {
|
|
707
|
+
const results = [];
|
|
708
|
+
const absPath = resolve2(projectPath);
|
|
709
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
710
|
+
const filePath = sourceFile.getFilePath();
|
|
711
|
+
const relPath = relative2(absPath, filePath);
|
|
712
|
+
if (relPath.startsWith("..") || relPath.includes("node_modules")) continue;
|
|
713
|
+
const result = analyzeFileComplexity(project, projectPath, filePath);
|
|
714
|
+
if (result) results.push(result);
|
|
715
|
+
}
|
|
716
|
+
return results.sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
|
|
717
|
+
}
|
|
718
|
+
function enrichFilesWithAST(files, graph, complexities) {
|
|
719
|
+
const importedByMap = /* @__PURE__ */ new Map();
|
|
720
|
+
const importsMap = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const edge of graph.edges) {
|
|
722
|
+
if (!importsMap.has(edge.from)) importsMap.set(edge.from, []);
|
|
723
|
+
importsMap.get(edge.from).push(edge.to);
|
|
724
|
+
if (!importedByMap.has(edge.to)) importedByMap.set(edge.to, []);
|
|
725
|
+
importedByMap.get(edge.to).push(edge.from);
|
|
726
|
+
}
|
|
727
|
+
const hubSet = new Set(graph.hubs.map((h) => h.relativePath));
|
|
728
|
+
const complexityMap = new Map(complexities.map((c) => [c.relativePath, c]));
|
|
729
|
+
return files.map((file) => {
|
|
730
|
+
const imports = importsMap.get(file.relativePath) ?? [];
|
|
731
|
+
const importedBy = importedByMap.get(file.relativePath) ?? [];
|
|
732
|
+
const comp = complexityMap.get(file.relativePath);
|
|
733
|
+
return {
|
|
734
|
+
...file,
|
|
735
|
+
imports,
|
|
736
|
+
importedBy,
|
|
737
|
+
importCount: imports.length,
|
|
738
|
+
importedByCount: importedBy.length,
|
|
739
|
+
isHub: hubSet.has(file.relativePath),
|
|
740
|
+
complexity: comp?.cyclomaticComplexity ?? 0,
|
|
741
|
+
modelSuggestion: comp?.modelSuggestion ?? "sonnet"
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function resolveImport(sourceFile, moduleSpecifier, projectRoot) {
|
|
746
|
+
if (!moduleSpecifier.startsWith(".")) return null;
|
|
747
|
+
const sourceDir = dirname(sourceFile.getFilePath());
|
|
748
|
+
const basePath = resolve2(sourceDir, moduleSpecifier);
|
|
749
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
|
|
750
|
+
for (const ext of extensions) {
|
|
751
|
+
const candidate = basePath.endsWith(ext) ? basePath : basePath + ext;
|
|
752
|
+
if (existsSync2(candidate)) {
|
|
753
|
+
const rel = relative2(projectRoot, candidate);
|
|
754
|
+
if (!rel.startsWith("..")) return rel;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (moduleSpecifier.endsWith(".js")) {
|
|
758
|
+
const tsPath = basePath.replace(/\.js$/, ".ts");
|
|
759
|
+
if (existsSync2(tsPath)) {
|
|
760
|
+
const rel = relative2(projectRoot, tsPath);
|
|
761
|
+
if (!rel.startsWith("..")) return rel;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
function calculateCyclomaticComplexity(node) {
|
|
767
|
+
let complexity = 1;
|
|
768
|
+
node.forEachDescendant((descendant) => {
|
|
769
|
+
switch (descendant.getKind()) {
|
|
770
|
+
case SyntaxKind.IfStatement:
|
|
771
|
+
case SyntaxKind.ConditionalExpression:
|
|
772
|
+
case SyntaxKind.ForStatement:
|
|
773
|
+
case SyntaxKind.ForInStatement:
|
|
774
|
+
case SyntaxKind.ForOfStatement:
|
|
775
|
+
case SyntaxKind.WhileStatement:
|
|
776
|
+
case SyntaxKind.DoStatement:
|
|
777
|
+
case SyntaxKind.CaseClause:
|
|
778
|
+
case SyntaxKind.CatchClause:
|
|
779
|
+
complexity++;
|
|
780
|
+
break;
|
|
781
|
+
case SyntaxKind.BinaryExpression: {
|
|
782
|
+
const text = descendant.getText();
|
|
783
|
+
if (text.includes("&&") || text.includes("||") || text.includes("??")) {
|
|
784
|
+
complexity++;
|
|
785
|
+
}
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return complexity;
|
|
791
|
+
}
|
|
792
|
+
function suggestModel(avgComplexity, totalComplexity) {
|
|
793
|
+
if (avgComplexity > 15 || totalComplexity > 50) return "opus";
|
|
794
|
+
if (avgComplexity > 5 || totalComplexity > 20) return "sonnet";
|
|
795
|
+
return "haiku";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/core/analyzer.ts
|
|
799
|
+
function estimateTokens2(sizeInBytes) {
|
|
800
|
+
return Math.ceil(sizeInBytes / CHARS_PER_TOKEN);
|
|
801
|
+
}
|
|
802
|
+
async function analyzeProject(projectPath) {
|
|
803
|
+
const absPath = resolve3(projectPath);
|
|
804
|
+
const config = await loadProjectConfig(absPath);
|
|
805
|
+
const extensions = getAllExtensions(config);
|
|
806
|
+
const useTiktoken = config.tokenEstimation === "tiktoken";
|
|
807
|
+
const entries = await walkProject(absPath, {
|
|
808
|
+
ignoreDirs: config.ignoreDirs,
|
|
809
|
+
ignorePatterns: config.ignorePatterns,
|
|
810
|
+
extensions
|
|
811
|
+
});
|
|
812
|
+
const files = [];
|
|
813
|
+
for (const entry of entries) {
|
|
814
|
+
let tokens;
|
|
815
|
+
if (useTiktoken) {
|
|
816
|
+
try {
|
|
817
|
+
const content = await readFile3(entry.path, "utf-8");
|
|
818
|
+
tokens = estimateTokens(content, entry.size, "tiktoken");
|
|
819
|
+
} catch {
|
|
820
|
+
tokens = estimateTokens2(entry.size);
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
tokens = estimateTokens2(entry.size);
|
|
824
|
+
}
|
|
825
|
+
const tier = classifyFile(entry, config.tiering);
|
|
826
|
+
files.push({
|
|
827
|
+
path: entry.path,
|
|
828
|
+
relativePath: entry.relativePath,
|
|
829
|
+
size: entry.size,
|
|
830
|
+
tokens,
|
|
831
|
+
extension: entry.extension,
|
|
832
|
+
lastModified: entry.lastModified,
|
|
833
|
+
lastAccessed: entry.lastAccessed,
|
|
834
|
+
tier,
|
|
835
|
+
lines: entry.lines
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
let dependencyGraph;
|
|
839
|
+
try {
|
|
840
|
+
const project = createProject(absPath, files.map((f) => f.path));
|
|
841
|
+
dependencyGraph = buildDependencyGraph(project, absPath);
|
|
842
|
+
const complexities = analyzeAllComplexity(project, absPath);
|
|
843
|
+
const enriched = enrichFilesWithAST(files, dependencyGraph, complexities);
|
|
844
|
+
for (let i = 0; i < enriched.length; i++) {
|
|
845
|
+
files[i] = enriched[i];
|
|
846
|
+
files[i].tier = classifyFileWithAST(enriched[i], config.tiering);
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
if (useTiktoken) freeEncoder();
|
|
851
|
+
files.sort((a, b) => b.tokens - a.tokens);
|
|
852
|
+
const totalTokens = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
853
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
854
|
+
const analysis = {
|
|
855
|
+
projectPath: absPath,
|
|
856
|
+
projectName: basename(absPath),
|
|
857
|
+
totalFiles: files.length,
|
|
858
|
+
totalTokens,
|
|
859
|
+
totalSize,
|
|
860
|
+
files,
|
|
861
|
+
tiers: buildTierSummary(files, totalTokens),
|
|
862
|
+
analyzedAt: /* @__PURE__ */ new Date(),
|
|
863
|
+
dependencyGraph,
|
|
864
|
+
tokenEstimationMethod: config.tokenEstimation
|
|
865
|
+
};
|
|
866
|
+
await saveAnalysis(absPath, analysis);
|
|
867
|
+
return analysis;
|
|
868
|
+
}
|
|
869
|
+
async function saveAnalysis(projectPath, analysis) {
|
|
870
|
+
const analysisPath = getProjectAnalysisPath(projectPath);
|
|
871
|
+
await writeJSON(analysisPath, analysis);
|
|
872
|
+
}
|
|
873
|
+
async function loadAnalysis(projectPath) {
|
|
874
|
+
const absPath = resolve3(projectPath);
|
|
875
|
+
const analysisPath = getProjectAnalysisPath(absPath);
|
|
876
|
+
return readJSON(analysisPath);
|
|
877
|
+
}
|
|
878
|
+
function detectStack(files) {
|
|
879
|
+
const stack = [];
|
|
880
|
+
const extensions = new Set(files.map((f) => f.extension));
|
|
881
|
+
const filenames = new Set(files.map((f) => basename(f.path)));
|
|
882
|
+
if (extensions.has("ts") || extensions.has("tsx")) stack.push("TypeScript");
|
|
883
|
+
else if (extensions.has("js") || extensions.has("jsx")) stack.push("JavaScript");
|
|
884
|
+
if (extensions.has("py")) stack.push("Python");
|
|
885
|
+
if (extensions.has("go")) stack.push("Go");
|
|
886
|
+
if (extensions.has("rs")) stack.push("Rust");
|
|
887
|
+
if (extensions.has("rb")) stack.push("Ruby");
|
|
888
|
+
if (extensions.has("java") || extensions.has("kt")) stack.push("Java/Kotlin");
|
|
889
|
+
if (extensions.has("swift")) stack.push("Swift");
|
|
890
|
+
if (extensions.has("php")) stack.push("PHP");
|
|
891
|
+
if (extensions.has("vue")) stack.push("Vue");
|
|
892
|
+
if (extensions.has("svelte")) stack.push("Svelte");
|
|
893
|
+
if (filenames.has("next.config.js") || filenames.has("next.config.mjs") || filenames.has("next.config.ts")) {
|
|
894
|
+
stack.push("Next.js");
|
|
895
|
+
}
|
|
896
|
+
if (filenames.has("nuxt.config.ts") || filenames.has("nuxt.config.js")) stack.push("Nuxt");
|
|
897
|
+
if (filenames.has("astro.config.mjs")) stack.push("Astro");
|
|
898
|
+
if (filenames.has("vite.config.ts") || filenames.has("vite.config.js")) stack.push("Vite");
|
|
899
|
+
if (filenames.has("tailwind.config.js") || filenames.has("tailwind.config.ts")) stack.push("TailwindCSS");
|
|
900
|
+
if (filenames.has("prisma")) stack.push("Prisma");
|
|
901
|
+
if (filenames.has("docker-compose.yml") || filenames.has("Dockerfile")) stack.push("Docker");
|
|
902
|
+
return stack;
|
|
903
|
+
}
|
|
904
|
+
function detectTestFramework(files) {
|
|
905
|
+
const filenames = files.map((f) => basename(f.path));
|
|
906
|
+
const paths = files.map((f) => f.relativePath);
|
|
907
|
+
if (filenames.some((f) => f.includes(".test.") || f.includes(".spec."))) {
|
|
908
|
+
if (paths.some((p) => p.includes("vitest") || p.includes("vite"))) return "vitest";
|
|
909
|
+
if (paths.some((p) => p.includes("jest"))) return "jest";
|
|
910
|
+
return "jest/vitest";
|
|
911
|
+
}
|
|
912
|
+
if (paths.some((p) => p.includes("pytest") || p.includes("test_"))) return "pytest";
|
|
913
|
+
if (paths.some((p) => p.includes("_test.go"))) return "go test";
|
|
914
|
+
if (paths.some((p) => p.includes("_test.rs"))) return "cargo test";
|
|
915
|
+
return "unknown";
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/core/generator.ts
|
|
919
|
+
import { join as join4 } from "path";
|
|
920
|
+
|
|
921
|
+
// src/core/security.ts
|
|
922
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
923
|
+
import { resolve as resolve4, relative as relative3 } from "path";
|
|
924
|
+
var DEFAULT_SECURITY_CONFIG = {
|
|
925
|
+
encryptionEnabled: false,
|
|
926
|
+
secretDetection: true,
|
|
927
|
+
auditLogging: true,
|
|
928
|
+
retentionDays: 90,
|
|
929
|
+
securePermissions: true
|
|
930
|
+
};
|
|
931
|
+
var BUILTIN_PATTERN_DEFS = [
|
|
932
|
+
// API Keys
|
|
933
|
+
{ type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
|
|
934
|
+
{ type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
|
|
935
|
+
{ type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
|
|
936
|
+
// AWS
|
|
937
|
+
{ type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
|
|
938
|
+
{ 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" },
|
|
939
|
+
// Private Keys
|
|
940
|
+
{ type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
|
|
941
|
+
{ type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
|
|
942
|
+
// Passwords
|
|
943
|
+
{ type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
|
|
944
|
+
{ type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
|
|
945
|
+
// Tokens
|
|
946
|
+
{ 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" },
|
|
947
|
+
{ type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
|
|
948
|
+
{ type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
|
|
949
|
+
{ type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
|
|
950
|
+
{ type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
|
|
951
|
+
// Connection strings
|
|
952
|
+
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
953
|
+
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
954
|
+
// Environment variables with secrets
|
|
955
|
+
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
|
|
956
|
+
];
|
|
957
|
+
function buildPatterns(customPatterns = []) {
|
|
958
|
+
const patterns = BUILTIN_PATTERN_DEFS.map((def) => ({
|
|
959
|
+
type: def.type,
|
|
960
|
+
pattern: new RegExp(def.source, def.flags),
|
|
961
|
+
severity: def.severity,
|
|
962
|
+
description: def.description
|
|
963
|
+
}));
|
|
964
|
+
for (const custom of customPatterns) {
|
|
965
|
+
try {
|
|
966
|
+
patterns.push({
|
|
967
|
+
type: "custom",
|
|
968
|
+
pattern: new RegExp(custom, "gi"),
|
|
969
|
+
severity: "medium",
|
|
970
|
+
description: `Custom pattern: ${custom}`
|
|
971
|
+
});
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return patterns;
|
|
976
|
+
}
|
|
977
|
+
function scanContentForSecrets(content, filePath, customPatterns = []) {
|
|
978
|
+
const findings = [];
|
|
979
|
+
const lines = content.split("\n");
|
|
980
|
+
const allPatterns = buildPatterns(customPatterns);
|
|
981
|
+
for (const secretPattern of allPatterns) {
|
|
982
|
+
for (let i = 0; i < lines.length; i++) {
|
|
983
|
+
const line = lines[i];
|
|
984
|
+
secretPattern.pattern.lastIndex = 0;
|
|
985
|
+
let match;
|
|
986
|
+
while ((match = secretPattern.pattern.exec(line)) !== null) {
|
|
987
|
+
const matchText = match[0];
|
|
988
|
+
if (isTemplateOrPlaceholder(matchText)) continue;
|
|
989
|
+
findings.push({
|
|
990
|
+
type: secretPattern.type,
|
|
991
|
+
file: filePath,
|
|
992
|
+
line: i + 1,
|
|
993
|
+
match: matchText,
|
|
994
|
+
redacted: redactSecret(matchText),
|
|
995
|
+
severity: secretPattern.severity
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return deduplicateFindings(findings);
|
|
1001
|
+
}
|
|
1002
|
+
async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
|
|
1003
|
+
try {
|
|
1004
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1005
|
+
const relPath = relative3(resolve4(projectPath), resolve4(filePath));
|
|
1006
|
+
return scanContentForSecrets(content, relPath, customPatterns);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return [];
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function scanProjectForSecrets(projectPath, filePaths, customPatterns = []) {
|
|
1012
|
+
const allFindings = [];
|
|
1013
|
+
for (const fp of filePaths) {
|
|
1014
|
+
const findings = await scanFileForSecrets(fp, projectPath, customPatterns);
|
|
1015
|
+
allFindings.push(...findings);
|
|
1016
|
+
}
|
|
1017
|
+
return allFindings.sort((a, b) => {
|
|
1018
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1019
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function sanitizeContent(content, customPatterns = []) {
|
|
1023
|
+
let sanitized = content;
|
|
1024
|
+
const allPatterns = buildPatterns(customPatterns);
|
|
1025
|
+
for (const secretPattern of allPatterns) {
|
|
1026
|
+
sanitized = sanitized.replace(secretPattern.pattern, (match) => {
|
|
1027
|
+
if (isTemplateOrPlaceholder(match)) return match;
|
|
1028
|
+
return redactSecret(match);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
return sanitized;
|
|
1032
|
+
}
|
|
1033
|
+
function redactSecret(value) {
|
|
1034
|
+
if (value.length <= 8) return "***REDACTED***";
|
|
1035
|
+
const prefix = value.substring(0, 4);
|
|
1036
|
+
const suffix = value.substring(value.length - 2);
|
|
1037
|
+
return `${prefix}${"*".repeat(Math.min(value.length - 6, 20))}${suffix}`;
|
|
1038
|
+
}
|
|
1039
|
+
function isTemplateOrPlaceholder(value) {
|
|
1040
|
+
const placeholders = [
|
|
1041
|
+
/\$\{.*\}/,
|
|
1042
|
+
/\{\{.*\}\}/,
|
|
1043
|
+
/%[sd]/,
|
|
1044
|
+
/<[A-Z_]+>/,
|
|
1045
|
+
/YOUR_.*_HERE/i,
|
|
1046
|
+
/\bCHANGE_ME\b/i,
|
|
1047
|
+
/\bPLACEHOLDER\b/i,
|
|
1048
|
+
/\bexample\b/i,
|
|
1049
|
+
/\bTODO\b/i,
|
|
1050
|
+
/xxx+/i,
|
|
1051
|
+
/\breplace.?me\b/i,
|
|
1052
|
+
/\bdummy\b/i,
|
|
1053
|
+
/\btest_?key\b/i,
|
|
1054
|
+
/\bsample\b/i
|
|
1055
|
+
];
|
|
1056
|
+
return placeholders.some((p) => p.test(value));
|
|
1057
|
+
}
|
|
1058
|
+
function deduplicateFindings(findings) {
|
|
1059
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1060
|
+
return findings.filter((f) => {
|
|
1061
|
+
const key = `${f.file}:${f.line}:${f.type}:${f.match}`;
|
|
1062
|
+
if (seen.has(key)) return false;
|
|
1063
|
+
seen.add(key);
|
|
1064
|
+
return true;
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/core/generator.ts
|
|
1069
|
+
async function generateClaudeMd(analysis) {
|
|
1070
|
+
const config = await loadProjectConfig(analysis.projectPath);
|
|
1071
|
+
const stack = detectStack(analysis.files);
|
|
1072
|
+
const testFramework = detectTestFramework(analysis.files);
|
|
1073
|
+
const hotFiles = analysis.tiers.hot.files;
|
|
1074
|
+
const warmFiles = analysis.tiers.warm.files;
|
|
1075
|
+
const lines = [];
|
|
1076
|
+
lines.push(`# CLAUDE.md \u2014 ${analysis.projectName}`);
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
lines.push("> Auto-generated by CTO (Claude Token Optimizer)");
|
|
1079
|
+
lines.push(`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
|
|
1080
|
+
lines.push("");
|
|
1081
|
+
lines.push("## Project Overview");
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
if (stack.length > 0) {
|
|
1084
|
+
lines.push(`**Stack:** ${stack.join(", ")}`);
|
|
1085
|
+
}
|
|
1086
|
+
lines.push(`**Files:** ${analysis.totalFiles} | **Tokens:** ~${formatNum(analysis.totalTokens)}`);
|
|
1087
|
+
if (testFramework !== "unknown") {
|
|
1088
|
+
lines.push(`**Test framework:** ${testFramework}`);
|
|
1089
|
+
}
|
|
1090
|
+
lines.push("");
|
|
1091
|
+
lines.push("## Token Budget");
|
|
1092
|
+
lines.push("");
|
|
1093
|
+
lines.push(`| Tier | Files | Tokens | % |`);
|
|
1094
|
+
lines.push(`|------|-------|--------|---|`);
|
|
1095
|
+
lines.push(`| \u{1F525} Hot | ${analysis.tiers.hot.count} | ~${formatNum(analysis.tiers.hot.totalTokens)} | ${analysis.tiers.hot.percentage.toFixed(1)}% |`);
|
|
1096
|
+
lines.push(`| \u{1F321}\uFE0F Warm | ${analysis.tiers.warm.count} | ~${formatNum(analysis.tiers.warm.totalTokens)} | ${analysis.tiers.warm.percentage.toFixed(1)}% |`);
|
|
1097
|
+
lines.push(`| \u2744\uFE0F Cold | ${analysis.tiers.cold.count} | ~${formatNum(analysis.tiers.cold.totalTokens)} | ${analysis.tiers.cold.percentage.toFixed(1)}% |`);
|
|
1098
|
+
lines.push("");
|
|
1099
|
+
if (hotFiles.length > 0) {
|
|
1100
|
+
lines.push("## \u{1F525} Hot Files (Read First)");
|
|
1101
|
+
lines.push("");
|
|
1102
|
+
lines.push("These files are actively being worked on:");
|
|
1103
|
+
lines.push("");
|
|
1104
|
+
for (const f of hotFiles.slice(0, 20)) {
|
|
1105
|
+
lines.push(`- \`${f.relativePath}\` (~${formatNum(f.tokens)} tokens)`);
|
|
1106
|
+
}
|
|
1107
|
+
if (hotFiles.length > 20) {
|
|
1108
|
+
lines.push(`- ... and ${hotFiles.length - 20} more`);
|
|
1109
|
+
}
|
|
1110
|
+
lines.push("");
|
|
1111
|
+
}
|
|
1112
|
+
if (warmFiles.length > 0) {
|
|
1113
|
+
lines.push("## \u{1F321}\uFE0F Warm Files (Read If Needed)");
|
|
1114
|
+
lines.push("");
|
|
1115
|
+
lines.push("Recently touched but not actively changing:");
|
|
1116
|
+
lines.push("");
|
|
1117
|
+
for (const f of warmFiles.slice(0, 15)) {
|
|
1118
|
+
lines.push(`- \`${f.relativePath}\` (~${formatNum(f.tokens)} tokens)`);
|
|
1119
|
+
}
|
|
1120
|
+
if (warmFiles.length > 15) {
|
|
1121
|
+
lines.push(`- ... and ${warmFiles.length - 15} more`);
|
|
1122
|
+
}
|
|
1123
|
+
lines.push("");
|
|
1124
|
+
}
|
|
1125
|
+
lines.push("## Guidelines for Claude");
|
|
1126
|
+
lines.push("");
|
|
1127
|
+
lines.push("1. **Start with hot files** \u2014 they contain the most relevant context");
|
|
1128
|
+
lines.push("2. **Ask before reading cold files** \u2014 they are likely stale or rarely changed");
|
|
1129
|
+
lines.push("3. **Use `/compact` after ~10 exchanges** to free up context window");
|
|
1130
|
+
lines.push("4. **Prefer focused edits** \u2014 avoid reading entire files when only a function is needed");
|
|
1131
|
+
if (config.model === "sonnet") {
|
|
1132
|
+
lines.push("5. **Use Sonnet for most tasks** \u2014 switch to Opus only for complex architectural changes");
|
|
1133
|
+
}
|
|
1134
|
+
lines.push("");
|
|
1135
|
+
const topDirs = getTopDirectories(analysis.files);
|
|
1136
|
+
if (topDirs.length > 0) {
|
|
1137
|
+
lines.push("## Project Structure");
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
for (const dir of topDirs) {
|
|
1140
|
+
lines.push(`- \`${dir.name}/\` \u2014 ${dir.fileCount} files, ~${formatNum(dir.tokens)} tokens`);
|
|
1141
|
+
}
|
|
1142
|
+
lines.push("");
|
|
1143
|
+
}
|
|
1144
|
+
const content = sanitizeContent(lines.join("\n"));
|
|
1145
|
+
const artifact = {
|
|
1146
|
+
type: "claude-md",
|
|
1147
|
+
content,
|
|
1148
|
+
targetPath: join4(analysis.projectPath, "CLAUDE.md"),
|
|
1149
|
+
projectPath: analysis.projectPath
|
|
1150
|
+
};
|
|
1151
|
+
const artifactsDir = getArtifactsDir(analysis.projectPath);
|
|
1152
|
+
await writeText(join4(artifactsDir, "CLAUDE.md"), content);
|
|
1153
|
+
return artifact;
|
|
1154
|
+
}
|
|
1155
|
+
async function generateClaudeignore(analysis) {
|
|
1156
|
+
const config = await loadProjectConfig(analysis.projectPath);
|
|
1157
|
+
const coldFiles = analysis.tiers.cold.files;
|
|
1158
|
+
const lines = [];
|
|
1159
|
+
lines.push("# .claudeignore \u2014 Auto-generated by CTO");
|
|
1160
|
+
lines.push(`# Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
|
|
1161
|
+
lines.push("# Cold-tier files that Claude should skip");
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push("# Dependencies & build output");
|
|
1164
|
+
for (const dir of config.ignoreDirs) {
|
|
1165
|
+
lines.push(dir);
|
|
1166
|
+
}
|
|
1167
|
+
lines.push("");
|
|
1168
|
+
lines.push("# Generated files");
|
|
1169
|
+
for (const pattern of config.ignorePatterns) {
|
|
1170
|
+
lines.push(pattern);
|
|
1171
|
+
}
|
|
1172
|
+
lines.push("");
|
|
1173
|
+
const coldDirs = getColdDirectories(coldFiles);
|
|
1174
|
+
if (coldDirs.length > 0) {
|
|
1175
|
+
lines.push("# Cold directories (rarely modified)");
|
|
1176
|
+
for (const dir of coldDirs) {
|
|
1177
|
+
lines.push(`# ${dir} \u2014 cold tier`);
|
|
1178
|
+
}
|
|
1179
|
+
lines.push("");
|
|
1180
|
+
}
|
|
1181
|
+
lines.push("# Ignored extensions");
|
|
1182
|
+
for (const ext of config.extensions.ignore) {
|
|
1183
|
+
lines.push(`*.${ext}`);
|
|
1184
|
+
}
|
|
1185
|
+
lines.push("");
|
|
1186
|
+
const content = lines.join("\n");
|
|
1187
|
+
const artifact = {
|
|
1188
|
+
type: "claudeignore",
|
|
1189
|
+
content,
|
|
1190
|
+
targetPath: join4(analysis.projectPath, ".claudeignore"),
|
|
1191
|
+
projectPath: analysis.projectPath
|
|
1192
|
+
};
|
|
1193
|
+
const artifactsDir = getArtifactsDir(analysis.projectPath);
|
|
1194
|
+
await writeText(join4(artifactsDir, ".claudeignore"), content);
|
|
1195
|
+
return artifact;
|
|
1196
|
+
}
|
|
1197
|
+
async function loadGeneratedArtifact(projectPath, type) {
|
|
1198
|
+
const artifactsDir = getArtifactsDir(projectPath);
|
|
1199
|
+
const filename = type === "claude-md" ? "CLAUDE.md" : ".claudeignore";
|
|
1200
|
+
return readText(join4(artifactsDir, filename));
|
|
1201
|
+
}
|
|
1202
|
+
function formatNum(n) {
|
|
1203
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1204
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
1205
|
+
return n.toString();
|
|
1206
|
+
}
|
|
1207
|
+
function getTopDirectories(files) {
|
|
1208
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const f of files) {
|
|
1210
|
+
const parts = f.relativePath.split("/");
|
|
1211
|
+
if (parts.length > 1) {
|
|
1212
|
+
const topDir = parts[0];
|
|
1213
|
+
const existing = dirs.get(topDir) ?? { count: 0, tokens: 0 };
|
|
1214
|
+
existing.count++;
|
|
1215
|
+
existing.tokens += f.tokens;
|
|
1216
|
+
dirs.set(topDir, existing);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return Array.from(dirs.entries()).map(([name, data]) => ({ name, fileCount: data.count, tokens: data.tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
|
|
1220
|
+
}
|
|
1221
|
+
function getColdDirectories(coldFiles) {
|
|
1222
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
1223
|
+
for (const f of coldFiles) {
|
|
1224
|
+
const parts = f.relativePath.split("/");
|
|
1225
|
+
if (parts.length > 1) {
|
|
1226
|
+
const dir = parts.slice(0, -1).join("/");
|
|
1227
|
+
dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return Array.from(dirs.entries()).filter(([, count]) => count >= 3).sort((a, b) => b[1] - a[1]).map(([dir]) => dir).slice(0, 20);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/core/session.ts
|
|
1234
|
+
import { join as join5, basename as basename3 } from "path";
|
|
1235
|
+
import { randomUUID } from "crypto";
|
|
1236
|
+
import { createPatch } from "diff";
|
|
1237
|
+
var BACKUP_MANIFEST = "manifest.json";
|
|
1238
|
+
async function loadManifest(projectPath) {
|
|
1239
|
+
const manifestPath = join5(getBackupsDir(projectPath), BACKUP_MANIFEST);
|
|
1240
|
+
return await readJSON(manifestPath) ?? [];
|
|
1241
|
+
}
|
|
1242
|
+
async function saveManifest(projectPath, entries) {
|
|
1243
|
+
const backupsDir = getBackupsDir(projectPath);
|
|
1244
|
+
await ensureDir(backupsDir);
|
|
1245
|
+
await writeJSON(join5(backupsDir, BACKUP_MANIFEST), entries);
|
|
1246
|
+
}
|
|
1247
|
+
async function applyArtifact(artifact) {
|
|
1248
|
+
const backupsDir = getBackupsDir(artifact.projectPath);
|
|
1249
|
+
await ensureDir(backupsDir);
|
|
1250
|
+
const hadOriginal = fileExists(artifact.targetPath);
|
|
1251
|
+
const backupId = randomUUID().substring(0, 8);
|
|
1252
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
1253
|
+
const backupFilename = `${artifact.type}_${backupId}_${timestamp.getTime()}`;
|
|
1254
|
+
const backupPath = join5(backupsDir, backupFilename);
|
|
1255
|
+
if (hadOriginal) {
|
|
1256
|
+
try {
|
|
1257
|
+
await copyFileWithBackup(artifact.targetPath, backupPath);
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
return {
|
|
1260
|
+
success: false,
|
|
1261
|
+
artifact,
|
|
1262
|
+
error: `Failed to create backup: ${err instanceof Error ? err.message : String(err)}`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
await writeText(artifact.targetPath, artifact.content);
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
return {
|
|
1270
|
+
success: false,
|
|
1271
|
+
artifact,
|
|
1272
|
+
error: `Failed to write artifact: ${err instanceof Error ? err.message : String(err)}`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const backup = {
|
|
1276
|
+
id: backupId,
|
|
1277
|
+
artifactType: artifact.type,
|
|
1278
|
+
originalPath: artifact.targetPath,
|
|
1279
|
+
backupPath,
|
|
1280
|
+
timestamp,
|
|
1281
|
+
projectPath: artifact.projectPath,
|
|
1282
|
+
hadOriginal
|
|
1283
|
+
};
|
|
1284
|
+
const manifest = await loadManifest(artifact.projectPath);
|
|
1285
|
+
manifest.push(backup);
|
|
1286
|
+
await saveManifest(artifact.projectPath, manifest);
|
|
1287
|
+
return { success: true, artifact, backup };
|
|
1288
|
+
}
|
|
1289
|
+
async function revertArtifact(projectPath, artifactType) {
|
|
1290
|
+
const manifest = await loadManifest(projectPath);
|
|
1291
|
+
if (manifest.length === 0) {
|
|
1292
|
+
return { success: false, backup: {}, error: "No backups found" };
|
|
1293
|
+
}
|
|
1294
|
+
const candidates = artifactType ? manifest.filter((b) => b.artifactType === artifactType) : manifest;
|
|
1295
|
+
if (candidates.length === 0) {
|
|
1296
|
+
return {
|
|
1297
|
+
success: false,
|
|
1298
|
+
backup: {},
|
|
1299
|
+
error: `No backups found for type: ${artifactType}`
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const latest = candidates[candidates.length - 1];
|
|
1303
|
+
try {
|
|
1304
|
+
if (latest.hadOriginal) {
|
|
1305
|
+
await copyFileWithBackup(latest.backupPath, latest.originalPath);
|
|
1306
|
+
} else {
|
|
1307
|
+
await removeFile(latest.originalPath);
|
|
1308
|
+
}
|
|
1309
|
+
const idx = manifest.findIndex((b) => b.id === latest.id);
|
|
1310
|
+
if (idx >= 0) manifest.splice(idx, 1);
|
|
1311
|
+
await saveManifest(projectPath, manifest);
|
|
1312
|
+
await removeFile(latest.backupPath);
|
|
1313
|
+
return { success: true, backup: latest };
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
return {
|
|
1316
|
+
success: false,
|
|
1317
|
+
backup: latest,
|
|
1318
|
+
error: `Failed to revert: ${err instanceof Error ? err.message : String(err)}`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function diffArtifact(artifact) {
|
|
1323
|
+
const currentContent = fileExists(artifact.targetPath) ? await readText(artifact.targetPath) : null;
|
|
1324
|
+
const oldContent = currentContent ?? "";
|
|
1325
|
+
const newContent = artifact.content;
|
|
1326
|
+
const hasChanges = oldContent !== newContent;
|
|
1327
|
+
const diff = createPatch(
|
|
1328
|
+
basename3(artifact.targetPath),
|
|
1329
|
+
oldContent,
|
|
1330
|
+
newContent,
|
|
1331
|
+
"current",
|
|
1332
|
+
"generated by CTO"
|
|
1333
|
+
);
|
|
1334
|
+
return {
|
|
1335
|
+
artifact,
|
|
1336
|
+
currentContent,
|
|
1337
|
+
diff,
|
|
1338
|
+
hasChanges
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async function listBackups(projectPath) {
|
|
1342
|
+
return loadManifest(projectPath);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/core/clean.ts
|
|
1346
|
+
import { resolve as resolve5 } from "path";
|
|
1347
|
+
async function cleanProject(projectPath) {
|
|
1348
|
+
const absPath = resolve5(projectPath);
|
|
1349
|
+
const projectDir = getProjectDir(absPath);
|
|
1350
|
+
if (!fileExists(projectDir)) {
|
|
1351
|
+
return {
|
|
1352
|
+
success: false,
|
|
1353
|
+
projectPath: absPath,
|
|
1354
|
+
removedDir: projectDir,
|
|
1355
|
+
error: "No CTO data found for this project"
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
const removed = await removeDir(projectDir);
|
|
1360
|
+
return {
|
|
1361
|
+
success: removed,
|
|
1362
|
+
projectPath: absPath,
|
|
1363
|
+
removedDir: projectDir,
|
|
1364
|
+
error: removed ? void 0 : "Failed to remove project directory"
|
|
1365
|
+
};
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
return {
|
|
1368
|
+
success: false,
|
|
1369
|
+
projectPath: absPath,
|
|
1370
|
+
removedDir: projectDir,
|
|
1371
|
+
error: `Clean failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/utils/format.ts
|
|
1377
|
+
import chalk from "chalk";
|
|
1378
|
+
function formatTokens(tokens) {
|
|
1379
|
+
if (tokens >= 1e6) {
|
|
1380
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
1381
|
+
}
|
|
1382
|
+
if (tokens >= 1e3) {
|
|
1383
|
+
return `${(tokens / 1e3).toFixed(1)}K`;
|
|
1384
|
+
}
|
|
1385
|
+
return tokens.toString();
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/core/prompts.ts
|
|
1389
|
+
function getPromptTemplates() {
|
|
1390
|
+
return PROMPT_TEMPLATES;
|
|
1391
|
+
}
|
|
1392
|
+
function getPromptById(id) {
|
|
1393
|
+
return PROMPT_TEMPLATES.find((t) => t.id === id);
|
|
1394
|
+
}
|
|
1395
|
+
function renderPrompt(template, analysis, taskDescription = "") {
|
|
1396
|
+
const stack = detectStack(analysis.files);
|
|
1397
|
+
const testFramework = detectTestFramework(analysis.files);
|
|
1398
|
+
const hotFiles = analysis.tiers.hot.files.slice(0, 15).map((f) => ` - ${f.relativePath} (~${formatTokens(f.tokens)} tokens)`).join("\n");
|
|
1399
|
+
const warmFiles = analysis.tiers.warm.files.slice(0, 10).map((f) => ` - ${f.relativePath} (~${formatTokens(f.tokens)} tokens)`).join("\n");
|
|
1400
|
+
return template.template.replace("{projectName}", analysis.projectName).replace("{stack}", stack.join(", ") || "Not detected").replace("{hotFiles}", hotFiles || " (no hot files)").replace("{warmFiles}", warmFiles || " (no warm files)").replace("{taskDescription}", taskDescription || "[describe your task here]").replace("{testFramework}", testFramework);
|
|
1401
|
+
}
|
|
1402
|
+
function listPromptCategories() {
|
|
1403
|
+
return [...new Set(PROMPT_TEMPLATES.map((t) => t.category))];
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/core/watcher.ts
|
|
1407
|
+
import chokidar from "chokidar";
|
|
1408
|
+
import { resolve as resolve6, extname as extname2 } from "path";
|
|
1409
|
+
import { EventEmitter } from "events";
|
|
1410
|
+
import "fs/promises";
|
|
1411
|
+
var ProjectWatcher = class extends EventEmitter {
|
|
1412
|
+
watcher = null;
|
|
1413
|
+
projectPath;
|
|
1414
|
+
options;
|
|
1415
|
+
currentAnalysis = null;
|
|
1416
|
+
tierCache = /* @__PURE__ */ new Map();
|
|
1417
|
+
debounceTimer = null;
|
|
1418
|
+
pendingChanges = [];
|
|
1419
|
+
extensions = /* @__PURE__ */ new Set();
|
|
1420
|
+
running = false;
|
|
1421
|
+
constructor(projectPath, options = {}) {
|
|
1422
|
+
super();
|
|
1423
|
+
this.projectPath = resolve6(projectPath);
|
|
1424
|
+
this.options = {
|
|
1425
|
+
debounceMs: options.debounceMs ?? 1e3,
|
|
1426
|
+
autoRegenerate: options.autoRegenerate ?? true,
|
|
1427
|
+
verbose: options.verbose ?? false
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
async start() {
|
|
1431
|
+
if (this.running) return;
|
|
1432
|
+
const config = await loadProjectConfig(this.projectPath);
|
|
1433
|
+
const allExtensions = getAllExtensions(config);
|
|
1434
|
+
this.extensions = new Set(allExtensions);
|
|
1435
|
+
this.emit("status", "Performing initial analysis...");
|
|
1436
|
+
this.currentAnalysis = await analyzeProject(this.projectPath);
|
|
1437
|
+
for (const file of this.currentAnalysis.files) {
|
|
1438
|
+
this.tierCache.set(file.relativePath, file.tier);
|
|
1439
|
+
}
|
|
1440
|
+
this.emit("status", `Watching ${this.currentAnalysis.totalFiles} files...`);
|
|
1441
|
+
this.emit("analysis", this.currentAnalysis);
|
|
1442
|
+
const globs = allExtensions.map((ext) => `**/*.${ext}`);
|
|
1443
|
+
this.watcher = chokidar.watch(globs, {
|
|
1444
|
+
cwd: this.projectPath,
|
|
1445
|
+
ignored: [
|
|
1446
|
+
...config.ignoreDirs.map((d) => `**/${d}/**`),
|
|
1447
|
+
...config.ignorePatterns
|
|
1448
|
+
],
|
|
1449
|
+
persistent: true,
|
|
1450
|
+
ignoreInitial: true,
|
|
1451
|
+
awaitWriteFinish: {
|
|
1452
|
+
stabilityThreshold: 300,
|
|
1453
|
+
pollInterval: 100
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
this.watcher.on("add", (path) => this.handleEvent("add", path));
|
|
1457
|
+
this.watcher.on("change", (path) => this.handleEvent("change", path));
|
|
1458
|
+
this.watcher.on("unlink", (path) => this.handleEvent("unlink", path));
|
|
1459
|
+
this.watcher.on("error", (error) => this.emit("error", error));
|
|
1460
|
+
this.running = true;
|
|
1461
|
+
}
|
|
1462
|
+
async stop() {
|
|
1463
|
+
if (!this.running) return;
|
|
1464
|
+
if (this.debounceTimer) {
|
|
1465
|
+
clearTimeout(this.debounceTimer);
|
|
1466
|
+
this.debounceTimer = null;
|
|
1467
|
+
}
|
|
1468
|
+
if (this.watcher) {
|
|
1469
|
+
await this.watcher.close();
|
|
1470
|
+
this.watcher = null;
|
|
1471
|
+
}
|
|
1472
|
+
this.running = false;
|
|
1473
|
+
this.emit("status", "Watcher stopped");
|
|
1474
|
+
}
|
|
1475
|
+
isRunning() {
|
|
1476
|
+
return this.running;
|
|
1477
|
+
}
|
|
1478
|
+
getLastAnalysis() {
|
|
1479
|
+
return this.currentAnalysis;
|
|
1480
|
+
}
|
|
1481
|
+
handleEvent(type, filePath) {
|
|
1482
|
+
const ext = extname2(filePath).slice(1).toLowerCase();
|
|
1483
|
+
if (!this.extensions.has(ext)) return;
|
|
1484
|
+
const event = {
|
|
1485
|
+
type,
|
|
1486
|
+
filePath: resolve6(this.projectPath, filePath),
|
|
1487
|
+
relativePath: filePath,
|
|
1488
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1489
|
+
};
|
|
1490
|
+
this.pendingChanges.push(event);
|
|
1491
|
+
this.emit("fileChange", event);
|
|
1492
|
+
if (this.debounceTimer) {
|
|
1493
|
+
clearTimeout(this.debounceTimer);
|
|
1494
|
+
}
|
|
1495
|
+
this.debounceTimer = setTimeout(() => {
|
|
1496
|
+
this.processPendingChanges();
|
|
1497
|
+
}, this.options.debounceMs);
|
|
1498
|
+
}
|
|
1499
|
+
async processPendingChanges() {
|
|
1500
|
+
const changes = [...this.pendingChanges];
|
|
1501
|
+
this.pendingChanges = [];
|
|
1502
|
+
if (changes.length === 0) return;
|
|
1503
|
+
this.emit("status", `Processing ${changes.length} change(s)...`);
|
|
1504
|
+
const tierChanges = [];
|
|
1505
|
+
for (const change of changes) {
|
|
1506
|
+
if (change.type === "unlink") {
|
|
1507
|
+
const prevTier2 = this.tierCache.get(change.relativePath);
|
|
1508
|
+
if (prevTier2) {
|
|
1509
|
+
this.tierCache.delete(change.relativePath);
|
|
1510
|
+
}
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const prevTier = this.tierCache.get(change.relativePath);
|
|
1514
|
+
if (prevTier) {
|
|
1515
|
+
const newTier = "hot";
|
|
1516
|
+
if (prevTier !== newTier) {
|
|
1517
|
+
tierChanges.push({
|
|
1518
|
+
relativePath: change.relativePath,
|
|
1519
|
+
previousTier: prevTier,
|
|
1520
|
+
newTier,
|
|
1521
|
+
reason: `File ${change.type === "add" ? "created" : "modified"}`
|
|
1522
|
+
});
|
|
1523
|
+
this.tierCache.set(change.relativePath, newTier);
|
|
1524
|
+
}
|
|
1525
|
+
} else {
|
|
1526
|
+
this.tierCache.set(change.relativePath, "hot");
|
|
1527
|
+
tierChanges.push({
|
|
1528
|
+
relativePath: change.relativePath,
|
|
1529
|
+
previousTier: "cold",
|
|
1530
|
+
newTier: "hot",
|
|
1531
|
+
reason: "New file detected"
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
for (const tc of tierChanges) {
|
|
1536
|
+
this.emit("tierChange", tc);
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
this.currentAnalysis = await analyzeProject(this.projectPath);
|
|
1540
|
+
this.emit("analysis", this.currentAnalysis);
|
|
1541
|
+
for (const file of this.currentAnalysis.files) {
|
|
1542
|
+
this.tierCache.set(file.relativePath, file.tier);
|
|
1543
|
+
}
|
|
1544
|
+
if (this.options.autoRegenerate && tierChanges.length > 0) {
|
|
1545
|
+
this.emit("status", "Regenerating artifacts...");
|
|
1546
|
+
const claudeMd = await generateClaudeMd(this.currentAnalysis);
|
|
1547
|
+
const claudeignore = await generateClaudeignore(this.currentAnalysis);
|
|
1548
|
+
this.emit("regenerated", { claudeMd, claudeignore });
|
|
1549
|
+
}
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
this.emit("error", err);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// src/core/sessions.ts
|
|
1557
|
+
import { resolve as resolve7 } from "path";
|
|
1558
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1559
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
1560
|
+
import { join as join6 } from "path";
|
|
1561
|
+
var CURRENT_SESSION_FILE = "current.json";
|
|
1562
|
+
async function startSession(projectPath, description) {
|
|
1563
|
+
const absPath = resolve7(projectPath);
|
|
1564
|
+
const sessionsDir = getSessionsDir(absPath);
|
|
1565
|
+
await ensureDir(sessionsDir);
|
|
1566
|
+
const session = {
|
|
1567
|
+
id: randomUUID2().substring(0, 8),
|
|
1568
|
+
projectPath: absPath,
|
|
1569
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1570
|
+
tokensEstimated: 0,
|
|
1571
|
+
filesRead: [],
|
|
1572
|
+
description,
|
|
1573
|
+
coldFilesRead: 0,
|
|
1574
|
+
warmFilesRead: 0,
|
|
1575
|
+
hotFilesRead: 0,
|
|
1576
|
+
suggestions: []
|
|
1577
|
+
};
|
|
1578
|
+
await writeJSON(join6(sessionsDir, CURRENT_SESSION_FILE), session);
|
|
1579
|
+
return session;
|
|
1580
|
+
}
|
|
1581
|
+
async function endSession(projectPath, analysis) {
|
|
1582
|
+
const absPath = resolve7(projectPath);
|
|
1583
|
+
const sessionsDir = getSessionsDir(absPath);
|
|
1584
|
+
const currentPath = join6(sessionsDir, CURRENT_SESSION_FILE);
|
|
1585
|
+
const session = await readJSON(currentPath);
|
|
1586
|
+
if (!session) return null;
|
|
1587
|
+
session.endedAt = /* @__PURE__ */ new Date();
|
|
1588
|
+
session.suggestions = generateSessionSuggestions(session, analysis);
|
|
1589
|
+
const archiveName = `session_${session.id}_${new Date(session.startedAt).getTime()}.json`;
|
|
1590
|
+
await writeJSON(join6(sessionsDir, archiveName), session);
|
|
1591
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
1592
|
+
try {
|
|
1593
|
+
await unlink2(currentPath);
|
|
1594
|
+
} catch {
|
|
1595
|
+
}
|
|
1596
|
+
return session;
|
|
1597
|
+
}
|
|
1598
|
+
async function getCurrentSession(projectPath) {
|
|
1599
|
+
const absPath = resolve7(projectPath);
|
|
1600
|
+
const sessionsDir = getSessionsDir(absPath);
|
|
1601
|
+
return readJSON(join6(sessionsDir, CURRENT_SESSION_FILE));
|
|
1602
|
+
}
|
|
1603
|
+
async function logFileRead(projectPath, fileRead) {
|
|
1604
|
+
const absPath = resolve7(projectPath);
|
|
1605
|
+
const sessionsDir = getSessionsDir(absPath);
|
|
1606
|
+
const currentPath = join6(sessionsDir, CURRENT_SESSION_FILE);
|
|
1607
|
+
const session = await readJSON(currentPath);
|
|
1608
|
+
if (!session) return;
|
|
1609
|
+
session.filesRead.push(fileRead.relativePath);
|
|
1610
|
+
session.tokensEstimated += fileRead.tokens;
|
|
1611
|
+
switch (fileRead.tier) {
|
|
1612
|
+
case "hot":
|
|
1613
|
+
session.hotFilesRead++;
|
|
1614
|
+
break;
|
|
1615
|
+
case "warm":
|
|
1616
|
+
session.warmFilesRead++;
|
|
1617
|
+
break;
|
|
1618
|
+
case "cold":
|
|
1619
|
+
session.coldFilesRead++;
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
await writeJSON(currentPath, session);
|
|
1623
|
+
}
|
|
1624
|
+
async function listSessions(projectPath, limit = 20) {
|
|
1625
|
+
const absPath = resolve7(projectPath);
|
|
1626
|
+
const sessionsDir = getSessionsDir(absPath);
|
|
1627
|
+
let files;
|
|
1628
|
+
try {
|
|
1629
|
+
files = await readdir2(sessionsDir);
|
|
1630
|
+
} catch {
|
|
1631
|
+
return [];
|
|
1632
|
+
}
|
|
1633
|
+
const sessionFiles = files.filter((f) => f.startsWith("session_") && f.endsWith(".json")).sort().reverse().slice(0, limit);
|
|
1634
|
+
const sessions = [];
|
|
1635
|
+
for (const file of sessionFiles) {
|
|
1636
|
+
const s = await readJSON(join6(sessionsDir, file));
|
|
1637
|
+
if (s) sessions.push(s);
|
|
1638
|
+
}
|
|
1639
|
+
return sessions;
|
|
1640
|
+
}
|
|
1641
|
+
async function getSessionMetrics(projectPath, analysis) {
|
|
1642
|
+
const sessions = await listSessions(projectPath, 100);
|
|
1643
|
+
const totalTokensInProject = analysis?.totalTokens ?? 0;
|
|
1644
|
+
const totalTokensConsumed = sessions.reduce((s, ses) => s + ses.tokensEstimated, 0);
|
|
1645
|
+
const totalTokensSaved = sessions.length * totalTokensInProject - totalTokensConsumed;
|
|
1646
|
+
const fileReadCounts = /* @__PURE__ */ new Map();
|
|
1647
|
+
for (const ses of sessions) {
|
|
1648
|
+
for (const f of ses.filesRead) {
|
|
1649
|
+
const existing = fileReadCounts.get(f) ?? { count: 0, tier: "cold" };
|
|
1650
|
+
existing.count++;
|
|
1651
|
+
const fileInfo = analysis?.files.find((fi) => fi.relativePath === f);
|
|
1652
|
+
if (fileInfo) existing.tier = fileInfo.tier;
|
|
1653
|
+
fileReadCounts.set(f, existing);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const mostReadFiles = Array.from(fileReadCounts.entries()).map(([relativePath, data]) => ({ relativePath, readCount: data.count, tier: data.tier })).sort((a, b) => b.readCount - a.readCount).slice(0, 10);
|
|
1657
|
+
const durationsMs = sessions.filter((s) => s.endedAt).map((s) => new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime());
|
|
1658
|
+
const averageSessionDuration = durationsMs.length > 0 ? durationsMs.reduce((a, b) => a + b, 0) / durationsMs.length / 6e4 : 0;
|
|
1659
|
+
const wastePatterns = detectWastePatterns(sessions, analysis);
|
|
1660
|
+
return {
|
|
1661
|
+
totalSessions: sessions.length,
|
|
1662
|
+
totalTokensConsumed,
|
|
1663
|
+
totalTokensSaved: Math.max(0, totalTokensSaved),
|
|
1664
|
+
savingsPercent: totalTokensInProject > 0 ? Math.max(0, totalTokensSaved / (sessions.length * totalTokensInProject) * 100) : 0,
|
|
1665
|
+
averageSessionDuration,
|
|
1666
|
+
mostReadFiles,
|
|
1667
|
+
wastePatterns
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function detectWastePatterns(sessions, analysis) {
|
|
1671
|
+
const patterns = [];
|
|
1672
|
+
const totalCold = sessions.reduce((s, ses) => s + ses.coldFilesRead, 0);
|
|
1673
|
+
if (totalCold > 5) {
|
|
1674
|
+
patterns.push({
|
|
1675
|
+
type: "cold-reads",
|
|
1676
|
+
description: `${totalCold} cold files read across ${sessions.length} sessions`,
|
|
1677
|
+
impact: totalCold * 500,
|
|
1678
|
+
suggestion: "Review .claudeignore \u2014 cold files are being read frequently"
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
const allReads = sessions.flatMap((s) => s.filesRead);
|
|
1682
|
+
const readCounts = /* @__PURE__ */ new Map();
|
|
1683
|
+
for (const f of allReads) {
|
|
1684
|
+
readCounts.set(f, (readCounts.get(f) ?? 0) + 1);
|
|
1685
|
+
}
|
|
1686
|
+
const repeatedFiles = Array.from(readCounts.entries()).filter(([, count]) => count >= 5);
|
|
1687
|
+
if (repeatedFiles.length > 0) {
|
|
1688
|
+
const topRepeated = repeatedFiles.sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1689
|
+
patterns.push({
|
|
1690
|
+
type: "repeated-reads",
|
|
1691
|
+
description: `${repeatedFiles.length} files read 5+ times: ${topRepeated.map(([f, c]) => `${f} (${c}x)`).join(", ")}`,
|
|
1692
|
+
impact: repeatedFiles.reduce((s, [, c]) => s + c * 200, 0),
|
|
1693
|
+
suggestion: "Consider moving frequently-read files to hot tier"
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
const largeSessions = sessions.filter((s) => s.tokensEstimated > 5e4);
|
|
1697
|
+
if (largeSessions.length > 0) {
|
|
1698
|
+
patterns.push({
|
|
1699
|
+
type: "large-context",
|
|
1700
|
+
description: `${largeSessions.length} sessions with >50K tokens estimated`,
|
|
1701
|
+
impact: largeSessions.reduce((s, ses) => s + ses.tokensEstimated - 5e4, 0),
|
|
1702
|
+
suggestion: "Use /compact more aggressively or narrow the file scope"
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return patterns;
|
|
1706
|
+
}
|
|
1707
|
+
function generateSessionSuggestions(session, analysis) {
|
|
1708
|
+
const suggestions = [];
|
|
1709
|
+
if (session.coldFilesRead > 3) {
|
|
1710
|
+
suggestions.push(`You read ${session.coldFilesRead} cold files \u2014 consider updating .claudeignore`);
|
|
1711
|
+
}
|
|
1712
|
+
if (session.tokensEstimated > 8e4) {
|
|
1713
|
+
suggestions.push("High token usage \u2014 use /compact to free context window");
|
|
1714
|
+
}
|
|
1715
|
+
if (analysis) {
|
|
1716
|
+
const coldFilesRead = session.filesRead.filter((f) => {
|
|
1717
|
+
const fileInfo = analysis.files.find((fi) => fi.relativePath === f);
|
|
1718
|
+
return fileInfo?.tier === "cold";
|
|
1719
|
+
});
|
|
1720
|
+
if (coldFilesRead.length > 0) {
|
|
1721
|
+
suggestions.push(`Move to hot tier: ${coldFilesRead.slice(0, 3).join(", ")}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return suggestions;
|
|
1725
|
+
}
|
|
1726
|
+
async function getTodaySessions(projectPath) {
|
|
1727
|
+
const sessions = await listSessions(projectPath, 50);
|
|
1728
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
1729
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
1730
|
+
return sessions.filter((s) => new Date(s.startedAt) >= todayStart);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/core/dashboard.ts
|
|
1734
|
+
import { resolve as resolve8, basename as basename5 } from "path";
|
|
1735
|
+
import chalk2 from "chalk";
|
|
1736
|
+
import "cli-table3";
|
|
1737
|
+
async function getDashboardData(projectPath) {
|
|
1738
|
+
const absPath = resolve8(projectPath);
|
|
1739
|
+
let analysis = await loadAnalysis(absPath);
|
|
1740
|
+
if (!analysis) {
|
|
1741
|
+
analysis = await analyzeProject(absPath);
|
|
1742
|
+
}
|
|
1743
|
+
const todaySessions = await getTodaySessions(absPath);
|
|
1744
|
+
const metrics = await getSessionMetrics(absPath, analysis);
|
|
1745
|
+
const recentSessions = await listSessions(absPath, 5);
|
|
1746
|
+
const savings = getTokenSavings(analysis.files);
|
|
1747
|
+
const tokensConsumed = todaySessions.reduce((s, ses) => s + ses.tokensEstimated, 0);
|
|
1748
|
+
const tokensSaved = todaySessions.length * analysis.totalTokens - tokensConsumed;
|
|
1749
|
+
return {
|
|
1750
|
+
projectName: basename5(absPath),
|
|
1751
|
+
sessionsToday: todaySessions.length,
|
|
1752
|
+
tokensConsumed,
|
|
1753
|
+
tokensSaved: Math.max(0, tokensSaved),
|
|
1754
|
+
savingsRatio: analysis.totalTokens > 0 ? Math.max(0, tokensSaved / (todaySessions.length * analysis.totalTokens || 1)) * 100 : 0,
|
|
1755
|
+
mostReadFiles: metrics.mostReadFiles.map(({ relativePath, readCount }) => ({ relativePath, readCount })),
|
|
1756
|
+
suggestions: metrics.wastePatterns.map((p) => p.suggestion),
|
|
1757
|
+
tiers: analysis.tiers,
|
|
1758
|
+
recentSessions
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function renderDashboard(data) {
|
|
1762
|
+
const lines = [];
|
|
1763
|
+
const width = 52;
|
|
1764
|
+
const border = "\u2500".repeat(width);
|
|
1765
|
+
lines.push("");
|
|
1766
|
+
lines.push(chalk2.bold.cyan(`\u250C${border}\u2510`));
|
|
1767
|
+
lines.push(chalk2.bold.cyan(`\u2502 \u{1F4CA} CTO Dashboard \u2014 ${pad(data.projectName, width - 22)} \u2502`));
|
|
1768
|
+
lines.push(chalk2.bold.cyan(`\u251C${border}\u2524`));
|
|
1769
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(`Sessions today: ${data.sessionsToday}`, width)} \u2502`));
|
|
1770
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(`Tokens consumed: ~${formatTokens(data.tokensConsumed)}`, width)} \u2502`));
|
|
1771
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(`Tokens saved: ~${formatTokens(data.tokensSaved)}`, width)} \u2502`));
|
|
1772
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(`Savings ratio: ${data.savingsRatio.toFixed(0)}%`, width)} \u2502`));
|
|
1773
|
+
lines.push(chalk2.cyan(`\u251C${border}\u2524`));
|
|
1774
|
+
lines.push(chalk2.cyan(`\u2502 ${pad("Tier Breakdown:", width)} \u2502`));
|
|
1775
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` \u{1F525} Hot: ${data.tiers.hot.count} files (~${formatTokens(data.tiers.hot.totalTokens)} tokens)`, width)} \u2502`));
|
|
1776
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` \u{1F321}\uFE0F Warm: ${data.tiers.warm.count} files (~${formatTokens(data.tiers.warm.totalTokens)} tokens)`, width)} \u2502`));
|
|
1777
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` \u2744\uFE0F Cold: ${data.tiers.cold.count} files (~${formatTokens(data.tiers.cold.totalTokens)} tokens)`, width)} \u2502`));
|
|
1778
|
+
lines.push(chalk2.cyan(`\u251C${border}\u2524`));
|
|
1779
|
+
if (data.mostReadFiles.length > 0) {
|
|
1780
|
+
lines.push(chalk2.cyan(`\u2502 ${pad("Most Read Files:", width)} \u2502`));
|
|
1781
|
+
for (const f of data.mostReadFiles.slice(0, 5)) {
|
|
1782
|
+
const name = f.relativePath.length > 35 ? "..." + f.relativePath.slice(-32) : f.relativePath;
|
|
1783
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` ${f.readCount}x ${name}`, width)} \u2502`));
|
|
1784
|
+
}
|
|
1785
|
+
lines.push(chalk2.cyan(`\u251C${border}\u2524`));
|
|
1786
|
+
}
|
|
1787
|
+
if (data.recentSessions.length > 0) {
|
|
1788
|
+
lines.push(chalk2.cyan(`\u2502 ${pad("Recent Sessions:", width)} \u2502`));
|
|
1789
|
+
for (const s of data.recentSessions.slice(0, 3)) {
|
|
1790
|
+
const date = new Date(s.startedAt);
|
|
1791
|
+
const timeStr = `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
|
|
1792
|
+
const desc = s.description ? s.description.slice(0, 25) : `${s.filesRead.length} files`;
|
|
1793
|
+
const duration = s.endedAt ? `${Math.round((new Date(s.endedAt).getTime() - date.getTime()) / 6e4)}m` : "active";
|
|
1794
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` ${timeStr} \u2014 ${desc} (${duration})`, width)} \u2502`));
|
|
1795
|
+
}
|
|
1796
|
+
lines.push(chalk2.cyan(`\u251C${border}\u2524`));
|
|
1797
|
+
}
|
|
1798
|
+
if (data.suggestions.length > 0) {
|
|
1799
|
+
lines.push(chalk2.cyan(`\u2502 ${pad("\u{1F4A1} Suggestions:", width)} \u2502`));
|
|
1800
|
+
for (const s of data.suggestions.slice(0, 3)) {
|
|
1801
|
+
const trimmed = s.length > width - 4 ? s.slice(0, width - 7) + "..." : s;
|
|
1802
|
+
lines.push(chalk2.cyan(`\u2502 ${pad(` ${trimmed}`, width)} \u2502`));
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
lines.push(chalk2.cyan(`\u2502 ${pad("\u2705 No issues detected", width)} \u2502`));
|
|
1806
|
+
}
|
|
1807
|
+
lines.push(chalk2.bold.cyan(`\u2514${border}\u2518`));
|
|
1808
|
+
lines.push("");
|
|
1809
|
+
return lines.join("\n");
|
|
1810
|
+
}
|
|
1811
|
+
function pad(str, len) {
|
|
1812
|
+
const stripped = str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
1813
|
+
if (stripped.length >= len) return str;
|
|
1814
|
+
return str + " ".repeat(len - stripped.length);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/core/reports.ts
|
|
1818
|
+
import { resolve as resolve9, basename as basename6 } from "path";
|
|
1819
|
+
import chalk3 from "chalk";
|
|
1820
|
+
async function generateWeeklyReport(projectPath) {
|
|
1821
|
+
const absPath = resolve9(projectPath);
|
|
1822
|
+
const now = /* @__PURE__ */ new Date();
|
|
1823
|
+
const weekStart = new Date(now);
|
|
1824
|
+
weekStart.setDate(weekStart.getDate() - 7);
|
|
1825
|
+
weekStart.setHours(0, 0, 0, 0);
|
|
1826
|
+
const allSessions = await listSessions(absPath, 200);
|
|
1827
|
+
const weekSessions = allSessions.filter(
|
|
1828
|
+
(s) => new Date(s.startedAt) >= weekStart
|
|
1829
|
+
);
|
|
1830
|
+
const prevWeekStart = new Date(weekStart);
|
|
1831
|
+
prevWeekStart.setDate(prevWeekStart.getDate() - 7);
|
|
1832
|
+
const prevWeekSessions = allSessions.filter(
|
|
1833
|
+
(s) => new Date(s.startedAt) >= prevWeekStart && new Date(s.startedAt) < weekStart
|
|
1834
|
+
);
|
|
1835
|
+
let analysis = await loadAnalysis(absPath);
|
|
1836
|
+
if (!analysis) {
|
|
1837
|
+
analysis = await analyzeProject(absPath);
|
|
1838
|
+
}
|
|
1839
|
+
const totalTokensConsumed = weekSessions.reduce((s, ses) => s + ses.tokensEstimated, 0);
|
|
1840
|
+
const totalTokensSaved = weekSessions.length * analysis.totalTokens - totalTokensConsumed;
|
|
1841
|
+
const prevTokens = prevWeekSessions.reduce((s, ses) => s + ses.tokensEstimated, 0);
|
|
1842
|
+
const trend = prevTokens > 0 ? (totalTokensConsumed - prevTokens) / prevTokens * 100 : 0;
|
|
1843
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const s of weekSessions) {
|
|
1845
|
+
for (const f of s.filesRead) {
|
|
1846
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const topFiles = Array.from(fileCounts.entries()).map(([relativePath, readCount]) => ({ relativePath, readCount })).sort((a, b) => b.readCount - a.readCount).slice(0, 10);
|
|
1850
|
+
const metrics = await getSessionMetrics(absPath, analysis);
|
|
1851
|
+
const suggestions = metrics.wastePatterns.map((p) => p.suggestion);
|
|
1852
|
+
return {
|
|
1853
|
+
projectPath: absPath,
|
|
1854
|
+
projectName: basename6(absPath),
|
|
1855
|
+
weekStart,
|
|
1856
|
+
weekEnd: now,
|
|
1857
|
+
sessions: weekSessions,
|
|
1858
|
+
totalTokensConsumed,
|
|
1859
|
+
totalTokensSaved: Math.max(0, totalTokensSaved),
|
|
1860
|
+
trend,
|
|
1861
|
+
topFiles,
|
|
1862
|
+
suggestions
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function renderWeeklyReport(report) {
|
|
1866
|
+
const lines = [];
|
|
1867
|
+
lines.push("");
|
|
1868
|
+
lines.push(chalk3.bold.cyan(`\u{1F4CA} Weekly Report \u2014 ${report.projectName}`));
|
|
1869
|
+
lines.push(chalk3.dim(` ${formatDateRange(report.weekStart, report.weekEnd)}`));
|
|
1870
|
+
lines.push("");
|
|
1871
|
+
lines.push(chalk3.bold(" Summary"));
|
|
1872
|
+
lines.push(` Sessions: ${report.sessions.length}`);
|
|
1873
|
+
lines.push(` Tokens consumed: ~${formatTokens(report.totalTokensConsumed)}`);
|
|
1874
|
+
lines.push(` Tokens saved: ~${formatTokens(report.totalTokensSaved)}`);
|
|
1875
|
+
if (report.trend !== 0) {
|
|
1876
|
+
const trendStr = report.trend > 0 ? chalk3.red(`+${report.trend.toFixed(1)}% vs last week`) : chalk3.green(`${report.trend.toFixed(1)}% vs last week`);
|
|
1877
|
+
lines.push(` Trend: ${trendStr}`);
|
|
1878
|
+
}
|
|
1879
|
+
lines.push("");
|
|
1880
|
+
if (report.topFiles.length > 0) {
|
|
1881
|
+
lines.push(chalk3.bold(" Most Read Files"));
|
|
1882
|
+
for (const f of report.topFiles.slice(0, 5)) {
|
|
1883
|
+
lines.push(` ${f.readCount}x ${f.relativePath}`);
|
|
1884
|
+
}
|
|
1885
|
+
lines.push("");
|
|
1886
|
+
}
|
|
1887
|
+
if (report.sessions.length > 0) {
|
|
1888
|
+
lines.push(chalk3.bold(" Sessions"));
|
|
1889
|
+
for (const s of report.sessions.slice(0, 10)) {
|
|
1890
|
+
const date = new Date(s.startedAt);
|
|
1891
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
1892
|
+
const desc = s.description ?? `${s.filesRead.length} files`;
|
|
1893
|
+
const tokens = `~${formatTokens(s.tokensEstimated)}`;
|
|
1894
|
+
lines.push(` ${dateStr} ${tokens.padEnd(8)} ${desc}`);
|
|
1895
|
+
}
|
|
1896
|
+
if (report.sessions.length > 10) {
|
|
1897
|
+
lines.push(chalk3.dim(` ... and ${report.sessions.length - 10} more`));
|
|
1898
|
+
}
|
|
1899
|
+
lines.push("");
|
|
1900
|
+
}
|
|
1901
|
+
if (report.suggestions.length > 0) {
|
|
1902
|
+
lines.push(chalk3.bold(" \u{1F4A1} Suggestions"));
|
|
1903
|
+
for (const s of report.suggestions) {
|
|
1904
|
+
lines.push(` \u2022 ${s}`);
|
|
1905
|
+
}
|
|
1906
|
+
lines.push("");
|
|
1907
|
+
}
|
|
1908
|
+
return lines.join("\n");
|
|
1909
|
+
}
|
|
1910
|
+
async function generateProjectReport(projectPaths) {
|
|
1911
|
+
const lines = [];
|
|
1912
|
+
lines.push("");
|
|
1913
|
+
lines.push(chalk3.bold.cyan("\u{1F4CA} Project Comparison Report"));
|
|
1914
|
+
lines.push("");
|
|
1915
|
+
for (const p of projectPaths) {
|
|
1916
|
+
const absPath = resolve9(p);
|
|
1917
|
+
let analysis = await loadAnalysis(absPath);
|
|
1918
|
+
if (!analysis) {
|
|
1919
|
+
try {
|
|
1920
|
+
analysis = await analyzeProject(absPath);
|
|
1921
|
+
} catch {
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const metrics = await getSessionMetrics(absPath, analysis);
|
|
1926
|
+
const name = basename6(absPath);
|
|
1927
|
+
lines.push(chalk3.bold(` ${name}`));
|
|
1928
|
+
lines.push(` Files: ${analysis.totalFiles} | Tokens: ~${formatTokens(analysis.totalTokens)}`);
|
|
1929
|
+
lines.push(` Sessions: ${metrics.totalSessions} | Consumed: ~${formatTokens(metrics.totalTokensConsumed)}`);
|
|
1930
|
+
lines.push(` Savings: ~${formatTokens(metrics.totalTokensSaved)} (${metrics.savingsPercent.toFixed(0)}%)`);
|
|
1931
|
+
if (metrics.wastePatterns.length > 0) {
|
|
1932
|
+
lines.push(` \u26A0\uFE0F ${metrics.wastePatterns.length} waste pattern(s) detected`);
|
|
1933
|
+
}
|
|
1934
|
+
lines.push("");
|
|
1935
|
+
}
|
|
1936
|
+
return lines.join("\n");
|
|
1937
|
+
}
|
|
1938
|
+
async function exportMetrics(projectPath, format = "json") {
|
|
1939
|
+
const absPath = resolve9(projectPath);
|
|
1940
|
+
const sessions = await listSessions(absPath, 1e3);
|
|
1941
|
+
let analysis = await loadAnalysis(absPath);
|
|
1942
|
+
if (!analysis) analysis = await analyzeProject(absPath);
|
|
1943
|
+
const metrics = await getSessionMetrics(absPath, analysis);
|
|
1944
|
+
if (format === "json") {
|
|
1945
|
+
return JSON.stringify({ metrics, sessions, analysis: { totalFiles: analysis.totalFiles, totalTokens: analysis.totalTokens } }, null, 2);
|
|
1946
|
+
}
|
|
1947
|
+
const header = "session_id,started_at,ended_at,tokens_estimated,files_read,hot_files,warm_files,cold_files,description";
|
|
1948
|
+
const rows = sessions.map(
|
|
1949
|
+
(s) => `${s.id},${s.startedAt},${s.endedAt ?? ""},${s.tokensEstimated},${s.filesRead.length},${s.hotFilesRead},${s.warmFilesRead},${s.coldFilesRead},"${(s.description ?? "").replace(/"/g, '""')}"`
|
|
1950
|
+
);
|
|
1951
|
+
return [header, ...rows].join("\n");
|
|
1952
|
+
}
|
|
1953
|
+
function formatDateRange(start, end) {
|
|
1954
|
+
const fmt = (d) => `${d.getMonth() + 1}/${d.getDate()}`;
|
|
1955
|
+
return `${fmt(start)} \u2014 ${fmt(end)}`;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// src/core/audit.ts
|
|
1959
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1960
|
+
import { createHash as createHash2 } from "crypto";
|
|
1961
|
+
import { readdir as readdir3, chmod } from "fs/promises";
|
|
1962
|
+
import { join as join7 } from "path";
|
|
1963
|
+
import { userInfo } from "os";
|
|
1964
|
+
var AUDIT_DIR = "audit";
|
|
1965
|
+
var MAX_ENTRIES_PER_FILE = 500;
|
|
1966
|
+
function getAuditDir() {
|
|
1967
|
+
return join7(getCTORoot(), AUDIT_DIR);
|
|
1968
|
+
}
|
|
1969
|
+
function getCurrentAuditFile() {
|
|
1970
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
1971
|
+
return join7(getAuditDir(), `audit_${date}.json`);
|
|
1972
|
+
}
|
|
1973
|
+
function computeIntegrityHash(entry) {
|
|
1974
|
+
const payload = JSON.stringify({
|
|
1975
|
+
id: entry.id,
|
|
1976
|
+
timestamp: entry.timestamp,
|
|
1977
|
+
action: entry.action,
|
|
1978
|
+
user: entry.user,
|
|
1979
|
+
projectPath: entry.projectPath,
|
|
1980
|
+
details: entry.details
|
|
1981
|
+
});
|
|
1982
|
+
return createHash2("sha256").update(payload).digest("hex");
|
|
1983
|
+
}
|
|
1984
|
+
async function logAudit(action, projectPath, details = {}) {
|
|
1985
|
+
const auditDir = getAuditDir();
|
|
1986
|
+
await ensureDir(auditDir);
|
|
1987
|
+
let currentUser;
|
|
1988
|
+
try {
|
|
1989
|
+
currentUser = userInfo().username;
|
|
1990
|
+
} catch {
|
|
1991
|
+
currentUser = process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
1992
|
+
}
|
|
1993
|
+
const partialEntry = {
|
|
1994
|
+
id: randomUUID3().substring(0, 12),
|
|
1995
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1996
|
+
action,
|
|
1997
|
+
user: currentUser,
|
|
1998
|
+
projectPath,
|
|
1999
|
+
details
|
|
2000
|
+
};
|
|
2001
|
+
const entry = {
|
|
2002
|
+
...partialEntry,
|
|
2003
|
+
integrityHash: computeIntegrityHash(partialEntry)
|
|
2004
|
+
};
|
|
2005
|
+
const auditFile = getCurrentAuditFile();
|
|
2006
|
+
let entries = await readJSON(auditFile) ?? [];
|
|
2007
|
+
entries.push(entry);
|
|
2008
|
+
if (entries.length > MAX_ENTRIES_PER_FILE) {
|
|
2009
|
+
entries = entries.slice(-MAX_ENTRIES_PER_FILE);
|
|
2010
|
+
}
|
|
2011
|
+
await writeJSON(auditFile, entries);
|
|
2012
|
+
try {
|
|
2013
|
+
await chmod(auditFile, 384);
|
|
2014
|
+
} catch {
|
|
2015
|
+
}
|
|
2016
|
+
return entry;
|
|
2017
|
+
}
|
|
2018
|
+
async function getAuditEntries(options = {}) {
|
|
2019
|
+
const auditDir = getAuditDir();
|
|
2020
|
+
let files;
|
|
2021
|
+
try {
|
|
2022
|
+
files = await readdir3(auditDir);
|
|
2023
|
+
} catch {
|
|
2024
|
+
return [];
|
|
2025
|
+
}
|
|
2026
|
+
const auditFiles = files.filter((f) => f.startsWith("audit_") && f.endsWith(".json")).sort().reverse();
|
|
2027
|
+
const allEntries = [];
|
|
2028
|
+
const limit = options.limit ?? 100;
|
|
2029
|
+
for (const file of auditFiles) {
|
|
2030
|
+
if (allEntries.length >= limit) break;
|
|
2031
|
+
const entries = await readJSON(join7(auditDir, file));
|
|
2032
|
+
if (!entries) continue;
|
|
2033
|
+
for (const entry of entries.reverse()) {
|
|
2034
|
+
if (allEntries.length >= limit) break;
|
|
2035
|
+
if (options.projectPath && entry.projectPath !== options.projectPath) continue;
|
|
2036
|
+
if (options.action && entry.action !== options.action) continue;
|
|
2037
|
+
if (options.since && new Date(entry.timestamp) < options.since) continue;
|
|
2038
|
+
allEntries.push(entry);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return allEntries;
|
|
2042
|
+
}
|
|
2043
|
+
function verifyAuditEntry(entry) {
|
|
2044
|
+
const { integrityHash, ...rest } = entry;
|
|
2045
|
+
const expected = computeIntegrityHash(rest);
|
|
2046
|
+
return expected === integrityHash;
|
|
2047
|
+
}
|
|
2048
|
+
async function verifyAuditIntegrity() {
|
|
2049
|
+
const entries = await getAuditEntries({ limit: 1e4 });
|
|
2050
|
+
const invalidEntries = [];
|
|
2051
|
+
for (const entry of entries) {
|
|
2052
|
+
if (!verifyAuditEntry(entry)) {
|
|
2053
|
+
invalidEntries.push(entry);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return {
|
|
2057
|
+
totalEntries: entries.length,
|
|
2058
|
+
validEntries: entries.length - invalidEntries.length,
|
|
2059
|
+
invalidEntries
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
async function purgeOldAuditEntries(retentionDays) {
|
|
2063
|
+
const auditDir = getAuditDir();
|
|
2064
|
+
let files;
|
|
2065
|
+
try {
|
|
2066
|
+
files = await readdir3(auditDir);
|
|
2067
|
+
} catch {
|
|
2068
|
+
return 0;
|
|
2069
|
+
}
|
|
2070
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
2071
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
2072
|
+
const cutoffStr = cutoff.toISOString().split("T")[0].replace(/-/g, "");
|
|
2073
|
+
let purged = 0;
|
|
2074
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2075
|
+
for (const file of files) {
|
|
2076
|
+
if (!file.startsWith("audit_") || !file.endsWith(".json")) continue;
|
|
2077
|
+
const dateStr = file.replace("audit_", "").replace(".json", "");
|
|
2078
|
+
if (dateStr < cutoffStr) {
|
|
2079
|
+
try {
|
|
2080
|
+
await unlink2(join7(auditDir, file));
|
|
2081
|
+
purged++;
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return purged;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/core/integrity.ts
|
|
2090
|
+
import { createHash as createHash3 } from "crypto";
|
|
2091
|
+
import { readFile as readFile6, readdir as readdir4, stat as stat2, chmod as chmod2 } from "fs/promises";
|
|
2092
|
+
import { join as join8 } from "path";
|
|
2093
|
+
var MANIFEST_FILE = "integrity.json";
|
|
2094
|
+
function hashContent(content) {
|
|
2095
|
+
return createHash3("sha256").update(content).digest("hex");
|
|
2096
|
+
}
|
|
2097
|
+
async function hashFile(filePath) {
|
|
2098
|
+
try {
|
|
2099
|
+
const content = await readFile6(filePath);
|
|
2100
|
+
return hashContent(content);
|
|
2101
|
+
} catch {
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async function buildIntegrityManifest(projectPath) {
|
|
2106
|
+
const projectDir = getProjectDir(projectPath);
|
|
2107
|
+
const entries = [];
|
|
2108
|
+
const artifactsDir = getArtifactsDir(projectPath);
|
|
2109
|
+
await hashDirEntries(artifactsDir, entries, "artifact");
|
|
2110
|
+
const backupsDir = getBackupsDir(projectPath);
|
|
2111
|
+
await hashDirEntries(backupsDir, entries, "backup");
|
|
2112
|
+
const sessionsDir = getSessionsDir(projectPath);
|
|
2113
|
+
await hashDirEntries(sessionsDir, entries, "session");
|
|
2114
|
+
const configFile = join8(projectDir, "config.yaml");
|
|
2115
|
+
const configHash = await hashFile(configFile);
|
|
2116
|
+
if (configHash) {
|
|
2117
|
+
const s = await stat2(configFile);
|
|
2118
|
+
entries.push({
|
|
2119
|
+
filePath: "config.yaml",
|
|
2120
|
+
hash: configHash,
|
|
2121
|
+
size: s.size,
|
|
2122
|
+
createdAt: s.mtime,
|
|
2123
|
+
type: "config"
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
const manifest = {
|
|
2127
|
+
version: CTO_VERSION,
|
|
2128
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2129
|
+
entries
|
|
2130
|
+
};
|
|
2131
|
+
const manifestPath = join8(projectDir, MANIFEST_FILE);
|
|
2132
|
+
await writeJSON(manifestPath, manifest);
|
|
2133
|
+
try {
|
|
2134
|
+
await chmod2(manifestPath, 384);
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2137
|
+
return manifest;
|
|
2138
|
+
}
|
|
2139
|
+
async function verifyIntegrity(projectPath) {
|
|
2140
|
+
const projectDir = getProjectDir(projectPath);
|
|
2141
|
+
const manifestPath = join8(projectDir, MANIFEST_FILE);
|
|
2142
|
+
const manifest = await readJSON(manifestPath);
|
|
2143
|
+
if (!manifest) {
|
|
2144
|
+
return { valid: false, total: 0, verified: 0, corrupted: [], missing: ["integrity.json"] };
|
|
2145
|
+
}
|
|
2146
|
+
const corrupted = [];
|
|
2147
|
+
const missing = [];
|
|
2148
|
+
let verified = 0;
|
|
2149
|
+
for (const entry of manifest.entries) {
|
|
2150
|
+
const fullPath = join8(projectDir, getSubDir(entry.type), entry.filePath);
|
|
2151
|
+
const currentHash = await hashFile(fullPath);
|
|
2152
|
+
if (currentHash === null) {
|
|
2153
|
+
missing.push(entry.filePath);
|
|
2154
|
+
} else if (currentHash !== entry.hash) {
|
|
2155
|
+
corrupted.push(entry.filePath);
|
|
2156
|
+
} else {
|
|
2157
|
+
verified++;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
valid: corrupted.length === 0 && missing.length === 0,
|
|
2162
|
+
total: manifest.entries.length,
|
|
2163
|
+
verified,
|
|
2164
|
+
corrupted,
|
|
2165
|
+
missing
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
async function secureFilePermissions(projectPath) {
|
|
2169
|
+
const projectDir = getProjectDir(projectPath);
|
|
2170
|
+
let count = 0;
|
|
2171
|
+
async function secureDirRecursive(dir) {
|
|
2172
|
+
let entries;
|
|
2173
|
+
try {
|
|
2174
|
+
entries = await readdir4(dir);
|
|
2175
|
+
} catch {
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
for (const entry of entries) {
|
|
2179
|
+
const fullPath = join8(dir, entry);
|
|
2180
|
+
try {
|
|
2181
|
+
const s = await stat2(fullPath);
|
|
2182
|
+
if (s.isDirectory()) {
|
|
2183
|
+
await chmod2(fullPath, 448);
|
|
2184
|
+
count++;
|
|
2185
|
+
await secureDirRecursive(fullPath);
|
|
2186
|
+
} else {
|
|
2187
|
+
await chmod2(fullPath, 384);
|
|
2188
|
+
count++;
|
|
2189
|
+
}
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
try {
|
|
2195
|
+
await chmod2(projectDir, 448);
|
|
2196
|
+
count++;
|
|
2197
|
+
await secureDirRecursive(projectDir);
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
2200
|
+
return count;
|
|
2201
|
+
}
|
|
2202
|
+
async function purgeOldData(projectPath, retentionDays) {
|
|
2203
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
2204
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
2205
|
+
let purgedSessions = 0;
|
|
2206
|
+
let purgedBackups = 0;
|
|
2207
|
+
const sessionsDir = getSessionsDir(projectPath);
|
|
2208
|
+
purgedSessions = await purgeOldFiles(sessionsDir, cutoff, "session_");
|
|
2209
|
+
return { purgedSessions, purgedBackups };
|
|
2210
|
+
}
|
|
2211
|
+
async function purgeOldFiles(dir, cutoff, prefix) {
|
|
2212
|
+
let count = 0;
|
|
2213
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2214
|
+
let files;
|
|
2215
|
+
try {
|
|
2216
|
+
files = await readdir4(dir);
|
|
2217
|
+
} catch {
|
|
2218
|
+
return 0;
|
|
2219
|
+
}
|
|
2220
|
+
for (const file of files) {
|
|
2221
|
+
if (!file.startsWith(prefix)) continue;
|
|
2222
|
+
const fullPath = join8(dir, file);
|
|
2223
|
+
try {
|
|
2224
|
+
const s = await stat2(fullPath);
|
|
2225
|
+
if (s.mtime < cutoff) {
|
|
2226
|
+
await unlink2(fullPath);
|
|
2227
|
+
count++;
|
|
2228
|
+
}
|
|
2229
|
+
} catch {
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return count;
|
|
2233
|
+
}
|
|
2234
|
+
async function hashDirEntries(dir, entries, type) {
|
|
2235
|
+
let files;
|
|
2236
|
+
try {
|
|
2237
|
+
files = await readdir4(dir);
|
|
2238
|
+
} catch {
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
for (const file of files) {
|
|
2242
|
+
const fullPath = join8(dir, file);
|
|
2243
|
+
try {
|
|
2244
|
+
const s = await stat2(fullPath);
|
|
2245
|
+
if (!s.isFile()) continue;
|
|
2246
|
+
const hash = await hashFile(fullPath);
|
|
2247
|
+
if (hash) {
|
|
2248
|
+
entries.push({
|
|
2249
|
+
filePath: file,
|
|
2250
|
+
hash,
|
|
2251
|
+
size: s.size,
|
|
2252
|
+
createdAt: s.mtime,
|
|
2253
|
+
type
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function getSubDir(type) {
|
|
2261
|
+
switch (type) {
|
|
2262
|
+
case "artifact":
|
|
2263
|
+
return "artifacts";
|
|
2264
|
+
case "backup":
|
|
2265
|
+
return "backups";
|
|
2266
|
+
case "session":
|
|
2267
|
+
return "sessions";
|
|
2268
|
+
case "config":
|
|
2269
|
+
return "";
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/core/pruning.ts
|
|
2274
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2275
|
+
import { extname as extname3 } from "path";
|
|
2276
|
+
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
2277
|
+
function getPruneLevelForTier(tier) {
|
|
2278
|
+
switch (tier) {
|
|
2279
|
+
case "hot":
|
|
2280
|
+
return "full";
|
|
2281
|
+
case "warm":
|
|
2282
|
+
return "signatures";
|
|
2283
|
+
case "cold":
|
|
2284
|
+
return "skeleton";
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
async function pruneFile(file, level) {
|
|
2288
|
+
const pruneLevel = level ?? getPruneLevelForTier(file.tier);
|
|
2289
|
+
if (pruneLevel === "full" || pruneLevel === "none") {
|
|
2290
|
+
let content2 = "";
|
|
2291
|
+
try {
|
|
2292
|
+
content2 = await readFile7(file.path, "utf-8");
|
|
2293
|
+
} catch {
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
relativePath: file.relativePath,
|
|
2297
|
+
originalTokens: file.tokens,
|
|
2298
|
+
prunedTokens: pruneLevel === "none" ? 0 : file.tokens,
|
|
2299
|
+
pruneLevel,
|
|
2300
|
+
content: pruneLevel === "none" ? "" : content2,
|
|
2301
|
+
savings: pruneLevel === "none" ? 100 : 0
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
let content = "";
|
|
2305
|
+
try {
|
|
2306
|
+
content = await readFile7(file.path, "utf-8");
|
|
2307
|
+
} catch {
|
|
2308
|
+
return emptyPruned(file, pruneLevel);
|
|
2309
|
+
}
|
|
2310
|
+
const ext = extname3(file.relativePath).slice(1).toLowerCase();
|
|
2311
|
+
const isTS = TS_EXTENSIONS2.has(ext);
|
|
2312
|
+
let prunedContent;
|
|
2313
|
+
if (isTS) {
|
|
2314
|
+
prunedContent = pruneLevel === "signatures" ? extractSignatures(content) : extractSkeleton(content);
|
|
2315
|
+
} else {
|
|
2316
|
+
prunedContent = pruneLevel === "signatures" ? extractGenericSignatures(content, ext) : extractGenericSkeleton(content);
|
|
2317
|
+
}
|
|
2318
|
+
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
2319
|
+
const savings = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
2320
|
+
return {
|
|
2321
|
+
relativePath: file.relativePath,
|
|
2322
|
+
originalTokens: file.tokens,
|
|
2323
|
+
prunedTokens,
|
|
2324
|
+
pruneLevel,
|
|
2325
|
+
content: prunedContent,
|
|
2326
|
+
savings: Math.max(0, savings)
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
async function pruneProject(files) {
|
|
2330
|
+
const prunedFiles = [];
|
|
2331
|
+
for (const file of files) {
|
|
2332
|
+
const pruned = await pruneFile(file);
|
|
2333
|
+
prunedFiles.push(pruned);
|
|
2334
|
+
}
|
|
2335
|
+
const totalOriginalTokens = prunedFiles.reduce((s, f) => s + f.originalTokens, 0);
|
|
2336
|
+
const totalPrunedTokens = prunedFiles.reduce((s, f) => s + f.prunedTokens, 0);
|
|
2337
|
+
const totalSavings = totalOriginalTokens - totalPrunedTokens;
|
|
2338
|
+
return {
|
|
2339
|
+
files: prunedFiles,
|
|
2340
|
+
totalOriginalTokens,
|
|
2341
|
+
totalPrunedTokens,
|
|
2342
|
+
totalSavings,
|
|
2343
|
+
savingsPercent: totalOriginalTokens > 0 ? totalSavings / totalOriginalTokens * 100 : 0
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function extractSignatures(content) {
|
|
2347
|
+
const lines = content.split("\n");
|
|
2348
|
+
const result = [];
|
|
2349
|
+
let braceDepth = 0;
|
|
2350
|
+
let inFunctionBody = false;
|
|
2351
|
+
let skipUntilBraceClose = false;
|
|
2352
|
+
let functionBraceStart = 0;
|
|
2353
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2354
|
+
const line = lines[i];
|
|
2355
|
+
const trimmed = line.trim();
|
|
2356
|
+
if (braceDepth === 0 || !inFunctionBody) {
|
|
2357
|
+
if (isImportOrExport(trimmed) || isTypeDefinition(trimmed) || isComment(trimmed) || trimmed === "") {
|
|
2358
|
+
result.push(line);
|
|
2359
|
+
updateBraceDepth(trimmed);
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (!inFunctionBody && isFunctionSignature(trimmed)) {
|
|
2364
|
+
result.push(line);
|
|
2365
|
+
const openBraces = (trimmed.match(/\{/g) || []).length;
|
|
2366
|
+
const closeBraces = (trimmed.match(/\}/g) || []).length;
|
|
2367
|
+
if (openBraces > closeBraces) {
|
|
2368
|
+
inFunctionBody = true;
|
|
2369
|
+
functionBraceStart = braceDepth + openBraces - closeBraces;
|
|
2370
|
+
result.push(" // ... implementation omitted");
|
|
2371
|
+
}
|
|
2372
|
+
braceDepth += openBraces - closeBraces;
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
if (inFunctionBody) {
|
|
2376
|
+
const openBraces = (trimmed.match(/\{/g) || []).length;
|
|
2377
|
+
const closeBraces = (trimmed.match(/\}/g) || []).length;
|
|
2378
|
+
braceDepth += openBraces - closeBraces;
|
|
2379
|
+
if (braceDepth < functionBraceStart) {
|
|
2380
|
+
result.push(line);
|
|
2381
|
+
inFunctionBody = false;
|
|
2382
|
+
}
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (isClassOrInterface(trimmed) || isPropertySignature(trimmed)) {
|
|
2386
|
+
result.push(line);
|
|
2387
|
+
}
|
|
2388
|
+
updateBraceDepthLine(line);
|
|
2389
|
+
}
|
|
2390
|
+
return result.join("\n");
|
|
2391
|
+
function updateBraceDepth(trimmed) {
|
|
2392
|
+
braceDepth += (trimmed.match(/\{/g) || []).length;
|
|
2393
|
+
braceDepth -= (trimmed.match(/\}/g) || []).length;
|
|
2394
|
+
}
|
|
2395
|
+
function updateBraceDepthLine(line) {
|
|
2396
|
+
const t = line.trim();
|
|
2397
|
+
braceDepth += (t.match(/\{/g) || []).length;
|
|
2398
|
+
braceDepth -= (t.match(/\}/g) || []).length;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function extractSkeleton(content) {
|
|
2402
|
+
const lines = content.split("\n");
|
|
2403
|
+
const result = [];
|
|
2404
|
+
for (const line of lines) {
|
|
2405
|
+
const trimmed = line.trim();
|
|
2406
|
+
if (isImportOrExport(trimmed) || isTypeDefinition(trimmed) || isClassOrInterface(trimmed) || isFunctionSignature(trimmed) || trimmed.startsWith("//") || trimmed === "") {
|
|
2407
|
+
if (isFunctionSignature(trimmed) && trimmed.includes("{")) {
|
|
2408
|
+
result.push(line.split("{")[0].trimEnd() + ";");
|
|
2409
|
+
} else {
|
|
2410
|
+
result.push(line);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
return result.join("\n");
|
|
2415
|
+
}
|
|
2416
|
+
function extractGenericSignatures(content, ext) {
|
|
2417
|
+
const lines = content.split("\n");
|
|
2418
|
+
const result = [];
|
|
2419
|
+
if (ext === "py") {
|
|
2420
|
+
let inBody = false;
|
|
2421
|
+
let bodyIndent = 0;
|
|
2422
|
+
for (const line of lines) {
|
|
2423
|
+
const trimmed = line.trim();
|
|
2424
|
+
const indent = line.length - line.trimStart().length;
|
|
2425
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("from ") || trimmed === "") {
|
|
2426
|
+
result.push(line);
|
|
2427
|
+
inBody = false;
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (trimmed.startsWith("def ") || trimmed.startsWith("class ") || trimmed.startsWith("async def ")) {
|
|
2431
|
+
result.push(line);
|
|
2432
|
+
inBody = true;
|
|
2433
|
+
bodyIndent = indent;
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (inBody && trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
|
|
2437
|
+
result.push(line);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
if (inBody && indent > bodyIndent && result.length > 0) {
|
|
2441
|
+
if (!result[result.length - 1].includes("...")) {
|
|
2442
|
+
result.push(" ".repeat(bodyIndent + 4) + "...");
|
|
2443
|
+
}
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (indent <= bodyIndent) {
|
|
2447
|
+
inBody = false;
|
|
2448
|
+
}
|
|
2449
|
+
if (trimmed.startsWith("#")) {
|
|
2450
|
+
result.push(line);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} else {
|
|
2454
|
+
let kept = 0;
|
|
2455
|
+
for (const line of lines) {
|
|
2456
|
+
const trimmed = line.trim();
|
|
2457
|
+
if (kept < 5 || trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("//") || /^(export|import|function|class|interface|type|const|let|var|def|pub |fn |struct |enum )/i.test(trimmed)) {
|
|
2458
|
+
result.push(line);
|
|
2459
|
+
kept++;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return result.join("\n");
|
|
2464
|
+
}
|
|
2465
|
+
function extractGenericSkeleton(content) {
|
|
2466
|
+
const lines = content.split("\n");
|
|
2467
|
+
return lines.filter((line) => {
|
|
2468
|
+
const t = line.trim();
|
|
2469
|
+
return t === "" || t.startsWith("#") || t.startsWith("//") || /^(export|import|from|function|class|interface|type|const|let|var|def |pub |fn |struct |enum |module )/i.test(t);
|
|
2470
|
+
}).join("\n");
|
|
2471
|
+
}
|
|
2472
|
+
function isImportOrExport(line) {
|
|
2473
|
+
return /^(import |export |export default |export type |export interface )/.test(line);
|
|
2474
|
+
}
|
|
2475
|
+
function isTypeDefinition(line) {
|
|
2476
|
+
return /^(type |interface |enum )/.test(line);
|
|
2477
|
+
}
|
|
2478
|
+
function isComment(line) {
|
|
2479
|
+
return line.startsWith("//") || line.startsWith("/*") || line.startsWith(" *") || line.startsWith("*/");
|
|
2480
|
+
}
|
|
2481
|
+
function isFunctionSignature(line) {
|
|
2482
|
+
return /^(export )?(async )?(function |const \w+ = (?:async )?\(|(?:public|private|protected|static|async) )/.test(line) || /^\w+\s*\(.*\)\s*[:{]/.test(line);
|
|
2483
|
+
}
|
|
2484
|
+
function isClassOrInterface(line) {
|
|
2485
|
+
return /^(export )?(abstract )?(class |interface )/.test(line);
|
|
2486
|
+
}
|
|
2487
|
+
function isPropertySignature(line) {
|
|
2488
|
+
return /^\s+(readonly |public |private |protected )?\w+[\?:]/.test(line) && !line.includes("{");
|
|
2489
|
+
}
|
|
2490
|
+
function emptyPruned(file, level) {
|
|
2491
|
+
return {
|
|
2492
|
+
relativePath: file.relativePath,
|
|
2493
|
+
originalTokens: file.tokens,
|
|
2494
|
+
prunedTokens: 0,
|
|
2495
|
+
pruneLevel: level,
|
|
2496
|
+
content: "",
|
|
2497
|
+
savings: 100
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// src/core/git.ts
|
|
2502
|
+
import { execFile } from "child_process";
|
|
2503
|
+
import { resolve as resolve11 } from "path";
|
|
2504
|
+
import { promisify } from "util";
|
|
2505
|
+
var exec = promisify(execFile);
|
|
2506
|
+
async function git(args, cwd) {
|
|
2507
|
+
try {
|
|
2508
|
+
const { stdout } = await exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
2509
|
+
return stdout.trim();
|
|
2510
|
+
} catch {
|
|
2511
|
+
return "";
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
async function isGitRepo(projectPath) {
|
|
2515
|
+
const result = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
|
|
2516
|
+
return result === "true";
|
|
2517
|
+
}
|
|
2518
|
+
async function getCurrentBranch(projectPath) {
|
|
2519
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
|
|
2520
|
+
}
|
|
2521
|
+
async function getGitContext(projectPath, diffBase) {
|
|
2522
|
+
const absPath = resolve11(projectPath);
|
|
2523
|
+
const isRepo = await isGitRepo(absPath);
|
|
2524
|
+
if (!isRepo) {
|
|
2525
|
+
return {
|
|
2526
|
+
branch: "",
|
|
2527
|
+
isGitRepo: false,
|
|
2528
|
+
changedFiles: [],
|
|
2529
|
+
recentCommits: 0,
|
|
2530
|
+
activeDevelopers: []
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
const branch = await getCurrentBranch(absPath);
|
|
2534
|
+
const changedFiles = await getChangedFiles(absPath, diffBase);
|
|
2535
|
+
const recentCommits = await getRecentCommitCount(absPath, 14);
|
|
2536
|
+
const activeDevelopers = await getActiveDevelopers(absPath, 14);
|
|
2537
|
+
return {
|
|
2538
|
+
branch,
|
|
2539
|
+
isGitRepo: true,
|
|
2540
|
+
changedFiles,
|
|
2541
|
+
recentCommits,
|
|
2542
|
+
activeDevelopers
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
async function getChangedFiles(projectPath, diffBase) {
|
|
2546
|
+
const absPath = resolve11(projectPath);
|
|
2547
|
+
const files = [];
|
|
2548
|
+
const diffOutput = await git(["diff", "--name-only", "HEAD"], absPath);
|
|
2549
|
+
const stagedOutput = await git(["diff", "--name-only", "--cached"], absPath);
|
|
2550
|
+
const untrackedOutput = await git(["ls-files", "--others", "--exclude-standard"], absPath);
|
|
2551
|
+
const changedSet = /* @__PURE__ */ new Set();
|
|
2552
|
+
for (const line of [...diffOutput.split("\n"), ...stagedOutput.split("\n"), ...untrackedOutput.split("\n")]) {
|
|
2553
|
+
const f = line.trim();
|
|
2554
|
+
if (f) changedSet.add(f);
|
|
2555
|
+
}
|
|
2556
|
+
if (diffBase) {
|
|
2557
|
+
const baseOutput = await git(["diff", "--name-only", diffBase, "HEAD"], absPath);
|
|
2558
|
+
for (const line of baseOutput.split("\n")) {
|
|
2559
|
+
const f = line.trim();
|
|
2560
|
+
if (f) changedSet.add(f);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
for (const relativePath of changedSet) {
|
|
2564
|
+
const info = await getFileGitInfo(absPath, relativePath);
|
|
2565
|
+
if (info) files.push(info);
|
|
2566
|
+
}
|
|
2567
|
+
return files.sort((a, b) => b.linesChanged - a.linesChanged);
|
|
2568
|
+
}
|
|
2569
|
+
async function getFileGitInfo(projectPath, relativePath) {
|
|
2570
|
+
const absPath = resolve11(projectPath);
|
|
2571
|
+
const logOutput = await git(
|
|
2572
|
+
["log", "-1", "--format=%aI|%an", "--", relativePath],
|
|
2573
|
+
absPath
|
|
2574
|
+
);
|
|
2575
|
+
let lastCommitDate = /* @__PURE__ */ new Date();
|
|
2576
|
+
let lastAuthor = "unknown";
|
|
2577
|
+
if (logOutput) {
|
|
2578
|
+
const [date, author] = logOutput.split("|");
|
|
2579
|
+
lastCommitDate = new Date(date);
|
|
2580
|
+
lastAuthor = author ?? "unknown";
|
|
2581
|
+
}
|
|
2582
|
+
const countOutput = await git(
|
|
2583
|
+
["rev-list", "--count", "HEAD", "--", relativePath],
|
|
2584
|
+
absPath
|
|
2585
|
+
);
|
|
2586
|
+
const commitCount = parseInt(countOutput, 10) || 0;
|
|
2587
|
+
const untrackedOutput = await git(["ls-files", "--others", "--exclude-standard", "--", relativePath], absPath);
|
|
2588
|
+
const isNewFile = untrackedOutput.trim() === relativePath;
|
|
2589
|
+
const statOutput = await git(["diff", "--numstat", "HEAD", "--", relativePath], absPath);
|
|
2590
|
+
let linesChanged = 0;
|
|
2591
|
+
if (statOutput) {
|
|
2592
|
+
const parts = statOutput.split(" ");
|
|
2593
|
+
linesChanged = (parseInt(parts[0], 10) || 0) + (parseInt(parts[1], 10) || 0);
|
|
2594
|
+
}
|
|
2595
|
+
const diffCheck = await git(["diff", "--name-only", "HEAD", "--", relativePath], absPath);
|
|
2596
|
+
const isInDiff = diffCheck.trim().length > 0 || isNewFile;
|
|
2597
|
+
return {
|
|
2598
|
+
relativePath,
|
|
2599
|
+
lastCommitDate,
|
|
2600
|
+
lastAuthor,
|
|
2601
|
+
commitCount,
|
|
2602
|
+
isInDiff,
|
|
2603
|
+
isNewFile,
|
|
2604
|
+
linesChanged
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
async function getRecentCommitCount(projectPath, days) {
|
|
2608
|
+
const since = /* @__PURE__ */ new Date();
|
|
2609
|
+
since.setDate(since.getDate() - days);
|
|
2610
|
+
const output = await git(
|
|
2611
|
+
["rev-list", "--count", "--since", since.toISOString(), "HEAD"],
|
|
2612
|
+
resolve11(projectPath)
|
|
2613
|
+
);
|
|
2614
|
+
return parseInt(output, 10) || 0;
|
|
2615
|
+
}
|
|
2616
|
+
async function getActiveDevelopers(projectPath, days) {
|
|
2617
|
+
const since = /* @__PURE__ */ new Date();
|
|
2618
|
+
since.setDate(since.getDate() - days);
|
|
2619
|
+
const output = await git(
|
|
2620
|
+
["log", "--since", since.toISOString(), "--format=%an"],
|
|
2621
|
+
resolve11(projectPath)
|
|
2622
|
+
);
|
|
2623
|
+
if (!output) return [];
|
|
2624
|
+
const authors = new Set(output.split("\n").map((a) => a.trim()).filter(Boolean));
|
|
2625
|
+
return Array.from(authors);
|
|
2626
|
+
}
|
|
2627
|
+
function classifyFileWithGit(file, gitInfo, thresholds) {
|
|
2628
|
+
if (gitInfo?.isInDiff) return "hot";
|
|
2629
|
+
if (gitInfo?.isNewFile) return "hot";
|
|
2630
|
+
const referenceDate = gitInfo?.lastCommitDate ?? file.lastModified;
|
|
2631
|
+
const date = referenceDate instanceof Date ? referenceDate : new Date(referenceDate);
|
|
2632
|
+
const now = /* @__PURE__ */ new Date();
|
|
2633
|
+
const diffDays = (now.getTime() - date.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2634
|
+
if (diffDays <= thresholds.hotDays) return "hot";
|
|
2635
|
+
if (diffDays <= thresholds.warmDays) return "warm";
|
|
2636
|
+
if (gitInfo && gitInfo.commitCount >= 20 && diffDays <= thresholds.warmDays * 2) {
|
|
2637
|
+
return "warm";
|
|
2638
|
+
}
|
|
2639
|
+
return "cold";
|
|
2640
|
+
}
|
|
2641
|
+
async function enrichFilesWithGit(files, projectPath, thresholds) {
|
|
2642
|
+
const isRepo = await isGitRepo(projectPath);
|
|
2643
|
+
if (!isRepo) return files;
|
|
2644
|
+
const gitContext = await getGitContext(projectPath);
|
|
2645
|
+
const gitMap = new Map(gitContext.changedFiles.map((f) => [f.relativePath, f]));
|
|
2646
|
+
return files.map((file) => {
|
|
2647
|
+
const gitInfo = gitMap.get(file.relativePath);
|
|
2648
|
+
const gitTier = classifyFileWithGit(file, gitInfo, thresholds);
|
|
2649
|
+
const promotedTier = promoteTier(file.tier, gitTier);
|
|
2650
|
+
return {
|
|
2651
|
+
...file,
|
|
2652
|
+
tier: promotedTier
|
|
2653
|
+
};
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
function promoteTier(current, candidate) {
|
|
2657
|
+
const order = { hot: 0, warm: 1, cold: 2 };
|
|
2658
|
+
return order[candidate] < order[current] ? candidate : current;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/core/costs.ts
|
|
2662
|
+
var MODEL_PRICING = {
|
|
2663
|
+
opus: {
|
|
2664
|
+
model: "opus",
|
|
2665
|
+
inputPerMillion: 15,
|
|
2666
|
+
outputPerMillion: 75,
|
|
2667
|
+
cacheReadPerMillion: 1.5
|
|
2668
|
+
},
|
|
2669
|
+
sonnet: {
|
|
2670
|
+
model: "sonnet",
|
|
2671
|
+
inputPerMillion: 3,
|
|
2672
|
+
outputPerMillion: 15,
|
|
2673
|
+
cacheReadPerMillion: 0.3
|
|
2674
|
+
},
|
|
2675
|
+
haiku: {
|
|
2676
|
+
model: "haiku",
|
|
2677
|
+
inputPerMillion: 0.25,
|
|
2678
|
+
outputPerMillion: 1.25,
|
|
2679
|
+
cacheReadPerMillion: 0.03
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
function estimateCost(tokens, model = "sonnet") {
|
|
2683
|
+
const pricing = MODEL_PRICING[model];
|
|
2684
|
+
const cost = tokens / 1e6 * pricing.inputPerMillion;
|
|
2685
|
+
return {
|
|
2686
|
+
tokens,
|
|
2687
|
+
cost,
|
|
2688
|
+
formatted: formatCurrency(cost)
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
function estimateSessionCost(files, model = "sonnet") {
|
|
2692
|
+
const allTokens = files.reduce((s, f) => s + f.tokens, 0);
|
|
2693
|
+
const hotTokens = files.filter((f) => f.tier === "hot").reduce((s, f) => s + f.tokens, 0);
|
|
2694
|
+
const withoutCTO = estimateCost(allTokens, model);
|
|
2695
|
+
const withCTO = estimateCost(hotTokens, model);
|
|
2696
|
+
const savedTokens = allTokens - hotTokens;
|
|
2697
|
+
const saved = estimateCost(savedTokens, model);
|
|
2698
|
+
return {
|
|
2699
|
+
model,
|
|
2700
|
+
withoutCTO,
|
|
2701
|
+
withCTO,
|
|
2702
|
+
saved,
|
|
2703
|
+
savingsPercent: allTokens > 0 ? savedTokens / allTokens * 100 : 0
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function estimateWeeklyCost(sessions, totalProjectTokens, model = "sonnet") {
|
|
2707
|
+
const totalConsumed = sessions.reduce((s, ses) => s + ses.tokensEstimated, 0);
|
|
2708
|
+
const totalWithout = sessions.length * totalProjectTokens;
|
|
2709
|
+
const totalCost = estimateCost(totalConsumed, model);
|
|
2710
|
+
const costWithoutCTO = estimateCost(totalWithout, model);
|
|
2711
|
+
const savedCost = estimateCost(Math.max(0, totalWithout - totalConsumed), model);
|
|
2712
|
+
const avgCostPerSession = sessions.length > 0 ? formatCurrency(totalCost.cost / sessions.length) : "$0.00";
|
|
2713
|
+
return {
|
|
2714
|
+
totalCost,
|
|
2715
|
+
savedCost,
|
|
2716
|
+
costWithoutCTO,
|
|
2717
|
+
sessionsCount: sessions.length,
|
|
2718
|
+
avgCostPerSession
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
function estimateMonthlySavings(avgSessionsPerDay, avgTokensSaved, model = "sonnet") {
|
|
2722
|
+
const pricing = MODEL_PRICING[model];
|
|
2723
|
+
const dailySaved = avgTokensSaved * avgSessionsPerDay / 1e6 * pricing.inputPerMillion;
|
|
2724
|
+
return {
|
|
2725
|
+
daily: formatCurrency(dailySaved),
|
|
2726
|
+
weekly: formatCurrency(dailySaved * 7),
|
|
2727
|
+
monthly: formatCurrency(dailySaved * 30),
|
|
2728
|
+
yearly: formatCurrency(dailySaved * 365)
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function getModelPricingTable() {
|
|
2732
|
+
const lines = [];
|
|
2733
|
+
lines.push("| Model | Input/1M | Output/1M | Cache Read/1M |");
|
|
2734
|
+
lines.push("|-------|----------|-----------|---------------|");
|
|
2735
|
+
for (const [, pricing] of Object.entries(MODEL_PRICING)) {
|
|
2736
|
+
lines.push(
|
|
2737
|
+
`| ${pricing.model.padEnd(7)} | $${pricing.inputPerMillion.toFixed(2).padStart(6)} | $${pricing.outputPerMillion.toFixed(2).padStart(7)} | $${pricing.cacheReadPerMillion.toFixed(2).padStart(11)} |`
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
return lines.join("\n");
|
|
2741
|
+
}
|
|
2742
|
+
function formatCurrency(amount) {
|
|
2743
|
+
if (amount < 0.01) return `$${amount.toFixed(4)}`;
|
|
2744
|
+
if (amount < 1) return `$${amount.toFixed(3)}`;
|
|
2745
|
+
if (amount < 100) return `$${amount.toFixed(2)}`;
|
|
2746
|
+
return `$${Math.round(amount).toLocaleString()}`;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// src/core/budget.ts
|
|
2750
|
+
async function optimizeBudget(files, budget) {
|
|
2751
|
+
const included = [];
|
|
2752
|
+
const excluded = [];
|
|
2753
|
+
let usedTokens = 0;
|
|
2754
|
+
const tierOrder = { hot: 0, warm: 1, cold: 2 };
|
|
2755
|
+
const sorted = [...files].sort((a, b) => {
|
|
2756
|
+
const tierDiff = tierOrder[a.tier] - tierOrder[b.tier];
|
|
2757
|
+
if (tierDiff !== 0) return tierDiff;
|
|
2758
|
+
return b.tokens - a.tokens;
|
|
2759
|
+
});
|
|
2760
|
+
for (const file of sorted) {
|
|
2761
|
+
const pruneLevel = getPruneLevelForTier(file.tier);
|
|
2762
|
+
if (file.tier === "hot") {
|
|
2763
|
+
if (usedTokens + file.tokens <= budget) {
|
|
2764
|
+
usedTokens += file.tokens;
|
|
2765
|
+
included.push(toBudgetFile(file, file.tokens, "full", true, "Hot file \u2014 included in full"));
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
const pruned = await pruneFile(file, "signatures");
|
|
2769
|
+
if (usedTokens + pruned.prunedTokens <= budget) {
|
|
2770
|
+
usedTokens += pruned.prunedTokens;
|
|
2771
|
+
included.push(toBudgetFile(file, pruned.prunedTokens, "signatures", true, "Hot file \u2014 pruned to signatures (budget constraint)"));
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (file.tier === "warm") {
|
|
2776
|
+
const pruned = await pruneFile(file, "signatures");
|
|
2777
|
+
if (usedTokens + pruned.prunedTokens <= budget) {
|
|
2778
|
+
usedTokens += pruned.prunedTokens;
|
|
2779
|
+
included.push(toBudgetFile(file, pruned.prunedTokens, "signatures", true, "Warm file \u2014 signatures only"));
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
const skeleton = await pruneFile(file, "skeleton");
|
|
2783
|
+
if (usedTokens + skeleton.prunedTokens <= budget) {
|
|
2784
|
+
usedTokens += skeleton.prunedTokens;
|
|
2785
|
+
included.push(toBudgetFile(file, skeleton.prunedTokens, "skeleton", true, "Warm file \u2014 skeleton (budget constraint)"));
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
if (file.tier === "cold") {
|
|
2790
|
+
const skeleton = await pruneFile(file, "skeleton");
|
|
2791
|
+
if (usedTokens + skeleton.prunedTokens <= budget) {
|
|
2792
|
+
usedTokens += skeleton.prunedTokens;
|
|
2793
|
+
included.push(toBudgetFile(file, skeleton.prunedTokens, "skeleton", true, "Cold file \u2014 skeleton only"));
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
excluded.push(toBudgetFile(file, file.tokens, pruneLevel, false, `Excluded \u2014 exceeds budget (${file.tokens} tokens)`));
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
budget,
|
|
2801
|
+
usedTokens,
|
|
2802
|
+
remainingTokens: budget - usedTokens,
|
|
2803
|
+
includedFiles: included,
|
|
2804
|
+
excludedFiles: excluded,
|
|
2805
|
+
fillPercent: budget > 0 ? usedTokens / budget * 100 : 0
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
function toBudgetFile(file, tokens, pruneLevel, included, reason) {
|
|
2809
|
+
return {
|
|
2810
|
+
relativePath: file.relativePath,
|
|
2811
|
+
tokens,
|
|
2812
|
+
tier: file.tier,
|
|
2813
|
+
pruneLevel,
|
|
2814
|
+
included,
|
|
2815
|
+
reason
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// src/core/prompt-engineer.ts
|
|
2820
|
+
var DEFAULT_OPTIONS = {
|
|
2821
|
+
enableCoT: true,
|
|
2822
|
+
enableConstraints: true,
|
|
2823
|
+
enableAntiHallucination: true
|
|
2824
|
+
};
|
|
2825
|
+
function buildRolePriming(stack) {
|
|
2826
|
+
const stackStr = stack.length > 0 ? stack.join(", ") : "software";
|
|
2827
|
+
return `You are a senior ${stackStr} engineer with deep expertise in clean architecture, testing, and production-quality code. You prioritize correctness, readability, and maintainability.`;
|
|
2828
|
+
}
|
|
2829
|
+
function buildChainOfThought(task) {
|
|
2830
|
+
const lines = [];
|
|
2831
|
+
lines.push("## Thinking Process");
|
|
2832
|
+
lines.push("");
|
|
2833
|
+
lines.push("Before writing any code:");
|
|
2834
|
+
if (task === "debug") {
|
|
2835
|
+
lines.push("1. **Reproduce** \u2014 Understand the exact symptom and when it occurs");
|
|
2836
|
+
lines.push("2. **Hypothesize** \u2014 List the most likely root causes (max 3)");
|
|
2837
|
+
lines.push("3. **Verify** \u2014 Check each hypothesis against the code");
|
|
2838
|
+
lines.push("4. **Fix** \u2014 Apply the minimal fix that addresses the root cause");
|
|
2839
|
+
lines.push("5. **Validate** \u2014 Explain why the fix works and what edge cases it covers");
|
|
2840
|
+
} else if (task === "review") {
|
|
2841
|
+
lines.push("1. **Understand** \u2014 Read the code and understand its purpose");
|
|
2842
|
+
lines.push("2. **Assess** \u2014 Evaluate correctness, readability, performance, security");
|
|
2843
|
+
lines.push("3. **Prioritize** \u2014 Rank issues by severity (critical > major > minor > nitpick)");
|
|
2844
|
+
lines.push("4. **Suggest** \u2014 Provide concrete, actionable improvements with code examples");
|
|
2845
|
+
} else if (task === "refactor") {
|
|
2846
|
+
lines.push("1. **Analyze** \u2014 Identify code smells and structural issues");
|
|
2847
|
+
lines.push("2. **Plan** \u2014 Define the target structure before changing anything");
|
|
2848
|
+
lines.push("3. **Preserve** \u2014 Ensure behavior doesn't change (no functional modifications)");
|
|
2849
|
+
lines.push("4. **Refactor** \u2014 Apply changes incrementally, one pattern at a time");
|
|
2850
|
+
lines.push("5. **Verify** \u2014 Confirm all existing tests still pass");
|
|
2851
|
+
} else if (task === "test") {
|
|
2852
|
+
lines.push("1. **Identify** \u2014 What needs testing? (happy path, edge cases, errors)");
|
|
2853
|
+
lines.push("2. **Structure** \u2014 Use AAA pattern: Arrange, Act, Assert");
|
|
2854
|
+
lines.push("3. **Cover** \u2014 Test boundaries, null/undefined, async errors, type edges");
|
|
2855
|
+
lines.push("4. **Isolate** \u2014 Mock external dependencies, test units independently");
|
|
2856
|
+
lines.push('5. **Name** \u2014 Use descriptive test names: "should [expected] when [condition]"');
|
|
2857
|
+
} else if (task === "feature") {
|
|
2858
|
+
lines.push("1. **Clarify** \u2014 Restate the requirement in your own words");
|
|
2859
|
+
lines.push("2. **Design** \u2014 Plan the approach before coding (types, interfaces, flow)");
|
|
2860
|
+
lines.push("3. **Implement** \u2014 Build incrementally, starting with types and interfaces");
|
|
2861
|
+
lines.push("4. **Test** \u2014 Write tests alongside implementation");
|
|
2862
|
+
lines.push("5. **Integrate** \u2014 Ensure it works with existing code without regressions");
|
|
2863
|
+
} else if (task === "architecture") {
|
|
2864
|
+
lines.push("1. **Context** \u2014 Understand current architecture and constraints");
|
|
2865
|
+
lines.push("2. **Options** \u2014 Present 2-3 viable approaches with trade-offs");
|
|
2866
|
+
lines.push("3. **Recommend** \u2014 Choose the best option and explain why");
|
|
2867
|
+
lines.push("4. **Plan** \u2014 Define migration steps if changing existing architecture");
|
|
2868
|
+
lines.push("5. **Risks** \u2014 Identify risks and mitigation strategies");
|
|
2869
|
+
} else {
|
|
2870
|
+
lines.push("1. **Understand** \u2014 Read the relevant code before making changes");
|
|
2871
|
+
lines.push("2. **Plan** \u2014 Think about the approach before writing code");
|
|
2872
|
+
lines.push("3. **Implement** \u2014 Write clean, well-typed code");
|
|
2873
|
+
lines.push("4. **Verify** \u2014 Check for edge cases and potential issues");
|
|
2874
|
+
}
|
|
2875
|
+
return lines.join("\n");
|
|
2876
|
+
}
|
|
2877
|
+
function buildConstraints(stack, testFramework) {
|
|
2878
|
+
const lines = [];
|
|
2879
|
+
lines.push("## Constraints");
|
|
2880
|
+
lines.push("");
|
|
2881
|
+
lines.push("- **Do NOT** delete or modify existing tests unless explicitly asked");
|
|
2882
|
+
lines.push("- **Do NOT** change function signatures that are part of the public API");
|
|
2883
|
+
lines.push("- **Do NOT** introduce new dependencies without mentioning it");
|
|
2884
|
+
lines.push("- **Always** handle errors explicitly (no silent catches)");
|
|
2885
|
+
lines.push("- **Always** preserve existing code style and conventions");
|
|
2886
|
+
lines.push("- **Prefer** minimal changes \u2014 smallest diff that solves the problem");
|
|
2887
|
+
if (stack.includes("TypeScript")) {
|
|
2888
|
+
lines.push("- **Always** use strict TypeScript types (no `any` unless unavoidable)");
|
|
2889
|
+
lines.push("- **Prefer** `interface` for object shapes, `type` for unions/intersections");
|
|
2890
|
+
lines.push("- **Use** named exports over default exports");
|
|
2891
|
+
}
|
|
2892
|
+
if (stack.includes("React") || stack.includes("Next.js")) {
|
|
2893
|
+
lines.push("- **Use** functional components with hooks (no class components)");
|
|
2894
|
+
lines.push("- **Prefer** composition over prop drilling");
|
|
2895
|
+
}
|
|
2896
|
+
if (stack.includes("Python")) {
|
|
2897
|
+
lines.push("- **Follow** PEP 8 and use type hints");
|
|
2898
|
+
lines.push("- **Use** f-strings over .format() or % formatting");
|
|
2899
|
+
}
|
|
2900
|
+
if (testFramework) {
|
|
2901
|
+
lines.push(`- **Tests** use ${testFramework} \u2014 follow existing test patterns`);
|
|
2902
|
+
lines.push("- **Run** tests after changes to verify nothing breaks");
|
|
2903
|
+
}
|
|
2904
|
+
return lines.join("\n");
|
|
2905
|
+
}
|
|
2906
|
+
function buildAntiHallucination() {
|
|
2907
|
+
const lines = [];
|
|
2908
|
+
lines.push("## Important");
|
|
2909
|
+
lines.push("");
|
|
2910
|
+
lines.push("- If you are unsure about something, **ask before proceeding**");
|
|
2911
|
+
lines.push("- Do not invent APIs, functions, or types that don't exist in the codebase");
|
|
2912
|
+
lines.push("- If you need to read a file to answer correctly, say so");
|
|
2913
|
+
lines.push("- Prefer showing a concrete code snippet over a vague description");
|
|
2914
|
+
lines.push("- If a task is ambiguous, state your assumptions explicitly");
|
|
2915
|
+
return lines.join("\n");
|
|
2916
|
+
}
|
|
2917
|
+
function buildOutputFormat(task) {
|
|
2918
|
+
const lines = [];
|
|
2919
|
+
lines.push("## Response Format");
|
|
2920
|
+
lines.push("");
|
|
2921
|
+
if (task === "debug") {
|
|
2922
|
+
lines.push("1. **Root cause**: One sentence explaining the bug");
|
|
2923
|
+
lines.push("2. **Fix**: The code change with explanation");
|
|
2924
|
+
lines.push("3. **Test**: How to verify the fix works");
|
|
2925
|
+
} else if (task === "review") {
|
|
2926
|
+
lines.push("For each issue found:");
|
|
2927
|
+
lines.push("- **Severity**: critical | major | minor | nitpick");
|
|
2928
|
+
lines.push("- **Location**: file:line");
|
|
2929
|
+
lines.push("- **Issue**: What's wrong");
|
|
2930
|
+
lines.push("- **Fix**: Suggested code change");
|
|
2931
|
+
} else if (task === "refactor") {
|
|
2932
|
+
lines.push("1. **Before/After**: Show the structural change clearly");
|
|
2933
|
+
lines.push("2. **Why**: Explain the improvement");
|
|
2934
|
+
lines.push("3. **Risk**: Any behavior changes to watch for");
|
|
2935
|
+
} else {
|
|
2936
|
+
lines.push("- Start with a brief summary of what you'll do");
|
|
2937
|
+
lines.push("- Show code changes with context (surrounding lines)");
|
|
2938
|
+
lines.push("- Explain non-obvious decisions");
|
|
2939
|
+
}
|
|
2940
|
+
return lines.join("\n");
|
|
2941
|
+
}
|
|
2942
|
+
function buildFilePriority(hotFiles) {
|
|
2943
|
+
if (hotFiles.length === 0) return "";
|
|
2944
|
+
const lines = [];
|
|
2945
|
+
lines.push("## Priority Files");
|
|
2946
|
+
lines.push("");
|
|
2947
|
+
lines.push("Read these files first \u2014 they are the most active and relevant:");
|
|
2948
|
+
lines.push("");
|
|
2949
|
+
for (const f of hotFiles.slice(0, 20)) {
|
|
2950
|
+
const tokens = f.tokens > 1e3 ? `~${Math.round(f.tokens / 1e3)}K` : `~${f.tokens}`;
|
|
2951
|
+
lines.push(`- \`${f.relativePath}\` (${tokens} tokens)`);
|
|
2952
|
+
}
|
|
2953
|
+
return lines.join("\n");
|
|
2954
|
+
}
|
|
2955
|
+
function buildEnhancedPrompt(analysis, opts = {}) {
|
|
2956
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
2957
|
+
const stack = opts.stack ?? detectStack(analysis.files);
|
|
2958
|
+
const testFramework = opts.testFramework ?? detectTestFramework(analysis.files);
|
|
2959
|
+
const hotFiles = opts.hotFiles ?? analysis.files.filter((f) => f.tier === "hot");
|
|
2960
|
+
const sections = [];
|
|
2961
|
+
sections.push(`# ${analysis.projectName} \u2014 AI Context`);
|
|
2962
|
+
sections.push("");
|
|
2963
|
+
sections.push(`> Generated by CTO v1.3.0 with prompt engineering enhancements.`);
|
|
2964
|
+
sections.push("");
|
|
2965
|
+
sections.push(buildRolePriming(stack));
|
|
2966
|
+
sections.push("");
|
|
2967
|
+
sections.push("## Project Overview");
|
|
2968
|
+
sections.push("");
|
|
2969
|
+
sections.push(`| Property | Value |`);
|
|
2970
|
+
sections.push(`|----------|-------|`);
|
|
2971
|
+
sections.push(`| Stack | ${stack.join(", ") || "Unknown"} |`);
|
|
2972
|
+
if (testFramework) sections.push(`| Tests | ${testFramework} |`);
|
|
2973
|
+
sections.push(`| Files | ${analysis.totalFiles} (${hotFiles.length} hot) |`);
|
|
2974
|
+
sections.push(`| Tokens | ~${Math.round(analysis.totalTokens / 1e3)}K |`);
|
|
2975
|
+
sections.push("");
|
|
2976
|
+
const filePriority = buildFilePriority(hotFiles);
|
|
2977
|
+
if (filePriority) {
|
|
2978
|
+
sections.push(filePriority);
|
|
2979
|
+
sections.push("");
|
|
2980
|
+
}
|
|
2981
|
+
if (options.enableCoT) {
|
|
2982
|
+
sections.push(buildChainOfThought(opts.task));
|
|
2983
|
+
sections.push("");
|
|
2984
|
+
}
|
|
2985
|
+
if (options.enableConstraints) {
|
|
2986
|
+
sections.push(buildConstraints(stack, testFramework));
|
|
2987
|
+
sections.push("");
|
|
2988
|
+
}
|
|
2989
|
+
if (opts.task) {
|
|
2990
|
+
sections.push(buildOutputFormat(opts.task));
|
|
2991
|
+
sections.push("");
|
|
2992
|
+
}
|
|
2993
|
+
if (options.enableAntiHallucination) {
|
|
2994
|
+
sections.push(buildAntiHallucination());
|
|
2995
|
+
sections.push("");
|
|
2996
|
+
}
|
|
2997
|
+
return sections.join("\n");
|
|
2998
|
+
}
|
|
2999
|
+
function getTaskPromptBlock(task) {
|
|
3000
|
+
const blocks = {
|
|
3001
|
+
"debug": [
|
|
3002
|
+
"## Debug Instructions",
|
|
3003
|
+
"",
|
|
3004
|
+
"You are debugging an issue. Follow this approach:",
|
|
3005
|
+
"- Read the error message/symptom carefully",
|
|
3006
|
+
"- Trace the data flow from input to the point of failure",
|
|
3007
|
+
"- Check for: null/undefined, type mismatches, async race conditions, off-by-one errors",
|
|
3008
|
+
"- Fix the ROOT CAUSE, not the symptom",
|
|
3009
|
+
"- Add a regression test if possible"
|
|
3010
|
+
].join("\n"),
|
|
3011
|
+
"review": [
|
|
3012
|
+
"## Code Review Instructions",
|
|
3013
|
+
"",
|
|
3014
|
+
"Review this code with focus on:",
|
|
3015
|
+
"- **Correctness**: Does it do what it claims? Edge cases?",
|
|
3016
|
+
"- **Security**: Input validation, injection risks, secrets exposure",
|
|
3017
|
+
"- **Performance**: N+1 queries, unnecessary iterations, memory leaks",
|
|
3018
|
+
"- **Readability**: Clear naming, appropriate abstractions, documentation",
|
|
3019
|
+
"- **Testing**: Adequate coverage, meaningful assertions",
|
|
3020
|
+
"",
|
|
3021
|
+
"Rate each issue: \u{1F534} Critical | \u{1F7E0} Major | \u{1F7E1} Minor | \u26AA Nitpick"
|
|
3022
|
+
].join("\n"),
|
|
3023
|
+
"refactor": [
|
|
3024
|
+
"## Refactor Instructions",
|
|
3025
|
+
"",
|
|
3026
|
+
"Refactor for better structure WITHOUT changing behavior:",
|
|
3027
|
+
"- Extract common logic into reusable functions",
|
|
3028
|
+
"- Replace magic numbers/strings with named constants",
|
|
3029
|
+
"- Simplify complex conditionals",
|
|
3030
|
+
"- Apply SOLID principles where beneficial (don't over-engineer)",
|
|
3031
|
+
"- Keep the diff as small as possible",
|
|
3032
|
+
"- Verify all tests pass after refactoring"
|
|
3033
|
+
].join("\n"),
|
|
3034
|
+
"test": [
|
|
3035
|
+
"## Testing Instructions",
|
|
3036
|
+
"",
|
|
3037
|
+
"Write comprehensive tests:",
|
|
3038
|
+
"- **Happy path**: Normal expected usage",
|
|
3039
|
+
"- **Edge cases**: Empty inputs, boundaries, large data",
|
|
3040
|
+
"- **Error cases**: Invalid inputs, network failures, permissions",
|
|
3041
|
+
"- **Async**: Race conditions, timeout handling",
|
|
3042
|
+
"",
|
|
3043
|
+
'Structure: describe("module") > describe("function") > it("should...")',
|
|
3044
|
+
"Use AAA: Arrange (setup) \u2192 Act (execute) \u2192 Assert (verify)"
|
|
3045
|
+
].join("\n"),
|
|
3046
|
+
"docs": [
|
|
3047
|
+
"## Documentation Instructions",
|
|
3048
|
+
"",
|
|
3049
|
+
"- Use clear, concise language",
|
|
3050
|
+
"- Include code examples for every API/function documented",
|
|
3051
|
+
"- Document parameters, return types, and exceptions",
|
|
3052
|
+
'- Add a "Quick Start" section for new users',
|
|
3053
|
+
"- Include troubleshooting for common issues"
|
|
3054
|
+
].join("\n"),
|
|
3055
|
+
"simple-edit": [
|
|
3056
|
+
"## Simple Edit",
|
|
3057
|
+
"",
|
|
3058
|
+
"- Make the minimum change necessary",
|
|
3059
|
+
"- Do not refactor surrounding code",
|
|
3060
|
+
"- Preserve existing style exactly"
|
|
3061
|
+
].join("\n"),
|
|
3062
|
+
"feature": [
|
|
3063
|
+
"## Feature Development Instructions",
|
|
3064
|
+
"",
|
|
3065
|
+
"Build this feature following this order:",
|
|
3066
|
+
"1. Define types/interfaces first",
|
|
3067
|
+
"2. Implement core logic with proper error handling",
|
|
3068
|
+
"3. Write tests alongside implementation",
|
|
3069
|
+
"4. Integrate with existing modules",
|
|
3070
|
+
"5. Update exports and any relevant documentation",
|
|
3071
|
+
"",
|
|
3072
|
+
"Ensure backward compatibility \u2014 existing tests must still pass."
|
|
3073
|
+
].join("\n"),
|
|
3074
|
+
"architecture": [
|
|
3075
|
+
"## Architecture Decision",
|
|
3076
|
+
"",
|
|
3077
|
+
"Analyze the architectural question by providing:",
|
|
3078
|
+
"1. **Current state**: How things work now",
|
|
3079
|
+
"2. **Options**: At least 2-3 approaches with pros/cons table",
|
|
3080
|
+
"3. **Recommendation**: Your pick with justification",
|
|
3081
|
+
"4. **Migration path**: Steps to get from current \u2192 proposed",
|
|
3082
|
+
"5. **Risks**: What could go wrong and how to mitigate",
|
|
3083
|
+
"",
|
|
3084
|
+
"Consider: scalability, maintainability, team familiarity, testing impact."
|
|
3085
|
+
].join("\n")
|
|
3086
|
+
};
|
|
3087
|
+
return blocks[task];
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// src/core/adapters.ts
|
|
3091
|
+
function generateClaude(analysis, config) {
|
|
3092
|
+
const lines = [];
|
|
3093
|
+
const hot = analysis.files.filter((f) => f.tier === "hot");
|
|
3094
|
+
const warm = analysis.files.filter((f) => f.tier === "warm");
|
|
3095
|
+
lines.push(`# CLAUDE.md \u2014 ${analysis.projectName}`);
|
|
3096
|
+
lines.push("");
|
|
3097
|
+
lines.push("## Project Overview");
|
|
3098
|
+
lines.push("");
|
|
3099
|
+
lines.push(`- **Stack:** ${detectStack(analysis.files).join(", ") || "Unknown"}`);
|
|
3100
|
+
lines.push(`- **Files:** ${analysis.totalFiles} (${hot.length} hot, ${warm.length} warm)`);
|
|
3101
|
+
lines.push(`- **Tokens:** ~${Math.round(analysis.totalTokens / 1e3)}K total`);
|
|
3102
|
+
lines.push("");
|
|
3103
|
+
lines.push("## Priority Files (read first)");
|
|
3104
|
+
lines.push("");
|
|
3105
|
+
for (const f of hot.slice(0, 20)) {
|
|
3106
|
+
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
|
|
3107
|
+
}
|
|
3108
|
+
lines.push("");
|
|
3109
|
+
lines.push("## Guidelines");
|
|
3110
|
+
lines.push("");
|
|
3111
|
+
lines.push("- Read hot-tier files first for full context");
|
|
3112
|
+
lines.push("- Warm files can be read on demand");
|
|
3113
|
+
lines.push("- Cold files are rarely needed");
|
|
3114
|
+
return lines.join("\n");
|
|
3115
|
+
}
|
|
3116
|
+
function generateCursor(analysis, config) {
|
|
3117
|
+
const lines = [];
|
|
3118
|
+
const hot = analysis.files.filter((f) => f.tier === "hot");
|
|
3119
|
+
const stack = detectStack(analysis.files);
|
|
3120
|
+
const testFw = detectTestFramework(analysis.files);
|
|
3121
|
+
lines.push("# Cursor Rules \u2014 Generated by CTO");
|
|
3122
|
+
lines.push("");
|
|
3123
|
+
lines.push("## Project Context");
|
|
3124
|
+
lines.push("");
|
|
3125
|
+
lines.push(`This is a ${stack.join(", ")} project with ${analysis.totalFiles} files.`);
|
|
3126
|
+
if (testFw) lines.push(`Test framework: ${testFw}`);
|
|
3127
|
+
lines.push("");
|
|
3128
|
+
lines.push("## Key Files");
|
|
3129
|
+
lines.push("");
|
|
3130
|
+
lines.push("Always reference these files for context:");
|
|
3131
|
+
lines.push("");
|
|
3132
|
+
for (const f of hot.slice(0, 15)) {
|
|
3133
|
+
lines.push(`- ${f.relativePath}`);
|
|
3134
|
+
}
|
|
3135
|
+
lines.push("");
|
|
3136
|
+
lines.push("## Code Style");
|
|
3137
|
+
lines.push("");
|
|
3138
|
+
if (stack.includes("TypeScript")) {
|
|
3139
|
+
lines.push("- Use TypeScript with strict types");
|
|
3140
|
+
lines.push("- Prefer interfaces over type aliases for object shapes");
|
|
3141
|
+
lines.push("- Use named exports");
|
|
3142
|
+
}
|
|
3143
|
+
if (stack.includes("React") || stack.includes("Next.js")) {
|
|
3144
|
+
lines.push("- Use functional components with hooks");
|
|
3145
|
+
lines.push("- Prefer composition over inheritance");
|
|
3146
|
+
}
|
|
3147
|
+
if (stack.includes("Python")) {
|
|
3148
|
+
lines.push("- Follow PEP 8");
|
|
3149
|
+
lines.push("- Use type hints");
|
|
3150
|
+
}
|
|
3151
|
+
lines.push("");
|
|
3152
|
+
lines.push("## File Organization");
|
|
3153
|
+
lines.push("");
|
|
3154
|
+
const dirs = [...new Set(analysis.files.map((f) => f.relativePath.split("/")[0]))].slice(0, 10);
|
|
3155
|
+
for (const dir of dirs) {
|
|
3156
|
+
const count = analysis.files.filter((f) => f.relativePath.startsWith(dir + "/")).length;
|
|
3157
|
+
if (count > 0) lines.push(`- \`${dir}/\` \u2014 ${count} files`);
|
|
3158
|
+
}
|
|
3159
|
+
return lines.join("\n");
|
|
3160
|
+
}
|
|
3161
|
+
function generateCopilot(analysis, config) {
|
|
3162
|
+
const lines = [];
|
|
3163
|
+
const hot = analysis.files.filter((f) => f.tier === "hot");
|
|
3164
|
+
const stack = detectStack(analysis.files);
|
|
3165
|
+
const testFw = detectTestFramework(analysis.files);
|
|
3166
|
+
lines.push("# Copilot Instructions \u2014 Generated by CTO");
|
|
3167
|
+
lines.push("");
|
|
3168
|
+
lines.push("## Context");
|
|
3169
|
+
lines.push("");
|
|
3170
|
+
lines.push(`This is a ${stack.join(", ")} project.`);
|
|
3171
|
+
lines.push(`Total files: ${analysis.totalFiles}. Focus on the ${hot.length} most active files.`);
|
|
3172
|
+
lines.push("");
|
|
3173
|
+
lines.push("## Important Files");
|
|
3174
|
+
lines.push("");
|
|
3175
|
+
for (const f of hot.slice(0, 15)) {
|
|
3176
|
+
lines.push(`- \`${f.relativePath}\``);
|
|
3177
|
+
}
|
|
3178
|
+
lines.push("");
|
|
3179
|
+
lines.push("## Conventions");
|
|
3180
|
+
lines.push("");
|
|
3181
|
+
if (stack.includes("TypeScript")) {
|
|
3182
|
+
lines.push("- All code must be TypeScript");
|
|
3183
|
+
lines.push("- Use ESM imports (import/export)");
|
|
3184
|
+
lines.push("- Strict null checks enabled");
|
|
3185
|
+
}
|
|
3186
|
+
if (testFw) {
|
|
3187
|
+
lines.push(`- Tests use ${testFw}`);
|
|
3188
|
+
lines.push("- Write tests for all new functions");
|
|
3189
|
+
}
|
|
3190
|
+
lines.push("");
|
|
3191
|
+
lines.push("## Architecture");
|
|
3192
|
+
lines.push("");
|
|
3193
|
+
const topDirs = [...new Set(analysis.files.map((f) => f.relativePath.split("/")[0]))].slice(0, 8);
|
|
3194
|
+
for (const dir of topDirs) {
|
|
3195
|
+
const dirFiles = analysis.files.filter((f) => f.relativePath.startsWith(dir + "/"));
|
|
3196
|
+
if (dirFiles.length > 0) {
|
|
3197
|
+
lines.push(`- \`${dir}/\` \u2014 ${dirFiles.length} files, ~${Math.round(dirFiles.reduce((s, f) => s + f.tokens, 0) / 1e3)}K tokens`);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
return lines.join("\n");
|
|
3201
|
+
}
|
|
3202
|
+
function generateGemini(analysis, config) {
|
|
3203
|
+
const lines = [];
|
|
3204
|
+
const hot = analysis.files.filter((f) => f.tier === "hot");
|
|
3205
|
+
const warm = analysis.files.filter((f) => f.tier === "warm");
|
|
3206
|
+
const stack = detectStack(analysis.files);
|
|
3207
|
+
lines.push("# GEMINI.md \u2014 Project Context");
|
|
3208
|
+
lines.push("");
|
|
3209
|
+
lines.push("## Overview");
|
|
3210
|
+
lines.push("");
|
|
3211
|
+
lines.push(`| Property | Value |`);
|
|
3212
|
+
lines.push(`|----------|-------|`);
|
|
3213
|
+
lines.push(`| Stack | ${stack.join(", ")} |`);
|
|
3214
|
+
lines.push(`| Total Files | ${analysis.totalFiles} |`);
|
|
3215
|
+
lines.push(`| Hot Files | ${hot.length} |`);
|
|
3216
|
+
lines.push(`| Warm Files | ${warm.length} |`);
|
|
3217
|
+
lines.push(`| Total Tokens | ~${Math.round(analysis.totalTokens / 1e3)}K |`);
|
|
3218
|
+
lines.push("");
|
|
3219
|
+
lines.push("## Priority Files");
|
|
3220
|
+
lines.push("");
|
|
3221
|
+
lines.push("Read these files first for full project understanding:");
|
|
3222
|
+
lines.push("");
|
|
3223
|
+
for (const f of hot.slice(0, 20)) {
|
|
3224
|
+
lines.push(`1. \`${f.relativePath}\` \u2014 ${f.tier} tier, ~${Math.round(f.tokens / 1e3)}K tokens`);
|
|
3225
|
+
}
|
|
3226
|
+
lines.push("");
|
|
3227
|
+
lines.push("## Project Structure");
|
|
3228
|
+
lines.push("");
|
|
3229
|
+
const topDirs = [...new Set(analysis.files.map((f) => f.relativePath.split("/")[0]))].slice(0, 10);
|
|
3230
|
+
for (const dir of topDirs) {
|
|
3231
|
+
const count = analysis.files.filter((f) => f.relativePath.startsWith(dir + "/")).length;
|
|
3232
|
+
if (count > 0) lines.push(`- **${dir}/** \u2014 ${count} files`);
|
|
3233
|
+
}
|
|
3234
|
+
return lines.join("\n");
|
|
3235
|
+
}
|
|
3236
|
+
function generateWindsurf(analysis, config) {
|
|
3237
|
+
const lines = [];
|
|
3238
|
+
const hot = analysis.files.filter((f) => f.tier === "hot");
|
|
3239
|
+
const warm = analysis.files.filter((f) => f.tier === "warm");
|
|
3240
|
+
const stack = detectStack(analysis.files);
|
|
3241
|
+
const testFw = detectTestFramework(analysis.files);
|
|
3242
|
+
lines.push("# Windsurf Rules \u2014 Project Context");
|
|
3243
|
+
lines.push("");
|
|
3244
|
+
lines.push(`You are working on **${analysis.projectName}**, a ${stack.join(", ") || "software"} project.`);
|
|
3245
|
+
lines.push("");
|
|
3246
|
+
lines.push("## Project Overview");
|
|
3247
|
+
lines.push("");
|
|
3248
|
+
lines.push(`| Property | Value |`);
|
|
3249
|
+
lines.push(`|----------|-------|`);
|
|
3250
|
+
lines.push(`| Stack | ${stack.join(", ")} |`);
|
|
3251
|
+
if (testFw) lines.push(`| Tests | ${testFw} |`);
|
|
3252
|
+
lines.push(`| Files | ${analysis.totalFiles} (${hot.length} hot, ${warm.length} warm) |`);
|
|
3253
|
+
lines.push(`| Tokens | ~${Math.round(analysis.totalTokens / 1e3)}K |`);
|
|
3254
|
+
lines.push("");
|
|
3255
|
+
lines.push("## Priority Files");
|
|
3256
|
+
lines.push("");
|
|
3257
|
+
lines.push("Read these files first \u2014 they are actively being worked on:");
|
|
3258
|
+
lines.push("");
|
|
3259
|
+
for (const f of hot.slice(0, 20)) {
|
|
3260
|
+
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
|
|
3261
|
+
}
|
|
3262
|
+
lines.push("");
|
|
3263
|
+
lines.push("## Guidelines");
|
|
3264
|
+
lines.push("");
|
|
3265
|
+
lines.push("- Read hot-tier files first for full context");
|
|
3266
|
+
lines.push("- Warm files can be read on demand");
|
|
3267
|
+
lines.push("- Cold files are rarely needed \u2014 skip unless specifically asked");
|
|
3268
|
+
lines.push("- Prefer minimal changes that solve the problem");
|
|
3269
|
+
lines.push("- Always preserve existing code style and conventions");
|
|
3270
|
+
if (testFw) {
|
|
3271
|
+
lines.push(`- Tests use ${testFw} \u2014 run tests after changes`);
|
|
3272
|
+
}
|
|
3273
|
+
lines.push("");
|
|
3274
|
+
lines.push("## Architecture");
|
|
3275
|
+
lines.push("");
|
|
3276
|
+
const topDirs = [...new Set(analysis.files.map((f) => f.relativePath.split("/")[0]))].slice(0, 10);
|
|
3277
|
+
for (const dir of topDirs) {
|
|
3278
|
+
const dirFiles = analysis.files.filter((f) => f.relativePath.startsWith(dir + "/"));
|
|
3279
|
+
if (dirFiles.length > 0) {
|
|
3280
|
+
lines.push(`- \`${dir}/\` \u2014 ${dirFiles.length} files, ~${Math.round(dirFiles.reduce((s, f) => s + f.tokens, 0) / 1e3)}K tokens`);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return lines.join("\n");
|
|
3284
|
+
}
|
|
3285
|
+
var AI_ADAPTERS = {
|
|
3286
|
+
claude: { target: "claude", fileName: "CLAUDE.md", generate: generateClaude },
|
|
3287
|
+
cursor: { target: "cursor", fileName: ".cursorrules", generate: generateCursor },
|
|
3288
|
+
copilot: { target: "copilot", fileName: "copilot-instructions.md", generate: generateCopilot },
|
|
3289
|
+
gemini: { target: "gemini", fileName: "GEMINI.md", generate: generateGemini },
|
|
3290
|
+
windsurf: { target: "windsurf", fileName: ".windsurfrules", generate: generateWindsurf }
|
|
3291
|
+
};
|
|
3292
|
+
var ALL_AI_TARGETS = ["claude", "cursor", "copilot", "gemini", "windsurf"];
|
|
3293
|
+
function generateForTarget(target, analysis, config, options) {
|
|
3294
|
+
const adapter = AI_ADAPTERS[target];
|
|
3295
|
+
let content = adapter.generate(analysis, config);
|
|
3296
|
+
if (options?.enhanced) {
|
|
3297
|
+
const enhanced = buildEnhancedPrompt(analysis, { task: options.task });
|
|
3298
|
+
content = enhanced;
|
|
3299
|
+
if (options.task) {
|
|
3300
|
+
content += "\n" + getTaskPromptBlock(options.task) + "\n";
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return {
|
|
3304
|
+
fileName: adapter.fileName,
|
|
3305
|
+
content
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
function generateForAllTargets(analysis, config, options) {
|
|
3309
|
+
return ALL_AI_TARGETS.map((target) => ({
|
|
3310
|
+
target,
|
|
3311
|
+
...generateForTarget(target, analysis, config, options)
|
|
3312
|
+
}));
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// src/core/pr-context.ts
|
|
3316
|
+
import { resolve as resolve12 } from "path";
|
|
3317
|
+
import { execFile as execFile2 } from "child_process";
|
|
3318
|
+
import { promisify as promisify2 } from "util";
|
|
3319
|
+
var exec2 = promisify2(execFile2);
|
|
3320
|
+
async function git2(args, cwd) {
|
|
3321
|
+
try {
|
|
3322
|
+
const { stdout } = await exec2("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
3323
|
+
return stdout.trim();
|
|
3324
|
+
} catch {
|
|
3325
|
+
return "";
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
async function generatePRContext(analysis, baseBranch = "main") {
|
|
3329
|
+
const projectPath = resolve12(analysis.projectPath);
|
|
3330
|
+
if (!await isGitRepo(projectPath)) {
|
|
3331
|
+
return emptyPRContext(baseBranch);
|
|
3332
|
+
}
|
|
3333
|
+
const diffOutput = await git2(["diff", "--name-only", baseBranch, "HEAD"], projectPath);
|
|
3334
|
+
const stagedOutput = await git2(["diff", "--name-only", "--cached"], projectPath);
|
|
3335
|
+
const untrackedOutput = await git2(["diff", "--name-only", "HEAD"], projectPath);
|
|
3336
|
+
const changedSet = /* @__PURE__ */ new Set();
|
|
3337
|
+
for (const line of [...diffOutput.split("\n"), ...stagedOutput.split("\n"), ...untrackedOutput.split("\n")]) {
|
|
3338
|
+
const f = line.trim();
|
|
3339
|
+
if (f) changedSet.add(f);
|
|
3340
|
+
}
|
|
3341
|
+
const changedFiles = Array.from(changedSet);
|
|
3342
|
+
const depSet = /* @__PURE__ */ new Set();
|
|
3343
|
+
if (analysis.dependencyGraph) {
|
|
3344
|
+
for (const changed of changedFiles) {
|
|
3345
|
+
collectDependencies(changed, analysis.dependencyGraph, depSet, 2);
|
|
3346
|
+
}
|
|
3347
|
+
for (const f of changedFiles) depSet.delete(f);
|
|
3348
|
+
}
|
|
3349
|
+
const dependencies = Array.from(depSet);
|
|
3350
|
+
const allRelevant = /* @__PURE__ */ new Set([...changedFiles, ...dependencies]);
|
|
3351
|
+
const contextFiles = analysis.files.filter((f) => allRelevant.has(f.relativePath));
|
|
3352
|
+
const totalTokens = contextFiles.reduce((s, f) => s + f.tokens, 0);
|
|
3353
|
+
const generatedContent = renderPRContext(analysis, changedFiles, dependencies, contextFiles);
|
|
3354
|
+
return {
|
|
3355
|
+
baseBranch,
|
|
3356
|
+
changedFiles,
|
|
3357
|
+
dependencies,
|
|
3358
|
+
contextFiles,
|
|
3359
|
+
totalTokens,
|
|
3360
|
+
generatedContent
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
function collectDependencies(file, graph, result, depth) {
|
|
3364
|
+
if (depth <= 0) return;
|
|
3365
|
+
const imports = graph.edges.filter((e) => e.from === file).map((e) => e.to);
|
|
3366
|
+
for (const imp of imports) {
|
|
3367
|
+
if (!result.has(imp)) {
|
|
3368
|
+
result.add(imp);
|
|
3369
|
+
collectDependencies(imp, graph, result, depth - 1);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
function renderPRContext(analysis, changedFiles, dependencies, contextFiles) {
|
|
3374
|
+
const lines = [];
|
|
3375
|
+
const totalTokens = contextFiles.reduce((s, f) => s + f.tokens, 0);
|
|
3376
|
+
lines.push(`# PR Context \u2014 ${analysis.projectName}`);
|
|
3377
|
+
lines.push("");
|
|
3378
|
+
lines.push(`> Focused context for code review. ${changedFiles.length} changed files + ${dependencies.length} dependencies.`);
|
|
3379
|
+
lines.push("");
|
|
3380
|
+
lines.push("## Changed Files");
|
|
3381
|
+
lines.push("");
|
|
3382
|
+
for (const f of changedFiles) {
|
|
3383
|
+
const info = contextFiles.find((c) => c.relativePath === f);
|
|
3384
|
+
const tokens = info ? `~${Math.round(info.tokens / 1e3)}K tokens` : "";
|
|
3385
|
+
lines.push(`- \`${f}\` ${tokens}`);
|
|
3386
|
+
}
|
|
3387
|
+
lines.push("");
|
|
3388
|
+
if (dependencies.length > 0) {
|
|
3389
|
+
lines.push("## Dependencies (context)");
|
|
3390
|
+
lines.push("");
|
|
3391
|
+
for (const f of dependencies) {
|
|
3392
|
+
const info = contextFiles.find((c) => c.relativePath === f);
|
|
3393
|
+
const tokens = info ? `~${Math.round(info.tokens / 1e3)}K tokens` : "";
|
|
3394
|
+
lines.push(`- \`${f}\` ${tokens}`);
|
|
3395
|
+
}
|
|
3396
|
+
lines.push("");
|
|
3397
|
+
}
|
|
3398
|
+
lines.push("## Review Guidelines");
|
|
3399
|
+
lines.push("");
|
|
3400
|
+
lines.push("- Focus on the changed files listed above");
|
|
3401
|
+
lines.push("- Dependencies are included for type/interface context only");
|
|
3402
|
+
lines.push(`- Total context: ~${Math.round(totalTokens / 1e3)}K tokens`);
|
|
3403
|
+
return lines.join("\n");
|
|
3404
|
+
}
|
|
3405
|
+
function emptyPRContext(baseBranch) {
|
|
3406
|
+
return {
|
|
3407
|
+
baseBranch,
|
|
3408
|
+
changedFiles: [],
|
|
3409
|
+
dependencies: [],
|
|
3410
|
+
contextFiles: [],
|
|
3411
|
+
totalTokens: 0,
|
|
3412
|
+
generatedContent: "# No git repository found\n"
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
// src/core/explain.ts
|
|
3417
|
+
import { resolve as resolve13 } from "path";
|
|
3418
|
+
async function explainTier(file, projectPath, thresholds) {
|
|
3419
|
+
const factors = [];
|
|
3420
|
+
const modDate = file.lastModified instanceof Date ? file.lastModified : new Date(file.lastModified);
|
|
3421
|
+
const daysSinceModified = (Date.now() - modDate.getTime()) / (1e3 * 60 * 60 * 24);
|
|
3422
|
+
let recencyImpact = "neutral";
|
|
3423
|
+
if (daysSinceModified <= thresholds.hotDays) recencyImpact = "promotes";
|
|
3424
|
+
else if (daysSinceModified > thresholds.warmDays) recencyImpact = "demotes";
|
|
3425
|
+
factors.push({
|
|
3426
|
+
name: "Recency",
|
|
3427
|
+
value: `Modified ${Math.round(daysSinceModified)}d ago (hot: \u2264${thresholds.hotDays}d, warm: \u2264${thresholds.warmDays}d)`,
|
|
3428
|
+
impact: recencyImpact,
|
|
3429
|
+
weight: 3
|
|
3430
|
+
});
|
|
3431
|
+
const cost = estimateCost(file.tokens, "sonnet");
|
|
3432
|
+
factors.push({
|
|
3433
|
+
name: "Token Cost",
|
|
3434
|
+
value: `${file.tokens.toLocaleString()} tokens (~${cost.formatted}/read)`,
|
|
3435
|
+
impact: "neutral",
|
|
3436
|
+
weight: 1
|
|
3437
|
+
});
|
|
3438
|
+
if (file.isHub !== void 0) {
|
|
3439
|
+
factors.push({
|
|
3440
|
+
name: "Hub Status",
|
|
3441
|
+
value: file.isHub ? `Hub file \u2014 imported by ${file.importedByCount ?? 0} files` : "Not a hub file",
|
|
3442
|
+
impact: file.isHub && (file.importedByCount ?? 0) >= 3 ? "promotes" : "neutral",
|
|
3443
|
+
weight: file.isHub ? 2 : 0
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
if (file.complexity !== void 0) {
|
|
3447
|
+
const complexityLevel = file.complexity > 30 ? "High" : file.complexity > 10 ? "Medium" : "Low";
|
|
3448
|
+
factors.push({
|
|
3449
|
+
name: "Complexity",
|
|
3450
|
+
value: `Cyclomatic: ${file.complexity} (${complexityLevel})`,
|
|
3451
|
+
impact: file.complexity > 30 ? "promotes" : "neutral",
|
|
3452
|
+
weight: file.complexity > 30 ? 2 : 1
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
const absPath = resolve13(projectPath);
|
|
3456
|
+
if (await isGitRepo(absPath)) {
|
|
3457
|
+
const gitInfo = await getFileGitInfo(absPath, file.relativePath);
|
|
3458
|
+
if (gitInfo) {
|
|
3459
|
+
factors.push({
|
|
3460
|
+
name: "Git Activity",
|
|
3461
|
+
value: `${gitInfo.commitCount} commits, last by ${gitInfo.lastAuthor}`,
|
|
3462
|
+
impact: gitInfo.isInDiff ? "promotes" : gitInfo.commitCount >= 20 ? "promotes" : "neutral",
|
|
3463
|
+
weight: gitInfo.isInDiff ? 3 : 1
|
|
3464
|
+
});
|
|
3465
|
+
if (gitInfo.isInDiff) {
|
|
3466
|
+
factors.push({
|
|
3467
|
+
name: "In Current Diff",
|
|
3468
|
+
value: `${gitInfo.linesChanged} lines changed`,
|
|
3469
|
+
impact: "promotes",
|
|
3470
|
+
weight: 3
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
if (gitInfo.isNewFile) {
|
|
3474
|
+
factors.push({
|
|
3475
|
+
name: "New File",
|
|
3476
|
+
value: "Untracked / newly created",
|
|
3477
|
+
impact: "promotes",
|
|
3478
|
+
weight: 3
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
const coreExts = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "py", "go", "rs", "java"]);
|
|
3484
|
+
const isCore = coreExts.has(file.extension);
|
|
3485
|
+
factors.push({
|
|
3486
|
+
name: "File Type",
|
|
3487
|
+
value: `.${file.extension} \u2014 ${isCore ? "Core source file" : "Supporting file"}`,
|
|
3488
|
+
impact: "neutral",
|
|
3489
|
+
weight: isCore ? 1 : 0
|
|
3490
|
+
});
|
|
3491
|
+
const promotingFactors = factors.filter((f) => f.impact === "promotes");
|
|
3492
|
+
const demotingFactors = factors.filter((f) => f.impact === "demotes");
|
|
3493
|
+
let summary;
|
|
3494
|
+
if (file.tier === "hot") {
|
|
3495
|
+
const reasons = promotingFactors.map((f) => f.name.toLowerCase()).join(", ") || "recent modification";
|
|
3496
|
+
summary = `\u{1F525} Hot \u2014 Read first. Promoted by: ${reasons}.`;
|
|
3497
|
+
} else if (file.tier === "warm") {
|
|
3498
|
+
summary = `\u{1F321}\uFE0F Warm \u2014 Read if needed.`;
|
|
3499
|
+
if (promotingFactors.length > 0) {
|
|
3500
|
+
summary += ` Near hot due to: ${promotingFactors.map((f) => f.name.toLowerCase()).join(", ")}.`;
|
|
3501
|
+
}
|
|
3502
|
+
if (demotingFactors.length > 0) {
|
|
3503
|
+
summary += ` Held back by: ${demotingFactors.map((f) => f.name.toLowerCase()).join(", ")}.`;
|
|
3504
|
+
}
|
|
3505
|
+
} else {
|
|
3506
|
+
const reasons = demotingFactors.map((f) => f.name.toLowerCase()).join(", ") || "inactivity";
|
|
3507
|
+
summary = `\u2744\uFE0F Cold \u2014 Skip unless needed. Reason: ${reasons}.`;
|
|
3508
|
+
}
|
|
3509
|
+
return {
|
|
3510
|
+
file: file.relativePath,
|
|
3511
|
+
tier: file.tier,
|
|
3512
|
+
factors,
|
|
3513
|
+
summary
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
function formatExplanation(explanation) {
|
|
3517
|
+
const lines = [];
|
|
3518
|
+
lines.push(`${explanation.summary}`);
|
|
3519
|
+
lines.push("");
|
|
3520
|
+
lines.push("Factors:");
|
|
3521
|
+
for (const factor of explanation.factors) {
|
|
3522
|
+
const icon = factor.impact === "promotes" ? "\u2191" : factor.impact === "demotes" ? "\u2193" : "\xB7";
|
|
3523
|
+
lines.push(` ${icon} ${factor.name}: ${factor.value}`);
|
|
3524
|
+
}
|
|
3525
|
+
return lines.join("\n");
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
// src/core/routing.ts
|
|
3529
|
+
var ROUTING_RULES = [
|
|
3530
|
+
// Simple tasks → Haiku (cheapest)
|
|
3531
|
+
{ task: "simple-edit", maxComplexity: 10, maxFiles: 3, defaultModel: "haiku" },
|
|
3532
|
+
{ task: "docs", maxComplexity: 5, maxFiles: 5, defaultModel: "haiku" },
|
|
3533
|
+
{ task: "test", maxComplexity: 20, maxFiles: 10, defaultModel: "haiku" },
|
|
3534
|
+
// Moderate tasks → Sonnet (balanced)
|
|
3535
|
+
{ task: "debug", maxComplexity: 30, maxFiles: 10, defaultModel: "sonnet" },
|
|
3536
|
+
{ task: "review", maxComplexity: 40, maxFiles: 20, defaultModel: "sonnet" },
|
|
3537
|
+
{ task: "refactor", maxComplexity: 30, maxFiles: 15, defaultModel: "sonnet" },
|
|
3538
|
+
// Complex tasks → Opus (best reasoning)
|
|
3539
|
+
{ task: "feature", maxComplexity: 50, maxFiles: 30, defaultModel: "sonnet" },
|
|
3540
|
+
{ task: "architecture", maxComplexity: 100, maxFiles: 50, defaultModel: "opus" }
|
|
3541
|
+
];
|
|
3542
|
+
var ALL_TASK_TYPES = [
|
|
3543
|
+
"simple-edit",
|
|
3544
|
+
"docs",
|
|
3545
|
+
"test",
|
|
3546
|
+
"debug",
|
|
3547
|
+
"review",
|
|
3548
|
+
"refactor",
|
|
3549
|
+
"feature",
|
|
3550
|
+
"architecture"
|
|
3551
|
+
];
|
|
3552
|
+
function recommendModel(task, analysis, targetFiles) {
|
|
3553
|
+
const rule = ROUTING_RULES.find((r) => r.task === task);
|
|
3554
|
+
const relevantFiles = targetFiles ? analysis.files.filter((f) => targetFiles.some((t) => f.relativePath.includes(t))) : analysis.files.filter((f) => f.tier === "hot");
|
|
3555
|
+
const avgComplexity = getAvgComplexity(relevantFiles);
|
|
3556
|
+
const fileCount = relevantFiles.length;
|
|
3557
|
+
const totalTokens = relevantFiles.reduce((s, f) => s + f.tokens, 0);
|
|
3558
|
+
let recommended = rule.defaultModel;
|
|
3559
|
+
if (avgComplexity > rule.maxComplexity || fileCount > rule.maxFiles) {
|
|
3560
|
+
recommended = upgradeModel(recommended);
|
|
3561
|
+
}
|
|
3562
|
+
if (avgComplexity < rule.maxComplexity * 0.3 && fileCount <= 3) {
|
|
3563
|
+
recommended = downgradeModel(recommended);
|
|
3564
|
+
}
|
|
3565
|
+
const cost = estimateCost(totalTokens, recommended);
|
|
3566
|
+
const alternatives = buildAlternatives(recommended, avgComplexity, fileCount, totalTokens);
|
|
3567
|
+
return {
|
|
3568
|
+
task,
|
|
3569
|
+
recommended,
|
|
3570
|
+
reason: buildReason(task, recommended, avgComplexity, fileCount),
|
|
3571
|
+
estimatedTokens: totalTokens,
|
|
3572
|
+
estimatedCost: cost.formatted,
|
|
3573
|
+
alternatives
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
function recommendModelForFiles(files) {
|
|
3577
|
+
const avgComplexity = getAvgComplexity(files);
|
|
3578
|
+
const totalTokens = files.reduce((s, f) => s + f.tokens, 0);
|
|
3579
|
+
if (avgComplexity <= 10 && totalTokens < 5e3) return "haiku";
|
|
3580
|
+
if (avgComplexity <= 30 && totalTokens < 5e4) return "sonnet";
|
|
3581
|
+
return "opus";
|
|
3582
|
+
}
|
|
3583
|
+
function getTaskDescription(task) {
|
|
3584
|
+
const descriptions = {
|
|
3585
|
+
"simple-edit": "Small change \u2014 rename, fix typo, update config",
|
|
3586
|
+
"docs": "Documentation \u2014 README, comments, JSDoc, guides",
|
|
3587
|
+
"test": "Write or fix tests",
|
|
3588
|
+
"debug": "Find and fix a bug",
|
|
3589
|
+
"review": "Code review \u2014 analyze quality, suggest improvements",
|
|
3590
|
+
"refactor": "Refactor code \u2014 restructure without changing behavior",
|
|
3591
|
+
"feature": "Build a new feature",
|
|
3592
|
+
"architecture": "Architecture decisions \u2014 design patterns, major restructuring"
|
|
3593
|
+
};
|
|
3594
|
+
return descriptions[task];
|
|
3595
|
+
}
|
|
3596
|
+
function getAvgComplexity(files) {
|
|
3597
|
+
const withComplexity = files.filter((f) => f.complexity !== void 0);
|
|
3598
|
+
if (withComplexity.length === 0) return 10;
|
|
3599
|
+
return withComplexity.reduce((s, f) => s + (f.complexity ?? 0), 0) / withComplexity.length;
|
|
3600
|
+
}
|
|
3601
|
+
function upgradeModel(model) {
|
|
3602
|
+
if (model === "haiku") return "sonnet";
|
|
3603
|
+
if (model === "sonnet") return "opus";
|
|
3604
|
+
return "opus";
|
|
3605
|
+
}
|
|
3606
|
+
function downgradeModel(model) {
|
|
3607
|
+
if (model === "opus") return "sonnet";
|
|
3608
|
+
if (model === "sonnet") return "haiku";
|
|
3609
|
+
return "haiku";
|
|
3610
|
+
}
|
|
3611
|
+
function buildReason(task, model, complexity, files) {
|
|
3612
|
+
const taskLabel = getTaskDescription(task);
|
|
3613
|
+
const complexityLabel = complexity > 30 ? "high" : complexity > 10 ? "moderate" : "low";
|
|
3614
|
+
if (model === "haiku") {
|
|
3615
|
+
return `${taskLabel}. Low complexity (${Math.round(complexity)}) and few files (${files}) \u2192 Haiku is sufficient and cheapest.`;
|
|
3616
|
+
}
|
|
3617
|
+
if (model === "opus") {
|
|
3618
|
+
return `${taskLabel}. ${complexityLabel} complexity (${Math.round(complexity)}) across ${files} files \u2192 Opus recommended for deep reasoning.`;
|
|
3619
|
+
}
|
|
3620
|
+
return `${taskLabel}. ${complexityLabel} complexity (${Math.round(complexity)}) across ${files} files \u2192 Sonnet balances quality and cost.`;
|
|
3621
|
+
}
|
|
3622
|
+
function buildAlternatives(recommended, complexity, files, tokens) {
|
|
3623
|
+
const alts = [];
|
|
3624
|
+
if (recommended !== "haiku") {
|
|
3625
|
+
const haikuCost = estimateCost(tokens, "haiku");
|
|
3626
|
+
alts.push({ model: "haiku", reason: `Cheapest (${haikuCost.formatted}) \u2014 use if task is straightforward` });
|
|
3627
|
+
}
|
|
3628
|
+
if (recommended !== "sonnet") {
|
|
3629
|
+
const sonnetCost = estimateCost(tokens, "sonnet");
|
|
3630
|
+
alts.push({ model: "sonnet", reason: `Balanced (${sonnetCost.formatted}) \u2014 good for most tasks` });
|
|
3631
|
+
}
|
|
3632
|
+
if (recommended !== "opus") {
|
|
3633
|
+
const opusCost = estimateCost(tokens, "opus");
|
|
3634
|
+
alts.push({ model: "opus", reason: `Best reasoning (${opusCost.formatted}) \u2014 use for complex logic` });
|
|
3635
|
+
}
|
|
3636
|
+
return alts;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// src/core/local-config.ts
|
|
3640
|
+
import { join as join9, resolve as resolve14 } from "path";
|
|
3641
|
+
import { readFile as readFile8, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
|
|
3642
|
+
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
3643
|
+
var LOCAL_CTO_DIR = ".cto";
|
|
3644
|
+
var LOCAL_CONFIG_FILE = "config.yaml";
|
|
3645
|
+
function getLocalCTOPath(projectPath) {
|
|
3646
|
+
return join9(resolve14(projectPath), LOCAL_CTO_DIR);
|
|
3647
|
+
}
|
|
3648
|
+
function getLocalConfigPath(projectPath) {
|
|
3649
|
+
return join9(getLocalCTOPath(projectPath), LOCAL_CONFIG_FILE);
|
|
3650
|
+
}
|
|
3651
|
+
async function hasLocalConfig(projectPath) {
|
|
3652
|
+
try {
|
|
3653
|
+
await access(getLocalConfigPath(projectPath));
|
|
3654
|
+
return true;
|
|
3655
|
+
} catch {
|
|
3656
|
+
return false;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
async function initLocalProject(projectPath, config) {
|
|
3660
|
+
const localDir = getLocalCTOPath(projectPath);
|
|
3661
|
+
await mkdir2(localDir, { recursive: true });
|
|
3662
|
+
const localConfig = {
|
|
3663
|
+
version: CTO_VERSION,
|
|
3664
|
+
autoRouting: true,
|
|
3665
|
+
...config
|
|
3666
|
+
};
|
|
3667
|
+
const configPath = getLocalConfigPath(projectPath);
|
|
3668
|
+
await writeFile2(configPath, stringify2(localConfig), "utf-8");
|
|
3669
|
+
const gitignorePath = join9(localDir, ".gitignore");
|
|
3670
|
+
const gitignoreContent = [
|
|
3671
|
+
"# CTO local cache (do not commit)",
|
|
3672
|
+
"analysis.json",
|
|
3673
|
+
"sessions/",
|
|
3674
|
+
"",
|
|
3675
|
+
"# Config IS committed (shared with team)",
|
|
3676
|
+
"!config.yaml",
|
|
3677
|
+
"!.gitignore",
|
|
3678
|
+
""
|
|
3679
|
+
].join("\n");
|
|
3680
|
+
await writeFile2(gitignorePath, gitignoreContent, "utf-8");
|
|
3681
|
+
return configPath;
|
|
3682
|
+
}
|
|
3683
|
+
async function loadLocalConfig(projectPath) {
|
|
3684
|
+
try {
|
|
3685
|
+
const content = await readFile8(getLocalConfigPath(projectPath), "utf-8");
|
|
3686
|
+
return parse2(content);
|
|
3687
|
+
} catch {
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
async function saveLocalConfig(projectPath, config) {
|
|
3692
|
+
const localDir = getLocalCTOPath(projectPath);
|
|
3693
|
+
await mkdir2(localDir, { recursive: true });
|
|
3694
|
+
await writeFile2(getLocalConfigPath(projectPath), stringify2(config), "utf-8");
|
|
3695
|
+
}
|
|
3696
|
+
function mergeLocalWithGlobal(globalConfig, localConfig) {
|
|
3697
|
+
return {
|
|
3698
|
+
...globalConfig,
|
|
3699
|
+
version: localConfig.version ?? globalConfig.version,
|
|
3700
|
+
model: localConfig.model ?? globalConfig.model,
|
|
3701
|
+
tiering: {
|
|
3702
|
+
...globalConfig.tiering,
|
|
3703
|
+
...localConfig.tiering
|
|
3704
|
+
},
|
|
3705
|
+
ignoreDirs: localConfig.ignoreDirs ?? globalConfig.ignoreDirs,
|
|
3706
|
+
extensions: {
|
|
3707
|
+
...globalConfig.extensions,
|
|
3708
|
+
...localConfig.extensions
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
async function detectProjectRoot(startPath) {
|
|
3713
|
+
let current = resolve14(startPath);
|
|
3714
|
+
const root = resolve14("/");
|
|
3715
|
+
while (current !== root) {
|
|
3716
|
+
if (await hasLocalConfig(current)) {
|
|
3717
|
+
return current;
|
|
3718
|
+
}
|
|
3719
|
+
for (const marker of ["package.json", "Cargo.toml", "go.mod", "pyproject.toml", "pom.xml", ".git"]) {
|
|
3720
|
+
try {
|
|
3721
|
+
await access(join9(current, marker));
|
|
3722
|
+
return current;
|
|
3723
|
+
} catch {
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
current = resolve14(current, "..");
|
|
3727
|
+
}
|
|
3728
|
+
return resolve14(startPath);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// src/core/sdd.ts
|
|
3732
|
+
import "ts-morph";
|
|
3733
|
+
import { resolve as resolve15, relative as relative8 } from "path";
|
|
3734
|
+
function extractSpecs(projectPath, files) {
|
|
3735
|
+
const absPath = resolve15(projectPath);
|
|
3736
|
+
const tsFiles = files.filter((f) => ["ts", "tsx"].includes(f.extension)).map((f) => f.path);
|
|
3737
|
+
const empty = {
|
|
3738
|
+
projectName: projectPath.split("/").pop() ?? "project",
|
|
3739
|
+
extractedAt: /* @__PURE__ */ new Date(),
|
|
3740
|
+
totalSpecs: 0,
|
|
3741
|
+
entries: [],
|
|
3742
|
+
byFile: {},
|
|
3743
|
+
byKind: {}
|
|
3744
|
+
};
|
|
3745
|
+
if (tsFiles.length === 0) return empty;
|
|
3746
|
+
const project = createProject(projectPath, tsFiles);
|
|
3747
|
+
const entries = [];
|
|
3748
|
+
for (const sf of project.getSourceFiles()) {
|
|
3749
|
+
const rel = relative8(absPath, sf.getFilePath());
|
|
3750
|
+
if (rel.startsWith("..") || rel.includes("node_modules")) continue;
|
|
3751
|
+
extractInterfaces(sf, rel, entries);
|
|
3752
|
+
extractTypeAliases(sf, rel, entries);
|
|
3753
|
+
extractEnums(sf, rel, entries);
|
|
3754
|
+
extractFunctionSigs(sf, rel, entries);
|
|
3755
|
+
extractClassSigs(sf, rel, entries);
|
|
3756
|
+
}
|
|
3757
|
+
const byFile = {};
|
|
3758
|
+
const byKind = {};
|
|
3759
|
+
for (const e of entries) {
|
|
3760
|
+
(byFile[e.file] ??= []).push(e);
|
|
3761
|
+
(byKind[e.kind] ??= []).push(e);
|
|
3762
|
+
}
|
|
3763
|
+
return {
|
|
3764
|
+
projectName: projectPath.split("/").pop() ?? "project",
|
|
3765
|
+
extractedAt: /* @__PURE__ */ new Date(),
|
|
3766
|
+
totalSpecs: entries.length,
|
|
3767
|
+
entries,
|
|
3768
|
+
byFile,
|
|
3769
|
+
byKind
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
function generateSpecDocument(map) {
|
|
3773
|
+
const lines = [];
|
|
3774
|
+
lines.push(`# Specification Document \u2014 ${map.projectName}`);
|
|
3775
|
+
lines.push("");
|
|
3776
|
+
lines.push("> **Specification-Driven Development (SDD)**");
|
|
3777
|
+
lines.push("> This document is the single source of truth. All implementations MUST conform to these specs.");
|
|
3778
|
+
lines.push(`> Extracted: ${map.extractedAt.toISOString()} | Total specs: ${map.totalSpecs}`);
|
|
3779
|
+
lines.push("");
|
|
3780
|
+
const kindCounts = Object.entries(map.byKind).map(([k, v]) => `${v.length} ${k}s`);
|
|
3781
|
+
lines.push(`**Specs:** ${kindCounts.join(" \xB7 ")}`);
|
|
3782
|
+
lines.push("");
|
|
3783
|
+
const interfaces = (map.byKind["interface"] ?? []).filter((e) => e.exported);
|
|
3784
|
+
if (interfaces.length > 0) {
|
|
3785
|
+
lines.push("---");
|
|
3786
|
+
lines.push("");
|
|
3787
|
+
lines.push("## Interfaces");
|
|
3788
|
+
lines.push("");
|
|
3789
|
+
lines.push("These are the primary contracts. Implementations MUST satisfy every property.");
|
|
3790
|
+
lines.push("");
|
|
3791
|
+
for (const entry of interfaces) {
|
|
3792
|
+
lines.push(`### \`${entry.name}\``);
|
|
3793
|
+
if (entry.jsdoc) lines.push(`> ${entry.jsdoc}`);
|
|
3794
|
+
lines.push(`\u{1F4CD} \`${entry.file}:${entry.line}\``);
|
|
3795
|
+
if (entry.extends && entry.extends.length > 0) {
|
|
3796
|
+
lines.push(`\u2197\uFE0F Extends: ${entry.extends.map((e) => `\`${e}\``).join(", ")}`);
|
|
3797
|
+
}
|
|
3798
|
+
lines.push("");
|
|
3799
|
+
if (entry.properties && entry.properties.length > 0) {
|
|
3800
|
+
lines.push("| Property | Type | Required |");
|
|
3801
|
+
lines.push("|----------|------|----------|");
|
|
3802
|
+
for (const p of entry.properties) {
|
|
3803
|
+
lines.push(`| \`${p.name}\` | \`${p.type}\` | ${p.optional ? "No" : "**Yes**"} |`);
|
|
3804
|
+
}
|
|
3805
|
+
lines.push("");
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
const types = (map.byKind["type"] ?? []).filter((e) => e.exported);
|
|
3810
|
+
if (types.length > 0) {
|
|
3811
|
+
lines.push("---");
|
|
3812
|
+
lines.push("");
|
|
3813
|
+
lines.push("## Types");
|
|
3814
|
+
lines.push("");
|
|
3815
|
+
for (const entry of types) {
|
|
3816
|
+
lines.push(`- **\`${entry.name}\`** \u2014 \`${entry.signature}\` (\`${entry.file}:${entry.line}\`)`);
|
|
3817
|
+
}
|
|
3818
|
+
lines.push("");
|
|
3819
|
+
}
|
|
3820
|
+
const enums = (map.byKind["enum"] ?? []).filter((e) => e.exported);
|
|
3821
|
+
if (enums.length > 0) {
|
|
3822
|
+
lines.push("---");
|
|
3823
|
+
lines.push("");
|
|
3824
|
+
lines.push("## Enums");
|
|
3825
|
+
lines.push("");
|
|
3826
|
+
for (const entry of enums) {
|
|
3827
|
+
lines.push(`- **\`${entry.name}\`** \u2014 \`${entry.signature}\` (\`${entry.file}:${entry.line}\`)`);
|
|
3828
|
+
}
|
|
3829
|
+
lines.push("");
|
|
3830
|
+
}
|
|
3831
|
+
const fns = (map.byKind["function"] ?? []).filter((e) => e.exported);
|
|
3832
|
+
if (fns.length > 0) {
|
|
3833
|
+
lines.push("---");
|
|
3834
|
+
lines.push("");
|
|
3835
|
+
lines.push("## Functions (Public API)");
|
|
3836
|
+
lines.push("");
|
|
3837
|
+
lines.push("These function signatures are the public API. Do NOT change signatures without updating this spec.");
|
|
3838
|
+
lines.push("");
|
|
3839
|
+
const byFile = {};
|
|
3840
|
+
for (const fn of fns) {
|
|
3841
|
+
(byFile[fn.file] ??= []).push(fn);
|
|
3842
|
+
}
|
|
3843
|
+
for (const [file, fileFns] of Object.entries(byFile)) {
|
|
3844
|
+
lines.push(`#### \`${file}\``);
|
|
3845
|
+
lines.push("```typescript");
|
|
3846
|
+
for (const fn of fileFns) {
|
|
3847
|
+
if (fn.jsdoc) lines.push(`// ${fn.jsdoc}`);
|
|
3848
|
+
lines.push(`export ${fn.signature};`);
|
|
3849
|
+
}
|
|
3850
|
+
lines.push("```");
|
|
3851
|
+
lines.push("");
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
const classes = (map.byKind["class"] ?? []).filter((e) => e.exported);
|
|
3855
|
+
if (classes.length > 0) {
|
|
3856
|
+
lines.push("---");
|
|
3857
|
+
lines.push("");
|
|
3858
|
+
lines.push("## Classes");
|
|
3859
|
+
lines.push("");
|
|
3860
|
+
for (const entry of classes) {
|
|
3861
|
+
lines.push(`- **\`${entry.name}\`** (\`${entry.file}:${entry.line}\`)`);
|
|
3862
|
+
if (entry.extends) lines.push(` - Extends: ${entry.extends.join(", ")}`);
|
|
3863
|
+
lines.push(` - \`${entry.signature}\``);
|
|
3864
|
+
}
|
|
3865
|
+
lines.push("");
|
|
3866
|
+
}
|
|
3867
|
+
lines.push("---");
|
|
3868
|
+
lines.push("");
|
|
3869
|
+
lines.push("## SDD Rules");
|
|
3870
|
+
lines.push("");
|
|
3871
|
+
lines.push("1. **Spec first** \u2014 Define or update interfaces/types BEFORE writing implementation");
|
|
3872
|
+
lines.push("2. **Contract compliance** \u2014 Every function must satisfy its declared return type");
|
|
3873
|
+
lines.push("3. **No implicit any** \u2014 All parameters and returns must be explicitly typed");
|
|
3874
|
+
lines.push("4. **Interface segregation** \u2014 Prefer small, focused interfaces over large ones");
|
|
3875
|
+
lines.push("5. **Backward compatibility** \u2014 Do NOT remove or rename properties from existing interfaces");
|
|
3876
|
+
lines.push("6. **Document changes** \u2014 Update this spec when adding/modifying public API");
|
|
3877
|
+
lines.push("7. **Test against spec** \u2014 Tests should verify contract compliance, not implementation details");
|
|
3878
|
+
lines.push("");
|
|
3879
|
+
return lines.join("\n");
|
|
3880
|
+
}
|
|
3881
|
+
function validateSpecs(projectPath, files) {
|
|
3882
|
+
const absPath = resolve15(projectPath);
|
|
3883
|
+
const tsFiles = files.filter((f) => ["ts", "tsx"].includes(f.extension)).map((f) => f.path);
|
|
3884
|
+
const errors = [];
|
|
3885
|
+
const warnings = [];
|
|
3886
|
+
let totalFunctions = 0;
|
|
3887
|
+
let withReturnType = 0;
|
|
3888
|
+
let totalInterfaces = 0;
|
|
3889
|
+
let fullyDocumented = 0;
|
|
3890
|
+
let exportedWithoutJSDoc = 0;
|
|
3891
|
+
if (tsFiles.length === 0) {
|
|
3892
|
+
return {
|
|
3893
|
+
valid: true,
|
|
3894
|
+
errors: [],
|
|
3895
|
+
warnings: [],
|
|
3896
|
+
stats: { totalFunctions: 0, withReturnType: 0, totalInterfaces: 0, fullyDocumented: 0, exportedWithoutJSDoc: 0 }
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3899
|
+
const project = createProject(projectPath, tsFiles);
|
|
3900
|
+
for (const sf of project.getSourceFiles()) {
|
|
3901
|
+
const rel = relative8(absPath, sf.getFilePath());
|
|
3902
|
+
if (rel.startsWith("..") || rel.includes("node_modules")) continue;
|
|
3903
|
+
for (const fn of sf.getFunctions()) {
|
|
3904
|
+
const name = fn.getName();
|
|
3905
|
+
if (!name) continue;
|
|
3906
|
+
totalFunctions++;
|
|
3907
|
+
const hasExplicitReturn = fn.getReturnTypeNode() !== void 0;
|
|
3908
|
+
if (hasExplicitReturn) withReturnType++;
|
|
3909
|
+
if (fn.isExported()) {
|
|
3910
|
+
if (!hasExplicitReturn) {
|
|
3911
|
+
warnings.push({
|
|
3912
|
+
file: rel,
|
|
3913
|
+
name,
|
|
3914
|
+
line: fn.getStartLineNumber(),
|
|
3915
|
+
message: `Exported function '${name}' has no explicit return type. SDD requires explicit types for public API.`
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
const jsDocs = fn.getJsDocs();
|
|
3919
|
+
if (jsDocs.length === 0) {
|
|
3920
|
+
exportedWithoutJSDoc++;
|
|
3921
|
+
warnings.push({
|
|
3922
|
+
file: rel,
|
|
3923
|
+
name,
|
|
3924
|
+
line: fn.getStartLineNumber(),
|
|
3925
|
+
message: `Exported function '${name}' has no JSDoc. SDD recommends documenting all public API.`
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
for (const param of fn.getParameters()) {
|
|
3929
|
+
if (!param.getTypeNode()) {
|
|
3930
|
+
errors.push({
|
|
3931
|
+
file: rel,
|
|
3932
|
+
name: `${name}(${param.getName()})`,
|
|
3933
|
+
line: fn.getStartLineNumber(),
|
|
3934
|
+
message: `Parameter '${param.getName()}' in '${name}' has no explicit type annotation.`
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
for (const iface of sf.getInterfaces()) {
|
|
3941
|
+
totalInterfaces++;
|
|
3942
|
+
const jsDocs = iface.getJsDocs();
|
|
3943
|
+
if (jsDocs.length > 0) fullyDocumented++;
|
|
3944
|
+
if (iface.isExported() && jsDocs.length === 0) {
|
|
3945
|
+
exportedWithoutJSDoc++;
|
|
3946
|
+
}
|
|
3947
|
+
if (iface.getProperties().length === 0 && iface.getMethods().length === 0) {
|
|
3948
|
+
warnings.push({
|
|
3949
|
+
file: rel,
|
|
3950
|
+
name: iface.getName(),
|
|
3951
|
+
line: iface.getStartLineNumber(),
|
|
3952
|
+
message: `Interface '${iface.getName()}' is empty. Consider removing or adding properties.`
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return {
|
|
3958
|
+
valid: errors.length === 0,
|
|
3959
|
+
errors,
|
|
3960
|
+
warnings,
|
|
3961
|
+
stats: {
|
|
3962
|
+
totalFunctions,
|
|
3963
|
+
withReturnType,
|
|
3964
|
+
totalInterfaces,
|
|
3965
|
+
fullyDocumented,
|
|
3966
|
+
exportedWithoutJSDoc
|
|
3967
|
+
}
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
function getJSDoc(node) {
|
|
3971
|
+
const docs = node.getJsDocs();
|
|
3972
|
+
if (docs.length === 0) return void 0;
|
|
3973
|
+
const desc = docs[0].getDescription().trim();
|
|
3974
|
+
return desc || void 0;
|
|
3975
|
+
}
|
|
3976
|
+
function extractInterfaces(sf, file, out) {
|
|
3977
|
+
for (const iface of sf.getInterfaces()) {
|
|
3978
|
+
const props = iface.getProperties().map((p) => ({
|
|
3979
|
+
name: p.getName(),
|
|
3980
|
+
type: p.getType().getText(p),
|
|
3981
|
+
optional: p.hasQuestionToken()
|
|
3982
|
+
}));
|
|
3983
|
+
const ext = iface.getExtends().map((e) => e.getText());
|
|
3984
|
+
out.push({
|
|
3985
|
+
kind: "interface",
|
|
3986
|
+
name: iface.getName(),
|
|
3987
|
+
file,
|
|
3988
|
+
line: iface.getStartLineNumber(),
|
|
3989
|
+
exported: iface.isExported(),
|
|
3990
|
+
signature: buildInterfaceSig(iface.getName(), props, ext),
|
|
3991
|
+
properties: props,
|
|
3992
|
+
extends: ext.length > 0 ? ext : void 0,
|
|
3993
|
+
jsdoc: getJSDoc(iface)
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
function extractTypeAliases(sf, file, out) {
|
|
3998
|
+
for (const ta of sf.getTypeAliases()) {
|
|
3999
|
+
out.push({
|
|
4000
|
+
kind: "type",
|
|
4001
|
+
name: ta.getName(),
|
|
4002
|
+
file,
|
|
4003
|
+
line: ta.getStartLineNumber(),
|
|
4004
|
+
exported: ta.isExported(),
|
|
4005
|
+
signature: `type ${ta.getName()} = ${ta.getTypeNode()?.getText() ?? ta.getType().getText(ta)}`,
|
|
4006
|
+
jsdoc: getJSDoc(ta)
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
function extractEnums(sf, file, out) {
|
|
4011
|
+
for (const en of sf.getEnums()) {
|
|
4012
|
+
const members = en.getMembers().map((m) => m.getName());
|
|
4013
|
+
out.push({
|
|
4014
|
+
kind: "enum",
|
|
4015
|
+
name: en.getName(),
|
|
4016
|
+
file,
|
|
4017
|
+
line: en.getStartLineNumber(),
|
|
4018
|
+
exported: en.isExported(),
|
|
4019
|
+
signature: `enum ${en.getName()} { ${members.join(", ")} }`,
|
|
4020
|
+
jsdoc: getJSDoc(en)
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
function extractFunctionSigs(sf, file, out) {
|
|
4025
|
+
for (const fn of sf.getFunctions()) {
|
|
4026
|
+
const name = fn.getName();
|
|
4027
|
+
if (!name) continue;
|
|
4028
|
+
const params = fn.getParameters().map((p) => {
|
|
4029
|
+
const typeNode = p.getTypeNode();
|
|
4030
|
+
return `${p.getName()}: ${typeNode?.getText() ?? p.getType().getText(p)}`;
|
|
4031
|
+
});
|
|
4032
|
+
const retNode = fn.getReturnTypeNode();
|
|
4033
|
+
const retType = retNode?.getText() ?? fn.getReturnType().getText(fn);
|
|
4034
|
+
out.push({
|
|
4035
|
+
kind: "function",
|
|
4036
|
+
name,
|
|
4037
|
+
file,
|
|
4038
|
+
line: fn.getStartLineNumber(),
|
|
4039
|
+
exported: fn.isExported(),
|
|
4040
|
+
signature: `function ${name}(${params.join(", ")}): ${retType}`,
|
|
4041
|
+
jsdoc: getJSDoc(fn)
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
function extractClassSigs(sf, file, out) {
|
|
4046
|
+
for (const cls of sf.getClasses()) {
|
|
4047
|
+
const name = cls.getName();
|
|
4048
|
+
if (!name) continue;
|
|
4049
|
+
const methods = cls.getMethods().map((m) => {
|
|
4050
|
+
const params = m.getParameters().map((p) => `${p.getName()}: ${p.getType().getText(p)}`);
|
|
4051
|
+
const ret = m.getReturnType().getText(m);
|
|
4052
|
+
return `${m.getName()}(${params.join(", ")}): ${ret}`;
|
|
4053
|
+
});
|
|
4054
|
+
const ext = cls.getExtends()?.getText();
|
|
4055
|
+
const impl = cls.getImplements().map((i) => i.getText());
|
|
4056
|
+
out.push({
|
|
4057
|
+
kind: "class",
|
|
4058
|
+
name,
|
|
4059
|
+
file,
|
|
4060
|
+
line: cls.getStartLineNumber(),
|
|
4061
|
+
exported: cls.isExported(),
|
|
4062
|
+
signature: `class ${name}${ext ? ` extends ${ext}` : ""}${impl.length > 0 ? ` implements ${impl.join(", ")}` : ""} { ${methods.join("; ")} }`,
|
|
4063
|
+
extends: ext ? [ext, ...impl] : impl.length > 0 ? impl : void 0,
|
|
4064
|
+
jsdoc: getJSDoc(cls)
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
function buildInterfaceSig(name, props, ext) {
|
|
4069
|
+
const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
|
|
4070
|
+
const propsStr = props.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join("; ");
|
|
4071
|
+
return `interface ${name}${extStr} { ${propsStr} }`;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// src/core/focus.ts
|
|
4075
|
+
import { resolve as resolve16, relative as relative9 } from "path";
|
|
4076
|
+
function buildFocusContext(analysis, targetPaths, depth = 1) {
|
|
4077
|
+
const absProjectPath = resolve16(analysis.projectPath);
|
|
4078
|
+
const normalizedTargets = targetPaths.map((p) => {
|
|
4079
|
+
if (p.startsWith("/")) return relative9(absProjectPath, p);
|
|
4080
|
+
return p;
|
|
4081
|
+
});
|
|
4082
|
+
const targetFiles = analysis.files.filter(
|
|
4083
|
+
(f) => normalizedTargets.some((t) => f.relativePath === t || f.relativePath.endsWith(t))
|
|
4084
|
+
);
|
|
4085
|
+
if (targetFiles.length === 0) {
|
|
4086
|
+
return {
|
|
4087
|
+
targetFiles: [],
|
|
4088
|
+
dependencies: [],
|
|
4089
|
+
allFiles: [],
|
|
4090
|
+
totalTokens: 0,
|
|
4091
|
+
savedTokens: analysis.totalTokens,
|
|
4092
|
+
savingsPercent: 100
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
const depPaths = /* @__PURE__ */ new Set();
|
|
4096
|
+
try {
|
|
4097
|
+
const tsFiles = analysis.files.filter((f) => ["ts", "tsx", "js", "jsx"].includes(f.extension)).map((f) => f.path);
|
|
4098
|
+
if (tsFiles.length > 0) {
|
|
4099
|
+
const project = createProject(analysis.projectPath, tsFiles);
|
|
4100
|
+
const graph = buildDependencyGraph(project, analysis.projectPath);
|
|
4101
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4102
|
+
let frontier = targetFiles.map((f) => f.relativePath);
|
|
4103
|
+
for (let d = 0; d < depth; d++) {
|
|
4104
|
+
const nextFrontier = [];
|
|
4105
|
+
for (const file of frontier) {
|
|
4106
|
+
if (visited.has(file)) continue;
|
|
4107
|
+
visited.add(file);
|
|
4108
|
+
for (const edge of graph.edges) {
|
|
4109
|
+
if (edge.from === file && !visited.has(edge.to)) {
|
|
4110
|
+
depPaths.add(edge.to);
|
|
4111
|
+
nextFrontier.push(edge.to);
|
|
4112
|
+
}
|
|
4113
|
+
if (edge.to === file && !visited.has(edge.from)) {
|
|
4114
|
+
depPaths.add(edge.from);
|
|
4115
|
+
nextFrontier.push(edge.from);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
frontier = nextFrontier;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
} catch {
|
|
4123
|
+
}
|
|
4124
|
+
const targetRelPaths = new Set(targetFiles.map((f) => f.relativePath));
|
|
4125
|
+
const dependencies = analysis.files.filter(
|
|
4126
|
+
(f) => depPaths.has(f.relativePath) && !targetRelPaths.has(f.relativePath)
|
|
4127
|
+
);
|
|
4128
|
+
const allFiles = [...targetFiles, ...dependencies];
|
|
4129
|
+
const totalTokens = allFiles.reduce((sum, f) => sum + f.tokens, 0);
|
|
4130
|
+
const savedTokens = analysis.totalTokens - totalTokens;
|
|
4131
|
+
const savingsPercent = analysis.totalTokens > 0 ? Math.round(savedTokens / analysis.totalTokens * 100) : 0;
|
|
4132
|
+
return {
|
|
4133
|
+
targetFiles,
|
|
4134
|
+
dependencies,
|
|
4135
|
+
allFiles,
|
|
4136
|
+
totalTokens,
|
|
4137
|
+
savedTokens,
|
|
4138
|
+
savingsPercent
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
function formatFocusDocument(analysis, focus) {
|
|
4142
|
+
const lines = [];
|
|
4143
|
+
lines.push(`# Focus Context \u2014 ${analysis.projectName}`);
|
|
4144
|
+
lines.push("");
|
|
4145
|
+
lines.push("> Focused context for targeted work. Only relevant files included.");
|
|
4146
|
+
lines.push("");
|
|
4147
|
+
lines.push(`| Property | Value |`);
|
|
4148
|
+
lines.push(`|----------|-------|`);
|
|
4149
|
+
lines.push(`| Target files | ${focus.targetFiles.length} |`);
|
|
4150
|
+
lines.push(`| Dependencies | ${focus.dependencies.length} |`);
|
|
4151
|
+
lines.push(`| Total tokens | ~${Math.round(focus.totalTokens / 1e3)}K |`);
|
|
4152
|
+
lines.push(`| Saved | ~${Math.round(focus.savedTokens / 1e3)}K tokens (${focus.savingsPercent}% reduction) |`);
|
|
4153
|
+
lines.push("");
|
|
4154
|
+
lines.push("## Target Files (read these first)");
|
|
4155
|
+
lines.push("");
|
|
4156
|
+
for (const f of focus.targetFiles) {
|
|
4157
|
+
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
|
|
4158
|
+
}
|
|
4159
|
+
lines.push("");
|
|
4160
|
+
if (focus.dependencies.length > 0) {
|
|
4161
|
+
lines.push("## Dependencies (read on demand)");
|
|
4162
|
+
lines.push("");
|
|
4163
|
+
for (const f of focus.dependencies) {
|
|
4164
|
+
lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
|
|
4165
|
+
}
|
|
4166
|
+
lines.push("");
|
|
4167
|
+
}
|
|
4168
|
+
lines.push("## Instructions");
|
|
4169
|
+
lines.push("");
|
|
4170
|
+
lines.push("- Focus ONLY on the target files listed above");
|
|
4171
|
+
lines.push("- Read dependencies only when needed to understand imports");
|
|
4172
|
+
lines.push("- Do NOT modify files outside this focus set unless asked");
|
|
4173
|
+
lines.push("");
|
|
4174
|
+
return lines.join("\n");
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
// src/core/todo-scanner.ts
|
|
4178
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4179
|
+
var TODO_PATTERNS = [
|
|
4180
|
+
{ type: "FIXME", regex: /\/\/\s*FIXME[:\s]*(.*)/i },
|
|
4181
|
+
{ type: "TODO", regex: /\/\/\s*TODO[:\s]*(.*)/i },
|
|
4182
|
+
{ type: "HACK", regex: /\/\/\s*HACK[:\s]*(.*)/i },
|
|
4183
|
+
{ type: "XXX", regex: /\/\/\s*XXX[:\s]*(.*)/i },
|
|
4184
|
+
{ type: "NOTE", regex: /\/\/\s*NOTE[:\s]*(.*)/i },
|
|
4185
|
+
// Also match # comments (Python, Ruby, YAML)
|
|
4186
|
+
{ type: "FIXME", regex: /#\s*FIXME[:\s]*(.*)/i },
|
|
4187
|
+
{ type: "TODO", regex: /#\s*TODO[:\s]*(.*)/i },
|
|
4188
|
+
{ type: "HACK", regex: /#\s*HACK[:\s]*(.*)/i }
|
|
4189
|
+
];
|
|
4190
|
+
function getPriority(type, tier) {
|
|
4191
|
+
if (type === "FIXME" || type === "HACK") {
|
|
4192
|
+
return tier === "hot" ? "high" : tier === "warm" ? "high" : "medium";
|
|
4193
|
+
}
|
|
4194
|
+
if (type === "TODO") {
|
|
4195
|
+
return tier === "hot" ? "high" : tier === "warm" ? "medium" : "low";
|
|
4196
|
+
}
|
|
4197
|
+
if (type === "XXX") return tier === "hot" ? "high" : "medium";
|
|
4198
|
+
return "low";
|
|
4199
|
+
}
|
|
4200
|
+
async function scanTodos(files) {
|
|
4201
|
+
const items = [];
|
|
4202
|
+
for (const file of files) {
|
|
4203
|
+
try {
|
|
4204
|
+
const content = await readFile9(file.path, "utf-8");
|
|
4205
|
+
const lines = content.split("\n");
|
|
4206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4207
|
+
const line = lines[i];
|
|
4208
|
+
for (const pattern of TODO_PATTERNS) {
|
|
4209
|
+
const match = line.match(pattern.regex);
|
|
4210
|
+
if (match) {
|
|
4211
|
+
const text = match[1]?.trim() || "(no description)";
|
|
4212
|
+
items.push({
|
|
4213
|
+
file: file.relativePath,
|
|
4214
|
+
line: i + 1,
|
|
4215
|
+
type: pattern.type,
|
|
4216
|
+
text,
|
|
4217
|
+
tier: file.tier,
|
|
4218
|
+
priority: getPriority(pattern.type, file.tier)
|
|
4219
|
+
});
|
|
4220
|
+
break;
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
} catch {
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
const tierOrder = { hot: 0, warm: 1, cold: 2 };
|
|
4228
|
+
const prioOrder = { high: 0, medium: 1, low: 2 };
|
|
4229
|
+
items.sort(
|
|
4230
|
+
(a, b) => prioOrder[a.priority] - prioOrder[b.priority] || tierOrder[a.tier] - tierOrder[b.tier]
|
|
4231
|
+
);
|
|
4232
|
+
const byType = {};
|
|
4233
|
+
const byPriority = {};
|
|
4234
|
+
for (const item of items) {
|
|
4235
|
+
(byType[item.type] ??= []).push(item);
|
|
4236
|
+
(byPriority[item.priority] ??= []).push(item);
|
|
4237
|
+
}
|
|
4238
|
+
return { items, byType, byPriority, totalCount: items.length };
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
// src/core/gitignore-parser.ts
|
|
4242
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4243
|
+
import { join as join10, resolve as resolve17 } from "path";
|
|
4244
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4245
|
+
var KNOWN_DIRS = /* @__PURE__ */ new Set([
|
|
4246
|
+
"node_modules",
|
|
4247
|
+
".git",
|
|
4248
|
+
"dist",
|
|
4249
|
+
"build",
|
|
4250
|
+
"out",
|
|
4251
|
+
".next",
|
|
4252
|
+
".nuxt",
|
|
4253
|
+
".svelte-kit",
|
|
4254
|
+
"coverage",
|
|
4255
|
+
"__pycache__",
|
|
4256
|
+
".pytest_cache",
|
|
4257
|
+
"venv",
|
|
4258
|
+
".venv",
|
|
4259
|
+
"env",
|
|
4260
|
+
".env",
|
|
4261
|
+
"vendor",
|
|
4262
|
+
"target",
|
|
4263
|
+
"bin",
|
|
4264
|
+
"obj",
|
|
4265
|
+
".idea",
|
|
4266
|
+
".vscode",
|
|
4267
|
+
".DS_Store",
|
|
4268
|
+
".turbo",
|
|
4269
|
+
".vercel",
|
|
4270
|
+
".output",
|
|
4271
|
+
".cache",
|
|
4272
|
+
"tmp",
|
|
4273
|
+
".tmp",
|
|
4274
|
+
".parcel-cache",
|
|
4275
|
+
".webpack",
|
|
4276
|
+
".rollup",
|
|
4277
|
+
".sass-cache",
|
|
4278
|
+
"bower_components",
|
|
4279
|
+
".gradle",
|
|
4280
|
+
".mvn"
|
|
4281
|
+
]);
|
|
4282
|
+
async function parseGitignore(projectPath) {
|
|
4283
|
+
const absPath = resolve17(projectPath);
|
|
4284
|
+
const gitignorePath = join10(absPath, ".gitignore");
|
|
4285
|
+
if (!existsSync3(gitignorePath)) return null;
|
|
4286
|
+
const content = await readFile10(gitignorePath, "utf-8");
|
|
4287
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
4288
|
+
const ignoreDirs = [];
|
|
4289
|
+
const ignorePatterns = [];
|
|
4290
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4291
|
+
for (const line of lines) {
|
|
4292
|
+
if (line.startsWith("!")) continue;
|
|
4293
|
+
const cleaned = line.replace(/\/$/, "").replace(/^\//, "");
|
|
4294
|
+
if (seen.has(cleaned)) continue;
|
|
4295
|
+
seen.add(cleaned);
|
|
4296
|
+
if (isDirPattern(line, cleaned)) {
|
|
4297
|
+
ignoreDirs.push(cleaned);
|
|
4298
|
+
} else if (isFilePattern(line)) {
|
|
4299
|
+
ignorePatterns.push(line);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
return { ignoreDirs, ignorePatterns, raw: lines };
|
|
4303
|
+
}
|
|
4304
|
+
function isDirPattern(raw, cleaned) {
|
|
4305
|
+
if (raw.endsWith("/")) return true;
|
|
4306
|
+
if (KNOWN_DIRS.has(cleaned)) return true;
|
|
4307
|
+
if (!cleaned.includes(".") && !cleaned.includes("*") && !cleaned.includes("/")) return true;
|
|
4308
|
+
if (cleaned.includes("/") && !cleaned.includes("*") && !cleaned.includes(".")) return true;
|
|
4309
|
+
return false;
|
|
4310
|
+
}
|
|
4311
|
+
function isFilePattern(line) {
|
|
4312
|
+
if (line.startsWith("*.")) return true;
|
|
4313
|
+
if (line.includes(".") && !line.includes("/") && !line.endsWith("/")) return true;
|
|
4314
|
+
return false;
|
|
4315
|
+
}
|
|
4316
|
+
function mergeWithDefaults(gitignoreResult, defaultDirs, defaultPatterns) {
|
|
4317
|
+
const dirSet = /* @__PURE__ */ new Set([...defaultDirs, ...gitignoreResult.ignoreDirs]);
|
|
4318
|
+
const patternSet = /* @__PURE__ */ new Set([...defaultPatterns, ...gitignoreResult.ignorePatterns]);
|
|
4319
|
+
return {
|
|
4320
|
+
ignoreDirs: Array.from(dirSet).sort(),
|
|
4321
|
+
ignorePatterns: Array.from(patternSet).sort()
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
export {
|
|
4325
|
+
AI_ADAPTERS,
|
|
4326
|
+
ALL_AI_TARGETS,
|
|
4327
|
+
ALL_TASK_TYPES,
|
|
4328
|
+
DEFAULT_SECURITY_CONFIG,
|
|
4329
|
+
MODEL_PRICING,
|
|
4330
|
+
ProjectWatcher,
|
|
4331
|
+
ROUTING_RULES,
|
|
4332
|
+
analyzeAllComplexity,
|
|
4333
|
+
analyzeFileComplexity,
|
|
4334
|
+
analyzeProject,
|
|
4335
|
+
applyArtifact,
|
|
4336
|
+
buildAntiHallucination,
|
|
4337
|
+
buildChainOfThought,
|
|
4338
|
+
buildConstraints,
|
|
4339
|
+
buildDependencyGraph,
|
|
4340
|
+
buildEnhancedPrompt,
|
|
4341
|
+
buildFilePriority,
|
|
4342
|
+
buildFocusContext,
|
|
4343
|
+
buildIntegrityManifest,
|
|
4344
|
+
buildOutputFormat,
|
|
4345
|
+
buildRolePriming,
|
|
4346
|
+
buildTierSummary,
|
|
4347
|
+
classifyFile,
|
|
4348
|
+
classifyFileWithAST,
|
|
4349
|
+
classifyFileWithGit,
|
|
4350
|
+
cleanProject,
|
|
4351
|
+
countTokensChars4,
|
|
4352
|
+
countTokensTiktoken,
|
|
4353
|
+
createProject,
|
|
4354
|
+
detectProjectRoot,
|
|
4355
|
+
detectStack,
|
|
4356
|
+
detectTestFramework,
|
|
4357
|
+
diffArtifact,
|
|
4358
|
+
endSession,
|
|
4359
|
+
enrichFilesWithAST,
|
|
4360
|
+
enrichFilesWithGit,
|
|
4361
|
+
estimateCost,
|
|
4362
|
+
estimateFileTokens,
|
|
4363
|
+
estimateMonthlySavings,
|
|
4364
|
+
estimateSessionCost,
|
|
4365
|
+
estimateTokens2 as estimateTokens,
|
|
4366
|
+
estimateWeeklyCost,
|
|
4367
|
+
explainTier,
|
|
4368
|
+
exportMetrics,
|
|
4369
|
+
extractSpecs,
|
|
4370
|
+
formatExplanation,
|
|
4371
|
+
formatFocusDocument,
|
|
4372
|
+
freeEncoder,
|
|
4373
|
+
generateClaudeMd,
|
|
4374
|
+
generateClaudeignore,
|
|
4375
|
+
generateForAllTargets,
|
|
4376
|
+
generateForTarget,
|
|
4377
|
+
generatePRContext,
|
|
4378
|
+
generateProjectReport,
|
|
4379
|
+
generateSpecDocument,
|
|
4380
|
+
generateWeeklyReport,
|
|
4381
|
+
getActiveDevelopers,
|
|
4382
|
+
getAuditEntries,
|
|
4383
|
+
getChangedFiles,
|
|
4384
|
+
getCurrentBranch,
|
|
4385
|
+
getCurrentSession,
|
|
4386
|
+
getDashboardData,
|
|
4387
|
+
getFileGitInfo,
|
|
4388
|
+
getGitContext,
|
|
4389
|
+
getLocalCTOPath,
|
|
4390
|
+
getModelPricingTable,
|
|
4391
|
+
getPromptById,
|
|
4392
|
+
getPromptTemplates,
|
|
4393
|
+
getPruneLevelForTier,
|
|
4394
|
+
getSessionMetrics,
|
|
4395
|
+
getTaskDescription,
|
|
4396
|
+
getTaskPromptBlock,
|
|
4397
|
+
getTierFiles,
|
|
4398
|
+
getTodaySessions,
|
|
4399
|
+
getTokenSavings,
|
|
4400
|
+
hasGlobalConfig,
|
|
4401
|
+
hasLocalConfig,
|
|
4402
|
+
hashContent,
|
|
4403
|
+
hashFile,
|
|
4404
|
+
initCTODir,
|
|
4405
|
+
initLocalProject,
|
|
4406
|
+
isGitRepo,
|
|
4407
|
+
listBackups,
|
|
4408
|
+
listPromptCategories,
|
|
4409
|
+
listSessions,
|
|
4410
|
+
loadAnalysis,
|
|
4411
|
+
loadGeneratedArtifact,
|
|
4412
|
+
loadGlobalConfig,
|
|
4413
|
+
loadLocalConfig,
|
|
4414
|
+
loadProjectConfig,
|
|
4415
|
+
logAudit,
|
|
4416
|
+
logFileRead,
|
|
4417
|
+
mergeLocalWithGlobal,
|
|
4418
|
+
mergeWithDefaults,
|
|
4419
|
+
optimizeBudget,
|
|
4420
|
+
parseGitignore,
|
|
4421
|
+
pruneFile,
|
|
4422
|
+
pruneProject,
|
|
4423
|
+
purgeOldAuditEntries,
|
|
4424
|
+
purgeOldData,
|
|
4425
|
+
recommendModel,
|
|
4426
|
+
recommendModelForFiles,
|
|
4427
|
+
renderDashboard,
|
|
4428
|
+
renderPrompt,
|
|
4429
|
+
renderWeeklyReport,
|
|
4430
|
+
revertArtifact,
|
|
4431
|
+
sanitizeContent,
|
|
4432
|
+
saveGlobalConfig,
|
|
4433
|
+
saveLocalConfig,
|
|
4434
|
+
saveProjectConfig,
|
|
4435
|
+
scanContentForSecrets,
|
|
4436
|
+
scanFileForSecrets,
|
|
4437
|
+
scanProjectForSecrets,
|
|
4438
|
+
scanTodos,
|
|
4439
|
+
secureFilePermissions,
|
|
4440
|
+
startSession,
|
|
4441
|
+
validateSpecs,
|
|
4442
|
+
verifyAuditEntry,
|
|
4443
|
+
verifyAuditIntegrity,
|
|
4444
|
+
verifyIntegrity
|
|
4445
|
+
};
|
|
4446
|
+
//# sourceMappingURL=index.js.map
|