vexp-cli 2.2.1 → 2.2.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/dist/agent-config.js +420 -43
- package/dist/cli.js +200 -31
- package/dist/hook-template.js +259 -26
- package/dist/workspace-repos.js +166 -0
- package/mcp/mcp-server.cjs +48 -43
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from "fs";
|
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as crypto from "crypto";
|
|
13
|
-
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD } from "./hook-template.js";
|
|
13
|
+
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD } from "./hook-template.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Constants
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -345,10 +345,47 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
345
345
|
const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
346
346
|
if (wrote)
|
|
347
347
|
mcpConfigs.push(wrote);
|
|
348
|
+
// Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
|
|
349
|
+
// markdown alone was demonstrably not enough (a reported session loaded
|
|
350
|
+
// vexp.md, quoted it back, and still read five files by hand).
|
|
351
|
+
const kilo = installKiloPlugin(workspaceRoot);
|
|
352
|
+
if (kilo.plugin) {
|
|
353
|
+
results.push({
|
|
354
|
+
agent: "Kilo Code Guard",
|
|
355
|
+
configFile: path.join(".kilo", "plugins", "vexp-guard.js"),
|
|
356
|
+
content: VEXP_OPENCODE_GUARD,
|
|
357
|
+
alreadyExists: kilo.plugin === "updated",
|
|
358
|
+
action: kilo.plugin,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (kilo.registered)
|
|
362
|
+
mcpConfigs.push(path.relative(workspaceRoot, kiloConfigTarget(workspaceRoot)));
|
|
363
|
+
}
|
|
364
|
+
// Cursor: preToolUse hook matching Grep. Text search only - Cursor's native
|
|
365
|
+
// semantic search is not hookable.
|
|
366
|
+
if (detector.agent === "Cursor") {
|
|
367
|
+
const cur = installCursorHook(workspaceRoot);
|
|
368
|
+
if (cur.hook) {
|
|
369
|
+
results.push({
|
|
370
|
+
agent: "Cursor Guard",
|
|
371
|
+
configFile: path.join(".cursor", "hooks", "vexp-guard.js"),
|
|
372
|
+
content: VEXP_CURSOR_GUARD,
|
|
373
|
+
alreadyExists: cur.hook === "updated",
|
|
374
|
+
action: cur.hook,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (cur.registered)
|
|
378
|
+
mcpConfigs.push(path.join(".cursor", "hooks.json"));
|
|
348
379
|
}
|
|
349
|
-
// Opencode:
|
|
350
|
-
//
|
|
380
|
+
// Opencode: MCP lives under the `mcp` key in opencode.json(c) — opencode
|
|
381
|
+
// carries no `mcpConfigFile`, so without this call the generic writer above
|
|
382
|
+
// skips it and setup registers no tool at all. Plus the guard plugin that
|
|
383
|
+
// blocks grep/glob while the daemon is up (opencode's analogue of the
|
|
384
|
+
// Claude Code PreToolUse hook) — which is only safe once MCP is registered.
|
|
351
385
|
if (detector.agent === "Opencode") {
|
|
386
|
+
const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
387
|
+
if (wroteMcp)
|
|
388
|
+
mcpConfigs.push(wroteMcp);
|
|
352
389
|
const pluginResult = installOpencodePlugin(workspaceRoot);
|
|
353
390
|
if (pluginResult) {
|
|
354
391
|
results.push({
|
|
@@ -376,6 +413,60 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
376
413
|
export function getAgentList() {
|
|
377
414
|
return [...AGENT_DETECTORS];
|
|
378
415
|
}
|
|
416
|
+
/** Fold a name to its comparable form: case, spaces, dots and dashes carry no
|
|
417
|
+
* meaning here ("claude-code", "Claude Code" and "claudecode" are one agent). */
|
|
418
|
+
function foldAgentName(s) {
|
|
419
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Canonicalize a name given to `--agents`, or null if it names no agent.
|
|
423
|
+
*
|
|
424
|
+
* `configureSelectedAgents` matches with `Array.includes` on the exact string,
|
|
425
|
+
* so "opencode" silently configured nothing while "Opencode" worked — nobody
|
|
426
|
+
* can be expected to know the capitalization, and the failure was invisible.
|
|
427
|
+
*/
|
|
428
|
+
export function resolveAgentName(input) {
|
|
429
|
+
const want = foldAgentName(input);
|
|
430
|
+
if (!want)
|
|
431
|
+
return null;
|
|
432
|
+
return AGENT_DETECTORS.find((d) => foldAgentName(d.agent) === want)?.agent ?? null;
|
|
433
|
+
}
|
|
434
|
+
/** Levenshtein distance, for "did you mean" on an unknown --agents name. */
|
|
435
|
+
function editDistance(a, b) {
|
|
436
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
437
|
+
const cur = new Array(b.length + 1).fill(0);
|
|
438
|
+
for (let i = 1; i <= a.length; i++) {
|
|
439
|
+
cur[0] = i;
|
|
440
|
+
for (let j = 1; j <= b.length; j++) {
|
|
441
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
442
|
+
}
|
|
443
|
+
prev.splice(0, prev.length, ...cur);
|
|
444
|
+
}
|
|
445
|
+
return prev[b.length];
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* The known agent an unknown name most likely meant, or null when nothing is
|
|
449
|
+
* close enough to be worth suggesting. Substring hits win (a bare "copilot"
|
|
450
|
+
* means "GitHub Copilot"); otherwise allow a couple of typos.
|
|
451
|
+
*/
|
|
452
|
+
export function suggestAgentName(input) {
|
|
453
|
+
const want = foldAgentName(input);
|
|
454
|
+
if (!want)
|
|
455
|
+
return null;
|
|
456
|
+
const contained = AGENT_DETECTORS.find((d) => {
|
|
457
|
+
const f = foldAgentName(d.agent);
|
|
458
|
+
return f.includes(want) || want.includes(f);
|
|
459
|
+
});
|
|
460
|
+
if (contained)
|
|
461
|
+
return contained.agent;
|
|
462
|
+
let best = null;
|
|
463
|
+
for (const d of AGENT_DETECTORS) {
|
|
464
|
+
const dist = editDistance(want, foldAgentName(d.agent));
|
|
465
|
+
if (!best || dist < best.d)
|
|
466
|
+
best = { name: d.agent, d: dist };
|
|
467
|
+
}
|
|
468
|
+
return best && best.d <= 3 ? best.name : null;
|
|
469
|
+
}
|
|
379
470
|
/**
|
|
380
471
|
* Configure specific agents selected by the user.
|
|
381
472
|
* Unlike configureAgents which auto-detects, this forces configuration
|
|
@@ -469,8 +560,24 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
469
560
|
mcpConfigs.push(".zed/settings.json");
|
|
470
561
|
}
|
|
471
562
|
if (detector.agent === "Opencode") {
|
|
563
|
+
const wrote = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
564
|
+
if (wrote)
|
|
565
|
+
mcpConfigs.push(wrote);
|
|
472
566
|
installOpencodePlugin(workspaceRoot);
|
|
473
567
|
}
|
|
568
|
+
if (detector.agent === "Cursor") {
|
|
569
|
+
installCursorHook(workspaceRoot);
|
|
570
|
+
}
|
|
571
|
+
// Kilo carries no `mcpConfigFile` (its MCP lives under the `mcp` key inside
|
|
572
|
+
// kilo.jsonc), so without this branch the generic writer above skips it and
|
|
573
|
+
// picking Kilo explicitly used to write the rules file and NOTHING else — an
|
|
574
|
+
// agent told "always call run_pipeline" while the tool was never registered.
|
|
575
|
+
if (detector.agent === "Kilo Code") {
|
|
576
|
+
const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
577
|
+
if (wrote)
|
|
578
|
+
mcpConfigs.push(wrote);
|
|
579
|
+
installKiloPlugin(workspaceRoot);
|
|
580
|
+
}
|
|
474
581
|
results.push({ agent: detector.agent, configFile: detector.configFile, content, alreadyExists, action });
|
|
475
582
|
}
|
|
476
583
|
return { agents: results, mcpConfigs };
|
|
@@ -1070,14 +1177,93 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
1070
1177
|
* an existing file is backed up before rewrite. Returns the workspace-relative
|
|
1071
1178
|
* path written, or null on a no-op / unparseable file.
|
|
1072
1179
|
*/
|
|
1073
|
-
|
|
1180
|
+
/**
|
|
1181
|
+
* The kilo.jsonc that Kilo will actually read: `.kilo/kilo.jsonc` wins over a
|
|
1182
|
+
* root `kilo.jsonc` when both exist, and a fresh setup gets the cleaner `.kilo/`
|
|
1183
|
+
* location. Shared by the MCP writer and the guard-plugin installer so the two
|
|
1184
|
+
* can never disagree about which file is live.
|
|
1185
|
+
*/
|
|
1186
|
+
export function kiloConfigTarget(workspaceRoot) {
|
|
1074
1187
|
const dotKilo = path.join(workspaceRoot, ".kilo", "kilo.jsonc");
|
|
1075
1188
|
const rootKilo = path.join(workspaceRoot, "kilo.jsonc");
|
|
1076
|
-
|
|
1189
|
+
return fs.existsSync(dotKilo)
|
|
1077
1190
|
? dotKilo
|
|
1078
1191
|
: fs.existsSync(rootKilo)
|
|
1079
1192
|
? rootKilo
|
|
1080
|
-
: dotKilo;
|
|
1193
|
+
: dotKilo;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* The opencode config file that opencode will actually read for `mcp`:
|
|
1197
|
+
* `opencode.jsonc` wins over `opencode.json` when both exist (jsonc is the
|
|
1198
|
+
* variant users keep when they want comments, and it is the one they edit),
|
|
1199
|
+
* and a fresh setup gets plain `opencode.json` — opencode's documented default.
|
|
1200
|
+
* Project-scope only: we never touch the machine-global
|
|
1201
|
+
* `~/.config/opencode/opencode.json`, because a per-workspace VEXP_WORKSPACE pin
|
|
1202
|
+
* in a global file would mis-target every other project on the machine.
|
|
1203
|
+
*/
|
|
1204
|
+
export function opencodeConfigTarget(workspaceRoot) {
|
|
1205
|
+
const jsonc = path.join(workspaceRoot, "opencode.jsonc");
|
|
1206
|
+
const json = path.join(workspaceRoot, "opencode.json");
|
|
1207
|
+
return fs.existsSync(jsonc) ? jsonc : json;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Opencode MCP setup. Opencode reads MCP servers from the top-level `mcp` key
|
|
1211
|
+
* of `opencode.json` / `opencode.jsonc` — the same shape Kilo Code uses, because
|
|
1212
|
+
* Kilo v7 vendors opencode wholesale: `{type:"local", command:[…], enabled:true}`.
|
|
1213
|
+
*
|
|
1214
|
+
* Until this existed, picking Opencode in `vexp setup` wrote AGENTS.md and the
|
|
1215
|
+
* guard plugin and NOTHING else — the worst possible end state, because the
|
|
1216
|
+
* guard blocks grep/glob whenever the daemon is healthy while `run_pipeline` was
|
|
1217
|
+
* never registered: the agent lost search and gained no replacement. Reported by
|
|
1218
|
+
* an AppSumo user who had to add the `mcp` block by hand.
|
|
1219
|
+
*
|
|
1220
|
+
* Other `mcp` entries and unrelated top-level keys are preserved; an existing
|
|
1221
|
+
* file is backed up before rewrite. Returns the workspace-relative path written,
|
|
1222
|
+
* or null on a no-op / unparseable file.
|
|
1223
|
+
*/
|
|
1224
|
+
export function configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
1225
|
+
const target = opencodeConfigTarget(workspaceRoot);
|
|
1226
|
+
const read = readJsonConfigSafe(target);
|
|
1227
|
+
if (!read.ok) {
|
|
1228
|
+
warnUnparseable(target);
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const cfg = read.data;
|
|
1232
|
+
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1233
|
+
const command = useNode ? ["node", mcpServerPath] : [binaryPath, "mcp"];
|
|
1234
|
+
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
1235
|
+
const mcp = cfg.mcp ?? {};
|
|
1236
|
+
const prev = mcp["vexp"];
|
|
1237
|
+
const envMatches = JSON.stringify(prev?.["env"]) === JSON.stringify(env);
|
|
1238
|
+
const identical = prev?.["type"] === "local" &&
|
|
1239
|
+
prev?.["enabled"] === true &&
|
|
1240
|
+
Array.isArray(prev?.["command"]) &&
|
|
1241
|
+
JSON.stringify(prev["command"]) === JSON.stringify(command) &&
|
|
1242
|
+
envMatches;
|
|
1243
|
+
if (identical)
|
|
1244
|
+
return null;
|
|
1245
|
+
// A working entry is already here — either another vexp install, or the user's
|
|
1246
|
+
// own hand-written block (this bug made several people write one). Don't fight
|
|
1247
|
+
// it: only the install path may differ, and `envMatches` still gates on the
|
|
1248
|
+
// VEXP_WORKSPACE pin so a moved project is repinned normally.
|
|
1249
|
+
if (envMatches && prev?.["enabled"] === true && vexpEntryStillResolves(prev))
|
|
1250
|
+
return null;
|
|
1251
|
+
mcp["vexp"] = { type: "local", command, env, enabled: true };
|
|
1252
|
+
cfg.mcp = mcp;
|
|
1253
|
+
// `$schema` first if opencode's own key is absent — a fresh file we author
|
|
1254
|
+
// should look like the one opencode's docs tell users to write.
|
|
1255
|
+
if (cfg.$schema === undefined) {
|
|
1256
|
+
cfg.$schema = "https://opencode.ai/config.json";
|
|
1257
|
+
}
|
|
1258
|
+
if (read.existed)
|
|
1259
|
+
backupConfig(target);
|
|
1260
|
+
// A .jsonc target loses its comments here (we reserialize parsed JSON), which
|
|
1261
|
+
// is why the pre-write backup is not optional for this writer.
|
|
1262
|
+
fs.writeFileSync(target, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1263
|
+
return path.relative(workspaceRoot, target);
|
|
1264
|
+
}
|
|
1265
|
+
export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
1266
|
+
const target = kiloConfigTarget(workspaceRoot);
|
|
1081
1267
|
const read = readJsonConfigSafe(target);
|
|
1082
1268
|
if (!read.ok) {
|
|
1083
1269
|
warnUnparseable(target);
|
|
@@ -1349,19 +1535,155 @@ export function installClaudeCodeHook(workspaceRoot) {
|
|
|
1349
1535
|
* startup — no config-file merge required). Returns the action taken, or null
|
|
1350
1536
|
* when the file is already byte-identical.
|
|
1351
1537
|
*/
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
fs.mkdirSync(pluginDir, { recursive: true });
|
|
1538
|
+
function writeGuardScript(scriptPath, content) {
|
|
1539
|
+
const pluginPath = scriptPath;
|
|
1540
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
1356
1541
|
const existed = fs.existsSync(pluginPath);
|
|
1357
1542
|
if (existed) {
|
|
1358
1543
|
const current = fs.readFileSync(pluginPath, "utf-8");
|
|
1359
|
-
if (current ===
|
|
1544
|
+
if (current === content)
|
|
1360
1545
|
return null; // identical - skip
|
|
1546
|
+
// Content differs: either an older vexp guard, or a copy the user tuned by
|
|
1547
|
+
// hand. We refresh either way — refusing to touch a hand-edited plugin would
|
|
1548
|
+
// freeze that user on a stale guard forever — but their version stays
|
|
1549
|
+
// recoverable at vexp-guard.js.vexp-bak instead of being silently discarded.
|
|
1550
|
+
backupConfig(pluginPath);
|
|
1361
1551
|
}
|
|
1362
|
-
fs.writeFileSync(pluginPath,
|
|
1552
|
+
fs.writeFileSync(pluginPath, content, "utf-8");
|
|
1363
1553
|
return existed ? "updated" : "created";
|
|
1364
1554
|
}
|
|
1555
|
+
export function installOpencodePlugin(workspaceRoot) {
|
|
1556
|
+
return writeGuardScript(path.join(workspaceRoot, ".opencode", "plugins", "vexp-guard.js"), VEXP_OPENCODE_GUARD);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Cursor enforcement — `preToolUse` hook with matcher `Grep`.
|
|
1560
|
+
*
|
|
1561
|
+
* Cursor is the only VS Code fork where this is possible: Windsurf's hooks are
|
|
1562
|
+
* action-category based (twelve fixed events, no matcher, no tool names) and
|
|
1563
|
+
* expose NO search event at all, so there is nothing there to redirect. Cursor
|
|
1564
|
+
* exposes `Grep` as a matchable tool and feeds `agent_message` back to the model,
|
|
1565
|
+
* which is the whole mechanism.
|
|
1566
|
+
*
|
|
1567
|
+
* Scope of the claim, deliberately narrow: this covers TEXT search. Cursor's
|
|
1568
|
+
* native semantic codebase search has no documented tool identifier, so it is not
|
|
1569
|
+
* hookable and not covered. Do not describe this as "Cursor is enforced".
|
|
1570
|
+
*
|
|
1571
|
+
* `command` is `node`, not a shell script: Cursor documents no PowerShell variant
|
|
1572
|
+
* and never says which shell runs `command` on Windows. A bash guard would repeat
|
|
1573
|
+
* the Claude Code hook's Windows bug (silently enforced nothing for releases).
|
|
1574
|
+
* The path is project-root-relative because Cursor runs PROJECT hooks from the
|
|
1575
|
+
* project root — `./hooks/x` there would resolve to `<project>/hooks/x`.
|
|
1576
|
+
*
|
|
1577
|
+
* `failClosed` is left at its default (false): a crashing guard must let the
|
|
1578
|
+
* agent work, not lock it out.
|
|
1579
|
+
*/
|
|
1580
|
+
export function installCursorHook(workspaceRoot) {
|
|
1581
|
+
const rel = path.join(".cursor", "hooks", "vexp-guard.js");
|
|
1582
|
+
const hookPath = path.join(workspaceRoot, rel);
|
|
1583
|
+
const hook = writeGuardScript(hookPath, VEXP_CURSOR_GUARD);
|
|
1584
|
+
const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
|
|
1585
|
+
const command = `node ${rel.split(path.sep).join("/")}`;
|
|
1586
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
1587
|
+
if (!read.ok) {
|
|
1588
|
+
warnUnparseable(cfgPath);
|
|
1589
|
+
return { hook, registered: false };
|
|
1590
|
+
}
|
|
1591
|
+
const cfg = read.data;
|
|
1592
|
+
const hooks = cfg.hooks ?? {};
|
|
1593
|
+
const preToolUse = Array.isArray(hooks.preToolUse) ? [...hooks.preToolUse] : [];
|
|
1594
|
+
// Idempotent by COMMAND, not by array position: re-running setup must not
|
|
1595
|
+
// append a second copy, and a user's own preToolUse hooks must survive.
|
|
1596
|
+
const ours = (e) => !!e && typeof e === "object" && e.command === command;
|
|
1597
|
+
const existing = preToolUse.findIndex(ours);
|
|
1598
|
+
const entry = { command, matcher: "Grep" };
|
|
1599
|
+
if (existing >= 0 && JSON.stringify(preToolUse[existing]) === JSON.stringify(entry)) {
|
|
1600
|
+
return { hook, registered: false }; // already exactly right
|
|
1601
|
+
}
|
|
1602
|
+
if (existing >= 0)
|
|
1603
|
+
preToolUse[existing] = entry;
|
|
1604
|
+
else
|
|
1605
|
+
preToolUse.push(entry);
|
|
1606
|
+
hooks.preToolUse = preToolUse;
|
|
1607
|
+
cfg.hooks = hooks;
|
|
1608
|
+
if (cfg.version === undefined)
|
|
1609
|
+
cfg.version = 1;
|
|
1610
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
1611
|
+
if (read.existed)
|
|
1612
|
+
backupConfig(cfgPath);
|
|
1613
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1614
|
+
return { hook, registered: true };
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Kilo Code enforcement — the same plugin, because Kilo v7 IS opencode: the
|
|
1618
|
+
* rewrite vendors it wholesale (the loader lives at packages/opencode/src/ inside
|
|
1619
|
+
* Kilo-Org/kilocode), so `tool.execute.before`, the `input.tool` ids and the
|
|
1620
|
+
* grep/glob arg schemas are the same code, not merely a similar API.
|
|
1621
|
+
*
|
|
1622
|
+
* Loading is by auto-discovery: the loader globs `{plugin,plugins}/*.{ts,js}`
|
|
1623
|
+
* with cwd = the directory of the kilo.jsonc it read, and every match is
|
|
1624
|
+
* registered at startup with no config entry. So `.kilo/plugins/vexp-guard.js`
|
|
1625
|
+
* is picked up for free — and we prefer that to an explicit `plugin` array entry
|
|
1626
|
+
* because it leaves the user's config untouched.
|
|
1627
|
+
*
|
|
1628
|
+
* The one case auto-discovery misses: a project whose config is the ROOT
|
|
1629
|
+
* `kilo.jsonc`, where cwd is the repo root and the scanned directory would be
|
|
1630
|
+
* `<root>/plugins/` — we will not create a top-level `plugins/` dir in someone's
|
|
1631
|
+
* repo just to be found. There we keep the file in `.kilo/plugins/` and register
|
|
1632
|
+
* it explicitly, with the path relative to the config file (the loader resolves
|
|
1633
|
+
* path specs against the declaring config's own directory, NOT the project root).
|
|
1634
|
+
*
|
|
1635
|
+
* The glob is non-recursive, so the file must sit flat in that directory.
|
|
1636
|
+
*
|
|
1637
|
+
* NOTE: `KILO_PURE=1` skips all external plugins. A CI run with it set gets no
|
|
1638
|
+
* guard — correctly so, but it means "the guard is installed" is not the same
|
|
1639
|
+
* claim as "the guard ran".
|
|
1640
|
+
*/
|
|
1641
|
+
export function installKiloPlugin(workspaceRoot) {
|
|
1642
|
+
const pluginPath = path.join(workspaceRoot, ".kilo", "plugins", "vexp-guard.js");
|
|
1643
|
+
const plugin = writeGuardScript(pluginPath, VEXP_OPENCODE_GUARD);
|
|
1644
|
+
// `.kilo/kilo.jsonc` (the common case) → auto-discovery already covers us.
|
|
1645
|
+
const target = kiloConfigTarget(workspaceRoot);
|
|
1646
|
+
if (path.dirname(target) !== workspaceRoot)
|
|
1647
|
+
return { plugin, registered: false };
|
|
1648
|
+
const rel = "./" + path.relative(workspaceRoot, pluginPath).split(path.sep).join("/");
|
|
1649
|
+
const read = readJsonConfigSafe(target);
|
|
1650
|
+
if (!read.ok) {
|
|
1651
|
+
warnUnparseable(target);
|
|
1652
|
+
return { plugin, registered: false };
|
|
1653
|
+
}
|
|
1654
|
+
const cfg = read.data;
|
|
1655
|
+
const list = Array.isArray(cfg.plugin) ? cfg.plugin : [];
|
|
1656
|
+
if (list.includes(rel))
|
|
1657
|
+
return { plugin, registered: false }; // already declared
|
|
1658
|
+
cfg.plugin = [...list, rel];
|
|
1659
|
+
if (read.existed)
|
|
1660
|
+
backupConfig(target);
|
|
1661
|
+
fs.writeFileSync(target, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1662
|
+
return { plugin, registered: true };
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* The single highest-leverage instruction we ship, and for a long time the one
|
|
1666
|
+
* most agents never saw.
|
|
1667
|
+
*
|
|
1668
|
+
* vexp retrieval is identifier/path-first: FTS supplies the candidate pool and
|
|
1669
|
+
* the semantic layer only RE-RANKS it, so a symbol that no query token reaches
|
|
1670
|
+
* never enters the running at all. A task written as prose ("why does the proxy
|
|
1671
|
+
* fail?") therefore ranks far worse than the same task anchored on the symbols
|
|
1672
|
+
* and paths it is actually about — the difference between a useful pivot set and
|
|
1673
|
+
* "the vexp result wasn't relevant".
|
|
1674
|
+
*
|
|
1675
|
+
* This shipped only in the Claude Code and Cursor templates, while the AGENTS.md
|
|
1676
|
+
* example (`"task": "fix auth bug"`) actively modelled the weak mode — so two
|
|
1677
|
+
* independent field reports ("the index seems not very useful") came from users
|
|
1678
|
+
* whose instructions never told them the one thing that makes retrieval land.
|
|
1679
|
+
* Shared by every prose template so the wording cannot drift again. ASCII only
|
|
1680
|
+
* (these land in files that get read on Windows).
|
|
1681
|
+
*/
|
|
1682
|
+
const QUERY_SHAPE = `### Query shape (do this)
|
|
1683
|
+
- Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
1684
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1685
|
+
- A pure natural-language question ("why does login fail?") falls back to text
|
|
1686
|
+
ranking and is much less reliable - name the symbols/files you want, not the question.`;
|
|
1365
1687
|
function generateAgentConfig(template, vars) {
|
|
1366
1688
|
switch (template) {
|
|
1367
1689
|
case "claude-code":
|
|
@@ -1403,6 +1725,12 @@ uses fewer tokens than manual searching. Prefer \`get_skeleton\` over Read to
|
|
|
1403
1725
|
inspect files (detail: minimal/standard/detailed, 70-90% token savings).
|
|
1404
1726
|
Only use Read when you need exact raw content to edit a specific line.
|
|
1405
1727
|
|
|
1728
|
+
The hook denies Grep/Glob while the daemon is up. That is policy, not a transient
|
|
1729
|
+
failure: call \`run_pipeline\` instead - do NOT work around it by shelling out or
|
|
1730
|
+
writing a script. vexp only covers indexed source inside the workspace: for runtime
|
|
1731
|
+
logs, build output (dist/, .vite/, node_modules/) or files outside the repo it has
|
|
1732
|
+
no answer - use Bash/Read there, those are never blocked.
|
|
1733
|
+
|
|
1406
1734
|
### Primary Tool
|
|
1407
1735
|
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
|
|
1408
1736
|
capsule + impact + memory server-side. Returns compressed results.
|
|
@@ -1418,6 +1746,8 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1418
1746
|
- \`index_status\` - indexing status and health check
|
|
1419
1747
|
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
1420
1748
|
|
|
1749
|
+
${QUERY_SHAPE}
|
|
1750
|
+
|
|
1421
1751
|
### Workflow
|
|
1422
1752
|
1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
|
|
1423
1753
|
2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
|
|
@@ -1438,9 +1768,6 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1438
1768
|
- **Session Memory**: auto-captures observations; memories auto-surfaced in results
|
|
1439
1769
|
- **LSP Bridge**: VS Code captures type-resolved call edges
|
|
1440
1770
|
- **Change Coupling**: co-changed files included as related context
|
|
1441
|
-
- **Query tips**: include real identifiers (ClassName, function_name) or file paths
|
|
1442
|
-
in the task for precise matches - pure natural-language phrasing falls back to
|
|
1443
|
-
text ranking and is less reliable
|
|
1444
1771
|
|
|
1445
1772
|
### Advanced Parameters
|
|
1446
1773
|
- \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
|
|
@@ -1471,19 +1798,19 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1471
1798
|
|
|
1472
1799
|
### Available MCP tools
|
|
1473
1800
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1474
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1801
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1475
1802
|
- \`get_skeleton\` - compact file structure
|
|
1476
1803
|
- \`index_status\` - indexing status
|
|
1477
1804
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1478
1805
|
|
|
1806
|
+
${QUERY_SHAPE}
|
|
1807
|
+
|
|
1479
1808
|
### Agentic search
|
|
1480
1809
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1481
1810
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1482
1811
|
rather than letting them search the codebase independently
|
|
1483
1812
|
|
|
1484
1813
|
### Tips
|
|
1485
|
-
- Include real identifiers (class/function names) or file paths in the task - literal
|
|
1486
|
-
matches rank best; pure natural-language phrasing falls back to text ranking
|
|
1487
1814
|
- Add \`include_tests: true\` when debugging
|
|
1488
1815
|
- Use \`preset: "refactor"\` for deep impact analysis
|
|
1489
1816
|
|
|
@@ -1511,11 +1838,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1511
1838
|
|
|
1512
1839
|
### Available MCP tools
|
|
1513
1840
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1514
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1841
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1515
1842
|
- \`get_skeleton\` - compact file structure
|
|
1516
1843
|
- \`index_status\` - indexing status
|
|
1517
1844
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1518
1845
|
|
|
1846
|
+
${QUERY_SHAPE}
|
|
1847
|
+
|
|
1519
1848
|
### Agentic search
|
|
1520
1849
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1521
1850
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1560,11 +1889,13 @@ When working on this codebase:
|
|
|
1560
1889
|
|
|
1561
1890
|
### Available MCP tools
|
|
1562
1891
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1563
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1892
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1564
1893
|
- \`get_skeleton\` - token-efficient file structure
|
|
1565
1894
|
- \`index_status\` - indexing status
|
|
1566
1895
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1567
1896
|
|
|
1897
|
+
${QUERY_SHAPE}
|
|
1898
|
+
|
|
1568
1899
|
### Agentic search
|
|
1569
1900
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1570
1901
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1590,11 +1921,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1590
1921
|
|
|
1591
1922
|
### Available MCP tools
|
|
1592
1923
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1593
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1924
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1594
1925
|
- \`get_skeleton\` - compact file structure
|
|
1595
1926
|
- \`index_status\` - indexing status
|
|
1596
1927
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1597
1928
|
|
|
1929
|
+
${QUERY_SHAPE}
|
|
1930
|
+
|
|
1598
1931
|
### Agentic search
|
|
1599
1932
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1600
1933
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1620,11 +1953,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1620
1953
|
|
|
1621
1954
|
### Available MCP tools
|
|
1622
1955
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1623
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1956
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1624
1957
|
- \`get_skeleton\` - compact file structure
|
|
1625
1958
|
- \`index_status\` - indexing status
|
|
1626
1959
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1627
1960
|
|
|
1961
|
+
${QUERY_SHAPE}
|
|
1962
|
+
|
|
1628
1963
|
### Agentic search
|
|
1629
1964
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1630
1965
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1650,13 +1985,20 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1650
1985
|
|
|
1651
1986
|
### Available MCP tools
|
|
1652
1987
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1653
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1988
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1654
1989
|
- \`get_skeleton\` - compact file structure
|
|
1655
1990
|
- \`index_status\` - indexing status
|
|
1656
1991
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1657
1992
|
|
|
1993
|
+
${QUERY_SHAPE}
|
|
1994
|
+
|
|
1658
1995
|
### Agentic search
|
|
1659
1996
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1997
|
+
- If a search tool is denied, that is policy, not a transient failure: call \`run_pipeline\`
|
|
1998
|
+
instead. Do NOT work around it with shell search or by writing a script.
|
|
1999
|
+
- vexp only covers indexed source inside the workspace. For runtime logs, build output
|
|
2000
|
+
(dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
|
|
2001
|
+
normal tools there; those searches are never blocked.
|
|
1660
2002
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1661
2003
|
rather than letting them search the codebase independently
|
|
1662
2004
|
|
|
@@ -1680,11 +2022,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1680
2022
|
|
|
1681
2023
|
## Available vexp tools
|
|
1682
2024
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1683
|
-
Example: \`run_pipeline({ "task": "fix
|
|
2025
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1684
2026
|
- \`get_skeleton\` - compact file structure
|
|
1685
2027
|
- \`index_status\` - indexing status
|
|
1686
2028
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1687
2029
|
|
|
2030
|
+
${QUERY_SHAPE}
|
|
2031
|
+
|
|
1688
2032
|
## Agentic search
|
|
1689
2033
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1690
2034
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1698,32 +2042,65 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
|
1698
2042
|
<!-- /vexp -->`;
|
|
1699
2043
|
}
|
|
1700
2044
|
function genericTemplate(vars) {
|
|
1701
|
-
return `## vexp <!-- vexp v${vars.version} -->
|
|
2045
|
+
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
1702
2046
|
|
|
1703
|
-
|
|
1704
|
-
|
|
2047
|
+
### MANDATORY: call run_pipeline FIRST - do NOT grep, glob, or read to explore
|
|
2048
|
+
For every task - bug fixes, features, refactors, questions about the code:
|
|
2049
|
+
**call \`run_pipeline\` before any other tool**. It runs context search + impact
|
|
2050
|
+
analysis + memory recall in a single call and returns compressed, graph-ranked
|
|
2051
|
+
results.
|
|
2052
|
+
|
|
2053
|
+
This holds even when you already know the file path. The pipeline returns the
|
|
2054
|
+
callers, the blast radius and the co-changed files that opening the file cannot.
|
|
2055
|
+
No tool here will stop you from searching manually, so honouring this rule is on
|
|
2056
|
+
you: "I already knew where to look" is not an exception, it is the exact case the
|
|
2057
|
+
rule exists for.
|
|
2058
|
+
|
|
2059
|
+
Do NOT use grep, glob, built-in codebase search, or shell commands to explore.
|
|
2060
|
+
Do NOT open files one by one to find your way around: use \`get_skeleton\`
|
|
2061
|
+
(detail: minimal/standard/detailed, 70-90% fewer tokens than reading the file).
|
|
2062
|
+
Read raw file content only when you need it to edit a specific line.
|
|
2063
|
+
|
|
2064
|
+
vexp indexes source inside the workspace and nothing else. Runtime logs, build
|
|
2065
|
+
output (dist/, .vite/, node_modules/) and files outside the repo are NOT indexed -
|
|
2066
|
+
read those directly, this rule does not cover them.
|
|
2067
|
+
|
|
2068
|
+
### Primary tool
|
|
2069
|
+
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Auto-detects intent
|
|
2070
|
+
(debug/modify/refactor/explore) from your task. Includes file content for pivots.
|
|
2071
|
+
- \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2072
|
+
- \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\`
|
|
2073
|
+
- \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - saves an insight in the same call
|
|
2074
|
+
|
|
2075
|
+
### Other MCP tools (only when run_pipeline is not enough)
|
|
2076
|
+
- \`get_skeleton\` - **preferred over reading a file**: signatures and structure, 3 detail levels
|
|
2077
|
+
- \`index_status\` - indexing status and health check
|
|
2078
|
+
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
2079
|
+
|
|
2080
|
+
${QUERY_SHAPE}
|
|
1705
2081
|
|
|
1706
2082
|
### Workflow
|
|
1707
|
-
1. \`run_pipeline
|
|
1708
|
-
2.
|
|
1709
|
-
3.
|
|
2083
|
+
1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
|
|
2084
|
+
2. Need more on a file? \`get_skeleton({ files: [...], detail: "detailed" })\` - not a raw read
|
|
2085
|
+
3. Make targeted changes based on the context returned
|
|
2086
|
+
4. \`run_pipeline\` again ONLY if you need more context while implementing
|
|
2087
|
+
5. Do NOT chain vexp calls - one \`run_pipeline\` replaces capsule + impact + memory + observation
|
|
1710
2088
|
|
|
1711
|
-
###
|
|
1712
|
-
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
- \`index_status\` - indexing status
|
|
1716
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2089
|
+
### Sub-agents and background tasks
|
|
2090
|
+
- Sub-agents CAN and MUST call \`run_pipeline\` - always give them the task description
|
|
2091
|
+
- Do NOT spawn an agent to search freely: call \`run_pipeline\` first, then pass the
|
|
2092
|
+
returned context into the agent prompt
|
|
1717
2093
|
|
|
1718
|
-
###
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2094
|
+
### Fallback
|
|
2095
|
+
If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
|
|
2096
|
+
the index is empty or still building. Use the built-in search and read tools directly
|
|
2097
|
+
until it is ready - do not stall waiting for vexp.
|
|
1722
2098
|
|
|
1723
|
-
### Smart
|
|
1724
|
-
Intent
|
|
2099
|
+
### Smart features (automatic - no action needed)
|
|
2100
|
+
Intent detection, hybrid keyword+semantic+graph ranking, session memory,
|
|
2101
|
+
change coupling, auto-expanding budget.
|
|
1725
2102
|
|
|
1726
|
-
### Multi-
|
|
2103
|
+
### Multi-repo
|
|
1727
2104
|
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
1728
2105
|
<!-- /vexp -->`;
|
|
1729
2106
|
}
|