vibecop 0.4.0 → 0.4.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/README.md +21 -2
- package/dist/cli.js +670 -128
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -64,7 +64,26 @@ Agent writes code → vibecop hook fires → Findings? Agent fixes → Clean? Co
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Four tools: `vibecop_scan`, `vibecop_check`, `vibecop_explain`, `vibecop_context_benchmark`.
|
|
68
|
+
|
|
69
|
+
## Context Optimization
|
|
70
|
+
|
|
71
|
+
Reduce token consumption by ~35% on Read tool re-reads. When Claude Code reads a file it's already seen, vibecop intercepts the Read and serves a compact AST skeleton instead of the full file. Unchanged files get smart-limited to 30 lines + skeleton context.
|
|
72
|
+
|
|
73
|
+
**Requires bun runtime** (uses `bun:sqlite` for zero-dependency caching).
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
vibecop context benchmark # See projected savings for your project
|
|
77
|
+
vibecop init --context # Configure hooks (Claude Code only)
|
|
78
|
+
vibecop context stats # View actual token savings after sessions
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
How it works:
|
|
82
|
+
1. **First read** — full file passes through, skeleton is cached
|
|
83
|
+
2. **Re-read (unchanged)** — smart-limited to 30 lines + skeleton injected via `additionalContext`
|
|
84
|
+
3. **Re-read (changed)** — full file passes through with "file changed" note
|
|
85
|
+
|
|
86
|
+
Skeletons include imports, function signatures, class outlines, and exports — enough for Claude to understand file structure without re-reading the full implementation.
|
|
68
87
|
|
|
69
88
|
## Benchmarks
|
|
70
89
|
|
|
@@ -114,7 +133,7 @@ Catches: god functions, N+1 queries, unsafe shell exec, SQL injection, hardcoded
|
|
|
114
133
|
- [x] **Phase 2.5**: Agent integration (7 tools), 6 LLM/agent detectors, `vibecop init`
|
|
115
134
|
- [x] **Phase 3**: Test quality detectors, custom YAML rules (28 → 35)
|
|
116
135
|
- [x] **Phase 3.5**: MCP server with scan/check/explain tools
|
|
117
|
-
- [
|
|
136
|
+
- [x] **Phase 4**: Context optimization (Read tool interception, AST skeleton caching)
|
|
118
137
|
- [ ] **Phase 5**: VS Code extension, cross-file analysis
|
|
119
138
|
|
|
120
139
|
## Links
|
package/dist/cli.js
CHANGED
|
@@ -13785,22 +13785,210 @@ var init_insecure_defaults = __esm(() => {
|
|
|
13785
13785
|
};
|
|
13786
13786
|
});
|
|
13787
13787
|
|
|
13788
|
-
// src/
|
|
13789
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
13790
|
-
import { basename, dirname, join as join2 } from "node:path";
|
|
13788
|
+
// src/ast-utils.ts
|
|
13791
13789
|
function extractJsPackageName(specifier) {
|
|
13792
13790
|
if (!specifier)
|
|
13793
13791
|
return null;
|
|
13794
13792
|
if (specifier.startsWith("@")) {
|
|
13795
13793
|
const parts = specifier.split("/");
|
|
13796
|
-
|
|
13797
|
-
return `${parts[0]}/${parts[1]}`;
|
|
13798
|
-
}
|
|
13799
|
-
return null;
|
|
13794
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
13800
13795
|
}
|
|
13801
13796
|
const slashIdx = specifier.indexOf("/");
|
|
13802
13797
|
return slashIdx === -1 ? specifier : specifier.slice(0, slashIdx);
|
|
13803
13798
|
}
|
|
13799
|
+
function findImports(root, language) {
|
|
13800
|
+
if (language === "python")
|
|
13801
|
+
return findPythonImports(root);
|
|
13802
|
+
return findJsImports(root);
|
|
13803
|
+
}
|
|
13804
|
+
function findJsImports(root) {
|
|
13805
|
+
const results = [];
|
|
13806
|
+
const nodes = root.findAll({ rule: { kind: "import_statement" } });
|
|
13807
|
+
for (const node of nodes) {
|
|
13808
|
+
const sourceNode = node.children().find((ch) => ch.kind() === "string");
|
|
13809
|
+
if (!sourceNode)
|
|
13810
|
+
continue;
|
|
13811
|
+
const source = sourceNode.text().slice(1, -1);
|
|
13812
|
+
results.push({ node, source, text: node.text() });
|
|
13813
|
+
}
|
|
13814
|
+
return results;
|
|
13815
|
+
}
|
|
13816
|
+
function findPythonImports(root) {
|
|
13817
|
+
const results = [];
|
|
13818
|
+
for (const node of root.findAll({ rule: { kind: "import_statement" } })) {
|
|
13819
|
+
const nameNode = node.children().find((ch) => ch.kind() === "dotted_name" || ch.kind() === "aliased_import");
|
|
13820
|
+
if (!nameNode)
|
|
13821
|
+
continue;
|
|
13822
|
+
let source;
|
|
13823
|
+
if (nameNode.kind() === "aliased_import") {
|
|
13824
|
+
const dotted = nameNode.children().find((ch) => ch.kind() === "dotted_name");
|
|
13825
|
+
source = dotted ? dotted.text() : nameNode.text();
|
|
13826
|
+
} else {
|
|
13827
|
+
source = nameNode.text();
|
|
13828
|
+
}
|
|
13829
|
+
results.push({ node, source, text: node.text() });
|
|
13830
|
+
}
|
|
13831
|
+
for (const node of root.findAll({ rule: { kind: "import_from_statement" } })) {
|
|
13832
|
+
const nameNode = node.children().find((ch) => ch.kind() === "dotted_name");
|
|
13833
|
+
if (!nameNode)
|
|
13834
|
+
continue;
|
|
13835
|
+
results.push({ node, source: nameNode.text(), text: node.text() });
|
|
13836
|
+
}
|
|
13837
|
+
return results;
|
|
13838
|
+
}
|
|
13839
|
+
function findFunctions(root, language) {
|
|
13840
|
+
if (language === "python")
|
|
13841
|
+
return findPythonFunctions(root);
|
|
13842
|
+
return findJsFunctions(root);
|
|
13843
|
+
}
|
|
13844
|
+
function findJsFunctions(root) {
|
|
13845
|
+
const results = [];
|
|
13846
|
+
for (const kind of ["function_declaration", "method_definition", "arrow_function"]) {
|
|
13847
|
+
for (const node of root.findAll({ rule: { kind } })) {
|
|
13848
|
+
const name = getJsFunctionName(node);
|
|
13849
|
+
const params = countJsParams(node);
|
|
13850
|
+
const body = node.children().find((c) => c.kind() === "statement_block") ?? null;
|
|
13851
|
+
results.push({ node, name, params, body, kind });
|
|
13852
|
+
}
|
|
13853
|
+
}
|
|
13854
|
+
return results;
|
|
13855
|
+
}
|
|
13856
|
+
function getJsFunctionName(node) {
|
|
13857
|
+
const kind = node.kind();
|
|
13858
|
+
if (kind === "function_declaration") {
|
|
13859
|
+
return node.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<anonymous>";
|
|
13860
|
+
}
|
|
13861
|
+
if (kind === "method_definition") {
|
|
13862
|
+
const nameNode = node.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "identifier");
|
|
13863
|
+
return nameNode?.text() ?? "<anonymous>";
|
|
13864
|
+
}
|
|
13865
|
+
if (kind === "arrow_function") {
|
|
13866
|
+
const parent = node.parent();
|
|
13867
|
+
if (parent?.kind() === "variable_declarator") {
|
|
13868
|
+
return parent.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<anonymous>";
|
|
13869
|
+
}
|
|
13870
|
+
if (parent?.kind() === "pair") {
|
|
13871
|
+
const nameNode = parent.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string");
|
|
13872
|
+
return nameNode?.text() ?? "<anonymous>";
|
|
13873
|
+
}
|
|
13874
|
+
return "<anonymous>";
|
|
13875
|
+
}
|
|
13876
|
+
return "<anonymous>";
|
|
13877
|
+
}
|
|
13878
|
+
function countJsParams(node) {
|
|
13879
|
+
const params = node.children().find((ch) => ch.kind() === "formal_parameters");
|
|
13880
|
+
if (!params)
|
|
13881
|
+
return 0;
|
|
13882
|
+
return params.children().filter((ch) => {
|
|
13883
|
+
const k = ch.kind();
|
|
13884
|
+
return k !== "(" && k !== ")" && k !== ",";
|
|
13885
|
+
}).length;
|
|
13886
|
+
}
|
|
13887
|
+
function findPythonFunctions(root) {
|
|
13888
|
+
const results = [];
|
|
13889
|
+
for (const node of root.findAll({ rule: { kind: "function_definition" } })) {
|
|
13890
|
+
const nameNode = node.children().find((ch) => ch.kind() === "identifier");
|
|
13891
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
13892
|
+
const body = node.children().find((ch) => ch.kind() === "block") ?? null;
|
|
13893
|
+
const params = countPyParams(node);
|
|
13894
|
+
results.push({ node, name, params, body, kind: "function_definition" });
|
|
13895
|
+
}
|
|
13896
|
+
return results;
|
|
13897
|
+
}
|
|
13898
|
+
function countPyParams(node) {
|
|
13899
|
+
const params = node.children().find((ch) => ch.kind() === "parameters");
|
|
13900
|
+
if (!params)
|
|
13901
|
+
return 0;
|
|
13902
|
+
return params.children().filter((ch) => {
|
|
13903
|
+
const k = ch.kind();
|
|
13904
|
+
if (k === "(" || k === ")" || k === ",")
|
|
13905
|
+
return false;
|
|
13906
|
+
const text = ch.text().split(":")[0].split("=")[0].trim();
|
|
13907
|
+
return text !== "self" && text !== "cls";
|
|
13908
|
+
}).length;
|
|
13909
|
+
}
|
|
13910
|
+
function findClasses(root, language) {
|
|
13911
|
+
if (language === "python")
|
|
13912
|
+
return findPythonClasses(root);
|
|
13913
|
+
return findJsClasses(root);
|
|
13914
|
+
}
|
|
13915
|
+
function findJsClasses(root) {
|
|
13916
|
+
const results = [];
|
|
13917
|
+
for (const node of root.findAll({ rule: { kind: "class_declaration" } })) {
|
|
13918
|
+
const nameNode = node.children().find((ch) => ch.kind() === "type_identifier" || ch.kind() === "identifier");
|
|
13919
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
13920
|
+
const methods = [];
|
|
13921
|
+
const classBody = node.children().find((ch) => ch.kind() === "class_body");
|
|
13922
|
+
if (classBody) {
|
|
13923
|
+
for (const member of classBody.findAll({ rule: { kind: "method_definition" } })) {
|
|
13924
|
+
const methodName = member.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "identifier");
|
|
13925
|
+
if (methodName)
|
|
13926
|
+
methods.push(methodName.text());
|
|
13927
|
+
}
|
|
13928
|
+
}
|
|
13929
|
+
results.push({ node, name, methods });
|
|
13930
|
+
}
|
|
13931
|
+
return results;
|
|
13932
|
+
}
|
|
13933
|
+
function findPythonClasses(root) {
|
|
13934
|
+
const results = [];
|
|
13935
|
+
for (const node of root.findAll({ rule: { kind: "class_definition" } })) {
|
|
13936
|
+
const nameNode = node.children().find((ch) => ch.kind() === "identifier");
|
|
13937
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
13938
|
+
const methods = [];
|
|
13939
|
+
for (const method of node.findAll({ rule: { kind: "function_definition" } })) {
|
|
13940
|
+
const methodName = method.children().find((ch) => ch.kind() === "identifier");
|
|
13941
|
+
if (methodName)
|
|
13942
|
+
methods.push(methodName.text());
|
|
13943
|
+
}
|
|
13944
|
+
results.push({ node, name, methods });
|
|
13945
|
+
}
|
|
13946
|
+
return results;
|
|
13947
|
+
}
|
|
13948
|
+
function findExports(root, language) {
|
|
13949
|
+
if (language === "python")
|
|
13950
|
+
return [];
|
|
13951
|
+
const results = [];
|
|
13952
|
+
for (const node of root.findAll({ rule: { kind: "export_statement" } })) {
|
|
13953
|
+
const children = node.children();
|
|
13954
|
+
const hasDefault = children.some((ch) => ch.kind() === "default");
|
|
13955
|
+
if (hasDefault) {
|
|
13956
|
+
results.push({ node, name: "default", kind: "default" });
|
|
13957
|
+
continue;
|
|
13958
|
+
}
|
|
13959
|
+
const funcDecl = children.find((ch) => ch.kind() === "function_declaration");
|
|
13960
|
+
if (funcDecl) {
|
|
13961
|
+
const name = funcDecl.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<unknown>";
|
|
13962
|
+
results.push({ node, name, kind: "function" });
|
|
13963
|
+
continue;
|
|
13964
|
+
}
|
|
13965
|
+
const classDecl = children.find((ch) => ch.kind() === "class_declaration");
|
|
13966
|
+
if (classDecl) {
|
|
13967
|
+
const nameNode = classDecl.children().find((ch) => ch.kind() === "type_identifier" || ch.kind() === "identifier");
|
|
13968
|
+
results.push({ node, name: nameNode?.text() ?? "<unknown>", kind: "class" });
|
|
13969
|
+
continue;
|
|
13970
|
+
}
|
|
13971
|
+
const typeDecl = children.find((ch) => ch.kind() === "type_alias_declaration" || ch.kind() === "interface_declaration");
|
|
13972
|
+
if (typeDecl) {
|
|
13973
|
+
const name = typeDecl.children().find((ch) => ch.kind() === "type_identifier")?.text() ?? "<unknown>";
|
|
13974
|
+
results.push({ node, name, kind: "type" });
|
|
13975
|
+
continue;
|
|
13976
|
+
}
|
|
13977
|
+
const lexDecl = children.find((ch) => ch.kind() === "lexical_declaration");
|
|
13978
|
+
if (lexDecl) {
|
|
13979
|
+
const declarator = lexDecl.children().find((ch) => ch.kind() === "variable_declarator");
|
|
13980
|
+
const name = declarator?.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<unknown>";
|
|
13981
|
+
results.push({ node, name, kind: "variable" });
|
|
13982
|
+
continue;
|
|
13983
|
+
}
|
|
13984
|
+
results.push({ node, name: "<unknown>", kind: "variable" });
|
|
13985
|
+
}
|
|
13986
|
+
return results;
|
|
13987
|
+
}
|
|
13988
|
+
|
|
13989
|
+
// src/detectors/undeclared-import.ts
|
|
13990
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
13991
|
+
import { basename, dirname, join as join2 } from "node:path";
|
|
13804
13992
|
function isRelativeImport(specifier) {
|
|
13805
13993
|
return specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..";
|
|
13806
13994
|
}
|
|
@@ -15622,8 +15810,7 @@ function detect6(ctx) {
|
|
|
15622
15810
|
const useReducerCount = (source.match(/\buseReducer\s*[<(]/g) || []).length;
|
|
15623
15811
|
const totalHooks = useStateCount + useEffectCount + useRefCount + useMemoCount + useCallbackCount + useReducerCount;
|
|
15624
15812
|
const root = ctx.root.root();
|
|
15625
|
-
const
|
|
15626
|
-
const importCount = importStatements.length;
|
|
15813
|
+
const importCount = findImports(root, ctx.file.language).length;
|
|
15627
15814
|
const lineCount = source.split(`
|
|
15628
15815
|
`).length;
|
|
15629
15816
|
const violations = [];
|
|
@@ -15699,52 +15886,6 @@ function countComplexity(node, isPython) {
|
|
|
15699
15886
|
}
|
|
15700
15887
|
return count;
|
|
15701
15888
|
}
|
|
15702
|
-
function countJsParams(funcNode) {
|
|
15703
|
-
const params = funcNode.children().find((ch) => ch.kind() === "formal_parameters");
|
|
15704
|
-
if (!params)
|
|
15705
|
-
return 0;
|
|
15706
|
-
return params.children().filter((ch) => {
|
|
15707
|
-
const k = ch.kind();
|
|
15708
|
-
return k !== "(" && k !== ")" && k !== ",";
|
|
15709
|
-
}).length;
|
|
15710
|
-
}
|
|
15711
|
-
function countPyParams(funcNode) {
|
|
15712
|
-
const params = funcNode.children().find((ch) => ch.kind() === "parameters");
|
|
15713
|
-
if (!params)
|
|
15714
|
-
return 0;
|
|
15715
|
-
const paramNodes = params.children().filter((ch) => {
|
|
15716
|
-
const k = ch.kind();
|
|
15717
|
-
return k !== "(" && k !== ")" && k !== ",";
|
|
15718
|
-
});
|
|
15719
|
-
return paramNodes.filter((ch) => {
|
|
15720
|
-
const text = ch.text().split(":")[0].split("=")[0].trim();
|
|
15721
|
-
return text !== "self" && text !== "cls";
|
|
15722
|
-
}).length;
|
|
15723
|
-
}
|
|
15724
|
-
function getJsFunctionName(node) {
|
|
15725
|
-
const kind = node.kind();
|
|
15726
|
-
if (kind === "function_declaration") {
|
|
15727
|
-
const nameNode = node.children().find((ch) => ch.kind() === "identifier");
|
|
15728
|
-
return nameNode?.text() ?? "<anonymous>";
|
|
15729
|
-
}
|
|
15730
|
-
if (kind === "method_definition") {
|
|
15731
|
-
const nameNode = node.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "identifier");
|
|
15732
|
-
return nameNode?.text() ?? "<anonymous>";
|
|
15733
|
-
}
|
|
15734
|
-
if (kind === "arrow_function") {
|
|
15735
|
-
const parent = node.parent();
|
|
15736
|
-
if (parent?.kind() === "variable_declarator") {
|
|
15737
|
-
const nameNode = parent.children().find((ch) => ch.kind() === "identifier");
|
|
15738
|
-
return nameNode?.text() ?? "<anonymous>";
|
|
15739
|
-
}
|
|
15740
|
-
if (parent?.kind() === "pair") {
|
|
15741
|
-
const nameNode = parent.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string");
|
|
15742
|
-
return nameNode?.text() ?? "<anonymous>";
|
|
15743
|
-
}
|
|
15744
|
-
return "<anonymous>";
|
|
15745
|
-
}
|
|
15746
|
-
return "<anonymous>";
|
|
15747
|
-
}
|
|
15748
15889
|
function buildFinding(ctx, m, severity) {
|
|
15749
15890
|
return makeLineFinding("god-function", ctx, m.startLine, m.startColumn, `Function '${m.name}' is too complex (${m.lines} lines, cyclomatic complexity ${m.complexity}, ${m.params} params)`, severity, "Break this function into smaller, focused functions. Extract helper methods, use early returns, and reduce branching.", m.endLine, m.endColumn);
|
|
15750
15891
|
}
|
|
@@ -15754,49 +15895,47 @@ function detectJavaScript5(ctx) {
|
|
|
15754
15895
|
const maxLines = ctx.config?.maxLines ?? 50;
|
|
15755
15896
|
const maxComplexity = ctx.config?.maxComplexity ?? 15;
|
|
15756
15897
|
const maxParams = ctx.config?.maxParams ?? 5;
|
|
15757
|
-
const
|
|
15758
|
-
for (const
|
|
15759
|
-
const
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
if (
|
|
15764
|
-
if (lines <= 10)
|
|
15765
|
-
continue;
|
|
15766
|
-
const parent = node.parent();
|
|
15767
|
-
if (!parent)
|
|
15768
|
-
continue;
|
|
15769
|
-
const pk = parent.kind();
|
|
15770
|
-
if (pk !== "variable_declarator" && pk !== "pair" && pk !== "assignment_expression") {
|
|
15771
|
-
continue;
|
|
15772
|
-
}
|
|
15773
|
-
}
|
|
15774
|
-
const name = getJsFunctionName(node);
|
|
15775
|
-
const complexity = 1 + countComplexity(node, false);
|
|
15776
|
-
const params = countJsParams(node);
|
|
15777
|
-
const linesExceeded = lines > maxLines;
|
|
15778
|
-
const complexityExceeded = complexity > maxComplexity;
|
|
15779
|
-
const paramsExceeded = params > maxParams;
|
|
15780
|
-
if (!linesExceeded && !complexityExceeded && !paramsExceeded)
|
|
15898
|
+
const allFunctions = findFunctions(root, ctx.file.language);
|
|
15899
|
+
for (const fn of allFunctions) {
|
|
15900
|
+
const node = fn.node;
|
|
15901
|
+
const range = node.range();
|
|
15902
|
+
const lines = range.end.line - range.start.line + 1;
|
|
15903
|
+
if (fn.kind === "arrow_function") {
|
|
15904
|
+
if (lines <= 10)
|
|
15781
15905
|
continue;
|
|
15782
|
-
|
|
15906
|
+
const parent = node.parent();
|
|
15907
|
+
if (!parent)
|
|
15908
|
+
continue;
|
|
15909
|
+
const pk = parent.kind();
|
|
15910
|
+
if (pk !== "variable_declarator" && pk !== "pair" && pk !== "assignment_expression") {
|
|
15783
15911
|
continue;
|
|
15784
|
-
let severity = "warning";
|
|
15785
|
-
if (lines > 100 || complexity > 20) {
|
|
15786
|
-
severity = "error";
|
|
15787
15912
|
}
|
|
15788
|
-
const metrics = {
|
|
15789
|
-
name,
|
|
15790
|
-
lines,
|
|
15791
|
-
complexity,
|
|
15792
|
-
params,
|
|
15793
|
-
startLine: range.start.line + 1,
|
|
15794
|
-
startColumn: range.start.column + 1,
|
|
15795
|
-
endLine: range.end.line + 1,
|
|
15796
|
-
endColumn: range.end.column + 1
|
|
15797
|
-
};
|
|
15798
|
-
findings.push(buildFinding(ctx, metrics, severity));
|
|
15799
15913
|
}
|
|
15914
|
+
const name = fn.name;
|
|
15915
|
+
const complexity = 1 + countComplexity(node, false);
|
|
15916
|
+
const params = fn.params;
|
|
15917
|
+
const linesExceeded = lines > maxLines;
|
|
15918
|
+
const complexityExceeded = complexity > maxComplexity;
|
|
15919
|
+
const paramsExceeded = params > maxParams;
|
|
15920
|
+
if (!linesExceeded && !complexityExceeded && !paramsExceeded)
|
|
15921
|
+
continue;
|
|
15922
|
+
if (!complexityExceeded && !paramsExceeded && complexity <= 2)
|
|
15923
|
+
continue;
|
|
15924
|
+
let severity = "warning";
|
|
15925
|
+
if (lines > 100 || complexity > 20) {
|
|
15926
|
+
severity = "error";
|
|
15927
|
+
}
|
|
15928
|
+
const metrics = {
|
|
15929
|
+
name,
|
|
15930
|
+
lines,
|
|
15931
|
+
complexity,
|
|
15932
|
+
params,
|
|
15933
|
+
startLine: range.start.line + 1,
|
|
15934
|
+
startColumn: range.start.column + 1,
|
|
15935
|
+
endLine: range.end.line + 1,
|
|
15936
|
+
endColumn: range.end.column + 1
|
|
15937
|
+
};
|
|
15938
|
+
findings.push(buildFinding(ctx, metrics, severity));
|
|
15800
15939
|
}
|
|
15801
15940
|
return findings;
|
|
15802
15941
|
}
|
|
@@ -15806,14 +15945,14 @@ function detectPython5(ctx) {
|
|
|
15806
15945
|
const maxLines = ctx.config?.maxLines ?? 50;
|
|
15807
15946
|
const maxComplexity = ctx.config?.maxComplexity ?? 15;
|
|
15808
15947
|
const maxParams = ctx.config?.maxParams ?? 5;
|
|
15809
|
-
const
|
|
15810
|
-
for (const
|
|
15948
|
+
const allFunctions = findFunctions(root, ctx.file.language);
|
|
15949
|
+
for (const fn of allFunctions) {
|
|
15950
|
+
const node = fn.node;
|
|
15811
15951
|
const range = node.range();
|
|
15812
15952
|
const lines = range.end.line - range.start.line + 1;
|
|
15813
|
-
const
|
|
15814
|
-
const name = nameNode?.text() ?? "<anonymous>";
|
|
15953
|
+
const name = fn.name;
|
|
15815
15954
|
const complexity = 1 + countComplexity(node, true);
|
|
15816
|
-
const params =
|
|
15955
|
+
const params = fn.params;
|
|
15817
15956
|
const linesExceeded = lines > maxLines;
|
|
15818
15957
|
const complexityExceeded = complexity > maxComplexity;
|
|
15819
15958
|
const paramsExceeded = params > maxParams;
|
|
@@ -16173,17 +16312,14 @@ function detect8(ctx) {
|
|
|
16173
16312
|
if (ctx.source.includes('"use server"') || ctx.source.includes("'use server'"))
|
|
16174
16313
|
return findings;
|
|
16175
16314
|
const root = ctx.root.root();
|
|
16176
|
-
const imports = root.
|
|
16315
|
+
const imports = findImports(root, ctx.file.language);
|
|
16177
16316
|
let hasUIImport = false;
|
|
16178
16317
|
let hasDBImport = false;
|
|
16179
16318
|
let hasServerImport = false;
|
|
16180
16319
|
let uiImportName = "";
|
|
16181
16320
|
let dbImportName = "";
|
|
16182
16321
|
for (const imp of imports) {
|
|
16183
|
-
const
|
|
16184
|
-
if (!sourceNode)
|
|
16185
|
-
continue;
|
|
16186
|
-
const specifier = sourceNode.text().slice(1, -1);
|
|
16322
|
+
const specifier = imp.source;
|
|
16187
16323
|
const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
16188
16324
|
if (UI_IMPORTS.has(pkgName)) {
|
|
16189
16325
|
hasUIImport = true;
|
|
@@ -22710,6 +22846,24 @@ __export(exports_init, {
|
|
|
22710
22846
|
import { execSync } from "node:child_process";
|
|
22711
22847
|
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
|
|
22712
22848
|
import { join as join7 } from "node:path";
|
|
22849
|
+
function resolveContextScript() {
|
|
22850
|
+
const fromDist = new URL("../dist/context.js", import.meta.url);
|
|
22851
|
+
try {
|
|
22852
|
+
const { existsSync: existsSync7 } = __require("node:fs");
|
|
22853
|
+
if (existsSync7(fromDist))
|
|
22854
|
+
return fromDist.pathname;
|
|
22855
|
+
} catch {}
|
|
22856
|
+
const { resolve: resolve4 } = __require("node:path");
|
|
22857
|
+
return resolve4("dist/context.js");
|
|
22858
|
+
}
|
|
22859
|
+
function contextCommands() {
|
|
22860
|
+
const script = resolveContextScript();
|
|
22861
|
+
return {
|
|
22862
|
+
pre: `bun ${script} --pre`,
|
|
22863
|
+
post: `bun ${script} --post`,
|
|
22864
|
+
compact: `bun ${script} --compact`
|
|
22865
|
+
};
|
|
22866
|
+
}
|
|
22713
22867
|
function detectTools(cwd) {
|
|
22714
22868
|
const tools = [];
|
|
22715
22869
|
tools.push({
|
|
@@ -22933,11 +23087,102 @@ After every code edit, run \`${SCAN_CMD}\` and fix any findings before proceedin
|
|
|
22933
23087
|
function padEnd(str, len) {
|
|
22934
23088
|
return str + " ".repeat(Math.max(0, len - str.length));
|
|
22935
23089
|
}
|
|
22936
|
-
|
|
23090
|
+
function isBunAvailable() {
|
|
23091
|
+
try {
|
|
23092
|
+
execSync("bun --version", { stdio: "pipe" });
|
|
23093
|
+
return true;
|
|
23094
|
+
} catch {
|
|
23095
|
+
return false;
|
|
23096
|
+
}
|
|
23097
|
+
}
|
|
23098
|
+
function generateContextHooks(root) {
|
|
23099
|
+
const generated = [];
|
|
23100
|
+
if (!existsSync6(join7(root, ".claude"))) {
|
|
23101
|
+
console.log(" Context optimization requires Claude Code (.claude/ directory).");
|
|
23102
|
+
return generated;
|
|
23103
|
+
}
|
|
23104
|
+
const settingsPath = join7(root, ".claude", "settings.json");
|
|
23105
|
+
let settings = {};
|
|
23106
|
+
if (existsSync6(settingsPath)) {
|
|
23107
|
+
try {
|
|
23108
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
23109
|
+
} catch {
|
|
23110
|
+
console.log(" Warning: Could not parse .claude/settings.json");
|
|
23111
|
+
return generated;
|
|
23112
|
+
}
|
|
23113
|
+
}
|
|
23114
|
+
const hooks = settings.hooks ?? {};
|
|
23115
|
+
const preHooks = hooks.PreToolUse ?? [];
|
|
23116
|
+
const hasExistingReadHook = preHooks.some((h) => h.matcher && /\bRead\b/.test(h.matcher));
|
|
23117
|
+
if (hasExistingReadHook) {
|
|
23118
|
+
console.log(" Warning: Existing PreToolUse Read hook detected.");
|
|
23119
|
+
console.log(" Context optimization uses updatedInput which is single-consumer.");
|
|
23120
|
+
console.log(" Skipping to avoid conflicts. See docs/agent-integration.md.");
|
|
23121
|
+
generated.push({
|
|
23122
|
+
path: ".claude/settings.json",
|
|
23123
|
+
description: "context hooks skipped (existing Read hook)"
|
|
23124
|
+
});
|
|
23125
|
+
return generated;
|
|
23126
|
+
}
|
|
23127
|
+
const cmds = contextCommands();
|
|
23128
|
+
hooks.PreToolUse = [
|
|
23129
|
+
...hooks.PreToolUse ?? [],
|
|
23130
|
+
{
|
|
23131
|
+
matcher: "Read",
|
|
23132
|
+
hooks: [{ type: "command", command: cmds.pre }]
|
|
23133
|
+
}
|
|
23134
|
+
];
|
|
23135
|
+
hooks.PostToolUse = [
|
|
23136
|
+
...hooks.PostToolUse ?? [],
|
|
23137
|
+
{
|
|
23138
|
+
matcher: "Read",
|
|
23139
|
+
hooks: [{ type: "command", command: cmds.post }]
|
|
23140
|
+
}
|
|
23141
|
+
];
|
|
23142
|
+
const postCompact = hooks.PostCompact ?? [];
|
|
23143
|
+
postCompact.push({
|
|
23144
|
+
hooks: [{ type: "command", command: cmds.compact }]
|
|
23145
|
+
});
|
|
23146
|
+
hooks.PostCompact = postCompact;
|
|
23147
|
+
settings.hooks = hooks;
|
|
23148
|
+
mkdirSync(join7(root, ".claude"), { recursive: true });
|
|
23149
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
23150
|
+
`);
|
|
23151
|
+
generated.push({
|
|
23152
|
+
path: ".claude/settings.json",
|
|
23153
|
+
description: "context optimization hooks (Pre/Post Read, PostCompact)"
|
|
23154
|
+
});
|
|
23155
|
+
return generated;
|
|
23156
|
+
}
|
|
23157
|
+
async function runInit(cwd, options) {
|
|
22937
23158
|
const root = cwd ?? process.cwd();
|
|
23159
|
+
const enableContext = options?.context ?? false;
|
|
22938
23160
|
console.log("");
|
|
22939
23161
|
console.log(" vibecop — agent integration setup");
|
|
22940
23162
|
console.log("");
|
|
23163
|
+
if (enableContext) {
|
|
23164
|
+
if (!isBunAvailable()) {
|
|
23165
|
+
console.log(" Error: Context optimization requires the bun runtime.");
|
|
23166
|
+
console.log(" Install bun: https://bun.sh");
|
|
23167
|
+
console.log("");
|
|
23168
|
+
return;
|
|
23169
|
+
}
|
|
23170
|
+
console.log(" Setting up context optimization...");
|
|
23171
|
+
console.log("");
|
|
23172
|
+
const generated2 = generateContextHooks(root);
|
|
23173
|
+
if (generated2.length > 0) {
|
|
23174
|
+
const maxPath = Math.max(...generated2.map((g) => g.path.length));
|
|
23175
|
+
console.log(" Generated:");
|
|
23176
|
+
for (const file of generated2) {
|
|
23177
|
+
console.log(` ${padEnd(file.path, maxPath)} — ${file.description}`);
|
|
23178
|
+
}
|
|
23179
|
+
console.log("");
|
|
23180
|
+
}
|
|
23181
|
+
console.log(" Context optimization configured.");
|
|
23182
|
+
console.log(" Run 'vibecop context stats' to see token savings.");
|
|
23183
|
+
console.log("");
|
|
23184
|
+
return;
|
|
23185
|
+
}
|
|
22941
23186
|
const tools = detectTools(root);
|
|
22942
23187
|
const anyDetected = tools.some((t) => t.detected);
|
|
22943
23188
|
if (!anyDetected) {
|
|
@@ -38820,6 +39065,224 @@ var init_stdio2 = __esm(() => {
|
|
|
38820
39065
|
init_stdio();
|
|
38821
39066
|
});
|
|
38822
39067
|
|
|
39068
|
+
// src/context/skeleton.ts
|
|
39069
|
+
import { parse as parse7, Lang as SgLang2, registerDynamicLanguage as registerDynamicLanguage2 } from "@ast-grep/napi";
|
|
39070
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
39071
|
+
function ensurePython() {
|
|
39072
|
+
if (pythonRegistered2)
|
|
39073
|
+
return;
|
|
39074
|
+
try {
|
|
39075
|
+
const req = createRequire3(import.meta.url);
|
|
39076
|
+
const pythonLang = req("@ast-grep/lang-python");
|
|
39077
|
+
registerDynamicLanguage2({ python: pythonLang });
|
|
39078
|
+
pythonRegistered2 = true;
|
|
39079
|
+
} catch {}
|
|
39080
|
+
}
|
|
39081
|
+
function languageForExtension(ext) {
|
|
39082
|
+
return EXTENSION_TO_LANG[ext] ?? null;
|
|
39083
|
+
}
|
|
39084
|
+
function extractSkeleton(source, language) {
|
|
39085
|
+
if (language === "python")
|
|
39086
|
+
ensurePython();
|
|
39087
|
+
const sgLang = LANG_TO_SG[language];
|
|
39088
|
+
let root;
|
|
39089
|
+
try {
|
|
39090
|
+
root = parse7(sgLang, source).root();
|
|
39091
|
+
} catch {
|
|
39092
|
+
return fallbackSkeleton(source);
|
|
39093
|
+
}
|
|
39094
|
+
const lines = [];
|
|
39095
|
+
const imports = findImports(root, language);
|
|
39096
|
+
if (imports.length > 0) {
|
|
39097
|
+
for (const imp of imports) {
|
|
39098
|
+
lines.push(imp.text);
|
|
39099
|
+
}
|
|
39100
|
+
lines.push("");
|
|
39101
|
+
}
|
|
39102
|
+
const classes = findClasses(root, language);
|
|
39103
|
+
for (const cls of classes) {
|
|
39104
|
+
if (language === "python") {
|
|
39105
|
+
lines.push(`class ${cls.name}:`);
|
|
39106
|
+
} else {
|
|
39107
|
+
lines.push(`class ${cls.name} {`);
|
|
39108
|
+
}
|
|
39109
|
+
for (const method of cls.methods) {
|
|
39110
|
+
lines.push(` ${method}(...)`);
|
|
39111
|
+
}
|
|
39112
|
+
if (language !== "python")
|
|
39113
|
+
lines.push("}");
|
|
39114
|
+
lines.push("");
|
|
39115
|
+
}
|
|
39116
|
+
const functions = findFunctions(root, language);
|
|
39117
|
+
const classMethodNodes = new Set(classes.flatMap((cls) => cls.node.findAll({ rule: { kind: language === "python" ? "function_definition" : "method_definition" } })).map((n) => n.range().start.line));
|
|
39118
|
+
for (const fn of functions) {
|
|
39119
|
+
if (classMethodNodes.has(fn.node.range().start.line))
|
|
39120
|
+
continue;
|
|
39121
|
+
const range = fn.node.range();
|
|
39122
|
+
const sig = source.split(`
|
|
39123
|
+
`).slice(range.start.line, range.start.line + 1)[0]?.trim() ?? "";
|
|
39124
|
+
if (sig)
|
|
39125
|
+
lines.push(sig.replace(/\{[\s\S]*$/, "{ ... }"));
|
|
39126
|
+
}
|
|
39127
|
+
if (functions.length > 0 && classes.length === 0)
|
|
39128
|
+
lines.push("");
|
|
39129
|
+
const exports = findExports(root, language);
|
|
39130
|
+
for (const exp of exports) {
|
|
39131
|
+
if (exp.kind === "function" || exp.kind === "class")
|
|
39132
|
+
continue;
|
|
39133
|
+
lines.push(`export ${exp.kind === "default" ? "default" : ""} ${exp.name}`.trim());
|
|
39134
|
+
}
|
|
39135
|
+
const skeleton = lines.join(`
|
|
39136
|
+
`).trim();
|
|
39137
|
+
return skeleton || fallbackSkeleton(source);
|
|
39138
|
+
}
|
|
39139
|
+
function fallbackSkeleton(source) {
|
|
39140
|
+
const lines = source.split(`
|
|
39141
|
+
`);
|
|
39142
|
+
const skeleton = [];
|
|
39143
|
+
for (const line of lines) {
|
|
39144
|
+
const trimmed = line.trim();
|
|
39145
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("from ") || trimmed.startsWith("export ") || trimmed.startsWith("class ") || /^(async\s+)?function\s/.test(trimmed) || /^(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/.test(trimmed) || /^def\s/.test(trimmed)) {
|
|
39146
|
+
skeleton.push(trimmed);
|
|
39147
|
+
}
|
|
39148
|
+
}
|
|
39149
|
+
return skeleton.join(`
|
|
39150
|
+
`);
|
|
39151
|
+
}
|
|
39152
|
+
var EXTENSION_TO_LANG, LANG_TO_SG, pythonRegistered2 = false;
|
|
39153
|
+
var init_skeleton = __esm(() => {
|
|
39154
|
+
EXTENSION_TO_LANG = {
|
|
39155
|
+
".js": "javascript",
|
|
39156
|
+
".jsx": "javascript",
|
|
39157
|
+
".mjs": "javascript",
|
|
39158
|
+
".cjs": "javascript",
|
|
39159
|
+
".ts": "typescript",
|
|
39160
|
+
".tsx": "tsx",
|
|
39161
|
+
".py": "python"
|
|
39162
|
+
};
|
|
39163
|
+
LANG_TO_SG = {
|
|
39164
|
+
javascript: SgLang2.JavaScript,
|
|
39165
|
+
typescript: SgLang2.TypeScript,
|
|
39166
|
+
tsx: SgLang2.Tsx,
|
|
39167
|
+
python: "python"
|
|
39168
|
+
};
|
|
39169
|
+
});
|
|
39170
|
+
|
|
39171
|
+
// src/context/session.ts
|
|
39172
|
+
function estimateTokens(text) {
|
|
39173
|
+
return Math.ceil(text.length / 4);
|
|
39174
|
+
}
|
|
39175
|
+
function isSupportedExtension(filePath) {
|
|
39176
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
39177
|
+
if (lastDot === -1)
|
|
39178
|
+
return false;
|
|
39179
|
+
return SUPPORTED_EXTENSIONS.has(filePath.slice(lastDot));
|
|
39180
|
+
}
|
|
39181
|
+
var SUPPORTED_EXTENSIONS;
|
|
39182
|
+
var init_session = __esm(() => {
|
|
39183
|
+
SUPPORTED_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
|
|
39184
|
+
});
|
|
39185
|
+
|
|
39186
|
+
// src/context/benchmark.ts
|
|
39187
|
+
var exports_benchmark = {};
|
|
39188
|
+
__export(exports_benchmark, {
|
|
39189
|
+
formatBenchmark: () => formatBenchmark,
|
|
39190
|
+
benchmark: () => benchmark
|
|
39191
|
+
});
|
|
39192
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
39193
|
+
import { extname as extname2, join as join8, relative as relative2, resolve as resolve4 } from "node:path";
|
|
39194
|
+
function walkSupported(dir, root, results) {
|
|
39195
|
+
let entries;
|
|
39196
|
+
try {
|
|
39197
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
39198
|
+
} catch {
|
|
39199
|
+
return;
|
|
39200
|
+
}
|
|
39201
|
+
for (const entry of entries) {
|
|
39202
|
+
if (entry.name.startsWith("."))
|
|
39203
|
+
continue;
|
|
39204
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "build")
|
|
39205
|
+
continue;
|
|
39206
|
+
const fullPath = join8(dir, entry.name);
|
|
39207
|
+
if (entry.isDirectory()) {
|
|
39208
|
+
walkSupported(fullPath, root, results);
|
|
39209
|
+
} else if (entry.isFile() && isSupportedExtension(fullPath)) {
|
|
39210
|
+
try {
|
|
39211
|
+
const stat = statSync2(fullPath);
|
|
39212
|
+
if (stat.size > 500000 || stat.size === 0)
|
|
39213
|
+
continue;
|
|
39214
|
+
const source = readFileSync8(fullPath, "utf-8");
|
|
39215
|
+
const lang = languageForExtension(extname2(fullPath));
|
|
39216
|
+
if (!lang)
|
|
39217
|
+
continue;
|
|
39218
|
+
const fullTokens = estimateTokens(source);
|
|
39219
|
+
const skeleton = extractSkeleton(source, lang);
|
|
39220
|
+
const skeletonTokens = estimateTokens(skeleton);
|
|
39221
|
+
const reductionPercent = fullTokens > 0 ? Math.round((1 - (skeletonTokens + 128) / fullTokens) * 100) : 0;
|
|
39222
|
+
results.push({
|
|
39223
|
+
path: relative2(root, fullPath),
|
|
39224
|
+
fullTokens,
|
|
39225
|
+
skeletonTokens,
|
|
39226
|
+
reductionPercent: Math.max(0, reductionPercent)
|
|
39227
|
+
});
|
|
39228
|
+
} catch {}
|
|
39229
|
+
}
|
|
39230
|
+
}
|
|
39231
|
+
}
|
|
39232
|
+
function benchmark(projectRoot) {
|
|
39233
|
+
const root = resolve4(projectRoot);
|
|
39234
|
+
const files = [];
|
|
39235
|
+
walkSupported(root, root, files);
|
|
39236
|
+
const totalTokens = files.reduce((sum, f) => sum + f.fullTokens, 0);
|
|
39237
|
+
const sorted = [...files].sort((a, b) => b.fullTokens - a.fullTokens);
|
|
39238
|
+
const projections = [20, 40, 60].map((rereadPercent) => {
|
|
39239
|
+
const rereadCount = Math.round(files.length * rereadPercent / 100);
|
|
39240
|
+
const rereadFiles = sorted.slice(0, rereadCount);
|
|
39241
|
+
const tokensSaved = rereadFiles.reduce((sum, f) => {
|
|
39242
|
+
const limited = 128 + f.skeletonTokens;
|
|
39243
|
+
return sum + Math.max(0, f.fullTokens - limited);
|
|
39244
|
+
}, 0);
|
|
39245
|
+
return {
|
|
39246
|
+
rereadPercent,
|
|
39247
|
+
tokensSaved,
|
|
39248
|
+
percentOfTotal: totalTokens > 0 ? Math.round(tokensSaved / totalTokens * 100) : 0
|
|
39249
|
+
};
|
|
39250
|
+
});
|
|
39251
|
+
return { files: sorted, totalFiles: files.length, totalTokens, projections };
|
|
39252
|
+
}
|
|
39253
|
+
function formatBenchmark(result) {
|
|
39254
|
+
if (result.totalFiles === 0) {
|
|
39255
|
+
return "No supported files found (.js, .ts, .tsx, .py).";
|
|
39256
|
+
}
|
|
39257
|
+
const lines = [];
|
|
39258
|
+
lines.push("vibecop context benchmark");
|
|
39259
|
+
lines.push("═════════════════════════");
|
|
39260
|
+
lines.push("");
|
|
39261
|
+
lines.push(`Files: ${result.totalFiles} supported`);
|
|
39262
|
+
lines.push(`Total tokens: ~${result.totalTokens.toLocaleString()}`);
|
|
39263
|
+
lines.push("");
|
|
39264
|
+
const top = result.files.slice(0, 10);
|
|
39265
|
+
lines.push("Largest files (most savings potential):");
|
|
39266
|
+
const maxPath = Math.max(...top.map((f) => f.path.length), 10);
|
|
39267
|
+
for (const f of top) {
|
|
39268
|
+
const pathPad = f.path.padEnd(maxPath);
|
|
39269
|
+
lines.push(` ${pathPad} ${f.fullTokens.toLocaleString().padStart(6)} tokens → skeleton: ${f.skeletonTokens.toLocaleString().padStart(5)} (${f.reductionPercent}% reduction)`);
|
|
39270
|
+
}
|
|
39271
|
+
lines.push("");
|
|
39272
|
+
lines.push("Projected savings per session:");
|
|
39273
|
+
for (const p of result.projections) {
|
|
39274
|
+
lines.push(` ${p.rereadPercent}% re-read rate: ~${p.tokensSaved.toLocaleString()} tokens saved (${p.percentOfTotal}% of total Read usage)`);
|
|
39275
|
+
}
|
|
39276
|
+
lines.push("");
|
|
39277
|
+
lines.push("To enable: vibecop init --context");
|
|
39278
|
+
return lines.join(`
|
|
39279
|
+
`);
|
|
39280
|
+
}
|
|
39281
|
+
var init_benchmark = __esm(() => {
|
|
39282
|
+
init_skeleton();
|
|
39283
|
+
init_session();
|
|
39284
|
+
});
|
|
39285
|
+
|
|
38823
39286
|
// src/mcp/server.ts
|
|
38824
39287
|
function formatScanResult(result) {
|
|
38825
39288
|
return {
|
|
@@ -38897,11 +39360,52 @@ ${availableIds.map((id) => ` - ${id}`).join(`
|
|
|
38897
39360
|
]
|
|
38898
39361
|
};
|
|
38899
39362
|
}
|
|
38900
|
-
|
|
39363
|
+
async function handleContextBenchmark(args) {
|
|
39364
|
+
try {
|
|
39365
|
+
const result = benchmark(args.path ?? ".");
|
|
39366
|
+
if (result.totalFiles === 0) {
|
|
39367
|
+
return {
|
|
39368
|
+
content: [{ type: "text", text: "No supported files found (.js, .ts, .tsx, .py)." }]
|
|
39369
|
+
};
|
|
39370
|
+
}
|
|
39371
|
+
const top10 = result.files.slice(0, 10).map((f) => ({
|
|
39372
|
+
file: f.path,
|
|
39373
|
+
tokens: f.fullTokens,
|
|
39374
|
+
skeletonTokens: f.skeletonTokens,
|
|
39375
|
+
reduction: `${f.reductionPercent}%`
|
|
39376
|
+
}));
|
|
39377
|
+
return {
|
|
39378
|
+
content: [
|
|
39379
|
+
{
|
|
39380
|
+
type: "text",
|
|
39381
|
+
text: JSON.stringify({
|
|
39382
|
+
totalFiles: result.totalFiles,
|
|
39383
|
+
totalTokens: result.totalTokens,
|
|
39384
|
+
topFiles: top10,
|
|
39385
|
+
projections: result.projections.map((p) => ({
|
|
39386
|
+
rereadRate: `${p.rereadPercent}%`,
|
|
39387
|
+
tokensSaved: p.tokensSaved,
|
|
39388
|
+
percentOfTotal: `${p.percentOfTotal}%`
|
|
39389
|
+
})),
|
|
39390
|
+
enableCommand: "vibecop init --context"
|
|
39391
|
+
}, null, 2)
|
|
39392
|
+
}
|
|
39393
|
+
]
|
|
39394
|
+
};
|
|
39395
|
+
} catch (err) {
|
|
39396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39397
|
+
return {
|
|
39398
|
+
content: [{ type: "text", text: `Error running benchmark: ${message}` }],
|
|
39399
|
+
isError: true
|
|
39400
|
+
};
|
|
39401
|
+
}
|
|
39402
|
+
}
|
|
39403
|
+
var scanInputSchema, checkInputSchema, explainInputSchema, contextBenchmarkInputSchema;
|
|
38901
39404
|
var init_server3 = __esm(() => {
|
|
38902
39405
|
init_zod();
|
|
38903
39406
|
init_engine();
|
|
38904
39407
|
init_detectors();
|
|
39408
|
+
init_benchmark();
|
|
38905
39409
|
scanInputSchema = {
|
|
38906
39410
|
path: exports_external.string().optional().describe("Directory to scan. Defaults to current working directory."),
|
|
38907
39411
|
maxFindings: exports_external.number().optional().describe("Maximum findings to return. Default 50.")
|
|
@@ -38913,6 +39417,9 @@ var init_server3 = __esm(() => {
|
|
|
38913
39417
|
explainInputSchema = {
|
|
38914
39418
|
detector_id: exports_external.string().describe('The detector ID (e.g., "unsafe-shell-exec", "god-function").')
|
|
38915
39419
|
};
|
|
39420
|
+
contextBenchmarkInputSchema = {
|
|
39421
|
+
path: exports_external.string().optional().describe("Directory to benchmark. Defaults to current working directory.")
|
|
39422
|
+
};
|
|
38916
39423
|
});
|
|
38917
39424
|
|
|
38918
39425
|
// src/mcp/index.ts
|
|
@@ -38921,11 +39428,11 @@ __export(exports_mcp, {
|
|
|
38921
39428
|
startServer: () => startServer,
|
|
38922
39429
|
createServer: () => createServer
|
|
38923
39430
|
});
|
|
38924
|
-
import { readFileSync as
|
|
39431
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
38925
39432
|
function getVersion() {
|
|
38926
39433
|
try {
|
|
38927
39434
|
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
38928
|
-
const pkg = JSON.parse(
|
|
39435
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
38929
39436
|
return pkg.version;
|
|
38930
39437
|
} catch {
|
|
38931
39438
|
return "0.0.0";
|
|
@@ -38949,6 +39456,10 @@ function createServer() {
|
|
|
38949
39456
|
description: "Explain what a vibecop detector checks for, its severity, and category.",
|
|
38950
39457
|
inputSchema: explainInputSchema
|
|
38951
39458
|
}, handleExplain);
|
|
39459
|
+
server.registerTool("vibecop_context_benchmark", {
|
|
39460
|
+
description: "Benchmark context optimization potential for a project. Shows per-file skeleton compression ratios and projected token savings at different re-read rates. No bun required.",
|
|
39461
|
+
inputSchema: contextBenchmarkInputSchema
|
|
39462
|
+
}, handleContextBenchmark);
|
|
38952
39463
|
return server;
|
|
38953
39464
|
}
|
|
38954
39465
|
async function startServer() {
|
|
@@ -38976,9 +39487,9 @@ var exports_test_rules = {};
|
|
|
38976
39487
|
__export(exports_test_rules, {
|
|
38977
39488
|
runTestRules: () => runTestRules
|
|
38978
39489
|
});
|
|
38979
|
-
import { existsSync as existsSync7, readdirSync as
|
|
38980
|
-
import { join as
|
|
38981
|
-
import { parse as
|
|
39490
|
+
import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync10 } from "node:fs";
|
|
39491
|
+
import { join as join9 } from "node:path";
|
|
39492
|
+
import { parse as parse8, Lang as SgLang3 } from "@ast-grep/napi";
|
|
38982
39493
|
function testRule(rule) {
|
|
38983
39494
|
const details = [];
|
|
38984
39495
|
let passed = true;
|
|
@@ -38990,11 +39501,11 @@ function testRule(rule) {
|
|
|
38990
39501
|
};
|
|
38991
39502
|
}
|
|
38992
39503
|
const lang = rule.languages[0];
|
|
38993
|
-
const sgLang = LANG_MAP2[lang] ??
|
|
39504
|
+
const sgLang = LANG_MAP2[lang] ?? SgLang3.JavaScript;
|
|
38994
39505
|
if (rule.examples.invalid) {
|
|
38995
39506
|
for (let i = 0;i < rule.examples.invalid.length; i++) {
|
|
38996
39507
|
const code = rule.examples.invalid[i];
|
|
38997
|
-
const root =
|
|
39508
|
+
const root = parse8(sgLang, code);
|
|
38998
39509
|
const matches = root.root().findAll({
|
|
38999
39510
|
rule: rule.rule
|
|
39000
39511
|
});
|
|
@@ -39007,7 +39518,7 @@ function testRule(rule) {
|
|
|
39007
39518
|
if (rule.examples.valid) {
|
|
39008
39519
|
for (let i = 0;i < rule.examples.valid.length; i++) {
|
|
39009
39520
|
const code = rule.examples.valid[i];
|
|
39010
|
-
const root =
|
|
39521
|
+
const root = parse8(sgLang, code);
|
|
39011
39522
|
const matches = root.root().findAll({
|
|
39012
39523
|
rule: rule.rule
|
|
39013
39524
|
});
|
|
@@ -39029,7 +39540,7 @@ function runTestRules(rulesDir) {
|
|
|
39029
39540
|
console.error(`Error: Rules directory not found: ${rulesDir}`);
|
|
39030
39541
|
return { results: [], passed: 0, failed: 0 };
|
|
39031
39542
|
}
|
|
39032
|
-
const entries =
|
|
39543
|
+
const entries = readdirSync6(rulesDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
39033
39544
|
if (entries.length === 0) {
|
|
39034
39545
|
console.log("No rule files found.");
|
|
39035
39546
|
return { results: [], passed: 0, failed: 0 };
|
|
@@ -39038,10 +39549,10 @@ function runTestRules(rulesDir) {
|
|
|
39038
39549
|
let passed = 0;
|
|
39039
39550
|
let failed = 0;
|
|
39040
39551
|
for (const entry of entries) {
|
|
39041
|
-
const filePath =
|
|
39552
|
+
const filePath = join9(rulesDir, entry);
|
|
39042
39553
|
let raw;
|
|
39043
39554
|
try {
|
|
39044
|
-
raw =
|
|
39555
|
+
raw = readFileSync10(filePath, "utf-8");
|
|
39045
39556
|
} catch {
|
|
39046
39557
|
console.error(`Could not read ${filePath}`);
|
|
39047
39558
|
continue;
|
|
@@ -39087,17 +39598,17 @@ var init_test_rules = __esm(() => {
|
|
|
39087
39598
|
init_custom_rules();
|
|
39088
39599
|
import_yaml3 = __toESM(require_dist(), 1);
|
|
39089
39600
|
LANG_MAP2 = {
|
|
39090
|
-
javascript:
|
|
39091
|
-
typescript:
|
|
39092
|
-
tsx:
|
|
39093
|
-
python:
|
|
39601
|
+
javascript: SgLang3.JavaScript,
|
|
39602
|
+
typescript: SgLang3.TypeScript,
|
|
39603
|
+
tsx: SgLang3.Tsx,
|
|
39604
|
+
python: SgLang3.JavaScript
|
|
39094
39605
|
};
|
|
39095
39606
|
});
|
|
39096
39607
|
|
|
39097
39608
|
// src/cli.ts
|
|
39098
39609
|
import { execSync as execSync2 } from "node:child_process";
|
|
39099
|
-
import { readFileSync as
|
|
39100
|
-
import { resolve as
|
|
39610
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
39611
|
+
import { resolve as resolve5 } from "node:path";
|
|
39101
39612
|
|
|
39102
39613
|
// node_modules/commander/esm.mjs
|
|
39103
39614
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -39641,7 +40152,7 @@ function getFormatter(format, options) {
|
|
|
39641
40152
|
function getVersion2() {
|
|
39642
40153
|
try {
|
|
39643
40154
|
const pkgPath = new URL("../package.json", import.meta.url);
|
|
39644
|
-
const pkg = JSON.parse(
|
|
40155
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
39645
40156
|
return pkg.version;
|
|
39646
40157
|
} catch {
|
|
39647
40158
|
return "0.0.0";
|
|
@@ -39707,7 +40218,7 @@ async function resolveFiles(options, scanRoot) {
|
|
|
39707
40218
|
return;
|
|
39708
40219
|
}
|
|
39709
40220
|
async function scanAction(scanPath, options) {
|
|
39710
|
-
const scanRoot =
|
|
40221
|
+
const scanRoot = resolve5(scanPath ?? ".");
|
|
39711
40222
|
const maxFindings = Number.parseInt(options.maxFindings, 10);
|
|
39712
40223
|
const files = await resolveFiles(options, scanRoot);
|
|
39713
40224
|
const result = await scan({
|
|
@@ -39753,17 +40264,48 @@ function main() {
|
|
|
39753
40264
|
program2.name("vibecop").description("AI code quality linter built on ast-grep").version(getVersion2());
|
|
39754
40265
|
program2.command("scan").description("Scan a directory for code quality issues").argument("[path]", "Directory to scan", ".").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent, gcc)", "text").option("-c, --config <path>", "Path to config file").option("--no-config", "Disable config file loading").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--diff <ref>", "Scan only files changed vs git ref").option("--stdin-files", "Read file list from stdin", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(scanAction);
|
|
39755
40266
|
program2.command("check").description("Check a single file for code quality issues").argument("<file>", "File to check").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent, gcc)", "text").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(checkAction);
|
|
39756
|
-
program2.command("init").description("Set up vibecop integration with AI coding tools").action(async () => {
|
|
40267
|
+
program2.command("init").description("Set up vibecop integration with AI coding tools").option("--context", "Enable context optimization (requires bun)", false).action(async (options) => {
|
|
39757
40268
|
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), exports_init));
|
|
39758
|
-
await runInit2();
|
|
40269
|
+
await runInit2(undefined, { context: options.context });
|
|
39759
40270
|
});
|
|
39760
40271
|
program2.command("serve").description("Start MCP server (stdio transport)").action(async () => {
|
|
39761
40272
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
39762
40273
|
await startServer2();
|
|
39763
40274
|
});
|
|
40275
|
+
program2.command("context").description("Context optimization — run as hook handler or view stats").argument("[mode]", "Mode: stats | benchmark (default: stats)").option("--pre", "PreToolUse handler (reads stdin)", false).option("--post", "PostToolUse handler (reads stdin)", false).option("--compact", "PostCompact handler (reads stdin)", false).action(async (mode, options) => {
|
|
40276
|
+
if (mode === "benchmark") {
|
|
40277
|
+
const { benchmark: benchmark2, formatBenchmark: formatBenchmark2 } = await Promise.resolve().then(() => (init_benchmark(), exports_benchmark));
|
|
40278
|
+
console.log(formatBenchmark2(benchmark2(process.cwd())));
|
|
40279
|
+
return;
|
|
40280
|
+
}
|
|
40281
|
+
const args = [];
|
|
40282
|
+
if (options.pre)
|
|
40283
|
+
args.push("--pre");
|
|
40284
|
+
else if (options.post)
|
|
40285
|
+
args.push("--post");
|
|
40286
|
+
else if (options.compact)
|
|
40287
|
+
args.push("--compact");
|
|
40288
|
+
else
|
|
40289
|
+
args.push(mode ?? "stats");
|
|
40290
|
+
const { execSync: execSync3 } = await import("node:child_process");
|
|
40291
|
+
const { existsSync: existsSync8 } = await import("node:fs");
|
|
40292
|
+
const distPath = new URL("../dist/context.js", import.meta.url).pathname;
|
|
40293
|
+
const srcPath = new URL("./context.ts", import.meta.url).pathname;
|
|
40294
|
+
const contextPath = existsSync8(distPath) ? distPath : srcPath;
|
|
40295
|
+
try {
|
|
40296
|
+
execSync3(`bun ${contextPath} ${args.join(" ")}`, {
|
|
40297
|
+
stdio: "inherit",
|
|
40298
|
+
cwd: process.cwd()
|
|
40299
|
+
});
|
|
40300
|
+
} catch (err) {
|
|
40301
|
+
if (err && typeof err === "object" && "status" in err) {
|
|
40302
|
+
process.exit(err.status);
|
|
40303
|
+
}
|
|
40304
|
+
}
|
|
40305
|
+
});
|
|
39764
40306
|
program2.command("test-rules").description("Validate custom rules against their inline examples").option("--rules-dir <path>", "Path to custom rules directory", ".vibecop/rules").action(async (options) => {
|
|
39765
40307
|
const { runTestRules: runTestRules2 } = await Promise.resolve().then(() => (init_test_rules(), exports_test_rules));
|
|
39766
|
-
const result = runTestRules2(
|
|
40308
|
+
const result = runTestRules2(resolve5(options.rulesDir));
|
|
39767
40309
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
39768
40310
|
});
|
|
39769
40311
|
program2.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecop",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "AI code quality toolkit — deterministic linter for the AI coding era",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun build src/cli.ts --outdir dist --target node --external @ast-grep/napi --external @ast-grep/lang-python",
|
|
11
|
+
"build:context": "bun build src/context.ts --outdir dist --target bun --external @ast-grep/napi --external @ast-grep/lang-python",
|
|
11
12
|
"build:action": "npx ncc build src/action/main.ts -o dist/action --source-map --license licenses.txt",
|
|
12
13
|
"test": "bun test",
|
|
13
14
|
"lint": "bunx biome check",
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"dist/cli.js",
|
|
21
|
+
"dist/context.js",
|
|
20
22
|
"README.md",
|
|
21
23
|
"LICENSE"
|
|
22
24
|
],
|