composto-ai 0.6.0 → 0.7.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 CHANGED
@@ -1,24 +1,23 @@
1
1
  # Composto
2
2
 
3
- **Send meaning to your LLM, not code. 89% fewer tokens, same understanding.**
3
+ **Causal memory layer for coding agents. Catches the bug your agent is about to reintroduce.**
4
4
 
5
- Composto parses your code into an AST, classifies every node by importance, and drops the noise. Your LLM gets the signal function signatures, control flow, dependencies without the braces, semicolons, and string literals it already knows.
5
+ Composto is a repo-local graph of your git history that your AI coding agent consults before every edit. When a file was reverted recently, has a fix cluster in its history, or was last touched by someone who left the team, Composto surfaces that signal as in-context guidance before the agent writes the code. Hook-enforced on Claude Code, Cursor, and Gemini CLI. Local-first, MIT.
6
6
 
7
7
  ```
8
- Raw source: 3,782 tokens → Composto IR: 663 tokens (82.5% savings)
9
-
10
- USE:[../types.js, ./structure.js, ./fingerprint.js, ./health.js]
11
- OUT FN:generateL0(code: string, filePath: string)
12
- RET `${filePath}\n${declarations.join("\n")}`
13
- OUT ASYNC FN:generateL1(code: string, filePath: string, health: HealthAnnotation...)
14
- IF:health RET annotateIR(ir, health)
15
- RET ir
16
- OUT FN:generateLayer(layer: IRLayer, options: {...})
17
- SWITCH:layer
18
- CASE:"L0" → RET generateL0(...)
19
- CASE:"L1" RET generateL1(...)
20
- CASE:"L2" RET generateL2(...)
21
- CASE:"L3" → RET options.code
8
+ $ composto impact src/memory/signals/hotspot.ts
9
+
10
+ verdict: medium
11
+ score: 0.52
12
+ confidence: 0.50
13
+ signals:
14
+ revert_match ■■■■■■■■■■ strength=1.00 precision=1.00
15
+ hotspot ■ strength=0.10 precision=0.54
16
+ fix_ratio ■ strength=0.07 precision=0.54
17
+ author_churn · strength=0.00 precision=0.16
18
+
19
+ # This file was touched by a Revert commit in history.
20
+ # blastradius remembers. Your LLM couldn't.
22
21
  ```
23
22
 
24
23
  ---
@@ -29,21 +28,35 @@ OUT FN:generateLayer(layer: IRLayer, options: {...})
29
28
  # Install
30
29
  npm install -g composto-ai
31
30
 
32
- # See how much you save
33
- composto benchmark .
31
+ # One-command setup, wires MCP + PreToolUse hook into your AI client
32
+ cd your-project
33
+ composto init --client=claude-code # or cursor, or gemini-cli
34
34
 
35
- # Generate IR for a file
36
- composto ir src/app.ts
35
+ # Restart your AI client. Hook fires on every Edit / Write / MultiEdit.
36
+ # On medium|high|unknown verdicts, the agent gets a composto_blastradius
37
+ # block in context before it acts. Passthrough on low.
37
38
 
38
- # Smart context within a token budget
39
- composto context src/ --budget 2000
39
+ # Observe
40
+ composto stats # hook invocations, verdict distribution, latency
41
+ composto stats --disable # local-only opt-out (writes .composto/telemetry-disabled)
40
42
 
41
- # Historical blast radius for a file (beta, feature-flagged)
42
- COMPOSTO_BLASTRADIUS=1 composto index
43
+ # Query on demand
43
44
  composto impact src/auth/login.ts
