project-graph-mcp 2.1.2 → 2.1.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.
Files changed (70) hide show
  1. package/GUIDE.md +237 -0
  2. package/package.json +2 -1
  3. package/rules/test-rules.json +15 -0
  4. package/src/.project-graph-cache.json +1 -1
  5. package/src/analysis/analysis-cache.js +3 -1
  6. package/src/analysis/complexity.js +9 -13
  7. package/src/analysis/custom-rules.js +16 -35
  8. package/src/analysis/db-analysis.js +2 -6
  9. package/src/analysis/dead-code.js +8 -18
  10. package/src/analysis/full-analysis.js +9 -17
  11. package/src/analysis/jsdoc-checker.js +11 -23
  12. package/src/analysis/jsdoc-generator.js +8 -9
  13. package/src/analysis/similar-functions.js +8 -15
  14. package/src/analysis/test-annotations.js +12 -20
  15. package/src/analysis/type-checker.js +5 -7
  16. package/src/analysis/undocumented.js +10 -13
  17. package/src/cli/cli-handlers.js +4 -3
  18. package/src/compact/ai-context.js +2 -2
  19. package/src/compact/compact-migrate.js +8 -16
  20. package/src/compact/compact.js +3 -5
  21. package/src/compact/compress.js +7 -13
  22. package/src/compact/ctx-resolver.js +5 -0
  23. package/src/compact/ctx-to-jsdoc.js +13 -28
  24. package/src/compact/doc-dialect.js +18 -29
  25. package/src/compact/expand.js +10 -36
  26. package/src/compact/jsdoc-builder.js +5 -0
  27. package/src/compact/mode-config.js +6 -6
  28. package/src/compact/split-declarations.js +2 -0
  29. package/src/compact/validate-pipeline.js +7 -8
  30. package/src/core/event-bus.js +2 -1
  31. package/src/core/file-walker.js +4 -0
  32. package/src/core/filters.js +6 -5
  33. package/src/core/graph-builder.js +4 -11
  34. package/src/core/parser.js +19 -29
  35. package/src/core/utils.js +2 -0
  36. package/src/lang/lang-sql.js +7 -20
  37. package/src/mcp/mcp-server.js +2 -3
  38. package/src/mcp/tool-defs.js +1 -1
  39. package/src/mcp/tools.js +13 -21
  40. package/src/network/backend-lifecycle.js +15 -18
  41. package/src/network/local-gateway.js +10 -22
  42. package/src/network/mdns.js +5 -11
  43. package/src/network/server.js +1 -2
  44. package/src/network/web-server.js +7 -33
  45. package/web/app.js +19 -14
  46. package/web/components/code-block.js +1 -0
  47. package/web/components/quick-open.js +1 -0
  48. package/web/dashboard-state.js +1 -0
  49. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  50. package/web/panels/ActionBoard/ActionBoard.js +5 -4
  51. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  52. package/web/panels/EventItem/EventItem.css.js +1 -0
  53. package/web/panels/EventItem/EventItem.js +4 -4
  54. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  55. package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
  56. package/web/panels/ProjectItem/ProjectItem.js +3 -4
  57. package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
  58. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  59. package/web/panels/ProjectList/ProjectList.js +5 -4
  60. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  61. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  62. package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
  63. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  64. package/web/panels/code-viewer.js +1 -0
  65. package/web/panels/ctx-panel.js +1 -0
  66. package/web/panels/dep-graph.js +1 -0
  67. package/web/panels/file-tree.js +4 -188
  68. package/web/panels/health-panel.js +1 -0
  69. package/web/panels/live-monitor.js +1 -0
  70. package/web/state.js +7 -10
@@ -1,3 +1,4 @@
1
+ // @ctx .context/web/panels/health-panel.ctx
1
2
  import e from"@symbiotejs/symbiote";import{api as t,state as n,events as s}from"../app.js";
