project-graph-mcp 2.2.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +81 -0
- package/CHANGELOG.md +57 -0
- package/README.md +9 -4
- package/package.json +6 -13
- package/src/compact/expand.js +2 -4
- package/src/core/graph-builder.js +2 -2
- package/src/core/parser.js +2 -2
- package/src/network/server.js +1 -2
- package/src/network/web-server.js +4 -1
- package/vendor/symbiote-node/CHANGELOG.md +31 -0
- package/vendor/symbiote-node/LICENSE +21 -0
- package/vendor/symbiote-node/README.md +206 -0
- package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
- package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
- package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
- package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
- package/vendor/symbiote-node/canvas/LODManager.js +88 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
- package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
- package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
- package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
- package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
- package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
- package/vendor/symbiote-node/core/Connection.js +45 -0
- package/vendor/symbiote-node/core/Editor.js +451 -0
- package/vendor/symbiote-node/core/Frame.js +31 -0
- package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
- package/vendor/symbiote-node/core/GraphText.js +210 -0
- package/vendor/symbiote-node/core/Node.js +143 -0
- package/vendor/symbiote-node/core/Portal.js +104 -0
- package/vendor/symbiote-node/core/Socket.js +185 -0
- package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
- package/vendor/symbiote-node/index.js +103 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
- package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
- package/vendor/symbiote-node/interactions/Drag.js +102 -0
- package/vendor/symbiote-node/interactions/Selector.js +132 -0
- package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
- package/vendor/symbiote-node/interactions/Zoom.js +140 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
- package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
- package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
- package/vendor/symbiote-node/layout/index.js +16 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
- package/vendor/symbiote-node/package.json +59 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
- package/vendor/symbiote-node/plugins/History.js +384 -0
- package/vendor/symbiote-node/plugins/Readonly.js +59 -0
- package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
- package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
- package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
- package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
- package/vendor/symbiote-node/shapes/PillShape.js +91 -0
- package/vendor/symbiote-node/shapes/RectShape.js +72 -0
- package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
- package/vendor/symbiote-node/shapes/index.js +53 -0
- package/vendor/symbiote-node/themes/Palette.js +32 -0
- package/vendor/symbiote-node/themes/Skin.js +113 -0
- package/vendor/symbiote-node/themes/Theme.js +84 -0
- package/vendor/symbiote-node/themes/carbon.js +137 -0
- package/vendor/symbiote-node/themes/dark.js +137 -0
- package/vendor/symbiote-node/themes/ebook.js +138 -0
- package/vendor/symbiote-node/themes/grey.js +137 -0
- package/vendor/symbiote-node/themes/light.js +137 -0
- package/vendor/symbiote-node/themes/neon.js +138 -0
- package/vendor/symbiote-node/themes/pcb.js +273 -0
- package/vendor/symbiote-node/themes/synthwave.js +137 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
- package/web/app.js +6 -5
- package/web/components/canvas-graph.js +1666 -0
- package/web/components/event-feed/CodeWidget.js +32 -0
- package/web/components/event-feed/EventWidget.js +97 -0
- package/web/components/event-feed/ListWidget.js +57 -0
- package/web/components/event-feed/MiniGraphWidget.js +69 -0
- package/web/dashboard.js +1 -1
- package/web/index.html +4 -0
- package/web/panels/ActionBoard/ActionBoard.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
- package/web/panels/code-viewer.js +50 -15
- package/web/panels/dep-graph.js +2712 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +33 -0
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/src/.contextignore +0 -22
- package/src/.project-graph-cache.json +0 -1
- package/src/compact/.project-graph-cache.json +0 -1
- package/web/.project-graph-cache.json +0 -1
- package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
## Architecture
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
project-graph-mcp/
|
|
5
|
+
├── src/
|
|
6
|
+
│ ├── core/ # Foundation
|
|
7
|
+
│ │ ├── parser.js # AST parser (Acorn) + language routing
|
|
8
|
+
│ │ ├── graph-builder.js # Minified graph + legend
|
|
9
|
+
│ │ ├── filters.js # Exclude patterns, .gitignore
|
|
10
|
+
│ │ ├── workspace.js # Path resolution + traversal protection
|
|
11
|
+
│ │ └── event-bus.js # Tool call/result events for web UI
|
|
12
|
+
│ ├── analysis/ # Code quality analysis
|
|
13
|
+
│ │ ├── dead-code.js # Unused code detection
|
|
14
|
+
│ │ ├── complexity.js # Cyclomatic complexity
|
|
15
|
+
│ │ ├── similar-functions.js # Duplicate detection
|
|
16
|
+
│ │ ├── large-files.js # File size analysis
|
|
17
|
+
│ │ ├── outdated-patterns.js # Legacy pattern detection
|
|
18
|
+
│ │ ├── full-analysis.js # Health Score (0–100) + streaming + summary
|
|
19
|
+
│ │ ├── jsdoc-checker.js # JSDoc ↔ AST consistency validator
|
|
20
|
+
│ │ ├── jsdoc-generator.js # JSDoc template generation
|
|
21
|
+
│ │ ├── type-checker.js # Optional tsc wrapper (async)
|
|
22
|
+
│ │ ├── undocumented.js # Missing JSDoc finder
|
|
23
|
+
│ │ ├── custom-rules.js # Configurable lint rules
|
|
24
|
+
│ │ ├── test-annotations.js # .ctx.md test checklist parsing
|
|
25
|
+
│ │ ├── db-analysis.js # SQL schema + table usage
|
|
26
|
+
│ │ └── analysis-cache.js # Incremental cache (.context/.cache/)
|
|
27
|
+
│ ├── compact/ # AI context compression
|
|
28
|
+
│ │ ├── compress.js # Terser minification + export legend
|
|
29
|
+
│ │ ├── compact.js # Project-wide compact/beautify (mangle: false)
|
|
30
|
+
│ │ ├── expand.js # Decompile: name restoration from .ctx
|
|
31
|
+
│ │ ├── doc-dialect.js # Doc Dialect (.context/ format)
|
|
32
|
+
│ │ ├── ctx-to-jsdoc.js # .ctx → JSDoc injection + stripping
|
|
33
|
+
│ │ ├── ai-context.js # AI boot aggregator
|
|
34
|
+
│ │ ├── mode-config.js # Compact mode config (1/2/3/4)
|
|
35
|
+
│ │ ├── validate-pipeline.js # .ctx ↔ source contract validation
|
|
36
|
+
│ │ ├── framework-references.js # Framework-specific docs
|
|
37
|
+
│ │ └── instructions.js # Agent guidelines
|
|
38
|
+
│ ├── lang/ # Multi-language parsers
|
|
39
|
+
│ │ ├── lang-typescript.js # TypeScript/TSX regex parser
|
|
40
|
+
│ │ ├── lang-python.js # Python regex parser
|
|
41
|
+
│ │ ├── lang-go.js # Go regex parser
|
|
42
|
+
│ │ ├── lang-sql.js # SQL extraction (tables, columns)
|
|
43
|
+
│ │ └── lang-utils.js # Shared: stripStringsAndComments
|
|
44
|
+
│ ├── cli/ # CLI interface
|
|
45
|
+
│ │ ├── cli.js # CLI entry point + help
|
|
46
|
+
│ │ └── cli-handlers.js # CLI command handlers
|
|
47
|
+
│ ├── mcp/ # MCP protocol
|
|
48
|
+
│ │ ├── mcp-server.js # MCP server + response hints
|
|
49
|
+
│ │ ├── tool-defs.js # MCP tool schemas (18 grouped tools)
|
|
50
|
+
│ │ └── tools.js # Graph tools (skeleton, expand, deps)
|
|
51
|
+
│ └── network/ # Server & networking
|
|
52
|
+
│ ├── server.js # Entry point (CLI/MCP/Serve mode switch)
|
|
53
|
+
│ ├── backend.js # Background backend process
|
|
54
|
+
│ ├── backend-lifecycle.js # Port file management + stdio proxy
|
|
55
|
+
│ ├── web-server.js # HTTP + WebSocket server for web UI
|
|
56
|
+
│ ├── local-gateway.js # Multi-project gateway registry
|
|
57
|
+
│ └── mdns.js # mDNS/DNS-SD service advertisement
|
|
58
|
+
├── web/ # Web dashboard
|
|
59
|
+
│ ├── index.html / dashboard.html
|
|
60
|
+
│ ├── app.js / dashboard.js # Application entry points
|
|
61
|
+
│ ├── state.js # WebSocket state management
|
|
62
|
+
│ └── panels/ # UI components (Symbiote.js)
|
|
63
|
+
├── rules/ # Pre-built rule sets (JSON)
|
|
64
|
+
├── vendor/
|
|
65
|
+
│ ├── acorn.mjs # AST parser (MIT, vendored)
|
|
66
|
+
│ ├── walk.mjs # AST walker (MIT, vendored)
|
|
67
|
+
│ └── terser.mjs # JS minifier (BSD, vendored)
|
|
68
|
+
├── tests/
|
|
69
|
+
│ ├── parser.test.js # AST parser + graph builder
|
|
70
|
+
│ ├── mcp.test.js # MCP tool integration
|
|
71
|
+
│ ├── compact.test.js # Compact/beautify, ctx-to-jsdoc
|
|
72
|
+
│ ├── consolidated.test.js # Analysis tools tests
|
|
73
|
+
│ └── orm.test.js # ORM/SQL test cases
|
|
74
|
+
├── docs/ # Public documentation
|
|
75
|
+
│ ├── ROADMAP.md
|
|
76
|
+
│ ├── examples/ # AGENT_ROLE templates
|
|
77
|
+
│ └── references/ # Framework reference docs
|
|
78
|
+
└── dev-docs/ # Internal R&D (not public)
|
|
79
|
+
├── ideas/ # Feature research & design docs
|
|
80
|
+
└── prototypes/ # Experimental code
|
|
81
|
+
```
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [2.2.8] — 2026-04-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Skeleton exports regression**: `createSkeleton()` was pushing `{id, l}` objects instead of plain string IDs into the `X` (exports) field. This broke `ctx-panel`, `dep-graph`, and integration tests. Exports now consistently contain string legend keys.
|
|
9
|
+
- **Expand pipeline crash**: `expandFile()` would crash with `Unexpected token` when scope-aware variable renaming produced invalid JS (e.g., callback parameter shadowing). Now falls back to beautified code without rename when re-parse fails.
|
|
10
|
+
- **Console.log pollution**: Removed debug `console.log` and `console.time` calls from `dep-graph.js` and `ActionBoard.js` that leaked into the browser console.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`.npmignore`**: Excludes debug artifacts (`web/debug-*`, `web/test-*`, `web/tune-*`), unused vendor directories (`engine/`, `demo/`, `tests/`), `.context/`, and internal tooling from the published npm package. Reduces package from 301 to ~237 files.
|
|
14
|
+
- **`.gitignore` patterns**: Added patterns for debug/test files to prevent re-committing.
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
- 9 debug/temp files removed from repository: `patch.js`, `temp_output.txt`, `temp_output_rects.txt`, `test-graph.js`, `web/debug-patch.js`, `web/debug-sleep.js`, `web/test-force-sim.html`, `web/test-graph-data.json`, `web/tune-physics.js`.
|
|
18
|
+
- `"files"` whitelist from `package.json` (replaced by `.npmignore` for granular exclusion control).
|
|
19
|
+
|
|
20
|
+
## [2.2.7] — 2026-04-22
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Force-directed graph layout with ForceWorker (continuous mode, live tick feedback)
|
|
24
|
+
- Hierarchical directory nesting with universal URL routing (`#graph/path/to/dir`)
|
|
25
|
+
- Focus-driven graph exploration mode (radial layout with imports/dependents hemispheres)
|
|
26
|
+
- File tree sidebar with bidirectional sync to graph view
|
|
27
|
+
- Path style toggle (PCB/orthogonal, straight, bezier)
|
|
28
|
+
- Mode toggle URL routing (`?mode=flat|tree`)
|
|
29
|
+
- Code viewer two-way linking with file tree and graph
|
|
30
|
+
- Depth-of-field effect during node drag interaction
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Graph navigation routing stability (URL path corruption, deep-linking)
|
|
34
|
+
- Force layout convergence and phantom position sync
|
|
35
|
+
- Node overlap inside subgraphs on page refresh
|
|
36
|
+
- LOD phantom promotion and text clipping
|
|
37
|
+
- PCB obstacle avoidance threshold at >200 nodes
|
|
38
|
+
- Severe lag when toggling TREE/FLAT inside subgraph
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Migrated from d3-force to pure force layout engine (zero external dependencies)
|
|
42
|
+
- Centralized fitView, flyToNode, LOD, and PinExpansion logic in symbiote-node
|
|
43
|
+
- `symbiote-node` updated to v0.3.0 with 41/41 force layout tests passing
|
|
44
|
+
|
|
45
|
+
## [2.2.5] — 2026-04-08
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- Expand pipeline with JSDoc injection from `.ctx` documentation
|
|
49
|
+
- Scope-aware identifier deduplication in expand renaming
|
|
50
|
+
- Validate pipeline for compact ↔ expand round-trip integrity
|
|
51
|
+
|
|
52
|
+
## [2.2.4] — 2026-03-28
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- Web explorer with interactive dependency graph visualization
|
|
56
|
+
- SubgraphRouter for hierarchical graph navigation
|
|
57
|
+
- Canvas-based graph rendering with LOD system
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
**Maximize your AI agent's context window.** An MCP server that lets agents read and edit your codebase in **compact mode** — minified source with all variable names preserved. Code tokens drop **↓40%**, and `.ctx` documentation is injected only in the focus zone. Fewer tokens per file → more files fit in context → **deeper understanding of your codebase**.
|
|
8
8
|
|
|
9
|
-

|
|
9
|
+

|
|
10
10
|
|
|
11
|
-

|
|
11
|
+

|
|
12
12
|
|
|
13
13
|
Includes a built-in [Web Dashboard](#web-dashboard) (`npx project-graph-mcp serve`) to visualize token metrics and compact ⇄ raw code in real-time.
|
|
14
14
|
|
|
@@ -328,12 +328,17 @@ See **[CONFIGURATION.md](CONFIGURATION.md)** for all supported IDEs (Antigravity
|
|
|
328
328
|
<summary>Alternative: from source</summary>
|
|
329
329
|
|
|
330
330
|
```bash
|
|
331
|
-
git clone https://github.com/rnd-pro/project-graph-mcp
|
|
331
|
+
git clone --recursive https://github.com/rnd-pro/project-graph-mcp
|
|
332
332
|
cd project-graph-mcp
|
|
333
|
-
|
|
333
|
+
npm install
|
|
334
334
|
# Use "node /path/to/project-graph-mcp/src/network/server.js" as the command in MCP config
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
+
> **Note:** The `--recursive` flag is required to fetch the `vendor/symbiote-node` submodule. If you already cloned without it, run:
|
|
338
|
+
> ```bash
|
|
339
|
+
> git submodule update --init --recursive
|
|
340
|
+
> ```
|
|
341
|
+
|
|
337
342
|
</details>
|
|
338
343
|
|
|
339
344
|
## CLI
|
package/package.json
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-graph-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for AI agents — project graph, code quality analysis, visual web explorer. JS, TS, Python, Go.",
|
|
6
6
|
"main": "src/network/server.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"project-graph-mcp": "src/network/server.js"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"rules/",
|
|
15
|
-
"docs/",
|
|
16
|
-
"GUIDE.md",
|
|
17
|
-
"CONFIGURATION.md",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
|
|
21
14
|
"scripts": {
|
|
22
15
|
"start": "node src/network/server.js",
|
|
23
16
|
"test": "node --test tests/*.test.js"
|
|
@@ -47,7 +40,7 @@
|
|
|
47
40
|
"license": "MIT",
|
|
48
41
|
"dependencies": {
|
|
49
42
|
"@symbiotejs/symbiote": "^3.2.1",
|
|
50
|
-
"symbiote-node": "
|
|
43
|
+
"symbiote-node": "file:vendor/symbiote-node",
|
|
51
44
|
"ws": "^8.20.0"
|
|
52
45
|
},
|
|
53
46
|
"engines": {
|
package/src/compact/expand.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// @ctx .context/src/compact/expand.ctx
|
|
2
2
|
import{parseCtxParams as h,buildJSDocBlock as g}from"./jsdoc-builder.js";import{readCtxFile as $}from"./ctx-resolver.js";import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as t,writeFileSync as e,mkdirSync as n,existsSync as s}from"fs";import{join as i,basename as a,extname as c,dirname as p,relative as l}from"path";import{minify as m}from"../../vendor/terser.mjs";import{parse as d}from"../../vendor/acorn.mjs";import{simple as f,ancestor as u}from"../../vendor/walk.mjs";
|
|
3
3
|
|
|
4
|
-
function y(t){if(!t)return null;const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}
|
|
5
|
-
|
|
6
|
-
function w(t,e,n,s,o){const r=new Map,i=[];for(const t of e.body)if("ImportDeclaration"===t.type)for(const e of t.specifiers)if("ImportSpecifier"===e.type&&e.imported.name!==e.local.name)r.set(e.local.name,e.imported.name),i.push({s:e.start,e:e.end,n:e.imported.name});else if("ImportDefaultSpecifier"===e.type){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:n}))}else if("ImportNamespaceSpecifier"===e.type&&e.local.name.length<=2){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:"* as "+n}))}for(const[t,e]of s)r.set(t,e);if(o.has("__top__"))for(const[t,e]of o.get("__top__"))r.set(t,e);const a=new Set(r.values());for(const[t]of[...r])a.has(t)&&r.delete(t);const c=[],p=[],l=[];const m=t=>{const e=t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left?.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument?.type?t.argument.name:null).filter(Boolean),s=new Map;if(t.id?.name){const r=n.get(t.id.name);if(r?.params)for(let t=0;t<Math.min(e.length,r.params.length);t++)e[t]!==r.params[t].name&&s.set(e[t],r.params[t].name);const i=o.get(t.id.name);if(i)for(const[t,e]of i)s.has(t)||s.set(t,e);if(s.size>0)for(const e of t.params){const t="Identifier"===e.type?e:"AssignmentPattern"===e.type&&"Identifier"===e.left?.type?e.left:null;t&&s.has(t.name)&&p.push({s:t.start,e:t.end,n:s.get(t.name)})}}const r=function(t){const e=new Set;return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.add(t.id.name)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.add(t.param.name)}}),e}(t.body);for(const t of e)r.add(t);const i=t.id?.name&&o.get(t.id.name);for(const e of function(t){const e=[];return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.push(t.id)}}),e}(t.body)){const t=i?.get(e.name);t&&l.push({s:e.start,e:e.end,n:t})}c.push({s:t.params.length>0?t.params[0].start:t.body.start,e:t.body.end,p:r,r:s})};f(e,{FunctionDeclaration:m,FunctionExpression:m,ArrowFunctionExpression:m});for(const t of e.body)if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type&&r.has(e.id.name)&&l.push({s:e.id.start,e:e.id.end,n:r.get(e.id.name)});const d=[...i,...p,...l];u(e,{Identifier(t,e,n){const s=n[n.length-2];if("MemberExpression"===s?.type&&s.property===t&&!s.computed)return;if("Property"===s?.type&&s.key===t&&!s.computed&&s.value!==t)return;if("ExportSpecifier"===s?.type)return;const o=t.name;let i=null;for(const e of c)t.start>=e.s&&t.end<=e.e&&(!i||e.e-e.s<i.e-i.s)&&(i=e);i&&i.p.has(o)?i.r.has(o)&&d.push({s:t.start,e:t.end,n:i.r.get(o)}):r.has(o)&&d.push({s:t.start,e:t.end,n:r.get(o)})}}),d.sort((t,e)=>e.s-t.s);let h=t;for(const t of d)h=h.slice(0,t.s)+t.n+h.slice(t.e);return h}
|
|
7
|
-
export async function expandFile(e,n,s={}){const{indentLevel:o=2}=s,r=t(e,"utf-8");if(!r.trim())return{code:"",injected:0,original:0,decompiled:0};let i;try{i=(await m(r,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:o,semicolons:!0}})).code||r}catch{i=r}{const t=i.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}i=e.join("\n")}i=i.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/o);return"\t".repeat(e)+" ".repeat(t.length%o)});const a=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const o=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(o){e.set(o[1],{type:"method",params:h(o[2]),description:o[3]?.trim()||""});continue}const r=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(r){const t=r[2],n=r[3],s=r[4]||"",o=(r[5]||"").split("|"),i=o[0]?.trim()||"";e.set(t,{type:"function",params:h(n),returnType:y(s),description:i,exported:!!r[1]});continue}}return e}(n),c=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}(n),p=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;const s=t.slice(0,n),o=t.slice(n+1),r=new Map;for(const t of o.split(",")){const e=t.trim().split("=");2===e.length&&r.set(e[0].trim(),e[1].trim())}r.size>0&&e.set(s,r)}}}return e}(n);let l;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}try{i=w(i,l,a,c,p),l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{}if(0===a.size)return{code:i,injected:0,original:r.length,decompiled:i.length};const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&u.push({pos:t.start,jsdoc:g(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:g(e)})},ClassDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);let x=i,j=0;for(const{pos:t,jsdoc:e}of u){const n=x.lastIndexOf("\n",t-1),s=-1===n?0:n+1,o=x.slice(s,t).match(/^(\s*)/)?.[1]||"",r=e.split("\n").map(t=>o+t).join("\n");x=x.slice(0,t)+r+"\n"+x.slice(t),j++}return{code:x,injected:j,original:r.length,decompiled:x.length}}
|
|
4
|
+
function y(t){if(!t)return null;const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}function w(t,e,n,s,o){const r=new Map;let i=[];for(const t of e.body)if("ImportDeclaration"===t.type)for(const e of t.specifiers)if("ImportSpecifier"===e.type&&e.imported.name!==e.local.name)r.set(e.local.name,e.imported.name),i.push({s:e.start,e:e.end,n:e.imported.name,k:e.local.name});else if("ImportDefaultSpecifier"===e.type){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:n,k:e.local.name}))}else if("ImportNamespaceSpecifier"===e.type&&e.local.name.length<=2){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:"* as "+n,k:e.local.name}))}for(const[t,e]of s)r.set(t,e);if(o.has("__top__"))for(const[t,e]of o.get("__top__"))r.set(t,e);const a=new Set(r.values());for(const[t]of[...r])a.has(t)&&r.delete(t);{const _u=new Set;for(const[k,v]of[...r])_u.has(v)?(r.delete(k),i=i.filter(x=>x.k!==k)):_u.add(v)}const c=[],p=[],l=[];const m=t=>{const e=t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left?.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument?.type?t.argument.name:null).filter(Boolean),s=new Map;if(t.id?.name){const r=n.get(t.id.name);if(r?.params)for(let t=0;t<Math.min(e.length,r.params.length);t++)e[t]!==r.params[t].name&&s.set(e[t],r.params[t].name);const i=o.get(t.id.name);if(i)for(const[t,e]of i)s.has(t)||s.set(t,e);if(s.size>0)for(const e of t.params){const t="Identifier"===e.type?e:"AssignmentPattern"===e.type&&"Identifier"===e.left?.type?e.left:null;t&&s.has(t.name)&&p.push({s:t.start,e:t.end,n:s.get(t.name)})}}const r=function(t){const e=new Set;return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.add(t.id.name)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.add(t.param.name)}}),e}(t.body);for(const t of e)r.add(t);const _tgts=Array.from(s.values());for(const t of _tgts){if(r.has(t)&&!s.has(t)){let n=t+"_";while(r.has(n)||_tgts.includes(n))n+="_";s.set(t,n)}}const i=t.id?.name&&o.get(t.id.name);for(const e of function(t){const e=[];return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.push(t.id)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.push(t.param)}}),e}(t.body)){const t=s.has(e.name)?s.get(e.name):i?.get(e.name);t&&l.push({s:e.start,e:e.end,n:t})}c.push({s:t.params.length>0?t.params[0].start:t.body.start,e:t.body.end,p:r,r:s})};f(e,{FunctionDeclaration:m,FunctionExpression:m,ArrowFunctionExpression:m});for(const t of e.body)if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type&&r.has(e.id.name)&&l.push({s:e.id.start,e:e.id.end,n:r.get(e.id.name)});const d=[...i,...p,...l];u(e,{Identifier(t,e,n){const s=n[n.length-2];if("MemberExpression"===s?.type&&s.property===t&&!s.computed)return;if("Property"===s?.type&&s.key===t&&!s.computed&&s.value!==t)return;if("ExportSpecifier"===s?.type)return;const o=t.name;const scopes=c.filter(e=>t.start>=e.s&&t.end<=e.e).sort((a,b)=>(a.e-a.s)-(b.e-b.s));let mapped=!1,isLocal=!1;for(const e of scopes){if(e.r.has(o)){d.push({s:t.start,e:t.end,n:e.r.get(o)});mapped=!0;break}if(e.p.has(o)){isLocal=!0;break}}if(!mapped&&!isLocal&&r.has(o)){d.push({s:t.start,e:t.end,n:r.get(o)})}}});d.sort((t,e)=>e.s-t.s);let h=t;for(const t of d)h=h.slice(0,t.s)+t.n+h.slice(t.e);return h}
|
|
5
|
+
export async function expandFile(e,n,s={}){const{indentLevel:o=2}=s,r=t(e,"utf-8");if(!r.trim())return{code:"",injected:0,original:0,decompiled:0};let i;try{i=(await m(r,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:o,semicolons:!0}})).code||r}catch{i=r}{const t=i.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}i=e.join("\n")}i=i.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/o);return"\t".repeat(e)+" ".repeat(t.length%o)});const a=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const o=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(o){e.set(o[1],{type:"method",params:h(o[2]),description:o[3]?.trim()||""});continue}const r=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(r){const t=r[2],n=r[3],s=r[4]||"",o=(r[5]||"").split("|"),i=o[0]?.trim()||"";e.set(t,{type:"function",params:h(n),returnType:y(s),description:i,exported:!!r[1]});continue}}return e}(n),c=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}(n),p=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;const s=t.slice(0,n),o=t.slice(n+1),r=new Map;for(const t of o.split(",")){const e=t.trim().split("=");2===e.length&&r.set(e[0].trim(),e[1].trim())}r.size>0&&e.set(s,r)}}}return e}(n);let l;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}const _preRename=i;i=w(i,l,a,c,p);try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{i=_preRename;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}};if(0===a.size)return{code:i,injected:0,original:r.length,decompiled:i.length};const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&u.push({pos:t.start,jsdoc:g(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:g(e)})},ClassDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);let x=i,j=0;for(const{pos:t,jsdoc:e}of u){const n=x.lastIndexOf("\n",t-1),s=-1===n?0:n+1,o=x.slice(s,t).match(/^(\s*)/)?.[1]||"",r=e.split("\n").map(t=>o+t).join("\n");x=x.slice(0,t)+r+"\n"+x.slice(t),j++}return{code:x,injected:j,original:r.length,decompiled:x.length}}
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ctx .context/src/core/graph-builder.ctx
|
|
2
2
|
export function minifyLegend(s){const t={},o=new Set;for(const n of s){let s=e(n),c=1;for(;o.has(s);)s=e(n)+c,c++;o.add(s),t[n]=s}return t}
|
|
3
3
|
function e(e){const s=e.replace(/[a-z]/g,"");if(s.length>=2)return s.slice(0,3);const t=e.match(/[A-Z]/g);return t&&t.length>0?e[0].toLowerCase()+t[0]:e.slice(0,2)}
|
|
4
|
-
export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[]};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"→",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};const l=new Set;for(const e of f.edges){const s=e[2].split(".")[0];l.add(s)}for(const e of Object.keys(f.nodes))l.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);const i=Object.create(null);for(const e of s)for(const s of e.methods||[])i[s]||(i[s]=[]),i[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(i))s.length>1&&(f.duplicates[e]=s);return f}
|
|
5
|
-
export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;const l={m:e};f>0&&(l.$=f),c.f&&(l.f=c.f),o[n]=l}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;const
|
|
4
|
+
export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[],fileImports:e.fileImports||{}};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0,l:e.line||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"→",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0,l:e.line||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};const l=new Set;for(const e of f.edges){const s=e[2].split(".")[0];l.add(s)}for(const e of Object.keys(f.nodes))l.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);const i=Object.create(null);for(const e of s)for(const s of e.methods||[])i[s]||(i[s]=[]),i[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(i))s.length>1&&(f.duplicates[e]=s);return f}
|
|
5
|
+
export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;const l={m:e};f>0&&(l.$=f),c.f&&(l.f=c.f),c.l&&(l.l=c.l),o[n]=l}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;const f=c.f||"?";n[f]||(n[f]=[]);n[f].push(o)}}const c=new Set;for(const e of Object.values(o))e.f&&c.add(e.f);for(const e of Object.keys(n))c.add(e);const f={};for(const s of e.files||[]){if(c.has(s))continue;const e=s.lastIndexOf("/"),t=e>=0?s.slice(0,e+1):"./",o=e>=0?s.slice(e+1):s;f[t]||(f[t]=[]),f[t].push(o)}const l={v:e.v,L:t,s:e.stats,n:o,X:n,e:e.edges.length,o:e.orphans.length,d:Object.keys(e.duplicates).length};if(Object.keys(f).length>0&&(l.f=f),s&&s.length>0){const t=new Set(e.files||[]),o=s.filter(e=>!t.has(e));if(o.length>0){const e={};for(const s of o){const t=s.lastIndexOf("/"),o=t>=0?s.slice(0,t+1):"./",n=t>=0?s.slice(t+1):s;e[o]||(e[o]=[]),e[o].push(n)}l.a=e}}const _fi=e.fileImports||{};if(Object.keys(_fi).length>0){const _I={};for(const[_file,_sources]of Object.entries(_fi)){const _compact=_sources.map(s=>s.s);if(_compact.length>0)_I[_file]=_compact}if(Object.keys(_I).length>0)l.I=_I}return l}
|
package/src/core/parser.js
CHANGED
|
@@ -9,12 +9,12 @@ import{parsePython as d}from"../lang/lang-python.js";
|
|
|
9
9
|
import{parseGo as m}from"../lang/lang-go.js";
|
|
10
10
|
import{parseSQL as h,extractSQLFromString as y,isSQLString as g}from"../lang/lang-sql.js";
|
|
11
11
|
const x=[".js",".ts",".tsx",".py",".go",".sql"];
|
|
12
|
-
export async function parseFile(e,s){const t={file:s,classes:[],functions:[],imports:[],exports:[]},n=[];let r;try{r=a(e,{ecmaVersion:"latest",sourceType:"module",locations:!0,onComment:n})}catch(e){return console.warn(`Parse error in ${s}:`,e.message),t}const o=function(e,s){const t=new Map;for(const n of e){if("Block"!==n.type||!n.value.startsWith("*"))continue;const e="/*"+n.value+"*/",r=s.slice(0,n.end).split("\n").length,o=[],i=/@param\s+\{/g;let a;for(;null!==(a=i.exec(e));){let s=1,t=a.index+a[0].length;for(;t<e.length&&s>0;)"{"===e[t]?s++:"}"===e[t]&&s--,t++;if(0!==s)continue;const n=e.slice(a.index+a[0].length,t-1),r=e.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let i=r[1];i.startsWith("[")&&(i=i.slice(1)),i.endsWith("]")&&(i=i.slice(0,-1)),i.includes(".")||o.push({name:i,type:n})}let c=null;const l=e.match(/@returns?\s+\{([^}]+)\}/);l&&(c=l[1]),(o.length>0||c)&&t.set(r,{params:o,returns:c})}return t}(n,e),i=new Set;c.simple(r,{ImportDeclaration(e){for(const s of e.specifiers)"ImportDefaultSpecifier"===s.type?t.imports.push(s.local.name):"ImportSpecifier"===s.type&&t.imports.push(s.imported.name)},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id)i.add(e.declaration.id.name);else if(e.declaration.declarations)for(const s of e.declaration.declarations)i.add(s.id.name);if(e.specifiers)for(const s of e.specifiers)i.add(s.exported.name)},ExportDefaultDeclaration(e){e.declaration&&e.declaration.id&&i.add(e.declaration.id.name)},ClassDeclaration(e){const n={name:e.id.name,extends:e.superClass?e.superClass.name:null,methods:[],properties:[],calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};for(const s of e.body.body)if("MethodDefinition"===s.type&&"constructor"!==s.key.name)n.methods.push(s.key.name),j(s.value.body,n.calls,n.dbReads,n.dbWrites);else if("PropertyDefinition"===s.type&&"init$"===s.key.name&&s.value&&"ObjectExpression"===s.value.type)for(const e of s.value.properties)e.key&&e.key.name&&n.properties.push(e.key.name);t.classes.push(n)},FunctionDeclaration(e){if(e.id){const n=e.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"RestElement"===e.type&&"Identifier"===e.argument.type?"..."+e.argument.name:"ObjectPattern"===e.type?"options":"?"),r=function(e,s){for(let t=1;t<=3;t++){const n=e.get(s-t);if(n)return n}return null}(o,e.loc.start.line),i=function(e,s){if(!s||0===s.params.length)return e;const t=new Map;for(const e of s.params)t.set(e.name,e.type);return e.map(e=>{const s=e.startsWith("..."),n=e.endsWith("=");let r=e;s&&(r=r.slice(3)),n&&(r=r.slice(0,-1));let o=t.get(r);return o?(o.startsWith("...")&&(o=o.slice(3)),`${s?"...":""}${r}:${o}${n?"=":""}`):e})}(n,r),a={name:e.id.name,exported:!1,params:i,async:e.async||!1,returns:r?.returns||null,calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};j(e.body,a.calls,a.dbReads,a.dbWrites),t.functions.push(a)}}});for(const e of t.functions)e.exported=i.has(e.name);return t.exports=[...i],t}
|
|
12
|
+
export async function parseFile(e,s){const t={file:s,classes:[],functions:[],imports:[],exports:[],importSources:[]},n=[];let r;try{r=a(e,{ecmaVersion:"latest",sourceType:"module",locations:!0,onComment:n})}catch(e){return console.warn(`Parse error in ${s}:`,e.message),t}const o=function(e,s){const t=new Map;for(const n of e){if("Block"!==n.type||!n.value.startsWith("*"))continue;const e="/*"+n.value+"*/",r=s.slice(0,n.end).split("\n").length,o=[],i=/@param\s+\{/g;let a;for(;null!==(a=i.exec(e));){let s=1,t=a.index+a[0].length;for(;t<e.length&&s>0;)"{"===e[t]?s++:"}"===e[t]&&s--,t++;if(0!==s)continue;const n=e.slice(a.index+a[0].length,t-1),r=e.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let i=r[1];i.startsWith("[")&&(i=i.slice(1)),i.endsWith("]")&&(i=i.slice(0,-1)),i.includes(".")||o.push({name:i,type:n})}let c=null;const l=e.match(/@returns?\s+\{([^}]+)\}/);l&&(c=l[1]),(o.length>0||c)&&t.set(r,{params:o,returns:c})}return t}(n,e),i=new Set;c.simple(r,{ImportDeclaration(e){const _names=[];for(const s of e.specifiers)"ImportDefaultSpecifier"===s.type?(t.imports.push(s.local.name),_names.push(s.local.name)):"ImportSpecifier"===s.type&&(t.imports.push(s.imported.name),_names.push(s.imported.name));if(e.source&&e.source.value){t.importSources.push({s:e.source.value,n:_names})}},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id)i.add(e.declaration.id.name);else if(e.declaration.declarations)for(const s of e.declaration.declarations)i.add(s.id.name);if(e.specifiers)for(const s of e.specifiers)i.add(s.exported.name)},ExportDefaultDeclaration(e){e.declaration&&e.declaration.id&&i.add(e.declaration.id.name)},ClassDeclaration(e){const n={name:e.id.name,extends:e.superClass?e.superClass.name:null,methods:[],properties:[],calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};for(const s of e.body.body)if("MethodDefinition"===s.type&&"constructor"!==s.key.name)n.methods.push(s.key.name),j(s.value.body,n.calls,n.dbReads,n.dbWrites);else if("PropertyDefinition"===s.type&&"init$"===s.key.name&&s.value&&"ObjectExpression"===s.value.type)for(const e of s.value.properties)e.key&&e.key.name&&n.properties.push(e.key.name);t.classes.push(n)},FunctionDeclaration(e){if(e.id){const n=e.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"RestElement"===e.type&&"Identifier"===e.argument.type?"..."+e.argument.name:"ObjectPattern"===e.type?"options":"?"),r=function(e,s){for(let t=1;t<=3;t++){const n=e.get(s-t);if(n)return n}return null}(o,e.loc.start.line),i=function(e,s){if(!s||0===s.params.length)return e;const t=new Map;for(const e of s.params)t.set(e.name,e.type);return e.map(e=>{const s=e.startsWith("..."),n=e.endsWith("=");let r=e;s&&(r=r.slice(3)),n&&(r=r.slice(0,-1));let o=t.get(r);return o?(o.startsWith("...")&&(o=o.slice(3)),`${s?"...":""}${r}:${o}${n?"=":""}`):e})}(n,r),a={name:e.id.name,exported:!1,params:i,async:e.async||!1,returns:r?.returns||null,calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};j(e.body,a.calls,a.dbReads,a.dbWrites),t.functions.push(a)}}});for(const e of t.functions)e.exported=i.has(e.name);return t.exports=[...i],t}
|
|
13
13
|
const b=new Set(["query","execute","raw","exec","queryFile","none","one","many","any","oneOrNone","manyOrNone","result"]);
|
|
14
14
|
function j(e,s,t,n){e&&c.simple(e,{CallExpression(e){const r=e.callee;if("MemberExpression"===r.type){const e=r.object,t=r.property;if("Identifier"===t.type)if("Identifier"===e.type){const n=`${e.name}.${t.name}`;s.includes(n)||s.push(n)}else if("MemberExpression"===e.type&&"Identifier"===e.property.type){const n=`${e.property.name}.${t.name}`;s.includes(n)||s.push(n)}else if("ThisExpression"===e.type){const e=t.name;s.includes(e)||s.push(e)}}else if("Identifier"===r.type){const e=r.name;s.includes(e)||s.push(e)}if(t&&n){const s=function(e){const s=e.callee;return"MemberExpression"===s.type&&"Identifier"===s.property.type?s.property.name:null}(e);if(s&&b.has(s)&&e.arguments.length>0){const s=function(e){return e?"Literal"===e.type&&"string"==typeof e.value?e.value:"TemplateLiteral"===e.type?S(e):null:null}(e.arguments[0]);if(s&&g(s)){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}}}},TaggedTemplateExpression(e){if(!t||!n)return;const s=function(e){return"Identifier"===e.type?e.name:"MemberExpression"===e.type&&"Identifier"===e.property.type?e.property.name:null}(e.tag);if(s&&/sql/i.test(s)){const s=S(e.quasi);if(s){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}}},TemplateLiteral(e){if(!t||!n)return;const s=S(e);if(s&&g(s)){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}},Literal(e){if(t&&n&&"string"==typeof e.value&&g(e.value)){const s=y(e.value);s.reads.forEach(e=>{t.includes(e)||t.push(e)}),s.writes.forEach(e=>{n.includes(e)||n.push(e)})}}})}
|
|
15
15
|
function S(e){if(!e||!e.quasis)return"";let s="";for(let t=0;t<e.quasis.length;t++)s+=e.quasis[t].value.cooked||e.quasis[t].value.raw||"",t<e.expressions?.length&&(s+="$"+(t+1));return s}
|
|
16
16
|
export function discoverSubProjects(a){const c=i(a),l=[],p=["packages","apps","services","modules","libs","plugins"];for(const i of p){const a=r(c,i);if(n(a))try{for(const i of s(a)){const s=r(a,i),p=r(s,"package.json");if(t(s).isDirectory()&&n(p))try{const t=JSON.parse(e(p,"utf-8"));l.push({name:t.name||i,path:o(c,s),absolutePath:s})}catch{l.push({name:i,path:o(c,s),absolutePath:s})}}}catch{}}return l}
|
|
17
|
-
export async function parseProject(s,t={}){const n={files:[],classes:[],functions:[],imports:[],exports:[],tables:[]},a=i(s),c=findJSFiles(s);for(const s of c)try{const t=e(s,"utf-8"),r=o(a,s),i=await v(t,r);n.files.push(r),n.classes.push(...i.classes),n.functions.push(...i.functions),n.imports.push(...i.imports),n.exports.push(...i.exports),i.tables?.length&&n.tables.push(...i.tables)}catch(e){}if(t.recursive){const e=discoverSubProjects(s);n.subProjects=[];for(const s of e)try{const e=await parseProject(s.absolutePath);for(const t of e.files)n.files.push(r(s.path,t));for(const t of e.classes)t.file=r(s.path,t.file),n.classes.push(t);for(const t of e.functions)t.file=r(s.path,t.file),n.functions.push(t);n.imports.push(...e.imports),n.exports.push(...e.exports),e.tables?.length&&n.tables.push(...e.tables),n.subProjects.push({name:s.name,path:s.path,files:e.files.length})}catch{}}return n.imports=[...new Set(n.imports)],n.exports=[...new Set(n.exports)],n}
|
|
17
|
+
export async function parseProject(s,t={}){const n={files:[],classes:[],functions:[],imports:[],exports:[],tables:[],fileImports:{}},a=i(s),c=findJSFiles(s);for(const s of c)try{const t=e(s,"utf-8"),r=o(a,s),i=await v(t,r);n.files.push(r),n.classes.push(...i.classes),n.functions.push(...i.functions),n.imports.push(...i.imports),n.exports.push(...i.exports),i.tables?.length&&n.tables.push(...i.tables);if(i.importSources?.length)n.fileImports[r]=i.importSources}catch(e){}if(t.recursive){const e=discoverSubProjects(s);n.subProjects=[];for(const s of e)try{const e=await parseProject(s.absolutePath);for(const t of e.files)n.files.push(r(s.path,t));for(const t of e.classes)t.file=r(s.path,t.file),n.classes.push(t);for(const t of e.functions)t.file=r(s.path,t.file),n.functions.push(t);n.imports.push(...e.imports),n.exports.push(...e.exports),e.tables?.length&&n.tables.push(...e.tables),n.subProjects.push({name:s.name,path:s.path,files:e.files.length})}catch{}}return n.imports=[...new Set(n.imports)],n.exports=[...new Set(n.exports)],n}
|
|
18
18
|
async function v(e,s){return s.endsWith(".sql")?h(e,s):s.endsWith(".py")?d(e,s):s.endsWith(".go")?m(e,s):s.endsWith(".ts")||s.endsWith(".tsx")?f(e,s):parseFile(e,s)}
|
|
19
19
|
function E(e){return!e.endsWith(".css.js")&&!e.endsWith(".tpl.js")&&x.some(s=>e.endsWith(s))}
|
|
20
20
|
export function findJSFiles(e,n=e){e===n&&u(n);const i=[];try{for(const a of s(e)){const s=r(e,a),c=t(s),u=o(n,e);c.isDirectory()?l(a,u)||i.push(...findJSFiles(s,n)):E(a)&&(p(a,u)||i.push(s))}}catch(s){console.warn(`Cannot read directory ${e}:`,s.message)}return i}
|
package/src/network/server.js
CHANGED
|
@@ -5,5 +5,4 @@ const c=i({input:process.stdin,terminal:!1}),a=[];
|
|
|
5
5
|
let l=!1,p=null,d=null;
|
|
6
6
|
const startProxy=async e=>{if(!l){l=!0,c.removeAllListeners("line"),c.close(),n.write(`RESOLVED: ${e}\n`),n.end();try{const t=await r(e);console.error(`[project-graph] Connected to backend on port ${t} (project: ${e})`),s(t,a)}catch(e){console.error(`[project-graph] Singleton failed (${e.message}), falling back to direct stdio`);const{startStdioServer:t}=await import("../mcp/mcp-server.js");t(a)}}};c.on("line",t=>{try{const r=JSON.parse(t);if(n.write(`IN: ${r.method||`response:${r.id}`}\n`),"initialize"===r.method){d=r.id,r.params?.roots?.length>0&&(e(r.params.roots),n.write("ROOTS from initialize.params\n"));
|
|
7
7
|
const t=JSON.stringify({jsonrpc:"2.0",id:r.id,result:{protocolVersion:"2025-06-18",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:_v}}});return n.write("OUT: initialize response\n"),void process.stdout.write(t+"\n")}if("initialized"===r.method||"notifications/initialized"===r.method){n.write("IN: initialized notification\n"),p=999999;
|
|
8
|
-
const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p
|
|
9
|
-
const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}a.push(t)}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
|
|
8
|
+
const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p){n.write(`IN: roots/list response: ${JSON.stringify(r.result)}\n`);if(r.result?.roots?.length>0){e(r.result.roots)}const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}if(r.method&&void 0!==r.id){n.write(`BUFFERED: ${r.method} id=${r.id}\n`),a.push(t)}else{a.push(t)}}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
|
|
@@ -5,4 +5,7 @@ function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[
|
|
|
5
5
|
function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
|
|
6
6
|
function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
|
|
7
7
|
function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
|
|
8
|
-
export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/expand-file":{const e=a.get("path");if(e){try{const _pt2=await import("path"),_fs2=await import("fs"),_abs=_pt2.resolve(t,e),_ext3=_pt2.extname(e).toLowerCase(),_jsExts3=new Set([".js",".mjs",".ts",".tsx"]);if(!_jsExts3.has(_ext3)){const raw=_fs2.readFileSync(_abs,"utf-8");c={code:raw,file:e,injected:0};break}let _ctxContent=null;const _ctxName=_pt2.basename(e,_pt2.extname(e))+".ctx";const _ctxPath1=_pt2.join(t,_pt2.dirname(e),_ctxName);const _ctxPath2=_pt2.join(t,".context",_pt2.dirname(e),_ctxName);if(_fs2.existsSync(_ctxPath1))_ctxContent=_fs2.readFileSync(_ctxPath1,"utf-8");else if(_fs2.existsSync(_ctxPath2))_ctxContent=_fs2.readFileSync(_ctxPath2,"utf-8");const _result=await _ef(_abs,_ctxContent);c={code:_result.code,file:e,injected:_result.injected,original:_result.original,decompiled:_result.decompiled}}catch(err){c={code:`// Expand failed: ${err.message}`,file:e}}}else c={code:"// No file specified",file:""};break}case"/api/image":{const e=a.get("path");if(e){try{const _fs=await import("fs"),_pt=await import("path"),_imgPath=_pt.resolve(t,e);if(!_fs.existsSync(_imgPath)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[_pt.extname(e).toLowerCase()]||"application/octet-stream";const buf=_fs.readFileSync(_imgPath);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
|
|
8
|
+
export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/expand-file":{const e=a.get("path");if(e){try{const _pt2=await import("path"),_fs2=await import("fs"),_abs=_pt2.resolve(t,e),_ext3=_pt2.extname(e).toLowerCase(),_jsExts3=new Set([".js",".mjs",".ts",".tsx"]);if(!_jsExts3.has(_ext3)){const raw=_fs2.readFileSync(_abs,"utf-8");c={code:raw,file:e,injected:0};break}let _ctxContent=null;const _ctxName=_pt2.basename(e,_pt2.extname(e))+".ctx";const _ctxPath1=_pt2.join(t,_pt2.dirname(e),_ctxName);const _ctxPath2=_pt2.join(t,".context",_pt2.dirname(e),_ctxName);if(_fs2.existsSync(_ctxPath1))_ctxContent=_fs2.readFileSync(_ctxPath1,"utf-8");else if(_fs2.existsSync(_ctxPath2))_ctxContent=_fs2.readFileSync(_ctxPath2,"utf-8");const _result=await _ef(_abs,_ctxContent);
|
|
9
|
+
// Validation removed: we enforce a zero-fallback policy. Code must be generated correctly or fail loudly.
|
|
10
|
+
let _expandedCode=_result.code;
|
|
11
|
+
c={code:_expandedCode,file:e,injected:_result.injected,original:_result.original,decompiled:_result.decompiled}}catch(err){c={code:`// Expand failed: ${err.message}`,file:e}}}else c={code:"// No file specified",file:""};break}case"/api/image":{const e=a.get("path");if(e){try{const _fs=await import("fs"),_pt=await import("path"),_imgPath=_pt.resolve(t,e);if(!_fs.existsSync(_imgPath)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[_pt.extname(e).toLowerCase()]||"application/octet-stream";const buf=_fs.readFileSync(_imgPath);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to symbiote-node will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.3.0-alpha.0] — 2026-04-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Memory leak**: zombie `setTimeout` loops in SubgraphNode preview rendering — replaced with on-demand redraws
|
|
9
|
+
- **Memory leak**: event listener accumulation in `NodeCanvas.setEditor()` — added explicit unsubscribe on context switch
|
|
10
|
+
- **Memory leak**: incorrect `cancelAnimationFrame` cleanup for `setTimeout` IDs in `NodeViewManager.removeView()`
|
|
11
|
+
- **Layout overlap**: nodes measured as 4px height (DOM not ready) caused overlap — enforced minimum `nodeHeight` floor in `getSize()`
|
|
12
|
+
- **Inspector z-index**: panel header overlapped toolbar buttons — removed header, added toolbar-aware padding
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `Editor.removeAllListeners()` — clean teardown method for editor event system
|
|
16
|
+
- `computeTreeLayout()` — directory-hierarchy-aware tree layout with indent levels
|
|
17
|
+
- Shape primitives: `CircleShape`, `DiamondShape`, `PillShape`, `RectShape`
|
|
18
|
+
- PCB dark theme enhancements: improved node styling, copper trace connections
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
- `InspectorPanel` no longer renders a title header bar — consumers relying on `.insp-header` CSS should update
|
|
22
|
+
|
|
23
|
+
## [0.2.1] — 2026-04-13
|
|
24
|
+
|
|
25
|
+
- Initial open-source release
|
|
26
|
+
- Node graph editor with Symbiote.js web components
|
|
27
|
+
- Sugiyama-based auto layout (`computeAutoLayout`)
|
|
28
|
+
- PCB/Carbon theming system
|
|
29
|
+
- Inspector panel with resize handle
|
|
30
|
+
- Subgraph navigation (drill-down/drill-up)
|
|
31
|
+
- Execution engine with topological sorting
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RND-PRO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|