44
- composto index --status
45
+ composto index --status # diagnostics: schema, freshness, calibration
45
46
  ```
46
47
 
48
+ ### Also in the box: AST compression tools
49
+
50
+ Composto also ships a tree-sitter based AST compressor (about 89% token savings) and a smart context packer for bug-fix tasks. These are separate from the causal layer but live in the same binary.
51
+
52
+ ```bash
53
+ composto ir src/app.ts # compress a file to IR (L0/L1/L2/L3)
54
+ composto context src/ --budget 2000 # smart context within a token budget
55
+ composto benchmark . # see compression stats
56
+ ```
57
+
58
+ See the [IR Layers](#ir-layers), [Health-Aware IR](#health-aware-ir), and [Context Budget](#context-budget) sections below for details.
59
+
47
60
  ### MCP plugin (Claude Code, Cursor, Claude Desktop)
48
61
 
49
62
  The MCP server is bundled inside `composto-ai`. Install the package globally first, then register the server with your client:
@@ -175,7 +188,7 @@ composto index --status # diagnostics: schema, freshness, calibration
175
188
 
176
189
  ---
177
190
 
178
- ## BlastRadius (beta)
191
+ ## BlastRadius
179
192
 
180
193
  Beyond compression, Composto indexes your repo's git history into a local SQLite graph and exposes it as a queryable risk surface. Before your agent edits a file, it can ask: *"has this region been reverted? who fixed the last similar bug? is the last author still around?"* — signals no LLM can infer from current code alone.
181
194
 
package/dist/index.js CHANGED
@@ -3062,30 +3062,45 @@ function writeCursorHooks(projectPath, result) {
3062
3062
  if (existed) result.merged.push(relPath);
3063
3063
  else result.written.push(relPath);
3064
3064
  }
3065
- function initCursor(projectPath, result) {
3066
- writeJsonMerged(
3067
- join7(projectPath, ".cursor", "mcp.json"),
3068
- { mcpServers: { composto: { command: "composto-mcp" } } },
3069
- result,
3070
- ".cursor/mcp.json"
3071
- );
3072
- writeFileSkipIfExists(
3073
- join7(projectPath, ".cursor", "rules", "composto.mdc"),
3074
- CURSOR_RULES_MDC,
3075
- result,
3076
- ".cursor/rules/composto.mdc"
3077
- );
3065
+ function initCursor(projectPath, result, options) {
3066
+ if (options.withMcp) {
3067
+ writeJsonMerged(
3068
+ join7(projectPath, ".cursor", "mcp.json"),
3069
+ {
3070
+ mcpServers: {
3071
+ composto: {
3072
+ command: "composto-mcp",
3073
+ env: { COMPOSTO_BLASTRADIUS: "1" }
3074
+ }
3075
+ }
3076
+ },
3077
+ result,
3078
+ ".cursor/mcp.json"
3079
+ );
3080
+ }
3081
+ if (options.withRules) {
3082
+ writeFileSkipIfExists(
3083
+ join7(projectPath, ".cursor", "rules", "composto.mdc"),
3084
+ CURSOR_RULES_MDC,
3085
+ result,
3086
+ ".cursor/rules/composto.mdc"
3087
+ );
3088
+ }
3078
3089
  writeCursorHooks(projectPath, result);
3079
3090
  }
3080
- function initClaudeCode(projectPath, result) {
3091
+ function initClaudeCode(projectPath, result, options) {
3081
3092
  const settingsPath = join7(projectPath, ".claude", "settings.json");
3082
3093
  const relPath = ".claude/settings.json";
3083
3094
  const existed = existsSync4(settingsPath);
3084
3095
  const existing = readJsonIfExists(settingsPath);
3085
- const mcpServers = {
3086
- ...existing.mcpServers ?? {},
3087
- composto: { command: "composto-mcp" }
3088
- };
3096
+ const baseExistingMcp = existing.mcpServers ?? {};
3097
+ const mcpServers = options.withMcp ? {
3098
+ ...baseExistingMcp,
3099
+ composto: {
3100
+ command: "composto-mcp",
3101
+ env: { COMPOSTO_BLASTRADIUS: "1" }
3102
+ }
3103
+ } : baseExistingMcp;
3089
3104
  const compostoHookEntry = {
3090
3105
  matcher: "Edit|Write|MultiEdit",
3091
3106
  hooks: [
@@ -3100,9 +3115,11 @@ function initClaudeCode(projectPath, result) {
3100
3115
  );
3101
3116
  const merged = {
3102
3117
  ...existing,
3103
- mcpServers,
3104
3118
  hooks: { ...existingHooks, PreToolUse: preToolUse }
3105
3119
  };
3120
+ if (Object.keys(mcpServers).length > 0) {
3121
+ merged.mcpServers = mcpServers;
3122
+ }
3106
3123
  ensureDir(settingsPath);
3107
3124
  writeFileSync2(settingsPath, JSON.stringify(merged, null, 2) + "\n");
3108
3125
  if (existed) result.merged.push(relPath);
@@ -3114,10 +3131,14 @@ function initGeminiCli(_projectPath, result, options) {
3114
3131
  try {
3115
3132
  const existed = existsSync4(settingsPath);
3116
3133
  const existing = readJsonIfExists(settingsPath);
3117
- const mcpServers = {
3118
- ...existing.mcpServers ?? {},
3119
- composto: { command: "composto-mcp" }
3120
- };
3134
+ const baseExistingMcp = existing.mcpServers ?? {};
3135
+ const mcpServers = options.withMcp ? {
3136
+ ...baseExistingMcp,
3137
+ composto: {
3138
+ command: "composto-mcp",
3139
+ env: { COMPOSTO_BLASTRADIUS: "1" }
3140
+ }
3141
+ } : baseExistingMcp;
3121
3142
  const compostoHookEntry = {
3122
3143
  matcher: "edit_file|write_file|replace",
3123
3144
  hooks: [
@@ -3132,9 +3153,11 @@ function initGeminiCli(_projectPath, result, options) {
3132
3153
  );
3133
3154
  const merged = {
3134
3155
  ...existing,
3135
- mcpServers,
3136
3156
  hooks: { ...existingHooks, BeforeTool: beforeTool }
3137
3157
  };
3158
+ if (Object.keys(mcpServers).length > 0) {
3159
+ merged.mcpServers = mcpServers;
3160
+ }
3138
3161
  ensureDir(settingsPath);
3139
3162
  writeFileSync2(settingsPath, JSON.stringify(merged, null, 2) + "\n");
3140
3163
  if (existed) result.merged.push(relPath);
@@ -3147,9 +3170,9 @@ function initGeminiCli(_projectPath, result, options) {
3147
3170
  function runInit(projectPath, options) {
3148
3171
  const client = options.client ?? "cursor";
3149
3172
  const result = { client, written: [], skipped: [], merged: [] };
3150
- if (client === "claude-code") initClaudeCode(projectPath, result);
3173
+ if (client === "claude-code") initClaudeCode(projectPath, result, options);
3151
3174
  else if (client === "gemini-cli") initGeminiCli(projectPath, result, options);
3152
- else initCursor(projectPath, result);
3175
+ else initCursor(projectPath, result, options);
3153
3176
  return result;
3154
3177
  }
3155
3178
 
@@ -3662,7 +3685,13 @@ switch (command) {
3662
3685
  console.error(`Unknown --client=${clientArg}. Valid: ${valid.join(", ")}`);
3663
3686
  process.exit(1);
3664
3687
  }
3665
- const result = runInit(resolve2("."), { client: clientArg });
3688
+ const withRules = args.includes("--with-rules");
3689
+ const withMcp = args.includes("--with-mcp");
3690
+ const result = runInit(resolve2("."), {
3691
+ client: clientArg,
3692
+ withRules,
3693
+ withMcp
3694
+ });
3666
3695
  console.log(`composto init \u2014 configured for ${result.client}
3667
3696
  `);
3668
3697
  for (const f of result.written) console.log(` wrote ${f}`);
@@ -3740,8 +3769,10 @@ switch (command) {
3740
3769
  console.log(" impact <file> Show historical blast radius for a file");
3741
3770
  console.log(" index [--since=YYYY-MM-DD] Build or refresh the memory index (--since bounds work for huge repos)");
3742
3771
  console.log(" index --status Show memory index diagnostics");
3743
- console.log(" init [--client=<name>] Configure Composto MCP + hooks for an AI client");
3744
- console.log(" (clients: cursor, claude-code, gemini-cli)");
3772
+ console.log(" init [--client=<name>] [--with-mcp] [--with-rules]");
3773
+ console.log(" Lean Hook init (clients: cursor, claude-code, gemini-cli)");
3774
+ console.log(" --with-mcp register the composto MCP server (5 tools)");
3775
+ console.log(" --with-rules write .cursor/rules/composto.mdc (cursor only)");
3745
3776
  console.log(" hook <platform> <event> Run BlastRadius hook (reads tool JSON from stdin)");
3746
3777
  console.log(" stats [--json] [--disable] Show hook telemetry (last 7d); --disable opts out");
3747
3778
  console.log(" version Show version");
@@ -2387,7 +2387,7 @@ var server = new McpServer({
2387
2387
  });
2388
2388
  server.tool(
2389
2389
  "composto_ir",
2390
- "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.",
2390
+ "Compressed AST-based IR for a file. ~89% fewer tokens than raw read.",
2391
2391
  {
2392
2392
  file: z.string().describe("Path to the source file"),
2393
2393
  layer: z.enum(["L0", "L1", "L2", "L3"]).default("L1").describe("L0=structure only, L1=full IR (default), L2=delta context, L3=raw source")
@@ -2423,7 +2423,7 @@ ${result}` }]
2423
2423
  );
