vexp-cli 2.2.1 → 2.2.2
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 +119 -13
- package/dist/cli.js +19 -3
- package/dist/hook-template.js +76 -25
- package/mcp/mcp-server.cjs +48 -43
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -376,6 +376,60 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
376
376
|
export function getAgentList() {
|
|
377
377
|
return [...AGENT_DETECTORS];
|
|
378
378
|
}
|
|
379
|
+
/** Fold a name to its comparable form: case, spaces, dots and dashes carry no
|
|
380
|
+
* meaning here ("claude-code", "Claude Code" and "claudecode" are one agent). */
|
|
381
|
+
function foldAgentName(s) {
|
|
382
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Canonicalize a name given to `--agents`, or null if it names no agent.
|
|
386
|
+
*
|
|
387
|
+
* `configureSelectedAgents` matches with `Array.includes` on the exact string,
|
|
388
|
+
* so "opencode" silently configured nothing while "Opencode" worked — nobody
|
|
389
|
+
* can be expected to know the capitalization, and the failure was invisible.
|
|
390
|
+
*/
|
|
391
|
+
export function resolveAgentName(input) {
|
|
392
|
+
const want = foldAgentName(input);
|
|
393
|
+
if (!want)
|
|
394
|
+
return null;
|
|
395
|
+
return AGENT_DETECTORS.find((d) => foldAgentName(d.agent) === want)?.agent ?? null;
|
|
396
|
+
}
|
|
397
|
+
/** Levenshtein distance, for "did you mean" on an unknown --agents name. */
|
|
398
|
+
function editDistance(a, b) {
|
|
399
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
400
|
+
const cur = new Array(b.length + 1).fill(0);
|
|
401
|
+
for (let i = 1; i <= a.length; i++) {
|
|
402
|
+
cur[0] = i;
|
|
403
|
+
for (let j = 1; j <= b.length; j++) {
|
|
404
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
405
|
+
}
|
|
406
|
+
prev.splice(0, prev.length, ...cur);
|
|
407
|
+
}
|
|
408
|
+
return prev[b.length];
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The known agent an unknown name most likely meant, or null when nothing is
|
|
412
|
+
* close enough to be worth suggesting. Substring hits win (a bare "copilot"
|
|
413
|
+
* means "GitHub Copilot"); otherwise allow a couple of typos.
|
|
414
|
+
*/
|
|
415
|
+
export function suggestAgentName(input) {
|
|
416
|
+
const want = foldAgentName(input);
|
|
417
|
+
if (!want)
|
|
418
|
+
return null;
|
|
419
|
+
const contained = AGENT_DETECTORS.find((d) => {
|
|
420
|
+
const f = foldAgentName(d.agent);
|
|
421
|
+
return f.includes(want) || want.includes(f);
|
|
422
|
+
});
|
|
423
|
+
if (contained)
|
|
424
|
+
return contained.agent;
|
|
425
|
+
let best = null;
|
|
426
|
+
for (const d of AGENT_DETECTORS) {
|
|
427
|
+
const dist = editDistance(want, foldAgentName(d.agent));
|
|
428
|
+
if (!best || dist < best.d)
|
|
429
|
+
best = { name: d.agent, d: dist };
|
|
430
|
+
}
|
|
431
|
+
return best && best.d <= 3 ? best.name : null;
|
|
432
|
+
}
|
|
379
433
|
/**
|
|
380
434
|
* Configure specific agents selected by the user.
|
|
381
435
|
* Unlike configureAgents which auto-detects, this forces configuration
|
|
@@ -1358,10 +1412,38 @@ export function installOpencodePlugin(workspaceRoot) {
|
|
|
1358
1412
|
const current = fs.readFileSync(pluginPath, "utf-8");
|
|
1359
1413
|
if (current === VEXP_OPENCODE_GUARD)
|
|
1360
1414
|
return null; // identical - skip
|
|
1415
|
+
// Content differs: either an older vexp guard, or a copy the user tuned by
|
|
1416
|
+
// hand. We refresh either way — refusing to touch a hand-edited plugin would
|
|
1417
|
+
// freeze that user on a stale guard forever — but their version stays
|
|
1418
|
+
// recoverable at vexp-guard.js.vexp-bak instead of being silently discarded.
|
|
1419
|
+
backupConfig(pluginPath);
|
|
1361
1420
|
}
|
|
1362
1421
|
fs.writeFileSync(pluginPath, VEXP_OPENCODE_GUARD, "utf-8");
|
|
1363
1422
|
return existed ? "updated" : "created";
|
|
1364
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The single highest-leverage instruction we ship, and for a long time the one
|
|
1426
|
+
* most agents never saw.
|
|
1427
|
+
*
|
|
1428
|
+
* vexp retrieval is identifier/path-first: FTS supplies the candidate pool and
|
|
1429
|
+
* the semantic layer only RE-RANKS it, so a symbol that no query token reaches
|
|
1430
|
+
* never enters the running at all. A task written as prose ("why does the proxy
|
|
1431
|
+
* fail?") therefore ranks far worse than the same task anchored on the symbols
|
|
1432
|
+
* and paths it is actually about — the difference between a useful pivot set and
|
|
1433
|
+
* "the vexp result wasn't relevant".
|
|
1434
|
+
*
|
|
1435
|
+
* This shipped only in the Claude Code and Cursor templates, while the AGENTS.md
|
|
1436
|
+
* example (`"task": "fix auth bug"`) actively modelled the weak mode — so two
|
|
1437
|
+
* independent field reports ("the index seems not very useful") came from users
|
|
1438
|
+
* whose instructions never told them the one thing that makes retrieval land.
|
|
1439
|
+
* Shared by every prose template so the wording cannot drift again. ASCII only
|
|
1440
|
+
* (these land in files that get read on Windows).
|
|
1441
|
+
*/
|
|
1442
|
+
const QUERY_SHAPE = `### Query shape (do this)
|
|
1443
|
+
- Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
1444
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1445
|
+
- A pure natural-language question ("why does login fail?") falls back to text
|
|
1446
|
+
ranking and is much less reliable - name the symbols/files you want, not the question.`;
|
|
1365
1447
|
function generateAgentConfig(template, vars) {
|
|
1366
1448
|
switch (template) {
|
|
1367
1449
|
case "claude-code":
|
|
@@ -1403,6 +1485,12 @@ uses fewer tokens than manual searching. Prefer \`get_skeleton\` over Read to
|
|
|
1403
1485
|
inspect files (detail: minimal/standard/detailed, 70-90% token savings).
|
|
1404
1486
|
Only use Read when you need exact raw content to edit a specific line.
|
|
1405
1487
|
|
|
1488
|
+
The hook denies Grep/Glob while the daemon is up. That is policy, not a transient
|
|
1489
|
+
failure: call \`run_pipeline\` instead - do NOT work around it by shelling out or
|
|
1490
|
+
writing a script. vexp only covers indexed source inside the workspace: for runtime
|
|
1491
|
+
logs, build output (dist/, .vite/, node_modules/) or files outside the repo it has
|
|
1492
|
+
no answer - use Bash/Read there, those are never blocked.
|
|
1493
|
+
|
|
1406
1494
|
### Primary Tool
|
|
1407
1495
|
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
|
|
1408
1496
|
capsule + impact + memory server-side. Returns compressed results.
|
|
@@ -1418,6 +1506,8 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1418
1506
|
- \`index_status\` - indexing status and health check
|
|
1419
1507
|
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
1420
1508
|
|
|
1509
|
+
${QUERY_SHAPE}
|
|
1510
|
+
|
|
1421
1511
|
### Workflow
|
|
1422
1512
|
1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
|
|
1423
1513
|
2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
|
|
@@ -1438,9 +1528,6 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1438
1528
|
- **Session Memory**: auto-captures observations; memories auto-surfaced in results
|
|
1439
1529
|
- **LSP Bridge**: VS Code captures type-resolved call edges
|
|
1440
1530
|
- **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
1531
|
|
|
1445
1532
|
### Advanced Parameters
|
|
1446
1533
|
- \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
|
|
@@ -1471,19 +1558,19 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1471
1558
|
|
|
1472
1559
|
### Available MCP tools
|
|
1473
1560
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1474
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1561
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1475
1562
|
- \`get_skeleton\` - compact file structure
|
|
1476
1563
|
- \`index_status\` - indexing status
|
|
1477
1564
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1478
1565
|
|
|
1566
|
+
${QUERY_SHAPE}
|
|
1567
|
+
|
|
1479
1568
|
### Agentic search
|
|
1480
1569
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1481
1570
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1482
1571
|
rather than letting them search the codebase independently
|
|
1483
1572
|
|
|
1484
1573
|
### 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
1574
|
- Add \`include_tests: true\` when debugging
|
|
1488
1575
|
- Use \`preset: "refactor"\` for deep impact analysis
|
|
1489
1576
|
|
|
@@ -1511,11 +1598,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1511
1598
|
|
|
1512
1599
|
### Available MCP tools
|
|
1513
1600
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1514
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1601
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1515
1602
|
- \`get_skeleton\` - compact file structure
|
|
1516
1603
|
- \`index_status\` - indexing status
|
|
1517
1604
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1518
1605
|
|
|
1606
|
+
${QUERY_SHAPE}
|
|
1607
|
+
|
|
1519
1608
|
### Agentic search
|
|
1520
1609
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1521
1610
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1560,11 +1649,13 @@ When working on this codebase:
|
|
|
1560
1649
|
|
|
1561
1650
|
### Available MCP tools
|
|
1562
1651
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1563
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1652
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1564
1653
|
- \`get_skeleton\` - token-efficient file structure
|
|
1565
1654
|
- \`index_status\` - indexing status
|
|
1566
1655
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1567
1656
|
|
|
1657
|
+
${QUERY_SHAPE}
|
|
1658
|
+
|
|
1568
1659
|
### Agentic search
|
|
1569
1660
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1570
1661
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1590,11 +1681,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1590
1681
|
|
|
1591
1682
|
### Available MCP tools
|
|
1592
1683
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1593
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1684
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1594
1685
|
- \`get_skeleton\` - compact file structure
|
|
1595
1686
|
- \`index_status\` - indexing status
|
|
1596
1687
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1597
1688
|
|
|
1689
|
+
${QUERY_SHAPE}
|
|
1690
|
+
|
|
1598
1691
|
### Agentic search
|
|
1599
1692
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1600
1693
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1620,11 +1713,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1620
1713
|
|
|
1621
1714
|
### Available MCP tools
|
|
1622
1715
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1623
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1716
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1624
1717
|
- \`get_skeleton\` - compact file structure
|
|
1625
1718
|
- \`index_status\` - indexing status
|
|
1626
1719
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1627
1720
|
|
|
1721
|
+
${QUERY_SHAPE}
|
|
1722
|
+
|
|
1628
1723
|
### Agentic search
|
|
1629
1724
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1630
1725
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1650,13 +1745,20 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1650
1745
|
|
|
1651
1746
|
### Available MCP tools
|
|
1652
1747
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1653
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1748
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1654
1749
|
- \`get_skeleton\` - compact file structure
|
|
1655
1750
|
- \`index_status\` - indexing status
|
|
1656
1751
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1657
1752
|
|
|
1753
|
+
${QUERY_SHAPE}
|
|
1754
|
+
|
|
1658
1755
|
### Agentic search
|
|
1659
1756
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1757
|
+
- If a search tool is denied, that is policy, not a transient failure: call \`run_pipeline\`
|
|
1758
|
+
instead. Do NOT work around it with shell search or by writing a script.
|
|
1759
|
+
- vexp only covers indexed source inside the workspace. For runtime logs, build output
|
|
1760
|
+
(dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
|
|
1761
|
+
normal tools there; those searches are never blocked.
|
|
1660
1762
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1661
1763
|
rather than letting them search the codebase independently
|
|
1662
1764
|
|
|
@@ -1680,11 +1782,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1680
1782
|
|
|
1681
1783
|
## Available vexp tools
|
|
1682
1784
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1683
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1785
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1684
1786
|
- \`get_skeleton\` - compact file structure
|
|
1685
1787
|
- \`index_status\` - indexing status
|
|
1686
1788
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1687
1789
|
|
|
1790
|
+
${QUERY_SHAPE}
|
|
1791
|
+
|
|
1688
1792
|
## Agentic search
|
|
1689
1793
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1690
1794
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1710,11 +1814,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1710
1814
|
|
|
1711
1815
|
### Available MCP tools
|
|
1712
1816
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1713
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1817
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1714
1818
|
- \`get_skeleton\` - compact file structure
|
|
1715
1819
|
- \`index_status\` - indexing status
|
|
1716
1820
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1717
1821
|
|
|
1822
|
+
${QUERY_SHAPE}
|
|
1823
|
+
|
|
1718
1824
|
### Agentic search
|
|
1719
1825
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1720
1826
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as net from "net";
|
|
9
9
|
import { checkbox, confirm } from "@inquirer/prompts";
|
|
10
10
|
import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
|
|
11
|
-
import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-config.js";
|
|
11
|
+
import { detectAgents, getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName } from "./agent-config.js";
|
|
12
12
|
import { CLI_VERSION } from "./version.js";
|
|
13
13
|
import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
|
|
14
14
|
import { checkForUpdate } from "./update-check.js";
|
|
@@ -555,8 +555,24 @@ program
|
|
|
555
555
|
const detectedNames = new Set(detected.map((a) => a.agent));
|
|
556
556
|
let selectedNames;
|
|
557
557
|
if (typeof opts.agents === "string") {
|
|
558
|
-
// Explicit list from --agents flag
|
|
559
|
-
|
|
558
|
+
// Explicit list from --agents flag. Resolve every name BEFORE writing
|
|
559
|
+
// anything: matching is exact downstream, so an unrecognised name used
|
|
560
|
+
// to configure nothing while setup still reported success — the worst
|
|
561
|
+
// possible outcome for the flag whose whole point is automation.
|
|
562
|
+
const requested = opts.agents.split(",").map((s) => s.trim()).filter(Boolean);
|
|
563
|
+
const unknown = requested.filter((n) => resolveAgentName(n) === null);
|
|
564
|
+
if (unknown.length > 0 || requested.length === 0) {
|
|
565
|
+
console.error(chalk.red(`\n✖ Unknown agent: ${unknown.map((u) => `"${u}"`).join(", ") || "(empty --agents list)"}`));
|
|
566
|
+
for (const u of unknown) {
|
|
567
|
+
const hint = suggestAgentName(u);
|
|
568
|
+
if (hint)
|
|
569
|
+
console.error(chalk.yellow(` Did you mean: ${hint}?`));
|
|
570
|
+
}
|
|
571
|
+
console.error(chalk.dim(` Valid agents: ${allAgents.map((a) => a.agent).join(", ")}`));
|
|
572
|
+
console.error(chalk.dim(" Names are matched ignoring case and punctuation.\n"));
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
selectedNames = requested.map((n) => resolveAgentName(n));
|
|
560
576
|
console.log(chalk.dim(` Agents (from flag): ${selectedNames.join(", ")}`));
|
|
561
577
|
}
|
|
562
578
|
else {
|
package/dist/hook-template.js
CHANGED
|
@@ -69,13 +69,25 @@ esac
|
|
|
69
69
|
* test in packages/vexp-cli/test/hook-template.test.ts). Do NOT introduce any
|
|
70
70
|
* of those characters here without updating that test's un-escape logic.
|
|
71
71
|
*/
|
|
72
|
-
export const VEXP_OPENCODE_GUARD = `// vexp-guard - opencode plugin (generated by 'vexp setup-agents'
|
|
73
|
-
//
|
|
74
|
-
//
|
|
72
|
+
export const VEXP_OPENCODE_GUARD = `// vexp-guard - opencode plugin (generated by 'vexp setup-agents'; it is rewritten
|
|
73
|
+
// on every setup, and a modified copy is saved to vexp-guard.js.vexp-bak first).
|
|
74
|
+
//
|
|
75
|
+
// Blocks opencode's native grep/glob ONLY where vexp has a better answer: indexed
|
|
76
|
+
// source inside the workspace, while the daemon is healthy. Two deliberate limits:
|
|
77
|
+
//
|
|
78
|
+
// 1. It never touches bash. Shelling out is the escape hatch for everything vexp
|
|
79
|
+
// cannot index - runtime logs, build output, files outside the repo. vexp's
|
|
80
|
+
// Claude Code guard has always matched Grep|Glob|Regex only, and it works
|
|
81
|
+
// precisely because that valve stays open. Blocking shell search instead
|
|
82
|
+
// teaches the agent to evade (any prefix defeats it) and strands it when the
|
|
83
|
+
// answer genuinely is not in the index.
|
|
84
|
+
// 2. It only blocks targets vexp actually indexed. A search aimed at a log file,
|
|
85
|
+
// at dist/ or node_modules, or outside the workspace has no run_pipeline
|
|
86
|
+
// answer, so blocking it would be a dead end rather than a redirect.
|
|
87
|
+
//
|
|
75
88
|
// Fails OPEN when the daemon is down, so native search still works with no index.
|
|
76
|
-
// Mirrors vexp's Claude Code PreToolUse guard (.claude/hooks/vexp-guard.sh).
|
|
77
89
|
import { existsSync, readFileSync } from "node:fs";
|
|
78
|
-
import { join, dirname } from "node:path";
|
|
90
|
+
import { join, dirname, relative, isAbsolute, sep } from "node:path";
|
|
79
91
|
|
|
80
92
|
// Walk up from start to the first ancestor that owns a .vexp dir.
|
|
81
93
|
function findVexpDir(start) {
|
|
@@ -108,35 +120,74 @@ function daemonHealthy(vexpDir) {
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
const SEARCH_TOOLS = new Set(["grep", "glob"]);
|
|
111
|
-
const SHELL_SEARCH_BINS = ["rg", "grep", "egrep", "fgrep", "ag", "ack", "find", "fd", "fdfind"];
|
|
112
123
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
// Directory names vexp never indexes: build output and vendored dependencies.
|
|
125
|
+
const NON_INDEXED_DIRS = [
|
|
126
|
+
"node_modules", "dist", "build", "out", "coverage", "target", "vendor",
|
|
127
|
+
".vite", ".next", ".nuxt", ".output", ".turbo", ".cache", ".git", ".vexp"
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// File kinds vexp never indexes. Logs are the case agents legitimately grep the
|
|
131
|
+
// most while debugging a running app. "-lock." is not redundant with ".lock":
|
|
132
|
+
// the two commonest JS lockfiles (package-lock.json, pnpm-lock.yaml) have no dot
|
|
133
|
+
// before "lock", so ".lock" alone let yarn.lock and Cargo.lock through while
|
|
134
|
+
// blocking them.
|
|
135
|
+
const NON_INDEXED_HINTS = [".log", ".lock", "-lock.", ".map", ".min.js"];
|
|
136
|
+
|
|
137
|
+
// Is this search target inside what vexp indexed? Only then is "call
|
|
138
|
+
// run_pipeline instead" real advice rather than a dead end. Anything that cannot
|
|
139
|
+
// be resolved confidently returns false (allow) - under-blocking just leaves the
|
|
140
|
+
// agent on grep, over-blocking strands it with no tool at all.
|
|
141
|
+
function isIndexedTarget(root, target) {
|
|
142
|
+
if (!target) return true; // no path given: a whole-workspace search is vexp's job
|
|
143
|
+
const s = String(target);
|
|
144
|
+
const low = s.toLowerCase();
|
|
145
|
+
for (const h of NON_INDEXED_HINTS) if (low.indexOf(h) !== -1) return false;
|
|
146
|
+
let rel;
|
|
147
|
+
try {
|
|
148
|
+
rel = relative(root, isAbsolute(s) ? s : join(root, s));
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return false;
|
|
118
151
|
}
|
|
119
|
-
return false;
|
|
152
|
+
if (rel === "..") return false;
|
|
153
|
+
if (rel.indexOf(".." + sep) === 0) return false; // escapes the workspace
|
|
154
|
+
if (isAbsolute(rel)) return false; // different root or drive
|
|
155
|
+
const segs = rel.split(sep);
|
|
156
|
+
for (const g of segs) if (NON_INDEXED_DIRS.indexOf(g) !== -1) return false;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Where a search is aimed. grep carries the regex in its pattern arg (never a
|
|
161
|
+
// path), so only path and the include filter describe its target; for glob the
|
|
162
|
+
// pattern IS the path.
|
|
163
|
+
function searchTargets(tool, args) {
|
|
164
|
+
if (!args) return [];
|
|
165
|
+
const out = [];
|
|
166
|
+
if (args.path) out.push(args.path);
|
|
167
|
+
if (tool === "glob" && args.pattern) out.push(args.pattern);
|
|
168
|
+
if (tool === "grep" && args.include) out.push(args.include);
|
|
169
|
+
return out;
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
export const VexpGuard = async ({ directory, worktree }) => {
|
|
123
|
-
const
|
|
173
|
+
const start = worktree || directory || ".";
|
|
124
174
|
let vexpDir = null;
|
|
125
175
|
return {
|
|
126
176
|
"tool.execute.before": async (input, output) => {
|
|
127
|
-
if (!vexpDir) vexpDir = findVexpDir(
|
|
128
|
-
if (!daemonHealthy(vexpDir)) return;
|
|
129
|
-
if (SEARCH_TOOLS.has(input.tool))
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
if (input.tool === "bash" && isShellSearch(output && output.args && output.args.command)) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
"vexp daemon is running - call run_pipeline instead of shell search (grep/rg/find)."
|
|
138
|
-
);
|
|
177
|
+
if (!vexpDir) vexpDir = findVexpDir(start);
|
|
178
|
+
if (!daemonHealthy(vexpDir)) return; // no live index -> allow
|
|
179
|
+
if (!SEARCH_TOOLS.has(input.tool)) return; // bash and everything else: never blocked
|
|
180
|
+
const root = dirname(vexpDir);
|
|
181
|
+
const targets = searchTargets(input.tool, output && output.args);
|
|
182
|
+
for (const t of targets) {
|
|
183
|
+
if (!isIndexedTarget(root, t)) return; // vexp has no answer here -> allow
|
|
139
184
|
}
|
|
185
|
+
throw new Error(
|
|
186
|
+
"vexp daemon is running. AGENT INSTRUCTION: call run_pipeline instead of " +
|
|
187
|
+
input.tool + " - it returns pre-indexed, graph-ranked context in one call." +
|
|
188
|
+
" This is policy, not a transient failure: do NOT work around it with shell" +
|
|
189
|
+
" search or by writing a script."
|
|
190
|
+
);
|
|
140
191
|
},
|
|
141
192
|
};
|
|
142
193
|
};
|