project-graph-mcp 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +223 -17
  2. package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
  3. package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -0
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/ai-context.js +7 -0
  23. package/src/compact/compact.js +18 -0
  24. package/src/compact/compress.js +13 -0
  25. package/src/compact/ctx-to-jsdoc.js +29 -0
  26. package/src/compact/doc-dialect.js +30 -0
  27. package/src/compact/expand.js +37 -0
  28. package/src/compact/framework-references.js +5 -0
  29. package/src/compact/instructions.js +3 -0
  30. package/src/compact/mode-config.js +8 -0
  31. package/src/compact/validate-pipeline.js +9 -0
  32. package/src/core/event-bus.js +9 -0
  33. package/src/core/filters.js +14 -0
  34. package/src/core/graph-builder.js +12 -0
  35. package/src/core/parser.js +31 -0
  36. package/src/core/workspace.js +8 -0
  37. package/src/lang/lang-go.js +17 -0
  38. package/src/lang/lang-python.js +12 -0
  39. package/src/lang/lang-sql.js +23 -0
  40. package/src/lang/lang-typescript.js +9 -0
  41. package/src/lang/lang-utils.js +4 -0
  42. package/src/mcp/mcp-server.js +17 -0
  43. package/src/mcp/tool-defs.js +3 -0
  44. package/src/mcp/tools.js +25 -0
  45. package/src/network/backend-lifecycle.js +19 -0
  46. package/src/network/backend.js +5 -0
  47. package/src/network/local-gateway.js +23 -0
  48. package/src/network/mdns.js +13 -0
  49. package/src/network/server.js +10 -0
  50. package/src/network/web-server.js +34 -0
  51. package/vendor/terser.mjs +49 -0
  52. package/web/.project-graph-cache.json +1 -0
  53. package/web/app.js +16 -0
  54. package/web/components/code-block.js +3 -0
  55. package/web/components/quick-open.js +5 -0
  56. package/web/dashboard-state.js +3 -0
  57. package/web/dashboard.html +27 -0
  58. package/web/dashboard.js +8 -0
  59. package/web/highlight.js +13 -0
  60. package/web/index.html +35 -0
  61. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  62. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  63. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  64. package/web/panels/EventItem/EventItem.css.js +1 -0
  65. package/web/panels/EventItem/EventItem.js +4 -0
  66. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  67. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  69. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  70. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.js +4 -0
  72. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  73. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  74. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  77. package/web/panels/code-viewer.js +5 -0
  78. package/web/panels/ctx-panel.js +4 -0
  79. package/web/panels/dep-graph.js +6 -0
  80. package/web/panels/file-tree.js +188 -0
  81. package/web/panels/health-panel.js +3 -0
  82. package/web/panels/live-monitor.js +3 -0
  83. package/web/state.js +17 -0
  84. package/web/style.css +157 -0
  85. package/references/symbiote-3x.md +0 -834
  86. package/src/cli-handlers.js +0 -140
  87. package/src/cli.js +0 -83
  88. package/src/complexity.js +0 -223
  89. package/src/custom-rules.js +0 -583
  90. package/src/db-analysis.js +0 -194
  91. package/src/dead-code.js +0 -468
  92. package/src/filters.js +0 -227
  93. package/src/framework-references.js +0 -177
  94. package/src/full-analysis.js +0 -174
  95. package/src/graph-builder.js +0 -299
  96. package/src/instructions.js +0 -175
  97. package/src/jsdoc-generator.js +0 -214
  98. package/src/lang-go.js +0 -285
  99. package/src/lang-python.js +0 -197
  100. package/src/lang-sql.js +0 -309
  101. package/src/lang-typescript.js +0 -190
  102. package/src/lang-utils.js +0 -124
  103. package/src/large-files.js +0 -162
  104. package/src/mcp-server.js +0 -468
  105. package/src/outdated-patterns.js +0 -295
  106. package/src/parser.js +0 -452
  107. package/src/server.js +0 -28
  108. package/src/similar-functions.js +0 -278
  109. package/src/test-annotations.js +0 -301
  110. package/src/tool-defs.js +0 -525
  111. package/src/tools.js +0 -470
  112. package/src/undocumented.js +0 -260
  113. package/src/workspace.js +0 -70
