composto-ai 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -192
- package/dist/chunk-MFNGSHTZ.js +1163 -0
- package/dist/index.js +31 -1164
- package/dist/mcp/server.js +202 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +7 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
benchmarkFile,
|
|
4
|
+
computeHealthFromTrends,
|
|
5
|
+
detectDecay,
|
|
6
|
+
detectHotspots,
|
|
7
|
+
detectInconsistencies,
|
|
8
|
+
estimateTokens,
|
|
9
|
+
generateLayer,
|
|
10
|
+
getGitLog,
|
|
11
|
+
loadConfig,
|
|
12
|
+
packContext,
|
|
13
|
+
runDetector,
|
|
14
|
+
summarize
|
|
15
|
+
} from "../chunk-MFNGSHTZ.js";
|
|
16
|
+
|
|
17
|
+
// src/mcp/server.ts
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { readFileSync, readdirSync } from "fs";
|
|
22
|
+
import { resolve, relative, join } from "path";
|
|
23
|
+
var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
|
|
24
|
+
function collectFiles(dir, extensions) {
|
|
25
|
+
const files = [];
|
|
26
|
+
try {
|
|
27
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const fullPath = join(dir, entry.name);
|
|
30
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
files.push(...collectFiles(fullPath, extensions));
|
|
33
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
34
|
+
files.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
var server = new McpServer({
|
|
42
|
+
name: "composto",
|
|
43
|
+
version: "0.1.2"
|
|
44
|
+
});
|
|
45
|
+
server.tool(
|
|
46
|
+
"composto_ir",
|
|
47
|
+
"Generate compressed IR (Intermediate Representation) for a source file. Uses AST parsing to keep function signatures, control flow, and dependencies while dropping 89% of noise tokens. Use this instead of reading raw files when you need to understand what a file does.",
|
|
48
|
+
{
|
|
49
|
+
file: z.string().describe("Path to the source file"),
|
|
50
|
+
layer: z.enum(["L0", "L1", "L2", "L3"]).default("L1").describe("L0=structure only, L1=full IR (default), L2=delta context, L3=raw source")
|
|
51
|
+
},
|
|
52
|
+
async ({ file, layer }) => {
|
|
53
|
+
const filePath = resolve(file);
|
|
54
|
+
const code = readFileSync(filePath, "utf-8");
|
|
55
|
+
const projectPath = resolve(".");
|
|
56
|
+
const relPath = relative(projectPath, filePath);
|
|
57
|
+
const config = loadConfig(projectPath);
|
|
58
|
+
const entries = getGitLog(projectPath, 100);
|
|
59
|
+
const trends = {
|
|
60
|
+
hotspots: detectHotspots(entries, { threshold: config.trends.hotspotThreshold, fixRatioThreshold: config.trends.bugFixRatioThreshold }),
|
|
61
|
+
decaySignals: detectDecay(entries),
|
|
62
|
+
inconsistencies: detectInconsistencies(entries)
|
|
63
|
+
};
|
|
64
|
+
const health = computeHealthFromTrends(relPath, trends);
|
|
65
|
+
const irLayer = layer || "L1";
|
|
66
|
+
const result = await generateLayer(irLayer, {
|
|
67
|
+
code,
|
|
68
|
+
filePath: relPath,
|
|
69
|
+
health: health.churn > 0 ? health : null
|
|
70
|
+
});
|
|
71
|
+
const rawTokens = estimateTokens(code);
|
|
72
|
+
const irTokens = estimateTokens(result);
|
|
73
|
+
const saved = rawTokens > 0 ? ((rawTokens - irTokens) / rawTokens * 100).toFixed(1) : "0";
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: `[${relPath} | ${irLayer} | ${irTokens} tokens, ${saved}% saved]
|
|
76
|
+
|
|
77
|
+
${result}` }]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
server.tool(
|
|
82
|
+
"composto_benchmark",
|
|
83
|
+
"Benchmark how much Composto saves across a directory. Shows per-file token savings comparing raw code vs compressed IR.",
|
|
84
|
+
{
|
|
85
|
+
path: z.string().default(".").describe("Directory to benchmark")
|
|
86
|
+
},
|
|
87
|
+
async ({ path }) => {
|
|
88
|
+
const projectPath = resolve(path);
|
|
89
|
+
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
90
|
+
const results = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const code = readFileSync(file, "utf-8");
|
|
93
|
+
const relPath = relative(projectPath, file);
|
|
94
|
+
results.push(await benchmarkFile(code, relPath));
|
|
95
|
+
}
|
|
96
|
+
results.sort((a, b) => b.savedPercent - a.savedPercent);
|
|
97
|
+
const summary = summarize(results);
|
|
98
|
+
const lines = [`Composto Benchmark \u2014 ${files.length} files
|
|
99
|
+
`];
|
|
100
|
+
lines.push("File | Raw | L1 | Saved | Engine");
|
|
101
|
+
lines.push("-----|-----|-----|-------|-------");
|
|
102
|
+
for (const r of results) {
|
|
103
|
+
lines.push(`${r.file} | ${r.rawTokens} | ${r.irL1Tokens} | ${r.savedPercent.toFixed(1)}% | ${r.engine}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(`Total: ${summary.totalRaw} \u2192 ${summary.totalIRL1} tokens (${summary.totalSavedPercent.toFixed(1)}% saved)`);
|
|
107
|
+
lines.push(`Engine: ${summary.astCount} AST, ${summary.fpCount} FP`);
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
server.tool(
|
|
114
|
+
"composto_context",
|
|
115
|
+
"Pack maximum code context into a token budget. Hotspot files get detailed IR (L1), remaining files get structure only (L0). Use this when you need to understand a large codebase without exceeding context limits.",
|
|
116
|
+
{
|
|
117
|
+
path: z.string().default(".").describe("Directory to pack"),
|
|
118
|
+
budget: z.number().default(4e3).describe("Maximum tokens to use")
|
|
119
|
+
},
|
|
120
|
+
async ({ path, budget }) => {
|
|
121
|
+
const projectPath = resolve(path);
|
|
122
|
+
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
123
|
+
const config = loadConfig(projectPath);
|
|
124
|
+
const entries = getGitLog(projectPath, 100);
|
|
125
|
+
const hotspots = detectHotspots(entries, {
|
|
126
|
+
threshold: config.trends.hotspotThreshold,
|
|
127
|
+
fixRatioThreshold: config.trends.bugFixRatioThreshold
|
|
128
|
+
});
|
|
129
|
+
const fileInputs = files.map((file) => {
|
|
130
|
+
const code = readFileSync(file, "utf-8");
|
|
131
|
+
const relPath = relative(projectPath, file);
|
|
132
|
+
return { path: relPath, code, rawTokens: estimateTokens(code) };
|
|
133
|
+
});
|
|
134
|
+
const result = await packContext(fileInputs, { budget, hotspots });
|
|
135
|
+
const lines = [`Composto Context \u2014 ${result.totalTokens}/${result.budget} tokens
|
|
136
|
+
`];
|
|
137
|
+
const l1 = result.entries.filter((e) => e.layer === "L1");
|
|
138
|
+
const l0 = result.entries.filter((e) => e.layer === "L0");
|
|
139
|
+
if (l1.length > 0) {
|
|
140
|
+
lines.push("== L1 (detailed) ==\n");
|
|
141
|
+
for (const entry of l1) {
|
|
142
|
+
const label = hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
|
|
143
|
+
lines.push(`[${label}] ${entry.path}`);
|
|
144
|
+
lines.push(entry.ir);
|
|
145
|
+
lines.push("");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (l0.length > 0) {
|
|
149
|
+
lines.push("== L0 (structure) ==\n");
|
|
150
|
+
for (const entry of l0) {
|
|
151
|
+
lines.push(entry.ir);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.push(`
|
|
155
|
+
Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
server.tool(
|
|
162
|
+
"composto_scan",
|
|
163
|
+
"Scan codebase for security issues (hardcoded secrets, API keys) and debug artifacts (console.log). Zero token cost \u2014 pure local analysis.",
|
|
164
|
+
{
|
|
165
|
+
path: z.string().default(".").describe("Directory to scan")
|
|
166
|
+
},
|
|
167
|
+
async ({ path }) => {
|
|
168
|
+
const projectPath = resolve(path);
|
|
169
|
+
const config = loadConfig(projectPath);
|
|
170
|
+
const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
|
|
171
|
+
const allFindings = [];
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const code = readFileSync(file, "utf-8");
|
|
174
|
+
const relPath = relative(projectPath, file);
|
|
175
|
+
const findings = runDetector(code, relPath, config.watchers);
|
|
176
|
+
allFindings.push(...findings);
|
|
177
|
+
}
|
|
178
|
+
if (allFindings.length === 0) {
|
|
179
|
+
return { content: [{ type: "text", text: "No issues found." }] };
|
|
180
|
+
}
|
|
181
|
+
const lines = [`Found ${allFindings.length} issues:
|
|
182
|
+
`];
|
|
183
|
+
for (const f of allFindings) {
|
|
184
|
+
const icon = f.severity === "critical" ? "!!" : f.severity === "warning" ? " !" : " ";
|
|
185
|
+
const loc = f.line ? `:${f.line}` : "";
|
|
186
|
+
lines.push(`${icon} [${f.severity.toUpperCase()}] ${f.file}${loc}`);
|
|
187
|
+
lines.push(` ${f.message}`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
async function main() {
|
|
195
|
+
const transport = new StdioServerTransport();
|
|
196
|
+
await server.connect(transport);
|
|
197
|
+
}
|
|
198
|
+
main().catch((err) => {
|
|
199
|
+
process.stderr.write(`[composto-mcp] Fatal: ${err}
|
|
200
|
+
`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "composto-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Proactive AI team companion — less tokens, more insight",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"composto": "dist/index.js"
|
|
7
|
+
"composto": "dist/index.js",
|
|
8
|
+
"composto-mcp": "dist/mcp/server.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|
|
12
|
+
"grammars",
|
|
11
13
|
"README.md"
|
|
12
14
|
],
|
|
13
15
|
"repository": {
|
|
@@ -39,8 +41,10 @@
|
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"@anthropic-ai/sdk": "^0.87.0",
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
45
|
"picomatch": "^4.0.4",
|
|
43
46
|
"web-tree-sitter": "^0.26.8",
|
|
44
|
-
"yaml": "^2.8.3"
|
|
47
|
+
"yaml": "^2.8.3",
|
|
48
|
+
"zod": "^4.3.6"
|
|
45
49
|
}
|
|
46
50
|
}
|