composto-ai 0.2.0 → 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-MFNGSHTZ.js → chunk-AARGW2GV.js} +399 -31
- package/dist/index.js +62 -24
- package/dist/mcp/server.js +24 -9
- package/package.json +1 -1
|
@@ -499,36 +499,94 @@ function detectLanguage(filePath) {
|
|
|
499
499
|
|
|
500
500
|
// src/ir/ast-walker.ts
|
|
501
501
|
var TIER_MAP = {
|
|
502
|
-
// Tier 1 — structural declarations
|
|
502
|
+
// Tier 1 — structural declarations (JS/TS)
|
|
503
503
|
import_statement: "T1_KEEP",
|
|
504
504
|
function_declaration: "T1_KEEP",
|
|
505
505
|
class_declaration: "T1_KEEP",
|
|
506
506
|
interface_declaration: "T1_KEEP",
|
|
507
507
|
type_alias_declaration: "T1_KEEP",
|
|
508
508
|
enum_declaration: "T1_KEEP",
|
|
509
|
-
// Tier
|
|
509
|
+
// Tier 1 — Python
|
|
510
|
+
function_definition: "T1_KEEP",
|
|
511
|
+
class_definition: "T1_KEEP",
|
|
512
|
+
import_from_statement: "T1_KEEP",
|
|
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
|
|
519
|
+
// Tier 1 — Go
|
|
520
|
+
function_item: "T1_KEEP",
|
|
521
|
+
// Rust
|
|
522
|
+
method_declaration: "T1_KEEP",
|
|
523
|
+
// Go
|
|
524
|
+
type_declaration: "T1_KEEP",
|
|
525
|
+
// Go
|
|
526
|
+
import_declaration: "T1_KEEP",
|
|
527
|
+
// Go
|
|
528
|
+
use_declaration: "T1_KEEP",
|
|
529
|
+
// Rust
|
|
530
|
+
struct_item: "T1_KEEP",
|
|
531
|
+
// Rust
|
|
532
|
+
enum_item: "T1_KEEP",
|
|
533
|
+
// Rust
|
|
534
|
+
trait_item: "T1_KEEP",
|
|
535
|
+
// Rust
|
|
536
|
+
impl_item: "T1_KEEP",
|
|
537
|
+
// Rust
|
|
538
|
+
// Tier 2 — control flow (universal)
|
|
510
539
|
if_statement: "T2_CONTROL",
|
|
540
|
+
if_expression: "T2_CONTROL",
|
|
541
|
+
// Rust
|
|
511
542
|
else_clause: "WALK_ONLY",
|
|
543
|
+
elif_clause: "T2_CONTROL",
|
|
544
|
+
// Python
|
|
512
545
|
for_statement: "T2_CONTROL",
|
|
513
546
|
for_in_statement: "T2_CONTROL",
|
|
547
|
+
for_expression: "T2_CONTROL",
|
|
548
|
+
// Rust
|
|
514
549
|
while_statement: "T2_CONTROL",
|
|
515
550
|
do_statement: "T2_CONTROL",
|
|
516
551
|
switch_statement: "T2_CONTROL",
|
|
517
552
|
switch_case: "T2_CONTROL",
|
|
518
553
|
switch_default: "T2_CONTROL",
|
|
554
|
+
match_expression: "T2_CONTROL",
|
|
555
|
+
// Rust
|
|
519
556
|
return_statement: "T2_CONTROL",
|
|
557
|
+
return_expression: "T2_CONTROL",
|
|
558
|
+
// Rust
|
|
520
559
|
throw_statement: "T2_CONTROL",
|
|
560
|
+
raise_statement: "T2_CONTROL",
|
|
561
|
+
// Python
|
|
521
562
|
try_statement: "T2_CONTROL",
|
|
522
563
|
catch_clause: "T2_CONTROL",
|
|
564
|
+
except_clause: "T2_CONTROL",
|
|
565
|
+
// Python
|
|
566
|
+
with_statement: "T2_CONTROL",
|
|
567
|
+
// Python
|
|
568
|
+
defer_statement: "T2_CONTROL",
|
|
569
|
+
// Go
|
|
523
570
|
// Tier 3 — compressible expressions
|
|
524
571
|
lexical_declaration: "T3_COMPRESS",
|
|
525
572
|
expression_statement: "T3_COMPRESS",
|
|
526
|
-
|
|
573
|
+
assignment: "T3_COMPRESS",
|
|
574
|
+
// Python
|
|
575
|
+
short_var_declaration: "T3_COMPRESS",
|
|
576
|
+
// Go
|
|
577
|
+
// Walk-only — containers
|
|
527
578
|
program: "WALK_ONLY",
|
|
579
|
+
module: "WALK_ONLY",
|
|
580
|
+
// Python
|
|
528
581
|
statement_block: "WALK_ONLY",
|
|
582
|
+
block: "WALK_ONLY",
|
|
583
|
+
// Python/Go/Rust
|
|
529
584
|
class_body: "WALK_ONLY",
|
|
585
|
+
// JS/TS
|
|
530
586
|
switch_body: "WALK_ONLY",
|
|
531
|
-
export_statement: "WALK_ONLY"
|
|
587
|
+
export_statement: "WALK_ONLY",
|
|
588
|
+
source_file: "WALK_ONLY"
|
|
589
|
+
// Go/Rust
|
|
532
590
|
};
|
|
533
591
|
function tierOf(nodeType) {
|
|
534
592
|
return TIER_MAP[nodeType] ?? "T4_DROP";
|
|
@@ -553,6 +611,49 @@ function isExported(node) {
|
|
|
553
611
|
function isAsync(node) {
|
|
554
612
|
return node.text.trimStart().startsWith("async");
|
|
555
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
|
+
}
|
|
556
657
|
function extractCondition(node) {
|
|
557
658
|
const condNode = node.childForFieldName("condition") ?? (() => {
|
|
558
659
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -644,6 +745,35 @@ function emitTier2(node) {
|
|
|
644
745
|
const paramText = param ? param.text : "...";
|
|
645
746
|
return `CATCH:${paramText}`;
|
|
646
747
|
}
|
|
748
|
+
// Python
|
|
749
|
+
case "raise_statement": {
|
|
750
|
+
const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
|
|
751
|
+
return `RAISE:${val.length > 50 ? val.slice(0, 47) + "..." : val}`;
|
|
752
|
+
}
|
|
753
|
+
case "except_clause":
|
|
754
|
+
return "EXCEPT";
|
|
755
|
+
case "elif_clause": {
|
|
756
|
+
const cond = extractCondition(node);
|
|
757
|
+
return `ELIF:${cond}`;
|
|
758
|
+
}
|
|
759
|
+
case "with_statement":
|
|
760
|
+
return "WITH";
|
|
761
|
+
// Rust
|
|
762
|
+
case "if_expression": {
|
|
763
|
+
const cond = extractCondition(node);
|
|
764
|
+
return `IF:${cond}`;
|
|
765
|
+
}
|
|
766
|
+
case "for_expression":
|
|
767
|
+
return "LOOP";
|
|
768
|
+
case "match_expression":
|
|
769
|
+
return "MATCH";
|
|
770
|
+
case "return_expression": {
|
|
771
|
+
const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
|
|
772
|
+
return `RET ${val.length > 60 ? val.slice(0, 57) + "..." : val}`.trimEnd();
|
|
773
|
+
}
|
|
774
|
+
// Go
|
|
775
|
+
case "defer_statement":
|
|
776
|
+
return "DEFER";
|
|
647
777
|
default:
|
|
648
778
|
return null;
|
|
649
779
|
}
|
|
@@ -661,25 +791,103 @@ function emitTier1(node) {
|
|
|
661
791
|
const rawParams = node.childForFieldName("parameters")?.text ?? "()";
|
|
662
792
|
const params = collapseText(rawParams, 60);
|
|
663
793
|
const asyncPrefix = isAsync(node) ? "ASYNC " : "";
|
|
664
|
-
|
|
794
|
+
const doc = extractDocComment(node);
|
|
795
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
796
|
+
return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
|
|
665
797
|
}
|
|
666
798
|
case "class_declaration": {
|
|
667
799
|
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
668
800
|
const typeParams = getTypeParams(node);
|
|
669
|
-
|
|
801
|
+
const doc = extractDocComment(node);
|
|
802
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
803
|
+
return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
|
|
670
804
|
}
|
|
671
805
|
case "interface_declaration": {
|
|
672
806
|
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
673
807
|
const typeParams = getTypeParams(node);
|
|
674
|
-
|
|
808
|
+
const doc = extractDocComment(node);
|
|
809
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
810
|
+
return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
|
|
675
811
|
}
|
|
676
812
|
case "type_alias_declaration": {
|
|
677
813
|
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
678
|
-
|
|
814
|
+
const doc = extractDocComment(node);
|
|
815
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
816
|
+
return `${docPrefix}${outPrefix}TYPE:${name}`;
|
|
679
817
|
}
|
|
680
818
|
case "enum_declaration": {
|
|
681
819
|
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
682
|
-
|
|
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}`;
|
|
845
|
+
}
|
|
846
|
+
// Python
|
|
847
|
+
case "function_definition": {
|
|
848
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
849
|
+
const params = node.childForFieldName("parameters")?.text ?? "()";
|
|
850
|
+
const returnType = node.childForFieldName("return_type")?.text ?? "";
|
|
851
|
+
const rt = returnType ? ` -> ${returnType}` : "";
|
|
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}`;
|
|
856
|
+
}
|
|
857
|
+
case "class_definition": {
|
|
858
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
859
|
+
const superclass = node.childForFieldName("superclasses")?.text ?? "";
|
|
860
|
+
const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
|
|
861
|
+
const body = node.childForFieldName("body");
|
|
862
|
+
const doc = extractPythonDocstring(body);
|
|
863
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
864
|
+
return `${docPrefix}CLASS:${name}${sc}`;
|
|
865
|
+
}
|
|
866
|
+
case "import_from_statement": {
|
|
867
|
+
return `USE:${collapseText(node.text, 80)}`;
|
|
868
|
+
}
|
|
869
|
+
case "decorated_definition": {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
// Go
|
|
873
|
+
case "method_declaration":
|
|
874
|
+
case "type_declaration":
|
|
875
|
+
case "import_declaration": {
|
|
876
|
+
return `${collapseText(node.text, 80)}`;
|
|
877
|
+
}
|
|
878
|
+
// Rust
|
|
879
|
+
case "function_item": {
|
|
880
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
881
|
+
const params = node.childForFieldName("parameters")?.text ?? "()";
|
|
882
|
+
return `FN:${name}${collapseText(params, 60)}`;
|
|
883
|
+
}
|
|
884
|
+
case "struct_item":
|
|
885
|
+
case "enum_item":
|
|
886
|
+
case "trait_item":
|
|
887
|
+
case "impl_item":
|
|
888
|
+
case "use_declaration": {
|
|
889
|
+
const firstLine = node.text.split("\n")[0];
|
|
890
|
+
return collapseText(firstLine, 80);
|
|
683
891
|
}
|
|
684
892
|
default:
|
|
685
893
|
return null;
|
|
@@ -726,6 +934,17 @@ function emitTier3(node) {
|
|
|
726
934
|
return null;
|
|
727
935
|
}
|
|
728
936
|
if (expr.type === "call_expression") {
|
|
937
|
+
const callee = expr.child(0)?.text ?? "";
|
|
938
|
+
if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
|
|
939
|
+
const args = expr.child(1);
|
|
940
|
+
if (args && args.childCount >= 4) {
|
|
941
|
+
const child = args.child(1)?.text ?? "?";
|
|
942
|
+
const parent = args.child(3)?.text ?? "?";
|
|
943
|
+
const shortChild = child.length > 30 ? child.slice(0, 27) + "..." : child;
|
|
944
|
+
const shortParent = parent.length > 30 ? parent.slice(0, 27) + "..." : parent;
|
|
945
|
+
return `EXTENDS:${shortChild} < ${shortParent}`;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
729
948
|
return null;
|
|
730
949
|
}
|
|
731
950
|
return null;
|
|
@@ -743,7 +962,9 @@ function walkNode(node, depth, lines) {
|
|
|
743
962
|
for (let i = 0; i < node.childCount; i++) {
|
|
744
963
|
const child = node.child(i);
|
|
745
964
|
const childType = child.type;
|
|
746
|
-
if (childType === "statement_block" || childType === "class_body"
|
|
965
|
+
if (childType === "statement_block" || childType === "class_body" || childType === "block" || // Python/Go/Rust
|
|
966
|
+
childType === "body" || // Python class/function body
|
|
967
|
+
childType === "declaration_list") {
|
|
747
968
|
walkNode(child, depth + 1, lines);
|
|
748
969
|
}
|
|
749
970
|
}
|
|
@@ -834,7 +1055,7 @@ async function astWalkIR(code, filePath) {
|
|
|
834
1055
|
const lines = [];
|
|
835
1056
|
walkNode(root, 0, lines);
|
|
836
1057
|
if (lines.length === 0) return null;
|
|
837
|
-
const
|
|
1058
|
+
const pass1 = [];
|
|
838
1059
|
let useBlock = [];
|
|
839
1060
|
for (const line of lines) {
|
|
840
1061
|
if (line.startsWith("USE:")) {
|
|
@@ -843,20 +1064,45 @@ async function astWalkIR(code, filePath) {
|
|
|
843
1064
|
} else {
|
|
844
1065
|
if (useBlock.length > 0) {
|
|
845
1066
|
if (useBlock.length <= 3) {
|
|
846
|
-
for (const mod of useBlock)
|
|
1067
|
+
for (const mod of useBlock) pass1.push(`USE:${mod}`);
|
|
847
1068
|
} else {
|
|
848
|
-
|
|
1069
|
+
pass1.push(`USE:[${useBlock.join(", ")}]`);
|
|
849
1070
|
}
|
|
850
1071
|
useBlock = [];
|
|
851
1072
|
}
|
|
852
|
-
|
|
1073
|
+
pass1.push(line);
|
|
853
1074
|
}
|
|
854
1075
|
}
|
|
855
1076
|
if (useBlock.length > 0) {
|
|
856
1077
|
if (useBlock.length <= 3) {
|
|
857
|
-
for (const mod of useBlock)
|
|
1078
|
+
for (const mod of useBlock) pass1.push(`USE:${mod}`);
|
|
858
1079
|
} else {
|
|
859
|
-
|
|
1080
|
+
pass1.push(`USE:[${useBlock.join(", ")}]`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const merged = [];
|
|
1084
|
+
let guardBlock = [];
|
|
1085
|
+
for (const line of pass1) {
|
|
1086
|
+
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
|
|
1087
|
+
if (guardMatch) {
|
|
1088
|
+
guardBlock.push(guardMatch[2].trim());
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (guardBlock.length > 0) {
|
|
1092
|
+
if (guardBlock.length < 3) {
|
|
1093
|
+
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1094
|
+
} else {
|
|
1095
|
+
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
1096
|
+
}
|
|
1097
|
+
guardBlock = [];
|
|
1098
|
+
}
|
|
1099
|
+
merged.push(line);
|
|
1100
|
+
}
|
|
1101
|
+
if (guardBlock.length > 0) {
|
|
1102
|
+
if (guardBlock.length < 3) {
|
|
1103
|
+
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1104
|
+
} else {
|
|
1105
|
+
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
860
1106
|
}
|
|
861
1107
|
}
|
|
862
1108
|
return merged.join("\n");
|
|
@@ -879,10 +1125,11 @@ ${declarations.join("\n")}`;
|
|
|
879
1125
|
}
|
|
880
1126
|
async function generateL1(code, filePath, health) {
|
|
881
1127
|
const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
|
|
1128
|
+
const result = ir.length < code.length ? ir : code;
|
|
882
1129
|
if (health) {
|
|
883
|
-
return annotateIR(
|
|
1130
|
+
return annotateIR(result, health);
|
|
884
1131
|
}
|
|
885
|
-
return
|
|
1132
|
+
return result;
|
|
886
1133
|
}
|
|
887
1134
|
function generateL2(delta, health) {
|
|
888
1135
|
const parts = [`FILE: ${delta.file}`];
|
|
@@ -1098,43 +1345,161 @@ function summarize(results) {
|
|
|
1098
1345
|
}
|
|
1099
1346
|
|
|
1100
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
|
+
}
|
|
1101
1404
|
async function packContext(files, options) {
|
|
1102
|
-
const { budget, hotspots } = options;
|
|
1405
|
+
const { budget, hotspots, target } = options;
|
|
1103
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
|
+
}
|
|
1104
1415
|
const entries = [];
|
|
1105
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
|
+
}
|
|
1106
1446
|
for (const file of files) {
|
|
1447
|
+
if (file.path === targetPath) continue;
|
|
1107
1448
|
const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
|
|
1108
1449
|
const l0Tokens = estimateTokens(l0);
|
|
1109
1450
|
entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
|
|
1110
1451
|
totalTokens += l0Tokens;
|
|
1111
1452
|
}
|
|
1112
1453
|
if (totalTokens > budget) {
|
|
1113
|
-
const truncated =
|
|
1114
|
-
let used = 0;
|
|
1454
|
+
const truncated = entries.filter((e) => e.isTarget);
|
|
1455
|
+
let used = truncated.reduce((s, e) => s + e.tokens, 0);
|
|
1115
1456
|
for (const entry of entries) {
|
|
1457
|
+
if (entry.isTarget) continue;
|
|
1116
1458
|
if (used + entry.tokens <= budget) {
|
|
1117
1459
|
truncated.push(entry);
|
|
1118
1460
|
used += entry.tokens;
|
|
1119
1461
|
}
|
|
1120
1462
|
}
|
|
1121
|
-
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
|
+
};
|
|
1122
1473
|
}
|
|
1123
|
-
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;
|
|
1124
1484
|
if (a.isHotspot && !b.isHotspot) return -1;
|
|
1125
1485
|
if (!a.isHotspot && b.isHotspot) return 1;
|
|
1126
1486
|
return b.rawTokens - a.rawTokens;
|
|
1127
1487
|
});
|
|
1128
1488
|
let filesAtL1 = 0;
|
|
1129
1489
|
for (const item of upgradeOrder) {
|
|
1130
|
-
const file = files
|
|
1490
|
+
const file = files.find((f) => f.path === item.path);
|
|
1131
1491
|
const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
|
|
1132
1492
|
const l1Tokens = estimateTokens(l1);
|
|
1133
|
-
const
|
|
1134
|
-
const
|
|
1135
|
-
if (totalTokens +
|
|
1136
|
-
entries[item.index] = {
|
|
1137
|
-
|
|
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;
|
|
1138
1503
|
filesAtL1++;
|
|
1139
1504
|
}
|
|
1140
1505
|
}
|
|
@@ -1142,8 +1507,11 @@ async function packContext(files, options) {
|
|
|
1142
1507
|
entries,
|
|
1143
1508
|
totalTokens,
|
|
1144
1509
|
budget,
|
|
1145
|
-
filesAtL0: entries.
|
|
1146
|
-
filesAtL1
|
|
1510
|
+
filesAtL0: entries.filter((e) => e.layer === "L0").length,
|
|
1511
|
+
filesAtL1,
|
|
1512
|
+
filesAtL3,
|
|
1513
|
+
targetFile: targetPath ?? void 0,
|
|
1514
|
+
targetDowngraded
|
|
1147
1515
|
};
|
|
1148
1516
|
}
|
|
1149
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
|
};
|