vexp-cli 2.3.1 → 2.4.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.
@@ -374,3 +374,66 @@ process.stdin.on("end", () => {
374
374
  try { main(input); } catch (e) { allow(); } // any surprise -> fail open
375
375
  });
376
376
  `;
377
+ /**
378
+ * UserPromptSubmit hint hook (2.3.3 event-driven mandate). The resident
379
+ * CLAUDE.md strategy text is gone; instead, when a prompt names no known
380
+ * symbol or file of the indexed workspace, the daemon returns a one-line
381
+ * hint suggesting run_pipeline. All logic (stdin parsing, classification,
382
+ * hook-JSON output) lives in the Rust binary so the script is OS-neutral;
383
+ * Claude Code runs shell-form hooks through Git Bash on Windows.
384
+ * FAIL-OPEN CONTRACT: every failure path (binary missing, daemon down,
385
+ * timeout) exits 0 with no output = vanilla behavior, never a broken
386
+ * prompt. Keep in lockstep with the VS Code extension copy.
387
+ * The __VEXP_BIN__ placeholder is baked at install time.
388
+ */
389
+ export const VEXP_HINT_HOOK = `#!/bin/bash
390
+ # vexp-hint: event-driven orientation hint (UserPromptSubmit). Fails open.
391
+ VEXP_BIN="__VEXP_BIN__"
392
+ [ -x "$VEXP_BIN" ] || exit 0
393
+ "$VEXP_BIN" prompt-hint 2>/dev/null
394
+ exit 0
395
+ `;
396
+ /** Bake the binary path into the hint hook script. */
397
+ export function vexpHintHookScript(binaryPath) {
398
+ return VEXP_HINT_HOOK.replace("__VEXP_BIN__", binaryPath.replace(/\\/g, "/"));
399
+ }
400
+ /**
401
+ * opencode/Kilo per-prompt hint plugin (2.4.0). The plugin API's
402
+ * `chat.message` hook sees the user message before the LLM call and can
403
+ * append parts — the opencode-family equivalent of UserPromptSubmit +
404
+ * additionalContext. All classification lives in the Rust binary
405
+ * (prompt-hint, Claude-shaped JSON envelope; we extract additionalContext
406
+ * here). FAIL-OPEN: any error/timeout/missing binary => no parts appended.
407
+ * __VEXP_BIN__ is baked at install time. Keep in lockstep with the VS Code
408
+ * extension copy.
409
+ */
410
+ export const VEXP_OPENCODE_HINT = `// vexp-hint: per-prompt orientation (fail-open). Managed by vexp.
411
+ const VEXP_BIN = "__VEXP_BIN__";
412
+ export const VexpHint = async ({ directory }) => {
413
+ return {
414
+ "chat.message": async (_input, output) => {
415
+ try {
416
+ const { execFileSync } = await import("node:child_process");
417
+ const text = (output.parts || [])
418
+ .filter((p) => p && p.type === "text" && typeof p.text === "string")
419
+ .map((p) => p.text)
420
+ .join("\\n");
421
+ if (!text || text.length < 40) return;
422
+ const out = execFileSync(VEXP_BIN, ["prompt-hint"], {
423
+ input: JSON.stringify({ prompt: text }),
424
+ timeout: 4000,
425
+ env: { ...process.env, CLAUDE_PROJECT_DIR: directory },
426
+ encoding: "utf8",
427
+ });
428
+ if (!out || !out.trim()) return;
429
+ const hint = JSON.parse(out).hookSpecificOutput?.additionalContext;
430
+ if (hint) output.parts.push({ type: "text", text: hint });
431
+ } catch (e) { /* fail open */ }
432
+ },
433
+ };
434
+ };
435
+ `;
436
+ /** Bake the binary path into the opencode hint plugin. */
437
+ export function vexpOpencodeHintPlugin(binaryPath) {
438
+ return VEXP_OPENCODE_HINT.replace("__VEXP_BIN__", binaryPath.replace(/\\/g, "/"));
439
+ }
package/dist/serve.js CHANGED
@@ -140,7 +140,17 @@ async function resurrectAll() {
140
140
  // Prune rows whose workspace vanished, but keep routing rows alive in the
141
141
  // registry — they are data, not spawn instructions.
142
142
  const pruned = {};
143
+ // Windows: dedupe case-variant keys for the same workspace (C:\ vs c:\)
144
+ // left behind by pre-2.3.3 daemons — they map to the same pipe and every
145
+ // rewrite here would otherwise immortalize the phantom twin.
146
+ const seenKeys = new Set();
143
147
  for (const [ws, sock] of Object.entries(reg)) {
148
+ const canonical = process.platform === "win32" ? ws.toLowerCase() : ws;
149
+ if (seenKeys.has(canonical)) {
150
+ appendLog(`registry prune: ${ws} (case-variant duplicate)`);
151
+ continue;
152
+ }
153
+ seenKeys.add(canonical);
144
154
  const manifest = path.join(ws, ".vexp", "manifest.json");
145
155
  if (!fs.existsSync(manifest)) {
146
156
  appendLog(`registry prune: ${ws} (no manifest)`);