vexp-cli 3.0.1 → 3.1.0
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 +134 -14
- package/dist/cli.js +21 -2
- package/dist/doctor.js +106 -0
- package/dist/license.js +4 -4
- package/mcp/mcp-server.cjs +1 -1
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -123,7 +123,9 @@ const AGENT_DETECTORS = [
|
|
|
123
123
|
{
|
|
124
124
|
agent: "Cursor",
|
|
125
125
|
detectPath: ".cursor",
|
|
126
|
-
|
|
126
|
+
// Cursor reads a DIRECTORY of rules here (`.cursor/rules/*.mdc`); the
|
|
127
|
+
// file inside is ours. See `cursorRulesTarget` for the one exception.
|
|
128
|
+
configFile: ".cursor/rules/vexp.mdc",
|
|
127
129
|
templateName: "cursor",
|
|
128
130
|
mcpConfigFile: ".cursor/mcp.json",
|
|
129
131
|
},
|
|
@@ -341,7 +343,8 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
341
343
|
const filter = agentFilter.map((a) => a.toLowerCase());
|
|
342
344
|
agents = agents.filter((d) => filter.includes(d.agent.toLowerCase()));
|
|
343
345
|
}
|
|
344
|
-
for (const
|
|
346
|
+
for (const detected of agents) {
|
|
347
|
+
const detector = withCursorTarget(detected, workspaceRoot);
|
|
345
348
|
const configFilePath = path.join(workspaceRoot, detector.configFile);
|
|
346
349
|
const alreadyExists = fs.existsSync(configFilePath);
|
|
347
350
|
// Sweep the location we used to write to. For several releases the
|
|
@@ -689,7 +692,8 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
689
692
|
const results = [];
|
|
690
693
|
const mcpConfigs = [];
|
|
691
694
|
const writtenConfigFiles = new Set();
|
|
692
|
-
for (const
|
|
695
|
+
for (const detected of AGENT_DETECTORS) {
|
|
696
|
+
const detector = withCursorTarget(detected, workspaceRoot);
|
|
693
697
|
if (!selectedAgentNames.includes(detector.agent))
|
|
694
698
|
continue;
|
|
695
699
|
// Create agent directory if it doesn't exist. Skip for file-based
|
|
@@ -966,6 +970,70 @@ export function readJsonConfigSafe(filePath) {
|
|
|
966
970
|
* {command: {path, args}} Zed
|
|
967
971
|
* {command: ["node", "…/mcp-server.cjs"]} Kilo Code
|
|
968
972
|
*/
|
|
973
|
+
/**
|
|
974
|
+
* The interpreter to write into an editor's MCP config for `mcp-server.cjs`.
|
|
975
|
+
*
|
|
976
|
+
* A bare `node` is resolved through the PATH of the process that spawns the
|
|
977
|
+
* server — and a VS Code / Cursor / Windsurf launched from the Dock, the Start
|
|
978
|
+
* menu or a desktop shortcut carries the login PATH, which on most developer
|
|
979
|
+
* machines has no nvm, Volta or Homebrew node in it. The server then dies with
|
|
980
|
+
* `spawn node ENOENT`, the editor lists no vexp tools, and nothing on our side
|
|
981
|
+
* logged anything (GitHub Copilot field report, 2026-08). Pin the node that is
|
|
982
|
+
* running us instead; the VS Code extension has done the same
|
|
983
|
+
* (`resolveNodePath`) since 2.x. Same search order, bare `node` as last resort.
|
|
984
|
+
*/
|
|
985
|
+
export function resolveNodeCommand() {
|
|
986
|
+
const execPath = process.execPath;
|
|
987
|
+
if (execPath && !/Code|Electron/i.test(execPath)) {
|
|
988
|
+
try {
|
|
989
|
+
fs.accessSync(execPath, fs.constants.X_OK);
|
|
990
|
+
return execPath;
|
|
991
|
+
}
|
|
992
|
+
catch { /* not executable */ }
|
|
993
|
+
}
|
|
994
|
+
if (process.platform !== "win32" && fs.existsSync("/usr/local/bin/node"))
|
|
995
|
+
return "/usr/local/bin/node";
|
|
996
|
+
const home = os.homedir();
|
|
997
|
+
const nvmDir = path.join(home, ".nvm", "versions", "node");
|
|
998
|
+
if (fs.existsSync(nvmDir)) {
|
|
999
|
+
try {
|
|
1000
|
+
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v")).sort().reverse();
|
|
1001
|
+
for (const v of versions) {
|
|
1002
|
+
const candidate = path.join(nvmDir, v, "bin", "node");
|
|
1003
|
+
if (fs.existsSync(candidate))
|
|
1004
|
+
return candidate;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
catch { /* ignore */ }
|
|
1008
|
+
}
|
|
1009
|
+
return "node";
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* An entry whose interpreter is a bare name (`node`, `node.exe`) rather than a
|
|
1013
|
+
* path. Such an entry may well work from a terminal and still fail inside a
|
|
1014
|
+
* GUI editor, so it is never adopted as "already working" — the writer replaces
|
|
1015
|
+
* it with the pinned interpreter on the next setup.
|
|
1016
|
+
*/
|
|
1017
|
+
export function isBareInterpreter(entry) {
|
|
1018
|
+
if (!entry || typeof entry !== "object")
|
|
1019
|
+
return false;
|
|
1020
|
+
const e = entry;
|
|
1021
|
+
let command = e.command;
|
|
1022
|
+
if (Array.isArray(command))
|
|
1023
|
+
command = command[0];
|
|
1024
|
+
else if (command !== null && typeof command === "object")
|
|
1025
|
+
command = command.path;
|
|
1026
|
+
return typeof command === "string" && command.length > 0 && !/[\\/]/.test(command);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Adopt a foreign entry only when it resolves AND pins its interpreter. Used by
|
|
1030
|
+
* the GUI-editor writers (VS Code/Copilot, Cursor/Windsurf/Kiro/Trae, Zed),
|
|
1031
|
+
* where a bare `node` is exactly the entry that fails; terminal agents
|
|
1032
|
+
* (opencode, Claude Code) keep adopting a bare interpreter — it works there.
|
|
1033
|
+
*/
|
|
1034
|
+
function adoptableEntry(entry) {
|
|
1035
|
+
return vexpEntryStillResolves(entry) && !isBareInterpreter(entry);
|
|
1036
|
+
}
|
|
969
1037
|
function vexpEntryStillResolves(entry) {
|
|
970
1038
|
if (!entry || typeof entry !== "object")
|
|
971
1039
|
return false;
|
|
@@ -1115,7 +1183,49 @@ export function removeVexpSection(filePath) {
|
|
|
1115
1183
|
return "absent";
|
|
1116
1184
|
}
|
|
1117
1185
|
}
|
|
1118
|
-
|
|
1186
|
+
/**
|
|
1187
|
+
* Where the Cursor rule goes, after clearing the way.
|
|
1188
|
+
*
|
|
1189
|
+
* Cursor reads `.cursor/rules/` as a directory of `.mdc` rules. vexp wrote a
|
|
1190
|
+
* single FILE at `.cursor/rules` for several releases — read by nothing, and
|
|
1191
|
+
* a file where Cursor wants a folder, so Cursor could not create its own
|
|
1192
|
+
* rules either. On a project where the folder already existed, `vexp setup`
|
|
1193
|
+
* then crashed with EISDIR reading the directory (field report, Windows /
|
|
1194
|
+
* Cursor, 2026-08) after indexing and the MCP start had already succeeded.
|
|
1195
|
+
*
|
|
1196
|
+
* Our stale file is removed when it holds only our section. If a user put
|
|
1197
|
+
* their own text in a `.cursor/rules` FILE we keep it and fall back to the
|
|
1198
|
+
* legacy root `.cursorrules`, which Cursor still reads — the folder cannot
|
|
1199
|
+
* exist while that file does.
|
|
1200
|
+
*/
|
|
1201
|
+
export function cursorRulesTarget(workspaceRoot) {
|
|
1202
|
+
const legacy = path.join(workspaceRoot, ".cursor", "rules");
|
|
1203
|
+
let isFile = false;
|
|
1204
|
+
try {
|
|
1205
|
+
isFile = fs.statSync(legacy).isFile();
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
/* absent, or a directory */
|
|
1209
|
+
}
|
|
1210
|
+
if (isFile) {
|
|
1211
|
+
removeVexpSection(legacy);
|
|
1212
|
+
if (fs.existsSync(legacy))
|
|
1213
|
+
return ".cursorrules";
|
|
1214
|
+
}
|
|
1215
|
+
return path.join(".cursor", "rules", "vexp.mdc");
|
|
1216
|
+
}
|
|
1217
|
+
/** The detector with its Cursor target resolved for THIS workspace. */
|
|
1218
|
+
function withCursorTarget(d, workspaceRoot) {
|
|
1219
|
+
return d.agent === "Cursor" ? { ...d, configFile: cursorRulesTarget(workspaceRoot) } : d;
|
|
1220
|
+
}
|
|
1221
|
+
export function appendOrCreate(filePath, content, version) {
|
|
1222
|
+
// A directory where a file was expected. Reading it is EISDIR, and that
|
|
1223
|
+
// aborted the whole setup once (the `.cursor/rules` folder above). The
|
|
1224
|
+
// detectors no longer point at folders; if one ever does again, write a
|
|
1225
|
+
// file inside it rather than crash on the user's machine.
|
|
1226
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
1227
|
+
filePath = path.join(filePath, "vexp.md");
|
|
1228
|
+
}
|
|
1119
1229
|
if (!fs.existsSync(filePath)) {
|
|
1120
1230
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1121
1231
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
@@ -1234,7 +1344,7 @@ approveKey = "alwaysAllow") {
|
|
|
1234
1344
|
}
|
|
1235
1345
|
const existing = read.data;
|
|
1236
1346
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1237
|
-
const targetCmd = useNode ?
|
|
1347
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1238
1348
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1239
1349
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1240
1350
|
const beforeServers = existing.mcpServers;
|
|
@@ -1258,7 +1368,7 @@ approveKey = "alwaysAllow") {
|
|
|
1258
1368
|
// Another vexp install already left a working entry here — don't fight it.
|
|
1259
1369
|
// Only the install path may differ: `envMatches` still gates on the
|
|
1260
1370
|
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
1261
|
-
if (removed.length === 0 && envMatches &&
|
|
1371
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1262
1372
|
return false;
|
|
1263
1373
|
const servers = existing.mcpServers ?? {};
|
|
1264
1374
|
servers["vexp"] = {
|
|
@@ -1500,7 +1610,7 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
1500
1610
|
}
|
|
1501
1611
|
const existing = read.data;
|
|
1502
1612
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1503
|
-
const targetCmd = useNode ?
|
|
1613
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1504
1614
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1505
1615
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1506
1616
|
const beforeServers = existing.servers;
|
|
@@ -1520,7 +1630,7 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
1520
1630
|
// Copilot branch, which is gated only on `.github/` existing. Only the
|
|
1521
1631
|
// install path may differ — `envMatches` still gates on the VEXP_WORKSPACE
|
|
1522
1632
|
// pin, so a moved or renamed project is repinned normally.
|
|
1523
|
-
if (removed.length === 0 && envMatches &&
|
|
1633
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1524
1634
|
return false;
|
|
1525
1635
|
const servers = existing.servers ?? {};
|
|
1526
1636
|
servers["vexp"] = {
|
|
@@ -1658,7 +1768,7 @@ export function configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
|
1658
1768
|
}
|
|
1659
1769
|
const cfg = read.data;
|
|
1660
1770
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1661
|
-
const command = useNode ? [
|
|
1771
|
+
const command = useNode ? [resolveNodeCommand(), mcpServerPath] : [binaryPath, "mcp"];
|
|
1662
1772
|
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
1663
1773
|
const mcp = cfg.mcp ?? {};
|
|
1664
1774
|
const prev = mcp["vexp"];
|
|
@@ -1699,7 +1809,7 @@ export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
|
1699
1809
|
}
|
|
1700
1810
|
const cfg = read.data;
|
|
1701
1811
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1702
|
-
const command = useNode ? [
|
|
1812
|
+
const command = useNode ? [resolveNodeCommand(), mcpServerPath] : [binaryPath, "mcp"];
|
|
1703
1813
|
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
1704
1814
|
const mcp = cfg.mcp ?? {};
|
|
1705
1815
|
const prev = mcp["vexp"];
|
|
@@ -1733,7 +1843,7 @@ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
1733
1843
|
}
|
|
1734
1844
|
const settings = read.data;
|
|
1735
1845
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1736
|
-
const targetCmd = useNode ?
|
|
1846
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1737
1847
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1738
1848
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1739
1849
|
const beforeCs = settings.context_servers;
|
|
@@ -1752,7 +1862,7 @@ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
1752
1862
|
// Another vexp install already left a working entry here — don't fight it.
|
|
1753
1863
|
// Only the install path may differ: `envMatches` still gates on the
|
|
1754
1864
|
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
1755
|
-
if (removed.length === 0 && envMatches &&
|
|
1865
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1756
1866
|
return false;
|
|
1757
1867
|
const cs = settings.context_servers ?? {};
|
|
1758
1868
|
cs["vexp"] = {
|
|
@@ -1795,7 +1905,7 @@ export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRo
|
|
|
1795
1905
|
const beforeServers = config.mcpServers;
|
|
1796
1906
|
const existing = beforeServers?.["vexp"];
|
|
1797
1907
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1798
|
-
const desiredCommand = useNode ?
|
|
1908
|
+
const desiredCommand = useNode ? resolveNodeCommand() : binaryPath;
|
|
1799
1909
|
const desiredArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1800
1910
|
// Multi-session fix: do NOT pin VEXP_WORKSPACE on this user-scope entry.
|
|
1801
1911
|
// Claude Code applies ~/.claude.json mcpServers to EVERY project, so a pinned
|
|
@@ -2960,7 +3070,17 @@ ${MANDATE_CORE}
|
|
|
2960
3070
|
<!-- /vexp -->`;
|
|
2961
3071
|
}
|
|
2962
3072
|
function cursorTemplate(vars) {
|
|
2963
|
-
|
|
3073
|
+
// Cursor project rules are `.cursor/rules/*.mdc` with front matter;
|
|
3074
|
+
// `alwaysApply: true` puts the rule in every chat without the agent having
|
|
3075
|
+
// to pick it. A rule file that Cursor reads at all is new: for several
|
|
3076
|
+
// releases vexp wrote a single FILE at `.cursor/rules`, a path Cursor never
|
|
3077
|
+
// reads, and one that stopped Cursor from creating its rules folder.
|
|
3078
|
+
return `---
|
|
3079
|
+
description: vexp code-graph orientation for this workspace
|
|
3080
|
+
alwaysApply: true
|
|
3081
|
+
---
|
|
3082
|
+
|
|
3083
|
+
## vexp rules for Cursor <!-- vexp v${vars.version} -->
|
|
2964
3084
|
|
|
2965
3085
|
${MANDATE_CORE}
|
|
2966
3086
|
<!-- /vexp -->`;
|
package/dist/cli.js
CHANGED
|
@@ -42,6 +42,11 @@ if (isTraceEnabled()) {
|
|
|
42
42
|
const AUTOSTART_SKIP = new Set([
|
|
43
43
|
"setup", "daemon-cmd", "activate", "deactivate", "license", "version",
|
|
44
44
|
"serve", "autostart", "use", "doctor",
|
|
45
|
+
// Lifecycle commands: `vexp daemons` LISTS daemons and `vexp stop` ENDS
|
|
46
|
+
// one — spawning a daemon for the current directory first made `stop`
|
|
47
|
+
// followed by `daemons` bring the stopped daemon straight back
|
|
48
|
+
// (3.1 e2e, 2026-08).
|
|
49
|
+
"daemons", "stop",
|
|
45
50
|
]);
|
|
46
51
|
program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
47
52
|
// One-shot config migration for the multi-session fix — de-pins a legacy
|
|
@@ -456,7 +461,7 @@ program
|
|
|
456
461
|
});
|
|
457
462
|
program
|
|
458
463
|
.command("search <query>")
|
|
459
|
-
.description("Exhaustive index search: every matching
|
|
464
|
+
.description("Exhaustive index search: every symbol matching the query (code and docs) plus every line in indexed symbol bodies that references it, no top-K. Built for rename sweeps and zero-reference audits")
|
|
460
465
|
.option("--substring", "Substring match (LIKE) instead of token search — partial identifiers, punctuation")
|
|
461
466
|
.option("--files-only", "Print only the distinct file paths")
|
|
462
467
|
.option("--json", "Machine-readable JSON with a stable shape")
|
|
@@ -828,7 +833,15 @@ program
|
|
|
828
833
|
// the daemon already cold-starts on the first `vexp` invocation, so this is
|
|
829
834
|
// purely a convenience for post-reboot warm-up. Skip entirely when the user
|
|
830
835
|
// opted out via env or when stdin is not a TTY (non-interactive install).
|
|
831
|
-
|
|
836
|
+
//
|
|
837
|
+
// A dry run must not reach this step at all: the prompt was asked even
|
|
838
|
+
// under --dry-run and answering Yes rewrote the Startup-folder .vbs while
|
|
839
|
+
// the summary still claimed nothing was written (field report, Peiyuan,
|
|
840
|
+
// 3.0.1 on Windows). Persistence is a write like any other.
|
|
841
|
+
if (opts.dryRun) {
|
|
842
|
+
console.log(chalk.dim(" --dry-run: would ask about login autostart (writes a Startup entry only if you accept)."));
|
|
843
|
+
}
|
|
844
|
+
else if (process.env.VEXP_NO_AUTOSTART_INSTALL === "1") {
|
|
832
845
|
// Explicit opt-out — do nothing.
|
|
833
846
|
}
|
|
834
847
|
else if (!process.stdin.isTTY) {
|
|
@@ -1108,6 +1121,9 @@ program
|
|
|
1108
1121
|
console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
|
|
1109
1122
|
console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
|
|
1110
1123
|
console.log(` All tools: ${limits.allTools ? "yes" : "no (7/10)"}`);
|
|
1124
|
+
if (limits.allTools) {
|
|
1125
|
+
console.log(` (the MCP tool list shows 4 by default to keep the catalog small; every tool stays callable — VEXP_ALL_TOOLS=1 lists them all)`);
|
|
1126
|
+
}
|
|
1111
1127
|
if (limits.renewsAt) {
|
|
1112
1128
|
console.log(ltd
|
|
1113
1129
|
? ` Renewal: none — lifetime licence (local token auto-refreshes, ` +
|
|
@@ -1749,6 +1765,9 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
1749
1765
|
console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
|
|
1750
1766
|
console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
|
|
1751
1767
|
console.log(` All tools: ${limits.allTools ? "yes" : "no (7/10)"}`);
|
|
1768
|
+
if (limits.allTools) {
|
|
1769
|
+
console.log(` (the MCP tool list shows 4 by default to keep the catalog small; every tool stays callable — VEXP_ALL_TOOLS=1 lists them all)`);
|
|
1770
|
+
}
|
|
1752
1771
|
if (limits.renewsAt) {
|
|
1753
1772
|
console.log(ltd
|
|
1754
1773
|
? ` Renewal: none — lifetime licence (token auto-refreshes, valid to ${limits.renewsAt.toLocaleDateString()})`
|
package/dist/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as net from "net";
|
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { socketPathFor } from "./socket-path.js";
|
|
8
|
+
import { parseJsonc } from "./agent-config.js";
|
|
8
9
|
// `vexp doctor` — audit the vexp MCP/daemon state WITHOUT connecting to a daemon.
|
|
9
10
|
// Surfaces the failure modes behind the Codex drift report: stale daemons.json
|
|
10
11
|
// entries, wrong-workspace resolution, mixed Codex transport (url+stdio),
|
|
@@ -191,6 +192,80 @@ export function gitHooksVerdict(hooksPath, repoRoot, installedCount) {
|
|
|
191
192
|
` the hooks only make that immediate — if you want them and a tool manages this directory (moon, husky, lefthook), add 'vexp index --finalize || true' through ITS config, not the generated file.`,
|
|
192
193
|
};
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The size-skip verdict, from `.vexp/coverage.json` (3.1 shape), as data.
|
|
197
|
+
*
|
|
198
|
+
* Files over `max_file_size_kb` are left out of the index by the walk. Until
|
|
199
|
+
* 3.1 the only trace was an INFO line in the daemon log: doctor, the
|
|
200
|
+
* `index_status` tool and coverage.json all reported a healthy index while a
|
|
201
|
+
* tier-4 user was missing five hand-written Dart files — 21% of the bytes of
|
|
202
|
+
* his lib/ — from every impact, search and pipeline answer (field report,
|
|
203
|
+
* 2026-08). Raising the cap only moved the cliff, so the cliff is reported
|
|
204
|
+
* wherever it stands. Null when nothing was skipped for size, or when the
|
|
205
|
+
* file predates 3.1 and has no skip keys.
|
|
206
|
+
*/
|
|
207
|
+
export function coverageVerdict(cov) {
|
|
208
|
+
if (!cov || typeof cov !== "object")
|
|
209
|
+
return null;
|
|
210
|
+
const c = cov;
|
|
211
|
+
const oversized = Number(c.skipped_oversized) || 0;
|
|
212
|
+
if (oversized === 0)
|
|
213
|
+
return null;
|
|
214
|
+
const cap = Number(c.max_file_size_kb) || 0;
|
|
215
|
+
const files = Array.isArray(c.skipped_files) ? c.skipped_files : [];
|
|
216
|
+
const examples = files
|
|
217
|
+
.filter((f) => f.reason === "oversized")
|
|
218
|
+
.slice(0, 5)
|
|
219
|
+
.map((f) => `${f.path} (${Number(f.size_kb) || 0} KB)`);
|
|
220
|
+
const more = oversized - examples.length;
|
|
221
|
+
return {
|
|
222
|
+
level: WARN,
|
|
223
|
+
message: `${oversized} file(s) over max_file_size_kb = ${cap} are NOT indexed — their symbols and callers are invisible to impact, search and run_pipeline:\n` +
|
|
224
|
+
examples.map((e) => ` - ${e}`).join("\n") +
|
|
225
|
+
(more > 0 ? `\n … +${more} more (full list: .vexp/coverage.json)` : "") +
|
|
226
|
+
`\n raise max_file_size_kb in .vexp/vexp.toml (0 = no cap) to include them, or exclude them on purpose with exclude_patterns.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The `.vscode/mcp.json` verdict for GitHub Copilot, as data.
|
|
231
|
+
*
|
|
232
|
+
* A Copilot user who "set up the vexp agent" and sees neither a vexp server in
|
|
233
|
+
* VS Code nor vexp tools in chat has, in our experience, one of three things:
|
|
234
|
+
* the file is not where VS Code looks (the folder open in VS Code is not the
|
|
235
|
+
* one that was set up), the server entry cannot start (a bare `node` that the
|
|
236
|
+
* GUI-launched editor cannot find on its PATH, or a bundle path an extension
|
|
237
|
+
* upgrade removed), or Chat is not in Agent mode — the only mode that offers
|
|
238
|
+
* MCP tools. doctor can prove the first two; it can only say the third.
|
|
239
|
+
*/
|
|
240
|
+
export function vsCodeMcpVerdict(cfg, wsRoot, exists = (p) => fs.existsSync(p)) {
|
|
241
|
+
if (!cfg || typeof cfg !== "object")
|
|
242
|
+
return { level: WARN, message: ".vscode/mcp.json is present but not valid JSON — VS Code will ignore every server in it; fix the syntax and re-run 'vexp setup'" };
|
|
243
|
+
const servers = cfg.servers;
|
|
244
|
+
const vexp = servers?.vexp;
|
|
245
|
+
if (!vexp)
|
|
246
|
+
return { level: WARN, message: ".vscode/mcp.json has no 'vexp' server — run: vexp setup --agents \"GitHub Copilot\"" };
|
|
247
|
+
const command = typeof vexp.command === "string" ? vexp.command : "";
|
|
248
|
+
const args = Array.isArray(vexp.args) ? vexp.args.filter((a) => typeof a === "string") : [];
|
|
249
|
+
const script = args.find((a) => /\.[cm]?js$/.test(a));
|
|
250
|
+
if (!command)
|
|
251
|
+
return { level: WARN, message: ".vscode/mcp.json vexp server has no 'command' — re-run 'vexp setup'" };
|
|
252
|
+
if (!/[\\/]/.test(command)) {
|
|
253
|
+
return {
|
|
254
|
+
level: WARN,
|
|
255
|
+
message: `.vscode/mcp.json starts the vexp server with a bare '${command}' — resolved through the editor's PATH, which a VS Code launched from the Dock/Start menu usually lacks (symptom: 'spawn ${command} ENOENT' in Output › MCP: vexp, no vexp tools in chat).\n` +
|
|
256
|
+
` re-run 'vexp setup' — 3.1 pins the absolute node path.`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (!exists(command))
|
|
260
|
+
return { level: WARN, message: `.vscode/mcp.json vexp command does not exist: ${command} — re-run 'vexp setup' to repin it` };
|
|
261
|
+
if (script && !exists(script))
|
|
262
|
+
return { level: WARN, message: `.vscode/mcp.json vexp server bundle is missing: ${script} (an editor upgrade removed the old extension folder?) — re-run 'vexp setup'` };
|
|
263
|
+
const pinned = typeof vexp.env?.VEXP_WORKSPACE === "string" ? vexp.env.VEXP_WORKSPACE : undefined;
|
|
264
|
+
if (pinned && path.resolve(pinned).toLowerCase() !== path.resolve(wsRoot).toLowerCase()) {
|
|
265
|
+
return { level: WARN, message: `.vscode/mcp.json vexp server is pinned to ${pinned}, but this workspace is ${wsRoot} — re-run 'vexp setup' here` };
|
|
266
|
+
}
|
|
267
|
+
return { level: OK, message: `.vscode/mcp.json vexp server: ${command} ${script ?? args.join(" ")}` };
|
|
268
|
+
}
|
|
194
269
|
export async function runDoctor() {
|
|
195
270
|
const home = vexpHome();
|
|
196
271
|
let warns = 0;
|
|
@@ -310,6 +385,16 @@ export async function runDoctor() {
|
|
|
310
385
|
}
|
|
311
386
|
}
|
|
312
387
|
}
|
|
388
|
+
// 3.1 — coverage gaps only the daemon log used to witness. Read from disk,
|
|
389
|
+
// not from the daemon: the index that skipped the files may have been
|
|
390
|
+
// built by a daemon that is no longer running.
|
|
391
|
+
try {
|
|
392
|
+
const cov = JSON.parse(fs.readFileSync(path.join(ws.root, ".vexp", "coverage.json"), "utf-8"));
|
|
393
|
+
const v = coverageVerdict(cov);
|
|
394
|
+
if (v)
|
|
395
|
+
line(v.level, v.message);
|
|
396
|
+
}
|
|
397
|
+
catch { /* no coverage.json: never indexed here, or an index older than 2.7 */ }
|
|
313
398
|
// 2) Daemon registry (~/.vexp/daemons.json) — stale entries are a drift source.
|
|
314
399
|
console.log(chalk.bold("\nDaemon registry (~/.vexp/daemons.json)"));
|
|
315
400
|
const regPath = path.join(home, ".vexp", "daemons.json");
|
|
@@ -434,6 +519,27 @@ export async function runDoctor() {
|
|
|
434
519
|
catch {
|
|
435
520
|
line(OK, "no ~/.claude.json");
|
|
436
521
|
}
|
|
522
|
+
// 5a) GitHub Copilot — VS Code reads MCP servers from <folder>/.vscode/mcp.json.
|
|
523
|
+
console.log(chalk.bold("\nGitHub Copilot / VS Code (.vscode/mcp.json)"));
|
|
524
|
+
{
|
|
525
|
+
const mcpPath = path.join(ws.root, ".vscode", "mcp.json");
|
|
526
|
+
if (!fs.existsSync(mcpPath)) {
|
|
527
|
+
line(OK, "no .vscode/mcp.json (Copilot MCP not configured in this folder — 'vexp setup --agents \"GitHub Copilot\"' writes it)");
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
let cfg = null;
|
|
531
|
+
try {
|
|
532
|
+
cfg = parseJsonc(fs.readFileSync(mcpPath, "utf-8"));
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
cfg = null;
|
|
536
|
+
}
|
|
537
|
+
const v = vsCodeMcpVerdict(cfg, ws.root);
|
|
538
|
+
line(v.level, v.message);
|
|
539
|
+
console.log(chalk.dim(" VS Code shows it under Extensions → MCP SERVERS - INSTALLED; vexp tools appear only in Copilot Chat AGENT mode (tools picker → 'MCP Server: vexp')."));
|
|
540
|
+
console.log(chalk.dim(" server log: Command Palette → 'MCP: List Servers' → vexp → Show Output (channel 'MCP: vexp'); daemon log: .vexp/daemon.log"));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
437
543
|
// 5b) Claude Code guard hook — EXECUTE it the way Claude Code would, don't
|
|
438
544
|
// just check presence. A shell-form command that word-splits on a project
|
|
439
545
|
// path containing a space fails non-blocking on every call: the guard never
|
package/dist/license.js
CHANGED
|
@@ -206,10 +206,10 @@ export async function tryOnlineRefresh(longJwt) {
|
|
|
206
206
|
// Only save if the freshToken itself verifies locally
|
|
207
207
|
if (verifyAndDecode(data.freshToken)) {
|
|
208
208
|
saveFreshToken(data.freshToken);
|
|
209
|
-
// The server re-issued
|
|
210
|
-
//
|
|
211
|
-
// the on-disk long JWT so the
|
|
212
|
-
// offline and
|
|
209
|
+
// The server re-issued the 30-day long token (rolled forward on every
|
|
210
|
+
// refresh since vexp-web 3.1; also on entitlement change). Overwrite
|
|
211
|
+
// the on-disk long JWT so the current entitlement survives even fully
|
|
212
|
+
// offline and a superseded one stops working — without the user
|
|
213
213
|
// re-pasting a key. Only persist if it verifies locally.
|
|
214
214
|
if (data.newLongToken && verifyAndDecode(data.newLongToken)) {
|
|
215
215
|
try {
|
package/mcp/mcp-server.cjs
CHANGED
|
@@ -119,7 +119,7 @@ To lift the limit now, upgrade to Pro or Team: https://vexp.dev/#pricing`)}var V
|
|
|
119
119
|
|
|
120
120
|
`)),e.push("```"),e.push("");return e.join(`
|
|
121
121
|
`)}var aF=S.object({}),Y$={name:"index_status",description:"Get the current status of the vexp index, including indexed repos, node/edge counts, cross-repo edges, daemon uptime, and indexing progress. Use this to verify vexp is working correctly before making queries.",inputSchema:{type:"object",properties:{},required:[]}};async function Q$(t,e){aF.parse(t);let r=Xa(),n=await e.call("index_status",{});return iF(n,r)}function J$(t){return t.replace(/[\\/]+$/,"").toLowerCase()}function iF(t,e){let r=[],n={healthy:"\u2713",indexing:"\u27F3",error:"\u2717",not_initialized:"\u25CB"}[t.status]??"?";if(r.push("# vexp Index Status"),r.push(`> ${n} ${t.status.toUpperCase()} | v${t.daemon_version} | uptime ${oF(t.daemon_uptime_s)}`),r.push(""),e||t.workspace_root){let a=e?.workspaceRoot,i=t.workspace_root;if(r.push("## Targeting"),e&&(r.push(`- This MCP resolved to: \`${a}\` (via ${e.source})`),e.socket&&r.push(`- Socket/pipe: \`${e.socket}\` (${e.socketOrigin})`)),i){let s=t.served_aliases?.length?` \u2014 repos: ${t.served_aliases.join(", ")}`:"";r.push(`- Daemon serving: \`${i}\`${s}`)}a&&i&&J$(a)!==J$(i)&&r.push(`- \u26A0\uFE0F MISMATCH: this session resolved a different workspace than the daemon it reached. Likely a parallel-session mis-target \u2014 set VEXP_WORKSPACE for this session, or launch \`claude\` from \`${a}\`.`);let o=t.other_daemons??[];r.push(o.length>0?`- Other live daemons: ${o.map(s=>`\`${s.root}\``).join(", ")}`:"- Other live daemons: none"),r.push("")}if(t.status==="error"&&t.error&&(r.push(`**Error:** ${t.error}`),r.push("")),t.status==="not_initialized")return r.push('*vexp is not initialized. Run `vexp index` or use the VS Code command "Setup Workspace".*'),r.join(`
|
|
122
|
-
`);if(r.push("## Overview"),r.push(""),r.push("| Metric | Value |"),r.push("|--------|-------|"),r.push(`| Repos indexed | ${t.repos.length} |`),r.push(`| Total nodes | ${t.total_nodes.toLocaleString()} |`),r.push(`| Total edges | ${t.total_edges.toLocaleString()} |`),r.push(`| Total files | ${t.total_files.toLocaleString()} |`),r.push(`| Cross-repo edges | ${t.cross_repo_edges.toLocaleString()} |`),r.push(`| Index DB size | ${X$(t.db_size_bytes)} |`),r.push(`| Daemon memory | ${X$(t.memory_rss_bytes)} |`),t.compressor){let a=t.compressor==="llm"?`llm (${t.llm_model??"?"}, ${t.llm_inference??"?"})`:"rule";r.push(`| Compressor | ${a} |`)}if(t.last_query_time_ms!==void 0&&r.push(`| Last query time | ${t.last_query_time_ms}ms |`),r.push(""),t.llm_configured_but_inactive&&(r.push("\u26A0\uFE0F A local LLM model is installed and enabled in config, but this daemon is running the rule compressor. Restart the daemon (`vexp daemon-cmd restart`) or re-run `vexp setup-llm` \u2014 until then, results are NOT LLM-compressed."),r.push("")),t.ledger){let a=t.ledger;r.push(`## Savings Ledger (last ${a.period_days??7} days)`),r.push(""),(a.prompts_analyzed??0)>0&&r.push(`- Activity: ${a.prompts_analyzed} prompt(s) analyzed \u2014 ${a.silences} silence(s) (task already oriented), ${a.hints_served} orientation hint(s) served${(a.held_out??0)>0?`, ${a.held_out} held out (randomized measurement)`:""}`),(a.measured_calls??0)>0&&r.push(`- Measured: ${a.measured_calls} tool call(s), ~${(a.measured_tokens_saved??0).toLocaleString()} tokens saved (full-read baseline minus served)`),(a.prompts_analyzed??0)===0&&(a.measured_calls??0)===0&&r.push("- No classified prompts or tool calls in this window."),r.push("")}if(t.sessions&&t.sessions.length>0){r.push("## Active sessions (last 4h)"),r.push("");for(let a of t.sessions){let i=a.idle_s<60?`${a.idle_s}s`:`${Math.floor(a.idle_s/60)}m`;r.push(`- \`${a.session}\u2026\` \u2014 ${a.pipeline_calls} pipeline, ${a.skeleton_calls} skeleton, ${a.other_calls} other (idle ${i})`)}r.push("")}if(t.repos.length>0){r.push("## Repos"),r.push("");for(let a of t.repos){let i=a.is_indexing?` *(indexing ${a.indexing_progress?.toFixed(0)??"?"}%)*`:"";if(r.push(`### \`${a.alias}\`${i}`),r.push(`*${a.path}*`),r.push(""),r.push(`- Nodes: ${a.node_count.toLocaleString()}`),r.push(`- Edges: ${a.edge_count.toLocaleString()}`),r.push(`- Files: ${a.file_count.toLocaleString()}`),r.push(`- Last indexed: ${a.last_indexed_at}`),Object.keys(a.languages).length>0){let o=Object.entries(a.languages).sort(([,s],[,c])=>c-s).map(([s,c])=>`${s}: ${c}`).join(", ");r.push(`- Languages: ${o}`)}r.push("")}}return r.join(`
|
|
122
|
+
`);if(r.push("## Overview"),r.push(""),r.push("| Metric | Value |"),r.push("|--------|-------|"),r.push(`| Repos indexed | ${t.repos.length} |`),r.push(`| Total nodes | ${t.total_nodes.toLocaleString()} |`),r.push(`| Total edges | ${t.total_edges.toLocaleString()} |`),r.push(`| Total files | ${t.total_files.toLocaleString()} |`),r.push(`| Cross-repo edges | ${t.cross_repo_edges.toLocaleString()} |`),r.push(`| Index DB size | ${X$(t.db_size_bytes)} |`),r.push(`| Daemon memory | ${X$(t.memory_rss_bytes)} |`),t.compressor){let a=t.compressor==="llm"?`llm (${t.llm_model??"?"}, ${t.llm_inference??"?"})`:"rule";r.push(`| Compressor | ${a} |`)}if(t.last_query_time_ms!==void 0&&r.push(`| Last query time | ${t.last_query_time_ms}ms |`),r.push(""),t.llm_configured_but_inactive&&(r.push("\u26A0\uFE0F A local LLM model is installed and enabled in config, but this daemon is running the rule compressor. Restart the daemon (`vexp daemon-cmd restart`) or re-run `vexp setup-llm` \u2014 until then, results are NOT LLM-compressed."),r.push("")),t.coverage){let a=t.coverage,i=[];if((a.skipped_oversized??0)>0){let o=a.skipped_examples?.length?` \u2014 e.g. ${a.skipped_examples.join(", ")}`:"";i.push(`- \u26A0\uFE0F ${a.skipped_oversized} file(s) over max_file_size_kb = ${a.max_file_size_kb??"?"} are NOT indexed${o}. Their symbols and callers are invisible to impact, search and run_pipeline. Full list: .vexp/coverage.json; raise the cap in .vexp/vexp.toml to include them.`)}a.unparsed_code_files?.length&&i.push(`- Unparsed file types (no parser yet): ${a.unparsed_code_files.map(o=>`${o.count} .${o.ext}`).join(", ")}`),i.length>0&&(r.push("## Coverage gaps"),r.push(""),r.push(...i),r.push(""))}if(t.ledger){let a=t.ledger;r.push(`## Savings Ledger (last ${a.period_days??7} days)`),r.push(""),(a.prompts_analyzed??0)>0&&r.push(`- Activity: ${a.prompts_analyzed} prompt(s) analyzed \u2014 ${a.silences} silence(s) (task already oriented), ${a.hints_served} orientation hint(s) served${(a.held_out??0)>0?`, ${a.held_out} held out (randomized measurement)`:""}`),(a.measured_calls??0)>0&&r.push(`- Measured: ${a.measured_calls} tool call(s), ~${(a.measured_tokens_saved??0).toLocaleString()} tokens saved (full-read baseline minus served)`),(a.prompts_analyzed??0)===0&&(a.measured_calls??0)===0&&r.push("- No classified prompts or tool calls in this window."),r.push("")}if(t.sessions&&t.sessions.length>0){r.push("## Active sessions (last 4h)"),r.push("");for(let a of t.sessions){let i=a.idle_s<60?`${a.idle_s}s`:`${Math.floor(a.idle_s/60)}m`;r.push(`- \`${a.session}\u2026\` \u2014 ${a.pipeline_calls} pipeline, ${a.skeleton_calls} skeleton, ${a.other_calls} other (idle ${i})`)}r.push("")}if(t.repos.length>0){r.push("## Repos"),r.push("");for(let a of t.repos){let i=a.is_indexing?` *(indexing ${a.indexing_progress?.toFixed(0)??"?"}%)*`:"";if(r.push(`### \`${a.alias}\`${i}`),r.push(`*${a.path}*`),r.push(""),r.push(`- Nodes: ${a.node_count.toLocaleString()}`),r.push(`- Edges: ${a.edge_count.toLocaleString()}`),r.push(`- Files: ${a.file_count.toLocaleString()}`),r.push(`- Last indexed: ${a.last_indexed_at}`),Object.keys(a.languages).length>0){let o=Object.entries(a.languages).sort(([,s],[,c])=>c-s).map(([s,c])=>`${s}: ${c}`).join(", ");r.push(`- Languages: ${o}`)}r.push("")}}return r.join(`
|
|
123
123
|
`)}function oF(t){if(t<60)return`${t}s`;if(t<3600)return`${Math.floor(t/60)}m ${t%60}s`;let e=Math.floor(t/3600),r=Math.floor(t%3600/60);return`${e}h ${r}m`}function X$(t){return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:t<1024*1024*1024?`${(t/1024/1024).toFixed(1)} MB`:`${(t/1024/1024/1024).toFixed(2)} GB`}var sF=S.object({workspace_root:S.string().optional().describe("Workspace root path (default: CWD)"),detect_agents:S.boolean().optional().default(!0).describe("Auto-detect AI agents")}),e4={name:"workspace_setup",description:"Set up vexp for the current workspace. Generates configuration files, detects AI coding agents (Claude Code, Cursor, Windsurf, Continue), and provides setup instructions. Run this once when starting to work on a new project.",inputSchema:{type:"object",properties:{workspace_root:{type:"string",description:"Workspace root path (default: current directory)"},detect_agents:{type:"boolean",description:"Auto-detect installed AI agents (default: true)"}},required:[]}};async function t4(t,e){let r=sF.parse(t),n=await e.call("workspace_setup",r);return cF(n)}function cF(t){let e=[];if(e.push("# vexp Workspace Setup"),e.push(`> v${t.vexp_version} | \`${t.workspace_root}\``),e.push(""),t.generated_files.length>0){e.push("## Generated Files"),e.push("");for(let r of t.generated_files)e.push(`- \`${r}\``);e.push("")}if(t.workspace_json&&(e.push("## workspace.json"),e.push(""),e.push("```json"),e.push(t.workspace_json),e.push("```"),e.push("")),t.detected_agents.length>0){e.push("## Detected AI Agents"),e.push("");for(let r of t.detected_agents)e.push(`- **${r}**`);e.push("")}if(t.agent_configs.length>0){e.push("## Agent Configuration Files"),e.push("");for(let r of t.agent_configs){let n=r.already_exists?" *(already exists \u2014 merge manually)*":" *(created)*";e.push(`### ${r.agent} \u2014 \`${r.config_file}\`${n}`),e.push(""),e.push("```"),e.push(r.content),e.push("```"),e.push("")}}if(t.git_hooks_installed&&(e.push("## Git Hooks"),e.push(""),e.push("\u2713 Git hooks installed: `pre-commit`, `post-merge`, `post-checkout`"),e.push("\u2713 Merge driver configured for `index.db`"),e.push("")),t.suggested_gitignore_additions.length>0){e.push("## Suggested .gitignore Additions"),e.push(""),e.push("Add these to your `.gitignore`:"),e.push("```gitignore");for(let r of t.suggested_gitignore_additions)e.push(r);e.push("```"),e.push("")}if(t.index_started&&(e.push("## Indexing"),e.push(""),e.push("\u2713 Initial indexing started in background. Use `index_status` to check progress."),e.push("")),t.setup_instructions.length>0){e.push("## Next Steps"),e.push("");for(let r=0;r<t.setup_instructions.length;r++)e.push(`${r+1}. ${t.setup_instructions[r]}`);e.push("")}return e.join(`
|
|
124
124
|
`)}var uF=S.object({include_previous:S.preprocess(ft,S.boolean().optional().default(!1)).describe("Include observations from previous sessions"),max_results:S.preprocess(pt,S.number().optional().default(20)).describe("Max observations to return"),types:S.preprocess(or,S.array(S.string()).optional()).describe("Filter by observation type")}),r4={name:"get_session_context",description:"Get observations from the current and optionally previous sessions. Observations are automatically captured from every vexp tool call and include what code was explored, decisions made, and insights discovered. Use this to recall what you worked on earlier or in prior sessions. Observations linked to code symbols are automatically marked stale when that code changes.",inputSchema:{type:"object",properties:{include_previous:{type:"boolean",description:"Include observations from previous sessions (default: false)"},max_results:{type:"number",description:"Max observations to return (default: 20)"},types:{type:"array",items:{type:"string",enum:["tool_call","insight","decision","error","manual"]},description:"Filter by observation type"}},required:[]}};async function n4(t,e){let r=uF.parse(t),n=await e.call("get_session_context",r);return pF(n)}function pF(t){let e=[];if(e.push("# Session Context"),e.push(`> Session: ${t.session_id} | ${t.count} observations`),e.push(""),t.observations.length===0)return e.push("*No observations yet in this session.*"),e.join(`
|
|
125
125
|
`);for(let r of t.observations){let n=new Date(r.created_at*1e3),a=`${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")} ${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`,i=r.stale?" ~stale~":"",o=r.current_session===!1?` [prev: ${r.session_id?.slice(0,8)}]`:"";e.push(`- [${a}${i}] (${r.type}) ${r.content}${o}`)}return e.join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexp-cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Local-first context engine for AI coding agents. Pre-indexes your codebase into a dependency graph and feeds any MCP agent only the code that matters — 87% fewer tokens per call. New in 2.5: mechanical work verification and a PII/secret scanner. Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Cline, Aider and 14 agents. Your code never leaves your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
},
|
|
106
106
|
"homepage": "https://vexp.dev",
|
|
107
107
|
"optionalDependencies": {
|
|
108
|
-
"@vexp/core-linux-x64": "3.0
|
|
109
|
-
"@vexp/core-linux-arm64": "3.0
|
|
110
|
-
"@vexp/core-darwin-x64": "3.0
|
|
111
|
-
"@vexp/core-darwin-arm64": "3.0
|
|
112
|
-
"@vexp/core-win32-x64": "3.0
|
|
108
|
+
"@vexp/core-linux-x64": "3.1.0",
|
|
109
|
+
"@vexp/core-linux-arm64": "3.1.0",
|
|
110
|
+
"@vexp/core-darwin-x64": "3.1.0",
|
|
111
|
+
"@vexp/core-darwin-arm64": "3.1.0",
|
|
112
|
+
"@vexp/core-win32-x64": "3.1.0"
|
|
113
113
|
}
|
|
114
114
|
}
|