2424
2424
  server.tool(
2425
2425
  "composto_benchmark",
2426
- "Benchmark how much Composto saves across a directory. Shows per-file token savings comparing raw code vs compressed IR.",
2426
+ "Per-file token-savings benchmark across a directory.",
2427
2427
  {
2428
2428
  path: z.string().default(".").describe("Directory to benchmark")
2429
2429
  },
@@ -2455,7 +2455,7 @@ server.tool(
2455
2455
  );
2456
2456
  server.tool(
2457
2457
  "composto_context",
2458
- "Pack maximum code context into a token budget. When target symbol is provided, its file is included as raw code (L3) while surrounding files get compressed IR. Perfect for 'fix this bug in X' or 'why does X return wrong value' \u2014 LLM sees exact code of target plus compressed context. Without target, hotspot files get L1, rest get L0.",
2458
+ "Pack code into a token budget; target raw, neighbors as IR.",
2459
2459
  {
2460
2460
  path: z.string().default(".").describe("Directory to pack"),
2461
2461
  budget: z.number().default(4e3).describe("Maximum tokens to use"),
@@ -2518,7 +2518,7 @@ Files: ${parts.join(", ")}`);
2518
2518
  );
2519
2519
  server.tool(
2520
2520
  "composto_scan",
2521
- "Scan codebase for security issues (hardcoded secrets, API keys) and debug artifacts (console.log). Zero token cost \u2014 pure local analysis.",
2521
+ "Scan for hardcoded secrets and debug artifacts. Local-only.",
2522
2522
  {
2523
2523
  path: z.string().default(".").describe("Directory to scan")
2524
2524
  },
@@ -2551,7 +2551,7 @@ server.tool(
2551
2551
  );
2552
2552
  server.tool(
2553
2553
  "composto_blastradius",
2554
- 'Predict the historical blast radius of a code change before applying it. Returns a risk verdict (low/medium/high/unknown), confidence, and the git-derived signals behind it (revert history, hotspots, fix ratio, coverage decline, ownership churn). Call BEFORE proposing significant edits to files with non-trivial history. Honest about uncertainty \u2014 returns "unknown" when confidence is low instead of guessing. Degraded modes (empty repo, shallow clone, indexing) are explicit in the `status` field.',
2554
+ "Risk verdict (low/medium/high/unknown) for editing a file, from git history.",
2555
2555
  {
2556
2556
  file: z.string().describe("Repo-relative path of the file the agent intends to modify."),
2557
2557
  intent: z.enum(["refactor", "bugfix", "feature", "test", "docs", "unknown"]).default("unknown").optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {