composto-ai 0.6.1 → 0.7.1

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/index.js CHANGED
@@ -843,8 +843,9 @@ function emitTier1(node) {
843
843
  const outPrefix = exported ? "OUT " : "";
844
844
  switch (node.type) {
845
845
  case "import_statement": {
846
- const text = collapseText(node.text, 80);
847
- return `USE:${text}`;
846
+ const source = node.childForFieldName("source")?.text;
847
+ if (source) return `USE:${source.slice(1, -1)}`;
848
+ return `USE:${collapseText(node.text, 80)}`;
848
849
  }
849
850
  case "function_declaration": {
850
851
  const name = node.childForFieldName("name")?.text ?? "anonymous";
@@ -974,9 +975,30 @@ function emitTier3(node) {
974
975
  }
975
976
  if (node.parent?.type === "statement_block") return null;
976
977
  const vt = value.type;
977
- if (vt === "number" || vt === "true" || vt === "false") return null;
978
- if (vt === "object" || vt === "array") return null;
979
- if (vt === "new_expression" || vt === "call_expression") return null;
978
+ if (vt === "number" || vt === "true" || vt === "false") {
979
+ return `VAR:${name} = ${value.text}`;
980
+ }
981
+ if (vt === "array") {
982
+ return `VAR:${name}[${value.namedChildCount}]`;
983
+ }
984
+ if (vt === "object") {
985
+ const keys = [];
986
+ for (let i = 0; i < value.namedChildCount; i++) {
987
+ const member = value.namedChild(i);
988
+ keys.push(member.childForFieldName("key")?.text ?? member.text);
989
+ if (keys.length >= 6) break;
990
+ }
991
+ const more = value.namedChildCount > keys.length ? ", ..." : "";
992
+ return `VAR:${name}{${collapseText(keys.join(", "), 50)}${more}}`;
993
+ }
994
+ if (vt === "new_expression") {
995
+ const ctor = value.childForFieldName("constructor")?.text ?? "?";
996
+ return `VAR:${name} = new ${ctor}(...)`;
997
+ }
998
+ if (vt === "call_expression") {
999
+ const callee = value.childForFieldName("function")?.text ?? "?";
1000
+ return `VAR:${name} = ${collapseText(callee, 40)}(...)`;
1001
+ }
980
1002
  const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
981
1003
  return `VAR:${name} = ${collapseText(valText, 50)}`;
982
1004
  }
@@ -1137,29 +1159,27 @@ async function astWalkIR(code, filePath) {
1137
1159
  }
1138
1160
  const merged = [];
1139
1161
  let guardBlock = [];
1162
+ const guardValue = (ret) => ret.replace(/^RET\s+/, "");
1163
+ const flushGuards = () => {
1164
+ if (guardBlock.length === 0) return;
1165
+ if (guardBlock.length < 3) {
1166
+ for (const g of guardBlock) merged.push(`${g.indent}IF:${g.cond} \u2192 ${g.ret}`);
1167
+ } else {
1168
+ const entries = guardBlock.map((g) => `${g.cond} \u2192 ${guardValue(g.ret)}`);
1169
+ merged.push(`${guardBlock[0].indent}GUARD:[${entries.join(", ")}]`);
1170
+ }
1171
+ guardBlock = [];
1172
+ };
1140
1173
  for (const line of pass1) {
1141
- const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
1174
+ const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 (.+)$/);
1142
1175
  if (guardMatch) {
1143
- guardBlock.push(guardMatch[2].trim());
1176
+ guardBlock.push({ indent: guardMatch[1], cond: guardMatch[2].trim(), ret: guardMatch[3].trim() });
1144
1177
  continue;
1145
1178
  }
1146
- if (guardBlock.length > 0) {
1147
- if (guardBlock.length < 3) {
1148
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1149
- } else {
1150
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
1151
- }
1152
- guardBlock = [];
1153
- }
1179
+ flushGuards();
1154
1180
  merged.push(line);
1155
1181
  }
1156
- if (guardBlock.length > 0) {
1157
- if (guardBlock.length < 3) {
1158
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1159
- } else {
1160
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
1161
- }
1162
- }
1182
+ flushGuards();
1163
1183
  return merged.join("\n");
1164
1184
  }
1165
1185
 
@@ -3062,40 +3082,45 @@ function writeCursorHooks(projectPath, result) {
3062
3082
  if (existed) result.merged.push(relPath);
3063
3083
  else result.written.push(relPath);
3064
3084
  }
3065
- function initCursor(projectPath, result) {
3066
- writeJsonMerged(
3067
- join7(projectPath, ".cursor", "mcp.json"),
3068
- {
3069
- mcpServers: {
3070
- composto: {
3071
- command: "composto-mcp",
3072
- env: { COMPOSTO_BLASTRADIUS: "1" }
3085
+ function initCursor(projectPath, result, options) {
3086
+ if (options.withMcp) {
3087
+ writeJsonMerged(
3088
+ join7(projectPath, ".cursor", "mcp.json"),
3089
+ {
3090
+ mcpServers: {
3091
+ composto: {
3092
+ command: "composto-mcp",
3093
+ env: { COMPOSTO_BLASTRADIUS: "1" }
3094
+ }
3073
3095
  }
3074
- }
3075
- },
3076
- result,
3077
- ".cursor/mcp.json"
3078
- );
3079
- writeFileSkipIfExists(
3080
- join7(projectPath, ".cursor", "rules", "composto.mdc"),
3081
- CURSOR_RULES_MDC,
3082
- result,
3083
- ".cursor/rules/composto.mdc"
3084
- );
3096
+ },
3097
+ result,
3098
+ ".cursor/mcp.json"
3099
+ );
3100
+ }
3101
+ if (options.withRules) {
3102
+ writeFileSkipIfExists(
3103
+ join7(projectPath, ".cursor", "rules", "composto.mdc"),
3104
+ CURSOR_RULES_MDC,
3105
+ result,
3106
+ ".cursor/rules/composto.mdc"
3107
+ );
3108
+ }
3085
3109
  writeCursorHooks(projectPath, result);
3086
3110
  }
3087
- function initClaudeCode(projectPath, result) {
3111
+ function initClaudeCode(projectPath, result, options) {
3088
3112
  const settingsPath = join7(projectPath, ".claude", "settings.json");
3089
3113
  const relPath = ".claude/settings.json";
3090
3114
  const existed = existsSync4(settingsPath);
3091
3115
  const existing = readJsonIfExists(settingsPath);
3092
- const mcpServers = {
3093
- ...existing.mcpServers ?? {},
3116
+ const baseExistingMcp = existing.mcpServers ?? {};
3117
+ const mcpServers = options.withMcp ? {
3118
+ ...baseExistingMcp,
3094
3119
  composto: {
3095
3120
  command: "composto-mcp",
3096
3121
  env: { COMPOSTO_BLASTRADIUS: "1" }
3097
3122
  }
3098
- };
3123
+ } : baseExistingMcp;
3099
3124
  const compostoHookEntry = {
3100
3125
  matcher: "Edit|Write|MultiEdit",
3101
3126
  hooks: [
@@ -3110,9 +3135,11 @@ function initClaudeCode(projectPath, result) {
3110
3135
  );
3111
3136
  const merged = {
3112
3137
  ...existing,
3113
- mcpServers,
3114
3138
  hooks: { ...existingHooks, PreToolUse: preToolUse }
3115
3139
  };
3140
+ if (Object.keys(mcpServers).length > 0) {
3141
+ merged.mcpServers = mcpServers;
3142
+ }
3116
3143
  ensureDir(settingsPath);
3117
3144
  writeFileSync2(settingsPath, JSON.stringify(merged, null, 2) + "\n");
3118
3145
  if (existed) result.merged.push(relPath);
@@ -3124,13 +3151,14 @@ function initGeminiCli(_projectPath, result, options) {
3124
3151
  try {
3125
3152
  const existed = existsSync4(settingsPath);
3126
3153
  const existing = readJsonIfExists(settingsPath);
3127
- const mcpServers = {
3128
- ...existing.mcpServers ?? {},
3154
+ const baseExistingMcp = existing.mcpServers ?? {};
3155
+ const mcpServers = options.withMcp ? {
3156
+ ...baseExistingMcp,
3129
3157
  composto: {
3130
3158
  command: "composto-mcp",
3131
3159
  env: { COMPOSTO_BLASTRADIUS: "1" }
3132
3160
  }
3133
- };
3161
+ } : baseExistingMcp;
3134
3162
  const compostoHookEntry = {
3135
3163
  matcher: "edit_file|write_file|replace",
3136
3164
  hooks: [
@@ -3145,9 +3173,11 @@ function initGeminiCli(_projectPath, result, options) {
3145
3173
  );
3146
3174
  const merged = {
3147
3175
  ...existing,
3148
- mcpServers,
3149
3176
  hooks: { ...existingHooks, BeforeTool: beforeTool }
3150
3177
  };
3178
+ if (Object.keys(mcpServers).length > 0) {
3179
+ merged.mcpServers = mcpServers;
3180
+ }
3151
3181
  ensureDir(settingsPath);
3152
3182
  writeFileSync2(settingsPath, JSON.stringify(merged, null, 2) + "\n");
3153
3183
  if (existed) result.merged.push(relPath);
@@ -3160,9 +3190,9 @@ function initGeminiCli(_projectPath, result, options) {
3160
3190
  function runInit(projectPath, options) {
3161
3191
  const client = options.client ?? "cursor";
3162
3192
  const result = { client, written: [], skipped: [], merged: [] };
3163
- if (client === "claude-code") initClaudeCode(projectPath, result);
3193
+ if (client === "claude-code") initClaudeCode(projectPath, result, options);
3164
3194
  else if (client === "gemini-cli") initGeminiCli(projectPath, result, options);
3165
- else initCursor(projectPath, result);
3195
+ else initCursor(projectPath, result, options);
3166
3196
  return result;
3167
3197
  }
3168
3198
 
@@ -3583,7 +3613,16 @@ function renderSummary(s) {
3583
3613
  }
3584
3614
 
3585
3615
  // src/index.ts
3616
+ import { createRequire } from "module";
3586
3617
  import { join as join13, resolve as resolve2 } from "path";
3618
+ var PKG_VERSION = (() => {
3619
+ try {
3620
+ const req = createRequire(import.meta.url);
3621
+ return req("../package.json").version;
3622
+ } catch {
3623
+ return "0.0.0";
3624
+ }
3625
+ })();
3587
3626
  async function readStdin() {
3588
3627
  if (process.stdin.isTTY) return "";
3589
3628
  const chunks = [];
@@ -3675,7 +3714,13 @@ switch (command) {
3675
3714
  console.error(`Unknown --client=${clientArg}. Valid: ${valid.join(", ")}`);
3676
3715
  process.exit(1);
3677
3716
  }
3678
- const result = runInit(resolve2("."), { client: clientArg });
3717
+ const withRules = args.includes("--with-rules");
3718
+ const withMcp = args.includes("--with-mcp");
3719
+ const result = runInit(resolve2("."), {
3720
+ client: clientArg,
3721
+ withRules,
3722
+ withMcp
3723
+ });
3679
3724
  console.log(`composto init \u2014 configured for ${result.client}
3680
3725
  `);
3681
3726
  for (const f of result.written) console.log(` wrote ${f}`);
@@ -3738,10 +3783,11 @@ switch (command) {
3738
3783
  break;
3739
3784
  }
3740
3785
  case "version":
3741
- console.log("composto v0.4.2");
3786
+ console.log(`composto v${PKG_VERSION}`);
3742
3787
  break;
3743
3788
  default:
3744
- console.log("composto v0.4.2 \u2014 less tokens, more insight\n");
3789
+ console.log(`composto v${PKG_VERSION} \u2014 less tokens, more insight
3790
+ `);
3745
3791
  console.log("Commands:");
3746
3792
  console.log(" scan [path] Scan codebase for issues");
3747
3793
  console.log(" trends [path] Analyze codebase health trends");
@@ -3753,8 +3799,10 @@ switch (command) {
3753
3799
  console.log(" impact <file> Show historical blast radius for a file");
3754
3800
  console.log(" index [--since=YYYY-MM-DD] Build or refresh the memory index (--since bounds work for huge repos)");
3755
3801
  console.log(" index --status Show memory index diagnostics");
3756
- console.log(" init [--client=<name>] Configure Composto MCP + hooks for an AI client");
3757
- console.log(" (clients: cursor, claude-code, gemini-cli)");
3802
+ console.log(" init [--client=<name>] [--with-mcp] [--with-rules]");
3803
+ console.log(" Lean Hook init (clients: cursor, claude-code, gemini-cli)");
3804
+ console.log(" --with-mcp register the composto MCP server (5 tools)");
3805
+ console.log(" --with-rules write .cursor/rules/composto.mdc (cursor only)");
3758
3806
  console.log(" hook <platform> <event> Run BlastRadius hook (reads tool JSON from stdin)");
3759
3807
  console.log(" stats [--json] [--disable] Show hook telemetry (last 7d); --disable opts out");
3760
3808
  console.log(" version Show version");
@@ -5,7 +5,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import { readFileSync as readFileSync4 } from "fs";
8
- import { resolve as resolve2, relative as relative2, join as join6 } from "path";
8
+ import { fileURLToPath as fileURLToPath3 } from "url";
9
+ import { resolve as resolve2, relative as relative2, join as join6, dirname as dirname5 } from "path";
9
10
 
10
11
  // src/ir/structure.ts
11
12
  var CLASSIFIERS = [
@@ -650,8 +651,9 @@ function emitTier1(node) {
650
651
  const outPrefix = exported ? "OUT " : "";
651
652
  switch (node.type) {
652
653
  case "import_statement": {
653
- const text = collapseText(node.text, 80);
654
- return `USE:${text}`;
654
+ const source = node.childForFieldName("source")?.text;
655
+ if (source) return `USE:${source.slice(1, -1)}`;
656
+ return `USE:${collapseText(node.text, 80)}`;
655
657
  }
656
658
  case "function_declaration": {
657
659
  const name = node.childForFieldName("name")?.text ?? "anonymous";
@@ -781,9 +783,30 @@ function emitTier3(node) {
781
783
  }
782
784
  if (node.parent?.type === "statement_block") return null;
783
785
  const vt = value.type;
784
- if (vt === "number" || vt === "true" || vt === "false") return null;
785
- if (vt === "object" || vt === "array") return null;
786
- if (vt === "new_expression" || vt === "call_expression") return null;
786
+ if (vt === "number" || vt === "true" || vt === "false") {
787
+ return `VAR:${name} = ${value.text}`;
788
+ }
789
+ if (vt === "array") {
790
+ return `VAR:${name}[${value.namedChildCount}]`;
791
+ }
792
+ if (vt === "object") {
793
+ const keys = [];
794
+ for (let i = 0; i < value.namedChildCount; i++) {
795
+ const member = value.namedChild(i);
796
+ keys.push(member.childForFieldName("key")?.text ?? member.text);
797
+ if (keys.length >= 6) break;
798
+ }
799
+ const more = value.namedChildCount > keys.length ? ", ..." : "";
800
+ return `VAR:${name}{${collapseText(keys.join(", "), 50)}${more}}`;
801
+ }
802
+ if (vt === "new_expression") {
803
+ const ctor = value.childForFieldName("constructor")?.text ?? "?";
804
+ return `VAR:${name} = new ${ctor}(...)`;
805
+ }
806
+ if (vt === "call_expression") {
807
+ const callee = value.childForFieldName("function")?.text ?? "?";
808
+ return `VAR:${name} = ${collapseText(callee, 40)}(...)`;
809
+ }
787
810
  const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
788
811
  return `VAR:${name} = ${collapseText(valText, 50)}`;
789
812
  }
@@ -944,29 +967,27 @@ async function astWalkIR(code, filePath) {
944
967
  }
945
968
  const merged = [];
946
969
  let guardBlock = [];
970
+ const guardValue = (ret) => ret.replace(/^RET\s+/, "");
971
+ const flushGuards = () => {
972
+ if (guardBlock.length === 0) return;
973
+ if (guardBlock.length < 3) {
974
+ for (const g of guardBlock) merged.push(`${g.indent}IF:${g.cond} \u2192 ${g.ret}`);
975
+ } else {
976
+ const entries = guardBlock.map((g) => `${g.cond} \u2192 ${guardValue(g.ret)}`);
977
+ merged.push(`${guardBlock[0].indent}GUARD:[${entries.join(", ")}]`);
978
+ }
979
+ guardBlock = [];
980
+ };
947
981
  for (const line of pass1) {
948
- const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
982
+ const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 (.+)$/);
949
983
  if (guardMatch) {
950
- guardBlock.push(guardMatch[2].trim());
984
+ guardBlock.push({ indent: guardMatch[1], cond: guardMatch[2].trim(), ret: guardMatch[3].trim() });
951
985
  continue;
952
986
  }
953
- if (guardBlock.length > 0) {
954
- if (guardBlock.length < 3) {
955
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
956
- } else {
957
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
958
- }
959
- guardBlock = [];
960
- }
987
+ flushGuards();
961
988
  merged.push(line);
962
989
  }
963
- if (guardBlock.length > 0) {
964
- if (guardBlock.length < 3) {
965
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
966
- } else {
967
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
968
- }
969
- }
990
+ flushGuards();
970
991
  return merged.join("\n");
971
992
  }
972
993
 
@@ -2381,13 +2402,26 @@ var MemoryAPI = class {
2381
2402
 
2382
2403
  // src/mcp/server.ts
2383
2404
  var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
2405
+ var PKG_VERSION = (() => {
2406
+ try {
2407
+ const pkgPath = join6(
2408
+ dirname5(fileURLToPath3(import.meta.url)),
2409
+ "..",
2410
+ "..",
2411
+ "package.json"
2412
+ );
2413
+ return JSON.parse(readFileSync4(pkgPath, "utf-8")).version;
2414
+ } catch {
2415
+ return "0.0.0";
2416
+ }
2417
+ })();
2384
2418
  var server = new McpServer({
2385
2419
  name: "composto",
2386
- version: "0.4.2"
2420
+ version: PKG_VERSION
2387
2421
  });
2388
2422
  server.tool(
2389
2423
  "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.",
2424
+ "Compressed AST-based IR for a file. ~89% fewer tokens than raw read.",
2391
2425
  {
2392
2426
  file: z.string().describe("Path to the source file"),
2393
2427
  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 +2457,7 @@ ${result}` }]
2423
2457
  );
2424
2458
  server.tool(
2425
2459
  "composto_benchmark",
2426
- "Benchmark how much Composto saves across a directory. Shows per-file token savings comparing raw code vs compressed IR.",
2460
+ "Per-file token-savings benchmark across a directory.",
2427
2461
  {
2428
2462
  path: z.string().default(".").describe("Directory to benchmark")
2429
2463
  },
@@ -2455,7 +2489,7 @@ server.tool(
2455
2489
  );
2456
2490
  server.tool(
2457
2491
  "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.",
2492
+ "Pack code into a token budget; target raw, neighbors as IR.",
2459
2493
  {
2460
2494
  path: z.string().default(".").describe("Directory to pack"),
2461
2495
  budget: z.number().default(4e3).describe("Maximum tokens to use"),
@@ -2518,7 +2552,7 @@ Files: ${parts.join(", ")}`);
2518
2552
  );
2519
2553
  server.tool(
2520
2554
  "composto_scan",
2521
- "Scan codebase for security issues (hardcoded secrets, API keys) and debug artifacts (console.log). Zero token cost \u2014 pure local analysis.",
2555
+ "Scan for hardcoded secrets and debug artifacts. Local-only.",
2522
2556
  {
2523
2557
  path: z.string().default(".").describe("Directory to scan")
2524
2558
  },
@@ -2551,7 +2585,7 @@ server.tool(
2551
2585
  );
2552
2586
  server.tool(
2553
2587
  "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.',
2588
+ "Risk verdict (low/medium/high/unknown) for editing a file, from git history.",
2555
2589
  {
2556
2590
  file: z.string().describe("Repo-relative path of the file the agent intends to modify."),
2557
2591
  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.1",
3
+ "version": "0.7.1",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {