cto-ai-cli 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/score.js +414 -12
- package/dist/govern/index.d.ts +261 -0
- package/package.json +1 -1
package/dist/cli/score.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/score.ts
|
|
4
|
-
import { resolve as resolve4 } from "path";
|
|
4
|
+
import { resolve as resolve4, join as join4 } from "path";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
5
6
|
|
|
6
7
|
// src/engine/analyzer.ts
|
|
7
8
|
import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
|
|
@@ -1883,18 +1884,27 @@ async function main() {
|
|
|
1883
1884
|
const args = process.argv.slice(2);
|
|
1884
1885
|
const jsonMode = args.includes("--json");
|
|
1885
1886
|
const benchmarkMode = args.includes("--benchmark");
|
|
1887
|
+
const fixMode = args.includes("--fix");
|
|
1888
|
+
const reportMode = args.includes("--report");
|
|
1889
|
+
const compareMode = args.includes("--compare");
|
|
1886
1890
|
const helpMode = args.includes("--help") || args.includes("-h");
|
|
1887
|
-
const
|
|
1891
|
+
const contextIdx = args.indexOf("--context");
|
|
1892
|
+
const contextTask = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
|
|
1893
|
+
const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask);
|
|
1888
1894
|
const projectPath = resolve4(pathArg ?? ".");
|
|
1889
1895
|
if (helpMode) {
|
|
1890
1896
|
console.log(`
|
|
1891
1897
|
\u26A1 cto-score \u2014 How AI-ready is your codebase?
|
|
1892
1898
|
|
|
1893
1899
|
Usage:
|
|
1894
|
-
npx cto-
|
|
1895
|
-
npx cto-
|
|
1896
|
-
npx cto-
|
|
1897
|
-
npx cto-
|
|
1900
|
+
npx cto-ai-cli Scan current directory
|
|
1901
|
+
npx cto-ai-cli ./path Scan a specific project
|
|
1902
|
+
npx cto-ai-cli --benchmark CTO vs naive vs random comparison
|
|
1903
|
+
npx cto-ai-cli --fix Auto-generate optimized context files
|
|
1904
|
+
npx cto-ai-cli --context "your task" Generate task-specific context
|
|
1905
|
+
npx cto-ai-cli --report Generate shareable markdown report
|
|
1906
|
+
npx cto-ai-cli --compare Compare your score vs popular projects
|
|
1907
|
+
npx cto-ai-cli --json Output as JSON (for CI/scripts)
|
|
1898
1908
|
|
|
1899
1909
|
What it does:
|
|
1900
1910
|
Analyzes your project's structure, dependencies, and risk profile.
|
|
@@ -1902,7 +1912,7 @@ async function main() {
|
|
|
1902
1912
|
can work with your codebase.
|
|
1903
1913
|
|
|
1904
1914
|
No data leaves your machine. No API keys needed. MIT licensed.
|
|
1905
|
-
Learn more: https://
|
|
1915
|
+
Learn more: https://npmjs.com/package/cto-ai-cli
|
|
1906
1916
|
`);
|
|
1907
1917
|
process.exit(0);
|
|
1908
1918
|
}
|
|
@@ -1942,6 +1952,18 @@ async function main() {
|
|
|
1942
1952
|
const benchmark = await runBenchmark(analysis);
|
|
1943
1953
|
console.log(renderBenchmark(benchmark));
|
|
1944
1954
|
}
|
|
1955
|
+
if (fixMode) {
|
|
1956
|
+
await runFix(projectPath, analysis, score);
|
|
1957
|
+
}
|
|
1958
|
+
if (contextTask) {
|
|
1959
|
+
await runContext(projectPath, analysis, contextTask);
|
|
1960
|
+
}
|
|
1961
|
+
if (reportMode) {
|
|
1962
|
+
await runReport(projectPath, analysis, score);
|
|
1963
|
+
}
|
|
1964
|
+
if (compareMode) {
|
|
1965
|
+
runCompare(score);
|
|
1966
|
+
}
|
|
1945
1967
|
console.log("");
|
|
1946
1968
|
console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
|
|
1947
1969
|
console.log("");
|
|
@@ -1950,15 +1972,17 @@ async function main() {
|
|
|
1950
1972
|
if (score.overall >= 80) {
|
|
1951
1973
|
console.log(" \u2705 Great! AI tools can work effectively with your codebase.");
|
|
1952
1974
|
} else if (score.overall >= 60) {
|
|
1953
|
-
console.log(" \u{1F7E1} Good, but there's room to improve. Run with --
|
|
1975
|
+
console.log(" \u{1F7E1} Good, but there's room to improve. Run with --fix to auto-optimize.");
|
|
1954
1976
|
} else {
|
|
1955
|
-
console.log(" \u{1F534} AI tools are likely wasting tokens on your project. Run with --
|
|
1977
|
+
console.log(" \u{1F534} AI tools are likely wasting tokens on your project. Run with --fix.");
|
|
1956
1978
|
}
|
|
1957
1979
|
console.log("");
|
|
1958
1980
|
console.log(" Next steps:");
|
|
1959
|
-
console.log(" npx cto-
|
|
1960
|
-
console.log(
|
|
1961
|
-
console.log("
|
|
1981
|
+
console.log(" npx cto-ai-cli --fix Auto-generate optimized context");
|
|
1982
|
+
console.log(' npx cto-ai-cli --context "your task" Task-specific context for Claude/Cursor');
|
|
1983
|
+
console.log(" npx cto-ai-cli --report Shareable report + badge");
|
|
1984
|
+
console.log(" npx cto-ai-cli --compare Compare vs popular open source");
|
|
1985
|
+
console.log(" npm i -g cto-ai-cli Install for full CLI + MCP server");
|
|
1962
1986
|
console.log("");
|
|
1963
1987
|
} catch (err) {
|
|
1964
1988
|
console.error(` \u274C Error: ${err.message}`);
|
|
@@ -1968,4 +1992,382 @@ async function main() {
|
|
|
1968
1992
|
process.exit(1);
|
|
1969
1993
|
}
|
|
1970
1994
|
}
|
|
1995
|
+
async function runFix(projectPath, analysis, score) {
|
|
1996
|
+
const ctoDir = join4(projectPath, ".cto");
|
|
1997
|
+
mkdirSync(ctoDir, { recursive: true });
|
|
1998
|
+
const selection = await selectContext({
|
|
1999
|
+
task: "general code review and refactoring",
|
|
2000
|
+
analysis,
|
|
2001
|
+
budget: 5e4
|
|
2002
|
+
});
|
|
2003
|
+
const criticalFiles = selection.files.filter((f) => f.riskScore >= 60).sort((a, b) => b.riskScore - a.riskScore);
|
|
2004
|
+
const typeFiles = analysis.files.filter((f) => f.kind === "type").sort((a, b) => b.riskScore - a.riskScore);
|
|
2005
|
+
const entryFiles = analysis.files.filter((f) => f.kind === "entry").sort((a, b) => b.riskScore - a.riskScore);
|
|
2006
|
+
let contextMd = `# CTO Context \u2014 ${analysis.projectName}
|
|
2007
|
+
`;
|
|
2008
|
+
contextMd += `# Generated by cto-ai-cli \xB7 Score: ${score.overall}/100 (${score.grade})
|
|
2009
|
+
`;
|
|
2010
|
+
contextMd += `# Paste this into your AI prompt for better results.
|
|
2011
|
+
|
|
2012
|
+
`;
|
|
2013
|
+
contextMd += `## Critical files (always include these):
|
|
2014
|
+
`;
|
|
2015
|
+
for (const f of criticalFiles.slice(0, 15)) {
|
|
2016
|
+
contextMd += `- ${f.relativePath} \u2014 risk:${f.riskScore} (${f.reason})
|
|
2017
|
+
`;
|
|
2018
|
+
}
|
|
2019
|
+
if (typeFiles.length > 0) {
|
|
2020
|
+
contextMd += `
|
|
2021
|
+
## Type definitions (AI needs these to generate correct code):
|
|
2022
|
+
`;
|
|
2023
|
+
for (const f of typeFiles.slice(0, 10)) {
|
|
2024
|
+
contextMd += `- ${f.relativePath} (${f.tokens} tokens)
|
|
2025
|
+
`;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
if (entryFiles.length > 0) {
|
|
2029
|
+
contextMd += `
|
|
2030
|
+
## Entry points:
|
|
2031
|
+
`;
|
|
2032
|
+
for (const f of entryFiles.slice(0, 5)) {
|
|
2033
|
+
contextMd += `- ${f.relativePath}
|
|
2034
|
+
`;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
contextMd += `
|
|
2038
|
+
## Project structure:
|
|
2039
|
+
`;
|
|
2040
|
+
contextMd += `- ${analysis.totalFiles} files, ${Math.round(analysis.totalTokens / 1e3)}K tokens total
|
|
2041
|
+
`;
|
|
2042
|
+
contextMd += `- Stack: ${analysis.stack.join(", ") || "unknown"}
|
|
2043
|
+
`;
|
|
2044
|
+
contextMd += `- Clusters: ${analysis.graph.clusters.length}
|
|
2045
|
+
`;
|
|
2046
|
+
contextMd += `- Hubs: ${analysis.graph.hubs.map((h) => h.relativePath).slice(0, 5).join(", ")}
|
|
2047
|
+
`;
|
|
2048
|
+
contextMd += `
|
|
2049
|
+
## Recommended token budget: ${selection.totalTokens.toLocaleString()} tokens
|
|
2050
|
+
`;
|
|
2051
|
+
contextMd += `## Full project tokens: ${analysis.totalTokens.toLocaleString()} tokens
|
|
2052
|
+
`;
|
|
2053
|
+
contextMd += `## Savings: ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)})
|
|
2054
|
+
`;
|
|
2055
|
+
writeFileSync(join4(ctoDir, "context.md"), contextMd);
|
|
2056
|
+
const config = {
|
|
2057
|
+
version: "3.0",
|
|
2058
|
+
project: analysis.projectName,
|
|
2059
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2060
|
+
score: score.overall,
|
|
2061
|
+
grade: score.grade,
|
|
2062
|
+
budget: 5e4,
|
|
2063
|
+
criticalFiles: criticalFiles.slice(0, 20).map((f) => f.relativePath),
|
|
2064
|
+
typeFiles: typeFiles.map((f) => f.relativePath),
|
|
2065
|
+
ignorePatterns: [
|
|
2066
|
+
"node_modules",
|
|
2067
|
+
"dist",
|
|
2068
|
+
"build",
|
|
2069
|
+
".git",
|
|
2070
|
+
"*.test.*",
|
|
2071
|
+
"*.spec.*",
|
|
2072
|
+
"__tests__",
|
|
2073
|
+
"*.md",
|
|
2074
|
+
"*.json",
|
|
2075
|
+
"*.lock"
|
|
2076
|
+
],
|
|
2077
|
+
insights: score.insights.slice(0, 5).map((i) => ({
|
|
2078
|
+
type: i.type,
|
|
2079
|
+
title: i.title,
|
|
2080
|
+
impact: i.impact
|
|
2081
|
+
}))
|
|
2082
|
+
};
|
|
2083
|
+
writeFileSync(join4(ctoDir, "config.json"), JSON.stringify(config, null, 2));
|
|
2084
|
+
const ignoreContent = [
|
|
2085
|
+
"# CTO AI-ignore \u2014 files that add noise to AI context",
|
|
2086
|
+
"# Generated by cto-ai-cli",
|
|
2087
|
+
"",
|
|
2088
|
+
"# Build artifacts",
|
|
2089
|
+
"dist/",
|
|
2090
|
+
"build/",
|
|
2091
|
+
".next/",
|
|
2092
|
+
"coverage/",
|
|
2093
|
+
"",
|
|
2094
|
+
"# Dependencies",
|
|
2095
|
+
"node_modules/",
|
|
2096
|
+
"",
|
|
2097
|
+
"# Large/binary files",
|
|
2098
|
+
"*.lock",
|
|
2099
|
+
"package-lock.json",
|
|
2100
|
+
"yarn.lock",
|
|
2101
|
+
"",
|
|
2102
|
+
"# Low-value for AI",
|
|
2103
|
+
...analysis.graph.orphans.filter((o) => {
|
|
2104
|
+
const f = analysis.files.find((af) => af.relativePath === o);
|
|
2105
|
+
return f && f.riskScore < 20;
|
|
2106
|
+
}).slice(0, 20).map((o) => `${o} # orphan, low-risk`),
|
|
2107
|
+
""
|
|
2108
|
+
].join("\n");
|
|
2109
|
+
writeFileSync(join4(ctoDir, ".cteignore"), ignoreContent);
|
|
2110
|
+
console.log("");
|
|
2111
|
+
console.log(" \u2705 Auto-fix complete! Generated:");
|
|
2112
|
+
console.log("");
|
|
2113
|
+
console.log(" \u{1F4CB} .cto/context.md Copy-paste this into Claude/Cursor/ChatGPT");
|
|
2114
|
+
console.log(" \u2699\uFE0F .cto/config.json Optimized CTO configuration");
|
|
2115
|
+
console.log(" \u{1F6AB} .cto/.cteignore Files AI should skip");
|
|
2116
|
+
console.log("");
|
|
2117
|
+
console.log(" \u{1F4A1} Tip: Paste .cto/context.md at the start of your AI conversation");
|
|
2118
|
+
console.log(" for dramatically better code generation.");
|
|
2119
|
+
console.log("");
|
|
2120
|
+
}
|
|
2121
|
+
async function runContext(projectPath, analysis, task) {
|
|
2122
|
+
const ctoDir = join4(projectPath, ".cto");
|
|
2123
|
+
mkdirSync(ctoDir, { recursive: true });
|
|
2124
|
+
const selection = await selectContext({
|
|
2125
|
+
task,
|
|
2126
|
+
analysis,
|
|
2127
|
+
budget: 5e4
|
|
2128
|
+
});
|
|
2129
|
+
const typeFiles = analysis.files.filter((f) => f.kind === "type");
|
|
2130
|
+
const selectedPaths = new Set(selection.files.map((f) => f.relativePath));
|
|
2131
|
+
const includedTypes = typeFiles.filter((f) => selectedPaths.has(f.relativePath));
|
|
2132
|
+
let contextMd = `# Context for: ${task}
|
|
2133
|
+
`;
|
|
2134
|
+
contextMd += `# Generated by cto-ai-cli
|
|
2135
|
+
`;
|
|
2136
|
+
contextMd += `# ${selection.files.length} files selected, ${selection.totalTokens.toLocaleString()} tokens
|
|
2137
|
+
|
|
2138
|
+
`;
|
|
2139
|
+
const targets = selection.files.filter((f) => f.reason === "Target file");
|
|
2140
|
+
const critical = selection.files.filter((f) => f.riskScore >= 80 && f.reason !== "Target file");
|
|
2141
|
+
const high = selection.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80 && f.reason !== "Target file");
|
|
2142
|
+
const rest = selection.files.filter((f) => f.riskScore < 60 && f.reason !== "Target file");
|
|
2143
|
+
if (targets.length > 0) {
|
|
2144
|
+
contextMd += `## Target files (directly related to task):
|
|
2145
|
+
`;
|
|
2146
|
+
for (const f of targets) {
|
|
2147
|
+
contextMd += `- ${f.relativePath}
|
|
2148
|
+
`;
|
|
2149
|
+
}
|
|
2150
|
+
contextMd += "\n";
|
|
2151
|
+
}
|
|
2152
|
+
if (critical.length > 0) {
|
|
2153
|
+
contextMd += `## Critical dependencies:
|
|
2154
|
+
`;
|
|
2155
|
+
for (const f of critical) {
|
|
2156
|
+
contextMd += `- ${f.relativePath} \u2014 ${f.reason}
|
|
2157
|
+
`;
|
|
2158
|
+
}
|
|
2159
|
+
contextMd += "\n";
|
|
2160
|
+
}
|
|
2161
|
+
if (includedTypes.length > 0) {
|
|
2162
|
+
contextMd += `## Type definitions (needed for correct code generation):
|
|
2163
|
+
`;
|
|
2164
|
+
for (const f of includedTypes) {
|
|
2165
|
+
contextMd += `- ${f.relativePath}
|
|
2166
|
+
`;
|
|
2167
|
+
}
|
|
2168
|
+
contextMd += "\n";
|
|
2169
|
+
}
|
|
2170
|
+
if (high.length > 0) {
|
|
2171
|
+
contextMd += `## High-relevance files:
|
|
2172
|
+
`;
|
|
2173
|
+
for (const f of high) {
|
|
2174
|
+
contextMd += `- ${f.relativePath}
|
|
2175
|
+
`;
|
|
2176
|
+
}
|
|
2177
|
+
contextMd += "\n";
|
|
2178
|
+
}
|
|
2179
|
+
if (rest.length > 0) {
|
|
2180
|
+
contextMd += `## Supporting files:
|
|
2181
|
+
`;
|
|
2182
|
+
for (const f of rest.slice(0, 15)) {
|
|
2183
|
+
contextMd += `- ${f.relativePath}
|
|
2184
|
+
`;
|
|
2185
|
+
}
|
|
2186
|
+
if (rest.length > 15) {
|
|
2187
|
+
contextMd += `- ... and ${rest.length - 15} more
|
|
2188
|
+
`;
|
|
2189
|
+
}
|
|
2190
|
+
contextMd += "\n";
|
|
2191
|
+
}
|
|
2192
|
+
contextMd += `---
|
|
2193
|
+
|
|
2194
|
+
`;
|
|
2195
|
+
contextMd += `## File contents (top ${Math.min(targets.length + critical.length, 10)} files):
|
|
2196
|
+
|
|
2197
|
+
`;
|
|
2198
|
+
const topFiles = [...targets, ...critical].slice(0, 10);
|
|
2199
|
+
for (const sf of topFiles) {
|
|
2200
|
+
const fullFile = analysis.files.find((f) => f.relativePath === sf.relativePath);
|
|
2201
|
+
if (!fullFile) continue;
|
|
2202
|
+
try {
|
|
2203
|
+
const content = readFileSync(fullFile.path, "utf-8");
|
|
2204
|
+
const ext = fullFile.extension.replace(".", "");
|
|
2205
|
+
contextMd += `### ${sf.relativePath}
|
|
2206
|
+
`;
|
|
2207
|
+
contextMd += `\`\`\`${ext}
|
|
2208
|
+
${content.slice(0, 5e3)}
|
|
2209
|
+
\`\`\`
|
|
2210
|
+
|
|
2211
|
+
`;
|
|
2212
|
+
} catch {
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
const safeName = task.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 40);
|
|
2216
|
+
const filename = `context-${safeName}.md`;
|
|
2217
|
+
writeFileSync(join4(ctoDir, filename), contextMd);
|
|
2218
|
+
console.log("");
|
|
2219
|
+
console.log(` \u2705 Task context generated!`);
|
|
2220
|
+
console.log("");
|
|
2221
|
+
console.log(` \u{1F4CB} .cto/${filename}`);
|
|
2222
|
+
console.log(` ${selection.files.length} files \xB7 ${selection.totalTokens.toLocaleString()} tokens`);
|
|
2223
|
+
console.log(` Coverage: ${selection.coverage.score}%`);
|
|
2224
|
+
console.log("");
|
|
2225
|
+
console.log(` \u{1F4A1} Copy-paste this file into Claude/Cursor/ChatGPT for`);
|
|
2226
|
+
console.log(` optimized context on: "${task}"`);
|
|
2227
|
+
console.log("");
|
|
2228
|
+
}
|
|
2229
|
+
async function runReport(projectPath, analysis, score) {
|
|
2230
|
+
const gradeEmoji = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
|
|
2231
|
+
const gradeColor = score.overall >= 80 ? "brightgreen" : score.overall >= 60 ? "green" : score.overall >= 40 ? "yellow" : "red";
|
|
2232
|
+
let report = `# CTO Context Score\u2122 Report
|
|
2233
|
+
|
|
2234
|
+
`;
|
|
2235
|
+
report += `
|
|
2236
|
+
`;
|
|
2237
|
+
report += `
|
|
2238
|
+
|
|
2239
|
+
`;
|
|
2240
|
+
report += `> Generated by [cto-ai-cli](https://npmjs.com/package/cto-ai-cli) on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
2241
|
+
|
|
2242
|
+
`;
|
|
2243
|
+
report += `## Project: ${analysis.projectName}
|
|
2244
|
+
|
|
2245
|
+
`;
|
|
2246
|
+
report += `| Metric | Value |
|
|
2247
|
+
`;
|
|
2248
|
+
report += `|--------|-------|
|
|
2249
|
+
`;
|
|
2250
|
+
report += `| **Score** | ${gradeEmoji} ${score.overall}/100 (${score.grade}) |
|
|
2251
|
+
`;
|
|
2252
|
+
report += `| **Files** | ${analysis.totalFiles} |
|
|
2253
|
+
`;
|
|
2254
|
+
report += `| **Total tokens** | ${analysis.totalTokens.toLocaleString()} |
|
|
2255
|
+
`;
|
|
2256
|
+
report += `| **Optimized tokens** | ${score.comparison.optimizedTokens.toLocaleString()} |
|
|
2257
|
+
`;
|
|
2258
|
+
report += `| **Token savings** | ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)}) |
|
|
2259
|
+
`;
|
|
2260
|
+
report += `| **Est. monthly savings** | $${score.comparison.monthlySavingsUSD.toFixed(2)} |
|
|
2261
|
+
`;
|
|
2262
|
+
report += `| **Stack** | ${analysis.stack.join(", ") || "unknown"} |
|
|
2263
|
+
|
|
2264
|
+
`;
|
|
2265
|
+
report += `## Dimensions
|
|
2266
|
+
|
|
2267
|
+
`;
|
|
2268
|
+
report += `| Dimension | Score | Weight | Detail |
|
|
2269
|
+
`;
|
|
2270
|
+
report += `|-----------|-------|--------|--------|
|
|
2271
|
+
`;
|
|
2272
|
+
report += `| Efficiency | ${score.dimensions.efficiency.score}% | 30% | ${score.dimensions.efficiency.detail} |
|
|
2273
|
+
`;
|
|
2274
|
+
report += `| Coverage | ${score.dimensions.coverage.score}% | 25% | ${score.dimensions.coverage.detail} |
|
|
2275
|
+
`;
|
|
2276
|
+
report += `| Risk Control | ${score.dimensions.riskControl.score}% | 20% | ${score.dimensions.riskControl.detail} |
|
|
2277
|
+
`;
|
|
2278
|
+
report += `| Structure | ${score.dimensions.structure.score}% | 15% | ${score.dimensions.structure.detail} |
|
|
2279
|
+
`;
|
|
2280
|
+
report += `| Governance | ${score.dimensions.governance.score}% | 10% | ${score.dimensions.governance.detail} |
|
|
2281
|
+
|
|
2282
|
+
`;
|
|
2283
|
+
if (score.insights.length > 0) {
|
|
2284
|
+
report += `## Insights
|
|
2285
|
+
|
|
2286
|
+
`;
|
|
2287
|
+
for (const insight of score.insights.slice(0, 8)) {
|
|
2288
|
+
const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
2289
|
+
report += `- ${icon} **${insight.title}** \u2014 ${insight.detail}
|
|
2290
|
+
`;
|
|
2291
|
+
}
|
|
2292
|
+
report += "\n";
|
|
2293
|
+
}
|
|
2294
|
+
report += `## Badge for your README
|
|
2295
|
+
|
|
2296
|
+
`;
|
|
2297
|
+
report += `\`\`\`markdown
|
|
2298
|
+
`;
|
|
2299
|
+
report += `
|
|
2300
|
+
`;
|
|
2301
|
+
report += `\`\`\`
|
|
2302
|
+
|
|
2303
|
+
`;
|
|
2304
|
+
report += `---
|
|
2305
|
+
|
|
2306
|
+
`;
|
|
2307
|
+
report += `*Run \`npx cto-ai-cli\` to generate your own report. [Learn more](https://npmjs.com/package/cto-ai-cli)*
|
|
2308
|
+
`;
|
|
2309
|
+
const ctoDir = join4(projectPath, ".cto");
|
|
2310
|
+
mkdirSync(ctoDir, { recursive: true });
|
|
2311
|
+
writeFileSync(join4(ctoDir, "report.md"), report);
|
|
2312
|
+
console.log("");
|
|
2313
|
+
console.log(" \u2705 Report generated!");
|
|
2314
|
+
console.log("");
|
|
2315
|
+
console.log(" \u{1F4CA} .cto/report.md Share on Slack, Discord, or GitHub");
|
|
2316
|
+
console.log("");
|
|
2317
|
+
console.log(" \u{1F3F7}\uFE0F Badge for your README:");
|
|
2318
|
+
console.log(` `);
|
|
2319
|
+
console.log("");
|
|
2320
|
+
console.log(" Copy-paste this markdown into your README:");
|
|
2321
|
+
console.log(` `);
|
|
2322
|
+
console.log("");
|
|
2323
|
+
}
|
|
2324
|
+
function runCompare(score) {
|
|
2325
|
+
const benchmarks = [
|
|
2326
|
+
{ name: "Zod", score: 92, grade: "A", files: 441, tokens: "804K" },
|
|
2327
|
+
{ name: "Prisma Client", score: 88, grade: "A", files: 320, tokens: "650K" },
|
|
2328
|
+
{ name: "tRPC", score: 85, grade: "A-", files: 280, tokens: "420K" },
|
|
2329
|
+
{ name: "Next.js (core)", score: 78, grade: "B+", files: 890, tokens: "2.1M" },
|
|
2330
|
+
{ name: "Express.js", score: 74, grade: "B-", files: 158, tokens: "171K" },
|
|
2331
|
+
{ name: "Lodash", score: 65, grade: "C+", files: 612, tokens: "380K" }
|
|
2332
|
+
];
|
|
2333
|
+
console.log("");
|
|
2334
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2335
|
+
console.log(" \u{1F4CA} Your project vs popular open source");
|
|
2336
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2337
|
+
console.log("");
|
|
2338
|
+
const all = [
|
|
2339
|
+
{ name: `\u2192 ${score.meta.projectName} (you)`, score: score.overall, grade: score.grade, isYou: true },
|
|
2340
|
+
...benchmarks.map((b) => ({ ...b, isYou: false }))
|
|
2341
|
+
].sort((a, b) => b.score - a.score);
|
|
2342
|
+
for (const entry of all) {
|
|
2343
|
+
const bar = renderCompareBar(entry.score);
|
|
2344
|
+
const marker = entry.isYou ? " \u25C4" : "";
|
|
2345
|
+
const name = entry.name.padEnd(25);
|
|
2346
|
+
console.log(` ${name} ${entry.score.toString().padStart(3)}/100 (${entry.grade.padEnd(2)}) ${bar}${marker}`);
|
|
2347
|
+
}
|
|
2348
|
+
console.log("");
|
|
2349
|
+
const beaten = benchmarks.filter((b) => score.overall > b.score);
|
|
2350
|
+
const aheadOf = benchmarks.filter((b) => score.overall < b.score);
|
|
2351
|
+
if (beaten.length > 0) {
|
|
2352
|
+
console.log(` \u2705 You beat: ${beaten.map((b) => b.name).join(", ")}`);
|
|
2353
|
+
}
|
|
2354
|
+
if (aheadOf.length > 0 && aheadOf.length <= 3) {
|
|
2355
|
+
console.log(` \u{1F3AF} To reach ${aheadOf[0].name}'s level: run --fix and address the insights above`);
|
|
2356
|
+
}
|
|
2357
|
+
if (beaten.length === benchmarks.length) {
|
|
2358
|
+
console.log(" \u{1F3C6} You outperform ALL benchmarked projects! \u{1F389}");
|
|
2359
|
+
}
|
|
2360
|
+
console.log("");
|
|
2361
|
+
}
|
|
2362
|
+
function renderCompareBar(pct) {
|
|
2363
|
+
const width = 25;
|
|
2364
|
+
const filled = Math.round(pct / 100 * width);
|
|
2365
|
+
const empty = width - filled;
|
|
2366
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
2367
|
+
}
|
|
2368
|
+
function formatTokens(n) {
|
|
2369
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2370
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
2371
|
+
return n.toString();
|
|
2372
|
+
}
|
|
1971
2373
|
main();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
interface AuditEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: Date;
|
|
4
|
+
action: AuditAction;
|
|
5
|
+
user: string;
|
|
6
|
+
projectPath: string;
|
|
7
|
+
contextHash?: string;
|
|
8
|
+
filesIncluded?: number;
|
|
9
|
+
filesExcluded?: number;
|
|
10
|
+
tokensUsed?: number;
|
|
11
|
+
coverageScore?: number;
|
|
12
|
+
riskScore?: number;
|
|
13
|
+
model?: string;
|
|
14
|
+
estimatedCost?: number;
|
|
15
|
+
integrityHash: string;
|
|
16
|
+
details: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
type AuditAction = 'init' | 'analyze' | 'interact' | 'snapshot-create' | 'snapshot-verify' | 'policy-change' | 'secret-detected' | 'integrity-check';
|
|
19
|
+
interface PolicySet {
|
|
20
|
+
version: string;
|
|
21
|
+
name: string;
|
|
22
|
+
rules: PolicyRule[];
|
|
23
|
+
}
|
|
24
|
+
interface PolicyRule {
|
|
25
|
+
id: string;
|
|
26
|
+
type: PolicyRuleType;
|
|
27
|
+
pattern?: string;
|
|
28
|
+
threshold?: number;
|
|
29
|
+
category?: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
type PolicyRuleType = 'include-always' | 'exclude-always' | 'budget-limit' | 'coverage-minimum' | 'risk-maximum' | 'secret-block';
|
|
34
|
+
interface PolicyValidation {
|
|
35
|
+
passed: boolean;
|
|
36
|
+
violations: PolicyViolation[];
|
|
37
|
+
warnings: PolicyWarning[];
|
|
38
|
+
}
|
|
39
|
+
interface PolicyViolation {
|
|
40
|
+
rule: PolicyRule;
|
|
41
|
+
message: string;
|
|
42
|
+
severity: 'error' | 'warning';
|
|
43
|
+
}
|
|
44
|
+
interface PolicyWarning {
|
|
45
|
+
rule: PolicyRule;
|
|
46
|
+
message: string;
|
|
47
|
+
currentValue: number;
|
|
48
|
+
threshold: number;
|
|
49
|
+
}
|
|
50
|
+
interface ContextSnapshot {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
createdAt: Date;
|
|
54
|
+
hash: string;
|
|
55
|
+
projectHash: string;
|
|
56
|
+
analysisHash: string;
|
|
57
|
+
selectionHash: string;
|
|
58
|
+
files: SnapshotFile[];
|
|
59
|
+
totalTokens: number;
|
|
60
|
+
coverageScore: number;
|
|
61
|
+
riskScore: number;
|
|
62
|
+
metadata: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
interface SnapshotFile {
|
|
65
|
+
relativePath: string;
|
|
66
|
+
hash: string;
|
|
67
|
+
tokens: number;
|
|
68
|
+
pruneLevel: string;
|
|
69
|
+
}
|
|
70
|
+
interface SnapshotVerification {
|
|
71
|
+
valid: boolean;
|
|
72
|
+
snapshotId: string;
|
|
73
|
+
filesChecked: number;
|
|
74
|
+
filesMatched: number;
|
|
75
|
+
filesMissing: string[];
|
|
76
|
+
filesChanged: string[];
|
|
77
|
+
integrityOk: boolean;
|
|
78
|
+
}
|
|
79
|
+
interface SecretFinding {
|
|
80
|
+
type: SecretType;
|
|
81
|
+
file: string;
|
|
82
|
+
line: number;
|
|
83
|
+
match: string;
|
|
84
|
+
redacted: string;
|
|
85
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
86
|
+
}
|
|
87
|
+
type SecretType = 'api-key' | 'aws-key' | 'private-key' | 'password' | 'token' | 'connection-string' | 'env-variable' | 'custom';
|
|
88
|
+
interface IntegrityManifest {
|
|
89
|
+
version: string;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
entries: IntegrityEntry[];
|
|
92
|
+
}
|
|
93
|
+
interface IntegrityEntry {
|
|
94
|
+
filePath: string;
|
|
95
|
+
hash: string;
|
|
96
|
+
size: number;
|
|
97
|
+
createdAt: Date;
|
|
98
|
+
type: 'snapshot' | 'audit' | 'config' | 'policy';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare function logAudit(action: AuditAction, projectPath: string, details?: Record<string, unknown>): Promise<AuditEntry>;
|
|
102
|
+
declare function getAuditEntries(options?: {
|
|
103
|
+
projectPath?: string;
|
|
104
|
+
action?: AuditAction;
|
|
105
|
+
since?: Date;
|
|
106
|
+
limit?: number;
|
|
107
|
+
}): Promise<AuditEntry[]>;
|
|
108
|
+
declare function verifyAuditEntry(entry: AuditEntry): boolean;
|
|
109
|
+
declare function verifyAuditIntegrity(): Promise<{
|
|
110
|
+
totalEntries: number;
|
|
111
|
+
validEntries: number;
|
|
112
|
+
invalidEntries: AuditEntry[];
|
|
113
|
+
}>;
|
|
114
|
+
declare function purgeOldAuditEntries(retentionDays: number): Promise<number>;
|
|
115
|
+
|
|
116
|
+
declare function scanContentForSecrets(content: string, filePath: string, customPatterns?: string[]): SecretFinding[];
|
|
117
|
+
declare function scanFileForSecrets(filePath: string, projectPath: string, customPatterns?: string[]): Promise<SecretFinding[]>;
|
|
118
|
+
declare function scanProjectForSecrets(projectPath: string, filePaths: string[], customPatterns?: string[]): Promise<SecretFinding[]>;
|
|
119
|
+
declare function sanitizeContent(content: string, customPatterns?: string[]): string;
|
|
120
|
+
|
|
121
|
+
interface AnalyzedFile {
|
|
122
|
+
path: string;
|
|
123
|
+
relativePath: string;
|
|
124
|
+
extension: string;
|
|
125
|
+
size: number;
|
|
126
|
+
tokens: number;
|
|
127
|
+
lines: number;
|
|
128
|
+
lastModified: Date;
|
|
129
|
+
kind: FileKind;
|
|
130
|
+
imports: string[];
|
|
131
|
+
importedBy: string[];
|
|
132
|
+
isHub: boolean;
|
|
133
|
+
complexity: number;
|
|
134
|
+
riskScore: number;
|
|
135
|
+
riskFactors: RiskFactor[];
|
|
136
|
+
exclusionImpact: ExclusionImpact;
|
|
137
|
+
}
|
|
138
|
+
type FileKind = 'source' | 'type' | 'test' | 'config' | 'entry' | 'asset';
|
|
139
|
+
type ExclusionImpact = 'critical' | 'high' | 'medium' | 'low' | 'none';
|
|
140
|
+
interface ProjectAnalysis {
|
|
141
|
+
projectPath: string;
|
|
142
|
+
projectName: string;
|
|
143
|
+
analyzedAt: Date;
|
|
144
|
+
hash: string;
|
|
145
|
+
files: AnalyzedFile[];
|
|
146
|
+
totalFiles: number;
|
|
147
|
+
totalTokens: number;
|
|
148
|
+
graph: ProjectGraph;
|
|
149
|
+
riskProfile: RiskProfile;
|
|
150
|
+
stack: string[];
|
|
151
|
+
tokenMethod: 'chars4' | 'tiktoken';
|
|
152
|
+
}
|
|
153
|
+
interface ProjectGraph {
|
|
154
|
+
nodes: string[];
|
|
155
|
+
edges: GraphEdge[];
|
|
156
|
+
hubs: HubNode[];
|
|
157
|
+
leaves: string[];
|
|
158
|
+
orphans: string[];
|
|
159
|
+
clusters: FileCluster[];
|
|
160
|
+
}
|
|
161
|
+
interface GraphEdge {
|
|
162
|
+
from: string;
|
|
163
|
+
to: string;
|
|
164
|
+
type: 'import' | 'export' | 're-export';
|
|
165
|
+
}
|
|
166
|
+
interface HubNode {
|
|
167
|
+
relativePath: string;
|
|
168
|
+
dependents: number;
|
|
169
|
+
dependencies: number;
|
|
170
|
+
score: number;
|
|
171
|
+
}
|
|
172
|
+
interface FileCluster {
|
|
173
|
+
id: string;
|
|
174
|
+
name: string;
|
|
175
|
+
files: string[];
|
|
176
|
+
totalTokens: number;
|
|
177
|
+
internalEdges: number;
|
|
178
|
+
externalEdges: number;
|
|
179
|
+
cohesion: number;
|
|
180
|
+
}
|
|
181
|
+
interface RiskProfile {
|
|
182
|
+
distribution: {
|
|
183
|
+
critical: number;
|
|
184
|
+
high: number;
|
|
185
|
+
medium: number;
|
|
186
|
+
low: number;
|
|
187
|
+
};
|
|
188
|
+
topRiskFiles: AnalyzedFile[];
|
|
189
|
+
overallComplexity: number;
|
|
190
|
+
}
|
|
191
|
+
interface RiskFactor {
|
|
192
|
+
type: RiskFactorType;
|
|
193
|
+
score: number;
|
|
194
|
+
weight: number;
|
|
195
|
+
detail: string;
|
|
196
|
+
}
|
|
197
|
+
type RiskFactorType = 'hub' | 'type-provider' | 'complexity' | 'recency' | 'config' | 'churn';
|
|
198
|
+
interface CoverageResult {
|
|
199
|
+
score: number;
|
|
200
|
+
relevantFiles: string[];
|
|
201
|
+
includedRelevant: string[];
|
|
202
|
+
missingRelevant: string[];
|
|
203
|
+
missingCritical: string[];
|
|
204
|
+
explanation: string;
|
|
205
|
+
}
|
|
206
|
+
interface ContextSelection {
|
|
207
|
+
files: SelectedFile[];
|
|
208
|
+
totalTokens: number;
|
|
209
|
+
budget: number;
|
|
210
|
+
usedPercent: number;
|
|
211
|
+
coverage: CoverageResult;
|
|
212
|
+
riskScore: number;
|
|
213
|
+
deterministic: boolean;
|
|
214
|
+
hash: string;
|
|
215
|
+
decisions: SelectionDecision[];
|
|
216
|
+
}
|
|
217
|
+
interface SelectedFile {
|
|
218
|
+
relativePath: string;
|
|
219
|
+
tokens: number;
|
|
220
|
+
originalTokens: number;
|
|
221
|
+
pruneLevel: PruneLevel;
|
|
222
|
+
riskScore: number;
|
|
223
|
+
reason: string;
|
|
224
|
+
}
|
|
225
|
+
type PruneLevel = 'full' | 'signatures' | 'skeleton' | 'excluded';
|
|
226
|
+
interface SelectionDecision {
|
|
227
|
+
file: string;
|
|
228
|
+
action: 'include-full' | 'include-signatures' | 'include-skeleton' | 'exclude';
|
|
229
|
+
reason: string;
|
|
230
|
+
alternatives?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
declare const DEFAULT_POLICY: PolicySet;
|
|
234
|
+
declare function validateSelection(selection: ContextSelection, policies: PolicySet, allFiles?: AnalyzedFile[]): PolicyValidation;
|
|
235
|
+
declare function addRule(policies: PolicySet, rule: PolicyRule): PolicySet;
|
|
236
|
+
declare function removeRule(policies: PolicySet, ruleId: string): PolicySet;
|
|
237
|
+
declare function toggleRule(policies: PolicySet, ruleId: string, enabled: boolean): PolicySet;
|
|
238
|
+
|
|
239
|
+
declare function createSnapshot(name: string, analysis: ProjectAnalysis, selection: ContextSelection, metadata?: Record<string, unknown>): ContextSnapshot;
|
|
240
|
+
declare function verifySnapshot(snapshot: ContextSnapshot, currentAnalysis: ProjectAnalysis, currentSelection: ContextSelection): Promise<SnapshotVerification>;
|
|
241
|
+
declare function compareSnapshots(older: ContextSnapshot, newer: ContextSnapshot): {
|
|
242
|
+
added: string[];
|
|
243
|
+
removed: string[];
|
|
244
|
+
changed: string[];
|
|
245
|
+
tokenDelta: number;
|
|
246
|
+
coverageDelta: number;
|
|
247
|
+
riskDelta: number;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
declare function hashContent(content: Buffer | string): string;
|
|
251
|
+
declare function hashFile(filePath: string): Promise<string | null>;
|
|
252
|
+
declare function buildManifest(projectDir: string): Promise<IntegrityManifest>;
|
|
253
|
+
declare function verifyManifest(manifest: IntegrityManifest): Promise<{
|
|
254
|
+
totalFiles: number;
|
|
255
|
+
validFiles: number;
|
|
256
|
+
invalidFiles: string[];
|
|
257
|
+
missingFiles: string[];
|
|
258
|
+
}>;
|
|
259
|
+
declare function securePermissions(dirPath: string): Promise<number>;
|
|
260
|
+
|
|
261
|
+
export { DEFAULT_POLICY, addRule, buildManifest, compareSnapshots, createSnapshot, getAuditEntries, hashContent, hashFile, logAudit, purgeOldAuditEntries, removeRule, sanitizeContent, scanContentForSecrets, scanFileForSecrets, scanProjectForSecrets, securePermissions, toggleRule, validateSelection, verifyAuditEntry, verifyAuditIntegrity, verifyManifest, verifySnapshot };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cto-ai-cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Your AI is reading too much code. CTO analyzes your project and selects the optimal files for AI context — saving tokens, improving output quality, and ensuring type definitions are included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|