vexp-cli 2.0.31 → 2.0.33
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/doctor.js +20 -0
- package/dist/hook-template.js +41 -8
- package/dist/license.js +22 -0
- package/mcp/mcp-server.cjs +41 -41
- package/package.json +6 -6
package/dist/doctor.js
CHANGED
|
@@ -102,6 +102,19 @@ export async function runDoctor() {
|
|
|
102
102
|
const ws = resolveWorkspace();
|
|
103
103
|
console.log(` cwd: ${process.cwd()}`);
|
|
104
104
|
line(OK, `resolves to: ${ws.root} (via ${ws.source})`);
|
|
105
|
+
// A *global* VEXP_WORKSPACE env var overrides the per-session signal
|
|
106
|
+
// (CLAUDE_PROJECT_DIR) for EVERY agent session, pinning Claude *and* Codex to
|
|
107
|
+
// one repo's daemon. Agents working in other repos then hit the wrong daemon
|
|
108
|
+
// and get [] / wrong-repo results from get_skeleton & run_pipeline. The
|
|
109
|
+
// ~/.claude.json / ~/.codex checks below only catch a pin written into the MCP
|
|
110
|
+
// *config*; a shell/OS env var is invisible to them — flag it here when it
|
|
111
|
+
// disagrees with this directory's natural target.
|
|
112
|
+
if (ws.source === "VEXP_WORKSPACE") {
|
|
113
|
+
const discovered = discoverWorkspaceRoot(process.cwd());
|
|
114
|
+
if (discovered.toLowerCase() !== ws.root.toLowerCase()) {
|
|
115
|
+
line(WARN, `global VEXP_WORKSPACE env var pins this session to ${ws.root}, but this directory's natural target is ${discovered} → agents here get [] / wrong-repo results. Unset the VEXP_WORKSPACE OS/shell env var for per-session targeting.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
105
118
|
const sock = socketForWorkspace(ws.root);
|
|
106
119
|
const live = await reachable(sock);
|
|
107
120
|
line(live ? OK : WARN, `daemon socket ${live ? "reachable" : "NOT reachable"}: ${sock}`);
|
|
@@ -132,6 +145,13 @@ export async function runDoctor() {
|
|
|
132
145
|
const inReg = entries.some(([r]) => r.toLowerCase() === ws.root.toLowerCase());
|
|
133
146
|
if (!inReg)
|
|
134
147
|
line(WARN, `current workspace (${ws.root}) is NOT registered — a child here could mis-route`);
|
|
148
|
+
// Multi-daemon + global pin: catches the case the section-1 check misses
|
|
149
|
+
// (doctor run from the same dir VEXP_WORKSPACE points at). With several
|
|
150
|
+
// daemons registered, a global VEXP_WORKSPACE still routes EVERY agent
|
|
151
|
+
// session to just one of them.
|
|
152
|
+
if (ws.source === "VEXP_WORKSPACE" && entries.length >= 2) {
|
|
153
|
+
line(WARN, `${entries.length} daemons registered but a global VEXP_WORKSPACE pins EVERY agent session to ${ws.root}; agents in the other repos get [] / wrong-repo results. Unset the global VEXP_WORKSPACE env var.`);
|
|
154
|
+
}
|
|
135
155
|
}
|
|
136
156
|
// 3) License (fresh.jwt rolling token vs license.jwt).
|
|
137
157
|
console.log(chalk.bold("\nLicense tokens (~/.vexp)"));
|
package/dist/hook-template.js
CHANGED
|
@@ -2,19 +2,52 @@
|
|
|
2
2
|
* Claude Code PreToolUse hook that conditionally blocks Grep/Glob
|
|
3
3
|
* when the vexp daemon is available, falling back to allow them
|
|
4
4
|
* when the daemon is not running.
|
|
5
|
+
*
|
|
6
|
+
* Cross-platform: Unix/macOS expose the daemon as a domain socket file
|
|
7
|
+
* (.vexp/daemon.sock); Windows uses a named pipe with NO socket file, so the
|
|
8
|
+
* daemon writes a .vexp/daemon.pipe marker instead. The previous template only
|
|
9
|
+
* checked `[ -S "$SOCK" ]`, which is ALWAYS false on Windows → the hook failed
|
|
10
|
+
* open and never blocked Grep/Glob there. We now branch on `uname` so each
|
|
11
|
+
* platform uses the right "daemon is listening" signal. The healthy marker
|
|
12
|
+
* (removed by the daemon on graceful shutdown) gates both branches.
|
|
5
13
|
*/
|
|
6
14
|
export const VEXP_GUARD_HOOK = `#!/bin/bash
|
|
7
|
-
# vexp-guard: block Grep/Glob when vexp daemon is running AND
|
|
8
|
-
#
|
|
9
|
-
#
|
|
15
|
+
# vexp-guard: block Grep/Glob/Read when the vexp daemon is running AND healthy.
|
|
16
|
+
# Cross-platform: Unix/macOS use the domain socket file (.vexp/daemon.sock);
|
|
17
|
+
# Windows (Git Bash) has no socket file, so the daemon writes .vexp/daemon.pipe.
|
|
10
18
|
VEXP_DIR="\${CLAUDE_PROJECT_DIR:-.}/.vexp"
|
|
11
19
|
SOCK="$VEXP_DIR/daemon.sock"
|
|
20
|
+
PIPE="$VEXP_DIR/daemon.pipe"
|
|
12
21
|
HEALTHY="$VEXP_DIR/healthy"
|
|
13
22
|
PID_FILE="$VEXP_DIR/daemon.pid"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
|
|
24
|
+
vexp_deny() {
|
|
25
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"vexp daemon is running. Use run_pipeline instead of Grep/Glob/Read."}}'
|
|
26
|
+
exit 0
|
|
27
|
+
}
|
|
28
|
+
vexp_allow() {
|
|
17
29
|
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"vexp index not ready, allowing direct search fallback."}}'
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
exit 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Healthy marker is the portable "daemon is up" signal; the daemon removes it on
|
|
34
|
+
# graceful shutdown. Required on every platform — if it's missing, fail open.
|
|
35
|
+
[ -f "$HEALTHY" ] || vexp_allow
|
|
36
|
+
|
|
37
|
+
case "$(uname -s 2>/dev/null)" in
|
|
38
|
+
MINGW*|MSYS*|CYGWIN*|Windows*)
|
|
39
|
+
# Windows: no Unix socket to stat, and \`kill -0\` on a native Windows PID is
|
|
40
|
+
# unreliable under Git Bash. The pipe marker + healthy marker are the signal.
|
|
41
|
+
[ -f "$PIPE" ] && vexp_deny
|
|
42
|
+
vexp_allow
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
# Unix/macOS: require the live socket AND a live PID (the PID check catches
|
|
46
|
+
# stale files left behind after \`kill -9\`).
|
|
47
|
+
[ -S "$SOCK" ] || vexp_allow
|
|
48
|
+
[ -f "$PID_FILE" ] || vexp_allow
|
|
49
|
+
kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null && vexp_deny
|
|
50
|
+
vexp_allow
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
20
53
|
`;
|
package/dist/license.js
CHANGED
|
@@ -183,6 +183,22 @@ export async function tryOnlineRefresh(longJwt) {
|
|
|
183
183
|
// Only save if the freshToken itself verifies locally
|
|
184
184
|
if (verifyAndDecode(data.freshToken)) {
|
|
185
185
|
saveFreshToken(data.freshToken);
|
|
186
|
+
// The server re-issued a 30-day long token (entitlement changed:
|
|
187
|
+
// AppSumo tier up/downgrade, or a Stripe resub migration). Overwrite
|
|
188
|
+
// the on-disk long JWT so the new entitlement survives even fully
|
|
189
|
+
// offline and the previous one stops working — without the user
|
|
190
|
+
// re-pasting a key. Only persist if it verifies locally.
|
|
191
|
+
if (data.newLongToken && verifyAndDecode(data.newLongToken)) {
|
|
192
|
+
try {
|
|
193
|
+
const licensePath = getLicensePath();
|
|
194
|
+
fs.mkdirSync(path.dirname(licensePath), { recursive: true });
|
|
195
|
+
fs.writeFileSync(licensePath, data.newLongToken, "utf-8");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// read-only FS: the freshToken still carries the new entitlement
|
|
199
|
+
// for this session; we just can't persist the long JWT.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
186
202
|
writeLastCheck(Date.now());
|
|
187
203
|
// Any previous device-blocked state is stale once the server issues
|
|
188
204
|
// a freshToken for us — we are back in a good state.
|
|
@@ -257,6 +273,12 @@ export function activateLicense(jwt) {
|
|
|
257
273
|
const licensePath = getLicensePath();
|
|
258
274
|
fs.mkdirSync(path.dirname(licensePath), { recursive: true });
|
|
259
275
|
fs.writeFileSync(licensePath, jwt, "utf-8");
|
|
276
|
+
// The freshly activated long JWT is authoritative. Drop any cached
|
|
277
|
+
// freshToken: it may be a 7-day token minted at the PREVIOUS tier, and both
|
|
278
|
+
// readLicenseLimits() and the Rust daemon prefer fresh.jwt over license.jwt —
|
|
279
|
+
// leaving it would shadow the just-activated tier until it expired. The next
|
|
280
|
+
// online refresh re-creates fresh.jwt at the new tier.
|
|
281
|
+
removeFreshToken();
|
|
260
282
|
return claims;
|
|
261
283
|
}
|
|
262
284
|
/** Remove the current license file (and any cached freshToken) */
|