@@ -0,0 +1,23 @@
1
+ // @ctx .context/src/network/local-gateway.ctx
2
+ import e from"node:http";
3
+ import t from"node:net";
4
+ import r from"node:fs";
5
+ import n from"node:path";import{registerLocal as o}from"./mdns.js";
6
+ const s=n.join(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway"),i=n.join(s,"services.json"),a=n.join(s,"gateway.pid");function readRegistry(){try{return JSON.parse(r.readFileSync(i,"utf8"))}catch{return{}}}
7
+ function writeRegistry(e){r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify(e,null,2))}
8
+ export function registerService(e,t,r={}){const n=`${e}.local`,s=readRegistry();if(r.projectName){s[n]||(s[n]={name:e,routes:{}});
9
+ const o=`/${r.projectName}`;s[n].routes=s[n].routes||{},s[n].routes[o]={port:t,pid:process.pid,projectPath:r.projectPath,projectName:r.projectName}}else s[n]={port:t,pid:process.pid,name:e};writeRegistry(s);
10
+ const i=o(n,80);ensureGateway();
11
+ const cleanup=()=>{i.cleanup();try{const e=readRegistry();r.projectName&&e[n]?.routes?(delete e[n].routes[`/${r.projectName}`],0===Object.keys(e[n].routes).length&&delete e[n]):delete e[n],writeRegistry(e),0===Object.keys(e).length&&stopGateway()}catch{}};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit()}),process.on("SIGTERM",()=>{cleanup(),process.exit()});
12
+ const a=getGatewayPort(),c=80===a?"":`:${a}`,p=r.projectName?`http://${n}${c}/${r.projectName}/`:`http://${n}${c}/`;return{cleanup:cleanup,url:p,directUrl:`http://localhost:${t}/`}}
13
+ function resolveBackend(e,t,r){const n=r[e];if(!n)return null;if(n.routes){const e=Object.keys(n.routes).sort((e,t)=>t.length-e.length);for(const r of e)if(t===r||t.startsWith(r+"/")){const e=n.routes[r];try{process.kill(e.pid,0)}catch{continue}const o=t.slice(r.length)||"/";return{port:e.port,rewritePath:o,prefix:r}}for(const r of e)try{const e=n.routes[r];process.kill(e.pid,0);
14
+ const o="/"===t||""===t?"/dashboard.html":t;return{port:e.port,rewritePath:o}}catch{continue}}if(n.port){const e="/"===t||""===t?"/dashboard.html":t;return{port:n.port,rewritePath:e}}return null}
15
+ function readGatewayPid(){try{const e=r.readFileSync(a,"utf8");return e.trim().startsWith("{")?JSON.parse(e):{pid:parseInt(e,10),port:80}}catch{return null}}
16
+ function isGatewayRunning(){const e=readGatewayPid();if(!e)return!1;try{return process.kill(e.pid,0),!0}catch{return!1}}
17
+ export function getGatewayPort(){const e=readGatewayPid();return e?.port||80}
18
+ function ensureGateway(){if(!isGatewayRunning())try{const n=e.createServer((t,r)=>{const n=(t.headers.host||"").split(":")[0],o=readRegistry(),s=resolveBackend(n,t.url,o);if(!s)return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${n}\nRegistered: ${Object.keys(o).join(", ")}`);if("/api/gateway-info"===t.url){const e=JSON.stringify(o["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(e)}const i=e.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:t.method,headers:{...t.headers,host:`localhost:${s.port}`}},e=>{if((e.headers["content-type"]||"").includes("text/html")&&s.prefix){const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{let n=Buffer.concat(t).toString("utf8");
19
+ const o=`<base href="${s.prefix}/">`;n=n.includes("<head>")?n.replace("<head>",`<head>\n ${o}`):o+"\n"+n;
20
+ const i=Buffer.from(n,"utf8"),a={...e.headers};a["content-length"]=i.length,delete a["transfer-encoding"],r.writeHead(e.statusCode,a),r.end(i)})}else r.writeHead(e.statusCode,e.headers),e.pipe(r)});i.on("error",()=>{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}),t.pipe(i)});function startListening(e){n.listen(e,"0.0.0.0",()=>{const e=n.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(a,JSON.stringify({pid:process.pid,port:e}))})}n.on("upgrade",(e,r,n)=>{const o=(e.headers.host||"").split(":")[0],s=readRegistry(),i=resolveBackend(o,e.url,s);if(!i||i.isDashboard)return void r.destroy();
21
+ const a=t.createConnection({host:"127.0.0.1",port:i.port},()=>{const t=i.rewritePath,o=`${e.method} ${t} HTTP/1.1\r\n`+Object.entries(e.headers).map(([e,t])=>`${e}: ${t}`).join("\r\n")+"\r\n\r\n";a.write(o),n.length&&a.write(n);
22
+ let s=Buffer.alloc(0);a.on("data",function onFirstData(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),a.removeListener("data",onFirstData),r.pipe(a),a.pipe(r))})});a.on("error",e=>{console.error("WS PROXY ERROR:",e.message),r.destroy()}),r.on("error",e=>{console.error("WS CLIENT ERROR:",e.message),a.destroy()})}),n.on("error",e=>{"EACCES"===e.code&&!1===n.listening?startListening(8080):"EADDRINUSE"===e.code&&n.listening}),startListening(80)}catch{}}
23
+ function stopGateway(){try{r.unlinkSync(a),r.unlinkSync(i)}catch{}}
@@ -0,0 +1,13 @@
1
+ // @ctx .context/src/network/mdns.ctx
2
+ import{spawn as t}from"node:child_process";
3
+ import e from"node:dgram";
4
+ const r="224.0.0.251";
5
+ export function registerLocal(t,e){if("darwin"===process.platform)return registerDnsSd(t,e);if("linux"===process.platform){const e=tryAvahi(t);if(e)return e}return registerMcast(t)}
6
+ function registerDnsSd(e,r){const n=t("dns-sd",["-P","Project Graph","_http._tcp","",String(r),e,"127.0.0.1"],{stdio:"ignore",detached:!1});return n.unref(),{method:"Bonjour (dns-sd)",cleanup:()=>{try{n.kill()}catch{}}}}
7
+ function tryAvahi(e){try{const r=t("avahi-publish-address",["-R",e,"127.0.0.1"],{stdio:"ignore",detached:!1});
8
+ let n=!1;return r.on("error",()=>{n=!0}),r.unref(),n?null:{method:"Avahi",cleanup:()=>{try{r.kill()}catch{}}}}catch{return null}}
9
+ function registerMcast(t){const n=t.split("."),c=Buffer.concat([...n.map(t=>{const e=Buffer.alloc(1+t.length);return e[0]=t.length,e.write(t,1,"ascii"),e}),Buffer.from([0])]);
10
+ let o;try{o=e.createSocket({type:"udp4",reuseAddr:!0})}catch{return{method:"none",cleanup:()=>{}}}o.on("message",t=>{if(t.length<12)return;if(32768&t.readUInt16BE(2))return;if(0===t.readUInt16BE(4))return;if(12+c.length+4>t.length)return;if(0!==t.compare(c,0,c.length,12,12+c.length))return;
11
+ const e=12+c.length,n=t.readUInt16BE(e),i=32767&t.readUInt16BE(e+2);if(1!==n||1!==i)return;
12
+ const s=Buffer.alloc(12+c.length+10+4);
13
+ let a=0;s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(33792,a),a+=2,s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(1,a),a+=2,s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(0,a),a+=2,c.copy(s,a),a+=c.length,s.writeUInt16BE(1,a),a+=2,s.writeUInt16BE(32769,a),a+=2,s.writeUInt32BE(120,a),a+=4,s.writeUInt16BE(4,a),a+=2,s[a++]=127,s[a++]=0,s[a++]=0,s[a++]=1,o.send(s,0,a,5353,r)}),o.on("error",()=>{try{o.close()}catch{}});try{o.bind({port:5353,exclusive:!1},()=>{try{o.addMembership(r),o.setMulticastTTL(255)}catch{try{o.close()}catch{}}})}catch{return{method:"none",cleanup:()=>{}}}return{method:"Node.js mDNS",cleanup:()=>{try{o.close()}catch{}}}}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // @ctx .context/src/network/server.ctx
3
+ import e from"node:path";
4
+ import t from"node:fs";if(process.argv[1]&&(process.argv[1].endsWith("server.js")||process.argv[1].endsWith("project-graph-mcp"))){const[,,o,...r]=process.argv;if("serve"===o){const t=r[0]||".",o=r.indexOf("--port"),s=-1!==o?parseInt(r[o+1],10):0;if(s){const{startWebServer:e}=await import("./web-server.js");e(t,s)}else{const{ensureBackend:o}=await import("./backend-lifecycle.js");try{const r=await o(t),s=e.resolve(t);console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${r}/`),console.log(` → Project: ${s}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${r}/mcp-ws\n`)}catch(e){console.error(`Failed to start backend: ${e.message}`),process.exit(1)}}}else if(o){const{runCLI:e}=await import("../cli/cli.js");e(o,r)}else if(process.env.PROJECT_GRAPH_BACKEND){const{startStdioServer:e}=await import("../mcp/mcp-server.js");console.error("Starting Project Graph MCP (stdio, direct)..."),e()}else{const{setRoots:e,getWorkspaceRoot:o}=await import("../core/workspace.js"),{ensureBackend:r,startStdioProxy:s}=await import("./backend-lifecycle.js"),{createInterface:i}=await import("node:readline"),n=t.createWriteStream("/tmp/pg-init-debug.log",{flags:"a"});n.write(`\n=== NEW SESSION ${(new Date).toISOString()} ===\n`);
5
+ const c=i({input:process.stdin,terminal:!1}),a=[];
6
+ let l=!1,p=null,d=null;
7
+ const startProxy=async e=>{if(!l){l=!0,c.removeAllListeners("line"),c.close(),n.write(`RESOLVED: ${e}\n`),n.end();try{const t=await r(e);console.error(`[project-graph] Connected to backend on port ${t} (project: ${e})`),s(t,a)}catch(e){console.error(`[project-graph] Singleton failed (${e.message}), falling back to direct stdio`);const{startStdioServer:t}=await import("../mcp/mcp-server.js");t(a)}}};c.on("line",t=>{try{const r=JSON.parse(t);if(n.write(`IN: ${r.method||`response:${r.id}`}\n`),"initialize"===r.method){d=r.id,r.params?.roots?.length>0&&(e(r.params.roots),n.write("ROOTS from initialize.params\n"));
8
+ const t=JSON.stringify({jsonrpc:"2.0",id:r.id,result:{protocolVersion:"2025-06-18",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:"2.0.0"}}});return n.write("OUT: initialize response\n"),void process.stdout.write(t+"\n")}if("initialized"===r.method||"notifications/initialized"===r.method){n.write("IN: initialized notification\n"),p=999999;
9
+ const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p&&(n.write(`IN: roots/list response: ${JSON.stringify(r.result)}\n`),r.result?.roots?.length>0)){e(r.result.roots);
10
+ const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}a.push(t)}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
@@ -0,0 +1,34 @@
1
+ // @ctx .context/src/network/web-server.ctx
2
+ import e from"node:http";
3
+ import t from"node:fs";
4
+ import o from"node:path";
5
+ import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{WebSocketServer as s}from"ws";import{createServer as r}from"../mcp/mcp-server.js";
6
+ import c from"../core/event-bus.js";import{registerService as i}from"./local-gateway.js";import{expandFile as l}from"../compact/expand.js";
7
+ const d=o.dirname(a(import.meta.url)),p=o.join(d,"..",".."),m=o.join(p,"web"),h={"symbiote-node":o.join(p,"node_modules","symbiote-node"),symbiote:o.join(p,"node_modules","@symbiotejs","symbiote")},u={".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"};function serveStatic(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);
8
+ let r,c;if(s&&h[s[1]]?(c=h[s[1]],r=o.join(c,s[2]||"index.js")):(c=m,r=o.join(m,"/"===a?"index.html":a)),!r.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(r)&&t.statSync(r).isDirectory()&&(r=o.join(r,"index.html")),!t.existsSync(r))return n.writeHead(404),void n.end("Not Found");
9
+ const i=o.extname(r),l=u[i]||"application/octet-stream",d=t.readFileSync(r);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
10
+ function computeWSAccept(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
11
+ function encodeWSFrame(e){const t=Buffer.from(e,"utf8"),o=t.length;
12
+ 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])}
13
+ function decodeWSFrame(e){if(e.length<2)return null;
14
+ const t=15&e[0],o=!!(128&e[1]);
15
+ 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;
16
+ const o=e.slice(a,a+4);a+=4;
17
+ 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}}
18
+ export function startWebServer(t,a){const d=r(()=>{}),p=o.basename(o.resolve(t))||"root";
19
+ let m=1;
20
+ const h=o.resolve(t),u=n.createHash("md5").update(h).digest("hex"),g=parseInt(u.slice(0,4),16)%360,f={project:{name:p,path:h,color:`hsl(${g}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function broadcastRPC(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of y)try{e.send(o)}catch{y.delete(e)}}
21
+ function patchState(e,t){const o=e.split(".");
22
+ let n=f;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,broadcastRPC("patch",{path:e,value:t})}
23
+ async function ensureSkeleton(){if(!f.skeleton)try{f.skeleton=await d.executeTool("get_skeleton",{path:t})}catch{}return f.skeleton}const w=new Map,y=new Set;
24
+ let S=null;function hasActiveClients(){return w.size>0||y.size>0}
25
+ function resetShutdownTimer(){S&&(clearTimeout(S),S=null)}
26
+ function startShutdownTimer(){hasActiveClients()||(resetShutdownTimer(),S=setTimeout(()=>{hasActiveClients()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}
27
+ function touchActivity(){resetShutdownTimer(),startShutdownTimer()}
28
+ async function handleAPI(e,a,s,r){try{let c; const i=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:i});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:r,existsSync:i}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),h=i(m)?r(m,"utf-8"):null,u=await l(d,h),g=Math.ceil(u.original/4),f=Math.ceil(u.decompiled/4),K=h?Math.ceil(h.length/4):0,T=g+K,w=f>0?Math.round(100*(1-T/f)):0;c={code:u.code,file:e,injected:u.injected,codeTok:g,ctxTok:K,totalTok:T,expanded:f,savings:w+"%"}}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":{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:g}=await import("fs"),{join:s,extname:r,basename:x,dirname:q,relative:z}=await import("path"),$ext=new Set([".js",".mjs"]),d=[],p=["node_modules",".git","vendor",".context",".expanded","web"];(function walk(t){try{for(const o of e(t)){if(o.startsWith("."))continue;const e=s(t,o);n(e).isDirectory()?p.includes(o)||walk(e):$ext.has(r(o))&&d.push(e)}}catch{}})(o.resolve(t,"src"));let m=0,u=0,C=0,E=0;const B=o.resolve(t);for(const e of d){try{m+=n(e).size;const r=z(B,e),s=x(r,".js"),v=o.resolve(B,".context",q(r),s+".ctx");let ct=0;if(g(v)){ct=n(v).size;C+=ct}try{const h=g(v)?a(v,"utf-8"):null,ex=await l(e,h);E+=ex.decompiled}catch{E+=n(e).size*1.3}}catch{continue}u++}c={files:u,codeTok:Math.ceil(m/4),ctxTok:Math.ceil(C/4),totalTok:Math.ceil((m+C)/4),expanded:Math.ceil(E/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:i,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:i});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:i});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/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:w.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:w.size}]}break;default:return"POST"===s&&"/api/restart"===e?(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),r.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(()=>process.exit(0),200)):(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void r.end(JSON.stringify({error:"Unknown API endpoint"})))}r.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),r.end(JSON.stringify(c))}catch(e){r.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),r.end(JSON.stringify({error:e.message}))}}startShutdownTimer(),c.on("tool:call",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)}),c.on("tool:result",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)});
29
+ const v=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/")?(touchActivity(),void handleAPI(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void serveStatic(o.pathname,t))}),j=new s({noServer:!0});j.on("connection",async e=>{y.add(e),touchActivity(),await ensureSkeleton();
30
+ const t={project:f.project,skeleton:f.skeleton};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;touchActivity();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);
31
+ let r=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(r=e(a,"utf-8"))}catch{}const c=await l(e,r),i=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),K=r?Math.ceil(r.length/4):0,T=i+K,p=d>0?Math.round(100*(1-T/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:i,ctxTok:K,totalTok:T,expanded:d,savings:p+"%"}}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",()=>{y.delete(e),startShutdownTimer()}),e.on("error",()=>{y.delete(e),startShutdownTimer()})}),v.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=computeWSAccept(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);
32
+ const o="agent-"+m++,a=r(e=>{try{t.write(encodeWSFrame(JSON.stringify(e)))}catch{}});w.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),resetShutdownTimer(),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_connect",agentId:o,agents:w.size,ts:Date.now()});
33
+ let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=decodeWSFrame(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),t.end(),void(0===w.size&&startShutdownTimer());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(encodeWSFrame(JSON.stringify(o)))}catch(e){t.write(encodeWSFrame(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),0===w.size&&startShutdownTimer()}),void t.on("error",()=>{w.delete(t),0===w.size&&startShutdownTimer()})}t.destroy()}else j.handleUpgrade(e,t,o,t=>{j.emit("connection",t,e)});else t.destroy()});
34
+ const b=!a,T=a||0;return v.listen(T,"127.0.0.1",()=>{const e=v.address().port;if(b){const n=i("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`)}),v}