project-graph-mcp 2.1.4 → 2.1.6

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.4",
3
+ "version": "2.1.6",
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",
@@ -3,6 +3,6 @@ import{estimateTokens as i}from"../core/utils.js";import{readFileSync as e}from"
3
3
  const o=new Set([".js",".mjs",".ts",".tsx"]);
4
4
 
5
5
  function c(e,n){const a=[];a.push(`--- ${t(n)} ---`);try{const t=s(e,{ecmaVersion:2022,sourceType:"module",locations:!0});r(t,{ExportNamedDeclaration(t){const n=t.declaration;if(!n)return;let s="";if(t.start>0){const n=Math.max(0,t.start-500),a=e.slice(n,t.start).trimEnd().match(/\/\*\*[\s\S]*?\*\/\s*$/);if(a&&e.slice(n+a.index+a[0].length,t.start).split("\n").length<=3){const e=a[0].replace(/\/\*\*\s*\n?/,"").replace(/\s*\*\//,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"").trim()).filter(e=>e&&!e.startsWith("@")).join(" ").trim();e&&(s=e.length>80?e.slice(0,77)+"...":e)}}if("FunctionDeclaration"===n.type){const e=n.id?.name||"anonymous",t=n.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(","),r=`${n.async?"async ":""}${e}(${t})`;a.push(s?`${r}|${s}`:r)}if("ClassDeclaration"===n.type){const e=n.id?.name||"AnonymousClass",t=n.superClass?` extends ${n.superClass.name||"?"}`:"";a.push(`class ${e}${t}${s?"|"+s:""}`);for(const e of n.body.body)if("MethodDefinition"===e.type&&e.key?.name){const t=e.value.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(",");a.push(` .${e.key.name}(${t})`)}}if("VariableDeclaration"===n.type)for(const e of n.declarations)e.id?.name&&a.push(`${n.kind} ${e.id.name}${s?"|"+s:""}`)}})}catch(e){a.push(`PARSE_ERROR: ${e.message}`)}return a.join("\n")}
6
- export async function compressFile(t,s={}){const{beautify:r=!0,legend:l=!0}=s,m=n(t).toLowerCase();if(!o.has(m))throw new Error(`Unsupported file type: ${m}. Supported: ${[...o].join(", ")}`);const d=e(t,"utf-8"),p=i(d);if(!d.trim())return{code:"",legend:"",original:0,compressed:0,savings:"0%"};const u={compress:{dead_code:!0,drop_console:!1,passes:2},mangle:!1,module:!0,output:{beautify:r,comments:!1,semicolons:!r}};let f;try{const e=await a(d,u);if(e.error)throw e.error;f=e.code}catch(e){f=d.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}f=f.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");const y=l?c(d,t):"",h=y?`/*\n${y}\n*/\n${f}`:f,$=i(h);return{code:h,legend:y,original:p,compressed:$,savings:`${p>0?Math.round(100*(1-$/p)):0}%`}}
6
+ export async function compressFile(t,_opts={}){const{beautify:r=!0,legend:l=!0}=_opts,m=n(t).toLowerCase();if(!o.has(m))throw new Error(`Unsupported file type: ${m}. Supported: ${[...o].join(", ")}`);const d=e(t,"utf-8"),p=i(d);if(!d.trim())return{code:"",legend:"",original:0,compressed:0,savings:"0%"};const u={compress:{dead_code:!0,drop_console:!1,passes:2},mangle:!1,module:!0,output:{beautify:r,comments:!1,semicolons:!r}};let f;try{const e=await a(d,u);if(e.error)throw e.error;f=e.code}catch(e){f=d.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}f=f.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");if(!r){try{const q=s(f,{ecmaVersion:2022,sourceType:"module"});const b=[];for(let j=q.body.length-1;j>0;j--){const n=q.body[j];if(n.type==="ImportDeclaration")continue;b.push(n.start)}for(const pos of b)f=f.slice(0,pos)+"\n"+f.slice(pos)}catch{}}let _beau;try{const _br=await a(d,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1}});_beau=i(_br.code)}catch{_beau=p}const y=l?c(d,t):"",h=y?`/*\n${y}\n*/\n${f}`:f,$=i(h);return{code:h,legend:y,original:p,expanded:_beau,compressed:$,savings:`${_beau>0?Math.round(100*(1-$/_beau)):0}%`}}
7
7
  export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,m=e(t,"utf-8");let d;try{d=s(m,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){throw new Error(`Failed to parse ${t}: ${e.message}`)}const p=l(d,m,n);if(!p)throw new Error(`Symbol "${n}" not found in ${t}`);let u=m.slice(0,p.start)+r+m.slice(p.end);if(i)try{const e=await a(u,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!0,semicolons:!1}});e.code&&(u=e.code)}catch{}try{s(u,{ecmaVersion:"latest",sourceType:"module"})}catch(e){throw new Error(`Edit would create invalid syntax: ${e.message}`)}if(!c){const{writeFileSync:e}=await import("fs");e(t,u,"utf-8")}return{success:!0,file:t,symbol:n,oldRange:{start:p.start,end:p.end},newLength:r.length,...c?{dryRun:!0}:{}}}
8
8
  function l(e,t,n){let a=null;return r(e,{FunctionDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"FunctionDeclaration"})},ClassDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"ClassDeclaration"})},VariableDeclaration(e){for(const t of e.declarations)t.id?.name===n&&(a={start:e.start,end:e.end,type:"VariableDeclaration"})},ExportNamedDeclaration(e){if(e.declaration){const t=e.declaration;(t.id?.name||t.declarations?.[0]?.id?.name)===n&&(a={start:e.start,end:e.end,type:"ExportNamedDeclaration"})}},ExportDefaultDeclaration(e){e.declaration?.id?.name===n&&(a={start:e.start,end:e.end,type:"ExportDefaultDeclaration"})}}),a}
@@ -8,4 +8,4 @@ export function registerService(t,e,r={}){const o=`${t}.local`,s=p();if(r.projec
8
8
  function u(t,e,r){const o=r[t];if(!o)return null;if(o.routes){const t=Object.keys(o.routes).sort((t,e)=>e.length-t.length);for(const r of t)if(e===r||e.startsWith(r+"/")){const t=o.routes[r];try{process.kill(t.pid,0)}catch{continue}const n=e.slice(r.length)||"/";return{port:t.port,rewritePath:n,prefix:r}}for(const r of t)try{const t=o.routes[r];process.kill(t.pid,0);const n="/"===e||""===e?"/dashboard.html":e;return{port:t.port,rewritePath:n}}catch{continue}}if(o.port){const t="/"===e||""===e?"/dashboard.html":e;return{port:o.port,rewritePath:t}}return null}
9
9
  function h(){try{const t=r.readFileSync(i,"utf8");return t.trim().startsWith("{")?JSON.parse(t):{pid:parseInt(t,10),port:80}}catch{return null}}
10
10
  export function getGatewayPort(){const t=h();return t?.port||80}
11
- function f(){if(!function(){const t=h();if(!t)return!1;try{return process.kill(t.pid,0),!0}catch{return!1}}())try{const o=t.createServer((e,r)=>{const o=(e.headers.host||"").split(":")[0];let n=p(),s=u(o,e.url,n);if(!s&&(d(),n=p(),s=u(o,e.url,n),!s))return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${o}\nRegistered: ${Object.keys(n).join(", ")}`);if("/api/gateway-info"===e.url){const t=JSON.stringify(n["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(t)}if("POST"===e.method&&"/api/remove-project"===e.url){let t="";return e.on("data",e=>t+=e),void e.on("end",()=>{try{const e=JSON.parse(t).route,o=p();if(o["project-graph.local"]?.routes?.[e]){const t=o["project-graph.local"].routes[e];try{process.kill(t.pid,9)}catch{}delete o["project-graph.local"].routes[e],l(o)}r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))}catch(t){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({error:t.message}))}})}const c=t.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:e.method,headers:{...e.headers,host:`localhost:${s.port}`}},t=>{if((t.headers["content-type"]||"").includes("text/html")&&s.prefix){const e=[];t.on("data",t=>e.push(t)),t.on("end",()=>{let o=Buffer.concat(e).toString("utf8");const n=`<base href="${s.prefix}/">`;o=o.includes("<head>")?o.replace("<head>",`<head>\n ${n}`):n+"\n"+o;const c=Buffer.from(o,"utf8"),i={...t.headers};i["content-length"]=c.length,delete i["transfer-encoding"],r.writeHead(t.statusCode,i),r.end(c)})}else r.writeHead(t.statusCode,t.headers),t.pipe(r)});c.on("error",()=>{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}),e.pipe(c)});function n(t){o.listen(t,"0.0.0.0",()=>{const t=o.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify({pid:process.pid,port:t})),d()})}o.on("upgrade",(t,r,o)=>{const n=(t.headers.host||"").split(":")[0],s=p(),c=u(n,t.url,s);if(!c||c.isDashboard)return void r.destroy();const i=e.createConnection({host:"127.0.0.1",port:c.port},()=>{const e=c.rewritePath,n=`${t.method} ${e} HTTP/1.1\r\n`+Object.entries(t.headers).map(([t,e])=>`${t}: ${e}`).join("\r\n")+"\r\n\r\n";i.write(n),o.length&&i.write(o);let s=Buffer.alloc(0);i.on("data",function t(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),i.removeListener("data",t),r.pipe(i),i.pipe(r))})});i.on("error",t=>{console.error("WS PROXY ERROR:",t.message),r.destroy()}),r.on("error",t=>{console.error("WS CLIENT ERROR:",t.message),i.destroy()})}),o.on("error",t=>{"EACCES"===t.code&&!1===o.listening?n(8080):"EADDRINUSE"===t.code&&o.listening}),n(80)}catch{}}
11
+ function f(){if(!function(){const t=h();if(!t)return!1;try{return process.kill(t.pid,0),!0}catch{return!1}}())try{process.on("uncaughtException",t=>{});process.on("unhandledRejection",t=>{});const o=t.createServer((e,r)=>{const o=(e.headers.host||"").split(":")[0];let n=p(),s=u(o,e.url,n);if(!s&&(d(),n=p(),s=u(o,e.url,n),!s))return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${o}\nRegistered: ${Object.keys(n).join(", ")}`);if("/api/gateway-info"===e.url){const t=JSON.stringify(n["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(t)}if("POST"===e.method&&"/api/remove-project"===e.url){let t="";return e.on("data",e=>t+=e),void e.on("end",()=>{try{const e=JSON.parse(t).route,o=p();if(o["project-graph.local"]?.routes?.[e]){const t=o["project-graph.local"].routes[e];try{process.kill(t.pid,9)}catch{}delete o["project-graph.local"].routes[e],l(o)}r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))}catch(t){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({error:t.message}))}})}const c=t.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:e.method,headers:{...e.headers,host:`localhost:${s.port}`}},t=>{if((t.headers["content-type"]||"").includes("text/html")&&s.prefix){const e=[];t.on("data",t=>e.push(t)),t.on("end",()=>{let o=Buffer.concat(e).toString("utf8");const n=`<base href="${s.prefix}/">`;o=o.includes("<head>")?o.replace("<head>",`<head>\n ${n}`):n+"\n"+o;const c=Buffer.from(o,"utf8"),i={...t.headers};i["content-length"]=c.length,delete i["transfer-encoding"];try{r.writeHead(t.statusCode,i),r.end(c)}catch{}})}else{try{r.writeHead(t.statusCode,t.headers)}catch{}t.pipe(r).on("error",()=>{})}});c.on("error",()=>{try{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}catch{}}),e.pipe(c).on("error",()=>{})});function n(t){o.listen(t,"0.0.0.0",()=>{const t=o.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify({pid:process.pid,port:t})),d()})}o.on("upgrade",(t,r,o)=>{const n=(t.headers.host||"").split(":")[0],s=p(),c=u(n,t.url,s);if(!c||c.isDashboard)return void r.destroy();const i=e.createConnection({host:"127.0.0.1",port:c.port},()=>{const e=c.rewritePath,n=`${t.method} ${e} HTTP/1.1\r\n`+Object.entries(t.headers).map(([t,e])=>`${t}: ${e}`).join("\r\n")+"\r\n\r\n";i.write(n),o.length&&i.write(o);let s=Buffer.alloc(0);i.on("data",function t(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),i.removeListener("data",t),r.pipe(i).on("error",()=>{}),i.pipe(r).on("error",()=>{}))})});i.on("error",()=>{try{r.destroy()}catch{}}),r.on("error",()=>{try{i.destroy()}catch{}})}),o.on("error",t=>{"EACCES"===t.code&&!1===o.listening?n(8080):"EADDRINUSE"===t.code&&o.listening}),n(80)}catch{}}
@@ -1,8 +1,8 @@
1
1
  // @ctx .context/src/network/web-server.ctx
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{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{expandFile as l}from"../compact/expand.js";import{setRoots as _setRoots}from"../core/workspace.js";
3
- 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")},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"};
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{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,"..","..");function _rv(k,n){const a=o.join(p,"node_modules",n||k);if(t.existsSync(a))return a;const b=o.join(p,"..","..","node_modules",n||k);if(t.existsSync(b))return b;const c=o.join(p,"..",n||k);return t.existsSync(c)?c:a}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("symbiote",o.join("@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 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;function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null)}function P(){N()||(O(),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),h=r(m)?i(m,"utf-8"):null,f=await l(d,h),g=Math.ceil(f.original/4),u=Math.ceil(f.decompiled/4),y=h?Math.ceil(h.length/4):0,w=g+y,v=u>0?Math.round(100*(1-w/u)):0;c={code:f.code,file:e,injected:f.injected,codeTok:g,ctxTok:y,totalTok:w,expanded:u,savings:v+"%"}}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: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;const j=o.resolve(t);for(const e of f){try{u+=n(e).size;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;v+=(await l(e,t)).decompiled}catch{v+=1.3*n(e).size}}catch{continue}y++}c={files:y,codeTok:Math.ceil(u/4),ctxTok:Math.ceil(w/4),totalTok:Math.ceil((u+w)/4),expanded:Math.ceil(v/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:r,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:r});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:r});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: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(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)}),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(),await k();const t={project:j.project,skeleton:j.skeleton};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 l(e,i),r=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),p=i?Math.ceil(i.length/4):0,m=r+p,f=d>0?Math.round(100*(1-m/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:r,ctxTok:p,totalTok:m,expanded:d,savings:f+"%"}}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 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)}function P(){N()||(O(),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/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}
package/web/app.js CHANGED
@@ -11,12 +11,13 @@ import"./panels/live-monitor.js";
11
11
  import"./panels/SettingsPanel/SettingsPanel.js";
12
12
  import"./components/quick-open.js";
13
13
  export const state={skeleton:null,activeFile:null,ws:null,monitorEvents:[]};
14
+ export{formatStats}from"./stats-format.js";
14
15
  const l=new URL(".",import.meta.url).href;
15
- export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/docs":{name:"docs",args:e=>({action:"get",path:e.path,file:e.file})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//,""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
16
+ export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//, ""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
16
17
  export const events=new EventTarget;
17
18
  export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
18
19
  const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),analysis:()=>t.createSplit("horizontal",t.createPanel("health"),t.createPanel("dep-graph"),.5),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
19
- async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";d[a]&&s.setLayout(d[a]()),"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;const t=e.detail.path;t&&history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),c(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),s("skeleton",e=>{if(!e)return;state.skeleton=e;const t=new Set;for(const n of Object.values(e.n||{}))n.f&&t.add(n.f);for(const n of Object.keys(e.X||{}))t.add(n);for(const[n,o]of Object.entries(e.f||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);for(const[n,o]of Object.entries(e.a||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);const n=document.getElementById("project-files");n&&(n.textContent=`${t.size} files`),emit("skeleton-loaded",e),fetch(l+"api/compression-stats").then(e=>e.json()).then(e=>{const t=document.getElementById("compression-stats");if(t&&e.codeTok){const n=e.ctxTok?`${(e.codeTok/1e3).toFixed(1)}K + ${(e.ctxTok/1e3).toFixed(1)}K ctx = ${(e.totalTok/1e3).toFixed(1)}K tok`:`${(e.codeTok/1e3).toFixed(1)}K tok`;t.textContent=n,t.style.display=""}})}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)})}
20
+ async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";d[a]&&s.setLayout(d[a]()),"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;const t=e.detail.path;t&&history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),events.addEventListener("skeleton-loaded",e=>{const t=e.detail;if(!t)return;state.skeleton=t;const n=new Set;for(const e of Object.values(t.n||{}))e.f&&n.add(e.f);for(const e of Object.keys(t.X||{}))n.add(e);for(const[e,o]of Object.entries(t.f||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);for(const[e,o]of Object.entries(t.a||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`)}),s("skeleton",e=>{if(!e)return;state.skeleton=e;emit("skeleton-loaded",e)}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)}),c()}
20
21
  function g(e){let t=document.getElementById("agent-badge");if(!t){const e=document.querySelector(".app-topbar");if(!e)return;t=document.createElement("span"),t.id="agent-badge",t.className="agent-badge",e.appendChild(t)}t.textContent=e>0?`● ${e} agent${1!==e?"s":""}`:"",t.style.display=e>0?"":"none"}
21
22
  function f(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
22
- "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f()}):setTimeout(()=>{u(),f()},100);
23
+ "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f()}):(u(),f());
package/web/dashboard.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import{Layout as e,LayoutTree as t,applyTheme as o}from"symbiote-node";import{CARBON as r}from"./vendor/symbiote-node/themes/carbon.js";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
3
3
  function p(e){if(!e.length)return void console.warn("[dashboard] No projects to connect WebSockets for");
4
4
  const t="https:"===location.protocol?"wss://":"ws://",o=location.host;for(const r of e)i(r,t,o)}
5
- function i(e,t,o){const r=`${t}${o}${e.prefix}/ws/monitor`,n=new WebSocket(r);n.onopen=()=>{console.log("[dashboard] WS connected:",e.projectName)},n.onmessage=t=>{let o;try{o=JSON.parse(t.data)}catch{return}if("snapshot"===o.method&&o.params?.state){const t=o.params.state,r=a.projects.find(t=>t.prefix===e.prefix);return void(r&&t.project&&(Object.assign(r,{projectName:t.project.name,projectPath:t.project.path,color:t.project.color,agents:t.project.agents,pid:t.project.pid,connected:!0}),s("projects-updated",a.projects)))}if("patch"===o.method&&o.params){const t=a.projects.find(t=>t.prefix===e.prefix);return void(t&&"project.agents"===o.params.path&&(t.agents=o.params.value,s("projects-updated",a.projects)))}if("event"===o.method&&o.params){const t=o.params;return t._projectPrefix=e.prefix,t._projectName=e.projectName,a.events.push(t),a.events.length>1e3&&a.events.shift(),void s("global-tool-event",t)}o.type&&(o._projectPrefix=e.prefix,o._projectName=e.projectName,a.events.push(o),a.events.length>1e3&&a.events.shift(),s("global-tool-event",o))},n.onerror=()=>{console.error("[dashboard] WS error:",e.projectName)},n.onclose=r=>{console.warn("[dashboard] WS closed:",e.projectName,r.code);
6
- const n=a.projects.find(t=>t.prefix===e.prefix);n&&(n.connected=!1,s("projects-updated",a.projects)),setTimeout(()=>i(e,t,o),5e3)}}const d={"project-list":{title:"Projects",icon:"dashboard",component:"pg-project-list"},"action-board":{title:"Action Board",icon:"monitor_heart",component:"pg-action-board"}};async function l(){o(document.documentElement,r);
5
+ function i(e,t,o,_att=0){const r=`${t}${o}${e.prefix}/ws/monitor`,n=new WebSocket(r);n.onopen=()=>{_att=0;console.log("[dashboard] WS connected:",e.projectName)},n.onmessage=t=>{let o;try{o=JSON.parse(t.data)}catch{return}if("snapshot"===o.method&&o.params?.state){const t=o.params.state,r=a.projects.find(t=>t.prefix===e.prefix);return void(r&&t.project&&(Object.assign(r,{projectName:t.project.name,projectPath:t.project.path,color:t.project.color,agents:t.project.agents,pid:t.project.pid,connected:!0}),s("projects-updated",a.projects)))}if("patch"===o.method&&o.params){const t=a.projects.find(t=>t.prefix===e.prefix);return void(t&&"project.agents"===o.params.path&&(t.agents=o.params.value,s("projects-updated",a.projects)))}if("event"===o.method&&o.params){const t=o.params;return t._projectPrefix=e.prefix,t._projectName=e.projectName,a.events.push(t),a.events.length>1e3&&a.events.shift(),void s("global-tool-event",t)}o.type&&(o._projectPrefix=e.prefix,o._projectName=e.projectName,a.events.push(o),a.events.length>1e3&&a.events.shift(),s("global-tool-event",o))},n.onerror=()=>{console.error("[dashboard] WS error:",e.projectName)},n.onclose=r=>{console.warn("[dashboard] WS closed:",e.projectName,r.code);
6
+ const n=a.projects.find(t=>t.prefix===e.prefix);n&&(n.connected=!1,s("projects-updated",a.projects)),setTimeout(()=>i(e,t,o,_att+1),Math.min(500*Math.pow(2,_att),3e4))}}const d={"project-list":{title:"Projects",icon:"dashboard",component:"pg-project-list"},"action-board":{title:"Action Board",icon:"monitor_heart",component:"pg-action-board"}};async function l(){o(document.documentElement,r);
7
7
  const e=document.querySelector(".app-workspace").querySelector(".app-content"),n=document.createElement("panel-layout");n.setAttribute("storage-key","pg-dashboard-layout"),n.setAttribute("min-panel-size","200"),n.id="dashboard-layout",e.appendChild(n),requestAnimationFrame(async()=>{for(const[e,t]of Object.entries(d))n.registerPanelType(e,t);localStorage.getItem("pg-dashboard-layout")||n.setLayout(t.createSplit("vertical",t.createPanel("project-list"),t.createPanel("action-board"),.3));
8
8
  const e=await c();a.projects=Object.entries(e.routes||{}).map(([e,t])=>({prefix:e,...t,connected:!1,agents:0})),s("projects-updated",a.projects),p(a.projects)})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):setTimeout(l,100);
@@ -1,4 +1,4 @@
1
1
  // @ctx .context/web/panels/ProjectItem/ProjectItem.ctx
2
2
  import e from"@symbiotejs/symbiote";import t from"./ProjectItem.css.js";import r from"./ProjectItem.tpl.js";
3
- export class ProjectItem extends e{init$={prefix:"",projectName:"",projectPath:""};renderCallback(){this.sub("prefix",e=>{this.ref.link.href=e?`${e}/`:"#";if(e){fetch(`${e}/api/compression-stats`).then(r=>r.json()).then(r=>{if(r.codeTok&&this.ref.tokenBadge){const c=(r.codeTok/1e3).toFixed(1),x=(r.ctxTok||0)/1e3,exp=r.expanded||0,expK=(exp/1e3).toFixed(1),codePct=exp>0?Math.round(100*(1-r.codeTok/exp)):0,totalPct=exp>0?Math.round(100*(1-(r.codeTok+(r.ctxTok||0))/exp)):0,fmt=v=>v>=0?`↓${v}%`:`↑${Math.abs(v)}%`;let t=r.ctxTok?`${c}K (${fmt(codePct)}) + ${x.toFixed(1)}K ctx`:`${c}K tok (${fmt(codePct)})`;if(exp>0){t+=r.ctxTok?` = ${((r.codeTok+r.ctxTok)/1e3).toFixed(1)}K (${fmt(totalPct)}) of ${expK}K`:`of ${expK}K`}this.ref.tokenBadge.textContent=t}}).catch(()=>{})}});this.ref.deleteBtn.addEventListener("click",async(ev)=>{ev.preventDefault();ev.stopPropagation();const prefix=this.$.prefix;if(!prefix||!confirm(`Remove ${this.$.projectName}?`))return;await fetch("/api/remove-project",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({route:prefix})});this.remove()})}}
3
+ export class ProjectItem extends e{init$={prefix:"",projectName:"",projectPath:""};renderCallback(){this.sub("prefix",e=>{this.ref.link.href=e?`${e}/`:"#";});this.ref.deleteBtn.addEventListener("click",async(ev)=>{ev.preventDefault();ev.stopPropagation();const prefix=this.$.prefix;if(!prefix||!confirm(`Remove ${this.$.projectName}?`))return;await fetch("/api/remove-project",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({route:prefix})});this.remove()})}}
4
4
  ProjectItem.template=r,ProjectItem.rootStyles=t,ProjectItem.reg("pg-project-item");
@@ -1,6 +1,4 @@
1
1
  // @ctx .context/web/panels/code-viewer.ctx
2
- import e from"@symbiotejs/symbiote";import{api as n,events as t,state as o}from"../app.js";import"../components/code-block.js";
3
- export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"compact",statsText:"",onToggleMode:()=>{this.$.viewMode="compact"===this.$.viewMode?"raw":"compact",this._showCurrentMode()}};_fileData=null;initCallback(){t.addEventListener("file-selected",e=>this._loadFile(e.detail.path))}renderCallback(){this.sub("hasFile",e=>{this.toggleAttribute("has-file",e)}),this.sub("viewMode",e=>{this.toggleAttribute("mode-raw","raw"===e)})}_getCodeBlock(){return this.querySelector("code-block")}_showCurrentMode(){if(!this._fileData)return;
4
- const e=this._getCodeBlock();e&&(e.$.code="compact"===this.$.viewMode?this._fileData.compact:this._fileData.raw)}async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this.$.viewMode="compact";try{const t=await n("/api/file",{path:e}),o="string"==typeof t.code?t.code:"string"==typeof t.compressed?t.compressed:t.content||JSON.stringify(t,null,2);
5
- let s=o;try{const t=await n("/api/raw-file",{path:e});t?.content&&(s=t.content)}catch{}this._fileData={compact:o,raw:s,codeTok:t.codeTok||0,ctxTok:t.ctxTok||0,totalTok:t.totalTok||0,expanded:t.expanded||0,savings:t.savings||"0%"},t.codeTok&&t.expanded&&(()=>{const e=t.expanded,n=e>0?Math.round(100*(1-t.codeTok/e)):0,o=e>0?Math.round(100*(1-t.totalTok/e)):0;const fmt=v=>v>=0?`↓${v}%`:`↑${Math.abs(v)}%`;this.$.statsText=t.ctxTok?`${t.codeTok} (${fmt(n)}) + ${t.ctxTok} ctx = ${t.totalTok} (${fmt(o)}) → ${e} tok`:`${t.codeTok} → ${e} tok (${fmt(n)})`})();
6
- const i=this._getCodeBlock();i&&(i.$.code=o),this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.code=`// Error: ${e.message}`),this.$.hasFile=!0}}}CodeViewer.template='\n <div class="pg-code-header">\n <span class="pg-code-filename" bind="textContent: filename"></span>\n <div class="pg-code-controls">\n <span class="pg-code-stats" bind="textContent: statsText"></span>\n <button class="pg-mode-toggle" bind="onclick: onToggleMode" title="Toggle Compact/Raw view">\n <span class="material-symbols-outlined" style="font-size:14px">compress</span>\n <span class="pg-mode-label" bind="textContent: viewMode"></span>\n </button>\n </div>\n </div>\n <code-block></code-block>\n',CodeViewer.rootStyles="\n pg-code-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n pg-code-viewer:not([has-file]) code-block {\n display: none;\n }\n .pg-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n background: var(--sn-node-header-bg, hsl(37, 25%, 93%));\n gap: 8px;\n }\n .pg-code-filename {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n .pg-code-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n .pg-code-stats {\n font-size: 10px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n white-space: nowrap;\n }\n .pg-mode-toggle {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 4px;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-family: inherit;\n font-size: 10px;\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n transition: all 120ms ease;\n }\n .pg-mode-toggle:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-code-viewer[mode-raw] .pg-mode-toggle {\n background: hsla(210, 45%, 45%, 0.12);\n border-color: var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n",CodeViewer.reg("pg-code-viewer");
2
+ import e from"@symbiotejs/symbiote";import{api as n,events as t,state as o,formatStats}from"../app.js";import"../components/code-block.js";
3
+ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"compact",modeLabel:"compact",statsText:"",onToggleMode:()=>{this.$.viewMode="compact"===this.$.viewMode?"raw":"compact",this._showCurrentMode()}};_fileData=null;_isReadable=!1;_compactCache=null;_loadingCompact=!1;_currentPath=null;initCallback(){t.addEventListener("file-selected",e=>this._loadFile(e.detail.path))}renderCallback(){this.sub("hasFile",e=>{this.toggleAttribute("has-file",e)}),this.sub("viewMode",e=>{this.toggleAttribute("mode-raw","raw"===e),this.$.modeLabel=this._isReadable?("raw"===e?"source":"compact"):("raw"===e?"source":"expanded")})}_getCodeBlock(){return this.querySelector("code-block")}async _showCurrentMode(){if(!this._fileData)return;const e=this._getCodeBlock();if(!e)return;if("compact"===this.$.viewMode){if(this._isReadable){if(this._compactCache){e.$.code=this._compactCache;return}if(this._loadingCompact)return;this._loadingCompact=!0;e.$.code="// Compressing...";try{const t=await n("/api/compact-file",{path:this._currentPath});this._compactCache=t?.code||"// Compression unavailable";e.$.code=this._compactCache}catch{e.$.code="// Compression failed"}finally{this._loadingCompact=!1}return}e.$.code=this._fileData.compact}else e.$.code=this._fileData.raw}async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this._compactCache=null,this._currentPath=e;try{const[t,_raw]=await Promise.all([n("/api/file",{path:e}),n("/api/raw-file",{path:e}).catch(()=>null)]);const o="string"==typeof t.code?t.code:"string"==typeof t.compressed?t.compressed:t.content||JSON.stringify(t,null,2);
4
+ let s=_raw?.content||o;this._isReadable=!!(t.expanded&&t.codeTok&&t.codeTok<t.expanded*.85),this._fileData={compact:o,raw:s,codeTok:t.codeTok||0,ctxTok:t.ctxTok||0,totalTok:t.totalTok||0,expanded:t.expanded||0,savings:t.savings||"0%"},t.codeTok&&t.expanded&&(this.$.statsText=formatStats(t));const i=this._getCodeBlock();if(this._isReadable){this.$.viewMode="raw",this.$.modeLabel="source",i&&(i.$.code=s)}else{this.$.viewMode="compact",this.$.modeLabel="expanded",i&&(i.$.code=o)}this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.code=`// Error: ${e.message}`),this.$.hasFile=!0}}}CodeViewer.template='\n <div class="pg-code-header">\n <span class="pg-code-filename" bind="textContent: filename"></span>\n <div class="pg-code-controls">\n <span class="pg-code-stats" bind="textContent: statsText"></span>\n <button class="pg-mode-toggle" bind="onclick: onToggleMode" title="Toggle Compact/Source view">\n <span class="material-symbols-outlined" style="font-size:14px">compress</span>\n <span class="pg-mode-label" bind="textContent: modeLabel"></span>\n </button>\n </div>\n </div>\n <code-block></code-block>\n',CodeViewer.rootStyles="\n pg-code-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n pg-code-viewer:not([has-file]) code-block {\n display: none;\n }\n .pg-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n background: var(--sn-node-header-bg, hsl(37, 25%, 93%));\n gap: 8px;\n }\n .pg-code-filename {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n .pg-code-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n .pg-code-stats {\n font-size: 10px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n white-space: nowrap;\n }\n .pg-mode-toggle {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 4px;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-family: inherit;\n font-size: 10px;\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n transition: all 120ms ease;\n }\n .pg-mode-toggle:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-code-viewer[mode-raw] .pg-mode-toggle {\n background: hsla(210, 45%, 45%, 0.12);\n border-color: var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n",CodeViewer.reg("pg-code-viewer");
@@ -1,4 +1,4 @@
1
1
  // @ctx .context/web/panels/file-tree.ctx
2
2
  import e from"@symbiotejs/symbiote";import{api as t,state as n,events as s,emit as r}from"../app.js";
3
- export class FileTree extends e{init$={treeHTML:'<div class="pg-placeholder">Loading files...</div>',filterText:"",onFilterInput:e=>{this.$.filterText=e.target.value.toLowerCase(),this._applyFilter()},onCollapseAll:()=>{this._collapseAll()}};initCallback(){this._expandedDirs=new Set;try{const e=localStorage.getItem("pg-tree-expanded");if(e){const t=JSON.parse(e);Array.isArray(t)&&(this._expandedDirs=new Set(t))}}catch(e){}s.addEventListener("skeleton-loaded",e=>{this._renderTree(e.detail),n.activeFile&&requestAnimationFrame(()=>this._highlightFile(n.activeFile))}),n.skeleton&&this._renderTree(n.skeleton),s.addEventListener("file-selected",e=>{e.detail.fromRoute&&requestAnimationFrame(()=>this._highlightFile(e.detail.path))}),this.addEventListener("click",e=>{const t=e.target.closest(".pg-tree-file");if(t)return this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active"),n.activeFile=t.dataset.file,void r("file-selected",{path:t.dataset.file});const s=e.target.closest(".pg-tree-dir");if(s){const e=s.dataset.dir;null!=e&&this._toggleDir(e)}})}_toggleDir(e){this._expandedDirs.has(e)?this._expandedDirs.delete(e):this._expandedDirs.add(e),this._saveExpandedState(),this._updateDirDOM(e)}_saveExpandedState(){localStorage.setItem("pg-tree-expanded",JSON.stringify(Array.from(this._expandedDirs)))}_updateDirDOM(e){const t=this.querySelector(`.pg-tree-dir[data-dir="${CSS.escape(e)}"]`),n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(e)}"]`);if(t&&n){const s=this._expandedDirs.has(e),r=t.querySelector(".pg-chevron");r&&(r.textContent=s?"expand_more":"chevron_right"),s?n.removeAttribute("hidden"):n.setAttribute("hidden","")}}_collapseAll(){this._expandedDirs.clear(),this._saveExpandedState(),this.querySelectorAll(".pg-tree-dir").forEach(e=>{this._updateDirDOM(e.dataset.dir)})}_highlightFile(e){const t=this.querySelector(`.pg-tree-file[data-file="${CSS.escape(e)}"]`);if(t){this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active");const n=e.split("/");n.pop();let s=!1;for(let e=1;e<=n.length;e++){const t=n.slice(0,e).join("/");this._expandedDirs.has(t)||(this._expandedDirs.add(t),this._updateDirDOM(t),s=!0)}s&&this._saveExpandedState(),t.scrollIntoView({block:"center",behavior:"smooth"})}}_renderTree(e){if(!e)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');const t=new Map,n=e.n||{};for(const e of Object.values(n))if(e.f){const n=t.get(e.f)||{exports:0,classes:0};n.classes++,t.set(e.f,n)}const s=e.X||{};for(const[e,n]of Object.entries(s)){const s=t.get(e)||{exports:0,classes:0};s.exports=n.length,t.set(e,s)}const r=e.f||{};for(const[e,n]of Object.entries(r))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0})}const i=e.a||{};for(const[e,n]of Object.entries(i))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0,nonSource:!0})}if(0===t.size)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');const l={children:{},files:[]};for(const[e,n]of t){const t=e.split("/"),s=t.pop();let r=l;for(const e of t)r.children[e]||(r.children[e]={children:{},files:[]}),r=r.children[e];r.files.push({f:e,name:s,...n})}const o=(e,t,n)=>{const s=[],r=Object.keys(e.children).sort(),i=e.files.sort((e,t)=>e.name.localeCompare(t.name)),l=16*n;for(const i of r){const r=t?`${t}/${i}`:i,a=this._expandedDirs&&this._expandedDirs.has(r),d=a?"expand_more":"chevron_right",p=a?"":" hidden";s.push(`<div class="pg-tree-dir" data-dir="${r}" style="padding-left:${l+6}px"><span class="material-symbols-outlined pg-chevron" style="font-size:16px">${d}</span> <span class="material-symbols-outlined" style="font-size:16px">folder</span> ${i}</div>`),s.push(`<div class="pg-tree-children" data-dir="${r}"${p}>`),s.push(o(e.children[i],r,n+1)),s.push("</div>")}for(const e of i){const t=FileTree._getFileIcon(e.name),n=[];e.exports>0&&n.push(`${e.exports}f`),e.classes>0&&n.push(`${e.classes}c`);const r=n.length>0?`<span class="pg-badge">${n.join(" ")}</span>`:"",i=e.nonSource?" pg-non-source":"";s.push(`<div class="pg-tree-file${i}" data-file="${e.f}" style="padding-left:${l+24}px"><span class="material-symbols-outlined" style="font-size:14px">${t}</span> ${e.name}${r}</div>`)}return s.join("")};this.$.treeHTML=o(l,"",0)}static _getFileIcon(e){return e.endsWith(".html")?"html":e.endsWith(".css")||e.endsWith(".css.js")?"css":e.endsWith(".tpl.js")?"web":e.endsWith(".json")?"data_object":e.endsWith(".md")?"description":e.endsWith(".svg")||e.endsWith(".png")||e.endsWith(".jpg")?"image":e.endsWith(".woff2")||e.endsWith(".ttf")?"font_download":"insert_drive_file"}_applyFilter(){const e=this.$.filterText;let t=!1;this.querySelectorAll(".pg-tree-file").forEach(n=>{const s=!e||n.dataset.file.toLowerCase().includes(e);if(n.hidden=!s,e&&s){const e=n.dataset.file.split("/");e.pop();for(let n=1;n<=e.length;n++){const s=e.slice(0,n).join("/");this._expandedDirs.has(s)||(this._expandedDirs.add(s),t=!0,this._updateDirDOM(s))}}}),t&&this._saveExpandedState(),e?this.querySelectorAll(".pg-tree-dir").forEach(e=>{const t=e.dataset.dir,n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(t)}"]`);if(!n)return;let s=!1;n.querySelectorAll(".pg-tree-file").forEach(e=>{e.hidden||(s=!0)}),n.querySelectorAll(".pg-tree-children").forEach(e=>{e.querySelector(".pg-tree-file:not([hidden])")&&(s=!0)}),e.hidden=!s}):this.querySelectorAll(".pg-tree-dir").forEach(e=>{e.hidden=!1})}}
3
+ export class FileTree extends e{init$={treeHTML:'<div class="pg-placeholder">Loading files...</div>',filterText:"",onFilterInput:e=>{this.$.filterText=e.target.value.toLowerCase(),this._applyFilter()},onCollapseAll:()=>{this._collapseAll()}};_treeData=null;initCallback(){this._expandedDirs=new Set;try{const e=localStorage.getItem("pg-tree-expanded");if(e){const t=JSON.parse(e);Array.isArray(t)&&(this._expandedDirs=new Set(t))}}catch(e){}s.addEventListener("skeleton-loaded",e=>{this._renderTree(e.detail),n.activeFile&&requestAnimationFrame(()=>this._highlightFile(n.activeFile))}),n.skeleton?this._renderTree(n.skeleton):this._fetchSkeleton(),s.addEventListener("file-selected",e=>{e.detail.fromRoute&&requestAnimationFrame(()=>this._highlightFile(e.detail.path))}),this.addEventListener("click",e=>{const t=e.target.closest(".pg-tree-file");if(t)return this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active"),n.activeFile=t.dataset.file,void r("file-selected",{path:t.dataset.file});const s=e.target.closest(".pg-tree-dir");if(s){const e=s.dataset.dir;null!=e&&this._toggleDir(e)}})}async _fetchSkeleton(){try{const e=await t("/api/skeleton",{});n.skeleton||(n.skeleton=e),this._renderTree(e),r("skeleton-loaded",e),n.activeFile&&requestAnimationFrame(()=>this._highlightFile(n.activeFile))}catch(e){}}_toggleDir(e){this._expandedDirs.has(e)?this._expandedDirs.delete(e):this._expandedDirs.add(e),this._saveExpandedState(),this._updateDirDOM(e)}_saveExpandedState(){localStorage.setItem("pg-tree-expanded",JSON.stringify(Array.from(this._expandedDirs)))}_lazyRenderChildren(e){const t=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(e)}"]`);if(!t||t.dataset.rendered)return;if(!this._treeData)return;const n=e.split("/");let s=this._treeData;for(const e of n){if(!s||!s.children[e]){s=null;break}s=s.children[e]}if(!s)return;t.innerHTML=this._renderNode(s,e,e.split("/").length);t.dataset.rendered="1"}_updateDirDOM(e){const t=this.querySelector(`.pg-tree-dir[data-dir="${CSS.escape(e)}"]`),n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(e)}"]`);if(t&&n){const s=this._expandedDirs.has(e),r=t.querySelector(".pg-chevron");r&&(r.textContent=s?"expand_more":"chevron_right"),s?(this._lazyRenderChildren(e),n.removeAttribute("hidden")):n.setAttribute("hidden","")}}_collapseAll(){this._expandedDirs.clear(),this._saveExpandedState(),this.querySelectorAll(".pg-tree-dir").forEach(e=>{this._updateDirDOM(e.dataset.dir)})}_highlightFile(e){let t=this.querySelector(`.pg-tree-file[data-file="${CSS.escape(e)}"]`);if(!t){const n=e.split("/");n.pop();for(let e=1;e<=n.length;e++){const t=n.slice(0,e).join("/");this._expandedDirs.has(t)||(this._expandedDirs.add(t),this._lazyRenderChildren(t),this._updateDirDOM(t))}this._saveExpandedState(),t=this.querySelector(`.pg-tree-file[data-file="${CSS.escape(e)}"]`)}if(t){this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active"),t.scrollIntoView({block:"center",behavior:"smooth"})}}_buildTree(e){const t=new Map,n=e.n||{};for(const e of Object.values(n))if(e.f){const n=t.get(e.f)||{exports:0,classes:0};n.classes++,t.set(e.f,n)}const s=e.X||{};for(const[e,n]of Object.entries(s)){const s=t.get(e)||{exports:0,classes:0};s.exports=n.length,t.set(e,s)}const r=e.f||{};for(const[e,n]of Object.entries(r))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0})}const i=e.a||{};for(const[e,n]of Object.entries(i))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0,nonSource:!0})}const l={children:{},files:[]};for(const[e,n]of t){const t=e.split("/"),s=t.pop();let r=l;for(const e of t)r.children[e]||(r.children[e]={children:{},files:[]}),r=r.children[e];r.files.push({f:e,name:s,...n})}return l}_renderNode(e,t,n){const s=[],r=Object.keys(e.children).sort(),i=e.files.sort((e,t)=>e.name.localeCompare(t.name)),l=16*n;for(const i of r){const r=t?`${t}/${i}`:i,a=this._expandedDirs&&this._expandedDirs.has(r),d=a?"expand_more":"chevron_right",p=a?"":" hidden";s.push(`<div class="pg-tree-dir" data-dir="${r}" style="padding-left:${l+6}px"><span class="material-symbols-outlined pg-chevron" style="font-size:16px">${d}</span> <span class="material-symbols-outlined" style="font-size:16px">folder</span> ${i}</div>`);if(a){s.push(`<div class="pg-tree-children" data-dir="${r}" data-rendered="1">`);s.push(this._renderNode(e.children[i],r,n+1));s.push("</div>")}else{s.push(`<div class="pg-tree-children" data-dir="${r}" hidden></div>`)}}for(const e of i){const t=FileTree._getFileIcon(e.name),n=[];e.exports>0&&n.push(`${e.exports}f`),e.classes>0&&n.push(`${e.classes}c`);const r=n.length>0?`<span class="pg-badge">${n.join(" ")}</span>`:"",i=e.nonSource?" pg-non-source":"";s.push(`<div class="pg-tree-file${i}" data-file="${e.f}" style="padding-left:${l+24}px"><span class="material-symbols-outlined" style="font-size:14px">${t}</span> ${e.name}${r}</div>`)}return s.join("")}_renderTree(e){if(!e)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');this._treeData=this._buildTree(e);const t=Object.keys(this._treeData.children).length+this._treeData.files.length;if(0===t)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');this.$.treeHTML=this._renderNode(this._treeData,"",0)}static _getFileIcon(e){return e.endsWith(".html")?"html":e.endsWith(".css")||e.endsWith(".css.js")?"css":e.endsWith(".tpl.js")?"web":e.endsWith(".json")?"data_object":e.endsWith(".md")?"description":e.endsWith(".svg")||e.endsWith(".png")||e.endsWith(".jpg")?"image":e.endsWith(".woff2")||e.endsWith(".ttf")?"font_download":"insert_drive_file"}_applyFilter(){const e=this.$.filterText;if(e){this._expandAllForFilter();this.querySelectorAll(".pg-tree-file").forEach(t=>{const n=t.dataset.file.toLowerCase().includes(e);t.hidden=!n}),this.querySelectorAll(".pg-tree-dir").forEach(e=>{const t=e.dataset.dir,n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(t)}"]`);if(!n)return;let s=!1;n.querySelectorAll(".pg-tree-file:not([hidden])").forEach(()=>s=!0),e.hidden=!s})}else{this.querySelectorAll(".pg-tree-file").forEach(e=>e.hidden=!1),this.querySelectorAll(".pg-tree-dir").forEach(e=>e.hidden=!1)}}_expandAllForFilter(){if(!this._treeData)return;const e=(t,n)=>{for(const s of Object.keys(t.children)){const r=n?`${n}/${s}`:s;this._lazyRenderChildren(r);const i=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(r)}"]`);i&&i.removeAttribute("hidden");e(t.children[s],r)}};e(this._treeData,"")}}
4
4
  FileTree.template='\n <div class="pg-panel-toolbar">\n <input type="search" placeholder="Filter files..." bind="oninput: onFilterInput">\n <button class="pg-collapse-all" bind="onclick: onCollapseAll" title="Collapse All Folders">\n <span class="material-symbols-outlined" style="font-size:14px">unfold_less</span>\n </button>\n </div>\n <div class="pg-tree-content" bind="innerHTML: treeHTML"></div>\n',FileTree.rootStyles="\n pg-file-tree {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n font-size: 12px;\n font-family: var(--sn-font, Georgia, serif);\n }\n pg-file-tree .pg-panel-toolbar {\n padding: 6px 8px;\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n display: flex;\n gap: 6px;\n }\n pg-file-tree .pg-panel-toolbar input {\n flex: 1;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-family: inherit;\n outline: none;\n min-width: 0;\n }\n pg-file-tree .pg-panel-toolbar input:focus {\n border-color: var(--sn-node-selected, hsl(210, 55%, 42%));\n }\n pg-file-tree .pg-collapse-all {\n background: var(--sn-bg, hsl(37, 30%, 91%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 6px;\n transition: all 100ms ease;\n }\n pg-file-tree .pg-collapse-all:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n }\n pg-file-tree .pg-tree-content {\n flex: 1;\n overflow-y: auto;\n padding: 4px;\n }\n pg-file-tree .pg-tree-dir {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 3px 6px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-weight: 600;\n font-size: 11px;\n cursor: pointer;\n user-select: none;\n }\n pg-file-tree .pg-tree-dir:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n border-radius: 4px;\n }\n pg-file-tree .pg-tree-dir .pg-chevron {\n transition: transform 150ms ease;\n }\n pg-file-tree .pg-tree-children[hidden] {\n display: none;\n }\n pg-file-tree .pg-tree-file {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 3px 6px 3px 24px;\n cursor: pointer;\n border-radius: 4px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n transition: all 100ms ease;\n }\n pg-file-tree .pg-tree-file:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-file-tree .pg-tree-file.active {\n background: hsla(210, 45%, 45%, 0.12);\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n pg-file-tree .pg-tree-file[hidden] {\n display: none;\n }\n pg-file-tree .pg-tree-file.pg-non-source {\n opacity: 0.6;\n }\n pg-file-tree .pg-badge {\n margin-left: auto;\n font-size: 10px;\n padding: 0 5px;\n border-radius: 8px;\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n }\n",FileTree.reg("pg-file-tree");
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Format compression stats for display.
3
+ * Always shows savings vs expanded (beautified) baseline.
4
+ * @param {{codeTok:number, ctxTok:number, totalTok:number, expanded:number}} s
5
+ * @param {"full"|"short"} mode
6
+ */
7
+ export function formatStats({codeTok=0,ctxTok=0,totalTok=0,expanded=0}={},mode="full"){
8
+ const _k=v=>(v/1e3).toFixed(1)+"K";
9
+ const total=totalTok||(codeTok+(ctxTok||0));
10
+ const pct=expanded>0?Math.round(100*(1-total/expanded)):0;
11
+ const dir=pct>=0?`↓${pct}%`:`↑${Math.abs(pct)}%`;
12
+ if(mode==="short")return`${_k(total)} of ${_k(expanded)} (${dir})`;
13
+ const compact=ctxTok?`${_k(codeTok)} + ${_k(ctxTok)} ctx = ${_k(total)}`:`${_k(codeTok)}`;
14
+ return`${_k(expanded)} source → ${compact} compact (${dir})`;
15
+ }