vexp-cli 3.2.0 → 3.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/serve.js +49 -5
- package/mcp/mcp-server.cjs +3 -3
- package/package.json +6 -6
package/dist/serve.js
CHANGED
|
@@ -60,6 +60,36 @@ function rotateLogIfLarge() {
|
|
|
60
60
|
}
|
|
61
61
|
catch { /* missing */ }
|
|
62
62
|
}
|
|
63
|
+
/** The daemon's log, opened the way `vexp-core daemon-cmd start` opens it:
|
|
64
|
+
* the previous boot rotated to daemon.log.1, this boot in daemon.log. The
|
|
65
|
+
* daemon writes its log to stderr and nowhere else, so a launcher that
|
|
66
|
+
* spawns it with stdio ignored gets a daemon that logs nothing at all — a
|
|
67
|
+
* 3.2.1 daemon answered requests for hours while daemon.log still ended at
|
|
68
|
+
* the crash of the day before, until a `daemon-cmd restart` brought the log
|
|
69
|
+
* back (field report, 2026-09-18). A rotation that fails (the file is still
|
|
70
|
+
* held open) appends instead: the evidence of the last boot is what a user
|
|
71
|
+
* restarting a daemon needs most. "ignore" when the file cannot be opened
|
|
72
|
+
* at all: a daemon without a log beats no daemon. */
|
|
73
|
+
export function openDaemonLog(workspaceRoot) {
|
|
74
|
+
const dir = path.join(workspaceRoot, ".vexp");
|
|
75
|
+
const log = path.join(dir, "daemon.log");
|
|
76
|
+
let flags = "w";
|
|
77
|
+
if (fs.existsSync(log)) {
|
|
78
|
+
try {
|
|
79
|
+
fs.rmSync(path.join(dir, "daemon.log.1"), { force: true });
|
|
80
|
+
fs.renameSync(log, path.join(dir, "daemon.log.1"));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
flags = "a";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return fs.openSync(log, flags);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "ignore";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
63
93
|
async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
64
94
|
const manifest = path.join(workspaceRoot, ".vexp", "manifest.json");
|
|
65
95
|
if (!fs.existsSync(manifest))
|
|
@@ -83,11 +113,25 @@ async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
|
83
113
|
// in vexp-core, which spawns every child with CREATE_NO_WINDOW
|
|
84
114
|
// (packages/vexp-core/src/proc.rs). `windowsHide` would not help here:
|
|
85
115
|
// Windows ignores it next to DETACHED_PROCESS.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
//
|
|
117
|
+
// stdout/stderr go to the daemon's own log file, never to a terminal:
|
|
118
|
+
// a file handle is not a console, and it is what `daemon-cmd start`
|
|
119
|
+
// hands the same detached daemon. See openDaemonLog.
|
|
120
|
+
const log = openDaemonLog(workspaceRoot);
|
|
121
|
+
let child;
|
|
122
|
+
try {
|
|
123
|
+
child = spawn(binary, ["daemon", "--workspace", workspaceRoot, "--socket", socketPath], {
|
|
124
|
+
detached: true,
|
|
125
|
+
stdio: ["ignore", log, log],
|
|
126
|
+
env: binaryEnv(binary),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
// The child holds its own handle from here; ours would only keep the
|
|
131
|
+
// file open (and unrotatable on Windows) for the supervisor's lifetime.
|
|
132
|
+
if (typeof log === "number")
|
|
133
|
+
fs.closeSync(log);
|
|
134
|
+
}
|
|
91
135
|
child.unref();
|
|
92
136
|
appendLog(`daemon spawned: ${workspaceRoot} (pid=${child.pid})`);
|
|
93
137
|
return true;
|
package/mcp/mcp-server.cjs
CHANGED
|
@@ -101,11 +101,11 @@ To lift the limit now, upgrade to Pro or Team: https://vexp.dev/#pricing`)}var H
|
|
|
101
101
|
`);if(i.length&&i.some(o=>o.trim()))return`
|
|
102
102
|
--- ${n} (last ${Math.min(e,i.length)} lines) ---
|
|
103
103
|
`+i.slice(-e).join(`
|
|
104
|
-
`)}catch{}return""}get socketPath(){return this.explicitSocketPath?this.explicitSocketPath:this.spawnWorkspaceRoot?GL(this.spawnWorkspaceRoot):D$()??HL()}static isRetryableError(e){let r=e.message;return t.RETRYABLE_CODES.some(n=>r.includes(n))}async call(e,r){let n;for(let a=0;a<=t.MAX_RETRIES;a++)try{return await this.callOnce(e,r)}catch(i){if(n=i instanceof Error?i:new Error(String(i)),t.isRetryableError(n)){let o=R$(Vo(this.spawnWorkspaceRoot));if(o)throw ML(o)}if(a<t.MAX_RETRIES&&t.isRetryableError(n)){a===0&&await this.maybeSpawnDaemon(),console.error(`[vexp-mcp] Daemon connection failed (${n.message}), retrying in ${t.RETRY_DELAY_MS}ms (attempt ${a+1}/${t.MAX_RETRIES})`),await new Promise(o=>setTimeout(o,t.RETRY_DELAY_MS));continue}throw this.augmentDaemonError(n)}throw this.augmentDaemonError(n)}augmentDaemonError(e){if(!t.isRetryableError(e))return e;let r=this.daemonLogTail();return r&&!e.message.includes("--- daemon.log")&&!e.message.includes("--- vexp.log")&&(e.message+=r),!this.explicitSocketPath&&!this.spawnWorkspaceRoot&&!e.message.includes("--- vexp targeting diagnostics ---")&&(e.message+=VL()),e}async callStreaming(e,r,n){return new Promise((a,i)=>{let s={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId,stream:!0},c=JSON.stringify(s)+`
|
|
104
|
+
`)}catch{}return""}get socketPath(){return this.explicitSocketPath?this.explicitSocketPath:this.spawnWorkspaceRoot?GL(this.spawnWorkspaceRoot):D$()??HL()}static isRetryableError(e){if(t.wasHandlingRequest(e))return!1;let r=e.message;return t.RETRYABLE_CODES.some(n=>r.includes(n))}static wasHandlingRequest(e){return e.vexpDaemonGone===!0}static daemonGoneError(e,r){let n=new Error(t.daemonGoneMessage(e,r));return n.vexpDaemonGone=!0,n}async call(e,r){let n;for(let a=0;a<=t.MAX_RETRIES;a++)try{return await this.callOnce(e,r)}catch(i){if(n=i instanceof Error?i:new Error(String(i)),t.isRetryableError(n)){let o=R$(Vo(this.spawnWorkspaceRoot));if(o)throw ML(o)}if(a<t.MAX_RETRIES&&t.isRetryableError(n)){a===0&&await this.maybeSpawnDaemon(),console.error(`[vexp-mcp] Daemon connection failed (${n.message}), retrying in ${t.RETRY_DELAY_MS}ms (attempt ${a+1}/${t.MAX_RETRIES})`),await new Promise(o=>setTimeout(o,t.RETRY_DELAY_MS));continue}throw this.augmentDaemonError(n)}throw this.augmentDaemonError(n)}static daemonGoneMessage(e,r){let n=r?` (${r})`:"";return`The vexp daemon closed the connection before answering '${e}': it stopped or was restarted while handling the request${n}. If it crashed, the reason is in the last lines of daemon.log (daemon.log.1 once a new daemon has started; vexp.log when VS Code runs it) in the .vexp folder of the workspace it serves. AGENT INSTRUCTION: retry this call once; if it fails the same way, continue with your own tools and tell the user.`}augmentDaemonError(e){if(!t.isRetryableError(e)&&!t.wasHandlingRequest(e))return e;let r=this.daemonLogTail();return r&&!e.message.includes("--- daemon.log")&&!e.message.includes("--- vexp.log")&&(e.message+=r),!t.wasHandlingRequest(e)&&!this.explicitSocketPath&&!this.spawnWorkspaceRoot&&!e.message.includes("--- vexp targeting diagnostics ---")&&(e.message+=VL()),e}async callStreaming(e,r,n){return new Promise((a,i)=>{let s={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId,stream:!0},c=JSON.stringify(s)+`
|
|
105
105
|
`,u,p="",l=!1,d=setTimeout(()=>{l||(l=!0,u?.destroy(),i(new Error(`Timeout: no streaming response from daemon for tool '${e}' after 60s`)))},6e4),f=m=>{l||(l=!0,clearTimeout(d),m())};process.platform==="win32"?u=Bo.createConnection(this.socketPath):u=Bo.createConnection({path:this.socketPath}),u.on("connect",()=>{u.write(c)}),u.on("data",m=>{p+=m.toString();let h;for(;(h=p.indexOf(`
|
|
106
106
|
`))!==-1;){let g=p.slice(0,h);if(p=p.slice(h+1),!!g.trim())try{let x=JSON.parse(g);if(x.type==="chunk")n?.(x);else if(x.type==="result"){u.destroy();let _=x.response;_?.type==="response"?f(()=>a(_.result)):_?.type==="error"?f(()=>i(new Error(_.message??"Daemon error"))):f(()=>a(_))}}catch{}}}),u.on("error",m=>{f(()=>i(new Error(`Daemon connection error: ${m.message}. ${j$()}`)))}),u.on("close",()=>{f(()=>i(new Error("Daemon connection closed unexpectedly")))})})}static callBudgetMs(e){switch(e){case"verify_done":return 12e4;case"run_pipeline":case"get_context_capsule":return 9e4;default:return 3e4}}static timeoutMessage(e,r){let n=`Timeout: no response from daemon for tool '${e}' after ${Math.round(r/1e3)}s. The daemon accepted the request and keeps processing it (a client that stops waiting does not cancel it)`;return e==="verify_done"?n+="; call verify_done once more in ~30s: a finished verdict on an unchanged tree is served from cache instantly. Phase timings for the slow call are in .vexp/daemon.log (grep verify_done).":n+=".",n}async callOnce(e,r){return new Promise((n,a)=>{let o={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId},s=JSON.stringify(o)+`
|
|
107
|
-
`,c,u="",p=!1,l=t.callBudgetMs(e),
|
|
108
|
-
`);if(
|
|
107
|
+
`,c,u="",p=!1,l=!1,d=t.callBudgetMs(e),f=setTimeout(()=>{p||(p=!0,c?.destroy(),a(new Error(t.timeoutMessage(e,d))))},d),m=h=>{p||(p=!0,clearTimeout(f),h())};process.platform==="win32"?c=Bo.createConnection(this.socketPath):c=Bo.createConnection({path:this.socketPath}),c.on("connect",()=>{c.write(s,h=>{h||(l=!0)})}),c.on("data",h=>{u+=h.toString();let g=u.indexOf(`
|
|
108
|
+
`);if(g!==-1){let x=u.slice(0,g);c.destroy();try{let _=JSON.parse(x);_.type==="response"?m(()=>n(_.result)):m(()=>a(new Error(_.message??"Daemon error")))}catch{m(()=>a(new Error(`Invalid response JSON: ${x}`)))}}}),c.on("error",h=>{m(()=>a(l?t.daemonGoneError(e,h.message):new Error(`Daemon connection error: ${h.message}. ${j$()}`)))}),c.on("close",()=>{m(()=>a(t.daemonGoneError(e)))})})}async health(){try{return await this.call("index_status",{}),!0}catch{return!1}}};function LL(){if(process.env.VEXP_CORE_PATH&&oe.existsSync(process.env.VEXP_CORE_PATH))return process.env.VEXP_CORE_PATH;let t;try{t=typeof __dirname<"u"?__dirname:L.dirname((0,A$.fileURLToPath)(WL.url))}catch{t=process.cwd()}let e=`${process.platform}-${process.arch==="arm64"?"arm64":"x64"}`,r=process.platform==="win32"?".exe":"",n=[L.resolve(t,"..","..","@vexp",`core-${e}`,"bin",`vexp-core${r}`),L.resolve(t,"..","node_modules","@vexp",`core-${e}`,"bin",`vexp-core${r}`),L.resolve(t,"..","binaries",`vexp-core-${e}`,`vexp-core${r}`),L.resolve(t,"..","..","..","target","release",`vexp-core${r}`)];for(let a of n)if(oe.existsSync(a))return a;return null}function FL(t){let e=L.dirname(t),r={...process.env};return process.platform==="linux"?r.LD_LIBRARY_PATH=r.LD_LIBRARY_PATH?`${e}:${r.LD_LIBRARY_PATH}`:e:process.platform==="darwin"&&(r.DYLD_LIBRARY_PATH=r.DYLD_LIBRARY_PATH?`${e}:${r.DYLD_LIBRARY_PATH}`:e,r.DYLD_FALLBACK_LIBRARY_PATH=r.DYLD_FALLBACK_LIBRARY_PATH?`${e}:${r.DYLD_FALLBACK_LIBRARY_PATH}`:e),r}function Iu(t){let e=BigInt("0xcbf29ce484222325"),r=BigInt("0x100000001b3"),n=BigInt("0xffffffffffffffff"),a=Buffer.from(t,"utf-8");for(let i of a)e^=BigInt(i),e=e*r&n;return e.toString(16)}function ZL(){let t=process.cwd(),e=t;for(;;){let n=L.join(e,".vexp");if(oe.existsSync(L.join(n,"manifest.json"))||oe.existsSync(L.join(n,"index.db")))return e;let a=L.dirname(e);if(a===e)break;e=a}let r=L.resolve(process.env.VEXP_HOME&&L.isAbsolute(process.env.VEXP_HOME)?process.env.VEXP_HOME:Gm.homedir());for(e=t;;){if(oe.existsSync(L.join(e,".vexp"))&&L.resolve(e)!==r)return e;let n=L.dirname(e);if(n===e)break;e=n}for(e=t;;){let n=L.join(e,".git");if(oe.existsSync(n))return e;let a=L.dirname(e);if(a===e)break;e=a}return t}var O$=new Set;function UL(t){O$.has(t)||(O$.add(t),console.error(t))}function N$(t){if(t)return{root:t,source:"VEXP_WORKSPACE"};let e=process.env.VEXP_WORKSPACE,r=process.env.CLAUDE_PROJECT_DIR;return e&&r&&L.resolve(e).toLowerCase()!==L.resolve(r).toLowerCase()?(UL(`[vexp-mcp] VEXP_WORKSPACE=${e} disagrees with this session's CLAUDE_PROJECT_DIR=${r}; using CLAUDE_PROJECT_DIR for per-session targeting. Unset the global VEXP_WORKSPACE env var to silence this.`),{root:r,source:"CLAUDE_PROJECT_DIR"}):e?{root:e,source:"VEXP_WORKSPACE"}:r?{root:r,source:"CLAUDE_PROJECT_DIR"}:{root:ZL(),source:"cwd-discovery"}}function Vo(t){return N$(t).root}function I$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r=L.join(e,".vexp","daemons.json"),n;try{n=JSON.parse(oe.readFileSync(r,"utf-8"))}catch{return null}let a=Object.entries(n),i=process.platform==="win32"?a:a.filter(([,u])=>{try{return oe.statSync(u).isSocket()}catch{return!1}});if(i.length===0)return null;let o=t.toLowerCase();for(let[u,p]of i)if(u.toLowerCase()===o)return p;let s="",c=null;for(let[u,p]of i){let l=u.toLowerCase();(o.startsWith(l+L.sep)||o===l)&&l.length>s.length&&(s=l,c=p)}return c||null}function BL(){let t=process.env.HOME??process.env.USERPROFILE??"";if(!t)return[];let e;try{e=JSON.parse(oe.readFileSync(L.join(t,".vexp","daemons.json"),"utf-8"))}catch{return[]}let r=Object.entries(e);return process.platform==="win32"?r:r.filter(([,n])=>{try{return oe.statSync(n).isSocket()}catch{return!1}})}function Qa(){let{root:t,source:e}=N$(),r=process.env.VEXP_SOCKET;if(r)return{socket:r,source:e,socketOrigin:"explicit",workspaceRoot:t};if(process.platform==="win32"){let i=L.join(t,".vexp","daemon.pipe");try{let c=oe.readFileSync(i,"utf-8").trim();if(c)return{socket:c,source:e,socketOrigin:"workspace-local",workspaceRoot:t}}catch{}let o=I$(t);return o?{socket:o,source:e,socketOrigin:"registry",workspaceRoot:t}:{socket:`\\\\.\\pipe\\vexp-${Iu(t.toLowerCase()).slice(0,8)}`,source:e,socketOrigin:"hash-fallback",workspaceRoot:t}}let n=L.join(t,".vexp","daemon.sock");if(oe.existsSync(n))return{socket:n,source:e,socketOrigin:"workspace-local",workspaceRoot:t};let a=I$(t);return a?{socket:a,source:e,socketOrigin:"registry",workspaceRoot:t}:{socket:null,source:e,socketOrigin:"none",workspaceRoot:t}}function D$(){return Qa().socket}function VL(){let t;try{t=Qa()}catch{return""}let e=process.env.VEXP_WORKSPACE,r=process.env.CLAUDE_PROJECT_DIR,n=t.socket??L.join(t.workspaceRoot,".vexp","daemon.sock"),a=(()=>{try{return oe.existsSync(n)}catch{return!1}})(),i=oe.existsSync(L.join(t.workspaceRoot,".vexp","manifest.json"))||oe.existsSync(L.join(t.workspaceRoot,".vexp","index.db")),o=t.source==="cwd-discovery"&&!i,s=BL(),c=["","--- vexp targeting diagnostics ---","The MCP server could not reach a vexp daemon for this session.","","Workspace resolution:",` VEXP_WORKSPACE ${e||"(not set)"}`,` CLAUDE_PROJECT_DIR ${r||"(not set)"}`,` resolved workspace ${t.workspaceRoot} (via ${t.source}${o?" \u2014 no indexed .vexp/ found walking up from cwd":""})`,` socket tried ${n}${a?"":" (missing)"}`,""];if(s.length){c.push(`Live daemons in ~/.vexp/daemons.json (${s.length}):`);for(let[u,p]of s.slice(0,8))c.push(` ${u} -> ${p}`);s.length>8&&c.push(` ... and ${s.length-8} more`)}else c.push("No live daemons are registered in ~/.vexp/daemons.json.");return c.push(""),o?c.push("This host launched the MCP server outside your project and passed no","VEXP_WORKSPACE, so vexp cannot tell which project you mean. Set VEXP_WORKSPACE","to your project root in the MCP server's env"+(s.length===1?`, e.g. VEXP_WORKSPACE=${s[0][0]}`:"")+"."):s.some(([u])=>u.toLowerCase()===t.workspaceRoot.toLowerCase())||c.push(`No live daemon is registered for ${t.workspaceRoot}.`,"Start it with `vexp daemon-cmd start` in that directory, or `vexp index` to (re)index."),c.join(`
|
|
109
109
|
`)}function HL(){let t=D$();if(t)return t;let e=Vo();return L.join(e,".vexp","daemon.sock")}function M$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r=L.join(e,".vexp","daemons.json"),n;try{n=JSON.parse(oe.readFileSync(r,"utf-8"))}catch{return null}let a=t.toLowerCase();for(let[i,o]of Object.entries(n))if(Iu(i.toLowerCase()).slice(0,8)===a){if(process.platform==="win32")return o;try{if(oe.statSync(o).isSocket())return o}catch{}}return null}function L$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r;try{r=JSON.parse(oe.readFileSync(L.join(e,".vexp","daemons.json"),"utf-8"))}catch{return null}let n=t.toLowerCase();for(let a of Object.keys(r))if(Iu(a.toLowerCase()).slice(0,8)===n)return a;return null}function GL(t){return process.platform==="win32"?`\\\\.\\pipe\\vexp-${Iu(t.toLowerCase()).slice(0,8)}`:L.join(t,".vexp","daemon.sock")}function j$(){let t=Vo(),e=process.env.HOME??process.env.USERPROFILE??"",r=[];if(process.env.VEXP_SOCKET&&r.push(`VEXP_SOCKET=${process.env.VEXP_SOCKET}`),r.push(`${t}/.vexp/daemon.sock`),e)try{let n=JSON.parse(oe.readFileSync(L.join(e,".vexp","daemons.json"),"utf-8"));for(let[a,i]of Object.entries(n))r.push(`registry: ${a} \u2192 ${i}`)}catch{}return`Tried: [${r.join(", ")}]. Run \`vexp setup\` in your project to start the daemon, or set VEXP_WORKSPACE / VEXP_SOCKET.`}var or=t=>typeof t=="string"?[t]:t,ft=t=>t==="true"?!0:t==="false"?!1:t,pt=t=>typeof t=="string"&&t.trim()!==""&&!isNaN(Number(t))?Number(t):t;var KL=S.object({query:S.string().describe("Description of the task or what you need context for"),repos:S.preprocess(or,S.array(S.string()).optional()).describe("Repo aliases to query (default: all)"),max_tokens:S.preprocess(pt,S.number().optional().default(8e3)).describe("Max tokens for the capsule"),pivot_depth:S.preprocess(pt,S.number().optional().default(2)).describe("BFS depth from pivot nodes"),include_tests:S.preprocess(ft,S.boolean().optional().default(!1)).describe("Include test files"),skeleton_detail:S.enum(["minimal","standard","detailed"]).optional().default("standard")}),F$={name:"get_context_capsule",description:"Lightweight context search \u2014 finds relevant code via semantic + graph search. NOTE: For most tasks, prefer run_pipeline instead \u2014 it includes this capsule plus impact analysis and memory in a single call with fewer tokens. Use get_context_capsule only for quick, simple lookups where you don't need impact analysis or memory recall, or when you want a lighter, faster response.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Description of the task"},repos:{type:"array",items:{type:"string"},description:"Repo aliases to query (from index_status). Default: all indexed repos"},max_tokens:{type:"number",description:"Max tokens (default: 8000)"},pivot_depth:{type:"number",description:"Graph traversal depth (default: 2)"},include_tests:{type:"boolean",description:"Include test files"},skeleton_detail:{type:"string",enum:["minimal","standard","detailed"],description:"Skeleton detail level (default: standard)"}},required:["query"]}};async function Z$(t,e){let r=KL.parse(t),n=await e.call("get_context_capsule",r);return JL(n)}function JL(t){let e=[];e.push("# vexp Context Capsule");let r=t.token_budget.saving_pct>1?`${t.token_budget.saving_pct.toFixed(0)}% token saving vs full content`:`${t.graph_stats.nodes_analyzed} nodes \xB7 ${t.graph_stats.edges_traversed} edges traversed`,n=t.memories?.length??0;if(e.push(`> Token budget: ${t.token_budget.used}/${t.token_budget.total} used | ${r} | ${t.graph_stats.query_time_ms}ms | memories: ${n}`),e.push(""),t.pivots.length===0)return e.push("*No relevant code found for this query. Try a different description.*"),e.join(`
|
|
110
110
|
`);let a=t.graph_stats.repos_queried.length>1;e.push("## Pivot Files (Full Content)"),e.push("");for(let i of t.pivots){let o=a?`\`${i.repo}\` / `:"";e.push(`### ${o}\`${i.file_path}\` (relevance: ${i.relevance_score.toFixed(2)})`),e.push(`*${i.why}*`),e.push(""),e.push("```"),e.push(i.content),e.push("```"),e.push("")}if(t.supporting.length>0){e.push("## Supporting Context (Skeletons)"),e.push("");for(let i of t.supporting){let o=i.cross_repo?" *(cross-repo)*":"";e.push(`### \`${i.file_path}\`${o}`),e.push(`*${i.relationship}*`),e.push(""),e.push("```"),e.push(i.skeleton),e.push("```"),e.push("")}}if(e.push("## Session Memory"),t.memories&&t.memories.length>0){e.push("*Relevant observations from this and previous sessions:*"),e.push("");for(let i of t.memories){let o=new Date(i.created_at*1e3),s=`${String(o.getMonth()+1).padStart(2,"0")}-${String(o.getDate()).padStart(2,"0")} ${String(o.getHours()).padStart(2,"0")}:${String(o.getMinutes()).padStart(2,"0")}`,c=i.stale?" ~stale~":"";e.push(`- [${s}${c}] ${i.content}`)}}else e.push("*No relevant memories found. Use `save_observation` to persist important decisions.*");return e.push(""),e.join(`
|
|
111
111
|
`)}var XL=S.object({symbol_fqn:S.string().describe("Fully qualified name of the symbol (e.g. 'src/auth/auth.ts::validateToken')"),depth:S.preprocess(pt,S.number().optional().default(5)).describe("BFS depth for impact traversal"),cross_repo:S.preprocess(ft,S.boolean().optional().default(!1)).describe("Include cross-repo dependencies"),format:S.enum(["list","tree","mermaid"]).optional().default("tree")}),U$={name:"get_impact_graph",description:"Get the impact graph for a symbol \u2014 shows all code that would break or be affected if this symbol changes. Returns callers, importers, and transitive dependents up to the specified depth. WHEN TO USE: (1) Before refactoring a function/class \u2014 to understand the blast radius. (2) Before changing a public API \u2014 to find all call sites. (3) When asked 'what uses X?' or 'what depends on X?'. Requires the exact FQN from a previous get_context_capsule result (e.g. 'src/auth/auth.ts::validateToken').",inputSchema:{type:"object",properties:{symbol_fqn:{type:"string",description:"Fully qualified name (e.g. 'src/auth/auth.ts::validateToken')"},depth:{type:"number",description:"Max traversal depth (default: 5)"},cross_repo:{type:"boolean",description:"Include cross-repo symbol matches and synthetic edges (API contracts, shared types). Use in multi-repo workspaces"},format:{type:"string",enum:["list","tree","mermaid"],description:"Output format (default: tree)"}},required:["symbol_fqn"]}};async function B$(t,e){let r=XL.parse(t),n=await e.call("get_impact_graph",r);return typeof n=="string"?n:YL(n,r.format??"tree")}function YL(t,e){let r=[];return r.push(`# Impact Graph: \`${t.root_fqn}\``),r.push(`> ${t.total_impacted} nodes impacted | depth ${t.max_depth_reached} | ${t.query_time_ms}ms`),r.push(""),t.total_impacted===0?(r.push("*No dependents found. This symbol is not referenced by other code.*"),r.join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexp-cli",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "Local-first context engine for AI coding agents. Pre-indexes your codebase into a dependency graph and feeds any MCP agent only the code that matters — 87% fewer tokens per call. New in 2.5: mechanical work verification and a PII/secret scanner. Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Cline, Aider and 14 agents. Your code never leaves your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
},
|
|
106
106
|
"homepage": "https://vexp.dev",
|
|
107
107
|
"optionalDependencies": {
|
|
108
|
-
"@vexp/core-linux-x64": "3.2.
|
|
109
|
-
"@vexp/core-linux-arm64": "3.2.
|
|
110
|
-
"@vexp/core-darwin-x64": "3.2.
|
|
111
|
-
"@vexp/core-darwin-arm64": "3.2.
|
|
112
|
-
"@vexp/core-win32-x64": "3.2.
|
|
108
|
+
"@vexp/core-linux-x64": "3.2.2",
|
|
109
|
+
"@vexp/core-linux-arm64": "3.2.2",
|
|
110
|
+
"@vexp/core-darwin-x64": "3.2.2",
|
|
111
|
+
"@vexp/core-darwin-arm64": "3.2.2",
|
|
112
|
+
"@vexp/core-win32-x64": "3.2.2"
|
|
113
113
|
}
|
|
114
114
|
}
|