composto-ai 0.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.
Files changed (3) hide show
  1. package/README.md +293 -0
  2. package/dist/index.js +857 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # Composto
2
+
3
+ **Proactive AI team companion — less tokens, more insight.**
4
+
5
+ Every AI coding tool sends raw source code to LLMs. Composto sends **meaning** — compressed code enriched with codebase health data. The result: fewer tokens carrying more information than raw source ever could.
6
+
7
+ ---
8
+
9
+ ## What Makes It Different
10
+
11
+ | | Traditional AI Tools | Composto |
12
+ |---|---|---|
13
+ | **Paradigm** | Reactive (you ask, it does) | Proactive (it finds, you approve) |
14
+ | **What LLM sees** | Raw source code | Health-Aware IR |
15
+ | **Token usage** | Full files every time | 60-75% savings |
16
+ | **Health context** | None | Hotspots, decay, inconsistencies |
17
+ | **Codebase monitoring** | None | Watcher Engine |
18
+
19
+ ### Health-Aware IR
20
+
21
+ Raw source tells the LLM *what* the code says. Composto IR tells it *what the code means* and *how healthy it is*:
22
+
23
+ ```
24
+ // Raw source: 340 tokens, zero health context
25
+ import { useState, useEffect } from "react";
26
+ export function UserProfile({ userId }) {
27
+ const [user, setUser] = useState(null);
28
+ const [loading, setLoading] = useState(true);
29
+ useEffect(() => { fetchUser(userId).then(...) }, [userId]);
30
+ if (loading) return <Spinner />;
31
+ if (!user) return <NotFound />;
32
+ return <div>{user.name}</div>;
33
+ }
34
+
35
+ // Composto IR: 85 tokens + health context
36
+ USE:react{useState,useEffect}
37
+ OUT FN:UserProfile({userId}) [HOT:12/30 FIX:67% COV:↓ INCON]
38
+ VAR:user = useState(null)
39
+ VAR:loading = useState(true)
40
+ IF:loading -> RET <Spinner />
41
+ IF:!user -> RET <NotFound />
42
+ RET <div>{user.name}</div>
43
+ ```
44
+
45
+ The LLM sees less, knows more, decides better.
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ ### Claude Code
52
+
53
+ ```
54
+ /plugin install composto
55
+ ```
56
+
57
+ ### Cursor
58
+
59
+ ```
60
+ /add-plugin composto
61
+ ```
62
+
63
+ ### Any platform (CLI)
64
+
65
+ ```bash
66
+ npm install -g composto
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Usage
72
+
73
+ ### CLI Commands
74
+
75
+ ```bash
76
+ # Scan codebase for issues
77
+ composto scan .
78
+
79
+ # Analyze codebase health trends
80
+ composto trends .
81
+
82
+ # Generate Health-Aware IR for a file
83
+ composto ir src/auth/login.ts L1
84
+
85
+ # Layer options:
86
+ # L0 — Structure map (~10 tokens)
87
+ # L1 — Health-Aware IR (~85 tokens)
88
+ # L2 — Delta context (~65 tokens)
89
+ # L3 — Raw source (fallback)
90
+ ```
91
+
92
+ ### As a Plugin
93
+
94
+ Once installed, Composto activates automatically. Your AI agent will:
95
+
96
+ 1. **Scan** the codebase for issues before starting work
97
+ 2. **Check trends** for files being modified
98
+ 3. **Use IR** instead of raw source when sharing code context
99
+
100
+ No commands needed — it just works.
101
+
102
+ ---
103
+
104
+ ## What It Does
105
+
106
+ ### IR Engine — Send meaning, not code
107
+
108
+ Four layers of code representation, from most compact to full source:
109
+
110
+ | Layer | Tokens | Use |
111
+ |---|---|---|
112
+ | L0: Structure Map | ~10 | File outline — functions, classes, line numbers |
113
+ | L1: Health-Aware IR | ~85 | Compressed code + health annotations |
114
+ | L2: Delta + Context | ~65 | Only what changed, with surrounding context |
115
+ | L3: Raw Source | variable | Original code, specific lines only |
116
+
117
+ No AST parser. No language-specific dependencies. Works with TypeScript, JavaScript, Python, Go, and more.
118
+
119
+ ### Watcher Engine — Proactive issue detection
120
+
121
+ Detects problems without being asked:
122
+
123
+ - **Security** — Hardcoded secrets, API keys, tokens
124
+ - **Debug artifacts** — `console.log`, `console.debug` left in source
125
+ - **Context-aware severity** — Same issue, different severity in `src/` vs `tests/`
126
+
127
+ ### Trend Analysis — Codebase health over time
128
+
129
+ Analyzes git history to find:
130
+
131
+ - **Hotspots** — Files that change too often with too many bug fixes
132
+ - **Decay signals** — Areas where churn is accelerating
133
+ - **Inconsistencies** — Files touched by many authors with conflicting patterns
134
+
135
+ All trend analysis is zero-token — pure local git analysis.
136
+
137
+ ### Health Annotations — The killer feature
138
+
139
+ IR Engine and Trend Analysis are not separate systems. Health data is embedded directly into code representation:
140
+
141
+ ```
142
+ FN:handleAuth({credentials}) [HOT:15/30 FIX:73% COV:↓ INCON]
143
+ VAR:session = createSession(credentials)
144
+ IF:!session -> RET 401
145
+ ```
146
+
147
+ - `[HOT:15/30]` — 15 changes in last 30 commits
148
+ - `[FIX:73%]` — 73% of changes were bug fixes
149
+ - `[COV:↓]` — Test coverage declining
150
+ - `[INCON]` — Inconsistent patterns from multiple authors
151
+
152
+ Only unhealthy code gets annotated. Healthy files stay clean.
153
+
154
+ ---
155
+
156
+ ## Architecture
157
+
158
+ ```
159
+ +----------------------------------------------+
160
+ | Platform Adapters |
161
+ | Claude Code | VS Code | Cursor | CLI |
162
+ +----------------------------------------------+
163
+ | Watcher Engine |
164
+ | Detector (0 token) -> Interpreter (~100 tok) |
165
+ | + Trend Analysis (hotspots, decay, incon.) |
166
+ +----------------------------------------------+
167
+ | IR Engine |
168
+ | Indentation Intel | Fingerprinting | Delta |
169
+ | + Health Annotations (from Trend Analysis) |
170
+ +----------------------------------------------+
171
+ | Rule-Based Router |
172
+ | Deterministic routing, zero tokens |
173
+ +----------------------------------------------+
174
+ | Agent Pool |
175
+ | Fixer (Haiku) | Reviewer (Sonnet) |
176
+ +----------------------------------------------+
177
+ | Project Memory |
178
+ | .composto/config.yaml | decisions/*.md |
179
+ +----------------------------------------------+
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Configuration
185
+
186
+ Create `.composto/config.yaml` in your project root:
187
+
188
+ ```yaml
189
+ watchers:
190
+ security:
191
+ enabled: true
192
+ severity:
193
+ "src/**": warning
194
+ "tests/**": info
195
+ consoleLog:
196
+ enabled: true
197
+ severity:
198
+ "src/**": warning
199
+ "tests/**": info
200
+
201
+ agents:
202
+ fixer:
203
+ enabled: true
204
+ model: haiku
205
+
206
+ ir:
207
+ deltaContextLines: 3
208
+ confidenceThreshold: 0.6
209
+ genericPatterns: default
210
+
211
+ trends:
212
+ enabled: true
213
+ hotspotThreshold: 10
214
+ bugFixRatioThreshold: 0.5
215
+ decayCheckTrigger: on-commit
216
+ fullReportSchedule: weekly
217
+ ```
218
+
219
+ All settings have sensible defaults. The config file is optional.
220
+
221
+ ---
222
+
223
+ ## How It Works
224
+
225
+ ```
226
+ 1. Developer saves src/auth/login.ts
227
+ |
228
+ 2. Watcher Engine triggers (debounced)
229
+ |
230
+ 3. Detector: pattern match → "hardcoded secret, line 23" (0 tokens)
231
+ |
232
+ 4. IR Engine: generates Health-Aware IR + annotations (0 tokens)
233
+ |
234
+ 5. Router: severity=critical → route to Fixer (0 tokens)
235
+ |
236
+ 6. Fixer: generates fix via IR, not full source (~150 tokens)
237
+ |
238
+ 7. User: "login.ts:23 has a hardcoded secret.
239
+ You added it for debugging. Move to .env?"
240
+ |
241
+ 8. User approves → patch applied
242
+
243
+ Total cost: ~250 tokens. Traditional tools: ~3000+ tokens.
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Tech Stack
249
+
250
+ - **Language:** TypeScript
251
+ - **Runtime:** Node.js
252
+ - **Testing:** Vitest (70 tests)
253
+ - **Build:** tsup
254
+ - **Zero native dependencies** — no tree-sitter, no language-specific parsers
255
+
256
+ ---
257
+
258
+ ## Roadmap
259
+
260
+ ### v0.5 — Usable Alpha
261
+ - Watcher Interpreter (batch Haiku calls for contextual explanations)
262
+ - Reviewer Agent (Sonnet, code review with challenge mode)
263
+ - Project Memory (decisions/ with YAML frontmatter)
264
+ - Python + Go language support
265
+
266
+ ### v1.0 — Public Release
267
+ - Framework-specific fingerprint patterns (React, Express, etc.)
268
+ - VS Code / Cursor / Claude Code deep integrations
269
+ - Benchmark results: Health-Aware IR vs raw source
270
+
271
+ ### v2.0 — Platform
272
+ - Security / Architect agents
273
+ - Custom Agent API
274
+ - Team sync features
275
+
276
+ ---
277
+
278
+ ## Contributing
279
+
280
+ ```bash
281
+ git clone https://github.com/mertcanaltin/composto
282
+ cd composto
283
+ pnpm install
284
+ pnpm test # 70 tests
285
+ pnpm build # builds to dist/
286
+ pnpm dev scan . # run locally
287
+ ```
288
+
289
+ ---
290
+
291
+ ## License
292
+
293
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/commands.ts
4
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
5
+ import { join as join2, relative } from "path";
6
+
7
+ // src/config/loader.ts
8
+ import { readFileSync, existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { parse } from "yaml";
11
+ var DEFAULT_CONFIG = {
12
+ watchers: {
13
+ security: {
14
+ enabled: true,
15
+ severity: { "src/**": "warning", "tests/**": "info" }
16
+ },
17
+ deadCode: { enabled: true, trigger: "on-commit" },
18
+ consoleLog: { enabled: true, severity: { "src/**": "warning", "tests/**": "info" } }
19
+ },
20
+ agents: {
21
+ fixer: { enabled: true, model: "haiku" },
22
+ reviewer: { enabled: false, model: "sonnet" }
23
+ },
24
+ ir: {
25
+ deltaContextLines: 3,
26
+ confidenceThreshold: 0.6,
27
+ genericPatterns: "default"
28
+ },
29
+ trends: {
30
+ enabled: true,
31
+ hotspotThreshold: 10,
32
+ bugFixRatioThreshold: 0.5,
33
+ decayCheckTrigger: "on-commit",
34
+ fullReportSchedule: "weekly"
35
+ }
36
+ };
37
+ function deepMerge(target, source) {
38
+ const result = { ...target };
39
+ for (const key of Object.keys(source)) {
40
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
41
+ result[key] = deepMerge(target[key] ?? {}, source[key]);
42
+ } else {
43
+ result[key] = source[key];
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+ function parseConfig(yamlContent) {
49
+ if (!yamlContent.trim()) return { ...DEFAULT_CONFIG };
50
+ const parsed = parse(yamlContent) ?? {};
51
+ return deepMerge(DEFAULT_CONFIG, parsed);
52
+ }
53
+ function loadConfig(projectPath) {
54
+ const configPath = join(projectPath, ".composto", "config.yaml");
55
+ if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
56
+ const content = readFileSync(configPath, "utf-8");
57
+ return parseConfig(content);
58
+ }
59
+
60
+ // src/watcher/detector.ts
61
+ import picomatch from "picomatch";
62
+ function getSeverity(filePath, severityMap) {
63
+ for (const [glob, severity] of Object.entries(severityMap)) {
64
+ if (picomatch.isMatch(filePath, glob)) return severity;
65
+ }
66
+ return "info";
67
+ }
68
+ var SECRET_PATTERNS = [
69
+ /["'](sk-[a-zA-Z0-9-]{20,})["']/,
70
+ /["'](AKIA[0-9A-Z]{16})["']/,
71
+ /["'](ghp_[a-zA-Z0-9]{36})["']/,
72
+ /(?:password|secret|token|api_?key)\s*[:=]\s*["']([^"']{8,})["']/i
73
+ ];
74
+ function securityRule(code, filePath, severityMap) {
75
+ const findings = [];
76
+ const severity = getSeverity(filePath, severityMap);
77
+ const lines = code.split("\n");
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ if (line.trim().startsWith("//") || line.trim().startsWith("/*")) continue;
81
+ for (const pattern of SECRET_PATTERNS) {
82
+ if (pattern.test(line)) {
83
+ findings.push({
84
+ watcherId: "security",
85
+ severity,
86
+ file: filePath,
87
+ line: i + 1,
88
+ message: "Potential hardcoded secret detected",
89
+ action: {
90
+ type: "agent-required",
91
+ agentHint: { role: "fixer", model: "haiku", contextFiles: [filePath] }
92
+ }
93
+ });
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ return findings;
99
+ }
100
+ function consoleLogRule(code, filePath, severityMap) {
101
+ const findings = [];
102
+ const severity = getSeverity(filePath, severityMap);
103
+ const lines = code.split("\n");
104
+ for (let i = 0; i < lines.length; i++) {
105
+ if (/\bconsole\.(log|debug|info|warn)\b/.test(lines[i])) {
106
+ findings.push({
107
+ watcherId: "consoleLog",
108
+ severity,
109
+ file: filePath,
110
+ line: i + 1,
111
+ message: "console.log detected \u2014 likely debug artifact",
112
+ action: { type: "auto-fix", autoFix: "remove-line" }
113
+ });
114
+ }
115
+ }
116
+ return findings;
117
+ }
118
+ var RULES = {
119
+ security: securityRule,
120
+ consoleLog: consoleLogRule
121
+ };
122
+ function runDetector(code, filePath, watcherConfigs) {
123
+ const findings = [];
124
+ for (const [name, config] of Object.entries(watcherConfigs)) {
125
+ if (!config.enabled) continue;
126
+ const rule = RULES[name];
127
+ if (rule && config.severity) {
128
+ findings.push(...rule(code, filePath, config.severity));
129
+ }
130
+ }
131
+ return findings;
132
+ }
133
+
134
+ // src/ir/structure.ts
135
+ var CLASSIFIERS = [
136
+ [/^(function|def|fn|func)\b/, "function-start"],
137
+ [/^(class|struct|interface)\b/, "type-start"],
138
+ [/^(if|else|elif|switch|match|case)\b/, "branch"],
139
+ [/^(for|while|loop|do)\b/, "loop"],
140
+ [/^(return|yield)\b/, "exit"],
141
+ [/^(import|require|use|from)\b/, "import"],
142
+ [/^(export|pub|public)\b/, "export"],
143
+ [/^(const|let|var|val|mut)\b/, "assignment"],
144
+ [/^(try|catch|except|finally)\b/, "error-handling"],
145
+ [/^(async|await)\b/, "async"],
146
+ [/^(\/\/|\/\*|#)/, "comment"]
147
+ ];
148
+ function classifyLine(firstToken) {
149
+ if (firstToken === "") return "blank";
150
+ for (const [pattern, type] of CLASSIFIERS) {
151
+ if (pattern.test(firstToken)) return type;
152
+ }
153
+ return "unknown";
154
+ }
155
+ function extractStructure(code) {
156
+ const lines = code.split("\n");
157
+ return lines.map((raw, i) => {
158
+ const indent = raw.search(/\S/);
159
+ const trimmed = raw.trim();
160
+ const firstToken = trimmed.split(/[\s({<]/)[0];
161
+ const type = classifyLine(firstToken);
162
+ return {
163
+ line: i + 1,
164
+ indent: indent === -1 ? -1 : indent,
165
+ type,
166
+ raw
167
+ };
168
+ });
169
+ }
170
+
171
+ // src/ir/fingerprint.ts
172
+ var PATTERNS = [
173
+ // import { x, y } from "module"
174
+ {
175
+ match: /^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
176
+ transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
177
+ confidence: 0.95
178
+ },
179
+ // import x from "module"
180
+ {
181
+ match: /^import\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
182
+ transform: (m) => `USE:${m[2]}{${m[1]}}`,
183
+ confidence: 0.95
184
+ },
185
+ // const x = require("module")
186
+ {
187
+ match: /^(?:const|let|var)\s+(\w+)\s*=\s*require\(["']([^"']+)["']\);?\s*$/,
188
+ transform: (m) => `USE:${m[2]}{${m[1]}}`,
189
+ confidence: 0.95
190
+ },
191
+ // export function name(params) {
192
+ {
193
+ match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
194
+ transform: (m) => `OUT FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
195
+ confidence: 0.95
196
+ },
197
+ // function name(params) {
198
+ {
199
+ match: /^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
200
+ transform: (m) => `FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
201
+ confidence: 0.95
202
+ },
203
+ // export class Name extends Base {
204
+ {
205
+ match: /^export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
206
+ transform: (m) => `OUT CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
207
+ confidence: 0.95
208
+ },
209
+ // class Name extends Base {
210
+ {
211
+ match: /^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
212
+ transform: (m) => `CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
213
+ confidence: 0.95
214
+ },
215
+ // if (cond) return expr;
216
+ {
217
+ match: /^if\s*\(([^)]+)\)\s*return\s+(.+);?\s*$/,
218
+ transform: (m) => `IF:${m[1].trim()} -> RET ${m[2].trim().replace(/;$/, "")}`,
219
+ confidence: 0.95
220
+ },
221
+ // if (cond) {
222
+ {
223
+ match: /^if\s*\(([^)]+)\)\s*\{?\s*$/,
224
+ transform: (m) => `IF:${m[1].trim()}`,
225
+ confidence: 0.9
226
+ },
227
+ // for (... of/in ...) {
228
+ {
229
+ match: /^for\s*\((?:const|let|var)\s+(\w+)\s+(?:of|in)\s+(\w+)\)\s*\{?\s*$/,
230
+ transform: (m) => `LOOP:${m[2]} -> ${m[1]}`,
231
+ confidence: 0.9
232
+ },
233
+ // return expr
234
+ {
235
+ match: /^return\s+(.+);?\s*$/,
236
+ transform: (m) => `RET ${m[1].trim().replace(/;$/, "")}`,
237
+ confidence: 0.95
238
+ },
239
+ // return;
240
+ {
241
+ match: /^return;?\s*$/,
242
+ transform: () => "RET",
243
+ confidence: 0.95
244
+ },
245
+ // try {
246
+ {
247
+ match: /^try\s*\{\s*$/,
248
+ transform: () => "TRY:",
249
+ confidence: 0.9
250
+ },
251
+ // catch (e) {
252
+ {
253
+ match: /^(?:\}\s*)?catch\s*\((\w+)\)\s*\{?\s*$/,
254
+ transform: (m) => `CATCH:${m[1]}`,
255
+ confidence: 0.9
256
+ },
257
+ // const [a, b] = expr (destructuring — before regular assignment)
258
+ {
259
+ match: /^(?:const|let|var)\s+\[([^\]]+)\]\s*=\s*(.+);?\s*$/,
260
+ transform: (m) => `VAR:[${m[1].replace(/\s/g, "")}] = ${m[2].replace(/;$/, "").trim()}`,
261
+ confidence: 0.65
262
+ },
263
+ // const name = value;
264
+ {
265
+ match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+);?\s*$/,
266
+ transform: (m) => {
267
+ const prefix = m[0].startsWith("export") ? "OUT " : "";
268
+ return `${prefix}VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
269
+ },
270
+ confidence: 0.7
271
+ }
272
+ ];
273
+ function fingerprintLine(line) {
274
+ const trimmed = line.trim();
275
+ if (trimmed === "" || trimmed === "{" || trimmed === "}" || trimmed === "});" || trimmed === ");") {
276
+ return { type: "fingerprint", ir: "", confidence: 1 };
277
+ }
278
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
279
+ return { type: "fingerprint", ir: "", confidence: 1 };
280
+ }
281
+ for (const pattern of PATTERNS) {
282
+ const match = trimmed.match(pattern.match);
283
+ if (match) {
284
+ const ir = pattern.transform(match);
285
+ if (pattern.confidence > 0.9) {
286
+ return { type: "fingerprint", ir, confidence: pattern.confidence };
287
+ }
288
+ return {
289
+ type: "fingerprint+hint",
290
+ ir,
291
+ hint: trimmed,
292
+ confidence: pattern.confidence
293
+ };
294
+ }
295
+ }
296
+ return { type: "raw", ir: trimmed, confidence: 0.3 };
297
+ }
298
+ function fingerprintFile(code, confidenceThreshold = 0.6) {
299
+ const lines = code.split("\n");
300
+ const irLines = [];
301
+ for (const line of lines) {
302
+ const indent = line.search(/\S/);
303
+ const indentStr = indent > 0 ? " ".repeat(Math.floor(indent / 2)) : "";
304
+ const result = fingerprintLine(line);
305
+ if (result.ir === "") continue;
306
+ if (result.confidence >= confidenceThreshold) {
307
+ irLines.push(`${indentStr}${result.ir}`);
308
+ } else {
309
+ irLines.push(`${indentStr}${result.ir}`);
310
+ }
311
+ }
312
+ return irLines.join("\n");
313
+ }
314
+
315
+ // src/ir/health.ts
316
+ var CHURN_THRESHOLD = 10;
317
+ var FIX_RATIO_THRESHOLD = 0.5;
318
+ function buildHealthTag(health) {
319
+ const parts = [];
320
+ if (health.churn > CHURN_THRESHOLD) parts.push(`HOT:${health.churn}/30`);
321
+ if (health.fixRatio > FIX_RATIO_THRESHOLD) parts.push(`FIX:${Math.round(health.fixRatio * 100)}%`);
322
+ if (health.coverageTrend === "down") parts.push("COV:\u2193");
323
+ if (health.consistency === "low") parts.push("INCON");
324
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
325
+ }
326
+ function annotateIR(ir, health) {
327
+ const tag = buildHealthTag(health);
328
+ if (!tag) return ir;
329
+ const lines = ir.split("\n");
330
+ lines[0] = `${lines[0]} ${tag}`;
331
+ return lines.join("\n");
332
+ }
333
+ function computeHealthFromTrends(file, trends) {
334
+ const hotspot = trends.hotspots.find((h) => h.file === file);
335
+ const decay = trends.decaySignals.find((d) => d.file === file);
336
+ const inconsistency = trends.inconsistencies.find((i) => i.file === file);
337
+ return {
338
+ churn: hotspot?.changesInLast30Commits ?? 0,
339
+ fixRatio: hotspot?.bugFixRatio ?? 0,
340
+ coverageTrend: decay?.trend === "declining" ? "down" : decay?.trend === "improving" ? "up" : "stable",
341
+ staleness: "",
342
+ authorCount: hotspot?.authorCount ?? 0,
343
+ consistency: inconsistency ? "low" : "high"
344
+ };
345
+ }
346
+
347
+ // src/ir/layers.ts
348
+ function generateL0(code, filePath) {
349
+ const structure = extractStructure(code);
350
+ const topLevel = structure.filter(
351
+ (s) => s.indent === 0 && ["function-start", "type-start", "export"].includes(s.type)
352
+ );
353
+ const declarations = topLevel.map((s) => {
354
+ const name = s.raw.match(
355
+ /(?:function|class|interface|const|let|var|export\s+(?:default\s+)?(?:function|class|async\s+function))\s+(\w+)/
356
+ )?.[1] ?? "unknown";
357
+ return ` ${s.type === "type-start" ? "CLASS" : "FN"}:${name} L${s.line}`;
358
+ });
359
+ return `${filePath}
360
+ ${declarations.join("\n")}`;
361
+ }
362
+ function generateL1(code, health) {
363
+ const ir = fingerprintFile(code, 0.6);
364
+ if (health) {
365
+ return annotateIR(ir, health);
366
+ }
367
+ return ir;
368
+ }
369
+ function generateL2(delta, health) {
370
+ const parts = [`FILE: ${delta.file}`];
371
+ for (const hunk of delta.hunks) {
372
+ if (hunk.functionScope) parts.push(`SCOPE: ${hunk.functionScope}`);
373
+ parts.push(`CHANGED: ${hunk.changed.join("\n ")}`);
374
+ if (hunk.surroundingIR) parts.push(`CONTEXT: ${hunk.surroundingIR}`);
375
+ if (hunk.blame) {
376
+ parts.push(`BLAME: ${hunk.blame.author}, ${hunk.blame.date}, commit:"${hunk.blame.commitMessage}"`);
377
+ }
378
+ }
379
+ const ir = parts.join("\n");
380
+ if (health) return annotateIR(ir, health);
381
+ return ir;
382
+ }
383
+ function generateL3(code, startLine, endLine) {
384
+ const lines = code.split("\n");
385
+ return lines.slice(startLine - 1, endLine).join("\n");
386
+ }
387
+ function generateLayer(layer, options) {
388
+ switch (layer) {
389
+ case "L0":
390
+ return generateL0(options.code, options.filePath);
391
+ case "L1":
392
+ return generateL1(options.code, options.health);
393
+ case "L2":
394
+ if (!options.delta) return generateL1(options.code, options.health);
395
+ return generateL2(options.delta, options.health);
396
+ case "L3":
397
+ if (options.lineRange) {
398
+ return generateL3(options.code, options.lineRange.start, options.lineRange.end);
399
+ }
400
+ return options.code;
401
+ }
402
+ }
403
+
404
+ // src/trends/git-log-parser.ts
405
+ import { execSync } from "child_process";
406
+ var BUG_FIX_PATTERNS = [
407
+ /\bfix\b/i,
408
+ /\bbugfix\b/i,
409
+ /\bhotfix\b/i,
410
+ /\bpatch\b/i,
411
+ /\bresolve\b/i,
412
+ /\bbug\b/i
413
+ ];
414
+ function isBugFixCommit(message) {
415
+ return BUG_FIX_PATTERNS.some((p) => p.test(message));
416
+ }
417
+ function parseGitLogOutput(output) {
418
+ const entries = [];
419
+ const lines = output.split("\n");
420
+ let i = 0;
421
+ while (i < lines.length) {
422
+ const line = lines[i].trim();
423
+ if (!line || !line.includes("|")) {
424
+ i++;
425
+ continue;
426
+ }
427
+ const [hash, author, date, ...messageParts] = line.split("|");
428
+ const message = messageParts.join("|");
429
+ const files = [];
430
+ i++;
431
+ while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
432
+ const fileLine = lines[i].trim();
433
+ if (fileLine) files.push(fileLine);
434
+ i++;
435
+ }
436
+ entries.push({ hash, author, date, message, files });
437
+ }
438
+ return entries;
439
+ }
440
+ function getGitLog(repoPath, count = 100) {
441
+ try {
442
+ const output = execSync(
443
+ `git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
444
+ { cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
445
+ );
446
+ return parseGitLogOutput(output);
447
+ } catch {
448
+ return [];
449
+ }
450
+ }
451
+
452
+ // src/trends/hotspot.ts
453
+ function detectHotspots(entries, options) {
454
+ const fileStats = /* @__PURE__ */ new Map();
455
+ for (const entry of entries) {
456
+ const isFix = isBugFixCommit(entry.message);
457
+ for (const file of entry.files) {
458
+ const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
459
+ stats.changes++;
460
+ if (isFix) stats.fixes++;
461
+ stats.authors.add(entry.author);
462
+ fileStats.set(file, stats);
463
+ }
464
+ }
465
+ const hotspots = [];
466
+ for (const [file, stats] of fileStats) {
467
+ const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
468
+ if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
469
+ hotspots.push({
470
+ file,
471
+ changesInLast30Commits: stats.changes,
472
+ bugFixRatio: fixRatio,
473
+ authorCount: stats.authors.size
474
+ });
475
+ }
476
+ }
477
+ return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
478
+ }
479
+
480
+ // src/trends/decay.ts
481
+ function detectDecay(entries) {
482
+ const fileChanges = /* @__PURE__ */ new Map();
483
+ for (const entry of entries) {
484
+ for (const file of entry.files) {
485
+ const changes = fileChanges.get(file) ?? [];
486
+ changes.push({ date: entry.date });
487
+ fileChanges.set(file, changes);
488
+ }
489
+ }
490
+ const signals = [];
491
+ for (const [file, changes] of fileChanges) {
492
+ if (changes.length < 4) continue;
493
+ const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
494
+ const firstDate = new Date(sorted[0].date).getTime();
495
+ const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
496
+ const midDate = firstDate + (lastDate - firstDate) / 2;
497
+ const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
498
+ const secondHalfCount = sorted.length - firstHalfCount;
499
+ if (secondHalfCount > firstHalfCount) {
500
+ signals.push({
501
+ file,
502
+ metric: "churn",
503
+ trend: "declining",
504
+ dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
505
+ });
506
+ }
507
+ }
508
+ return signals;
509
+ }
510
+
511
+ // src/trends/inconsistency.ts
512
+ function detectInconsistencies(entries, minAuthors = 3) {
513
+ const fileAuthors = /* @__PURE__ */ new Map();
514
+ for (const entry of entries) {
515
+ for (const file of entry.files) {
516
+ const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
517
+ const commits = authors.get(entry.author) ?? [];
518
+ commits.push(entry.message);
519
+ authors.set(entry.author, commits);
520
+ fileAuthors.set(file, authors);
521
+ }
522
+ }
523
+ const inconsistencies = [];
524
+ for (const [file, authors] of fileAuthors) {
525
+ if (authors.size >= minAuthors) {
526
+ const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
527
+ author,
528
+ style: categorizeStyle(commits)
529
+ }));
530
+ inconsistencies.push({ file, patterns });
531
+ }
532
+ }
533
+ return inconsistencies;
534
+ }
535
+ function categorizeStyle(commits) {
536
+ const types = commits.map((m) => {
537
+ if (m.match(/^fix/i)) return "fix";
538
+ if (m.match(/^feat/i)) return "feature";
539
+ if (m.match(/^refactor/i)) return "refactor";
540
+ return "other";
541
+ });
542
+ const primary = mode(types);
543
+ return `primarily ${primary} (${commits.length} commits)`;
544
+ }
545
+ function mode(arr) {
546
+ const counts = /* @__PURE__ */ new Map();
547
+ for (const item of arr) {
548
+ counts.set(item, (counts.get(item) ?? 0) + 1);
549
+ }
550
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
551
+ }
552
+
553
+ // src/router/router.ts
554
+ import picomatch2 from "picomatch";
555
+ var DEFAULT_ROUTES = [
556
+ { pattern: "**/auth/**", agents: ["reviewer"], irLayer: "L1" },
557
+ { pattern: "**/*.test.*", agents: ["reviewer"], irLayer: "L0" },
558
+ { pattern: "**/*.spec.*", agents: ["reviewer"], irLayer: "L0" },
559
+ { pattern: "**/*.md", agents: ["fixer"], irLayer: "L0" },
560
+ { pattern: "**/*.json", agents: ["fixer"], irLayer: "L0" },
561
+ { pattern: "**/*.yaml", agents: ["fixer"], irLayer: "L0" },
562
+ { pattern: "**/*.yml", agents: ["fixer"], irLayer: "L0" }
563
+ ];
564
+ var FALLBACK = {
565
+ agents: ["fixer"],
566
+ irLayer: "L1",
567
+ deterministic: true
568
+ };
569
+ function route(finding, rules) {
570
+ for (const rule of rules) {
571
+ if (!picomatch2.isMatch(finding.file, rule.pattern)) continue;
572
+ if (rule.contentSignal) {
573
+ const content = finding.message + (finding.file ?? "");
574
+ if (!rule.contentSignal.test(content)) continue;
575
+ }
576
+ return {
577
+ agents: rule.agents,
578
+ irLayer: rule.irLayer,
579
+ deterministic: true
580
+ };
581
+ }
582
+ return FALLBACK;
583
+ }
584
+
585
+ // src/cli/adapter.ts
586
+ var CLIAdapter = class {
587
+ onFileChange(_event) {
588
+ }
589
+ onCommand(_cmd, _args) {
590
+ }
591
+ onApproval(_proposalId, _approved) {
592
+ }
593
+ notify(message) {
594
+ switch (message.type) {
595
+ case "finding":
596
+ this.printFinding(message.data);
597
+ break;
598
+ case "trend-report":
599
+ this.printTrendReport(message.data);
600
+ break;
601
+ case "proposal":
602
+ console.log(`
603
+ Proposal [${message.data.id}]: ${message.data.description}`);
604
+ for (const f of message.data.findings) this.printFinding(f);
605
+ break;
606
+ case "edit":
607
+ console.log(` [EDIT] ${message.data.file}`);
608
+ break;
609
+ case "question":
610
+ console.log(` [?] ${message.data.text}`);
611
+ if (message.data.options) {
612
+ message.data.options.forEach((o, i) => console.log(` ${i + 1}. ${o}`));
613
+ }
614
+ break;
615
+ }
616
+ }
617
+ printFinding(finding) {
618
+ const icon = finding.severity === "critical" ? "!!" : finding.severity === "warning" ? " !" : " ";
619
+ const loc = finding.line ? `:${finding.line}` : "";
620
+ console.log(` ${icon} [${finding.severity.toUpperCase()}] ${finding.file}${loc}`);
621
+ console.log(` ${finding.message}`);
622
+ }
623
+ printTrendReport(trends) {
624
+ if (trends.hotspots.length === 0 && trends.decaySignals.length === 0 && trends.inconsistencies.length === 0) {
625
+ console.log(" Codebase is healthy. No trends to report.");
626
+ return;
627
+ }
628
+ if (trends.hotspots.length > 0) {
629
+ console.log("\n Hotspots:");
630
+ for (const h of trends.hotspots) {
631
+ console.log(` ${h.file} \u2014 ${h.changesInLast30Commits} changes, ${Math.round(h.bugFixRatio * 100)}% fixes, ${h.authorCount} authors`);
632
+ }
633
+ }
634
+ if (trends.decaySignals.length > 0) {
635
+ console.log("\n Decay Signals:");
636
+ for (const d of trends.decaySignals) {
637
+ console.log(` ${d.file} \u2014 ${d.metric} is ${d.trend}`);
638
+ }
639
+ }
640
+ if (trends.inconsistencies.length > 0) {
641
+ console.log("\n Inconsistencies:");
642
+ for (const ic of trends.inconsistencies) {
643
+ console.log(` ${ic.file} \u2014 ${ic.patterns.length} different patterns`);
644
+ }
645
+ }
646
+ }
647
+ };
648
+
649
+ // src/benchmark/tokenizer.ts
650
+ function estimateTokens(text) {
651
+ if (!text) return 0;
652
+ const tokens = text.split(/[\s]+|(?<=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])|(?=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])/).filter(Boolean);
653
+ return tokens.length;
654
+ }
655
+
656
+ // src/benchmark/runner.ts
657
+ function benchmarkFile(code, filePath) {
658
+ const rawTokens = estimateTokens(code);
659
+ const irL0 = generateLayer("L0", { code, filePath, health: null });
660
+ const irL1 = generateLayer("L1", { code, filePath, health: null });
661
+ const irL0Tokens = estimateTokens(irL0);
662
+ const irL1Tokens = estimateTokens(irL1);
663
+ const lines = code.split("\n");
664
+ let totalConf = 0;
665
+ let count = 0;
666
+ for (const line of lines) {
667
+ const result = fingerprintLine(line);
668
+ if (result.ir !== "") {
669
+ totalConf += result.confidence;
670
+ count++;
671
+ }
672
+ }
673
+ const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
674
+ const avgConfidence = count > 0 ? totalConf / count : 0;
675
+ return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, avgConfidence };
676
+ }
677
+ function summarize(results) {
678
+ const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
679
+ const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
680
+ const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
681
+ const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
682
+ const avgConfidence = results.length > 0 ? results.reduce((s, r) => s + r.avgConfidence, 0) / results.length : 0;
683
+ return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, avgConfidence };
684
+ }
685
+
686
+ // src/cli/commands.ts
687
+ function collectFiles(dir, extensions) {
688
+ const files = [];
689
+ try {
690
+ const entries = readdirSync(dir, { withFileTypes: true });
691
+ for (const entry of entries) {
692
+ const fullPath = join2(dir, entry.name);
693
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
694
+ if (entry.isDirectory()) {
695
+ files.push(...collectFiles(fullPath, extensions));
696
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
697
+ files.push(fullPath);
698
+ }
699
+ }
700
+ } catch {
701
+ }
702
+ return files;
703
+ }
704
+ function runScan(projectPath) {
705
+ const adapter = new CLIAdapter();
706
+ const config = loadConfig(projectPath);
707
+ console.log("composto v0.1.0 \u2014 scanning...\n");
708
+ const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
709
+ console.log(` Found ${files.length} files
710
+ `);
711
+ const allFindings = [];
712
+ for (const file of files) {
713
+ const code = readFileSync2(file, "utf-8");
714
+ const relPath = relative(projectPath, file);
715
+ const findings = runDetector(code, relPath, config.watchers);
716
+ allFindings.push(...findings);
717
+ }
718
+ if (allFindings.length > 0) {
719
+ console.log(` Findings (${allFindings.length}):
720
+ `);
721
+ for (const finding of allFindings) {
722
+ adapter.notify({ type: "finding", data: finding });
723
+ const decision = route(finding, DEFAULT_ROUTES);
724
+ console.log(` -> Route: ${decision.agents.join(",")} @ ${decision.irLayer}
725
+ `);
726
+ }
727
+ } else {
728
+ console.log(" No issues found.\n");
729
+ }
730
+ }
731
+ function runTrends(projectPath) {
732
+ const adapter = new CLIAdapter();
733
+ const config = loadConfig(projectPath);
734
+ console.log("composto v0.1.0 \u2014 trend analysis...\n");
735
+ const entries = getGitLog(projectPath, 100);
736
+ if (entries.length === 0) {
737
+ console.log(" No git history found.\n");
738
+ return;
739
+ }
740
+ console.log(` Analyzed ${entries.length} commits
741
+ `);
742
+ const trends = {
743
+ hotspots: detectHotspots(entries, {
744
+ threshold: config.trends.hotspotThreshold,
745
+ fixRatioThreshold: config.trends.bugFixRatioThreshold
746
+ }),
747
+ decaySignals: detectDecay(entries),
748
+ inconsistencies: detectInconsistencies(entries)
749
+ };
750
+ adapter.notify({ type: "trend-report", data: trends });
751
+ }
752
+ function runIR(projectPath, filePath, layer) {
753
+ const config = loadConfig(projectPath);
754
+ const code = readFileSync2(filePath, "utf-8");
755
+ const relPath = relative(projectPath, filePath);
756
+ const entries = getGitLog(projectPath, 100);
757
+ const trends = {
758
+ hotspots: detectHotspots(entries, {
759
+ threshold: config.trends.hotspotThreshold,
760
+ fixRatioThreshold: config.trends.bugFixRatioThreshold
761
+ }),
762
+ decaySignals: detectDecay(entries),
763
+ inconsistencies: detectInconsistencies(entries)
764
+ };
765
+ const health = computeHealthFromTrends(relPath, trends);
766
+ const irLayer = layer || "L1";
767
+ const result = generateLayer(irLayer, {
768
+ code,
769
+ filePath: relPath,
770
+ health: health.churn > 0 ? health : null
771
+ });
772
+ console.log(result);
773
+ }
774
+ function runBenchmark(projectPath) {
775
+ console.log("composto v0.1.0 \u2014 benchmark\n");
776
+ const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
777
+ console.log(` ${files.length} files
778
+ `);
779
+ const results = files.map((file) => {
780
+ const code = readFileSync2(file, "utf-8");
781
+ const relPath = relative(projectPath, file);
782
+ return benchmarkFile(code, relPath);
783
+ });
784
+ results.sort((a, b) => b.savedPercent - a.savedPercent);
785
+ const header = " File Raw L0 L1 Saved Conf";
786
+ const divider = " " + "\u2500".repeat(header.length - 2);
787
+ console.log(header);
788
+ console.log(divider);
789
+ for (const r of results) {
790
+ const file = r.file.length > 38 ? "\u2026" + r.file.slice(-37) : r.file.padEnd(38);
791
+ const raw = String(r.rawTokens).padStart(5);
792
+ const l0 = String(r.irL0Tokens).padStart(7);
793
+ const l1 = String(r.irL1Tokens).padStart(7);
794
+ const saved = (r.savedPercent.toFixed(1) + "%").padStart(7);
795
+ const conf = r.avgConfidence.toFixed(2).padStart(6);
796
+ console.log(` ${file} ${raw} ${l0} ${l1} ${saved} ${conf}`);
797
+ }
798
+ const summary = summarize(results);
799
+ console.log(divider);
800
+ const totalLabel = "TOTAL".padEnd(38);
801
+ const totalRaw = String(summary.totalRaw).padStart(5);
802
+ const totalL0 = String(summary.totalIRL0).padStart(7);
803
+ const totalL1 = String(summary.totalIRL1).padStart(7);
804
+ const totalSaved = (summary.totalSavedPercent.toFixed(1) + "%").padStart(7);
805
+ const totalConf = summary.avgConfidence.toFixed(2).padStart(6);
806
+ console.log(` ${totalLabel} ${totalRaw} ${totalL0} ${totalL1} ${totalSaved} ${totalConf}`);
807
+ const l0Percent = summary.totalRaw > 0 ? (summary.totalRaw - summary.totalIRL0) / summary.totalRaw * 100 : 0;
808
+ console.log(`
809
+ L0 (structure map): ${summary.totalRaw} \u2192 ${summary.totalIRL0} tokens (${l0Percent.toFixed(1)}% reduction)`);
810
+ console.log(` L1 (full IR): ${summary.totalRaw} \u2192 ${summary.totalIRL1} tokens (${summary.totalSavedPercent.toFixed(1)}% reduction)`);
811
+ console.log(` Files analyzed: ${summary.fileCount}`);
812
+ console.log(` Avg confidence: ${summary.avgConfidence.toFixed(2)}`);
813
+ }
814
+
815
+ // src/index.ts
816
+ import { resolve } from "path";
817
+ var args = process.argv.slice(2);
818
+ var command = args[0];
819
+ switch (command) {
820
+ case "scan": {
821
+ const projectPath = resolve(args[1] ?? ".");
822
+ runScan(projectPath);
823
+ break;
824
+ }
825
+ case "trends": {
826
+ const projectPath = resolve(args[1] ?? ".");
827
+ runTrends(projectPath);
828
+ break;
829
+ }
830
+ case "ir": {
831
+ const filePath = args[1];
832
+ const layer = args[2] ?? "L1";
833
+ if (!filePath) {
834
+ console.error("Usage: composto ir <file> [L0|L1|L2|L3]");
835
+ process.exit(1);
836
+ }
837
+ runIR(resolve("."), resolve(filePath), layer);
838
+ break;
839
+ }
840
+ case "benchmark": {
841
+ const projectPath = resolve(args[1] ?? ".");
842
+ runBenchmark(projectPath);
843
+ break;
844
+ }
845
+ case "version":
846
+ console.log("composto v0.1.0");
847
+ break;
848
+ default:
849
+ console.log("composto v0.1.0 \u2014 less tokens, more insight\n");
850
+ console.log("Commands:");
851
+ console.log(" scan [path] Scan codebase for issues");
852
+ console.log(" trends [path] Analyze codebase health trends");
853
+ console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
854
+ console.log(" benchmark [path] Benchmark IR token savings");
855
+ console.log(" version Show version");
856
+ break;
857
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "composto-ai",
3
+ "version": "0.1.0",
4
+ "description": "Proactive AI team companion — less tokens, more insight",
5
+ "type": "module",
6
+ "bin": {
7
+ "composto": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/mertcanaltin/composto"
16
+ },
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "dev": "tsx src/index.ts"
22
+ },
23
+ "keywords": ["ai", "coding", "companion", "proactive"],
24
+ "author": "Mert Can Altin",
25
+ "license": "MIT",
26
+ "packageManager": "pnpm@10.30.1",
27
+ "devDependencies": {
28
+ "@types/node": "^25.5.2",
29
+ "@types/picomatch": "^4.0.3",
30
+ "tsup": "^8.5.1",
31
+ "tsx": "^4.21.0",
32
+ "typescript": "^6.0.2",
33
+ "vitest": "^4.1.4"
34
+ },
35
+ "dependencies": {
36
+ "@anthropic-ai/sdk": "^0.87.0",
37
+ "picomatch": "^4.0.4",
38
+ "yaml": "^2.8.3"
39
+ }
40
+ }