project-graph-mcp 2.1.2 → 2.1.3

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 (69) hide show
  1. package/package.json +1 -1
  2. package/rules/test-rules.json +15 -0
  3. package/src/.project-graph-cache.json +1 -1
  4. package/src/analysis/analysis-cache.js +3 -1
  5. package/src/analysis/complexity.js +9 -13
  6. package/src/analysis/custom-rules.js +16 -35
  7. package/src/analysis/db-analysis.js +2 -6
  8. package/src/analysis/dead-code.js +8 -18
  9. package/src/analysis/full-analysis.js +9 -17
  10. package/src/analysis/jsdoc-checker.js +11 -23
  11. package/src/analysis/jsdoc-generator.js +8 -9
  12. package/src/analysis/similar-functions.js +8 -15
  13. package/src/analysis/test-annotations.js +12 -20
  14. package/src/analysis/type-checker.js +5 -7
  15. package/src/analysis/undocumented.js +10 -13
  16. package/src/cli/cli-handlers.js +4 -3
  17. package/src/compact/ai-context.js +2 -2
  18. package/src/compact/compact-migrate.js +8 -16
  19. package/src/compact/compact.js +3 -5
  20. package/src/compact/compress.js +7 -13
  21. package/src/compact/ctx-resolver.js +5 -0
  22. package/src/compact/ctx-to-jsdoc.js +13 -28
  23. package/src/compact/doc-dialect.js +18 -29
  24. package/src/compact/expand.js +10 -36
  25. package/src/compact/jsdoc-builder.js +5 -0
  26. package/src/compact/mode-config.js +6 -6
  27. package/src/compact/split-declarations.js +2 -0
  28. package/src/compact/validate-pipeline.js +7 -8
  29. package/src/core/event-bus.js +2 -1
  30. package/src/core/file-walker.js +4 -0
  31. package/src/core/filters.js +6 -5
  32. package/src/core/graph-builder.js +4 -11
  33. package/src/core/parser.js +19 -29
  34. package/src/core/utils.js +2 -0
  35. package/src/lang/lang-sql.js +7 -20
  36. package/src/mcp/mcp-server.js +2 -3
  37. package/src/mcp/tool-defs.js +1 -1
  38. package/src/mcp/tools.js +13 -21
  39. package/src/network/backend-lifecycle.js +15 -18
  40. package/src/network/local-gateway.js +10 -22
  41. package/src/network/mdns.js +5 -11
  42. package/src/network/server.js +1 -2
  43. package/src/network/web-server.js +7 -33
  44. package/web/app.js +19 -14
  45. package/web/components/code-block.js +1 -0
  46. package/web/components/quick-open.js +1 -0
  47. package/web/dashboard-state.js +1 -0
  48. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  49. package/web/panels/ActionBoard/ActionBoard.js +5 -4
  50. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  51. package/web/panels/EventItem/EventItem.css.js +1 -0
  52. package/web/panels/EventItem/EventItem.js +4 -4
  53. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  54. package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
  55. package/web/panels/ProjectItem/ProjectItem.js +3 -4
  56. package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
  57. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  58. package/web/panels/ProjectList/ProjectList.js +5 -4
  59. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  60. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  61. package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
  62. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  63. package/web/panels/code-viewer.js +1 -0
  64. package/web/panels/ctx-panel.js +1 -0
  65. package/web/panels/dep-graph.js +1 -0
  66. package/web/panels/file-tree.js +4 -188
  67. package/web/panels/health-panel.js +1 -0
  68. package/web/panels/live-monitor.js +1 -0
  69. package/web/state.js +7 -10
@@ -1,12 +1,11 @@
1
1
  // @ctx .context/src/mcp/mcp-server.ctx
2
- import e from"fs";
3
- 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";
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";
4
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;
5
4
  const r=new RegExp(`## ${s.topic}`,"i"),n=a.match(r);if(!n)return`Topic '${s.topic}' not found in guide.`;
6
5
  const i=n.index;
7
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
  let i=null;
