vibecop 0.4.1 → 0.4.3
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 +5 -3
- package/dist/cli.js +9 -8
- package/dist/context.js +765 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -66,11 +66,13 @@ Agent writes code → vibecop hook fires → Findings? Agent fixes → Clean? Co
|
|
|
66
66
|
|
|
67
67
|
Four tools: `vibecop_scan`, `vibecop_check`, `vibecop_explain`, `vibecop_context_benchmark`.
|
|
68
68
|
|
|
69
|
-
## Context Optimization
|
|
69
|
+
## Context Optimization (Beta)
|
|
70
|
+
|
|
71
|
+
> **Beta:** This feature is under active testing. Re-run `vibecop init --context` after upgrading vibecop or reinstalling dependencies.
|
|
70
72
|
|
|
71
73
|
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
74
|
|
|
73
|
-
**Requires bun runtime** (uses `bun:sqlite` for zero-dependency caching).
|
|
75
|
+
**Requires bun runtime** (uses `bun:sqlite` for zero-dependency caching). Claude Code only.
|
|
74
76
|
|
|
75
77
|
```bash
|
|
76
78
|
vibecop context benchmark # See projected savings for your project
|
|
@@ -133,7 +135,7 @@ Catches: god functions, N+1 queries, unsafe shell exec, SQL injection, hardcoded
|
|
|
133
135
|
- [x] **Phase 2.5**: Agent integration (7 tools), 6 LLM/agent detectors, `vibecop init`
|
|
134
136
|
- [x] **Phase 3**: Test quality detectors, custom YAML rules (28 → 35)
|
|
135
137
|
- [x] **Phase 3.5**: MCP server with scan/check/explain tools
|
|
136
|
-
- [x] **Phase 4**: Context optimization (Read tool interception, AST skeleton caching)
|
|
138
|
+
- [x] **Phase 4**: Context optimization — Beta (Read tool interception, AST skeleton caching)
|
|
137
139
|
- [ ] **Phase 5**: VS Code extension, cross-file analysis
|
|
138
140
|
|
|
139
141
|
## Links
|
package/dist/cli.js
CHANGED
|
@@ -22847,12 +22847,12 @@ import { execSync } from "node:child_process";
|
|
|
22847
22847
|
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
|
|
22848
22848
|
import { join as join7 } from "node:path";
|
|
22849
22849
|
function resolveContextScript() {
|
|
22850
|
-
const
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
22854
|
-
|
|
22855
|
-
|
|
22850
|
+
const sibling = new URL("./context.js", import.meta.url);
|
|
22851
|
+
if (existsSync6(sibling))
|
|
22852
|
+
return sibling.pathname;
|
|
22853
|
+
const fromSource = new URL("../dist/context.js", import.meta.url);
|
|
22854
|
+
if (existsSync6(fromSource))
|
|
22855
|
+
return fromSource.pathname;
|
|
22856
22856
|
const { resolve: resolve4 } = __require("node:path");
|
|
22857
22857
|
return resolve4("dist/context.js");
|
|
22858
22858
|
}
|
|
@@ -23167,7 +23167,7 @@ async function runInit(cwd, options) {
|
|
|
23167
23167
|
console.log("");
|
|
23168
23168
|
return;
|
|
23169
23169
|
}
|
|
23170
|
-
console.log(" Setting up context optimization...");
|
|
23170
|
+
console.log(" Setting up context optimization (beta)...");
|
|
23171
23171
|
console.log("");
|
|
23172
23172
|
const generated2 = generateContextHooks(root);
|
|
23173
23173
|
if (generated2.length > 0) {
|
|
@@ -23178,7 +23178,8 @@ async function runInit(cwd, options) {
|
|
|
23178
23178
|
}
|
|
23179
23179
|
console.log("");
|
|
23180
23180
|
}
|
|
23181
|
-
console.log(" Context optimization configured.");
|
|
23181
|
+
console.log(" Context optimization configured (beta).");
|
|
23182
|
+
console.log(" Re-run this command after upgrading vibecop or reinstalling deps.");
|
|
23182
23183
|
console.log(" Run 'vibecop context stats' to see token savings.");
|
|
23183
23184
|
console.log("");
|
|
23184
23185
|
return;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/context.ts
|
|
5
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
6
|
+
import { dirname as dirname2, extname as extname2, resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/context/cache.ts
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import { existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
var DB_FILENAME = ".vibecop-context.db";
|
|
13
|
+
function getDbPath(projectRoot) {
|
|
14
|
+
return join(projectRoot, ".vibecop", DB_FILENAME);
|
|
15
|
+
}
|
|
16
|
+
function openDb(projectRoot) {
|
|
17
|
+
const dbPath = getDbPath(projectRoot);
|
|
18
|
+
const dir = dirname(dbPath);
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const db = new Database(dbPath);
|
|
22
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
23
|
+
db.run("PRAGMA busy_timeout = 1000");
|
|
24
|
+
initSchema(db);
|
|
25
|
+
return db;
|
|
26
|
+
}
|
|
27
|
+
function initSchema(db) {
|
|
28
|
+
db.run(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS skeletons (
|
|
30
|
+
path TEXT PRIMARY KEY,
|
|
31
|
+
hash TEXT NOT NULL,
|
|
32
|
+
skeleton TEXT NOT NULL,
|
|
33
|
+
language TEXT NOT NULL,
|
|
34
|
+
full_tokens INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
skeleton_tokens INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
db.run(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS session_reads (
|
|
41
|
+
session_id TEXT NOT NULL,
|
|
42
|
+
path TEXT NOT NULL,
|
|
43
|
+
hash TEXT NOT NULL,
|
|
44
|
+
read_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
45
|
+
PRIMARY KEY (session_id, path)
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
db.run(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
50
|
+
session_id TEXT NOT NULL,
|
|
51
|
+
total_reads INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
cache_hits INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
tokens_saved INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
55
|
+
PRIMARY KEY (session_id)
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
function getSkeleton(db, path, hash) {
|
|
60
|
+
const row = db.query("SELECT skeleton, full_tokens, skeleton_tokens FROM skeletons WHERE path = ? AND hash = ?").get(path, hash);
|
|
61
|
+
if (!row)
|
|
62
|
+
return null;
|
|
63
|
+
return { skeleton: row.skeleton, fullTokens: row.full_tokens, skeletonTokens: row.skeleton_tokens };
|
|
64
|
+
}
|
|
65
|
+
function upsertSkeleton(db, path, hash, skeleton, language, fullTokens, skeletonTokens) {
|
|
66
|
+
db.run(`INSERT INTO skeletons (path, hash, skeleton, language, full_tokens, skeleton_tokens, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, unixepoch())
|
|
68
|
+
ON CONFLICT(path) DO UPDATE SET hash=excluded.hash, skeleton=excluded.skeleton, language=excluded.language, full_tokens=excluded.full_tokens, skeleton_tokens=excluded.skeleton_tokens, updated_at=excluded.updated_at`, [path, hash, skeleton, language, fullTokens, skeletonTokens]);
|
|
69
|
+
}
|
|
70
|
+
function hasSessionRead(db, sessionId, path) {
|
|
71
|
+
const row = db.query("SELECT hash FROM session_reads WHERE session_id = ? AND path = ?").get(sessionId, path);
|
|
72
|
+
return row ?? null;
|
|
73
|
+
}
|
|
74
|
+
function recordSessionRead(db, sessionId, path, hash) {
|
|
75
|
+
db.run(`INSERT INTO session_reads (session_id, path, hash, read_at)
|
|
76
|
+
VALUES (?, ?, ?, unixepoch())
|
|
77
|
+
ON CONFLICT(session_id, path) DO UPDATE SET hash=excluded.hash, read_at=excluded.read_at`, [sessionId, path, hash]);
|
|
78
|
+
}
|
|
79
|
+
function incrementStats(db, sessionId, fields) {
|
|
80
|
+
db.run(`INSERT INTO stats (session_id, total_reads, cache_hits, tokens_saved, updated_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, unixepoch())
|
|
82
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
83
|
+
total_reads = stats.total_reads + excluded.total_reads,
|
|
84
|
+
cache_hits = stats.cache_hits + excluded.cache_hits,
|
|
85
|
+
tokens_saved = stats.tokens_saved + excluded.tokens_saved,
|
|
86
|
+
updated_at = excluded.updated_at`, [sessionId, fields.totalReads ?? 0, fields.cacheHits ?? 0, fields.tokensSaved ?? 0]);
|
|
87
|
+
}
|
|
88
|
+
function getSessionStats(db, sessionId) {
|
|
89
|
+
const row = db.query("SELECT * FROM stats WHERE session_id = ?").get(sessionId);
|
|
90
|
+
if (!row)
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
sessionId: row.session_id,
|
|
94
|
+
totalReads: row.total_reads,
|
|
95
|
+
cacheHits: row.cache_hits,
|
|
96
|
+
tokensSaved: row.tokens_saved
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function getAllStats(db) {
|
|
100
|
+
const rows = db.query("SELECT * FROM stats ORDER BY updated_at DESC").all();
|
|
101
|
+
return rows.map((r) => ({
|
|
102
|
+
sessionId: r.session_id,
|
|
103
|
+
totalReads: r.total_reads,
|
|
104
|
+
cacheHits: r.cache_hits,
|
|
105
|
+
tokensSaved: r.tokens_saved
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
function pruneOldSessions(db, days = 7) {
|
|
109
|
+
const cutoff = Math.floor(Date.now() / 1000) - days * 86400;
|
|
110
|
+
const readsResult = db.run("DELETE FROM session_reads WHERE read_at < ?", [cutoff]);
|
|
111
|
+
const statsResult = db.run("DELETE FROM stats WHERE updated_at < ?", [cutoff]);
|
|
112
|
+
return (readsResult.changes ?? 0) + (statsResult.changes ?? 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/context/session.ts
|
|
116
|
+
import { createHash } from "crypto";
|
|
117
|
+
import { readFileSync, statSync } from "fs";
|
|
118
|
+
function hashFile(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
const content = readFileSync(filePath);
|
|
121
|
+
return createHash("sha256").update(content).digest("hex");
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function estimateTokens(text) {
|
|
127
|
+
return Math.ceil(text.length / 4);
|
|
128
|
+
}
|
|
129
|
+
var SUPPORTED_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
|
|
130
|
+
function isSupportedExtension(filePath) {
|
|
131
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
132
|
+
if (lastDot === -1)
|
|
133
|
+
return false;
|
|
134
|
+
return SUPPORTED_EXTENSIONS.has(filePath.slice(lastDot));
|
|
135
|
+
}
|
|
136
|
+
var MAX_FILE_SIZE = 500000;
|
|
137
|
+
function isFileEligible(filePath) {
|
|
138
|
+
try {
|
|
139
|
+
const stat = statSync(filePath);
|
|
140
|
+
return stat.isFile() && stat.size <= MAX_FILE_SIZE;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/context/skeleton.ts
|
|
147
|
+
import { parse, Lang as SgLang, registerDynamicLanguage } from "@ast-grep/napi";
|
|
148
|
+
import { createRequire } from "module";
|
|
149
|
+
|
|
150
|
+
// src/ast-utils.ts
|
|
151
|
+
function findImports(root, language) {
|
|
152
|
+
if (language === "python")
|
|
153
|
+
return findPythonImports(root);
|
|
154
|
+
return findJsImports(root);
|
|
155
|
+
}
|
|
156
|
+
function findJsImports(root) {
|
|
157
|
+
const results = [];
|
|
158
|
+
const nodes = root.findAll({ rule: { kind: "import_statement" } });
|
|
159
|
+
for (const node of nodes) {
|
|
160
|
+
const sourceNode = node.children().find((ch) => ch.kind() === "string");
|
|
161
|
+
if (!sourceNode)
|
|
162
|
+
continue;
|
|
163
|
+
const source = sourceNode.text().slice(1, -1);
|
|
164
|
+
results.push({ node, source, text: node.text() });
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
function findPythonImports(root) {
|
|
169
|
+
const results = [];
|
|
170
|
+
for (const node of root.findAll({ rule: { kind: "import_statement" } })) {
|
|
171
|
+
const nameNode = node.children().find((ch) => ch.kind() === "dotted_name" || ch.kind() === "aliased_import");
|
|
172
|
+
if (!nameNode)
|
|
173
|
+
continue;
|
|
174
|
+
let source;
|
|
175
|
+
if (nameNode.kind() === "aliased_import") {
|
|
176
|
+
const dotted = nameNode.children().find((ch) => ch.kind() === "dotted_name");
|
|
177
|
+
source = dotted ? dotted.text() : nameNode.text();
|
|
178
|
+
} else {
|
|
179
|
+
source = nameNode.text();
|
|
180
|
+
}
|
|
181
|
+
results.push({ node, source, text: node.text() });
|
|
182
|
+
}
|
|
183
|
+
for (const node of root.findAll({ rule: { kind: "import_from_statement" } })) {
|
|
184
|
+
const nameNode = node.children().find((ch) => ch.kind() === "dotted_name");
|
|
185
|
+
if (!nameNode)
|
|
186
|
+
continue;
|
|
187
|
+
results.push({ node, source: nameNode.text(), text: node.text() });
|
|
188
|
+
}
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
function findFunctions(root, language) {
|
|
192
|
+
if (language === "python")
|
|
193
|
+
return findPythonFunctions(root);
|
|
194
|
+
return findJsFunctions(root);
|
|
195
|
+
}
|
|
196
|
+
function findJsFunctions(root) {
|
|
197
|
+
const results = [];
|
|
198
|
+
for (const kind of ["function_declaration", "method_definition", "arrow_function"]) {
|
|
199
|
+
for (const node of root.findAll({ rule: { kind } })) {
|
|
200
|
+
const name = getJsFunctionName(node);
|
|
201
|
+
const params = countJsParams(node);
|
|
202
|
+
const body = node.children().find((c) => c.kind() === "statement_block") ?? null;
|
|
203
|
+
results.push({ node, name, params, body, kind });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
function getJsFunctionName(node) {
|
|
209
|
+
const kind = node.kind();
|
|
210
|
+
if (kind === "function_declaration") {
|
|
211
|
+
return node.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<anonymous>";
|
|
212
|
+
}
|
|
213
|
+
if (kind === "method_definition") {
|
|
214
|
+
const nameNode = node.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "identifier");
|
|
215
|
+
return nameNode?.text() ?? "<anonymous>";
|
|
216
|
+
}
|
|
217
|
+
if (kind === "arrow_function") {
|
|
218
|
+
const parent = node.parent();
|
|
219
|
+
if (parent?.kind() === "variable_declarator") {
|
|
220
|
+
return parent.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<anonymous>";
|
|
221
|
+
}
|
|
222
|
+
if (parent?.kind() === "pair") {
|
|
223
|
+
const nameNode = parent.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string");
|
|
224
|
+
return nameNode?.text() ?? "<anonymous>";
|
|
225
|
+
}
|
|
226
|
+
return "<anonymous>";
|
|
227
|
+
}
|
|
228
|
+
return "<anonymous>";
|
|
229
|
+
}
|
|
230
|
+
function countJsParams(node) {
|
|
231
|
+
const params = node.children().find((ch) => ch.kind() === "formal_parameters");
|
|
232
|
+
if (!params)
|
|
233
|
+
return 0;
|
|
234
|
+
return params.children().filter((ch) => {
|
|
235
|
+
const k = ch.kind();
|
|
236
|
+
return k !== "(" && k !== ")" && k !== ",";
|
|
237
|
+
}).length;
|
|
238
|
+
}
|
|
239
|
+
function findPythonFunctions(root) {
|
|
240
|
+
const results = [];
|
|
241
|
+
for (const node of root.findAll({ rule: { kind: "function_definition" } })) {
|
|
242
|
+
const nameNode = node.children().find((ch) => ch.kind() === "identifier");
|
|
243
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
244
|
+
const body = node.children().find((ch) => ch.kind() === "block") ?? null;
|
|
245
|
+
const params = countPyParams(node);
|
|
246
|
+
results.push({ node, name, params, body, kind: "function_definition" });
|
|
247
|
+
}
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
function countPyParams(node) {
|
|
251
|
+
const params = node.children().find((ch) => ch.kind() === "parameters");
|
|
252
|
+
if (!params)
|
|
253
|
+
return 0;
|
|
254
|
+
return params.children().filter((ch) => {
|
|
255
|
+
const k = ch.kind();
|
|
256
|
+
if (k === "(" || k === ")" || k === ",")
|
|
257
|
+
return false;
|
|
258
|
+
const text = ch.text().split(":")[0].split("=")[0].trim();
|
|
259
|
+
return text !== "self" && text !== "cls";
|
|
260
|
+
}).length;
|
|
261
|
+
}
|
|
262
|
+
function findClasses(root, language) {
|
|
263
|
+
if (language === "python")
|
|
264
|
+
return findPythonClasses(root);
|
|
265
|
+
return findJsClasses(root);
|
|
266
|
+
}
|
|
267
|
+
function findJsClasses(root) {
|
|
268
|
+
const results = [];
|
|
269
|
+
for (const node of root.findAll({ rule: { kind: "class_declaration" } })) {
|
|
270
|
+
const nameNode = node.children().find((ch) => ch.kind() === "type_identifier" || ch.kind() === "identifier");
|
|
271
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
272
|
+
const methods = [];
|
|
273
|
+
const classBody = node.children().find((ch) => ch.kind() === "class_body");
|
|
274
|
+
if (classBody) {
|
|
275
|
+
for (const member of classBody.findAll({ rule: { kind: "method_definition" } })) {
|
|
276
|
+
const methodName = member.children().find((ch) => ch.kind() === "property_identifier" || ch.kind() === "identifier");
|
|
277
|
+
if (methodName)
|
|
278
|
+
methods.push(methodName.text());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
results.push({ node, name, methods });
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
function findPythonClasses(root) {
|
|
286
|
+
const results = [];
|
|
287
|
+
for (const node of root.findAll({ rule: { kind: "class_definition" } })) {
|
|
288
|
+
const nameNode = node.children().find((ch) => ch.kind() === "identifier");
|
|
289
|
+
const name = nameNode?.text() ?? "<anonymous>";
|
|
290
|
+
const methods = [];
|
|
291
|
+
for (const method of node.findAll({ rule: { kind: "function_definition" } })) {
|
|
292
|
+
const methodName = method.children().find((ch) => ch.kind() === "identifier");
|
|
293
|
+
if (methodName)
|
|
294
|
+
methods.push(methodName.text());
|
|
295
|
+
}
|
|
296
|
+
results.push({ node, name, methods });
|
|
297
|
+
}
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
function findExports(root, language) {
|
|
301
|
+
if (language === "python")
|
|
302
|
+
return [];
|
|
303
|
+
const results = [];
|
|
304
|
+
for (const node of root.findAll({ rule: { kind: "export_statement" } })) {
|
|
305
|
+
const children = node.children();
|
|
306
|
+
const hasDefault = children.some((ch) => ch.kind() === "default");
|
|
307
|
+
if (hasDefault) {
|
|
308
|
+
results.push({ node, name: "default", kind: "default" });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const funcDecl = children.find((ch) => ch.kind() === "function_declaration");
|
|
312
|
+
if (funcDecl) {
|
|
313
|
+
const name = funcDecl.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<unknown>";
|
|
314
|
+
results.push({ node, name, kind: "function" });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const classDecl = children.find((ch) => ch.kind() === "class_declaration");
|
|
318
|
+
if (classDecl) {
|
|
319
|
+
const nameNode = classDecl.children().find((ch) => ch.kind() === "type_identifier" || ch.kind() === "identifier");
|
|
320
|
+
results.push({ node, name: nameNode?.text() ?? "<unknown>", kind: "class" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const typeDecl = children.find((ch) => ch.kind() === "type_alias_declaration" || ch.kind() === "interface_declaration");
|
|
324
|
+
if (typeDecl) {
|
|
325
|
+
const name = typeDecl.children().find((ch) => ch.kind() === "type_identifier")?.text() ?? "<unknown>";
|
|
326
|
+
results.push({ node, name, kind: "type" });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const lexDecl = children.find((ch) => ch.kind() === "lexical_declaration");
|
|
330
|
+
if (lexDecl) {
|
|
331
|
+
const declarator = lexDecl.children().find((ch) => ch.kind() === "variable_declarator");
|
|
332
|
+
const name = declarator?.children().find((ch) => ch.kind() === "identifier")?.text() ?? "<unknown>";
|
|
333
|
+
results.push({ node, name, kind: "variable" });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
results.push({ node, name: "<unknown>", kind: "variable" });
|
|
337
|
+
}
|
|
338
|
+
return results;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/context/skeleton.ts
|
|
342
|
+
var EXTENSION_TO_LANG = {
|
|
343
|
+
".js": "javascript",
|
|
344
|
+
".jsx": "javascript",
|
|
345
|
+
".mjs": "javascript",
|
|
346
|
+
".cjs": "javascript",
|
|
347
|
+
".ts": "typescript",
|
|
348
|
+
".tsx": "tsx",
|
|
349
|
+
".py": "python"
|
|
350
|
+
};
|
|
351
|
+
var LANG_TO_SG = {
|
|
352
|
+
javascript: SgLang.JavaScript,
|
|
353
|
+
typescript: SgLang.TypeScript,
|
|
354
|
+
tsx: SgLang.Tsx,
|
|
355
|
+
python: "python"
|
|
356
|
+
};
|
|
357
|
+
var pythonRegistered = false;
|
|
358
|
+
function ensurePython() {
|
|
359
|
+
if (pythonRegistered)
|
|
360
|
+
return;
|
|
361
|
+
try {
|
|
362
|
+
const req = createRequire(import.meta.url);
|
|
363
|
+
const pythonLang = req("@ast-grep/lang-python");
|
|
364
|
+
registerDynamicLanguage({ python: pythonLang });
|
|
365
|
+
pythonRegistered = true;
|
|
366
|
+
} catch {}
|
|
367
|
+
}
|
|
368
|
+
function languageForExtension(ext) {
|
|
369
|
+
return EXTENSION_TO_LANG[ext] ?? null;
|
|
370
|
+
}
|
|
371
|
+
function extractSkeleton(source, language) {
|
|
372
|
+
if (language === "python")
|
|
373
|
+
ensurePython();
|
|
374
|
+
const sgLang = LANG_TO_SG[language];
|
|
375
|
+
let root;
|
|
376
|
+
try {
|
|
377
|
+
root = parse(sgLang, source).root();
|
|
378
|
+
} catch {
|
|
379
|
+
return fallbackSkeleton(source);
|
|
380
|
+
}
|
|
381
|
+
const lines = [];
|
|
382
|
+
const imports = findImports(root, language);
|
|
383
|
+
if (imports.length > 0) {
|
|
384
|
+
for (const imp of imports) {
|
|
385
|
+
lines.push(imp.text);
|
|
386
|
+
}
|
|
387
|
+
lines.push("");
|
|
388
|
+
}
|
|
389
|
+
const classes = findClasses(root, language);
|
|
390
|
+
for (const cls of classes) {
|
|
391
|
+
if (language === "python") {
|
|
392
|
+
lines.push(`class ${cls.name}:`);
|
|
393
|
+
} else {
|
|
394
|
+
lines.push(`class ${cls.name} {`);
|
|
395
|
+
}
|
|
396
|
+
for (const method of cls.methods) {
|
|
397
|
+
lines.push(` ${method}(...)`);
|
|
398
|
+
}
|
|
399
|
+
if (language !== "python")
|
|
400
|
+
lines.push("}");
|
|
401
|
+
lines.push("");
|
|
402
|
+
}
|
|
403
|
+
const functions = findFunctions(root, language);
|
|
404
|
+
const classMethodNodes = new Set(classes.flatMap((cls) => cls.node.findAll({ rule: { kind: language === "python" ? "function_definition" : "method_definition" } })).map((n) => n.range().start.line));
|
|
405
|
+
for (const fn of functions) {
|
|
406
|
+
if (classMethodNodes.has(fn.node.range().start.line))
|
|
407
|
+
continue;
|
|
408
|
+
const range = fn.node.range();
|
|
409
|
+
const sig = source.split(`
|
|
410
|
+
`).slice(range.start.line, range.start.line + 1)[0]?.trim() ?? "";
|
|
411
|
+
if (sig)
|
|
412
|
+
lines.push(sig.replace(/\{[\s\S]*$/, "{ ... }"));
|
|
413
|
+
}
|
|
414
|
+
if (functions.length > 0 && classes.length === 0)
|
|
415
|
+
lines.push("");
|
|
416
|
+
const exports = findExports(root, language);
|
|
417
|
+
for (const exp of exports) {
|
|
418
|
+
if (exp.kind === "function" || exp.kind === "class")
|
|
419
|
+
continue;
|
|
420
|
+
lines.push(`export ${exp.kind === "default" ? "default" : ""} ${exp.name}`.trim());
|
|
421
|
+
}
|
|
422
|
+
const skeleton = lines.join(`
|
|
423
|
+
`).trim();
|
|
424
|
+
return skeleton || fallbackSkeleton(source);
|
|
425
|
+
}
|
|
426
|
+
function fallbackSkeleton(source) {
|
|
427
|
+
const lines = source.split(`
|
|
428
|
+
`);
|
|
429
|
+
const skeleton = [];
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const trimmed = line.trim();
|
|
432
|
+
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)) {
|
|
433
|
+
skeleton.push(trimmed);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return skeleton.join(`
|
|
437
|
+
`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/context/benchmark.ts
|
|
441
|
+
import { readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
442
|
+
import { extname, join as join2, relative, resolve } from "path";
|
|
443
|
+
function walkSupported(dir, root, results) {
|
|
444
|
+
let entries;
|
|
445
|
+
try {
|
|
446
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
447
|
+
} catch {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const entry of entries) {
|
|
451
|
+
if (entry.name.startsWith("."))
|
|
452
|
+
continue;
|
|
453
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "build")
|
|
454
|
+
continue;
|
|
455
|
+
const fullPath = join2(dir, entry.name);
|
|
456
|
+
if (entry.isDirectory()) {
|
|
457
|
+
walkSupported(fullPath, root, results);
|
|
458
|
+
} else if (entry.isFile() && isSupportedExtension(fullPath)) {
|
|
459
|
+
try {
|
|
460
|
+
const stat = statSync2(fullPath);
|
|
461
|
+
if (stat.size > 500000 || stat.size === 0)
|
|
462
|
+
continue;
|
|
463
|
+
const source = readFileSync2(fullPath, "utf-8");
|
|
464
|
+
const lang = languageForExtension(extname(fullPath));
|
|
465
|
+
if (!lang)
|
|
466
|
+
continue;
|
|
467
|
+
const fullTokens = estimateTokens(source);
|
|
468
|
+
const skeleton = extractSkeleton(source, lang);
|
|
469
|
+
const skeletonTokens = estimateTokens(skeleton);
|
|
470
|
+
const reductionPercent = fullTokens > 0 ? Math.round((1 - (skeletonTokens + 128) / fullTokens) * 100) : 0;
|
|
471
|
+
results.push({
|
|
472
|
+
path: relative(root, fullPath),
|
|
473
|
+
fullTokens,
|
|
474
|
+
skeletonTokens,
|
|
475
|
+
reductionPercent: Math.max(0, reductionPercent)
|
|
476
|
+
});
|
|
477
|
+
} catch {}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function benchmark(projectRoot) {
|
|
482
|
+
const root = resolve(projectRoot);
|
|
483
|
+
const files = [];
|
|
484
|
+
walkSupported(root, root, files);
|
|
485
|
+
const totalTokens = files.reduce((sum, f) => sum + f.fullTokens, 0);
|
|
486
|
+
const sorted = [...files].sort((a, b) => b.fullTokens - a.fullTokens);
|
|
487
|
+
const projections = [20, 40, 60].map((rereadPercent) => {
|
|
488
|
+
const rereadCount = Math.round(files.length * rereadPercent / 100);
|
|
489
|
+
const rereadFiles = sorted.slice(0, rereadCount);
|
|
490
|
+
const tokensSaved = rereadFiles.reduce((sum, f) => {
|
|
491
|
+
const limited = 128 + f.skeletonTokens;
|
|
492
|
+
return sum + Math.max(0, f.fullTokens - limited);
|
|
493
|
+
}, 0);
|
|
494
|
+
return {
|
|
495
|
+
rereadPercent,
|
|
496
|
+
tokensSaved,
|
|
497
|
+
percentOfTotal: totalTokens > 0 ? Math.round(tokensSaved / totalTokens * 100) : 0
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
return { files: sorted, totalFiles: files.length, totalTokens, projections };
|
|
501
|
+
}
|
|
502
|
+
function formatBenchmark(result) {
|
|
503
|
+
if (result.totalFiles === 0) {
|
|
504
|
+
return "No supported files found (.js, .ts, .tsx, .py).";
|
|
505
|
+
}
|
|
506
|
+
const lines = [];
|
|
507
|
+
lines.push("vibecop context benchmark");
|
|
508
|
+
lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
509
|
+
lines.push("");
|
|
510
|
+
lines.push(`Files: ${result.totalFiles} supported`);
|
|
511
|
+
lines.push(`Total tokens: ~${result.totalTokens.toLocaleString()}`);
|
|
512
|
+
lines.push("");
|
|
513
|
+
const top = result.files.slice(0, 10);
|
|
514
|
+
lines.push("Largest files (most savings potential):");
|
|
515
|
+
const maxPath = Math.max(...top.map((f) => f.path.length), 10);
|
|
516
|
+
for (const f of top) {
|
|
517
|
+
const pathPad = f.path.padEnd(maxPath);
|
|
518
|
+
lines.push(` ${pathPad} ${f.fullTokens.toLocaleString().padStart(6)} tokens \u2192 skeleton: ${f.skeletonTokens.toLocaleString().padStart(5)} (${f.reductionPercent}% reduction)`);
|
|
519
|
+
}
|
|
520
|
+
lines.push("");
|
|
521
|
+
lines.push("Projected savings per session:");
|
|
522
|
+
for (const p of result.projections) {
|
|
523
|
+
lines.push(` ${p.rereadPercent}% re-read rate: ~${p.tokensSaved.toLocaleString()} tokens saved (${p.percentOfTotal}% of total Read usage)`);
|
|
524
|
+
}
|
|
525
|
+
lines.push("");
|
|
526
|
+
lines.push("To enable: vibecop init --context");
|
|
527
|
+
return lines.join(`
|
|
528
|
+
`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/context/stats.ts
|
|
532
|
+
function formatStats(stats) {
|
|
533
|
+
if (stats.length === 0)
|
|
534
|
+
return "No context optimization data recorded yet.\nRun `vibecop context benchmark` to see projected savings for this project.";
|
|
535
|
+
const lines = [];
|
|
536
|
+
let totalReads = 0;
|
|
537
|
+
let totalHits = 0;
|
|
538
|
+
let totalSaved = 0;
|
|
539
|
+
for (const s of stats) {
|
|
540
|
+
totalReads += s.totalReads;
|
|
541
|
+
totalHits += s.cacheHits;
|
|
542
|
+
totalSaved += s.tokensSaved;
|
|
543
|
+
}
|
|
544
|
+
const hitRate = totalReads > 0 ? (totalHits / totalReads * 100).toFixed(1) : "0.0";
|
|
545
|
+
lines.push("vibecop context optimization");
|
|
546
|
+
lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
547
|
+
lines.push("");
|
|
548
|
+
lines.push(`Sessions: ${stats.length}`);
|
|
549
|
+
lines.push(`Total reads: ${totalReads}`);
|
|
550
|
+
lines.push(`Cache hits: ${totalHits} (${hitRate}% of reads)`);
|
|
551
|
+
lines.push(`Tokens saved: ~${totalSaved.toLocaleString()}`);
|
|
552
|
+
if (totalSaved > 0 && totalHits > 0) {
|
|
553
|
+
const avgSavedPerHit = Math.round(totalSaved / totalHits);
|
|
554
|
+
lines.push(`Avg savings: ~${avgSavedPerHit.toLocaleString()} tokens per re-read`);
|
|
555
|
+
}
|
|
556
|
+
lines.push("");
|
|
557
|
+
if (stats.length <= 10 && stats.length > 0) {
|
|
558
|
+
lines.push("Per-session:");
|
|
559
|
+
for (const s of stats) {
|
|
560
|
+
const rate = s.totalReads > 0 ? (s.cacheHits / s.totalReads * 100).toFixed(0) : "0";
|
|
561
|
+
lines.push(` ${s.sessionId.slice(0, 8)}\u2026 ${s.totalReads} reads, ${s.cacheHits} hits (${rate}%), ~${s.tokensSaved.toLocaleString()} saved`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return lines.join(`
|
|
565
|
+
`);
|
|
566
|
+
}
|
|
567
|
+
function printStats(db, sessionId) {
|
|
568
|
+
if (sessionId) {
|
|
569
|
+
const stats = getSessionStats(db, sessionId);
|
|
570
|
+
if (stats) {
|
|
571
|
+
console.log(formatStats([stats]));
|
|
572
|
+
} else {
|
|
573
|
+
console.log(`No stats found for session ${sessionId}`);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
console.log(formatStats(getAllStats(db)));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/context.ts
|
|
581
|
+
function findProjectRoot() {
|
|
582
|
+
let dir = process.cwd();
|
|
583
|
+
while (true) {
|
|
584
|
+
try {
|
|
585
|
+
const entries = new Set(readdirSync2(dir));
|
|
586
|
+
if (entries.has(".vibecop.yml") || entries.has(".git"))
|
|
587
|
+
return dir;
|
|
588
|
+
} catch {}
|
|
589
|
+
const parent = dirname2(dir);
|
|
590
|
+
if (parent === dir)
|
|
591
|
+
return process.cwd();
|
|
592
|
+
dir = parent;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function handlePre(input) {
|
|
596
|
+
const filePath = input.tool_input?.file_path;
|
|
597
|
+
if (!filePath) {
|
|
598
|
+
console.log("{}");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (!isSupportedExtension(filePath)) {
|
|
602
|
+
console.log("{}");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (input.tool_input.offset !== undefined && input.tool_input.offset > 0) {
|
|
606
|
+
console.log("{}");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const sessionId = input.session_id;
|
|
610
|
+
if (!sessionId) {
|
|
611
|
+
console.log("{}");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const projectRoot = findProjectRoot();
|
|
615
|
+
try {
|
|
616
|
+
const db = openDb(projectRoot);
|
|
617
|
+
try {
|
|
618
|
+
const resolvedPath = resolve2(filePath);
|
|
619
|
+
const currentHash = hashFile(resolvedPath);
|
|
620
|
+
if (!currentHash) {
|
|
621
|
+
console.log("{}");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const previousRead = hasSessionRead(db, sessionId, resolvedPath);
|
|
625
|
+
if (!previousRead) {
|
|
626
|
+
const cached = getSkeleton(db, resolvedPath, currentHash);
|
|
627
|
+
if (cached) {
|
|
628
|
+
const response = {
|
|
629
|
+
additionalContext: `[vibecop] File structure:
|
|
630
|
+
${cached.skeleton}`
|
|
631
|
+
};
|
|
632
|
+
console.log(JSON.stringify(response));
|
|
633
|
+
} else {
|
|
634
|
+
console.log("{}");
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (previousRead.hash === currentHash) {
|
|
639
|
+
const cached = getSkeleton(db, resolvedPath, currentHash);
|
|
640
|
+
const response = {
|
|
641
|
+
updatedInput: {
|
|
642
|
+
file_path: filePath,
|
|
643
|
+
limit: 30
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
if (cached) {
|
|
647
|
+
response.additionalContext = `[vibecop] File unchanged since last read. Structure:
|
|
648
|
+
${cached.skeleton}`;
|
|
649
|
+
const limitedTokens = 128 + cached.skeletonTokens;
|
|
650
|
+
const saved = Math.max(0, cached.fullTokens - limitedTokens);
|
|
651
|
+
incrementStats(db, sessionId, { cacheHits: 1, tokensSaved: saved });
|
|
652
|
+
} else {
|
|
653
|
+
incrementStats(db, sessionId, { cacheHits: 1 });
|
|
654
|
+
}
|
|
655
|
+
console.log(JSON.stringify(response));
|
|
656
|
+
} else {
|
|
657
|
+
const response = {
|
|
658
|
+
additionalContext: "[vibecop] File has changed since last read."
|
|
659
|
+
};
|
|
660
|
+
console.log(JSON.stringify(response));
|
|
661
|
+
}
|
|
662
|
+
} finally {
|
|
663
|
+
db.close();
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
console.log("{}");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function handlePost(input) {
|
|
670
|
+
const filePath = input.tool_input?.file_path;
|
|
671
|
+
if (!filePath)
|
|
672
|
+
return;
|
|
673
|
+
if (!isSupportedExtension(filePath))
|
|
674
|
+
return;
|
|
675
|
+
const sessionId = input.session_id;
|
|
676
|
+
if (!sessionId)
|
|
677
|
+
return;
|
|
678
|
+
const projectRoot = findProjectRoot();
|
|
679
|
+
try {
|
|
680
|
+
const db = openDb(projectRoot);
|
|
681
|
+
try {
|
|
682
|
+
const resolvedPath = resolve2(filePath);
|
|
683
|
+
if (!isFileEligible(resolvedPath))
|
|
684
|
+
return;
|
|
685
|
+
const currentHash = hashFile(resolvedPath);
|
|
686
|
+
if (!currentHash)
|
|
687
|
+
return;
|
|
688
|
+
recordSessionRead(db, sessionId, resolvedPath, currentHash);
|
|
689
|
+
incrementStats(db, sessionId, { totalReads: 1 });
|
|
690
|
+
const existing = getSkeleton(db, resolvedPath, currentHash);
|
|
691
|
+
if (!existing) {
|
|
692
|
+
const ext = extname2(resolvedPath);
|
|
693
|
+
const lang = languageForExtension(ext);
|
|
694
|
+
if (!lang)
|
|
695
|
+
return;
|
|
696
|
+
const source = readFileSync3(resolvedPath, "utf-8");
|
|
697
|
+
const skeleton = extractSkeleton(source, lang);
|
|
698
|
+
if (skeleton) {
|
|
699
|
+
const fullTokens = estimateTokens(source);
|
|
700
|
+
const skeletonTokens = estimateTokens(skeleton);
|
|
701
|
+
upsertSkeleton(db, resolvedPath, currentHash, skeleton, lang, fullTokens, skeletonTokens);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} finally {
|
|
705
|
+
db.close();
|
|
706
|
+
}
|
|
707
|
+
} catch {}
|
|
708
|
+
}
|
|
709
|
+
function handleCompact(input) {
|
|
710
|
+
const sessionId = input.session_id;
|
|
711
|
+
if (!sessionId)
|
|
712
|
+
return;
|
|
713
|
+
const projectRoot = findProjectRoot();
|
|
714
|
+
try {
|
|
715
|
+
const db = openDb(projectRoot);
|
|
716
|
+
try {
|
|
717
|
+
pruneOldSessions(db, 7);
|
|
718
|
+
} finally {
|
|
719
|
+
db.close();
|
|
720
|
+
}
|
|
721
|
+
} catch {}
|
|
722
|
+
}
|
|
723
|
+
function handleStats(sessionId) {
|
|
724
|
+
const projectRoot = findProjectRoot();
|
|
725
|
+
try {
|
|
726
|
+
const db = openDb(projectRoot);
|
|
727
|
+
try {
|
|
728
|
+
printStats(db, sessionId);
|
|
729
|
+
} finally {
|
|
730
|
+
db.close();
|
|
731
|
+
}
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.error(`Failed to read stats: ${err instanceof Error ? err.message : String(err)}`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
var args = process.argv.slice(2);
|
|
738
|
+
var command = args[0];
|
|
739
|
+
if (command === "stats") {
|
|
740
|
+
handleStats(args[1]);
|
|
741
|
+
} else if (command === "benchmark") {
|
|
742
|
+
console.log(formatBenchmark(benchmark(findProjectRoot())));
|
|
743
|
+
} else if (command === "--pre" || command === "--post" || command === "--compact") {
|
|
744
|
+
try {
|
|
745
|
+
const stdin = readFileSync3("/dev/stdin", "utf-8");
|
|
746
|
+
const input = JSON.parse(stdin);
|
|
747
|
+
switch (command) {
|
|
748
|
+
case "--pre":
|
|
749
|
+
handlePre(input);
|
|
750
|
+
break;
|
|
751
|
+
case "--post":
|
|
752
|
+
handlePost(input);
|
|
753
|
+
break;
|
|
754
|
+
case "--compact":
|
|
755
|
+
handleCompact(input);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
} catch {
|
|
759
|
+
if (command === "--pre")
|
|
760
|
+
console.log("{}");
|
|
761
|
+
}
|
|
762
|
+
} else {
|
|
763
|
+
console.error("Usage: vibecop context [--pre|--post|--compact|stats]");
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecop",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "AI code quality toolkit — deterministic linter for the AI coding era",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"lint": "bunx biome check",
|
|
15
15
|
"typecheck": "bunx tsc --noEmit",
|
|
16
16
|
"test:install": "bash scripts/test-install.sh",
|
|
17
|
-
"prepublishOnly": "bun run lint && bun run typecheck && bun test && bun run build && bun run test:install"
|
|
17
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun test && bun run build && bun run build:context && bun run test:install"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist/cli.js",
|