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.
Files changed (3) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +670 -128
  3. 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
- Three tools: `vibecop_scan`, `vibecop_check`, `vibecop_explain`.
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
- - [ ] **Phase 4**: Context optimization (Read tool interception, AST skeleton caching)
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/detectors/undeclared-import.ts
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
- if (parts.length >= 2) {
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 importStatements = root.findAll({ rule: { kind: "import_statement" } });
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 funcKinds = ["function_declaration", "method_definition", "arrow_function"];
15758
- for (const kind of funcKinds) {
15759
- const nodes = root.findAll({ rule: { kind } });
15760
- for (const node of nodes) {
15761
- const range = node.range();
15762
- const lines = range.end.line - range.start.line + 1;
15763
- if (kind === "arrow_function") {
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
- if (!complexityExceeded && !paramsExceeded && complexity <= 2)
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 funcNodes = root.findAll({ rule: { kind: "function_definition" } });
15810
- for (const node of funcNodes) {
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 nameNode = node.children().find((ch) => ch.kind() === "identifier");
15814
- const name = nameNode?.text() ?? "<anonymous>";
15953
+ const name = fn.name;
15815
15954
  const complexity = 1 + countComplexity(node, true);
15816
- const params = countPyParams(node);
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.findAll({ rule: { kind: "import_statement" } });
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 sourceNode = imp.children().find((ch) => ch.kind() === "string");
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
- async function runInit(cwd) {
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
- var scanInputSchema, checkInputSchema, explainInputSchema;
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 readFileSync8 } from "node:fs";
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(readFileSync8(pkgPath, "utf-8"));
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 readdirSync5, readFileSync as readFileSync9 } from "node:fs";
38980
- import { join as join8 } from "node:path";
38981
- import { parse as parse7, Lang as SgLang2 } from "@ast-grep/napi";
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] ?? SgLang2.JavaScript;
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 = parse7(sgLang, code);
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 = parse7(sgLang, code);
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 = readdirSync5(rulesDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
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 = join8(rulesDir, entry);
39552
+ const filePath = join9(rulesDir, entry);
39042
39553
  let raw;
39043
39554
  try {
39044
- raw = readFileSync9(filePath, "utf-8");
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: SgLang2.JavaScript,
39091
- typescript: SgLang2.TypeScript,
39092
- tsx: SgLang2.Tsx,
39093
- python: SgLang2.JavaScript
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 readFileSync10 } from "node:fs";
39100
- import { resolve as resolve4 } from "node:path";
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(readFileSync10(pkgPath, "utf-8"));
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 = resolve4(scanPath ?? ".");
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(resolve4(options.rulesDir));
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.0",
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
  ],