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.
- package/dist/{chunk-T2SFR7QS.js → chunk-AARGW2GV.js} +227 -20
- package/dist/index.js +62 -24
- package/dist/mcp/server.js +24 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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) => ({
|
|
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
|
|
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
|
|
1295
|
-
const
|
|
1296
|
-
if (totalTokens +
|
|
1297
|
-
entries[item.index] = {
|
|
1298
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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.
|
|
476
|
+
console.log("composto v0.2.3");
|
|
441
477
|
break;
|
|
442
478
|
default:
|
|
443
|
-
console.log("composto v0.
|
|
479
|
+
console.log("composto v0.2.3 \u2014 less tokens, more insight\n");
|
|
444
480
|
console.log("Commands:");
|
|
445
|
-
console.log(" scan [path]
|
|
446
|
-
console.log(" trends [path]
|
|
447
|
-
console.log(" ir <file> [layer]
|
|
448
|
-
console.log(" benchmark [path]
|
|
449
|
-
console.log(" benchmark-quality <file>
|
|
450
|
-
console.log(" context [path] --budget N
|
|
451
|
-
console.log("
|
|
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;
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
packContext,
|
|
13
13
|
runDetector,
|
|
14
14
|
summarize
|
|
15
|
-
} from "../chunk-
|
|
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.
|
|
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.
|
|
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: ${
|
|
170
|
+
Files: ${parts.join(", ")}`);
|
|
156
171
|
return {
|
|
157
172
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
158
173
|
};
|