2
3
  export class HealthPanel extends e{init$={contentHTML:'<div class="pg-placeholder">Loading health analysis...</div>',loaded:!1};initCallback(){s.addEventListener("skeleton-loaded",()=>this._loadHealth()),setTimeout(()=>this._loadHealth(),500)}async _loadHealth(){if(!this.$.loaded){this.$.contentHTML='<div class="pg-placeholder pg-pulse">Analyzing project health...</div>';try{const e=await t("/api/analysis-summary");this.$.loaded=!0;
3
4
  const s=e.healthScore??e.score??"?",a=s>=80?"good":s>=50?"warning":"critical",i=e.grade||(s>=80?"healthy":s>=50?"needs work":"critical"),l=n.skeleton?.s||{},o=l.files||Object.keys(n.skeleton?.X||{}).length||"—",r=l.functions||0,c=l.classes||0,p=Object.values(n.skeleton?.X||{}).reduce((e,t)=>e+t.length,0);this.$.contentHTML=`\n <div class="pg-health-grid">\n <div class="pg-health-card pg-health-score-card">\n <div class="pg-health-score ${a}">${s}</div>\n <div class="pg-health-score-label">Health Score · ${i}</div>\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">code</span>\n Code\n </div>\n ${this._metric("Source files",o)}\n ${this._metric("Functions",r)}\n ${this._metric("Classes",c)}\n ${this._metric("Exports",p)}\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">bug_report</span>\n Issues\n </div>\n ${this._metric("Complexity",e.complexity||0,e.complexity>200)}\n ${this._metric("JSDoc issues",e.jsdocIssues||0,e.jsdocIssues>10)}\n ${this._metric("Undocumented",e.undocumented||0,e.undocumented>5)}\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">speed</span>\n Cache Performance\n </div>\n ${this._metric("Cache hits",e.cache?.hits??"—")}\n ${this._metric("Cache misses",e.cache?.misses??"—")}\n ${this._metric("Hit rate",e.cache?Math.round(e.cache.hits/(e.cache.hits+e.cache.misses)*100)+"%":"—")}\n </div>\n </div>\n ${e.note?`<div class="pg-health-note"><span class="material-symbols-outlined" style="font-size:14px">info</span> ${e.note}</div>`:""}\n `}catch(e){this.$.contentHTML=`<div class="pg-placeholder" style="color:var(--sn-danger-color)">Error: ${e.message}</div>`}}}_metric(e,t,n=!1){return`<div class="pg-metric${n?" pg-metric-warn":""}"><span>${e}</span><span class="pg-metric-val">${t}</span></div>`}}HealthPanel.template='<div bind="innerHTML: contentHTML"></div>',HealthPanel.rootStyles="\n pg-health-panel { display:block; height:100%; overflow-y:auto; padding:16px; font-family:var(--sn-font, Georgia, serif); }\n .pg-health-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px,1fr)); gap:12px; align-content:start; }\n .pg-health-card {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 8px;\n padding: 14px;\n }\n .pg-health-score-card { text-align:center; grid-column:1/-1; padding:20px; }\n .pg-health-score { font-size:56px; font-weight:800; font-family:monospace; }\n .pg-health-score.good { color: var(--sn-success-color, hsl(150, 55%, 38%)); }\n .pg-health-score.warning { color: var(--sn-warning-color, hsl(38, 55%, 42%)); }\n .pg-health-score.critical { color: var(--sn-danger-color, hsl(4, 55%, 48%)); }\n .pg-health-score-label { font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--sn-text-dim); margin-top:4px; }\n .pg-health-card-title {\n display: flex; align-items: center; gap: 6px;\n font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px;\n color:var(--sn-text-dim); margin-bottom:8px;\n }\n .pg-metric { display:flex; justify-content:space-between; padding:5px 0; border-bottom:1px solid var(--sn-node-hover); font-size:12px; color:var(--sn-text); }\n .pg-metric:last-child { border:none; }\n .pg-metric-val { font-weight:600; font-family:monospace; }\n .pg-metric-warn .pg-metric-val { color:var(--sn-warning-color); }\n .pg-health-note {\n display: flex; align-items: center; gap: 6px;\n margin-top: 12px; padding: 10px 12px;\n font-size: 11px; color: var(--sn-text-dim);\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 6px;\n }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:40px; font-style:italic; font-size:13px; }\n .pg-pulse { animation:pulse 1.5s ease infinite; }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }\n",HealthPanel.reg("pg-health-panel");
@@ -1,3 +1,4 @@
1
+ // @ctx .context/web/panels/live-monitor.ctx
1
2
  import n from"@symbiotejs/symbiote";import{events as o}from"../app.js";
