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 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)"));
@@ -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 index is healthy.
8
- # Fast path: if socket file or healthy marker doesn't exist, allow immediately.
9
- # PID check: verify daemon process is alive (handles stale files after kill -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
- if [ -S "$SOCK" ] && [ -f "$HEALTHY" ] && [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
15
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"vexp daemon is running. Use run_pipeline instead of Grep/Glob."}}'
16
- else
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
- fi
19
- exit 0
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) */