project-graph-mcp 2.2.2 → 2.2.4

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.2.2",
3
+ "version": "2.2.4",
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",
@@ -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{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{setRoots as _setRoots}from"../core/workspace.js";
2
+ import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{expandFile as _ef}from"../compact/expand.js";import{setRoots as _setRoots}from"../core/workspace.js";
3
3
  const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);let _pkgVersion="0.0.0";try{_pkgVersion=JSON.parse(t.readFileSync(o.join(p,"package.json"),"utf8")).version}catch{}function _rv(k){try{const r=_rq.resolve(k);const marker=o.sep+"node_modules"+o.sep+k.replace(/\//g,o.sep);const idx=r.lastIndexOf(marker);if(idx>=0)return r.substring(0,idx+marker.length);return o.dirname(r)}catch{return o.join(p,"node_modules",...k.split("/"))}}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
4
4
  function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
5
5
  function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
6
6
  function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
7
7
  function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
8
- export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const 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{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}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/image":{const e=a.get("path");if(e){try{const{resolve:n,existsSync:a,readFileSync:r}=await import("fs"),{extname:s}=await import("path"),d=n(t,e);if(!a(d)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[s(e).toLowerCase()]||"application/octet-stream";const buf=r(d);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const 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{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}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 v${_pkgVersion}`),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 v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
8
+ export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const 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{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}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/expand-file":{const e=a.get("path");if(e){try{const _pt2=await import("path"),_fs2=await import("fs"),_abs=_pt2.resolve(t,e),_ext3=_pt2.extname(e).toLowerCase(),_jsExts3=new Set([".js",".mjs",".ts",".tsx"]);if(!_jsExts3.has(_ext3)){const raw=_fs2.readFileSync(_abs,"utf-8");c={code:raw,file:e,injected:0};break}let _ctxContent=null;const _ctxName=_pt2.basename(e,_pt2.extname(e))+".ctx";const _ctxPath1=_pt2.join(t,_pt2.dirname(e),_ctxName);const _ctxPath2=_pt2.join(t,".context",_pt2.dirname(e),_ctxName);if(_fs2.existsSync(_ctxPath1))_ctxContent=_fs2.readFileSync(_ctxPath1,"utf-8");else if(_fs2.existsSync(_ctxPath2))_ctxContent=_fs2.readFileSync(_ctxPath2,"utf-8");const _result=await _ef(_abs,_ctxContent);c={code:_result.code,file:e,injected:_result.injected,original:_result.original,decompiled:_result.decompiled}}catch(err){c={code:`// Expand failed: ${err.message}`,file:e}}}else c={code:"// No file specified",file:""};break}case"/api/image":{const e=a.get("path");if(e){try{const _fs=await import("fs"),_pt=await import("path"),_imgPath=_pt.resolve(t,e);if(!_fs.existsSync(_imgPath)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[_pt.extname(e).toLowerCase()]||"application/octet-stream";const buf=_fs.readFileSync(_imgPath);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const 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{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}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 v${_pkgVersion}`),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 v${_pkgVersion}`),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
@@ -12,7 +12,7 @@ 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
14
  export{formatStats}from"./stats-format.js";
15
- const l=new URL(".",import.meta.url).href;
15
+ export const baseUrl=new URL(".",import.meta.url).href;const l=baseUrl;
16
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()}
17
17
  export const events=new EventTarget;
18
18
  export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
@@ -1,10 +1,19 @@
1
1
  // @ctx .context/web/components/code-block.ctx
2
- import o from"@symbiotejs/symbiote";import{highlight as n,renderMarkdown,highlightSQL,highlightPlain}from"../highlight.js";
2
+ import o from"@symbiotejs/symbiote";import{highlight as n,renderMarkdown,highlightSQL,highlightPlain}from"../highlight.js";import{baseUrl}from"../app.js";
3
3
 
4
- export class CodeBlock extends o{init$={code:"",lang:"js",highlighted:"",lineNums:"",isMarkdown:false};renderCallback(){
4
+ export class CodeBlock extends o{init$={code:"",lang:"js",highlighted:"",lineNums:"",isMarkdown:false,isImage:false,imageSrc:""};renderCallback(){
5
5
  this.sub("code",o=>{
6
6
  if(!o)return this.$.highlighted="",void(this.$.lineNums="");
7
7
  const lang=this.$.lang;
8
+ if(lang==="image"){
9
+ this.$.isMarkdown=false;
10
+ this.$.isImage=true;
11
+ this.$.imageSrc=`${baseUrl}api/image?path=${encodeURIComponent(o)}`;
12
+ this.$.highlighted="";
13
+ this.$.lineNums="";
14
+ return;
15
+ }
16
+ this.$.isImage=false;
8
17
  if(lang==="md"||lang==="markdown"){
9
18
  this.$.isMarkdown=true;
10
19
  this.$.highlighted=renderMarkdown(o,this._basePath||"");
@@ -19,7 +28,7 @@ else hl=n(o);
19
28
  this.$.highlighted=hl;
20
29
  const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n");
21
30
  }
22
- }),this.sub("isMarkdown",v=>{this.toggleAttribute("mode-markdown",v)})
31
+ }),this.sub("isMarkdown",v=>{this.toggleAttribute("mode-markdown",v)}),this.sub("isImage",v=>{this.toggleAttribute("mode-image",v)})
23
32
  }setBasePath(p){this._basePath=p}}
24
- CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n <div class="cb-md" bind="innerHTML: highlighted"></div>\n </div>\n';
25
- CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Markdown container — hidden by default */\n code-block .cb-md {\n display: none;\n padding: 20px 28px;\n flex: 1;\n min-width: 0;\n overflow-wrap: break-word;\n word-wrap: break-word;\n line-height: 1.7;\n color: var(--sn-text, hsl(30, 15%, 18%));\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n font-size: 14px;\n }\n /* In markdown mode: hide code, show md */\n code-block[mode-markdown] .cb-gutter { display: none; }\n code-block[mode-markdown] .cb-pre { display: none; }\n code-block[mode-markdown] .cb-md { display: block; }\n\n /* Markdown styles */\n code-block .md-h { margin: 20px 0 8px; color: var(--sn-text, #222); font-weight: 700; }\n code-block h1.md-h { font-size: 24px; border-bottom: 2px solid var(--sn-node-border, #ddd); padding-bottom: 8px; }\n code-block h2.md-h { font-size: 20px; border-bottom: 1px solid var(--sn-node-border, #ddd); padding-bottom: 6px; }\n code-block h3.md-h { font-size: 16px; }\n code-block h4.md-h { font-size: 14px; }\n code-block .md-p { margin: 8px 0; }\n code-block .md-quote {\n margin: 8px 0;\n padding: 8px 16px;\n border-left: 4px solid var(--sn-cat-server, hsl(210, 45%, 55%));\n background: hsla(210, 40%, 55%, 0.08);\n border-radius: 0 4px 4px 0;\n font-style: italic;\n }\n code-block .md-list {\n margin: 8px 0;\n padding-left: 24px;\n }\n code-block .md-list li {\n margin: 3px 0;\n }\n code-block .md-code-block {\n margin: 12px 0;\n padding: 12px 16px;\n background: var(--sn-bg, hsl(37, 30%, 94%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n border-radius: 6px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n }\n code-block .md-inline-code {\n padding: 1px 5px;\n background: hsla(30, 20%, 50%, 0.12);\n border-radius: 3px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 0.9em;\n }\n code-block .md-link {\n color: var(--sn-cat-server, hsl(210, 55%, 50%));\n text-decoration: underline;\n text-decoration-style: dotted;\n }\n code-block .md-link:hover { text-decoration-style: solid; }\n code-block .md-img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n margin: 8px 0;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n }\n code-block .md-hr {\n border: none;\n border-top: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n margin: 16px 0;\n }\n code-block .md-table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n font-size: 13px;\n }\n code-block .md-table th,\n code-block .md-table td {\n padding: 6px 12px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n text-align: left;\n }\n code-block .md-table th {\n background: hsla(30, 15%, 50%, 0.08);\n font-weight: 600;\n }\n code-block .md-table tr:hover td {\n background: hsla(30, 15%, 50%, 0.04);\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
33
+ CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n <div class="cb-md" bind="innerHTML: highlighted"></div>\n <div class="cb-img-wrap"><img class="cb-img" bind="src: imageSrc"></div>\n </div>\n';
34
+ CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Markdown container — hidden by default */\n code-block .cb-md {\n display: none;\n padding: 20px 28px;\n flex: 1;\n min-width: 0;\n overflow-wrap: break-word;\n word-wrap: break-word;\n line-height: 1.7;\n color: var(--sn-text, hsl(30, 15%, 18%));\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n font-size: 14px;\n }\n /* Image container — hidden by default */\n code-block .cb-img-wrap {\n display: none;\n flex: 1;\n padding: 20px;\n justify-content: center;\n align-items: center;\n background: repeating-conic-gradient(hsl(30, 10%, 88%) 0% 25%, hsl(30, 10%, 94%) 0% 50%) 0 0 / 16px 16px;\n }\n code-block .cb-img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: 4px;\n box-shadow: 0 2px 12px rgba(0,0,0,0.12);\n }\n /* In markdown mode: hide code, show md */\n code-block[mode-markdown] .cb-gutter { display: none; }\n code-block[mode-markdown] .cb-pre { display: none; }\n code-block[mode-markdown] .cb-md { display: block; }\n /* In image mode: hide code, show image */\n code-block[mode-image] .cb-gutter { display: none; }\n code-block[mode-image] .cb-pre { display: none; }\n code-block[mode-image] .cb-img-wrap { display: flex; }\n\n /* Markdown styles */\n code-block .md-h { margin: 20px 0 8px; color: var(--sn-text, #222); font-weight: 700; }\n code-block h1.md-h { font-size: 24px; border-bottom: 2px solid var(--sn-node-border, #ddd); padding-bottom: 8px; }\n code-block h2.md-h { font-size: 20px; border-bottom: 1px solid var(--sn-node-border, #ddd); padding-bottom: 6px; }\n code-block h3.md-h { font-size: 16px; }\n code-block h4.md-h { font-size: 14px; }\n code-block .md-p { margin: 8px 0; }\n code-block .md-quote {\n margin: 8px 0;\n padding: 8px 16px;\n border-left: 4px solid var(--sn-cat-server, hsl(210, 45%, 55%));\n background: hsla(210, 40%, 55%, 0.08);\n border-radius: 0 4px 4px 0;\n font-style: italic;\n }\n code-block .md-list {\n margin: 8px 0;\n padding-left: 24px;\n }\n code-block .md-list li {\n margin: 3px 0;\n }\n code-block .md-code-block {\n margin: 12px 0;\n padding: 12px 16px;\n background: var(--sn-bg, hsl(37, 30%, 94%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n border-radius: 6px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n }\n code-block .md-inline-code {\n padding: 1px 5px;\n background: hsla(30, 20%, 50%, 0.12);\n border-radius: 3px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 0.9em;\n }\n code-block .md-link {\n color: var(--sn-cat-server, hsl(210, 55%, 50%));\n text-decoration: underline;\n text-decoration-style: dotted;\n }\n code-block .md-link:hover { text-decoration-style: solid; }\n code-block .md-img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n margin: 8px 0;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n }\n code-block .md-hr {\n border: none;\n border-top: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n margin: 16px 0;\n }\n code-block .md-table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n font-size: 13px;\n }\n code-block .md-table th,\n code-block .md-table td {\n padding: 6px 12px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n text-align: left;\n }\n code-block .md-table th {\n background: hsla(30, 15%, 50%, 0.08);\n font-weight: 600;\n }\n code-block .md-table tr:hover td {\n background: hsla(30, 15%, 50%, 0.04);\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
@@ -1,25 +1,38 @@
1
1
  // @ctx .context/web/panels/code-viewer.ctx
2
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
3
 
4
- const _extLang={'.md':'md','.markdown':'md','.sql':'sql','.json':'json','.css':'css','.html':'html','.htm':'html','.xml':'xml','.yaml':'yaml','.yml':'yaml','.toml':'toml','.sh':'sh','.bash':'bash','.env':'env','.ini':'ini','.conf':'conf','.cfg':'cfg','.txt':'plain','.csv':'csv','.gitignore':'plain','.dockerignore':'plain','.editorconfig':'plain'};
4
+ const _extLang={'.md':'md','.markdown':'md','.sql':'sql','.json':'json','.css':'css','.html':'html','.htm':'html','.xml':'xml','.yaml':'yaml','.yml':'yaml','.toml':'toml','.sh':'sh','.bash':'bash','.env':'env','.ini':'ini','.conf':'conf','.cfg':'cfg','.txt':'plain','.csv':'csv','.gitignore':'plain','.dockerignore':'plain','.editorconfig':'plain','.png':'image','.jpg':'image','.jpeg':'image','.gif':'image','.svg':'image','.webp':'image','.bmp':'image','.ico':'image','.pdf':'binary','.zip':'binary','.tar':'binary','.gz':'binary','.woff':'binary','.woff2':'binary','.ttf':'binary','.eot':'binary','.mp3':'binary','.mp4':'binary','.wav':'binary','.avi':'binary','.mov':'binary'};
5
5
  function _getLang(path){if(!path)return'js';const i=path.lastIndexOf('.');if(i<0){const base=path.split('/').pop()||'';if(['Dockerfile','Makefile','Procfile','LICENSE','README','CHANGELOG'].some(n=>base.startsWith(n)))return'plain';return'plain'}return _extLang[path.substring(i).toLowerCase()]||'js'}
6
6
 
7
- export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"compact",modeLabel:"compact",statsText:"",onToggleMode:()=>{
7
+ // Two operational modes for JS files:
8
+ //
9
+ // MODE A — Readable source (normal projects like 1sim_local):
10
+ // Source file is human-written, readable code.
11
+ // Toggle button label: "COMPACT" — shows Terser-compressed view.
12
+ // _isReadable = true (compression saves >15% tokens)
13
+ //
14
+ // MODE B — Compact source (compact projects like project-graph-mcp):
15
+ // Source file is already minified/compressed on disk.
16
+ // Toggle button label: "EXPAND" — beautifies via Terser + injects JSDoc from .ctx.
17
+ // _isReadable = false (compression saves <15% — already compact)
18
+
19
+ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"source",modeLabel:"source",statsText:"",showToggle:!1,toggleLabel:"",onToggleMode:()=>{
8
20
  const lang=_getLang(this._currentPath);
9
21
  if(lang==='md'){
10
22
  this.$.viewMode=this.$.viewMode==="rendered"?"raw":"rendered";
11
23
  this._showCurrentMode();
12
24
  return;
13
25
  }
14
- this.$.viewMode="compact"===this.$.viewMode?"raw":"compact";
26
+ // Toggle between source and the transformation
27
+ this.$.viewMode=this.$.viewMode==="source"?"transformed":"source";
15
28
  this._showCurrentMode();
16
- }};_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=>{
29
+ }};_fileData=null;_isReadable=!1;_transformCache=null;_loadingTransform=!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=>{
17
30
  const lang=_getLang(this._currentPath);
18
- this.toggleAttribute("mode-raw","raw"===e);
31
+ this.toggleAttribute("mode-raw","source"!==e);
19
32
  if(lang==='md'){
20
33
  this.$.modeLabel=e==="rendered"?"rendered":"source";
21
34
  }else{
22
- this.$.modeLabel=this._isReadable?("raw"===e?"source":"compact"):("raw"===e?"source":"expanded");
35
+ this.$.modeLabel=e==="source"?"source":(this._isReadable?"compact":"expanded");
23
36
  }
24
37
  })}_getCodeBlock(){return this.querySelector("code-block")}async _showCurrentMode(){if(!this._fileData)return;const e=this._getCodeBlock();if(!e)return;
25
38
  const lang=_getLang(this._currentPath);
@@ -35,16 +48,70 @@ if(lang==='md'){
35
48
  return;
36
49
  }
37
50
  e.$.lang=lang;
38
- 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;
51
+ if("transformed"===this.$.viewMode){
52
+ // Show cached transform if available
53
+ if(this._transformCache){e.$.code=this._transformCache;return}
54
+ if(this._loadingTransform)return;
55
+ this._loadingTransform=!0;
56
+ e.$.code=this._isReadable?"// Compressing...":"// Expanding...";
57
+ try{
58
+ if(this._isReadable){
59
+ // MODE A: readable source → compress
60
+ const t=await n("/api/compact-file",{path:this._currentPath});
61
+ this._transformCache=t?.code||"// Compression unavailable";
62
+ }else{
63
+ // MODE B: compact source → expand (beautify + inject JSDoc from .ctx)
64
+ const t=await n("/api/expand-file",{path:this._currentPath});
65
+ this._transformCache=t?.code||"// Expand unavailable";
66
+ }
67
+ e.$.code=this._transformCache;
68
+ }catch{e.$.code=this._isReadable?"// Compression failed":"// Expand failed"}
69
+ finally{this._loadingTransform=!1}
70
+ return;
71
+ }
72
+ // Source mode — raw file as-is
73
+ e.$.code=this._fileData.raw;
74
+ }async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this._transformCache=null,this._currentPath=e;
39
75
  const lang=_getLang(e);
76
+ if(lang==='image'){
77
+ const i=this._getCodeBlock();
78
+ if(i){i.$.lang='image';i.setBasePath(e);i.$.code=e}
79
+ this.$.viewMode="rendered";
80
+ this.$.modeLabel="image";
81
+ this.$.showToggle=!1;
82
+ this.$.hasFile=!0;
83
+ return;
84
+ }
85
+ if(lang==='binary'){
86
+ const i=this._getCodeBlock();
87
+ if(i){i.$.lang='plain';i.$.code=`// Binary file: ${e}\n// Cannot display binary content`}
88
+ this.$.viewMode="source";
89
+ this.$.modeLabel="binary";
90
+ this.$.showToggle=!1;
91
+ this.$.hasFile=!0;
92
+ return;
93
+ }
40
94
  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);
41
- 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();
95
+ let s=_raw?.content||o;
96
+ // Detect mode: if .ctx documentation exists (ctxTok > 0), source is compact → EXPAND available
97
+ // If no .ctx, source is readable → COMPACT available
98
+ const hasCtx=!!(t.ctxTok&&t.ctxTok>0);
99
+ this._isReadable=!hasCtx;
100
+ 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();
42
101
  if(lang==='md'){
43
102
  this.$.viewMode="rendered";
44
103
  this.$.modeLabel="rendered";
104
+ this.$.showToggle=!0;
105
+ this.$.toggleLabel="source";
45
106
  if(i){i.$.lang='md';i.setBasePath(e);i.$.code=s}
46
107
  }else{
47
108
  i&&(i.$.lang=lang);
48
- if(this._isReadable){this.$.viewMode="raw",this.$.modeLabel="source",i&&(i.$.code=s)}else{this.$.viewMode="compact",this.$.modeLabel="expanded",i&&(i.$.code=o)}
109
+ // Always start in SOURCE mode
110
+ this.$.viewMode="source";
111
+ this.$.modeLabel="source";
112
+ // Toggle: readable → COMPACT button, compact → EXPAND button
113
+ this.$.showToggle=!0;
114
+ this.$.toggleLabel=this._isReadable?"compact":"expand";
115
+ i&&(i.$.code=s);
49
116
  }
50
- this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.lang='plain',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 view mode">\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");
117
+ this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.lang='plain',n.$.code=`// Error: ${e.message}`),this.$.showToggle=!1,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; hidden: !showToggle" title="Toggle view mode">\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 .pg-mode-toggle[hidden] {\n display: none;\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n",CodeViewer.reg("pg-code-viewer");