composto-ai 0.2.1 → 0.3.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.
@@ -511,6 +511,11 @@ var TIER_MAP = {
511
511
  class_definition: "T1_KEEP",
512
512
  import_from_statement: "T1_KEEP",
513
513
  decorated_definition: "T1_KEEP",
514
+ // Tier 1 — class members (qualified methods)
515
+ method_definition: "T1_KEEP",
516
+ // JS/TS class method
517
+ public_field_definition: "T1_KEEP",
518
+ // TS class property
514
519
  // Tier 1 — Go
515
520
  function_item: "T1_KEEP",
516
521
  // Rust
@@ -606,6 +611,49 @@ function isExported(node) {
606
611
  function isAsync(node) {
607
612
  return node.text.trimStart().startsWith("async");
608
613
  }
614
+ function extractDocComment(node) {
615
+ let prev = node.previousNamedSibling;
616
+ if (!prev && node.parent?.type === "export_statement") {
617
+ prev = node.parent.previousNamedSibling;
618
+ }
619
+ if (!prev || prev.type !== "comment") return null;
620
+ const text = prev.text;
621
+ if (!text.startsWith("/**")) return null;
622
+ const body = text.replace(/^\/\*\*|\*\/$/g, "").replace(/^\s*\*\s?/gm, "").trim();
623
+ const tags = [];
624
+ const tagMatches = body.matchAll(/@(\w+)(?:\s+([^\n@]+))?/g);
625
+ for (const m of tagMatches) {
626
+ const tag = m[1];
627
+ const val = (m[2] ?? "").trim();
628
+ if (tag === "deprecated") tags.push("@deprecated");
629
+ else if (tag === "internal") tags.push("@internal");
630
+ else if (tag === "throws" && val) tags.push(`@throws:${val.length > 30 ? val.slice(0, 27) + "..." : val}`);
631
+ }
632
+ const beforeTags = body.split(/@\w+/)[0].trim();
633
+ const desc = beforeTags.split(/[.\n]/)[0].trim();
634
+ const parts = [];
635
+ if (tags.length > 0) parts.push(tags.join(" "));
636
+ if (desc && desc.length > 3) {
637
+ parts.push(`"${desc.length > 50 ? desc.slice(0, 47) + "..." : desc}"`);
638
+ }
639
+ return parts.length > 0 ? parts.join(" ") : null;
640
+ }
641
+ function extractPythonDocstring(bodyNode) {
642
+ if (!bodyNode) return null;
643
+ for (let i = 0; i < bodyNode.childCount; i++) {
644
+ const child = bodyNode.child(i);
645
+ if (child.type === "expression_statement" && child.childCount > 0) {
646
+ const expr = child.child(0);
647
+ if (expr.type === "string") {
648
+ const text = expr.text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
649
+ const firstLine = text.split("\n")[0].trim();
650
+ return firstLine.length > 3 ? `"${firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine}"` : null;
651
+ }
652
+ break;
653
+ }
654
+ }
655
+ return null;
656
+ }
609
657
  function extractCondition(node) {
610
658
  const condNode = node.childForFieldName("condition") ?? (() => {
611
659
  for (let i = 0; i < node.childCount; i++) {
@@ -743,25 +791,57 @@ function emitTier1(node) {
743
791
  const rawParams = node.childForFieldName("parameters")?.text ?? "()";
744
792
  const params = collapseText(rawParams, 60);
745
793
  const asyncPrefix = isAsync(node) ? "ASYNC " : "";
746
- return `${outPrefix}${asyncPrefix}FN:${name}${params}`;
794
+ const doc = extractDocComment(node);
795
+ const docPrefix = doc ? `${doc} ` : "";
796
+ return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
747
797
  }
748
798
  case "class_declaration": {
749
799
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
750
800
  const typeParams = getTypeParams(node);
751
- return `${outPrefix}CLASS:${name}${typeParams}`;
801
+ const doc = extractDocComment(node);
802
+ const docPrefix = doc ? `${doc} ` : "";
803
+ return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
752
804
  }
753
805
  case "interface_declaration": {
754
806
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
755
807
  const typeParams = getTypeParams(node);
756
- return `${outPrefix}INTERFACE:${name}${typeParams}`;
808
+ const doc = extractDocComment(node);
809
+ const docPrefix = doc ? `${doc} ` : "";
810
+ return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
757
811
  }
758
812
  case "type_alias_declaration": {
759
813
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
760
- return `${outPrefix}TYPE:${name}`;
814
+ const doc = extractDocComment(node);
815
+ const docPrefix = doc ? `${doc} ` : "";
816
+ return `${docPrefix}${outPrefix}TYPE:${name}`;
761
817
  }
762
818
  case "enum_declaration": {
763
819
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
764
- return `${outPrefix}ENUM:${name}`;
820
+ const doc = extractDocComment(node);
821
+ const docPrefix = doc ? `${doc} ` : "";
822
+ return `${docPrefix}${outPrefix}ENUM:${name}`;
823
+ }
824
+ case "method_definition": {
825
+ let enclosingClass = null;
826
+ let parent = node.parent;
827
+ while (parent) {
828
+ if (parent.type === "class_declaration" || parent.type === "class_definition") {
829
+ enclosingClass = parent.childForFieldName("name")?.text ?? null;
830
+ break;
831
+ }
832
+ parent = parent.parent;
833
+ }
834
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
835
+ const params = node.childForFieldName("parameters")?.text ?? "()";
836
+ const asyncPrefix = isAsync(node) ? "ASYNC " : "";
837
+ const doc = extractDocComment(node);
838
+ const docPrefix = doc ? `${doc} ` : "";
839
+ const qualifiedName = enclosingClass ? `${enclosingClass}.${name}` : name;
840
+ return `${docPrefix}${asyncPrefix}METHOD:${qualifiedName}${collapseText(params, 60)}`;
841
+ }
842
+ case "public_field_definition": {
843
+ const name = node.childForFieldName("name")?.text ?? "field";
844
+ return `FIELD:${name}`;
765
845
  }
766
846
  // Python
767
847
  case "function_definition": {
@@ -769,13 +849,19 @@ function emitTier1(node) {
769
849
  const params = node.childForFieldName("parameters")?.text ?? "()";
770
850
  const returnType = node.childForFieldName("return_type")?.text ?? "";
771
851
  const rt = returnType ? ` -> ${returnType}` : "";
772
- return `FN:${name}${collapseText(params, 60)}${rt}`;
852
+ const body = node.childForFieldName("body");
853
+ const doc = extractPythonDocstring(body);
854
+ const docPrefix = doc ? `${doc} ` : "";
855
+ return `${docPrefix}FN:${name}${collapseText(params, 60)}${rt}`;
773
856
  }
774
857
  case "class_definition": {
775
858
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
776
859
  const superclass = node.childForFieldName("superclasses")?.text ?? "";
777
860
  const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
778
- return `CLASS:${name}${sc}`;
861
+ const body = node.childForFieldName("body");
862
+ const doc = extractPythonDocstring(body);
863
+ const docPrefix = doc ? `${doc} ` : "";
864
+ return `${docPrefix}CLASS:${name}${sc}`;
779
865
  }
780
866
  case "import_from_statement": {
781
867
  return `USE:${collapseText(node.text, 80)}`;
@@ -1259,43 +1345,161 @@ function summarize(results) {
1259
1345
  }
1260
1346
 
1261
1347
  // src/context/packer.ts
1348
+ function escapeRegex(s) {
1349
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1350
+ }
1351
+ function findTargetFile(files, target) {
1352
+ const t = escapeRegex(target);
1353
+ const declarationPatterns = [
1354
+ // JS/TS declarations
1355
+ new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${t}\\b`),
1356
+ new RegExp(`(?:export\\s+)?class\\s+${t}\\b`),
1357
+ new RegExp(`(?:export\\s+)?interface\\s+${t}\\b`),
1358
+ new RegExp(`(?:export\\s+)?type\\s+${t}\\b`),
1359
+ new RegExp(`(?:export\\s+)?enum\\s+${t}\\b`),
1360
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${t}\\b`),
1361
+ // Python
1362
+ new RegExp(`\\bdef\\s+${t}\\b`),
1363
+ // Rust
1364
+ new RegExp(`\\bfn\\s+${t}\\b`),
1365
+ new RegExp(`\\bstruct\\s+${t}\\b`),
1366
+ new RegExp(`\\btrait\\s+${t}\\b`),
1367
+ // Go
1368
+ new RegExp(`\\bfunc\\s+${t}\\b`),
1369
+ new RegExp(`\\btype\\s+${t}\\b`),
1370
+ // Object method shorthand
1371
+ new RegExp(`\\b${t}\\s*:\\s*(?:async\\s+)?function\\b`),
1372
+ new RegExp(`\\b${t}\\s*\\([^)]*\\)\\s*\\{`)
1373
+ ];
1374
+ for (const pattern of declarationPatterns) {
1375
+ const match2 = files.find((f) => pattern.test(f.code));
1376
+ if (match2) return match2.path;
1377
+ }
1378
+ const fallback = new RegExp(`\\b${t}\\s*\\(`);
1379
+ const match = files.find((f) => fallback.test(f.code));
1380
+ return match ? match.path : null;
1381
+ }
1382
+ function findRelatedFiles(files, targetPath) {
1383
+ const related = /* @__PURE__ */ new Set();
1384
+ const targetFile = files.find((f) => f.path === targetPath);
1385
+ if (!targetFile) return related;
1386
+ const importPattern = /(?:import|require)\s*(?:\([^)]*|\{[^}]*\}|\w+)?\s*(?:from)?\s*["']([^"']+)["']/g;
1387
+ const imports = [...targetFile.code.matchAll(importPattern)].map((m) => m[1]);
1388
+ for (const imp of imports) {
1389
+ const match = files.find((f) => {
1390
+ const basename = f.path.replace(/\.[^.]+$/, "");
1391
+ return imp.includes(basename) || basename.endsWith(imp.replace(/^\.\.?\//, "").replace(/\.[^.]+$/, ""));
1392
+ });
1393
+ if (match) related.add(match.path);
1394
+ }
1395
+ const targetBasename = targetPath.replace(/\.[^.]+$/, "").split("/").pop() ?? "";
1396
+ for (const file of files) {
1397
+ if (file.path === targetPath) continue;
1398
+ if (file.code.includes(targetBasename)) {
1399
+ related.add(file.path);
1400
+ }
1401
+ }
1402
+ return related;
1403
+ }
1262
1404
  async function packContext(files, options) {
1263
- const { budget, hotspots } = options;
1405
+ const { budget, hotspots, target } = options;
1264
1406
  const hotspotSet = new Set(hotspots.map((h) => h.file));
1407
+ let targetPath = null;
1408
+ let relatedFiles = /* @__PURE__ */ new Set();
1409
+ if (target) {
1410
+ targetPath = findTargetFile(files, target);
1411
+ if (targetPath) {
1412
+ relatedFiles = findRelatedFiles(files, targetPath);
1413
+ }
1414
+ }
1265
1415
  const entries = [];
1266
1416
  let totalTokens = 0;
1417
+ let filesAtL3 = 0;
1418
+ let targetDowngraded = false;
1419
+ if (targetPath) {
1420
+ const targetFile = files.find((f) => f.path === targetPath);
1421
+ const rawTokens = estimateTokens(targetFile.code);
1422
+ if (rawTokens <= budget * 0.6) {
1423
+ entries.push({
1424
+ path: targetPath,
1425
+ layer: "L3",
1426
+ ir: targetFile.code,
1427
+ tokens: rawTokens,
1428
+ isTarget: true
1429
+ });
1430
+ totalTokens += rawTokens;
1431
+ filesAtL3 = 1;
1432
+ } else {
1433
+ targetDowngraded = true;
1434
+ const l1 = await generateLayer("L1", { code: targetFile.code, filePath: targetFile.path, health: null });
1435
+ const l1Tokens = estimateTokens(l1);
1436
+ entries.push({
1437
+ path: targetPath,
1438
+ layer: "L1",
1439
+ ir: l1,
1440
+ tokens: l1Tokens,
1441
+ isTarget: true
1442
+ });
1443
+ totalTokens += l1Tokens;
1444
+ }
1445
+ }
1267
1446
  for (const file of files) {
1447
+ if (file.path === targetPath) continue;
1268
1448
  const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
1269
1449
  const l0Tokens = estimateTokens(l0);
1270
1450
  entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
1271
1451
  totalTokens += l0Tokens;
1272
1452
  }
1273
1453
  if (totalTokens > budget) {
1274
- const truncated = [];
1275
- let used = 0;
1454
+ const truncated = entries.filter((e) => e.isTarget);
1455
+ let used = truncated.reduce((s, e) => s + e.tokens, 0);
1276
1456
  for (const entry of entries) {
1457
+ if (entry.isTarget) continue;
1277
1458
  if (used + entry.tokens <= budget) {
1278
1459
  truncated.push(entry);
1279
1460
  used += entry.tokens;
1280
1461
  }
1281
1462
  }
1282
- return { entries: truncated, totalTokens: used, budget, filesAtL0: truncated.length, filesAtL1: 0 };
1463
+ return {
1464
+ entries: truncated,
1465
+ totalTokens: used,
1466
+ budget,
1467
+ filesAtL0: truncated.filter((e) => e.layer === "L0").length,
1468
+ filesAtL1: truncated.filter((e) => e.layer === "L1").length,
1469
+ filesAtL3,
1470
+ targetFile: targetPath ?? void 0,
1471
+ targetDowngraded
1472
+ };
1283
1473
  }
1284
- const upgradeOrder = entries.map((e, i) => ({ index: i, path: e.path, rawTokens: files[i].rawTokens, isHotspot: hotspotSet.has(e.path) })).sort((a, b) => {
1474
+ const upgradeOrder = entries.map((e, i) => ({
1475
+ index: i,
1476
+ path: e.path,
1477
+ rawTokens: files.find((f) => f.path === e.path)?.rawTokens ?? 0,
1478
+ isHotspot: hotspotSet.has(e.path),
1479
+ isRelated: relatedFiles.has(e.path),
1480
+ isTarget: e.isTarget ?? false
1481
+ })).filter((x) => x.isTarget === false && entries[x.index].layer === "L0").sort((a, b) => {
1482
+ if (a.isRelated && !b.isRelated) return -1;
1483
+ if (!a.isRelated && b.isRelated) return 1;
1285
1484
  if (a.isHotspot && !b.isHotspot) return -1;
1286
1485
  if (!a.isHotspot && b.isHotspot) return 1;
1287
1486
  return b.rawTokens - a.rawTokens;
1288
1487
  });
1289
1488
  let filesAtL1 = 0;
1290
1489
  for (const item of upgradeOrder) {
1291
- const file = files[item.index];
1490
+ const file = files.find((f) => f.path === item.path);
1292
1491
  const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
1293
1492
  const l1Tokens = estimateTokens(l1);
1294
- const currentL0Tokens = entries[item.index].tokens;
1295
- const additionalTokens = l1Tokens - currentL0Tokens;
1296
- if (totalTokens + additionalTokens <= budget) {
1297
- entries[item.index] = { path: item.path, layer: "L1", ir: l1, tokens: l1Tokens };
1298
- totalTokens += additionalTokens;
1493
+ const currentTokens = entries[item.index].tokens;
1494
+ const additional = l1Tokens - currentTokens;
1495
+ if (totalTokens + additional <= budget) {
1496
+ entries[item.index] = {
1497
+ path: item.path,
1498
+ layer: "L1",
1499
+ ir: l1,
1500
+ tokens: l1Tokens
1501
+ };
1502
+ totalTokens += additional;
1299
1503
  filesAtL1++;
1300
1504
  }
1301
1505
  }
@@ -1303,8 +1507,11 @@ async function packContext(files, options) {
1303
1507
  entries,
1304
1508
  totalTokens,
1305
1509
  budget,
1306
- filesAtL0: entries.length - filesAtL1,
1307
- filesAtL1
1510
+ filesAtL0: entries.filter((e) => e.layer === "L0").length,
1511
+ filesAtL1,
1512
+ filesAtL3,
1513
+ targetFile: targetPath ?? void 0,
1514
+ targetDowngraded
1308
1515
  };
1309
1516
  }
1310
1517
 
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  packContext,
13
13
  runDetector,
14
14
  summarize
15
- } from "./chunk-T2SFR7QS.js";
15
+ } from "./chunk-AARGW2GV.js";
16
16
 
17
17
  // src/cli/commands.ts
18
18
  import { readFileSync, readdirSync } from "fs";
@@ -201,7 +201,7 @@ function collectFiles(dir, extensions) {
201
201
  function runScan(projectPath) {
202
202
  const adapter = new CLIAdapter();
203
203
  const config = loadConfig(projectPath);
204
- console.log("composto v0.1.0 \u2014 scanning...\n");
204
+ console.log("composto v0.2.3 \u2014 scanning...\n");
205
205
  const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
206
206
  console.log(` Found ${files.length} files
207
207
  `);
@@ -228,7 +228,7 @@ function runScan(projectPath) {
228
228
  function runTrends(projectPath) {
229
229
  const adapter = new CLIAdapter();
230
230
  const config = loadConfig(projectPath);
231
- console.log("composto v0.1.0 \u2014 trend analysis...\n");
231
+ console.log("composto v0.2.3 \u2014 trend analysis...\n");
232
232
  const entries = getGitLog(projectPath, 100);
233
233
  if (entries.length === 0) {
234
234
  console.log(" No git history found.\n");
@@ -270,7 +270,7 @@ async function runIR(projectPath, filePath, layer) {
270
270
  }
271
271
  var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
272
272
  async function runBenchmark(projectPath) {
273
- console.log("composto v0.1.0 \u2014 benchmark\n");
273
+ console.log("composto v0.2.3 \u2014 benchmark\n");
274
274
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
275
275
  console.log(` ${files.length} files
276
276
  `);
@@ -317,7 +317,7 @@ async function runBenchmarkQuality(projectPath, filePath) {
317
317
  }
318
318
  const code = readFileSync(filePath, "utf-8");
319
319
  const relPath = relative(projectPath, filePath);
320
- console.log("composto v0.1.0 \u2014 quality benchmark\n");
320
+ console.log("composto v0.2.3 \u2014 quality benchmark\n");
321
321
  console.log(` File: ${relPath}
322
322
  `);
323
323
  console.log(" Sending to Claude Haiku...\n");
@@ -346,9 +346,11 @@ ${result.ir.response}
346
346
  console.log(` Verdict: ${result.savedPercent.toFixed(1)}% fewer tokens with IR.`);
347
347
  }
348
348
  }
349
- async function runContext(projectPath, budget) {
350
- console.log(`composto v0.1.0 \u2014 context (budget: ${budget} tokens)
351
- `);
349
+ async function runContext(projectPath, budget, target) {
350
+ const header = target ? `composto v0.2.3 \u2014 context (target: ${target}, budget: ${budget} tokens)
351
+ ` : `composto v0.2.3 \u2014 context (budget: ${budget} tokens)
352
+ `;
353
+ console.log(header);
352
354
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
353
355
  console.log(` ${files.length} files
354
356
  `);
@@ -363,13 +365,34 @@ async function runContext(projectPath, budget) {
363
365
  const relPath = relative(projectPath, file);
364
366
  return { path: relPath, code, rawTokens: estimateTokens(code) };
365
367
  });
366
- const result = await packContext(fileInputs, { budget, hotspots });
368
+ const result = await packContext(fileInputs, { budget, hotspots, target });
369
+ if (target && !result.targetFile) {
370
+ console.log(` Warning: symbol "${target}" not found in any file. Showing general context.
371
+ `);
372
+ } else if (result.targetFile) {
373
+ console.log(` Target: ${result.targetFile} (contains ${target})`);
374
+ if (result.targetDowngraded) {
375
+ console.log(` Note: target file too large for raw mode \u2014 using L1 IR instead. Increase --budget for L3.`);
376
+ }
377
+ console.log();
378
+ }
379
+ const l3Entries = result.entries.filter((e) => e.layer === "L3");
367
380
  const l1Entries = result.entries.filter((e) => e.layer === "L1");
368
381
  const l0Entries = result.entries.filter((e) => e.layer === "L0");
382
+ if (l3Entries.length > 0) {
383
+ console.log(" == L3 (raw \u2014 target file) ==\n");
384
+ for (const entry of l3Entries) {
385
+ console.log(` [target] ${entry.path}`);
386
+ for (const line of entry.ir.split("\n")) {
387
+ console.log(` ${line}`);
388
+ }
389
+ console.log();
390
+ }
391
+ }
369
392
  if (l1Entries.length > 0) {
370
393
  console.log(" == L1 (detailed) ==\n");
371
394
  for (const entry of l1Entries) {
372
- const label = hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
395
+ const label = entry.isTarget ? "target" : hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
373
396
  console.log(` [${label}] ${entry.path}`);
374
397
  for (const line of entry.ir.split("\n")) {
375
398
  console.log(` ${line}`);
@@ -386,8 +409,12 @@ async function runContext(projectPath, budget) {
386
409
  }
387
410
  console.log();
388
411
  }
412
+ const parts = [];
413
+ if (result.filesAtL3 > 0) parts.push(`${result.filesAtL3} at L3 (raw)`);
414
+ if (result.filesAtL1 > 0) parts.push(`${result.filesAtL1} at L1`);
415
+ if (result.filesAtL0 > 0) parts.push(`${result.filesAtL0} at L0`);
389
416
  console.log(` Budget: ${result.totalTokens}/${result.budget} tokens`);
390
- console.log(` Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
417
+ console.log(` Files: ${parts.join(", ")}`);
391
418
  }
392
419
 
393
420
  // src/index.ts
@@ -430,24 +457,35 @@ switch (command) {
430
457
  break;
431
458
  }
432
459
  case "context": {
433
- const projectPath = resolve(args[1] ?? ".");
434
- const budgetFlag = args.indexOf("--budget");
435
- const budget = budgetFlag !== -1 && args[budgetFlag + 1] ? parseInt(args[budgetFlag + 1], 10) : 4e3;
436
- await runContext(projectPath, budget);
460
+ let parseFlag = function(name) {
461
+ const equalsForm = args.find((a) => a.startsWith(`--${name}=`));
462
+ if (equalsForm) return equalsForm.slice(name.length + 3);
463
+ const idx = args.indexOf(`--${name}`);
464
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
465
+ return void 0;
466
+ };
467
+ parseFlag2 = parseFlag;
468
+ const projectPath = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
469
+ const budgetStr = parseFlag("budget");
470
+ const budget = budgetStr ? parseInt(budgetStr, 10) : 4e3;
471
+ const target = parseFlag("target");
472
+ await runContext(projectPath, budget, target);
437
473
  break;
438
474
  }
439
475
  case "version":
440
- console.log("composto v0.1.0");
476
+ console.log("composto v0.2.3");
441
477
  break;
442
478
  default:
443
- console.log("composto v0.1.0 \u2014 less tokens, more insight\n");
479
+ console.log("composto v0.2.3 \u2014 less tokens, more insight\n");
444
480
  console.log("Commands:");
445
- console.log(" scan [path] Scan codebase for issues");
446
- console.log(" trends [path] Analyze codebase health trends");
447
- console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
448
- console.log(" benchmark [path] Benchmark IR token savings");
449
- console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
450
- console.log(" context [path] --budget N Smart context within token budget");
451
- console.log(" version Show version");
481
+ console.log(" scan [path] Scan codebase for issues");
482
+ console.log(" trends [path] Analyze codebase health trends");
483
+ console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
484
+ console.log(" benchmark [path] Benchmark IR token savings");
485
+ console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
486
+ console.log(" context [path] --budget N Smart context within token budget");
487
+ console.log(" context [path] --target <symbol> Target file as raw, surrounding as IR");
488
+ console.log(" version Show version");
452
489
  break;
453
490
  }
491
+ var parseFlag2;
@@ -12,7 +12,7 @@ import {
12
12
  packContext,
13
13
  runDetector,
14
14
  summarize
15
- } from "../chunk-T2SFR7QS.js";
15
+ } from "../chunk-AARGW2GV.js";
16
16
 
17
17
  // src/mcp/server.ts
18
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -40,7 +40,7 @@ function collectFiles(dir, extensions) {
40
40
  }
41
41
  var server = new McpServer({
42
42
  name: "composto",
43
- version: "0.1.2"
43
+ version: "0.2.3"
44
44
  });
45
45
  server.tool(
46
46
  "composto_ir",
@@ -112,12 +112,13 @@ server.tool(
112
112
  );
113
113
  server.tool(
114
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.",
115
+ "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.",
116
116
  {
117
117
  path: z.string().default(".").describe("Directory to pack"),
118
- budget: z.number().default(4e3).describe("Maximum tokens to use")
118
+ budget: z.number().default(4e3).describe("Maximum tokens to use"),
119
+ target: z.string().optional().describe("Target symbol (function/class/variable name). Its file will be included as raw code for implementation tasks.")
119
120
  },
120
- async ({ path, budget }) => {
121
+ async ({ path, budget, target }) => {
121
122
  const projectPath = resolve(path);
122
123
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
123
124
  const config = loadConfig(projectPath);
@@ -131,11 +132,21 @@ server.tool(
131
132
  const relPath = relative(projectPath, file);
132
133
  return { path: relPath, code, rawTokens: estimateTokens(code) };
133
134
  });
134
- const result = await packContext(fileInputs, { budget, hotspots });
135
- const lines = [`Composto Context \u2014 ${result.totalTokens}/${result.budget} tokens
136
- `];
135
+ const result = await packContext(fileInputs, { budget, hotspots, target });
136
+ const lines = [`Composto Context \u2014 ${result.totalTokens}/${result.budget} tokens`];
137
+ if (target && result.targetFile) lines.push(`Target: ${target} in ${result.targetFile}`);
138
+ lines.push("");
139
+ const l3 = result.entries.filter((e) => e.layer === "L3");
137
140
  const l1 = result.entries.filter((e) => e.layer === "L1");
138
141
  const l0 = result.entries.filter((e) => e.layer === "L0");
142
+ if (l3.length > 0) {
143
+ lines.push("== L3 (raw \u2014 target file) ==\n");
144
+ for (const entry of l3) {
145
+ lines.push(`[target] ${entry.path}`);
146
+ lines.push(entry.ir);
147
+ lines.push("");
148
+ }
149
+ }
139
150
  if (l1.length > 0) {
140
151
  lines.push("== L1 (detailed) ==\n");
141
152
  for (const entry of l1) {
@@ -151,8 +162,12 @@ server.tool(
151
162
  lines.push(entry.ir);
152
163
  }
153
164
  }
165
+ const parts = [];
166
+ if (result.filesAtL3 > 0) parts.push(`${result.filesAtL3} at L3`);
167
+ if (result.filesAtL1 > 0) parts.push(`${result.filesAtL1} at L1`);
168
+ if (result.filesAtL0 > 0) parts.push(`${result.filesAtL0} at L0`);
154
169
  lines.push(`
155
- Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
170
+ Files: ${parts.join(", ")}`);
156
171
  return {
157
172
  content: [{ type: "text", text: lines.join("\n") }]
158
173
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {