project-graph-mcp 2.1.10 → 2.1.13

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.10",
3
+ "version": "2.1.13",
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,6 +1,6 @@
1
1
  // @ctx .context/src/mcp/mcp-server.ctx
2
2
  import e from"fs";import t from"path";import{fileURLToPath as s}from"url";import{TOOLS as o}from"./tool-defs.js";import{emitToolCall as a,emitToolResult as r}from"../core/event-bus.js";import{getSkeleton as n,getFocusZone as i,expand as c,deps as d,usages as l,invalidateCache as p,getCallChain as u}from"./tools.js";import{getPendingTests as m,markTestPassed as _,markTestFailed as g,getTestSummary as h,resetTestState as f}from"../analysis/test-annotations.js";import{getFilters as y,setFilters as x,addExcludes as j,removeExcludes as w,resetFilters as v}from"../core/filters.js";import{getInstructions as b}from"../compact/instructions.js";import{getUndocumentedSummary as k}from"../analysis/undocumented.js";import{getDeadCode as S}from"../analysis/dead-code.js";import{generateJSDoc as $,generateJSDocFor as R}from"../analysis/jsdoc-generator.js";import{getSimilarFunctions as F}from"../analysis/similar-functions.js";import{getComplexity as E}from"../analysis/complexity.js";import{getLargeFiles as C}from"../analysis/large-files.js";import{getOutdatedPatterns as T}from"../analysis/outdated-patterns.js";import{getFullAnalysis as U,getAnalysisSummaryOnly as D}from"../analysis/full-analysis.js";import{getCustomRules as P,setCustomRule as O,checkCustomRules as I}from"../analysis/custom-rules.js";import{getFrameworkReference as J}from"../compact/framework-references.js";import{setRoots as q,resolvePath as A}from"../core/workspace.js";import{getDBSchema as N,getTableUsage as L,getDBDeadTables as M}from"../analysis/db-analysis.js";import{compressFile as B,editCompressed as z}from"../compact/compress.js";import{getProjectDocs as G,generateContextFiles as V,checkStaleness as W}from"../compact/doc-dialect.js";import{getGraph as Q}from"./tools.js";import{parseProject as Y,discoverSubProjects as H}from"../core/parser.js";import{getAiContext as Z}from"../compact/ai-context.js";import{checkJSDocConsistency as K}from"../analysis/jsdoc-checker.js";import{checkTypes as X}from"../analysis/type-checker.js";import{compactProject as ee,expandProject as te}from"../compact/compact.js";import{expandFile as se,expandProject as oe}from"../compact/expand.js";import{validatePipeline as ae}from"../compact/validate-pipeline.js";import{validateCtxContracts as re}from"../compact/ctx-to-jsdoc.js";import{getConfig as ne,setConfig as ie,getModeDescription as ce,getModeWorkflow as de}from"../compact/mode-config.js";import{readFileSync as le,existsSync as pe}from"fs";