9
- 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})},_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 errors, ${e.summary.astErrors} AST errors.`];return"PASS"===e.status&&t.push(`💡 ${e.summary.jsdocInjected} JSDoc blocks injected. Token savings: ${e.summary.tokenSavings}.`),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):[]}};
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):[]}};
10
9
  export function createServer(s){let n=1;
11
10
  const i=new Map;
12
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)}];
@@ -1,3 +1,3 @@
1
1
  // @ctx .context/src/mcp/tool-defs.ctx
2
2
  const e={get_skeleton:{name:"get_skeleton",description:"Get compact minified project overview (10-50x smaller than source). Returns legend, stats, and node summaries.",inputSchema:{type:"object",properties:{path:{type:"string",description:'Path to scan (e.g., "src/components")'}},required:["path"]}},get_focus_zone:{name:"get_focus_zone",description:"Get enriched context for recently modified files. Auto-detects from git or accepts explicit file list.",inputSchema:{type:"object",properties:{path:{type:"string"},useGitDiff:{type:"boolean",description:"Auto-detect from git diff"},recentFiles:{type:"array",items:{type:"string"},description:"Explicit list of files to expand"}}}},get_ai_context:{name:"get_ai_context",description:"Boot AI agent context: skeleton + doc-dialect + optional compressed files in one call. Call FIRST when starting work on a new project. Returns totalTokens and savings vs reading raw source.",inputSchema:{type:"object",properties:{path:{type:"string",description:"Project root path"},includeFiles:{type:"array",items:{type:"string"},description:'Specific files to include compressed (e.g., ["parser.js", "tools.js"])'},includeDocs:{type:"boolean",description:"Include doc-dialect documentation (default: true)"},includeSkeleton:{type:"boolean",description:"Include project skeleton (default: true)"}},required:["path"]}},invalidate_cache:{name:"invalidate_cache",description:"Invalidate the cached graph. Use after making code changes.",inputSchema:{type:"object",properties:{}}},get_usage_guide:{name:"get_usage_guide",description:"Get the comprehensive usage guide for project-graph with examples and best practices.\nCall this FIRST when planning how to analyze, navigate, or audit a codebase.\nReturns practical examples and recommended workflow for each feature area.\n\nAvailable topics: navigation, analysis, testing, documentation, rules, workflow.\nOmit topic to get the full guide.",inputSchema:{type:"object",properties:{topic:{type:"string",description:"Optional topic filter: navigation, analysis, testing, documentation, rules, workflow"}}}},get_agent_instructions:{name:"get_agent_instructions",description:"Get coding guidelines, architectural standards, and JSDoc rules for this project.",inputSchema:{type:"object",properties:{}}},get_custom_rules:{name:"get_custom_rules",description:"List all custom code analysis rules. Rules are stored in JSON files in rules/ directory.",inputSchema:{type:"object",properties:{}}},set_custom_rule:{name:"set_custom_rule",description:"Add or update a custom code analysis rule. Creates ruleset if it does not exist.",inputSchema:{type:"object",properties:{ruleSet:{type:"string",description:'Name of ruleset (e.g., "symbiote", "react", "custom")'},rule:{type:"object",description:"Rule definition with id, name, description, pattern, patternType, replacement, severity, filePattern"}},required:["ruleSet","rule"]}},check_custom_rules:{name:"check_custom_rules",description:"Run custom rules analysis on a directory. Returns violations found.",inputSchema:{type:"object",properties:{path:{type:"string",description:"Path to scan"},ruleSet:{type:"string",description:"Optional: specific ruleset to use"},severity:{type:"string",description:"Optional: filter by severity (error/warning/info)"}},required:["path"]}},get_framework_reference:{name:"get_framework_reference",description:"Get framework-specific AI reference documentation. Auto-detects framework from project or accepts explicit name. Returns full API reference, patterns, and common mistakes as agent context.",inputSchema:{type:"object",properties:{framework:{type:"string",description:'Framework reference name (e.g., "symbiote-3x"). If omitted, auto-detects from path.'},path:{type:"string",description:'Project path for auto-detection (e.g., "src/")'}}}}};
3
- export const TOOLS=[e.get_skeleton,e.get_focus_zone,e.get_ai_context,e.invalidate_cache,e.get_usage_guide,e.get_agent_instructions,e.get_custom_rules,e.set_custom_rule,e.check_custom_rules,e.get_framework_reference,{name:"navigate",description:"Navigate the project graph. Actions: expand|deps|usages|call_chain|sub_projects",inputSchema:{type:"object",properties:{action:{type:"string",enum:["expand","deps","usages","call_chain","sub_projects"],description:"Navigation action to perform"},symbol:{type:"string",description:"Symbol name (for expand, deps, usages)"},from:{type:"string",description:"Starting symbol (for call_chain)"},to:{type:"string",description:"Target symbol (for call_chain)"},path:{type:"string",description:"Path to scan (for call_chain, sub_projects)"}},required:["action"]}},{name:"analyze",description:"Code quality analysis. Actions: dead_code|similar_functions|complexity|large_files|outdated_patterns|full_analysis|analysis_summary|undocumented",inputSchema:{type:"object",properties:{action:{type:"string",enum:["dead_code","similar_functions","complexity","large_files","outdated_patterns","full_analysis","analysis_summary","undocumented"],description:"Analysis type to run"},path:{type:"string",description:"Path to scan"},minComplexity:{type:"number",description:"For complexity: minimum threshold (default: 1)"},onlyProblematic:{type:"boolean",description:"For complexity/large_files: only show issues"},threshold:{type:"number",description:"For similar_functions: min similarity % (default: 60)"},includeItems:{type:"boolean",description:"For full_analysis: include individual items"},level:{type:"string",enum:["tests","params","all"],description:"For undocumented: strictness level"},codeOnly:{type:"boolean",description:"For outdated_patterns: only check code"},depsOnly:{type:"boolean",description:"For outdated_patterns: only check deps"}},required:["action","path"]}},{name:"testing",description:"Test checklist management. Actions: pending|pass|fail|summary|reset",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pending","pass","fail","summary","reset"],description:"Test action to perform"},path:{type:"string",description:"Path to scan (for pending, summary)"},testId:{type:"string",description:"Test ID (for pass, fail)"},reason:{type:"string",description:"Failure reason (for fail)"}},required:["action"]}},{name:"filters",description:"Filter configuration. Actions: get|set|add_excludes|remove_excludes|reset",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","set","add_excludes","remove_excludes","reset"],description:"Filter action to perform"},excludeDirs:{type:"array",items:{type:"string"},description:"For set: directories to exclude"},excludePatterns:{type:"array",items:{type:"string"},description:"For set: file patterns to exclude"},useGitignore:{type:"boolean",description:"For set: use .gitignore patterns"},includeHidden:{type:"boolean",description:"For set: include hidden directories"},dirs:{type:"array",items:{type:"string"},description:"For add_excludes/remove_excludes"}},required:["action"]}},{name:"jsdoc",description:"JSDoc operations. Actions: check_consistency|check_types|generate",inputSchema:{type:"object",properties:{action:{type:"string",enum:["check_consistency","check_types","generate"],description:"JSDoc action to perform"},path:{type:"string",description:"Path to scan"},name:{type:"string",description:"For generate: specific function name"},files:{type:"array",items:{type:"string"},description:"For check_types: specific files"},maxDiagnostics:{type:"number",description:"For check_types: max diagnostics (default: 50)"}},required:["action","path"]}},{name:"docs",description:"Documentation (.ctx) management. Actions: get|generate|check_stale|validate_contracts",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","generate","check_stale","validate_contracts"],description:"Documentation action to perform"},path:{type:"string",description:"Project root path"},file:{type:"string",description:"For get: specific file docs"},overwrite:{type:"boolean",description:"For generate: overwrite existing (merge preserves descriptions)"},scope:{description:'For generate: "all", "focus" (git diff), or array of file paths'},strict:{type:"boolean",description:"For validate_contracts: report functions missing from .ctx"}},required:["action","path"]}},{name:"compact",description:"Compact code operations. Actions: compact_file|edit|compact_all|beautify|expand_file|expand_project|validate_pipeline|get_mode|set_mode",inputSchema:{type:"object",properties:{action:{type:"string",enum:["compact_file","edit","compact_all","beautify","expand_file","expand_project","validate_pipeline","get_mode","set_mode"],description:"Compact action to perform"},path:{type:"string",description:"Path to file or directory"},symbol:{type:"string",description:"For edit: function/class name to replace"},code:{type:"string",description:"For edit: new code for the symbol"},beautify:{type:"boolean",description:"Beautify output (default: true)"},legend:{type:"boolean",description:"For compact_file: include export legend"},dryRun:{type:"boolean",description:"Preview without modifying"},mode:{type:"number",description:"For set_mode: 1 (compact, recommended) or 2 (full)"},autoValidate:{type:"boolean",description:"For set_mode: auto-validate after edits"},stripJSDoc:{type:"boolean",description:"For set_mode: strip JSDoc when compacting"},strict:{type:"boolean",description:"For validate_pipeline: report fns missing from .ctx"}},required:["action"]}},{name:"db",description:"Database analysis. Actions: schema|table_usage|dead_tables",inputSchema:{type:"object",properties:{action:{type:"string",enum:["schema","table_usage","dead_tables"],description:"Database analysis action"},path:{type:"string",description:"Path to scan"},table:{type:"string",description:"For table_usage: filter to specific table"}},required:["action","path"]}}];
3
+ export const TOOLS=[e.get_skeleton,e.get_focus_zone,e.get_ai_context,e.invalidate_cache,e.get_usage_guide,e.get_agent_instructions,e.get_custom_rules,e.set_custom_rule,e.check_custom_rules,e.get_framework_reference,{name:"navigate",description:"Navigate the project graph. Actions: expand|deps|usages|call_chain|sub_projects",inputSchema:{type:"object",properties:{action:{type:"string",enum:["expand","deps","usages","call_chain","sub_projects"],description:"Navigation action to perform"},symbol:{type:"string",description:"Symbol name (for expand, deps, usages)"},from:{type:"string",description:"Starting symbol (for call_chain)"},to:{type:"string",description:"Target symbol (for call_chain)"},path:{type:"string",description:"Path to scan (for call_chain, sub_projects)"}},required:["action"]}},{name:"analyze",description:"Code quality analysis. Actions: dead_code|similar_functions|complexity|large_files|outdated_patterns|full_analysis|analysis_summary|undocumented",inputSchema:{type:"object",properties:{action:{type:"string",enum:["dead_code","similar_functions","complexity","large_files","outdated_patterns","full_analysis","analysis_summary","undocumented"],description:"Analysis type to run"},path:{type:"string",description:"Path to scan"},minComplexity:{type:"number",description:"For complexity: minimum threshold (default: 1)"},onlyProblematic:{type:"boolean",description:"For complexity/large_files: only show issues"},threshold:{type:"number",description:"For similar_functions: min similarity % (default: 60)"},includeItems:{type:"boolean",description:"For full_analysis: include individual items"},level:{type:"string",enum:["tests","params","all"],description:"For undocumented: strictness level"},codeOnly:{type:"boolean",description:"For outdated_patterns: only check code"},depsOnly:{type:"boolean",description:"For outdated_patterns: only check deps"}},required:["action","path"]}},{name:"testing",description:"Test checklist management. Actions: pending|pass|fail|summary|reset",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pending","pass","fail","summary","reset"],description:"Test action to perform"},path:{type:"string",description:"Path to scan (for pending, summary)"},testId:{type:"string",description:"Test ID (for pass, fail)"},reason:{type:"string",description:"Failure reason (for fail)"}},required:["action"]}},{name:"filters",description:"Filter configuration. Actions: get|set|add_excludes|remove_excludes|reset",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","set","add_excludes","remove_excludes","reset"],description:"Filter action to perform"},excludeDirs:{type:"array",items:{type:"string"},description:"For set: directories to exclude"},excludePatterns:{type:"array",items:{type:"string"},description:"For set: file patterns to exclude"},useGitignore:{type:"boolean",description:"For set: use .gitignore patterns"},includeHidden:{type:"boolean",description:"For set: include hidden directories"},dirs:{type:"array",items:{type:"string"},description:"For add_excludes/remove_excludes"}},required:["action"]}},{name:"jsdoc",description:"JSDoc operations. Actions: check_consistency|check_types|generate",inputSchema:{type:"object",properties:{action:{type:"string",enum:["check_consistency","check_types","generate"],description:"JSDoc action to perform"},path:{type:"string",description:"Path to scan"},name:{type:"string",description:"For generate: specific function name"},files:{type:"array",items:{type:"string"},description:"For check_types: specific files"},maxDiagnostics:{type:"number",description:"For check_types: max diagnostics (default: 50)"}},required:["action","path"]}},{name:"docs",description:"Documentation (.ctx) management. Actions: get|generate|check_stale|validate_contracts",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","generate","check_stale","validate_contracts"],description:"Documentation action to perform"},path:{type:"string",description:"Project root path"},file:{type:"string",description:"For get: specific file docs"},overwrite:{type:"boolean",description:"For generate: overwrite existing (merge preserves descriptions)"},scope:{description:'For generate: "all", "focus" (git diff), or array of file paths'},strict:{type:"boolean",description:"For validate_contracts: report functions missing from .ctx"}},required:["action","path"]}},{name:"compact",description:"Compact code operations. Actions: compact_file|edit|compact_all|beautify|expand_file|expand_project|validate_pipeline|get_mode|set_mode",inputSchema:{type:"object",properties:{action:{type:"string",enum:["compact_file","edit","compact_all","beautify","expand_file","expand_project","validate_pipeline","get_mode","set_mode"],description:"Compact action to perform"},path:{type:"string",description:"Path to file or directory"},symbol:{type:"string",description:"For edit: function/class name to replace"},code:{type:"string",description:"For edit: new code for the symbol"},beautify:{type:"boolean",description:"Beautify output (default: true)"},legend:{type:"boolean",description:"For compact_file: include export legend"},dryRun:{type:"boolean",description:"Preview without modifying"},mode:{type:"number",description:"For set_mode: 1 (compact, recommended) or 2 (full)"},autoValidate:{type:"boolean",description:"For set_mode: auto-validate after edits"},stripJSDoc:{type:"boolean",description:"For set_mode: strip JSDoc when compacting"},strict:{type:"boolean",description:"For validate_pipeline: report fns missing from .ctx"},fix:{type:"boolean",description:"For validate_pipeline: auto-fix all style issues — generates .ctx documentation from readable code, then minifies (headers, imports, indentation, long names). Bidirectional: expand restores from .ctx."}},required:["action"]}},{name:"db",description:"Database analysis. Actions: schema|table_usage|dead_tables",inputSchema:{type:"object",properties:{action:{type:"string",enum:["schema","table_usage","dead_tables"],description:"Database analysis action"},path:{type:"string",description:"Path to scan"},table:{type:"string",description:"For table_usage: filter to specific table"}},required:["action","path"]}}];
package/src/mcp/tools.js CHANGED
@@ -1,25 +1,17 @@
1
1
  // @ctx .context/src/mcp/tools.ctx
2
- import{parseProject as e,parseFile as t,findJSFiles as n,findAllProjectFiles as r}from"../core/parser.js";import{buildGraph as s,createSkeleton as o}from"../core/graph-builder.js";import{readFileSync as c,statSync as i,writeFileSync as a,existsSync as l,unlinkSync as f}from"fs";import{execSync as u}from"child_process";import{join as p}from"path";
3
- let h=null,d=null,m=new Map;function saveDiskCache(e,t){try{const n=p(e,".project-graph-cache.json"),r={version:1,path:e,mtimes:Object.fromEntries(m),graph:t};a(n,JSON.stringify(r),"utf-8")}catch(e){}}
4
- function loadDiskCache(e){try{const t=p(e,".project-graph-cache.json");if(!l(t))return!1;
5
- const n=c(t,"utf-8"),r=JSON.parse(n);if(1!==r.version||r.path!==e)return!1;m.clear();for(const[e,t]of Object.entries(r.mtimes))m.set(e,t);return h=r.graph,d=e,!detectChanges(e)||(h=null,d=null,m.clear(),!1)}catch(e){return!1}}
6
- export async function getGraph(t){if(h&&d===t){if(!detectChanges(t))return h}else if(!h&&loadDiskCache(t))return h;
7
- const n=await e(t);return h=s(n),d=t,snapshotMtimes(t),saveDiskCache(t,h),h}
8
- function detectChanges(e){if(0===m.size)return!0;try{const t=n(e),r=new Set(t),s=new Set(m.keys());if(t.length!==m.size)return!0;for(const e of t)if(!s.has(e))return!0;for(const e of s)if(!r.has(e))return!0;for(const e of t)try{if(i(e).mtimeMs!==m.get(e))return!0}catch{return!0}return!1}catch{return!0}}
9
- function snapshotMtimes(e){m.clear();try{const t=n(e);for(const e of t)try{m.set(e,i(e).mtimeMs)}catch{}}catch{}}
2
+ import{parseProject as e,parseFile as t,findJSFiles as n,findAllProjectFiles as r}from"../core/parser.js";
3
+ import{buildGraph as s,createSkeleton as o}from"../core/graph-builder.js";
4
+ import{readFileSync as c,statSync as i,writeFileSync as a,existsSync as l,unlinkSync as f}from"fs";
5
+ import{execSync as u}from"child_process";
6
+ import{join as p}from"path";
7
+ let h=null,d=null,m=new Map;
8
+ export async function getGraph(t){if(h&&d===t){if(!g(t))return h}else if(!h&&function(e){try{const t=p(e,".project-graph-cache.json");if(!l(t))return!1;const n=c(t,"utf-8"),r=JSON.parse(n);if(1!==r.version||r.path!==e)return!1;m.clear();for(const[e,t]of Object.entries(r.mtimes))m.set(e,t);return h=r.graph,d=e,!g(e)||(h=null,d=null,m.clear(),!1)}catch(e){return!1}}(t))return h;const n=await e(t);return h=s(n),d=t,y(t),function(e,t){try{const n=p(e,".project-graph-cache.json"),r={version:1,path:e,mtimes:Object.fromEntries(m),graph:t};a(n,JSON.stringify(r),"utf-8")}catch(e){}}(t,h),h}
9
+ function g(e){if(0===m.size)return!0;try{const t=n(e),r=new Set(t),s=new Set(m.keys());if(t.length!==m.size)return!0;for(const e of t)if(!s.has(e))return!0;for(const e of s)if(!r.has(e))return!0;for(const e of t)try{if(i(e).mtimeMs!==m.get(e))return!0}catch{return!0}return!1}catch{return!0}}
10
+ function y(e){m.clear();try{const t=n(e);for(const e of t)try{m.set(e,i(e).mtimeMs)}catch{}}catch{}}
10
11
  export async function getSkeleton(e){const t=await getGraph(e),n=r(e);return o(t,n)}
11
- export async function getFocusZone(e={}){const n=e.path||"src/components",r=await getGraph(n);
12
- let s=e.recentFiles||[];if(e.useGitDiff)try{s=u("git diff --name-only HEAD~5",{encoding:"utf-8"}).split("\n").filter(e=>e.endsWith(".js"))}catch(e){}const o={};for(const e of s){const n=c(e,"utf-8"),s=await t(n,e);for(const e of s.classes){const t=r.legend[e.name];t&&r.nodes[t]&&(o[t]={...r.nodes[t],methods:e.methods,properties:e.properties,file:e.file,line:e.line})}}return{focusFiles:s,expanded:o,expandable:Object.keys(r.nodes).filter(e=>!o[e])}}
13
- export async function expand(t){const n=d||"src/components",r=await getGraph(n),[s,o]=t.split("."),i=r.reverseLegend[s];if(!i)return{error:`Unknown symbol: ${t}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.`};
14
- const a=await e(n),l=a.classes.find(e=>e.name===i),f=a.functions.find(e=>e.name===i);if(!l&&!f)return{error:`Symbol not found: ${i}`};if(f&&!o)return{symbol:t,fullName:i,type:"function",file:f.file,line:f.line,exported:f.exported,calls:f.calls};if(o&&l){const e=r.reverseLegend[o]||o,n=extractMethod(c(l.file,"utf-8"),e);return{symbol:t,fullName:`${i}.${e}`,file:l.file,line:l.line,code:n}}return{symbol:t,fullName:i,file:l.file,line:l.line,extends:l.extends,methods:l.methods,properties:l.properties,calls:l.calls}}
15
- export async function deps(e){const t=d||"src/components",n=await getGraph(t),r=n.nodes[e];if(!r)return{error:`Unknown symbol: ${e}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.`};
16
- const s=n.edges.filter(t=>t[2].startsWith(e)).map(e=>e[0]),o=n.edges.filter(t=>t[0]===e).map(e=>e[2]);return{symbol:e,imports:r.i||[],usedBy:[...new Set(s)],calls:[...new Set(o)]}}
12
+ export async function getFocusZone(e={}){const n=e.path||"src/components",r=await getGraph(n);let s=e.recentFiles||[];if(e.useGitDiff)try{s=u("git diff --name-only HEAD~5",{encoding:"utf-8"}).split("\n").filter(e=>e.endsWith(".js"))}catch(e){}const o={};for(const e of s){const n=c(e,"utf-8"),s=await t(n,e);for(const e of s.classes){const t=r.legend[e.name];t&&r.nodes[t]&&(o[t]={...r.nodes[t],methods:e.methods,properties:e.properties,file:e.file,line:e.line})}}return{focusFiles:s,expanded:o,expandable:Object.keys(r.nodes).filter(e=>!o[e])}}
13
+ export async function expand(t){const n=d||"src/components",r=await getGraph(n),[s,o]=t.split("."),i=r.reverseLegend[s];if(!i)return{error:`Unknown symbol: ${t}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.`};const a=await e(n),l=a.classes.find(e=>e.name===i),f=a.functions.find(e=>e.name===i);if(!l&&!f)return{error:`Symbol not found: ${i}`};if(f&&!o)return{symbol:t,fullName:i,type:"function",file:f.file,line:f.line,exported:f.exported,calls:f.calls};if(o&&l){const e=r.reverseLegend[o]||o,n=function(e,t){const n=new RegExp(`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)(?:async\\s+)?${t}\\s*\\([^)]*\\)\\s*{`,"g").exec(e);if(!n)return"";const r=n.index;let s=0,o=n.index+n[0].length-1;for(;o<e.length;){if("{"===e[o])s++;else if("}"===e[o]&&(s--,0===s))return e.slice(r,o+1);o++}return e.slice(r)}(c(l.file,"utf-8"),e);return{symbol:t,fullName:`${i}.${e}`,file:l.file,line:l.line,code:n}}return{symbol:t,fullName:i,file:l.file,line:l.line,extends:l.extends,methods:l.methods,properties:l.properties,calls:l.calls}}
14
+ export async function deps(e){const t=d||"src/components",n=await getGraph(t),r=n.nodes[e];if(!r)return{error:`Unknown symbol: ${e}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.`};const s=n.edges.filter(t=>t[2].startsWith(e)).map(e=>e[0]),o=n.edges.filter(t=>t[0]===e).map(e=>e[2]);return{symbol:e,imports:r.i||[],usedBy:[...new Set(s)],calls:[...new Set(o)]}}
17
15
  export async function usages(t){const n=d||"src/components",r=await getGraph(n),s=await e(n),o=r.reverseLegend[t]||t,c=[];for(const e of s.classes)(e.calls?.includes(o)||e.calls?.some(e=>e.includes(o)))&&c.push({file:e.file,line:e.line,context:`${e.name} calls ${o}`});return c}
18
- function extractMethod(e,t){const n=new RegExp(`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)(?:async\\s+)?${t}\\s*\\([^)]*\\)\\s*{`,"g").exec(e);if(!n)return"";
19
- const r=n.index;
20
- let s=0,o=n.index+n[0].length-1;for(;o<e.length;){if("{"===e[o])s++;else if("}"===e[o]&&(s--,0===s))return e.slice(r,o+1);o++}return e.slice(r)}
21
- export async function getCallChain(e={}){const{from:t,to:n,path:r}=e;if(!t||!n)return{error:'Both "from" and "to" parameters are required'};
22
- const s=r||d||"src/components",o=await getGraph(s),c=o.legend[t]||t,i=o.legend[n]||n,a={};for(const[e,t,n]of o.edges)a[e]||(a[e]=[]),a[e].push(n);
23
- const l=[{current:c,path:[c]}],f=new Set,u=new Set;for(f.add(c);l.length>0;){const{current:e,path:t}=l.shift(),n=e.split(".")[0],r=e.split(".")[1];if(e===i||n===i||r===i)return t.map(e=>{const t=e.split("."),n=o.reverseLegend[t[0]]||t[0];return 2===t.length?`${n}.${o.reverseLegend[t[1]]||t[1]}`:n});if(u.has(n))continue;u.add(n);
24
- const s=a[n]||[];for(const e of s)f.has(e)||(f.add(e),l.push({current:e,path:[...t,e]}))}return{error:`No call path found from "${t}" to "${n}"`}}
16
+ export async function getCallChain(e={}){const{from:t,to:n,path:r}=e;if(!t||!n)return{error:'Both "from" and "to" parameters are required'};const s=r||d||"src/components",o=await getGraph(s),c=o.legend[t]||t,i=o.legend[n]||n,a={};for(const[e,t,n]of o.edges)a[e]||(a[e]=[]),a[e].push(n);const l=[{current:c,path:[c]}],f=new Set,u=new Set;for(f.add(c);l.length>0;){const{current:e,path:t}=l.shift(),n=e.split(".")[0],r=e.split(".")[1];if(e===i||n===i||r===i)return t.map(e=>{const t=e.split("."),n=o.reverseLegend[t[0]]||t[0];return 2===t.length?`${n}.${o.reverseLegend[t[1]]||t[1]}`:n});if(u.has(n))continue;u.add(n);const s=a[n]||[];for(const e of s)f.has(e)||(f.add(e),l.push({current:e,path:[...t,e]}))}return{error:`No call path found from "${t}" to "${n}"`}}
25
17
  export function invalidateCache(){if(d)try{const e=p(d,".project-graph-cache.json");l(e)&&f(e)}catch(e){}h=null,d=null,m.clear()}
@@ -1,19 +1,16 @@
1
1
  // @ctx .context/src/network/backend-lifecycle.ctx
2
- import{createHash as e,randomBytes as t}from"node:crypto";import{existsSync as r,mkdirSync as o,readFileSync as n,writeFileSync as c,unlinkSync as s,readdirSync as i}from"node:fs";import{join as a,resolve as l,basename as f}from"node:path";import{spawn as d}from"node:child_process";import{createInterface as u}from"node:readline";import{createConnection as p}from"node:net";import{fileURLToPath as h}from"node:url";
3
- const m=a(h(import.meta.url),".."),g=a(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway","backends");function getPortFilePath(t){const r=l(t),o=e("md5").update(r).digest("hex").slice(0,8);return a(g,`${o}.json`)}
4
- function readPortFile(e){const t=getPortFilePath(e);if(!r(t))return null;try{const e=JSON.parse(n(t,"utf8"));try{process.kill(e.pid,0)}catch{try{s(t)}catch{}return null}return e}catch{return null}}
5
- export function writePortFile(e,t){o(g,{recursive:!0});
6
- const r=l(e),n={port:t,pid:process.pid,project:r,name:f(r)||"root",startedAt:Date.now()};c(getPortFilePath(e),JSON.stringify(n,null,2))}
7
- export function removePortFile(e){try{s(getPortFilePath(e))}catch{}}
8
- export function listBackends(){if(!r(g))return[];
9
- const e=i(g).filter(e=>e.endsWith(".json")),t=[];for(const r of e)try{const e=JSON.parse(n(a(g,r),"utf8"));try{process.kill(e.pid,0),t.push(e)}catch{try{s(a(g,r))}catch{}}}catch{}return t}
10
- export async function ensureBackend(e){const t=l(e),o=readPortFile(t);if(o)return o.port;
11
- const n=a(m,"backend.js");d(process.execPath,[n,t],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref();
12
- const c=getPortFilePath(t),s=Date.now();for(;Date.now()-s<1e4;)if(await new Promise(e=>setTimeout(e,200)),r(c)){const e=readPortFile(t);if(e)return e.port}throw new Error("Backend failed to start within 10s")}
13
- export function startStdioProxy(e,r=[]){const o=t(16).toString("base64"),n=p({host:"127.0.0.1",port:e},()=>{n.write(`GET /mcp-ws HTTP/1.1\r\nHost: 127.0.0.1:${e}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${o}\r\nSec-WebSocket-Version: 13\r\n\r\n`)});
14
- let c=!1,s=Buffer.alloc(0),i=[...r];
15
- const a=u({input:process.stdin,terminal:!1});function encodeClientFrame(e){const r=Buffer.from(e,"utf8"),o=t(4),n=Buffer.alloc(r.length);for(let e=0;e<r.length;e++)n[e]=r[e]^o[e%4];
16
- 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,o,n])}
17
- function decodeFrame(e){if(e.length<2)return null;
18
- const t=15&e[0];
19
- let r=127&e[1],o=2;if(126===r){if(e.length<4)return null;r=e.readUInt16BE(2),o=4}else if(127===r){if(e.length<10)return null;r=Number(e.readBigUInt64BE(2)),o=10}return e.length<o+r?null:{opcode:t,data:e.slice(o,o+r).toString("utf8"),totalLen:o+r}}a.on("line",e=>{if(c)try{n.write(encodeClientFrame(e))}catch{}else i.push(e)}),a.on("close",()=>{n.end(),process.exit(0)}),n.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{n.write(encodeClientFrame(e))}catch{}i=[]}for(;s.length>=2;){const e=decodeFrame(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,n.write(e)}}}),n.on("close",()=>process.exit(0)),n.on("error",e=>{console.error(`[project-graph] Proxy connection error: ${e.message}`),process.exit(1)})}
2
+ import{createHash as e,randomBytes as t}from"node:crypto";
3
+ import{existsSync as r,mkdirSync as n,readFileSync as o,writeFileSync as c,unlinkSync as s,readdirSync as i}from"node:fs";
4
+ import{join as a,resolve as l,basename as f}from"node:path";
5
+ import{spawn as u}from"node:child_process";
6
+ import{createInterface as p}from"node:readline";
7
+ import{createConnection as d}from"node:net";
8
+ import{fileURLToPath as h}from"node:url";
9
+ const m=a(h(import.meta.url),".."),g=a(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway","backends");
10
+ function y(t){const r=l(t),n=e("md5").update(r).digest("hex").slice(0,8);return a(g,`${n}.json`)}
11
+ 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 removePortFile(e){try{s(y(e))}catch{}}
14
+ 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 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,23 +1,11 @@
1
1
  // @ctx .context/src/network/local-gateway.ctx
2
- import e from"node:http";
3
- import t from"node:net";
4
- import r from"node:fs";
5
- import n from"node:path";import{registerLocal as o}from"./mdns.js";
6
- const s=n.join(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway"),i=n.join(s,"services.json"),a=n.join(s,"gateway.pid");function readRegistry(){try{return JSON.parse(r.readFileSync(i,"utf8"))}catch{return{}}}
7
- function writeRegistry(e){r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify(e,null,2))}
8
- export function registerService(e,t,r={}){const n=`${e}.local`,s=readRegistry();if(r.projectName){s[n]||(s[n]={name:e,routes:{}});
9
- const o=`/${r.projectName}`;s[n].routes=s[n].routes||{},s[n].routes[o]={port:t,pid:process.pid,projectPath:r.projectPath,projectName:r.projectName}}else s[n]={port:t,pid:process.pid,name:e};writeRegistry(s);
10
- const i=o(n,80);ensureGateway();
11
- const cleanup=()=>{i.cleanup();try{const e=readRegistry();r.projectName&&e[n]?.routes?(delete e[n].routes[`/${r.projectName}`],0===Object.keys(e[n].routes).length&&delete e[n]):delete e[n],writeRegistry(e),0===Object.keys(e).length&&stopGateway()}catch{}};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit()}),process.on("SIGTERM",()=>{cleanup(),process.exit()});
12
- const a=getGatewayPort(),c=80===a?"":`:${a}`,p=r.projectName?`http://${n}${c}/${r.projectName}/`:`http://${n}${c}/`;return{cleanup:cleanup,url:p,directUrl:`http://localhost:${t}/`}}
13
- function resolveBackend(e,t,r){const n=r[e];if(!n)return null;if(n.routes){const e=Object.keys(n.routes).sort((e,t)=>t.length-e.length);for(const r of e)if(t===r||t.startsWith(r+"/")){const e=n.routes[r];try{process.kill(e.pid,0)}catch{continue}const o=t.slice(r.length)||"/";return{port:e.port,rewritePath:o,prefix:r}}for(const r of e)try{const e=n.routes[r];process.kill(e.pid,0);
14
- const o="/"===t||""===t?"/dashboard.html":t;return{port:e.port,rewritePath:o}}catch{continue}}if(n.port){const e="/"===t||""===t?"/dashboard.html":t;return{port:n.port,rewritePath:e}}return null}
15
- function readGatewayPid(){try{const e=r.readFileSync(a,"utf8");return e.trim().startsWith("{")?JSON.parse(e):{pid:parseInt(e,10),port:80}}catch{return null}}
16
- function isGatewayRunning(){const e=readGatewayPid();if(!e)return!1;try{return process.kill(e.pid,0),!0}catch{return!1}}
17
- export function getGatewayPort(){const e=readGatewayPid();return e?.port||80}
18
- function ensureGateway(){if(!isGatewayRunning())try{const n=e.createServer((t,r)=>{const n=(t.headers.host||"").split(":")[0],o=readRegistry(),s=resolveBackend(n,t.url,o);if(!s)return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${n}\nRegistered: ${Object.keys(o).join(", ")}`);if("/api/gateway-info"===t.url){const e=JSON.stringify(o["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(e)}const i=e.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:t.method,headers:{...t.headers,host:`localhost:${s.port}`}},e=>{if((e.headers["content-type"]||"").includes("text/html")&&s.prefix){const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{let n=Buffer.concat(t).toString("utf8");
19
- const o=`<base href="${s.prefix}/">`;n=n.includes("<head>")?n.replace("<head>",`<head>\n ${o}`):o+"\n"+n;
20
- const i=Buffer.from(n,"utf8"),a={...e.headers};a["content-length"]=i.length,delete a["transfer-encoding"],r.writeHead(e.statusCode,a),r.end(i)})}else r.writeHead(e.statusCode,e.headers),e.pipe(r)});i.on("error",()=>{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}),t.pipe(i)});function startListening(e){n.listen(e,"0.0.0.0",()=>{const e=n.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(a,JSON.stringify({pid:process.pid,port:e}))})}n.on("upgrade",(e,r,n)=>{const o=(e.headers.host||"").split(":")[0],s=readRegistry(),i=resolveBackend(o,e.url,s);if(!i||i.isDashboard)return void r.destroy();
21
- const a=t.createConnection({host:"127.0.0.1",port:i.port},()=>{const t=i.rewritePath,o=`${e.method} ${t} HTTP/1.1\r\n`+Object.entries(e.headers).map(([e,t])=>`${e}: ${t}`).join("\r\n")+"\r\n\r\n";a.write(o),n.length&&a.write(n);
22
- let s=Buffer.alloc(0);a.on("data",function onFirstData(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),a.removeListener("data",onFirstData),r.pipe(a),a.pipe(r))})});a.on("error",e=>{console.error("WS PROXY ERROR:",e.message),r.destroy()}),r.on("error",e=>{console.error("WS CLIENT ERROR:",e.message),a.destroy()})}),n.on("error",e=>{"EACCES"===e.code&&!1===n.listening?startListening(8080):"EADDRINUSE"===e.code&&n.listening}),startListening(80)}catch{}}
23
- function stopGateway(){try{r.unlinkSync(a),r.unlinkSync(i)}catch{}}
2
+ import t from"node:http";import e from"node:net";import r from"node:fs";import o from"node:path";import{registerLocal as n}from"./mdns.js";
3
+ const s=o.join(process.env.HOME||process.env.USERPROFILE||"/tmp",".local-gateway"),c=o.join(s,"services.json"),i=o.join(s,"gateway.pid"),a=o.join(s,"backends");
4
+ function p(){try{return JSON.parse(r.readFileSync(c,"utf8"))}catch{return{}}}
5
+ function l(t){r.mkdirSync(s,{recursive:!0}),r.writeFileSync(c,JSON.stringify(t,null,2))}
6
+ function d(){if(!r.existsSync(a))return;const t=r.readdirSync(a).filter(t=>t.endsWith(".json"));let e=!1;const n=p();for(const s of t)try{const t=JSON.parse(r.readFileSync(o.join(a,s),"utf8"));try{process.kill(t.pid,0)}catch{r.unlinkSync(o.join(a,s));continue}const c=t.name||"root",i=`/${c}`;n["project-graph.local"]=n["project-graph.local"]||{name:"project-graph",routes:{}},n["project-graph.local"].routes[i]||(n["project-graph.local"].routes[i]={port:t.port,pid:t.pid,projectPath:t.project,projectName:c},e=!0)}catch{}e&&l(n)}
7
+ export function registerService(t,e,r={}){const o=`${t}.local`,s=p();if(r.projectName){s[o]||(s[o]={name:t,routes:{}});const n=`/${r.projectName}`;s[o].routes=s[o].routes||{},s[o].routes[n]={port:e,pid:process.pid,projectPath:r.projectPath,projectName:r.projectName}}else s[o]={port:e,pid:process.pid,name:t};l(s);const c=n(o,80);f();const i=()=>{c.cleanup()};process.on("exit",i),process.on("SIGINT",()=>{i(),process.exit()}),process.on("SIGTERM",()=>{i(),process.exit()});const a=getGatewayPort(),d=80===a?"":`:${a}`,u=r.projectName?`http://${o}${d}/${r.projectName}/`:`http://${o}${d}/`;return{cleanup:i,url:u,directUrl:`http://localhost:${e}/`}}
8
+ function u(t,e,r){const o=r[t];if(!o)return null;if(o.routes){const t=Object.keys(o.routes).sort((t,e)=>e.length-t.length);for(const r of t)if(e===r||e.startsWith(r+"/")){const t=o.routes[r];try{process.kill(t.pid,0)}catch{continue}const n=e.slice(r.length)||"/";return{port:t.port,rewritePath:n,prefix:r}}for(const r of t)try{const t=o.routes[r];process.kill(t.pid,0);const n="/"===e||""===e?"/dashboard.html":e;return{port:t.port,rewritePath:n}}catch{continue}}if(o.port){const t="/"===e||""===e?"/dashboard.html":e;return{port:o.port,rewritePath:t}}return null}
9
+ function h(){try{const t=r.readFileSync(i,"utf8");return t.trim().startsWith("{")?JSON.parse(t):{pid:parseInt(t,10),port:80}}catch{return null}}
10
+ export function getGatewayPort(){const t=h();return t?.port||80}
11
+ function f(){if(!function(){const t=h();if(!t)return!1;try{return process.kill(t.pid,0),!0}catch{return!1}}())try{const o=t.createServer((e,r)=>{const o=(e.headers.host||"").split(":")[0];let n=p(),s=u(o,e.url,n);if(!s&&(d(),n=p(),s=u(o,e.url,n),!s))return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${o}\nRegistered: ${Object.keys(n).join(", ")}`);if("/api/gateway-info"===e.url){const t=JSON.stringify(n["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(t)}if("POST"===e.method&&"/api/remove-project"===e.url){let t="";return e.on("data",e=>t+=e),void e.on("end",()=>{try{const e=JSON.parse(t).route,o=p();if(o["project-graph.local"]?.routes?.[e]){const t=o["project-graph.local"].routes[e];try{process.kill(t.pid,9)}catch{}delete o["project-graph.local"].routes[e],l(o)}r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))}catch(t){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({error:t.message}))}})}const c=t.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:e.method,headers:{...e.headers,host:`localhost:${s.port}`}},t=>{if((t.headers["content-type"]||"").includes("text/html")&&s.prefix){const e=[];t.on("data",t=>e.push(t)),t.on("end",()=>{let o=Buffer.concat(e).toString("utf8");const n=`<base href="${s.prefix}/">`;o=o.includes("<head>")?o.replace("<head>",`<head>\n ${n}`):n+"\n"+o;const c=Buffer.from(o,"utf8"),i={...t.headers};i["content-length"]=c.length,delete i["transfer-encoding"],r.writeHead(t.statusCode,i),r.end(c)})}else r.writeHead(t.statusCode,t.headers),t.pipe(r)});c.on("error",()=>{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}),e.pipe(c)});function n(t){o.listen(t,"0.0.0.0",()=>{const t=o.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify({pid:process.pid,port:t})),d()})}o.on("upgrade",(t,r,o)=>{const n=(t.headers.host||"").split(":")[0],s=p(),c=u(n,t.url,s);if(!c||c.isDashboard)return void r.destroy();const i=e.createConnection({host:"127.0.0.1",port:c.port},()=>{const e=c.rewritePath,n=`${t.method} ${e} HTTP/1.1\r\n`+Object.entries(t.headers).map(([t,e])=>`${t}: ${e}`).join("\r\n")+"\r\n\r\n";i.write(n),o.length&&i.write(o);let s=Buffer.alloc(0);i.on("data",function t(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),i.removeListener("data",t),r.pipe(i),i.pipe(r))})});i.on("error",t=>{console.error("WS PROXY ERROR:",t.message),r.destroy()}),r.on("error",t=>{console.error("WS CLIENT ERROR:",t.message),i.destroy()})}),o.on("error",t=>{"EACCES"===t.code&&!1===o.listening?n(8080):"EADDRINUSE"===t.code&&o.listening}),n(80)}catch{}}
@@ -1,13 +1,7 @@
1
1
  // @ctx .context/src/network/mdns.ctx
2
- import{spawn as t}from"node:child_process";
3
- import e from"node:dgram";
2
+ import{spawn as t}from"node:child_process";import e from"node:dgram";
4
3
  const r="224.0.0.251";
5
- export function registerLocal(t,e){if("darwin"===process.platform)return registerDnsSd(t,e);if("linux"===process.platform){const e=tryAvahi(t);if(e)return e}return registerMcast(t)}
6
- function registerDnsSd(e,r){const n=t("dns-sd",["-P","Project Graph","_http._tcp","",String(r),e,"127.0.0.1"],{stdio:"ignore",detached:!1});return n.unref(),{method:"Bonjour (dns-sd)",cleanup:()=>{try{n.kill()}catch{}}}}
7
- function tryAvahi(e){try{const r=t("avahi-publish-address",["-R",e,"127.0.0.1"],{stdio:"ignore",detached:!1});
8
- let n=!1;return r.on("error",()=>{n=!0}),r.unref(),n?null:{method:"Avahi",cleanup:()=>{try{r.kill()}catch{}}}}catch{return null}}
9
- function registerMcast(t){const n=t.split("."),c=Buffer.concat([...n.map(t=>{const e=Buffer.alloc(1+t.length);return e[0]=t.length,e.write(t,1,"ascii"),e}),Buffer.from([0])]);
10
- let o;try{o=e.createSocket({type:"udp4",reuseAddr:!0})}catch{return{method:"none",cleanup:()=>{}}}o.on("message",t=>{if(t.length<12)return;if(32768&t.readUInt16BE(2))return;if(0===t.readUInt16BE(4))return;if(12+c.length+4>t.length)return;if(0!==t.compare(c,0,c.length,12,12+c.length))return;
11
- const e=12+c.length,n=t.readUInt16BE(e),i=32767&t.readUInt16BE(e+2);if(1!==n||1!==i)return;
12
- const s=Buffer.alloc(12+c.length+10+4);
13
- let a=0;s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(33792,a),a+=2,s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(1,a),a+=2,s.writeUInt16BE(0,a),a+=2,s.writeUInt16BE(0,a),a+=2,c.copy(s,a),a+=c.length,s.writeUInt16BE(1,a),a+=2,s.writeUInt16BE(32769,a),a+=2,s.writeUInt32BE(120,a),a+=4,s.writeUInt16BE(4,a),a+=2,s[a++]=127,s[a++]=0,s[a++]=0,s[a++]=1,o.send(s,0,a,5353,r)}),o.on("error",()=>{try{o.close()}catch{}});try{o.bind({port:5353,exclusive:!1},()=>{try{o.addMembership(r),o.setMulticastTTL(255)}catch{try{o.close()}catch{}}})}catch{return{method:"none",cleanup:()=>{}}}return{method:"Node.js mDNS",cleanup:()=>{try{o.close()}catch{}}}}
4
+ export function registerLocal(t,e){if("darwin"===process.platform)return n(t,e);if("linux"===process.platform){const e=c(t);if(e)return e}return o(t)}
5
+ function n(e,r){const n=t("dns-sd",["-P","Project Graph","_http._tcp","",String(r),e,"127.0.0.1"],{stdio:"ignore",detached:!1});return n.unref(),{method:"Bonjour (dns-sd)",cleanup:()=>{try{n.kill()}catch{}}}}
6
+ function c(e){try{const r=t("avahi-publish-address",["-R",e,"127.0.0.1"],{stdio:"ignore",detached:!1});let n=!1;return r.on("error",()=>{n=!0}),r.unref(),n?null:{method:"Avahi",cleanup:()=>{try{r.kill()}catch{}}}}catch{return null}}
7
+ function o(t){const n=t.split("."),c=Buffer.concat([...n.map(t=>{const e=Buffer.alloc(1+t.length);return e[0]=t.length,e.write(t,1,"ascii"),e}),Buffer.from([0])]);let o;try{o=e.createSocket({type:"udp4",reuseAddr:!0})}catch{return{method:"none",cleanup:()=>{}}}o.on("message",t=>{if(t.length<12)return;if(32768&t.readUInt16BE(2))return;if(0===t.readUInt16BE(4))return;if(12+c.length+4>t.length)return;if(0!==t.compare(c,0,c.length,12,12+c.length))return;const e=12+c.length,n=t.readUInt16BE(e),i=32767&t.readUInt16BE(e+2);if(1!==n||1!==i)return;const a=Buffer.alloc(12+c.length+10+4);let l=0;a.writeUInt16BE(0,l),l+=2,a.writeUInt16BE(33792,l),l+=2,a.writeUInt16BE(0,l),l+=2,a.writeUInt16BE(1,l),l+=2,a.writeUInt16BE(0,l),l+=2,a.writeUInt16BE(0,l),l+=2,c.copy(a,l),l+=c.length,a.writeUInt16BE(1,l),l+=2,a.writeUInt16BE(32769,l),l+=2,a.writeUInt32BE(120,l),l+=4,a.writeUInt16BE(4,l),l+=2,a[l++]=127,a[l++]=0,a[l++]=0,a[l++]=1,o.send(a,0,l,5353,r)}),o.on("error",()=>{try{o.close()}catch{}});try{o.bind({port:5353,exclusive:!1},()=>{try{o.addMembership(r),o.setMulticastTTL(255)}catch{try{o.close()}catch{}}})}catch{return{method:"none",cleanup:()=>{}}}return{method:"Node.js mDNS",cleanup:()=>{try{o.close()}catch{}}}}
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // @ctx .context/src/network/server.ctx
3
- import e from"node:path";
4
- import t from"node:fs";if(process.argv[1]&&(process.argv[1].endsWith("server.js")||process.argv[1].endsWith("project-graph-mcp"))){const[,,o,...r]=process.argv;if("serve"===o){const t=r[0]||".",o=r.indexOf("--port"),s=-1!==o?parseInt(r[o+1],10):0;if(s){const{startWebServer:e}=await import("./web-server.js");e(t,s)}else{const{ensureBackend:o}=await import("./backend-lifecycle.js");try{const r=await o(t),s=e.resolve(t);console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${r}/`),console.log(` → Project: ${s}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${r}/mcp-ws\n`)}catch(e){console.error(`Failed to start backend: ${e.message}`),process.exit(1)}}}else if(o){const{runCLI:e}=await import("../cli/cli.js");e(o,r)}else if(process.env.PROJECT_GRAPH_BACKEND){const{startStdioServer:e}=await import("../mcp/mcp-server.js");console.error("Starting Project Graph MCP (stdio, direct)..."),e()}else{const{setRoots:e,getWorkspaceRoot:o}=await import("../core/workspace.js"),{ensureBackend:r,startStdioProxy:s}=await import("./backend-lifecycle.js"),{createInterface:i}=await import("node:readline"),n=t.createWriteStream("/tmp/pg-init-debug.log",{flags:"a"});n.write(`\n=== NEW SESSION ${(new Date).toISOString()} ===\n`);
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`);
5
4
  const c=i({input:process.stdin,terminal:!1}),a=[];
6
5
  let l=!1,p=null,d=null;
7
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"));
@@ -1,34 +1,8 @@
1
1
  // @ctx .context/src/network/web-server.ctx
2
- import e from"node:http";
3
- import t from"node:fs";
4
- import o from"node:path";
5
- import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{WebSocketServer as s}from"ws";import{createServer as r}from"../mcp/mcp-server.js";
6
- import c from"../core/event-bus.js";import{registerService as i}from"./local-gateway.js";import{expandFile as l}from"../compact/expand.js";
7
- const d=o.dirname(a(import.meta.url)),p=o.join(d,"..",".."),m=o.join(p,"web"),h={"symbiote-node":o.join(p,"node_modules","symbiote-node"),symbiote:o.join(p,"node_modules","@symbiotejs","symbiote")},u={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};function serveStatic(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);
8
- let r,c;if(s&&h[s[1]]?(c=h[s[1]],r=o.join(c,s[2]||"index.js")):(c=m,r=o.join(m,"/"===a?"index.html":a)),!r.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(r)&&t.statSync(r).isDirectory()&&(r=o.join(r,"index.html")),!t.existsSync(r))return n.writeHead(404),void n.end("Not Found");
9
- const i=o.extname(r),l=u[i]||"application/octet-stream",d=t.readFileSync(r);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
10
- function computeWSAccept(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
11
- function encodeWSFrame(e){const t=Buffer.from(e,"utf8"),o=t.length;
12
- let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
13
- function decodeWSFrame(e){if(e.length<2)return null;
14
- const t=15&e[0],o=!!(128&e[1]);
15
- let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;
16
- const o=e.slice(a,a+4);a+=4;
17
- const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
18
- export function startWebServer(t,a){const d=r(()=>{}),p=o.basename(o.resolve(t))||"root";
19
- let m=1;
20
- const h=o.resolve(t),u=n.createHash("md5").update(h).digest("hex"),g=parseInt(u.slice(0,4),16)%360,f={project:{name:p,path:h,color:`hsl(${g}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function broadcastRPC(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of y)try{e.send(o)}catch{y.delete(e)}}
21
- function patchState(e,t){const o=e.split(".");
22
- let n=f;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,broadcastRPC("patch",{path:e,value:t})}
23
- async function ensureSkeleton(){if(!f.skeleton)try{f.skeleton=await d.executeTool("get_skeleton",{path:t})}catch{}return f.skeleton}const w=new Map,y=new Set;
24
- let S=null;function hasActiveClients(){return w.size>0||y.size>0}
25
- function resetShutdownTimer(){S&&(clearTimeout(S),S=null)}
26
- function startShutdownTimer(){hasActiveClients()||(resetShutdownTimer(),S=setTimeout(()=>{hasActiveClients()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}
27
- function touchActivity(){resetShutdownTimer(),startShutdownTimer()}
28
- async function handleAPI(e,a,s,r){try{let c; const i=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:i});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:r,existsSync:i}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),h=i(m)?r(m,"utf-8"):null,u=await l(d,h),g=Math.ceil(u.original/4),f=Math.ceil(u.decompiled/4),K=h?Math.ceil(h.length/4):0,T=g+K,w=f>0?Math.round(100*(1-T/f)):0;c={code:u.code,file:e,injected:u.injected,codeTok:g,ctxTok:K,totalTok:T,expanded:f,savings:w+"%"}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:g}=await import("fs"),{join:s,extname:r,basename:x,dirname:q,relative:z}=await import("path"),$ext=new Set([".js",".mjs"]),d=[],p=["node_modules",".git","vendor",".context",".expanded","web"];(function walk(t){try{for(const o of e(t)){if(o.startsWith("."))continue;const e=s(t,o);n(e).isDirectory()?p.includes(o)||walk(e):$ext.has(r(o))&&d.push(e)}}catch{}})(o.resolve(t,"src"));let m=0,u=0,C=0,E=0;const B=o.resolve(t);for(const e of d){try{m+=n(e).size;const r=z(B,e),s=x(r,".js"),v=o.resolve(B,".context",q(r),s+".ctx");let ct=0;if(g(v)){ct=n(v).size;C+=ct}try{const h=g(v)?a(v,"utf-8"):null,ex=await l(e,h);E+=ex.decompiled}catch{E+=n(e).size*1.3}}catch{continue}u++}c={files:u,codeTok:Math.ceil(m/4),ctxTok:Math.ceil(C/4),totalTok:Math.ceil((m+C)/4),expanded:Math.ceil(E/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:i,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:i});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:i});break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:w.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:w.size}]}break;default:return"POST"===s&&"/api/restart"===e?(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),r.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(()=>process.exit(0),200)):(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void r.end(JSON.stringify({error:"Unknown API endpoint"})))}r.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),r.end(JSON.stringify(c))}catch(e){r.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),r.end(JSON.stringify({error:e.message}))}}startShutdownTimer(),c.on("tool:call",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)}),c.on("tool:result",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)});
29
- const v=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(touchActivity(),void handleAPI(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void serveStatic(o.pathname,t))}),j=new s({noServer:!0});j.on("connection",async e=>{y.add(e),touchActivity(),await ensureSkeleton();
30
- const t={project:f.project,skeleton:f.skeleton};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;touchActivity();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);
31
- let r=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(r=e(a,"utf-8"))}catch{}const c=await l(e,r),i=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),K=r?Math.ceil(r.length/4):0,T=i+K,p=d>0?Math.round(100*(1-T/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:i,ctxTok:K,totalTok:T,expanded:d,savings:p+"%"}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{y.delete(e),startShutdownTimer()}),e.on("error",()=>{y.delete(e),startShutdownTimer()})}),v.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=computeWSAccept(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);
32
- const o="agent-"+m++,a=r(e=>{try{t.write(encodeWSFrame(JSON.stringify(e)))}catch{}});w.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),resetShutdownTimer(),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_connect",agentId:o,agents:w.size,ts:Date.now()});
33
- let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=decodeWSFrame(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),t.end(),void(0===w.size&&startShutdownTimer());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(encodeWSFrame(JSON.stringify(o)))}catch(e){t.write(encodeWSFrame(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),0===w.size&&startShutdownTimer()}),void t.on("error",()=>{w.delete(t),0===w.size&&startShutdownTimer()})}t.destroy()}else j.handleUpgrade(e,t,o,t=>{j.emit("connection",t,e)});else t.destroy()});
34
- const b=!a,T=a||0;return v.listen(T,"127.0.0.1",()=>{const e=v.address().port;if(b){const n=i("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),v}
2
+ import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{expandFile as l}from"../compact/expand.js";import{setRoots as _setRoots}from"../core/workspace.js";
3
+ const d=o.dirname(a(import.meta.url)),p=o.join(d,"..",".."),m=o.join(p,"web"),h={"symbiote-node":o.join(p,"node_modules","symbiote-node"),symbiote:o.join(p,"node_modules","@symbiotejs","symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
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
+ function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
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
+ function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
8
+ export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null)}function P(){N()||(O(),C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),h=r(m)?i(m,"utf-8"):null,f=await l(d,h),g=Math.ceil(f.original/4),u=Math.ceil(f.decompiled/4),y=h?Math.ceil(h.length/4):0,w=g+y,v=u>0?Math.round(100*(1-w/u)):0;c={code:f.code,file:e,injected:f.injected,codeTok:g,ctxTok:y,totalTok:w,expanded:u,savings:v+"%"}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0;const j=o.resolve(t);for(const e of f){try{u+=n(e).size;const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;v+=(await l(e,t)).decompiled}catch{v+=1.3*n(e).size}}catch{continue}y++}c={files:y,codeTok:Math.ceil(u/4),ctxTok:Math.ceil(w/4),totalTok:Math.ceil((u+w)/4),expanded:Math.ceil(v/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:r,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:r});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:r});break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z(),await k();const t={project:j.project,skeleton:j.skeleton};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await l(e,i),r=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),p=i?Math.ceil(i.length/4):0,m=r+p,f=d>0?Math.round(100*(1-m/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:r,ctxTok:p,totalTok:m,expanded:d,savings:f+"%"}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
package/web/app.js CHANGED
@@ -1,17 +1,22 @@
1
1
  // @ctx .context/web/app.ctx
2
- import{Layout as t,LayoutTree as n,applyTheme as o}from"symbiote-node";import{CARBON as a}from"./vendor/symbiote-node/themes/carbon.js";import{state as s,subscribe as i,onEvent as r,call as c,connect as l}from"./state.js";import"./panels/file-tree.js";import"./panels/code-viewer.js";import"./panels/ctx-panel.js";import"./panels/dep-graph.js";import"./panels/health-panel.js";import"./panels/live-monitor.js";import"./panels/SettingsPanel/SettingsPanel.js";import"./components/quick-open.js";
2
+ import{Layout as e,LayoutTree as t,applyTheme as n}from"symbiote-node";
3
+ import{CARBON as o}from"./vendor/symbiote-node/themes/carbon.js";
4
+ import{state as a,subscribe as s,onEvent as i,call as r,connect as c}from"./state.js";
5
+ import"./panels/file-tree.js";
6
+ import"./panels/code-viewer.js";
7
+ import"./panels/ctx-panel.js";
8
+ import"./panels/dep-graph.js";
9
+ import"./panels/health-panel.js";
10
+ import"./panels/live-monitor.js";
11
+ import"./panels/SettingsPanel/SettingsPanel.js";
12
+ import"./components/quick-open.js";
3
13
  export const state={skeleton:null,activeFile:null,ws:null,monitorEvents:[]};
4
- const m=new URL(".",import.meta.url).href;
5
- export async function api(t,n={}){if(s.connected&&t.startsWith("/api/")){const o=await p(t,n);if(null!==o)return o}const o=new URLSearchParams(n).toString(),a=t.replace(/^\//,""),i=o?`${m}${a}?${o}`:`${m}${a}`,r=await fetch(i);if(!r.ok)throw new Error(`API error: ${r.status}`);return r.json()}
6
- async function p(t,n){const o={"/api/skeleton":{name:"get_skeleton",args:t=>({path:t.path})},"/api/file":{name:"compact",args:t=>({action:"compact_file",path:t.path,beautify:!0})},"/api/docs":{name:"docs",args:t=>({action:"get",path:t.path,file:t.file})},"/api/analysis":{name:"analyze",args:t=>({action:"full_analysis",path:t.path})},"/api/analysis-summary":{name:"analyze",args:t=>({action:"analysis_summary",path:t.path})},"/api/deps":{name:"navigate",args:t=>({action:"deps",symbol:t.symbol})},"/api/usages":{name:"navigate",args:t=>({action:"usages",symbol:t.symbol})},"/api/expand":{name:"navigate",args:t=>({action:"expand",symbol:t.symbol})},"/api/chain":{name:"navigate",args:t=>({action:"call_chain",from:t.from,to:t.to})}}[t];return o?c(o.name,o.args(n)):null}
14
+ const l=new URL(".",import.meta.url).href;
15
+ export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/docs":{name:"docs",args:e=>({action:"get",path:e.path,file:e.file})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//,""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
7
16
  export const events=new EventTarget;
8
- export function emit(t,n={}){events.dispatchEvent(new CustomEvent(t,{detail:n}))}const d={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},u=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],b={explorer:()=>n.createSplit("horizontal",n.createPanel("file-tree"),n.createSplit("horizontal",n.createPanel("code-viewer"),n.createPanel("ctx-panel"),.65),.2),analysis:()=>n.createSplit("horizontal",n.createPanel("health"),n.createPanel("dep-graph"),.5),monitor:()=>n.createPanel("monitor"),settings:()=>n.createPanel("settings")};function g(){o(document.documentElement,a);
9
- const t=document.querySelector(".app-workspace"),n=document.createElement("layout-sidebar");t.prepend(n);
10
- const s=t.querySelector(".app-content"),i=document.createElement("panel-layout");i.setAttribute("storage-key","pg-explorer-layout"),i.setAttribute("min-panel-size","150"),i.id="main-layout",s.appendChild(i),requestAnimationFrame(()=>{for(const[t,n]of Object.entries(d))i.registerPanelType(t,n);function e(){const t=location.hash.replace("#","")||"explorer",n=t.indexOf("?"),o=n>=0?t.substring(0,n):t,a=o.indexOf("/"),s=a>=0?o.substring(0,a):o,r=a>=0?o.substring(a+1):"";b[s]&&i.setLayout(b[s]()),"explorer"===s&&r&&requestAnimationFrame(()=>{state.activeFile=r,emit("file-selected",{path:r,fromRoute:!0})})}n.setSections(u),window.addEventListener("hashchange",e),events.addEventListener("file-selected",t=>{if(t.detail.fromRoute)return;
11
- const n=t.detail.path;n&&history.replaceState(null,"",`#explorer/${n}`)}),localStorage.getItem("pg-explorer-layout")||i.setLayout(b.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})}
12
- async function f(){g(),l(),i("project",t=>{t&&(document.title=`${t.name} — Project Graph`,document.getElementById("project-name").textContent=t.name,document.documentElement.style.setProperty("--project-accent",t.color),h(t.agents))}),i("skeleton",t=>{if(!t)return;state.skeleton=t;
13
- const n=new Set;for(const o of Object.values(t.n||{}))o.f&&n.add(o.f);for(const o of Object.keys(t.X||{}))n.add(o);for(const[o,a]of Object.entries(t.f||{}))for(const t of a)n.add("./"===o?t:`${o}${t}`);for(const[o,a]of Object.entries(t.a||{}))for(const t of a)n.add("./"===o?t:`${o}${t}`);
14
- const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`),emit("skeleton-loaded",t),fetch(m+"api/compression-stats").then(t=>t.json()).then(t=>{const n=document.getElementById("compression-stats");if(n&&t.codeTok){const o=t.ctxTok?`${(t.codeTok/1e3).toFixed(1)}K + ${(t.ctxTok/1e3).toFixed(1)}K ctx = ${(t.totalTok/1e3).toFixed(1)}K tok`:`${(t.codeTok/1e3).toFixed(1)}K tok`;n.textContent=o,n.style.display=""}})}),i("connected",t=>{const n=document.getElementById("status-indicator");n&&(n.className=t?"status connected":"status disconnected")}),r(t=>{if("agent_connect"===t.type||"agent_disconnect"===t.type)return h(t.agents),void emit("agent-event",t);state.monitorEvents.push(t),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",t)})}
15
- function h(t){let n=document.getElementById("agent-badge");if(!n){const t=document.querySelector(".app-topbar");if(!t)return;n=document.createElement("span"),n.id="agent-badge",n.className="agent-badge",t.appendChild(n)}n.textContent=t>0?`● ${t} agent${1!==t?"s":""}`:"",n.style.display=t>0?"":"none"}
16
- function y(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
17
- "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{f(),y()}):setTimeout(()=>{f(),y()},100);
17
+ export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
18
+ const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),analysis:()=>t.createSplit("horizontal",t.createPanel("health"),t.createPanel("dep-graph"),.5),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
19
+ async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";d[a]&&s.setLayout(d[a]()),"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;const t=e.detail.path;t&&history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),c(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),s("skeleton",e=>{if(!e)return;state.skeleton=e;const t=new Set;for(const n of Object.values(e.n||{}))n.f&&t.add(n.f);for(const n of Object.keys(e.X||{}))t.add(n);for(const[n,o]of Object.entries(e.f||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);for(const[n,o]of Object.entries(e.a||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);const n=document.getElementById("project-files");n&&(n.textContent=`${t.size} files`),emit("skeleton-loaded",e),fetch(l+"api/compression-stats").then(e=>e.json()).then(e=>{const t=document.getElementById("compression-stats");if(t&&e.codeTok){const n=e.ctxTok?`${(e.codeTok/1e3).toFixed(1)}K + ${(e.ctxTok/1e3).toFixed(1)}K ctx = ${(e.totalTok/1e3).toFixed(1)}K tok`:`${(e.codeTok/1e3).toFixed(1)}K tok`;t.textContent=n,t.style.display=""}})}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)})}
20
+ function g(e){let t=document.getElementById("agent-badge");if(!t){const e=document.querySelector(".app-topbar");if(!e)return;t=document.createElement("span"),t.id="agent-badge",t.className="agent-badge",e.appendChild(t)}t.textContent=e>0?`● ${e} agent${1!==e?"s":""}`:"",t.style.display=e>0?"":"none"}
21
+ function f(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
22
+ "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f()}):setTimeout(()=>{u(),f()},100);
@@ -1,3 +1,4 @@
1
+ // @ctx .context/web/components/code-block.ctx
1
2
  import o from"@symbiotejs/symbiote";import{highlight as n}from"../highlight.js";
2
3
  export class CodeBlock extends o{init$={code:"",highlighted:"",lineNums:""};renderCallback(){this.sub("code",o=>{if(!o)return this.$.highlighted="",void(this.$.lineNums="");this.$.highlighted=n(o);
3
4
  const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n")})}}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>\n',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 /* 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,3 +1,4 @@
1
+ // @ctx .context/web/components/quick-open.ctx
1
2
  import e from"@symbiotejs/symbiote";import{state as n,events as t,emit as s}from"../app.js";
2
3
  export class QuickOpen extends e{init$={visible:!1,query:"",resultsHTML:"",selectedIdx:0};_results=[];_allFiles=[];renderCallback(){t.addEventListener("skeleton-loaded",e=>this._collectFiles(e.detail)),n.skeleton&&this._collectFiles(n.skeleton),this._overlay=this.querySelector(".qo-overlay"),this._overlay.addEventListener("click",e=>{e.target===this._overlay&&this._close()}),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&"k"===e.key&&(e.preventDefault(),this._toggle()),"Escape"===e.key&&this.$.visible&&(e.preventDefault(),this._close())}),this.sub("visible",e=>{this._overlay&&(this._overlay.style.display=e?"flex":"none",e&&requestAnimationFrame(()=>{const e=this.querySelector(".qo-input");e&&(e.value="",e.focus())}))})}_collectFiles(e){const n=new Set;for(const t of Object.keys(e.X||{}))n.add(t);for(const t of Object.values(e.n||{}))t.f&&n.add(t.f);for(const[t,s]of Object.entries(e.f||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);for(const[t,s]of Object.entries(e.a||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);this._allFiles=[...n].sort()}_toggle(){this.$.visible=!this.$.visible,this.$.visible&&(this.$.query="",this.$.selectedIdx=0,this._search(""))}_close(){this.$.visible=!1}_onInput(e){this.$.query=e.target.value,this.$.selectedIdx=0,this._search(this.$.query)}_onKeydown(e){if("ArrowDown"===e.key)e.preventDefault(),this.$.selectedIdx=Math.min(this.$.selectedIdx+1,this._results.length-1),this._renderResults();else if("ArrowUp"===e.key)e.preventDefault(),this.$.selectedIdx=Math.max(this.$.selectedIdx-1,0),this._renderResults();else if("Enter"===e.key){e.preventDefault();
3
4
  const t=this._results[this.$.selectedIdx];t&&(this._close(),n.activeFile=t.file,s("file-selected",{path:t.file}),location.hash.startsWith("#explorer")?history.replaceState(null,"",`#explorer/${t.file}`):location.hash=`explorer/${t.file}`)}}_search(e){const n=e.toLowerCase().trim();if(n){const e=[];for(const t of this._allFiles){const s=QuickOpen._fuzzyScore(n,t.toLowerCase());s>0&&e.push({file:t,score:s})}e.sort((e,n)=>n.score-e.score),this._results=e.slice(0,15)}else this._results=this._allFiles.slice(0,15).map(e=>({file:e,score:0}));this._renderResults()}static _fuzzyScore(e,n){if(n.includes(e))return 100+e.length/n.length*50;
@@ -1,3 +1,4 @@
1
+ // @ctx .context/web/dashboard-state.ctx
1
2
  export const state={projects:[],events:[]};
2
3
  export const events=new EventTarget;
3
4
  export function emit(t,e={}){events.dispatchEvent(new CustomEvent(t,{detail:e}))}
@@ -1 +1,2 @@
1
+ // @ctx .context/web/panels/ActionBoard/ActionBoard.css.ctx
1
2
  export default"\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--sn-bg-primary);\n color: var(--sn-fg-primary);\n overflow: hidden;\n}\n.list {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n}\n";