vexp-cli 2.2.4 → 2.3.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 +385 -152
- package/dist/cli.js +6 -2
- package/dist/doctor.js +54 -0
- package/dist/serve.js +56 -2
- package/mcp/mcp-server.cjs +29 -29
- package/package.json +6 -6
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, resolveAgentName, suggestAgentName } from "./agent-config.js";
|
|
11
|
+
import { detectAgents, getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName, setGuardMode } 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";
|
|
@@ -533,8 +533,10 @@ program
|
|
|
533
533
|
.option("--agents [list]", "Filter agents to configure (comma-separated). Without value: auto-detect all")
|
|
534
534
|
.option("--dry-run", "Show what would be configured without writing files")
|
|
535
535
|
.option("--personal", "Personal mode: index locally without writing agent configs or git hooks to the shared repo")
|
|
536
|
+
.option("--guard-strict", "Install the Grep/Glob deny hooks (opt-in since 2.3; default setup removes them)")
|
|
536
537
|
.action(async (dir, opts) => {
|
|
537
538
|
const workspaceRoot = path.resolve(dir ?? process.cwd());
|
|
539
|
+
setGuardMode(opts.guardStrict ? "strict" : "off");
|
|
538
540
|
console.log(chalk.bold(`\nvexp setup — ${workspaceRoot}\n`));
|
|
539
541
|
// Step 1: Ensure binary
|
|
540
542
|
const spinner1 = ora("Checking vexp binary...").start();
|
|
@@ -737,7 +739,9 @@ program
|
|
|
737
739
|
program
|
|
738
740
|
.command("setup-agents [dir]")
|
|
739
741
|
.description("Configure AI coding agents to use vexp MCP (interactive multi-select)")
|
|
740
|
-
.
|
|
742
|
+
.option("--guard-strict", "Install the Grep/Glob deny hooks (opt-in since 2.3; default removes them)")
|
|
743
|
+
.action(async (dir, opts) => {
|
|
744
|
+
setGuardMode(opts.guardStrict ? "strict" : "off");
|
|
741
745
|
await runSetupAgents(dir);
|
|
742
746
|
});
|
|
743
747
|
async function runSetupAgents(dir) {
|
package/dist/doctor.js
CHANGED
|
@@ -48,6 +48,32 @@ function resolveWorkspace() {
|
|
|
48
48
|
// it — so doctor probed a pipe name the daemon never binds for ~6% of Windows
|
|
49
49
|
// workspaces and declared a healthy daemon down.
|
|
50
50
|
const socketForWorkspace = socketPathFor;
|
|
51
|
+
/** One-shot daemon query over the newline-JSON socket protocol. Returns the
|
|
52
|
+
* `result` payload or null on any failure — doctor must never hang or throw. */
|
|
53
|
+
function queryDaemon(sock, tool, timeoutMs = 2500) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const c = process.platform === "win32" ? net.createConnection(sock) : net.createConnection({ path: sock });
|
|
56
|
+
let buf = "";
|
|
57
|
+
const t = setTimeout(() => { c.destroy(); resolve(null); }, timeoutMs);
|
|
58
|
+
c.on("connect", () => c.write(JSON.stringify({ id: 1, tool, params: {} }) + "\n"));
|
|
59
|
+
c.on("data", (d) => {
|
|
60
|
+
buf += d.toString();
|
|
61
|
+
const i = buf.indexOf("\n");
|
|
62
|
+
if (i !== -1) {
|
|
63
|
+
clearTimeout(t);
|
|
64
|
+
c.destroy();
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(buf.slice(0, i));
|
|
67
|
+
resolve(parsed?.result ?? null);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
resolve(null);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
c.on("error", () => { clearTimeout(t); resolve(null); });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
51
77
|
function reachable(sock, timeoutMs = 600) {
|
|
52
78
|
return new Promise((resolve) => {
|
|
53
79
|
const c = process.platform === "win32" ? net.createConnection(sock) : net.createConnection({ path: sock });
|
|
@@ -111,6 +137,34 @@ export async function runDoctor() {
|
|
|
111
137
|
line(live ? OK : WARN, `daemon socket ${live ? "reachable" : "NOT reachable"}: ${sock}`);
|
|
112
138
|
if (!live)
|
|
113
139
|
console.log(chalk.dim(` → run: vexp daemon-cmd start (or 'vexp daemon-cmd restart')`));
|
|
140
|
+
// 2.3 B1/B2 — live daemon truth: coverage + active compressor. The config can
|
|
141
|
+
// say "LLM enabled" while the daemon serves rule-compressed output (config
|
|
142
|
+
// race / stale daemon / non-LLM build); only the daemon knows what's active.
|
|
143
|
+
if (live) {
|
|
144
|
+
const st = await queryDaemon(sock, "index_status");
|
|
145
|
+
if (st) {
|
|
146
|
+
const comp = st.compressor === "llm"
|
|
147
|
+
? `llm (${st.llm_model ?? "?"}, ${st.llm_inference ?? "?"})`
|
|
148
|
+
: st.compressor ?? "unknown (older daemon)";
|
|
149
|
+
line(OK, `index: ${st.total_files ?? "?"} files · ${st.total_nodes ?? "?"} nodes · state ${st.status ?? "?"} · compressor ${comp}`);
|
|
150
|
+
if (st.llm_configured_but_inactive === true) {
|
|
151
|
+
line(WARN, `local LLM is installed and enabled in config but this daemon runs the RULE compressor — results are not LLM-compressed. Run 'vexp daemon-cmd restart' to load the model.`);
|
|
152
|
+
}
|
|
153
|
+
// 2.3 C1 — which agent sessions actually used vexp (a session with zero
|
|
154
|
+
// calls never shows up here; that absence is the diagnostic).
|
|
155
|
+
const sessions = Array.isArray(st.sessions) ? st.sessions : [];
|
|
156
|
+
if (sessions.length > 0) {
|
|
157
|
+
const total = sessions.reduce((n, s) => n + (Number(s.pipeline_calls) || 0), 0);
|
|
158
|
+
line(OK, `sessions (4h): ${sessions.length} active, ${total} pipeline calls total`);
|
|
159
|
+
}
|
|
160
|
+
else if (Number(st.daemon_uptime_s) > 600) {
|
|
161
|
+
line(WARN, `no agent session has called vexp since daemon start — if an agent is working here, it is not using vexp (check its MCP config)`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
line(OK, `no session calls yet (daemon just started)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
114
168
|
// 2) Daemon registry (~/.vexp/daemons.json) — stale entries are a drift source.
|
|
115
169
|
console.log(chalk.bold("\nDaemon registry (~/.vexp/daemons.json)"));
|
|
116
170
|
const regPath = path.join(home, ".vexp", "daemons.json");
|
package/dist/serve.js
CHANGED
|
@@ -89,17 +89,71 @@ async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/** True when `sock` is the canonical socket for `ws` itself (unix path or the
|
|
93
|
+
* win32 named pipe derived from `ws`). A registry row that FAILS this is a
|
|
94
|
+
* ROUTING entry: a connected secondary repo mapped to its parent workspace's
|
|
95
|
+
* socket so lookups route correctly. */
|
|
96
|
+
export function isOwnerEntry(ws, sock) {
|
|
97
|
+
if (process.platform === "win32") {
|
|
98
|
+
// Named pipes carry no path; treat the row as owner unless another row
|
|
99
|
+
// with the same pipe proves otherwise (handled by the grouping below).
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return path.resolve(sock) === path.resolve(path.join(ws, ".vexp", "daemon.sock"));
|
|
103
|
+
}
|
|
104
|
+
/** One (workspace, socket) spawn per SOCKET, always the socket's OWNER root.
|
|
105
|
+
* Field incident (2026-07-23): the registry legitimately maps a connected
|
|
106
|
+
* repo to its PARENT workspace's socket for routing; resurrecting that row
|
|
107
|
+
* verbatim spawned `daemon --workspace <child> --socket <parent sock>` — a
|
|
108
|
+
* cross-wired daemon serving the child's index on the parent's socket, which
|
|
109
|
+
* then blocked every legitimate restart of the parent (it held the bind) and
|
|
110
|
+
* never matched the parent's config. Routing rows must never be spawned. */
|
|
111
|
+
export function resurrectionPlan(reg) {
|
|
112
|
+
const bySock = new Map();
|
|
113
|
+
for (const [ws, sock] of Object.entries(reg)) {
|
|
114
|
+
const list = bySock.get(sock) ?? [];
|
|
115
|
+
list.push(ws);
|
|
116
|
+
bySock.set(sock, list);
|
|
117
|
+
}
|
|
118
|
+
const plan = [];
|
|
119
|
+
for (const [sock, roots] of bySock) {
|
|
120
|
+
const owner = roots.find((ws) => isOwnerEntry(ws, sock));
|
|
121
|
+
if (owner) {
|
|
122
|
+
plan.push({ ws: owner, sock });
|
|
123
|
+
}
|
|
124
|
+
else if (roots.length === 1 && !sock.endsWith(path.join(".vexp", "daemon.sock"))) {
|
|
125
|
+
// Long-path /tmp-fallback socket (lives OUTSIDE any repo): a single
|
|
126
|
+
// registrant is unambiguous, keep prior behavior. A canonical
|
|
127
|
+
// in-repo socket that is NOT under the registrant is a routing row
|
|
128
|
+
// whose owner vanished from the registry — never cross-spawn it.
|
|
129
|
+
plan.push({ ws: roots[0], sock });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
appendLog(`registry: no owner for socket ${sock} (roots: ${roots.join(", ")}) — skipping resurrect`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return plan;
|
|
136
|
+
}
|
|
92
137
|
async function resurrectAll() {
|
|
93
138
|
const reg = readRegistry();
|
|
94
139
|
const live = {};
|
|
140
|
+
// Prune rows whose workspace vanished, but keep routing rows alive in the
|
|
141
|
+
// registry — they are data, not spawn instructions.
|
|
142
|
+
const pruned = {};
|
|
95
143
|
for (const [ws, sock] of Object.entries(reg)) {
|
|
96
144
|
const manifest = path.join(ws, ".vexp", "manifest.json");
|
|
97
145
|
if (!fs.existsSync(manifest)) {
|
|
98
146
|
appendLog(`registry prune: ${ws} (no manifest)`);
|
|
99
147
|
continue;
|
|
100
148
|
}
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
pruned[ws] = sock;
|
|
150
|
+
}
|
|
151
|
+
const resurrected = new Map();
|
|
152
|
+
for (const { ws, sock } of resurrectionPlan(pruned)) {
|
|
153
|
+
resurrected.set(sock, await resurrectDaemon(ws, sock));
|
|
154
|
+
}
|
|
155
|
+
for (const [ws, sock] of Object.entries(pruned)) {
|
|
156
|
+
if (resurrected.get(sock))
|
|
103
157
|
live[ws] = sock;
|
|
104
158
|
}
|
|
105
159
|
writeRegistry(live);
|