project-graph-mcp 2.1.9 → 2.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-graph-mcp",
3
- "version": "2.1.9",
3
+ "version": "2.1.11",
4
4
  "type": "module",
5
5
  "description": "MCP server for AI agents — project graph, code quality analysis, visual web explorer. JS, TS, Python, Go.",
6
6
  "main": "src/network/server.js",
@@ -7,10 +7,11 @@ import{createInterface as p}from"node:readline";
7
7
  import{createConnection as d}from"node:net";
8
8
  import{fileURLToPath as h}from"node:url";
9
9
  const m=a(h(import.meta.url),".."),g=a(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway","backends");
10
+ function _getVersion(){try{return JSON.parse(o(a(m,"..","..","package.json"),"utf8")).version}catch{return"0.0.0"}}
10
11
  function y(t){const r=l(t),n=e("md5").update(r).digest("hex").slice(0,8);return a(g,`${n}.json`)}
11
12
  function B(e){const t=y(e);if(!r(t))return null;try{const e=JSON.parse(o(t,"utf8"));try{process.kill(e.pid,0)}catch{try{s(t)}catch{}return null}return e}catch{return null}}
12
- export function writePortFile(e,t){n(g,{recursive:!0});const r=l(e),o={port:t,pid:process.pid,project:r,name:f(r)||"root",startedAt:Date.now()};c(y(e),JSON.stringify(o,null,2))}
13
+ export function writePortFile(e,t){n(g,{recursive:!0});const r=l(e),i={port:t,pid:process.pid,project:r,name:f(r)||"root",version:_getVersion(),startedAt:Date.now()};c(y(e),JSON.stringify(i,null,2))}
13
14
  export function removePortFile(e){try{s(y(e))}catch{}}
14
15
  export function listBackends(){if(!r(g))return[];const e=i(g).filter(e=>e.endsWith(".json")),t=[];for(const r of e)try{const e=JSON.parse(o(a(g,r),"utf8"));try{process.kill(e.pid,0),t.push(e)}catch{try{s(a(g,r))}catch{}}}catch{}return t}
15
- export async function ensureBackend(e){const t=l(e),n=B(t);if(n)return n.port;const o=a(m,"backend.js");u(process.execPath,[o,t],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref();const c=y(t),s=Date.now();for(;Date.now()-s<1e4;)if(await new Promise(e=>setTimeout(e,200)),r(c)){const e=B(t);if(e)return e.port}throw new Error("Backend failed to start within 10s")}
16
+ export async function ensureBackend(e){const t=l(e),n=B(t);if(n){const cv=_getVersion();if(n.version&&n.version!==cv){console.error(`[project-graph] Version mismatch: running ${n.version}, installed ${cv} — restarting...`);try{process.kill(n.pid,"SIGTERM")}catch{}try{s(y(t))}catch{}await new Promise(r=>setTimeout(r,500))}else{return n.port}}const o=a(m,"backend.js");u(process.execPath,[o,t],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref();const c=y(t),f=Date.now();for(;Date.now()-f<1e4;)if(await new Promise(e=>setTimeout(e,200)),r(c)){const e=B(t);if(e)return e.port}throw new Error("Backend failed to start within 10s")}
16
17
  export function startStdioProxy(e,r=[]){const n=t(16).toString("base64"),o=d({host:"127.0.0.1",port:e},()=>{o.write(`GET /mcp-ws HTTP/1.1\r\nHost: 127.0.0.1:${e}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${n}\r\nSec-WebSocket-Version: 13\r\n\r\n`)});let c=!1,s=Buffer.alloc(0),i=[...r];const a=p({input:process.stdin,terminal:!1});function l(e){const r=Buffer.from(e,"utf8"),n=t(4),o=Buffer.alloc(r.length);for(let e=0;e<r.length;e++)o[e]=r[e]^n[e%4];let c;return r.length<126?(c=Buffer.alloc(2),c[0]=129,c[1]=128|r.length):r.length<65536?(c=Buffer.alloc(4),c[0]=129,c[1]=254,c.writeUInt16BE(r.length,2)):(c=Buffer.alloc(10),c[0]=129,c[1]=255,c.writeBigUInt64BE(BigInt(r.length),2)),Buffer.concat([c,n,o])}function f(e){if(e.length<2)return null;const t=15&e[0];let r=127&e[1],n=2;if(126===r){if(e.length<4)return null;r=e.readUInt16BE(2),n=4}else if(127===r){if(e.length<10)return null;r=Number(e.readBigUInt64BE(2)),n=10}return e.length<n+r?null:{opcode:t,data:e.slice(n,n+r).toString("utf8"),totalLen:n+r}}a.on("line",e=>{if(c)try{o.write(l(e))}catch{}else i.push(e)}),a.on("close",()=>{o.end(),process.exit(0)}),o.on("data",e=>{if(c)s=Buffer.concat([s,e]);else{const t=Buffer.concat([s,e]),r=t.indexOf("\r\n\r\n");if(-1===r)return void(s=t);t.slice(0,r).toString().includes("101")||(console.error("[project-graph] WebSocket handshake failed"),process.exit(1)),c=!0,s=t.slice(r+4);for(const e of i)try{o.write(l(e))}catch{}i=[]}for(;s.length>=2;){const e=f(s);if(!e)break;if(s=s.slice(e.totalLen),1===e.opcode)process.stdout.write(e.data+"\n");else if(8===e.opcode)process.exit(0);else if(9===e.opcode){const e=Buffer.alloc(2);e[0]=138,e[1]=0,o.write(e)}}}),o.on("close",()=>process.exit(0)),o.on("error",e=>{console.error(`[project-graph] Proxy connection error: ${e.message}`),process.exit(1)})}
@@ -1,8 +1,8 @@
1
1
  // @ctx .context/src/network/web-server.ctx
2
2
  import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{setRoots as _setRoots}from"../core/workspace.js";
3
- const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);function _rv(k){try{return o.dirname(_rq.resolve(k+"/package.json"))}catch{return o.join(p,"node_modules",k)}}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
3
+ const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);let _pkgVersion="0.0.0";try{_pkgVersion=JSON.parse(t.readFileSync(o.join(p,"package.json"),"utf8")).version}catch{}function _rv(k){try{const r=_rq.resolve(k);const marker=o.sep+"node_modules"+o.sep+k.replace(/\//g,o.sep);const idx=r.lastIndexOf(marker);if(idx>=0)return r.substring(0,idx+marker.length);return o.dirname(r)}catch{return o.join(p,"node_modules",...k.split("/"))}}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
4
4
  function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
5
5
  function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
6
6
  function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
7
7
  function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
8
- export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
8
+ export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}