3
- const ue=t.dirname(s(import.meta.url)),me={get_skeleton:e=>n(A(e.path)),get_focus_zone:e=>i({...e,path:A(e.path)}),expand:e=>c(e.symbol),deps:e=>d(e.symbol),usages:e=>l(e.symbol),get_call_chain:e=>u({from:e.from,to:e.to,path:e.path?A(e.path):void 0}),invalidate_cache:()=>(p(),{success:!0}),get_pending_tests:e=>m(A(e.path)),mark_test_passed:e=>_(e.testId),mark_test_failed:e=>g(e.testId,e.reason),get_test_summary:e=>h(A(e.path)),reset_test_state:()=>f(),get_filters:()=>y(),set_filters:e=>x(e),add_excludes:e=>j(e.dirs),remove_excludes:e=>w(e.dirs),reset_filters:()=>v(),get_usage_guide:s=>{try{const o=t.join(ue,"..","..","GUIDE.md"),a=e.readFileSync(o,"utf8");if(!s.topic)return a;
3
+ const ue=t.dirname(s(import.meta.url));let _pkgVer="0.0.0";try{_pkgVer=JSON.parse(le(t.join(ue,"..","..","package.json"),"utf8")).version}catch{}const me={get_skeleton:e=>n(A(e.path)),get_focus_zone:e=>i({...e,path:A(e.path)}),expand:e=>c(e.symbol),deps:e=>d(e.symbol),usages:e=>l(e.symbol),get_call_chain:e=>u({from:e.from,to:e.to,path:e.path?A(e.path):void 0}),invalidate_cache:()=>(p(),{success:!0}),get_pending_tests:e=>m(A(e.path)),mark_test_passed:e=>_(e.testId),mark_test_failed:e=>g(e.testId,e.reason),get_test_summary:e=>h(A(e.path)),reset_test_state:()=>f(),get_filters:()=>y(),set_filters:e=>x(e),add_excludes:e=>j(e.dirs),remove_excludes:e=>w(e.dirs),reset_filters:()=>v(),get_usage_guide:s=>{try{const o=t.join(ue,"..","..","GUIDE.md"),a=e.readFileSync(o,"utf8");if(!s.topic)return a;
4
4
  const r=new RegExp(`## ${s.topic}`,"i"),n=a.match(r);if(!n)return`Topic '${s.topic}' not found in guide.`;
5
5
  const i=n.index;
6
6
  let c=a.indexOf("\n## ",i+1);return-1===c&&(c=a.length),a.substring(i,c).trim()}catch(e){return`Failed to read usage guide: ${e.message}`}},get_agent_instructions:()=>b(),get_undocumented:e=>k(A(e.path),e.level||"tests"),get_dead_code:e=>S(A(e.path)),generate_jsdoc:e=>e.name?R(A(e.path),e.name):$(A(e.path)),get_similar_functions:e=>F(A(e.path),{threshold:e.threshold}),get_complexity:e=>E(A(e.path),{minComplexity:e.minComplexity,onlyProblematic:e.onlyProblematic}),get_large_files:e=>C(A(e.path),{onlyProblematic:e.onlyProblematic}),get_outdated_patterns:e=>T(A(e.path),{codeOnly:e.codeOnly,depsOnly:e.depsOnly}),get_full_analysis:e=>U(A(e.path),{includeItems:e.includeItems}),get_custom_rules:()=>P(),set_custom_rule:e=>O(e.ruleSet,e.rule),check_custom_rules:e=>I(A(e.path),{ruleSet:e.ruleSet,severity:e.severity}),get_framework_reference:e=>J({framework:e.framework,path:e.path?A(e.path):void 0}),get_db_schema:e=>N(A(e.path)),get_table_usage:e=>L(A(e.path),e.table),get_db_dead_tables:e=>M(A(e.path)),get_compressed_file:e=>B(A(e.path),{beautify:e.beautify,legend:e.legend}),get_project_docs:async e=>{const t=A(e.path),s=await Q(t),o=G(s,t,{file:e.file});try{const e=await Y(t),s=W(t,e);return{docs:o,staleFiles:s.stale,freshCount:s.fresh}}catch{return{docs:o}}},generate_context_docs:async e=>{const t=A(e.path),s=await Q(t),o=await Y(t);return V(s,t,o,{overwrite:e.overwrite,scope:e.scope})},check_stale_docs:async e=>{const t=A(e.path),s=await Y(t);return W(t,s)},get_ai_context:async e=>{const t=A(e.path),s=await Z(t,{includeFiles:e.includeFiles,includeDocs:e.includeDocs,includeSkeleton:e.includeSkeleton});try{const e=await Y(t),o=W(t,e);s.staleFiles=o.stale}catch{}return s},check_jsdoc_consistency:e=>K(A(e.path)),check_types:async e=>X(A(e.path),{files:e.files,maxDiagnostics:e.maxDiagnostics}),discover_sub_projects:e=>H(A(e.path)),get_analysis_summary:e=>D(A(e.path)),compact_project:e=>ee(A(e.path),{dryRun:e.dryRun||!1}),beautify_project:e=>te(A(e.path),{dryRun:e.dryRun||!1}),validate_ctx_contracts:e=>re(A(e.path),{strict:e.strict||!1}),edit_compressed:e=>z(A(e.path),e.symbol,e.code,{beautify:!1!==e.beautify,dryRun:e.dryRun||!1}),get_mode:e=>{const t=A(e.path),s=ne(t);return{...s,description:ce(s.mode),workflow:de(s.mode)}},set_mode:e=>{const t=A(e.path),s={mode:e.mode};return void 0!==e.beautify&&(s.beautify=e.beautify),void 0!==e.autoValidate&&(s.autoValidate=e.autoValidate),void 0!==e.stripJSDoc&&(s.stripJSDoc=e.stripJSDoc),ie(t,s)},expand_file:async e=>{const s=A(e.path),o=t.dirname(t.dirname(s)),a=t.relative(o,s),r=t.basename(a,t.extname(a))+".ctx",n=t.dirname(a);
@@ -8,7 +8,7 @@ let i=null;
8
8
  const c=t.join(o,n,r),d=t.join(o,".context",n,r);return pe(c)?i=le(c,"utf-8"):pe(d)&&(i=le(d,"utf-8")),se(s,i)},expand_project:e=>oe(A(e.path),{dryRun:e.dryRun||!1}),validate_pipeline:e=>ae(A(e.path),{strict:e.strict||!1,fix:e.fix||!1})},_e={get_skeleton:()=>['💡 Use expand("SYMBOL") to see code for a specific class.','💡 Use deps("SYMBOL") to see architecture dependencies.',"💡 After code changes, run invalidate_cache() to refresh the graph.","🌐 Web explorer: run `npx project-graph-mcp serve .` to browse code visually."],expand:e=>{const t=[];return e.methods?.length>10&&t.push("💡 Large class detected. Run get_complexity() to find refactoring targets."),t.push("💡 Use deps() to see what depends on this symbol."),e.file&&t.push(`📝 No .ctx for ${e.file}? Run generate_context_docs({ scope: ["${e.file}"] }) to create documentation.`),t},deps:()=>["💡 Use usages() for cross-project reference search."],get_call_chain:e=>e.error?[]:["💡 Use expand() on intermediate steps to understand how data is passed along the chain."],invalidate_cache:()=>["✅ Cache cleared. Run get_skeleton() to rebuild the project graph."],get_dead_code:e=>{const t=["💡 Review each item before removing — some may be used dynamically."];return e.unusedExports?.length>20&&t.push('💡 Consider delegating cleanup to agent-pool: delegate_task({ prompt: "Remove dead code..." })'),t},get_full_analysis:()=>['💡 Focus on items with "critical" severity first.',"💡 Run individual tools (get_complexity, get_dead_code) for detailed breakdowns."],get_complexity:()=>["💡 Functions with complexity >10 are candidates for refactoring.","💡 Use expand() to read the function code before refactoring."],get_undocumented:()=>["💡 Use generate_jsdoc() to auto-generate documentation templates."],get_similar_functions:()=>["💡 Consider extracting duplicated logic into a shared utility."],get_pending_tests:()=>["💡 Use mark_test_passed(testId) or mark_test_failed(testId, reason) to track progress."],get_db_schema:e=>{const t=[];return e.totalTables>0?t.push(`💡 Found ${e.totalTables} tables. Use get_table_usage() to see which code reads/writes them.`):t.push("💡 No .sql schema files found. Add schema.sql or migrations/*.sql to your project."),t},get_table_usage:e=>{const t=["💡 Use get_db_dead_tables() to find tables defined in schema but never queried."];return 0===e.totalTables&&t.push("💡 No SQL queries detected. This tool finds SQL in .query(), .execute(), sql`...` patterns."),t},get_db_dead_tables:()=>["💡 Dead columns detection is best-effort — verify before removing."],get_compressed_file:e=>{const t=[`💡 Saved ${e.savings} tokens (${e.original} → ${e.compressed}).`];return t.push("💡 Use get_ai_context() for full project boot: skeleton + docs + compressed files."),e.file&&t.push(`📝 Working on ${e.file}? Run generate_context_docs({ scope: ["${e.file}"] }) to document it.`),t},get_project_docs:e=>{const t=["💡 Enrich docs by editing .context/*.ctx files — they are git-tracked.","💡 Use generate_context_docs() to create initial .ctx stubs."];return e.staleFiles?.length>0&&t.push(`âš ī¸ ${e.staleFiles.length} .ctx files are STALE: ${e.staleFiles.slice(0,5).join(", ")}. Run generate_context_docs({ scope: ${JSON.stringify(e.staleFiles)}, overwrite: true }) to update (descriptions will be preserved).`),t},check_stale_docs:e=>{const t=[];return e.stale?.length>0?(t.push(`âš ī¸ ${e.stale.length} stale: ${e.stale.join(", ")}`),t.push(`💡 Run generate_context_docs({ scope: ${JSON.stringify(e.stale)}, overwrite: true }) — existing descriptions will be preserved.`)):t.push("✅ All .ctx docs are up to date."),e.unknown>0&&t.push(`â„šī¸ ${e.unknown} .ctx files without @sig header (pre-staleness format).`),t},generate_context_docs:e=>{const t=[];return e.created?.length>0&&t.push(`✅ Created ${e.created.length} .ctx files with @sig hashes.`),e.skipped?.length>0&&t.push(`â„šī¸ Skipped ${e.skipped.length} existing files. Use overwrite=true to regenerate (descriptions are preserved via merge).`),e.templates&&Object.keys(e.templates).length>0&&(t.push("📝 .ctx files have {DESCRIBE} markers. To enrich automatically:"),t.push(' delegate_task({ prompt: "Enrich .context/*.ctx files — replace {DESCRIBE} with compact descriptions", skill: "doc-enricher" })'),t.push(" Or enrich manually: read source files and replace {DESCRIBE} markers with pipe-separated descriptions (max 80 chars).")),t},get_ai_context:e=>{const t=[`💡 Context loaded: ${e.totalTokens} tokens (${e.savings} savings vs ${e.vsOriginal} original).`];return t.push("💡 Use expand() to drill into specific symbols. Use get_compressed_file() for additional files."),t.push("📋 Read .context/*.ctx files for typed signatures and documentation. Check .gemini/AGENTS.md for project-specific rules."),e.staleFiles?.length>0&&t.push(`âš ī¸ ${e.staleFiles.length} .ctx docs are stale. Run generate_context_docs({ scope: ${JSON.stringify(e.staleFiles)}, overwrite: true }) then delegate_task({ skill: "doc-enricher" }) to update.`),t},validate_ctx_contracts:e=>{const t=[];return e.summary?.errors>0?t.push(`âš ī¸ ${e.summary.errors} contract violations found. Run generate_context_docs({ overwrite: true }) to regenerate .ctx files.`):t.push("✅ All .ctx contracts valid — documentation matches source."),t},edit_compressed:e=>{const t=[];return e.success&&(t.push(`✅ Symbol "${e.symbol}" replaced in ${e.file}.`),t.push("💡 Run invalidate_cache() to refresh the graph after editing."),t.push("💡 Run validate_ctx_contracts() to check if .ctx docs need updating.")),t},get_mode:e=>{const t=[`📋 Current mode: ${e.mode} — ${e.description}`];return 1===e.mode&&(t.push("💡 Compact mode: read/write .js directly. Run expand_project to update .expanded/ for human review."),t.push("🌐 Web explorer: `npx project-graph-mcp serve .` for visual code browsing with compression stats.")),2===e.mode&&(t.push("💡 Full mode: get_compressed_file() → read → edit_compressed() → write."),t.push("🌐 Web explorer: `npx project-graph-mcp serve .` for visual code browsing with compression stats.")),t},set_mode:e=>e.saved?[`✅ Mode set to ${e.config.mode}. Saved to ${e.path}.`]:[],validate_pipeline:e=>{const t=[`${"PASS"===e.status?"✅":"❌"} Pipeline ${e.status} (${e.duration}): ${e.summary.contractErrors} contract, ${e.summary.astErrors} AST, ${e.summary.styleErrors} style errors.`];return"PASS"===e.status&&t.push(`💡 ${e.summary.jsdocInjected} JSDoc blocks injected. Token savings: ${e.summary.tokenSavings}.`),e.summary.styleErrors>0&&t.push("💡 Run compact({ action: 'validate_pipeline', path: '.', fix: true }) to auto-fix style issues."),e.fix&&t.push(`🔧 Auto-fixed ${e.fix.fixed}/${e.fix.total} files.`),e.summary.contractErrors>0&&t.push("âš ī¸ Fix .ctx contract errors first, then re-run validate_pipeline."),t},expand_project:e=>[`✅ Expanded ${e.files} files → ${e.outputDir}. ${e.totalJSDocInjected} JSDoc blocks injected.`]},ge={navigate:e=>{const t={expand:"expand",deps:"deps",usages:"usages",call_chain:"get_call_chain",sub_projects:"discover_sub_projects"}[e.action];if(!t)throw new Error(`Unknown navigate action: ${e.action}`);return me[t](e)},analyze:e=>{const t={dead_code:"get_dead_code",similar_functions:"get_similar_functions",complexity:"get_complexity",large_files:"get_large_files",outdated_patterns:"get_outdated_patterns",full_analysis:"get_full_analysis",analysis_summary:"get_analysis_summary",undocumented:"get_undocumented"}[e.action];if(!t)throw new Error(`Unknown analyze action: ${e.action}`);return me[t](e)},testing:e=>{const t={pending:"get_pending_tests",pass:"mark_test_passed",fail:"mark_test_failed",summary:"get_test_summary",reset:"reset_test_state"}[e.action];if(!t)throw new Error(`Unknown testing action: ${e.action}`);return me[t](e)},filters:e=>{const t={get:"get_filters",set:"set_filters",add_excludes:"add_excludes",remove_excludes:"remove_excludes",reset:"reset_filters"}[e.action];if(!t)throw new Error(`Unknown filters action: ${e.action}`);return me[t](e)},jsdoc:e=>{const t={check_consistency:"check_jsdoc_consistency",check_types:"check_types",generate:"generate_jsdoc"}[e.action];if(!t)throw new Error(`Unknown jsdoc action: ${e.action}`);return me[t](e)},docs:e=>{const t={get:"get_project_docs",generate:"generate_context_docs",check_stale:"check_stale_docs",validate_contracts:"validate_ctx_contracts"}[e.action];if(!t)throw new Error(`Unknown docs action: ${e.action}`);return me[t](e)},compact:e=>{const t={compact_file:"get_compressed_file",edit:"edit_compressed",compact_all:"compact_project",beautify:"beautify_project",expand_file:"expand_file",expand_project:"expand_project",validate_pipeline:"validate_pipeline",get_mode:"get_mode",set_mode:"set_mode"}[e.action];if(!t)throw new Error(`Unknown compact action: ${e.action}`);return me[t](e)},db:e=>{const t={schema:"get_db_schema",table_usage:"get_table_usage",dead_tables:"get_db_dead_tables"}[e.action];if(!t)throw new Error(`Unknown db action: ${e.action}`);return me[t](e)}},he={navigate:(e,t)=>{const s=_e[{expand:"expand",deps:"deps",call_chain:"get_call_chain"}[t.action]];return s?s(e):[]},analyze:(e,t)=>{const s=_e[{dead_code:"get_dead_code",full_analysis:"get_full_analysis",complexity:"get_complexity",undocumented:"get_undocumented",similar_functions:"get_similar_functions"}[t.action]];return s?s(e):[]},testing:(e,t)=>"pending"===t.action&&_e.get_pending_tests?.(e)||[],docs:(e,t)=>{const s=_e[{get:"get_project_docs",check_stale:"check_stale_docs",generate:"generate_context_docs",validate_contracts:"validate_ctx_contracts"}[t.action]];return s?s(e):[]},compact:(e,t)=>{const s=_e[{compact_file:"get_compressed_file",edit:"edit_compressed",get_mode:"get_mode",set_mode:"set_mode",validate_pipeline:"validate_pipeline",expand_project:"expand_project"}[t.action]];return s?s(e):[]},db:(e,t)=>{const s=_e[{schema:"get_db_schema",table_usage:"get_table_usage",dead_tables:"get_db_dead_tables"}[t.action]];return s?s(e):[]}};
9
9
  export function createServer(s){let n=1;
10
10
  const i=new Map;
11
- let c=!1;return{pendingRequests:i,async handleMessage(s){if(void 0!==s.result||void 0!==s.error){const e=i.get(s.id);return e&&(i.delete(s.id),s.error?e.reject(new Error(s.error.message)):e.resolve(s.result)),null}const{method:a,params:r,id:n}=s;if(void 0===n)return await this.handleNotification(a,r),null;try{switch(a){case"initialize":return r?.capabilities?.roots&&(c=!0),r?.roots&&q(r.roots),{jsonrpc:"2.0",id:n,result:{protocolVersion:"2024-11-05",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:"2.1.0"}}};case"resources/list":return{jsonrpc:"2.0",id:n,result:{resources:[{uri:"project-graph://guide",name:"Project Graph Usage Guide",description:"Comprehensive guide with workflows and examples",mimeType:"text/markdown"}]}};case"resources/read":return"project-graph://guide"!==r.uri?{jsonrpc:"2.0",id:n,error:{code:-32602,message:`Resource not found: ${r.uri}`}}:{jsonrpc:"2.0",id:n,result:{contents:[{uri:"project-graph://guide",mimeType:"text/markdown",text:e.readFileSync(t.join(ue,"..","..","GUIDE.md"),"utf8")}]}};case"tools/list":return{jsonrpc:"2.0",id:n,result:{tools:o}};case"tools/call":{const e=await this.executeTool(r.name,r.arguments),t=[{type:"text",text:JSON.stringify(e,null,2)}];
11
+ let c=!1;return{pendingRequests:i,async handleMessage(s){if(void 0!==s.result||void 0!==s.error){const e=i.get(s.id);return e&&(i.delete(s.id),s.error?e.reject(new Error(s.error.message)):e.resolve(s.result)),null}const{method:a,params:r,id:n}=s;if(void 0===n)return await this.handleNotification(a,r),null;try{switch(a){case"initialize":return r?.capabilities?.roots&&(c=!0),r?.roots&&q(r.roots),{jsonrpc:"2.0",id:n,result:{protocolVersion:"2024-11-05",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:_pkgVer}}};case"resources/list":return{jsonrpc:"2.0",id:n,result:{resources:[{uri:"project-graph://guide",name:"Project Graph Usage Guide",description:"Comprehensive guide with workflows and examples",mimeType:"text/markdown"}]}};case"resources/read":return"project-graph://guide"!==r.uri?{jsonrpc:"2.0",id:n,error:{code:-32602,message:`Resource not found: ${r.uri}`}}:{jsonrpc:"2.0",id:n,result:{contents:[{uri:"project-graph://guide",mimeType:"text/markdown",text:e.readFileSync(t.join(ue,"..","..","GUIDE.md"),"utf8")}]}};case"tools/list":return{jsonrpc:"2.0",id:n,result:{tools:o}};case"tools/call":{const e=await this.executeTool(r.name,r.arguments),t=[{type:"text",text:JSON.stringify(e,null,2)}];
12
12
  let s=[];
13
13
  const o=he[r.name];if(o&&r.arguments?.action)s=o(e,r.arguments);else{const t=_e[r.name];t&&(s=t(e))}return s.length>0&&t.push({type:"text",text:"\n"+s.join("\n")}),{jsonrpc:"2.0",id:n,result:{content:t}}}default:return{jsonrpc:"2.0",id:n,error:{code:-32601,message:`Method not found: ${a}`}}}}catch(e){return{jsonrpc:"2.0",id:n,error:{code:-32e3,message:e.message}}}},async handleNotification(e,t){switch(e){case"notifications/initialized":if(c)try{const e=await this.requestRoots();e&&e.length>0&&q(e)}catch(e){console.error(`[project-graph] Failed to get roots: ${e.message}`)}break;case"notifications/roots/list_changed":if(c)try{const e=await this.requestRoots();e&&e.length>0&&(q(e),p())}catch(e){console.error(`[project-graph] Failed to refresh roots: ${e.message}`)}}},requestRoots:()=>new Promise((e,t)=>{const o=n++,a=setTimeout(()=>{i.delete(o),t(new Error("roots/list request timed out"))},5e3);i.set(o,{resolve:t=>{clearTimeout(a),e(t.roots||[])},reject:e=>{clearTimeout(a),t(e)}}),s({jsonrpc:"2.0",id:o,method:"roots/list"})}),async executeTool(e,t){a(e,t);
14
14
  const s=Date.now();try{const o=ge[e];
@@ -7,10 +7,11 @@ import{createInterface as p}from"node:readline";
7
7
  import{createConnection as d}from"node:net";
8
8
  import{fileURLToPath as h}from"node:url";
9
9
  const m=a(h(import.meta.url),".."),g=a(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway","backends");
10
+ function _getVersion(){try{return JSON.parse(o(a(m,"..","..","package.json"),"utf8")).version}catch{return"0.0.0"}}
10
11
  function y(t){const r=l(t),n=e("md5").update(r).digest("hex").slice(0,8);return a(g,`${n}.json`)}
11
12
  function B(e){const t=y(e);if(!r(t))return null;try{const e=JSON.parse(o(t,"utf8"));try{process.kill(e.pid,0)}catch{try{s(t)}catch{}return null}return e}catch{return null}}
12
- export function writePortFile(e,t){n(g,{recursive:!0});const r=l(e),o={port:t,pid:process.pid,project:r,name:f(r)||"root",startedAt:Date.now()};c(y(e),JSON.stringify(o,null,2))}
13
+ export function writePortFile(e,t){n(g,{recursive:!0});const r=l(e),i={port:t,pid:process.pid,project:r,name:f(r)||"root",version:_getVersion(),startedAt:Date.now()};c(y(e),JSON.stringify(i,null,2))}
13
14
  export function removePortFile(e){try{s(y(e))}catch{}}
14
15
  export function listBackends(){if(!r(g))return[];const e=i(g).filter(e=>e.endsWith(".json")),t=[];for(const r of e)try{const e=JSON.parse(o(a(g,r),"utf8"));try{process.kill(e.pid,0),t.push(e)}catch{try{s(a(g,r))}catch{}}}catch{}return t}
15
- export async function ensureBackend(e){const t=l(e),n=B(t);if(n)return n.port;const o=a(m,"backend.js");u(process.execPath,[o,t],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref();const c=y(t),s=Date.now();for(;Date.now()-s<1e4;)if(await new Promise(e=>setTimeout(e,200)),r(c)){const e=B(t);if(e)return e.port}throw new Error("Backend failed to start within 10s")}
16
+ export async function ensureBackend(e,{force:f}={}){const t=l(e),n=B(t);if(n){const cv=_getVersion();if(f||n.version&&n.version!==cv){if(n.version!==cv)console.error(`[project-graph] Version mismatch: running ${n.version}, installed ${cv}`);console.error("[project-graph] Restarting backend...");try{process.kill(n.pid,"SIGTERM")}catch{}try{s(y(t))}catch{}await new Promise(r=>setTimeout(r,500))}else{return n.port}}const o=a(m,"backend.js");u(process.execPath,[o,t],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref();const c=y(t),_t=Date.now();for(;Date.now()-_t<1e4;)if(await new Promise(e=>setTimeout(e,200)),r(c)){const e=B(t);if(e)return e.port}throw new Error("Backend failed to start within 10s")}
16
17
  export function startStdioProxy(e,r=[]){const n=t(16).toString("base64"),o=d({host:"127.0.0.1",port:e},()=>{o.write(`GET /mcp-ws HTTP/1.1\r\nHost: 127.0.0.1:${e}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${n}\r\nSec-WebSocket-Version: 13\r\n\r\n`)});let c=!1,s=Buffer.alloc(0),i=[...r];const a=p({input:process.stdin,terminal:!1});function l(e){const r=Buffer.from(e,"utf8"),n=t(4),o=Buffer.alloc(r.length);for(let e=0;e<r.length;e++)o[e]=r[e]^n[e%4];let c;return r.length<126?(c=Buffer.alloc(2),c[0]=129,c[1]=128|r.length):r.length<65536?(c=Buffer.alloc(4),c[0]=129,c[1]=254,c.writeUInt16BE(r.length,2)):(c=Buffer.alloc(10),c[0]=129,c[1]=255,c.writeBigUInt64BE(BigInt(r.length),2)),Buffer.concat([c,n,o])}function f(e){if(e.length<2)return null;const t=15&e[0];let r=127&e[1],n=2;if(126===r){if(e.length<4)return null;r=e.readUInt16BE(2),n=4}else if(127===r){if(e.length<10)return null;r=Number(e.readBigUInt64BE(2)),n=10}return e.length<n+r?null:{opcode:t,data:e.slice(n,n+r).toString("utf8"),totalLen:n+r}}a.on("line",e=>{if(c)try{o.write(l(e))}catch{}else i.push(e)}),a.on("close",()=>{o.end(),process.exit(0)}),o.on("data",e=>{if(c)s=Buffer.concat([s,e]);else{const t=Buffer.concat([s,e]),r=t.indexOf("\r\n\r\n");if(-1===r)return void(s=t);t.slice(0,r).toString().includes("101")||(console.error("[project-graph] WebSocket handshake failed"),process.exit(1)),c=!0,s=t.slice(r+4);for(const e of i)try{o.write(l(e))}catch{}i=[]}for(;s.length>=2;){const e=f(s);if(!e)break;if(s=s.slice(e.totalLen),1===e.opcode)process.stdout.write(e.data+"\n");else if(8===e.opcode)process.exit(0);else if(9===e.opcode){const e=Buffer.alloc(2);e[0]=138,e[1]=0,o.write(e)}}}),o.on("close",()=>process.exit(0)),o.on("error",e=>{console.error(`[project-graph] Proxy connection error: ${e.message}`),process.exit(1)})}
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // @ctx .context/src/network/server.ctx
3
- import e from"node:path";import t from"node:fs";if(process.argv[1]&&(process.argv[1].endsWith("server.js")||process.argv[1].endsWith("project-graph-mcp"))){const[,,o,...r]=process.argv;if("serve"===o){const t=r[0]||".",o=r.indexOf("--port"),s=-1!==o?parseInt(r[o+1],10):0;if(s){const{startWebServer:e}=await import("./web-server.js");e(t,s)}else{const{ensureBackend:o}=await import("./backend-lifecycle.js");try{const r=await o(t),s=e.resolve(t);console.log("\n âŦĄ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${r}/`),console.log(` → Project: ${s}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${r}/mcp-ws\n`)}catch(e){console.error(`Failed to start backend: ${e.message}`),process.exit(1)}}}else if(o){const{runCLI:e}=await import("../cli/cli.js");e(o,r)}else if(process.env.PROJECT_GRAPH_BACKEND){const{startStdioServer:e}=await import("../mcp/mcp-server.js");console.error("Starting Project Graph MCP (stdio, direct)..."),e()}else{const{setRoots:e,getWorkspaceRoot:o}=await import("../core/workspace.js"),{ensureBackend:r,startStdioProxy:s}=await import("./backend-lifecycle.js"),{createInterface:i}=await import("node:readline"),n=t.createWriteStream("/tmp/pg-init-debug.log",{flags:"a"});n.write(`\n=== NEW SESSION ${(new Date).toISOString()} ===\n`);
3
+ import e from"node:path";import t from"node:fs";let _v="0.0.0";try{const _d=e.dirname(new URL(import.meta.url).pathname);_v=JSON.parse(t.readFileSync(e.join(_d,"..","..","package.json"),"utf8")).version}catch{}if(process.argv[1]&&(process.argv[1].endsWith("server.js")||process.argv[1].endsWith("project-graph-mcp"))){const[,,o,...r]=process.argv;if("serve"===o){const t=r[0]||".",o=r.indexOf("--port"),s=-1!==o?parseInt(r[o+1],10):0;if(s){const{startWebServer:e}=await import("./web-server.js");e(t,s)}else{const{ensureBackend:o}=await import("./backend-lifecycle.js");try{const r=await o(t,{force:true}),s=e.resolve(t);console.log(`\n âŦĄ project-graph-mcp v${_v}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${r}/`),console.log(` → Project: ${s}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${r}/mcp-ws\n`)}catch(e){console.error(`Failed to start backend: ${e.message}`),process.exit(1)}}}else if(o){const{runCLI:e}=await import("../cli/cli.js");e(o,r)}else if(process.env.PROJECT_GRAPH_BACKEND){const{startStdioServer:e}=await import("../mcp/mcp-server.js");console.error("Starting Project Graph MCP (stdio, direct)..."),e()}else{const{setRoots:e,getWorkspaceRoot:o}=await import("../core/workspace.js"),{ensureBackend:r,startStdioProxy:s}=await import("./backend-lifecycle.js"),{createInterface:i}=await import("node:readline"),n=t.createWriteStream("/tmp/pg-init-debug.log",{flags:"a"});n.write(`\n=== NEW SESSION ${(new Date).toISOString()} ===\n`);
4
4
  const c=i({input:process.stdin,terminal:!1}),a=[];
5
5
  let l=!1,p=null,d=null;
6
6
  const startProxy=async e=>{if(!l){l=!0,c.removeAllListeners("line"),c.close(),n.write(`RESOLVED: ${e}\n`),n.end();try{const t=await r(e);console.error(`[project-graph] Connected to backend on port ${t} (project: ${e})`),s(t,a)}catch(e){console.error(`[project-graph] Singleton failed (${e.message}), falling back to direct stdio`);const{startStdioServer:t}=await import("../mcp/mcp-server.js");t(a)}}};c.on("line",t=>{try{const r=JSON.parse(t);if(n.write(`IN: ${r.method||`response:${r.id}`}\n`),"initialize"===r.method){d=r.id,r.params?.roots?.length>0&&(e(r.params.roots),n.write("ROOTS from initialize.params\n"));
7
- const t=JSON.stringify({jsonrpc:"2.0",id:r.id,result:{protocolVersion:"2025-06-18",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:"2.0.0"}}});return n.write("OUT: initialize response\n"),void process.stdout.write(t+"\n")}if("initialized"===r.method||"notifications/initialized"===r.method){n.write("IN: initialized notification\n"),p=999999;
7
+ const t=JSON.stringify({jsonrpc:"2.0",id:r.id,result:{protocolVersion:"2025-06-18",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:_v}}});return n.write("OUT: initialize response\n"),void process.stdout.write(t+"\n")}if("initialized"===r.method||"notifications/initialized"===r.method){n.write("IN: initialized notification\n"),p=999999;
8
8
  const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p&&(n.write(`IN: roots/list response: ${JSON.stringify(r.result)}\n`),r.result?.roots?.length>0)){e(r.result.roots);
9
9
  const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}a.push(t)}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
@@ -1,8 +1,8 @@
1
1
  // @ctx .context/src/network/web-server.ctx
2
2
  import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{setRoots as _setRoots}from"../core/workspace.js";
3
- const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);function _rv(k){try{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"};
3
+ const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);let _pkgVersion="0.0.0";try{_pkgVersion=JSON.parse(t.readFileSync(o.join(p,"package.json"),"utf8")).version}catch{}function _rv(k){try{const r=_rq.resolve(k);const marker=o.sep+"node_modules"+o.sep+k.replace(/\//g,o.sep);const idx=r.lastIndexOf(marker);if(idx>=0)return r.substring(0,idx+marker.length);return o.dirname(r)}catch{return o.join(p,"node_modules",...k.split("/"))}}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
4
4
  function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
5
5
  function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
6
6
  function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
7
7
  function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
8
- export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n âŦĄ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n âŦĄ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
8
+ export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log(`\n âŦĄ project-graph-mcp 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}