vexp-cli 2.2.0 → 2.2.2
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 +308 -20
- package/dist/cli.js +28 -10
- package/dist/doctor.js +5 -14
- package/dist/hook-template.js +141 -0
- package/dist/socket-path.js +40 -0
- package/mcp/mcp-server.cjs +48 -43
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from "fs";
|
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as crypto from "crypto";
|
|
13
|
-
import { VEXP_GUARD_HOOK } from "./hook-template.js";
|
|
13
|
+
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD } from "./hook-template.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Constants
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -47,7 +47,11 @@ function workspaceHash(workspaceRoot) {
|
|
|
47
47
|
hash ^= BigInt(byte);
|
|
48
48
|
hash = (hash * prime) & mask;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// NOT zero-padded — see doctor.ts:socketForWorkspace. Rust renders the u64
|
|
51
|
+
// with `format!("{:x}")` and slices [..8]; padding to 16 first shifts every
|
|
52
|
+
// digit right whenever the hash has a leading zero nibble (~6% of paths),
|
|
53
|
+
// which silently breaks the lockstep this comment demands.
|
|
54
|
+
return hash.toString(16).slice(0, 8);
|
|
51
55
|
}
|
|
52
56
|
/**
|
|
53
57
|
* Remove every legacy vexp entry from a JSON-shaped MCP config object.
|
|
@@ -150,8 +154,13 @@ const AGENT_DETECTORS = [
|
|
|
150
154
|
{
|
|
151
155
|
agent: "Opencode",
|
|
152
156
|
detectPath: "opencode.json",
|
|
157
|
+
// opencode also reads `opencode.jsonc` (and a global ~/.config/opencode/),
|
|
158
|
+
// so `opencode.json` alone missed the many users who keep a commented config.
|
|
159
|
+
detectPaths: ["opencode.jsonc"],
|
|
153
160
|
configFile: "AGENTS.md",
|
|
154
161
|
templateName: "agents-md",
|
|
162
|
+
// Enforcement (block grep/glob when the daemon is up) is installed as an
|
|
163
|
+
// opencode plugin at .opencode/plugins/vexp-guard.js — see installOpencodePlugin().
|
|
155
164
|
},
|
|
156
165
|
{
|
|
157
166
|
agent: "Kilo Code",
|
|
@@ -337,6 +346,20 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
337
346
|
if (wrote)
|
|
338
347
|
mcpConfigs.push(wrote);
|
|
339
348
|
}
|
|
349
|
+
// Opencode: install the guard plugin that blocks grep/glob while the daemon
|
|
350
|
+
// is up (opencode's analogue of the Claude Code PreToolUse hook).
|
|
351
|
+
if (detector.agent === "Opencode") {
|
|
352
|
+
const pluginResult = installOpencodePlugin(workspaceRoot);
|
|
353
|
+
if (pluginResult) {
|
|
354
|
+
results.push({
|
|
355
|
+
agent: "Opencode Guard",
|
|
356
|
+
configFile: ".opencode/plugins/vexp-guard.js",
|
|
357
|
+
content: VEXP_OPENCODE_GUARD,
|
|
358
|
+
alreadyExists: pluginResult === "updated",
|
|
359
|
+
action: pluginResult,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
340
363
|
results.push({
|
|
341
364
|
agent: detector.agent,
|
|
342
365
|
configFile: detector.configFile,
|
|
@@ -353,6 +376,60 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
353
376
|
export function getAgentList() {
|
|
354
377
|
return [...AGENT_DETECTORS];
|
|
355
378
|
}
|
|
379
|
+
/** Fold a name to its comparable form: case, spaces, dots and dashes carry no
|
|
380
|
+
* meaning here ("claude-code", "Claude Code" and "claudecode" are one agent). */
|
|
381
|
+
function foldAgentName(s) {
|
|
382
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Canonicalize a name given to `--agents`, or null if it names no agent.
|
|
386
|
+
*
|
|
387
|
+
* `configureSelectedAgents` matches with `Array.includes` on the exact string,
|
|
388
|
+
* so "opencode" silently configured nothing while "Opencode" worked — nobody
|
|
389
|
+
* can be expected to know the capitalization, and the failure was invisible.
|
|
390
|
+
*/
|
|
391
|
+
export function resolveAgentName(input) {
|
|
392
|
+
const want = foldAgentName(input);
|
|
393
|
+
if (!want)
|
|
394
|
+
return null;
|
|
395
|
+
return AGENT_DETECTORS.find((d) => foldAgentName(d.agent) === want)?.agent ?? null;
|
|
396
|
+
}
|
|
397
|
+
/** Levenshtein distance, for "did you mean" on an unknown --agents name. */
|
|
398
|
+
function editDistance(a, b) {
|
|
399
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
400
|
+
const cur = new Array(b.length + 1).fill(0);
|
|
401
|
+
for (let i = 1; i <= a.length; i++) {
|
|
402
|
+
cur[0] = i;
|
|
403
|
+
for (let j = 1; j <= b.length; j++) {
|
|
404
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
405
|
+
}
|
|
406
|
+
prev.splice(0, prev.length, ...cur);
|
|
407
|
+
}
|
|
408
|
+
return prev[b.length];
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The known agent an unknown name most likely meant, or null when nothing is
|
|
412
|
+
* close enough to be worth suggesting. Substring hits win (a bare "copilot"
|
|
413
|
+
* means "GitHub Copilot"); otherwise allow a couple of typos.
|
|
414
|
+
*/
|
|
415
|
+
export function suggestAgentName(input) {
|
|
416
|
+
const want = foldAgentName(input);
|
|
417
|
+
if (!want)
|
|
418
|
+
return null;
|
|
419
|
+
const contained = AGENT_DETECTORS.find((d) => {
|
|
420
|
+
const f = foldAgentName(d.agent);
|
|
421
|
+
return f.includes(want) || want.includes(f);
|
|
422
|
+
});
|
|
423
|
+
if (contained)
|
|
424
|
+
return contained.agent;
|
|
425
|
+
let best = null;
|
|
426
|
+
for (const d of AGENT_DETECTORS) {
|
|
427
|
+
const dist = editDistance(want, foldAgentName(d.agent));
|
|
428
|
+
if (!best || dist < best.d)
|
|
429
|
+
best = { name: d.agent, d: dist };
|
|
430
|
+
}
|
|
431
|
+
return best && best.d <= 3 ? best.name : null;
|
|
432
|
+
}
|
|
356
433
|
/**
|
|
357
434
|
* Configure specific agents selected by the user.
|
|
358
435
|
* Unlike configureAgents which auto-detects, this forces configuration
|
|
@@ -445,6 +522,9 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
445
522
|
if (writeZedMcpConfig(zedPath, binaryPath, mcpServerPath, workspaceRoot))
|
|
446
523
|
mcpConfigs.push(".zed/settings.json");
|
|
447
524
|
}
|
|
525
|
+
if (detector.agent === "Opencode") {
|
|
526
|
+
installOpencodePlugin(workspaceRoot);
|
|
527
|
+
}
|
|
448
528
|
results.push({ agent: detector.agent, configFile: detector.configFile, content, alreadyExists, action });
|
|
449
529
|
}
|
|
450
530
|
return { agents: results, mcpConfigs };
|
|
@@ -537,6 +617,65 @@ export function readJsonConfigSafe(filePath) {
|
|
|
537
617
|
return { data: {}, ok: false, existed: true };
|
|
538
618
|
}
|
|
539
619
|
/** Copy an existing config to <file>.vexp-bak before overwriting it. */
|
|
620
|
+
/**
|
|
621
|
+
* True when the vexp entry already in a config still resolves to something
|
|
622
|
+
* runnable — even if it points at a DIFFERENT vexp install than ours.
|
|
623
|
+
*
|
|
624
|
+
* Mirrors `vexpEntryStillResolves` in
|
|
625
|
+
* packages/vexp-vscode/src/providers/agent-auto-config.ts; duplicated because
|
|
626
|
+
* the CLI cannot depend on the extension at runtime.
|
|
627
|
+
*
|
|
628
|
+
* `mcp-server.cjs` is a standalone script that reaches the daemon over its
|
|
629
|
+
* socket; it does not care which install it was launched from. So an entry
|
|
630
|
+
* naming another install is not stale, it is just someone else's path — and
|
|
631
|
+
* rewriting it starts a fight the user sees as "my MCP config gets
|
|
632
|
+
* overwritten every time I open a different IDE". Every VS Code fork carries
|
|
633
|
+
* the extension and writes these same files with its own absolute extension
|
|
634
|
+
* path, while the CLI writes `command: "node"` plus its npm-global path — a
|
|
635
|
+
* third shape — so `vexp setup` and any IDE activation also undid each other.
|
|
636
|
+
* The old value-equality check could never settle it: a different path is
|
|
637
|
+
* never identical, so whoever ran last won.
|
|
638
|
+
*
|
|
639
|
+
* Staleness is handled by the check rather than by rewriting: editors reap the
|
|
640
|
+
* previous extension directory on upgrade (and the path embeds the version),
|
|
641
|
+
* so an entry left by an uninstalled build fails here and is rewritten.
|
|
642
|
+
*
|
|
643
|
+
* Accepts every shape we emit:
|
|
644
|
+
* {command: "node", args: ["…/mcp-server.cjs"]} most JSON configs
|
|
645
|
+
* {command: "…/vexp-core", args: ["mcp"]} binary-subcommand
|
|
646
|
+
* {command: {path, args}} Zed
|
|
647
|
+
* {command: ["node", "…/mcp-server.cjs"]} Kilo Code
|
|
648
|
+
*/
|
|
649
|
+
function vexpEntryStillResolves(entry) {
|
|
650
|
+
if (!entry || typeof entry !== "object")
|
|
651
|
+
return false;
|
|
652
|
+
const e = entry;
|
|
653
|
+
let command;
|
|
654
|
+
let rawArgs;
|
|
655
|
+
if (Array.isArray(e.command)) {
|
|
656
|
+
command = e.command[0];
|
|
657
|
+
rawArgs = e.command.slice(1);
|
|
658
|
+
}
|
|
659
|
+
else if (e.command !== null && typeof e.command === "object") {
|
|
660
|
+
const nested = e.command;
|
|
661
|
+
command = nested.path;
|
|
662
|
+
rawArgs = nested.args;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
command = e.command;
|
|
666
|
+
rawArgs = e.args;
|
|
667
|
+
}
|
|
668
|
+
if (typeof command !== "string" || command.length === 0)
|
|
669
|
+
return false;
|
|
670
|
+
// An interpreter pinned by absolute path must still exist. A bare "node" is
|
|
671
|
+
// resolved via PATH at spawn time, so there is nothing to verify here.
|
|
672
|
+
if (path.isAbsolute(command) && !fs.existsSync(command))
|
|
673
|
+
return false;
|
|
674
|
+
// The script it runs, when the entry names one. `args: ["mcp"]` names a
|
|
675
|
+
// subcommand of the binary in `command`, already checked above.
|
|
676
|
+
const script = (Array.isArray(rawArgs) ? rawArgs : []).find((a) => typeof a === "string" && path.isAbsolute(a) && /\.[cm]?js$/.test(a));
|
|
677
|
+
return script === undefined || fs.existsSync(script);
|
|
678
|
+
}
|
|
540
679
|
function backupConfig(filePath) {
|
|
541
680
|
try {
|
|
542
681
|
if (fs.existsSync(filePath))
|
|
@@ -685,10 +824,11 @@ export function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServer
|
|
|
685
824
|
const currentCmd = previousVexp?.["command"];
|
|
686
825
|
const prevArgs = Array.isArray(previousVexp?.["args"]) ? previousVexp?.["args"] : undefined;
|
|
687
826
|
const prevEnv = previousVexp?.env;
|
|
827
|
+
const envMatches = JSON.stringify(prevEnv) === JSON.stringify(targetEnv);
|
|
688
828
|
const identical = currentCmd === targetCmd &&
|
|
689
829
|
prevArgs !== undefined &&
|
|
690
830
|
JSON.stringify(prevArgs) === JSON.stringify(targetArgs) &&
|
|
691
|
-
|
|
831
|
+
envMatches;
|
|
692
832
|
// Legacy cleanup: remove any vexp/vexp-* keys; if the canonical 'vexp'
|
|
693
833
|
// entry points at a command path that no longer exists on disk, drop it
|
|
694
834
|
// too so we don't keep a stale pointer.
|
|
@@ -697,6 +837,11 @@ export function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServer
|
|
|
697
837
|
if (identical && removed.length === 0) {
|
|
698
838
|
return false; // Already up to date and no legacy to clean
|
|
699
839
|
}
|
|
840
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
841
|
+
// Only the install path may differ: `envMatches` still gates on the
|
|
842
|
+
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
843
|
+
if (removed.length === 0 && envMatches && vexpEntryStillResolves(previousVexp))
|
|
844
|
+
return false;
|
|
700
845
|
const servers = existing.mcpServers ?? {};
|
|
701
846
|
servers["vexp"] = {
|
|
702
847
|
command: targetCmd,
|
|
@@ -765,6 +910,34 @@ function tomlString(value) {
|
|
|
765
910
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
766
911
|
}
|
|
767
912
|
/** Extract the existing canonical [mcp_servers.vexp] block (incl. subsections). */
|
|
913
|
+
/** Inverse of `tomlString` for the two forms it emits: a literal `'…'` (no
|
|
914
|
+
* escapes) or a basic `"…"` with `\\` / `\"` escaped. */
|
|
915
|
+
function parseTomlString(raw) {
|
|
916
|
+
const literal = /^'([^']*)'$/.exec(raw);
|
|
917
|
+
if (literal)
|
|
918
|
+
return literal[1];
|
|
919
|
+
const basic = /^"((?:[^"\\]|\\.)*)"$/.exec(raw);
|
|
920
|
+
if (basic)
|
|
921
|
+
return basic[1].replace(/\\(["\\])/g, "$1");
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* The binary an existing managed *direct* section points at, when it is still
|
|
926
|
+
* on disk. Undefined for an http section, a malformed one, or one naming a
|
|
927
|
+
* binary that is gone — each of which must be rebuilt from our own path.
|
|
928
|
+
*
|
|
929
|
+
* Mirrors `codexDirectBinary` in
|
|
930
|
+
* packages/vexp-vscode/src/providers/agent-auto-config.ts.
|
|
931
|
+
*/
|
|
932
|
+
function codexDirectBinary(section) {
|
|
933
|
+
const m = /^\s*command\s*=\s*(.+?)\s*$/m.exec(section);
|
|
934
|
+
if (!m)
|
|
935
|
+
return undefined;
|
|
936
|
+
const cmd = parseTomlString(m[1]);
|
|
937
|
+
if (!cmd || !path.isAbsolute(cmd) || !fs.existsSync(cmd))
|
|
938
|
+
return undefined;
|
|
939
|
+
return cmd;
|
|
940
|
+
}
|
|
768
941
|
function extractCodexVexpSection(content) {
|
|
769
942
|
const m = content.match(CANONICAL_VEXP_TOML_SECTION_RE);
|
|
770
943
|
return m ? m.join("\n") : "";
|
|
@@ -839,7 +1012,7 @@ export function configureCodexGlobal(binaryPath, _mcpServerPath, workspaceRoot)
|
|
|
839
1012
|
// stanza Codex can cache → "url is not supported for stdio". Pass the binary only
|
|
840
1013
|
// when it exists; buildCodexSection(direct, undefined) returns null → we then
|
|
841
1014
|
// PRESERVE the existing managed section instead of clobbering it with a url.
|
|
842
|
-
const
|
|
1015
|
+
const ourCoreBinary = binaryPath && fs.existsSync(binaryPath) ? binaryPath : undefined;
|
|
843
1016
|
let content = "";
|
|
844
1017
|
if (fs.existsSync(configPath))
|
|
845
1018
|
content = fs.readFileSync(configPath, "utf-8");
|
|
@@ -861,6 +1034,15 @@ export function configureCodexGlobal(binaryPath, _mcpServerPath, workspaceRoot)
|
|
|
861
1034
|
}
|
|
862
1035
|
return false;
|
|
863
1036
|
}
|
|
1037
|
+
// Step 2b: adopt the binary a previous install already pinned here, when it
|
|
1038
|
+
// still exists. Our own sections are not "user-managed", so `vexp setup` and
|
|
1039
|
+
// every IDE activation rewrote this file with their own binary path — and the
|
|
1040
|
+
// file is machine-global while Codex is detected by AGENTS.md, so it churned
|
|
1041
|
+
// for every project at once. Adopting rather than skipping is what keeps this
|
|
1042
|
+
// safe: the binary is interchangeable, but the workspace pin below is not and
|
|
1043
|
+
// must still follow the project being configured. See the twin comment in
|
|
1044
|
+
// vexp-vscode/src/providers/agent-auto-config.ts.
|
|
1045
|
+
const coreBinaryPath = (transport === "direct" ? codexDirectBinary(existingSection) : undefined) ?? ourCoreBinary;
|
|
864
1046
|
const newSection = buildCodexSection({ transport, coreBinaryPath, workspaceRoot, mcpPort, token, home });
|
|
865
1047
|
if (!newSection) {
|
|
866
1048
|
// direct requested but binary missing → preserve the existing section (only
|
|
@@ -898,14 +1080,22 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
898
1080
|
const beforeServers = existing.servers;
|
|
899
1081
|
const previousVexp = beforeServers?.["vexp"];
|
|
900
1082
|
const prevArgs = Array.isArray(previousVexp?.["args"]) ? previousVexp?.["args"] : undefined;
|
|
1083
|
+
const envMatches = JSON.stringify(previousVexp?.env) === JSON.stringify(targetEnv);
|
|
901
1084
|
const identical = previousVexp?.["command"] === targetCmd &&
|
|
902
1085
|
prevArgs !== undefined &&
|
|
903
1086
|
JSON.stringify(prevArgs) === JSON.stringify(targetArgs) &&
|
|
904
|
-
|
|
1087
|
+
envMatches;
|
|
905
1088
|
const removed = stripLegacyVexpEntries(existing, "servers");
|
|
906
1089
|
logRemoval(removed);
|
|
907
1090
|
if (identical && removed.length === 0)
|
|
908
1091
|
return false;
|
|
1092
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
1093
|
+
// This file is the worst offender: every VS Code fork reaches it through the
|
|
1094
|
+
// Copilot branch, which is gated only on `.github/` existing. Only the
|
|
1095
|
+
// install path may differ — `envMatches` still gates on the VEXP_WORKSPACE
|
|
1096
|
+
// pin, so a moved or renamed project is repinned normally.
|
|
1097
|
+
if (removed.length === 0 && envMatches && vexpEntryStillResolves(previousVexp))
|
|
1098
|
+
return false;
|
|
909
1099
|
const servers = existing.servers ?? {};
|
|
910
1100
|
servers["vexp"] = {
|
|
911
1101
|
type: "stdio",
|
|
@@ -953,13 +1143,20 @@ export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
|
953
1143
|
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
954
1144
|
const mcp = cfg.mcp ?? {};
|
|
955
1145
|
const prev = mcp["vexp"];
|
|
1146
|
+
const envMatches = JSON.stringify(prev?.["env"]) === JSON.stringify(env);
|
|
956
1147
|
const identical = prev?.["type"] === "local" &&
|
|
957
1148
|
prev?.["enabled"] === true &&
|
|
958
1149
|
Array.isArray(prev?.["command"]) &&
|
|
959
1150
|
JSON.stringify(prev["command"]) === JSON.stringify(command) &&
|
|
960
|
-
|
|
1151
|
+
envMatches;
|
|
961
1152
|
if (identical)
|
|
962
1153
|
return null;
|
|
1154
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
1155
|
+
// Kilo's shape is `command: ["node", "<script>"]`. Only the install path may
|
|
1156
|
+
// differ: `envMatches` still gates on the VEXP_WORKSPACE pin, and `enabled`
|
|
1157
|
+
// must be true or the user turned it off deliberately.
|
|
1158
|
+
if (envMatches && prev?.["enabled"] === true && vexpEntryStillResolves(prev))
|
|
1159
|
+
return null;
|
|
963
1160
|
mcp["vexp"] = { type: "local", command, env, enabled: true };
|
|
964
1161
|
cfg.mcp = mcp;
|
|
965
1162
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -982,14 +1179,21 @@ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
982
1179
|
const beforeCs = settings.context_servers;
|
|
983
1180
|
const previousVexp = beforeCs?.["vexp"];
|
|
984
1181
|
const curCmd = previousVexp?.["command"];
|
|
1182
|
+
// Zed nests env inside `command`, unlike every other writer.
|
|
1183
|
+
const envMatches = JSON.stringify(curCmd?.["env"]) === JSON.stringify(targetEnv);
|
|
985
1184
|
const identical = curCmd?.["path"] === targetCmd &&
|
|
986
1185
|
Array.isArray(curCmd?.["args"]) &&
|
|
987
1186
|
JSON.stringify(curCmd["args"]) === JSON.stringify(targetArgs) &&
|
|
988
|
-
|
|
1187
|
+
envMatches;
|
|
989
1188
|
const removed = stripLegacyVexpEntries(settings, "context_servers");
|
|
990
1189
|
logRemoval(removed);
|
|
991
1190
|
if (identical && removed.length === 0)
|
|
992
1191
|
return false;
|
|
1192
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
1193
|
+
// Only the install path may differ: `envMatches` still gates on the
|
|
1194
|
+
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
1195
|
+
if (removed.length === 0 && envMatches && vexpEntryStillResolves(previousVexp))
|
|
1196
|
+
return false;
|
|
993
1197
|
const cs = settings.context_servers ?? {};
|
|
994
1198
|
cs["vexp"] = {
|
|
995
1199
|
command: {
|
|
@@ -1050,6 +1254,13 @@ export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRo
|
|
|
1050
1254
|
logRemoval(removed);
|
|
1051
1255
|
if (identical && removed.length === 0)
|
|
1052
1256
|
return false;
|
|
1257
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
1258
|
+
// `prevEnv === undefined` is load-bearing, not a nicety: an entry still
|
|
1259
|
+
// pinning VEXP_WORKSPACE must be rewritten no matter whose it is, because
|
|
1260
|
+
// this file applies to EVERY project and the pin forces all parallel
|
|
1261
|
+
// sessions onto one daemon (the migration described above).
|
|
1262
|
+
if (removed.length === 0 && prevEnv === undefined && vexpEntryStillResolves(existing))
|
|
1263
|
+
return false;
|
|
1053
1264
|
const servers = config.mcpServers ?? {};
|
|
1054
1265
|
servers["vexp"] = {
|
|
1055
1266
|
command: desiredCommand,
|
|
@@ -1180,6 +1391,59 @@ export function installClaudeCodeHook(workspaceRoot) {
|
|
|
1180
1391
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1181
1392
|
return existed ? "updated" : "created";
|
|
1182
1393
|
}
|
|
1394
|
+
// ---------------------------------------------------------------------------
|
|
1395
|
+
// opencode plugin - blocks grep/glob (and shelled-out search) when the daemon
|
|
1396
|
+
// is healthy. opencode has no PreToolUse hook, but auto-loads plugins from
|
|
1397
|
+
// .opencode/plugins/ and lets a `tool.execute.before` hook abort a tool by
|
|
1398
|
+
// throwing — the same enforcement the Claude Code guard hook provides.
|
|
1399
|
+
// ---------------------------------------------------------------------------
|
|
1400
|
+
/**
|
|
1401
|
+
* Install the vexp-guard plugin for opencode.
|
|
1402
|
+
* Writes .opencode/plugins/vexp-guard.js (opencode auto-loads the directory at
|
|
1403
|
+
* startup — no config-file merge required). Returns the action taken, or null
|
|
1404
|
+
* when the file is already byte-identical.
|
|
1405
|
+
*/
|
|
1406
|
+
export function installOpencodePlugin(workspaceRoot) {
|
|
1407
|
+
const pluginDir = path.join(workspaceRoot, ".opencode", "plugins");
|
|
1408
|
+
const pluginPath = path.join(pluginDir, "vexp-guard.js");
|
|
1409
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
1410
|
+
const existed = fs.existsSync(pluginPath);
|
|
1411
|
+
if (existed) {
|
|
1412
|
+
const current = fs.readFileSync(pluginPath, "utf-8");
|
|
1413
|
+
if (current === VEXP_OPENCODE_GUARD)
|
|
1414
|
+
return null; // identical - skip
|
|
1415
|
+
// Content differs: either an older vexp guard, or a copy the user tuned by
|
|
1416
|
+
// hand. We refresh either way — refusing to touch a hand-edited plugin would
|
|
1417
|
+
// freeze that user on a stale guard forever — but their version stays
|
|
1418
|
+
// recoverable at vexp-guard.js.vexp-bak instead of being silently discarded.
|
|
1419
|
+
backupConfig(pluginPath);
|
|
1420
|
+
}
|
|
1421
|
+
fs.writeFileSync(pluginPath, VEXP_OPENCODE_GUARD, "utf-8");
|
|
1422
|
+
return existed ? "updated" : "created";
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The single highest-leverage instruction we ship, and for a long time the one
|
|
1426
|
+
* most agents never saw.
|
|
1427
|
+
*
|
|
1428
|
+
* vexp retrieval is identifier/path-first: FTS supplies the candidate pool and
|
|
1429
|
+
* the semantic layer only RE-RANKS it, so a symbol that no query token reaches
|
|
1430
|
+
* never enters the running at all. A task written as prose ("why does the proxy
|
|
1431
|
+
* fail?") therefore ranks far worse than the same task anchored on the symbols
|
|
1432
|
+
* and paths it is actually about — the difference between a useful pivot set and
|
|
1433
|
+
* "the vexp result wasn't relevant".
|
|
1434
|
+
*
|
|
1435
|
+
* This shipped only in the Claude Code and Cursor templates, while the AGENTS.md
|
|
1436
|
+
* example (`"task": "fix auth bug"`) actively modelled the weak mode — so two
|
|
1437
|
+
* independent field reports ("the index seems not very useful") came from users
|
|
1438
|
+
* whose instructions never told them the one thing that makes retrieval land.
|
|
1439
|
+
* Shared by every prose template so the wording cannot drift again. ASCII only
|
|
1440
|
+
* (these land in files that get read on Windows).
|
|
1441
|
+
*/
|
|
1442
|
+
const QUERY_SHAPE = `### Query shape (do this)
|
|
1443
|
+
- Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
1444
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1445
|
+
- A pure natural-language question ("why does login fail?") falls back to text
|
|
1446
|
+
ranking and is much less reliable - name the symbols/files you want, not the question.`;
|
|
1183
1447
|
function generateAgentConfig(template, vars) {
|
|
1184
1448
|
switch (template) {
|
|
1185
1449
|
case "claude-code":
|
|
@@ -1221,6 +1485,12 @@ uses fewer tokens than manual searching. Prefer \`get_skeleton\` over Read to
|
|
|
1221
1485
|
inspect files (detail: minimal/standard/detailed, 70-90% token savings).
|
|
1222
1486
|
Only use Read when you need exact raw content to edit a specific line.
|
|
1223
1487
|
|
|
1488
|
+
The hook denies Grep/Glob while the daemon is up. That is policy, not a transient
|
|
1489
|
+
failure: call \`run_pipeline\` instead - do NOT work around it by shelling out or
|
|
1490
|
+
writing a script. vexp only covers indexed source inside the workspace: for runtime
|
|
1491
|
+
logs, build output (dist/, .vite/, node_modules/) or files outside the repo it has
|
|
1492
|
+
no answer - use Bash/Read there, those are never blocked.
|
|
1493
|
+
|
|
1224
1494
|
### Primary Tool
|
|
1225
1495
|
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
|
|
1226
1496
|
capsule + impact + memory server-side. Returns compressed results.
|
|
@@ -1236,6 +1506,8 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1236
1506
|
- \`index_status\` - indexing status and health check
|
|
1237
1507
|
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
1238
1508
|
|
|
1509
|
+
${QUERY_SHAPE}
|
|
1510
|
+
|
|
1239
1511
|
### Workflow
|
|
1240
1512
|
1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
|
|
1241
1513
|
2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
|
|
@@ -1256,9 +1528,6 @@ Only use Read when you need exact raw content to edit a specific line.
|
|
|
1256
1528
|
- **Session Memory**: auto-captures observations; memories auto-surfaced in results
|
|
1257
1529
|
- **LSP Bridge**: VS Code captures type-resolved call edges
|
|
1258
1530
|
- **Change Coupling**: co-changed files included as related context
|
|
1259
|
-
- **Query tips**: include real identifiers (ClassName, function_name) or file paths
|
|
1260
|
-
in the task for precise matches - pure natural-language phrasing falls back to
|
|
1261
|
-
text ranking and is less reliable
|
|
1262
1531
|
|
|
1263
1532
|
### Advanced Parameters
|
|
1264
1533
|
- \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
|
|
@@ -1289,19 +1558,19 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1289
1558
|
|
|
1290
1559
|
### Available MCP tools
|
|
1291
1560
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1292
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1561
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1293
1562
|
- \`get_skeleton\` - compact file structure
|
|
1294
1563
|
- \`index_status\` - indexing status
|
|
1295
1564
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1296
1565
|
|
|
1566
|
+
${QUERY_SHAPE}
|
|
1567
|
+
|
|
1297
1568
|
### Agentic search
|
|
1298
1569
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1299
1570
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1300
1571
|
rather than letting them search the codebase independently
|
|
1301
1572
|
|
|
1302
1573
|
### Tips
|
|
1303
|
-
- Include real identifiers (class/function names) or file paths in the task - literal
|
|
1304
|
-
matches rank best; pure natural-language phrasing falls back to text ranking
|
|
1305
1574
|
- Add \`include_tests: true\` when debugging
|
|
1306
1575
|
- Use \`preset: "refactor"\` for deep impact analysis
|
|
1307
1576
|
|
|
@@ -1329,11 +1598,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1329
1598
|
|
|
1330
1599
|
### Available MCP tools
|
|
1331
1600
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1332
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1601
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1333
1602
|
- \`get_skeleton\` - compact file structure
|
|
1334
1603
|
- \`index_status\` - indexing status
|
|
1335
1604
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1336
1605
|
|
|
1606
|
+
${QUERY_SHAPE}
|
|
1607
|
+
|
|
1337
1608
|
### Agentic search
|
|
1338
1609
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1339
1610
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1378,11 +1649,13 @@ When working on this codebase:
|
|
|
1378
1649
|
|
|
1379
1650
|
### Available MCP tools
|
|
1380
1651
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1381
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1652
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1382
1653
|
- \`get_skeleton\` - token-efficient file structure
|
|
1383
1654
|
- \`index_status\` - indexing status
|
|
1384
1655
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1385
1656
|
|
|
1657
|
+
${QUERY_SHAPE}
|
|
1658
|
+
|
|
1386
1659
|
### Agentic search
|
|
1387
1660
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1388
1661
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1408,11 +1681,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1408
1681
|
|
|
1409
1682
|
### Available MCP tools
|
|
1410
1683
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1411
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1684
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1412
1685
|
- \`get_skeleton\` - compact file structure
|
|
1413
1686
|
- \`index_status\` - indexing status
|
|
1414
1687
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1415
1688
|
|
|
1689
|
+
${QUERY_SHAPE}
|
|
1690
|
+
|
|
1416
1691
|
### Agentic search
|
|
1417
1692
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1418
1693
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1438,11 +1713,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1438
1713
|
|
|
1439
1714
|
### Available MCP tools
|
|
1440
1715
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1441
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1716
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1442
1717
|
- \`get_skeleton\` - compact file structure
|
|
1443
1718
|
- \`index_status\` - indexing status
|
|
1444
1719
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1445
1720
|
|
|
1721
|
+
${QUERY_SHAPE}
|
|
1722
|
+
|
|
1446
1723
|
### Agentic search
|
|
1447
1724
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1448
1725
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1468,13 +1745,20 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1468
1745
|
|
|
1469
1746
|
### Available MCP tools
|
|
1470
1747
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1471
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix
|
|
1748
|
+
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1472
1749
|
- \`get_skeleton\` - compact file structure
|
|
1473
1750
|
- \`index_status\` - indexing status
|
|
1474
1751
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1475
1752
|
|
|
1753
|
+
${QUERY_SHAPE}
|
|
1754
|
+
|
|
1476
1755
|
### Agentic search
|
|
1477
1756
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1757
|
+
- If a search tool is denied, that is policy, not a transient failure: call \`run_pipeline\`
|
|
1758
|
+
instead. Do NOT work around it with shell search or by writing a script.
|
|
1759
|
+
- vexp only covers indexed source inside the workspace. For runtime logs, build output
|
|
1760
|
+
(dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
|
|
1761
|
+
normal tools there; those searches are never blocked.
|
|
1478
1762
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
1479
1763
|
rather than letting them search the codebase independently
|
|
1480
1764
|
|
|
@@ -1498,11 +1782,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1498
1782
|
|
|
1499
1783
|
## Available vexp tools
|
|
1500
1784
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1501
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1785
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1502
1786
|
- \`get_skeleton\` - compact file structure
|
|
1503
1787
|
- \`index_status\` - indexing status
|
|
1504
1788
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1505
1789
|
|
|
1790
|
+
${QUERY_SHAPE}
|
|
1791
|
+
|
|
1506
1792
|
## Agentic search
|
|
1507
1793
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1508
1794
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
@@ -1528,11 +1814,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
|
|
|
1528
1814
|
|
|
1529
1815
|
### Available MCP tools
|
|
1530
1816
|
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
1531
|
-
Example: \`run_pipeline({ "task": "fix
|
|
1817
|
+
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
1532
1818
|
- \`get_skeleton\` - compact file structure
|
|
1533
1819
|
- \`index_status\` - indexing status
|
|
1534
1820
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
1535
1821
|
|
|
1822
|
+
${QUERY_SHAPE}
|
|
1823
|
+
|
|
1536
1824
|
### Agentic search
|
|
1537
1825
|
- Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
|
|
1538
1826
|
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
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 } from "./agent-config.js";
|
|
11
|
+
import { detectAgents, getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName } 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";
|
|
@@ -16,6 +16,7 @@ import { ensureMcpHttpServer, mcpHttpStatus } from "./mcp-supervisor.js";
|
|
|
16
16
|
import { installAutostart, uninstallAutostart, autostartStatus, migrateClaudeUnpinIfNeeded } from "./autostart.js";
|
|
17
17
|
import { runServe } from "./serve.js";
|
|
18
18
|
import { runDoctor } from "./doctor.js";
|
|
19
|
+
import { socketPathFor } from "./socket-path.js";
|
|
19
20
|
import { isTraceEnabled } from "./trace.js";
|
|
20
21
|
const program = new Command();
|
|
21
22
|
program
|
|
@@ -158,7 +159,9 @@ function findConfiguredWorkspace(startDir = process.cwd()) {
|
|
|
158
159
|
/** True if the Unix socket at `socketPath` accepts a connection within `timeoutMs`. */
|
|
159
160
|
function isSocketAlive(socketPath, timeoutMs = 300) {
|
|
160
161
|
return new Promise((resolve) => {
|
|
161
|
-
|
|
162
|
+
// A Windows named pipe is not a filesystem entry, so the existsSync
|
|
163
|
+
// pre-check would reject every live daemon. Only unix sockets are files.
|
|
164
|
+
if (process.platform !== "win32" && !fs.existsSync(socketPath)) {
|
|
162
165
|
resolve(false);
|
|
163
166
|
return;
|
|
164
167
|
}
|
|
@@ -198,8 +201,7 @@ function isDaemonPidAlive(workspaceRoot) {
|
|
|
198
201
|
async function ensureDaemonRunning(workspaceRoot, binaryPath) {
|
|
199
202
|
if (process.env.VEXP_NO_AUTOSTART === "1")
|
|
200
203
|
return;
|
|
201
|
-
|
|
202
|
-
if (await isSocketAlive(socketPath))
|
|
204
|
+
if (await isSocketAlive(socketPathFor(workspaceRoot)))
|
|
203
205
|
return;
|
|
204
206
|
try {
|
|
205
207
|
await runBinaryAsync(binaryPath, ["daemon-cmd", "start"], {
|
|
@@ -252,8 +254,9 @@ async function startBackgroundServices(binaryPath, workspaceRoot) {
|
|
|
252
254
|
const spinnerDaemon = ora("Starting vexp daemon...").start();
|
|
253
255
|
try {
|
|
254
256
|
await runBinaryAsync(binaryPath, ["daemon-cmd", "start"], { silent: true, cwd: workspaceRoot });
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
// Connect rather than stat: on Windows the daemon binds a named pipe and
|
|
258
|
+
// there is no socket file to find, so this always reported failure.
|
|
259
|
+
if (await isSocketAlive(socketPathFor(workspaceRoot))) {
|
|
257
260
|
spinnerDaemon.succeed("vexp daemon started");
|
|
258
261
|
}
|
|
259
262
|
else {
|
|
@@ -552,8 +555,24 @@ program
|
|
|
552
555
|
const detectedNames = new Set(detected.map((a) => a.agent));
|
|
553
556
|
let selectedNames;
|
|
554
557
|
if (typeof opts.agents === "string") {
|
|
555
|
-
// Explicit list from --agents flag
|
|
556
|
-
|
|
558
|
+
// Explicit list from --agents flag. Resolve every name BEFORE writing
|
|
559
|
+
// anything: matching is exact downstream, so an unrecognised name used
|
|
560
|
+
// to configure nothing while setup still reported success — the worst
|
|
561
|
+
// possible outcome for the flag whose whole point is automation.
|
|
562
|
+
const requested = opts.agents.split(",").map((s) => s.trim()).filter(Boolean);
|
|
563
|
+
const unknown = requested.filter((n) => resolveAgentName(n) === null);
|
|
564
|
+
if (unknown.length > 0 || requested.length === 0) {
|
|
565
|
+
console.error(chalk.red(`\n✖ Unknown agent: ${unknown.map((u) => `"${u}"`).join(", ") || "(empty --agents list)"}`));
|
|
566
|
+
for (const u of unknown) {
|
|
567
|
+
const hint = suggestAgentName(u);
|
|
568
|
+
if (hint)
|
|
569
|
+
console.error(chalk.yellow(` Did you mean: ${hint}?`));
|
|
570
|
+
}
|
|
571
|
+
console.error(chalk.dim(` Valid agents: ${allAgents.map((a) => a.agent).join(", ")}`));
|
|
572
|
+
console.error(chalk.dim(" Names are matched ignoring case and punctuation.\n"));
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
selectedNames = requested.map((n) => resolveAgentName(n));
|
|
557
576
|
console.log(chalk.dim(` Agents (from flag): ${selectedNames.join(", ")}`));
|
|
558
577
|
}
|
|
559
578
|
else {
|
|
@@ -1043,8 +1062,7 @@ async function printBanner() {
|
|
|
1043
1062
|
console.log(chalk.dim(" → type '1 > 3' to run full setup, or `vexp setup`"));
|
|
1044
1063
|
}
|
|
1045
1064
|
else {
|
|
1046
|
-
const
|
|
1047
|
-
const socketAlive = await isSocketAlive(socketPath);
|
|
1065
|
+
const socketAlive = await isSocketAlive(socketPathFor(ws));
|
|
1048
1066
|
if (socketAlive) {
|
|
1049
1067
|
// Display PID + uptime when we can read them; otherwise a plain "running".
|
|
1050
1068
|
const { pid } = isDaemonPidAlive(ws);
|