2
3
  export class LiveMonitor extends n{init$={eventsHTML:'<div class="pg-placeholder">Waiting for tool calls...</div>',eventCount:"0"};_events=[];initCallback(){o.addEventListener("tool-event",n=>this._addEvent(n.detail))}_addEvent(n){this._events.unshift(n),this._events.length>200&&this._events.pop(),this.$.eventCount=String(this._events.length);
3
4
  const o=this._events.slice(0,100).map(n=>{if("tool_call"===n.type){const o=JSON.stringify(n.args||{}).slice(0,80);return`<div class="pg-mon-event pg-mon-call">\n <span class="pg-mon-arrow">→</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-args">${this._esc(o)}</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}return`<div class="pg-mon-event pg-mon-result ${n.success?"pg-mon-ok":"pg-mon-err"}">\n <span class="pg-mon-arrow">←</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-duration">${n.duration_ms}ms</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}).join("");this.$.eventsHTML=o||'<div class="pg-placeholder">Waiting for tool calls...</div>'}_esc(n){return n.replace(/</g,"&lt;").replace(/>/g,"&gt;")}_formatTime(n){return n?new Date(n).toLocaleTimeString("en",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"}):""}}LiveMonitor.template='\n <div class="pg-mon-header">\n <span>Events: </span><span bind="textContent: eventCount"></span>\n </div>\n <div class="pg-mon-body" bind="innerHTML: eventsHTML"></div>\n',LiveMonitor.rootStyles="\n pg-live-monitor { display:flex; flex-direction:column; height:100%; overflow:hidden; font-size:12px; font-family:var(--sn-font, Georgia, serif); }\n .pg-mon-header { padding:6px 12px; border-bottom:1px solid var(--sn-node-border); background:var(--sn-node-header-bg); font-size:11px; color:var(--sn-text-dim); }\n .pg-mon-body { flex:1; overflow-y:auto; padding:4px; }\n .pg-mon-event {\n display:flex; align-items:center; gap:8px;\n padding:4px 8px; border-radius:4px; font-family:monospace; font-size:11px;\n animation: slideIn 0.15s ease;\n }\n .pg-mon-event:hover { background:var(--sn-node-hover); }\n .pg-mon-arrow { font-weight:bold; width:14px; }\n .pg-mon-call .pg-mon-arrow { color: var(--sn-cat-server, hsl(210, 45%, 45%)); }\n .pg-mon-ok .pg-mon-arrow { color: var(--sn-success-color, hsl(150, 55%, 38%)); }\n .pg-mon-err .pg-mon-arrow { color: var(--sn-danger-color, hsl(4, 55%, 48%)); }\n .pg-mon-tool { color:var(--sn-text); font-weight:600; min-width:100px; }\n .pg-mon-args { color:var(--sn-text-dim); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .pg-mon-duration { color: hsl(250, 35%, 50%); min-width:50px; text-align:right; }\n .pg-mon-time { color:var(--sn-text-dim); font-size:10px; min-width:60px; text-align:right; }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:30px; font-style:italic; }\n @keyframes slideIn { from{opacity:0;transform:translateY(-4px)} to{opacity:1;transform:translateY(0)} }\n",LiveMonitor.reg("pg-live-monitor");
package/web/state.js CHANGED
@@ -2,16 +2,13 @@
2
2
  const e=new URL(".",import.meta.url).href;
3
3
  export const state={project:null,skeleton:null,events:[],connected:!1};
4
4
  const t=new Map;
5
- export function subscribe(e,n){return t.has(e)||t.set(e,new Set),t.get(e).add(n),()=>t.get(e)?.delete(n)}const n=new Set;
5
+ export function subscribe(e,n){return t.has(e)||t.set(e,new Set),t.get(e).add(n),()=>t.get(e)?.delete(n)}
6
+ const n=new Set;
6
7
  export function onEvent(e){return n.add(e),()=>n.delete(e)}
7
- function o(e,n){t.get(e)?.forEach(t=>t(n,e));
8
- const r=e.indexOf(".");if(r>0){const n=e.slice(0,r);t.get(n)?.forEach(e=>e(state[n],n))}t.get("*")?.forEach(t=>t(n,e))}let r=1;
8
+ function o(e,n){t.get(e)?.forEach(t=>t(n,e));const o=e.indexOf(".");if(o>0){const n=e.slice(0,o);t.get(n)?.forEach(e=>e(state[n],n))}t.get("*")?.forEach(t=>t(n,e))}
9
+ let r=1;
9
10
  const c=new Map;
10
- export function call(e,t={}){return new Promise((n,a)=>{if(!s||s.readyState!==WebSocket.OPEN)return void a(new Error("WebSocket not connected"));
11
- const d=r++;c.set(d,{resolve:n,reject:a}),s.send(JSON.stringify({jsonrpc:"2.0",id:d,method:"tool",params:{name:e,args:t}})),setTimeout(()=>{c.has(d)&&(c.delete(d),a(new Error(`Tool call timeout: ${e}`)))},3e4)})}let s=null,a=null;function l(e){Object.assign(state,e),state.connected=!0,o("*",state);for(const t of Object.keys(e))o(t,e[t])}
12
- function i(e,t){const n=e.split(".");
13
- let r=state;for(let e=0;e<n.length-1;e++)r[n[e]]||(r[n[e]]={}),r=r[n[e]];r[n[n.length-1]]=t,o(e,t)}
14
- function u(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id&&(void 0!==t.result||t.error)){const e=c.get(t.id);return void(e&&(c.delete(t.id),t.error?e.reject(new Error(t.error.message||"Tool error")):e.resolve(t.result)))}"snapshot"!==t.method?"patch"!==t.method?"event"!==t.method?t.type&&n.forEach(e=>e(t)):n.forEach(e=>e(t.params)):i(t.params.path,t.params.value):l(t.params.state)}
15
- export function connect(){if(s)return;
16
- const t=e.replace(/^http/,"ws");s=new WebSocket(`${t}ws/monitor`),s.onopen=()=>{state.connected=!0,o("connected",!0),a&&(clearTimeout(a),a=null)},s.onmessage=e=>u(e.data),s.onclose=()=>{state.connected=!1,s=null,o("connected",!1);for(const[e,{reject:t}]of c)t(new Error("WebSocket disconnected"));c.clear(),a=setTimeout(connect,3e3)},s.onerror=()=>{}}
11
+ export function call(e,t={}){return new Promise((n,o)=>{if(!s||s.readyState!==WebSocket.OPEN)return void o(new Error("WebSocket not connected"));const a=r++;c.set(a,{resolve:n,reject:o}),s.send(JSON.stringify({jsonrpc:"2.0",id:a,method:"tool",params:{name:e,args:t}})),setTimeout(()=>{c.has(a)&&(c.delete(a),o(new Error(`Tool call timeout: ${e}`)))},3e4)})}
12
+ let s=null,a=null;
13
+ export function connect(){if(s)return;const t=e.replace(/^http/,"ws");s=new WebSocket(`${t}ws/monitor`),s.onopen=()=>{state.connected=!0,o("connected",!0),a&&(clearTimeout(a),a=null)},s.onmessage=e=>function(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id&&(void 0!==t.result||t.error)){const e=c.get(t.id);return void(e&&(c.delete(t.id),t.error?e.reject(new Error(t.error.message||"Tool error")):e.resolve(t.result)))}"snapshot"!==t.method?"patch"!==t.method?"event"!==t.method?t.type&&n.forEach(e=>e(t)):n.forEach(e=>e(t.params)):function(e,t){const n=e.split(".");let r=state;for(let e=0;e<n.length-1;e++)r[n[e]]||(r[n[e]]={}),r=r[n[e]];r[n[n.length-1]]=t,o(e,t)}(t.params.path,t.params.value):function(e){Object.assign(state,e),state.connected=!0,o("*",state);for(const t of Object.keys(e))o(t,e[t])}(t.params.state)}(e.data),s.onclose=()=>{state.connected=!1,s=null,o("connected",!1);for(const[e,{reject:t}]of c)t(new Error("WebSocket disconnected"));c.clear(),a=setTimeout(connect,3e3)},s.onerror=()=>{}}
17
14
  export function disconnect(){a&&(clearTimeout(a),a=null),s&&(s.close(),s=null)}