project-graph-mcp 2.2.6 → 2.3.1
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 +4 -13
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/src/compact/expand.js +1 -1
- 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 +1 -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 +9 -5
- package/web/components/canvas-graph.js +1705 -0
- package/web/components/code-block.js +1 -1
- 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 +159 -0
- package/web/components/follow-ribbon.js +134 -0
- package/web/dashboard.js +1 -1
- package/web/follow-controller.js +241 -0
- 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 +2691 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +39 -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,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-graph-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
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
|
-
"vendor/",
|
|
14
|
-
"rules/",
|
|
15
|
-
"docs/",
|
|
16
|
-
"GUIDE.md",
|
|
17
|
-
"CONFIGURATION.md",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
21
13
|
"scripts": {
|
|
22
14
|
"start": "node src/network/server.js",
|
|
23
15
|
"test": "node --test tests/*.test.js"
|
|
@@ -47,7 +39,6 @@
|
|
|
47
39
|
"license": "MIT",
|
|
48
40
|
"dependencies": {
|
|
49
41
|
"@symbiotejs/symbiote": "^3.2.1",
|
|
50
|
-
"symbiote-node": "^0.2.0",
|
|
51
42
|
"ws": "^8.20.0"
|
|
52
43
|
},
|
|
53
44
|
"engines": {
|
|
Binary file
|
package/src/compact/expand.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
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}}i=w(i,l,a,c,p);l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0});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}}
|
|
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}}
|
|
6
6
|
|
|
7
7
|
|
|
8
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)}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ctx .context/src/network/web-server.ctx
|
|
2
2
|
import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{expandFile as _ef}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,"..","..");const _rq=_createRequire(import.meta.url);let _pkgVersion="0.0.0";try{_pkgVersion=JSON.parse(t.readFileSync(o.join(p,"package.json"),"utf8")).version}catch{}function _rv(k){try{const r=_rq.resolve(k);const marker=o.sep+"node_modules"+o.sep+k.replace(/\//g,o.sep);const idx=r.lastIndexOf(marker);if(idx>=0)return r.substring(0,idx+marker.length);return o.dirname(r)}catch{return o.join(p,"node_modules",...k.split("/"))}}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
|
|
3
|
+
const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");const _rq=_createRequire(import.meta.url);let _pkgVersion="0.0.0";try{_pkgVersion=JSON.parse(t.readFileSync(o.join(p,"package.json"),"utf8")).version}catch{}function _rv(k){try{const r=_rq.resolve(k);const marker=o.sep+"node_modules"+o.sep+k.replace(/\//g,o.sep);const idx=r.lastIndexOf(marker);if(idx>=0)return r.substring(0,idx+marker.length);return o.dirname(r)}catch{return o.join(p,"node_modules",...k.split("/"))}}const m=o.join(p,"web"),_symNodeVendor=o.join(p,"vendor","symbiote-node"),h={"symbiote-node":t.existsSync(_symNodeVendor)?_symNodeVendor:_rv("symbiote-node"),symbiote:_rv("@symbiotejs/symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
|
|
4
4
|
function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
|
|
5
5
|
function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
|
|
6
6
|
function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
|
|
@@ -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.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
|
+
[](https://nodejs.org)
|
|
3
|
+
[](#)
|
|
4
|
+
[](#)
|
|
5
|
+
|
|
6
|
+
# symbiote-node
|
|
7
|
+
|
|
8
|
+
A **visual node graph editor** and **execution engine** built on [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — extensible, themeable, zero-dependency. Pure Web Components, works anywhere: vanilla HTML, React, Vue, Svelte, or any framework that supports custom elements.
|
|
9
|
+
|
|
10
|
+
> [!TIP]
|
|
11
|
+
> **22K lines, 150 files, zero external dependencies.** 70+ public API exports. Clone, serve, and start building node graphs in under a minute.
|
|
12
|
+
|
|
13
|
+
### Graph Editor
|
|
14
|
+
|
|
15
|
+
The editor constructs visual node graphs from a data model. Nodes have typed input/output ports with compatibility validation, color-coded category headers, inline controls, and drag & drop with snap-to-grid. Connections render as SVG Bézier curves with gradient coloring.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { NodeEditor, Node, Socket, Input, Output, NodeCanvas } from 'symbiote-node';
|
|
19
|
+
|
|
20
|
+
const editor = new NodeEditor();
|
|
21
|
+
const socket = new Socket('data', { color: '#4a9eff' });
|
|
22
|
+
|
|
23
|
+
const node1 = new Node('Source', { category: 'server' });
|
|
24
|
+
node1.addOutput('out', new Output(socket, 'Output'));
|
|
25
|
+
|
|
26
|
+
const node2 = new Node('Target', { category: 'control' });
|
|
27
|
+
node2.addInput('in', new Input(socket, 'Input'));
|
|
28
|
+
|
|
29
|
+
editor.addNode(node1);
|
|
30
|
+
editor.addNode(node2);
|
|
31
|
+
|
|
32
|
+
const canvas = document.querySelector('node-canvas');
|
|
33
|
+
canvas.setEditor(editor);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Execution Engine
|
|
37
|
+
|
|
38
|
+
Server-side graph runtime with handler packs for data flow, control flow, I/O, and transforms. Graphs serialize to JSON and execute with topological ordering, retry logic, and parallel barriers.
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { Graph, Executor, Registry } from 'symbiote-node/engine';
|
|
42
|
+
|
|
43
|
+
const registry = new Registry();
|
|
44
|
+
await registry.loadDir('./engine/packs');
|
|
45
|
+
|
|
46
|
+
const graph = Graph.fromFile('workflow.json');
|
|
47
|
+
const executor = new Executor(graph, registry);
|
|
48
|
+
const results = await executor.run();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Node Shapes
|
|
52
|
+
|
|
53
|
+
Two coexisting rendering modes on the same canvas — **HTML nodes** (CSS-styled rectangles) and **SVG nodes** (arbitrary vector shapes with perimeter-aware connector positioning). Built-in presets: `hexagon`, `star`, `cloud`, `shield`, `heart`, `rect`, `pill`, `circle`, `diamond`, `comment`.
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import { createSVGShape, registerShape } from 'symbiote-node';
|
|
57
|
+
|
|
58
|
+
const myShape = createSVGShape('myshape', 'M12 2L22 8V16L12 22L2 16V8Z');
|
|
59
|
+
registerShape('myshape', myShape);
|
|
60
|
+
|
|
61
|
+
const node = new Node('Custom', { shape: 'myshape' });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Theme System
|
|
65
|
+
|
|
66
|
+
Separate **Palette** (colors), **Skin** (geometry), and **Theme** (combined) layers — all driven by CSS custom properties. Switch at runtime without page reload.
|
|
67
|
+
|
|
68
|
+
| Theme | Description |
|
|
69
|
+
|-------|-------------|
|
|
70
|
+
| `GREY_NEUTRAL` | Balanced grey UI (default) |
|
|
71
|
+
| `DARK_DEFAULT` | Professional dark interface |
|
|
72
|
+
| `LIGHT_CLEAN` | Light mode |
|
|
73
|
+
| `SYNTHWAVE` | Neon retro aesthetic |
|
|
74
|
+
| `NEON_GLOW` | Vivid glow effects |
|
|
75
|
+
| `CARBON` | IBM Carbon-inspired |
|
|
76
|
+
| `EBOOK` | Warm paper-like reading theme |
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
import { applyTheme, CARBON, applyPalette, SYNTHWAVE_PALETTE } from 'symbiote-node';
|
|
80
|
+
|
|
81
|
+
applyTheme(canvasElement, CARBON); // Full theme
|
|
82
|
+
applyPalette(canvasElement, SYNTHWAVE_PALETTE); // Colors only
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Layout System (BSP)
|
|
86
|
+
|
|
87
|
+
Binary Space Partitioning layout engine for IDE-style panel workspaces. Panels resize by dragging dividers, sections split horizontally or vertically. Sidebar navigation with section switching and panel routing.
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { Layout, LayoutTree, LayoutSidebar } from 'symbiote-node';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Plugins & Interactions
|
|
94
|
+
|
|
95
|
+
- **History** — undo/redo with keyboard bindings (Ctrl+Z / Ctrl+Shift+Z)
|
|
96
|
+
- **Readonly** — toggle read-only mode, blocks all mutations
|
|
97
|
+
- **FlowSimulator** — topological sort-based data flow animation with marching ants
|
|
98
|
+
- **SnapGrid** — configurable grid snapping
|
|
99
|
+
- **Selector** — rubber-band multi-select
|
|
100
|
+
- **ConnectFlow** — socket highlighting during connection drag
|
|
101
|
+
- **AutoLayout** — Sugiyama-based automatic node arrangement
|
|
102
|
+
- **Minimap** — viewport minimap with live position tracking
|
|
103
|
+
- **NodeSearch** — search/omnibox for quick node insertion
|
|
104
|
+
- **GraphTabs** — multi-page graph management
|
|
105
|
+
- **Subgraphs** — drill-down with breadcrumb navigation
|
|
106
|
+
- **Portals** — named reroutes for cross-graph connections
|
|
107
|
+
- **InspectorPanel** — property inspector sidebar
|
|
108
|
+
- **QuickToolbar** — floating action toolbar above selected node
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/RND-PRO/symbiote-node.git
|
|
114
|
+
cd symbiote-node
|
|
115
|
+
npx -y serve -l 3000 .
|
|
116
|
+
# Open http://localhost:3000/demo/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### As ES Module (CDN)
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<script type="importmap">
|
|
123
|
+
{
|
|
124
|
+
"imports": {
|
|
125
|
+
"@symbiotejs/symbiote": "https://esm.sh/@symbiotejs/symbiote@3.2.1",
|
|
126
|
+
"symbiote-node": "./index.js"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
<script type="module">
|
|
131
|
+
import { NodeEditor, Node, Socket, Input, Output, NodeCanvas } from 'symbiote-node';
|
|
132
|
+
import 'symbiote-node/canvas/NodeCanvas/NodeCanvas.js';
|
|
133
|
+
import 'symbiote-node/node/GraphNode/GraphNode.js';
|
|
134
|
+
// ...
|
|
135
|
+
</script>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## CLI (Engine)
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
node engine/cli.js run <workflow.json> # Execute graph
|
|
142
|
+
node engine/cli.js validate <workflow.json> # Validate graph
|
|
143
|
+
node engine/cli.js list # List available node types
|
|
144
|
+
node engine/cli.js inspect <workflow.json> # Inspect graph structure
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Engine Handler Packs
|
|
148
|
+
|
|
149
|
+
Built-in node types organized by domain:
|
|
150
|
+
|
|
151
|
+
| Pack | Handlers | Description |
|
|
152
|
+
|------|----------|-------------|
|
|
153
|
+
| `flow` | `if`, `switch`, `loop`, `merge`, `retry`, `wait-all`, `agent` | Control flow and branching |
|
|
154
|
+
| `data` | `db-query`, `prompt-loader`, `rss-feed` | Data sources |
|
|
155
|
+
| `transform` | `json-parse`, `set`, `template`, `template-builder` | Data transformation |
|
|
156
|
+
| `io` | `http-request`, `read-file`, `write-file` | External I/O |
|
|
157
|
+
| `util` | `delay`, `log` | Utility nodes |
|
|
158
|
+
| `debug` | `inject` | Testing and debugging |
|
|
159
|
+
|
|
160
|
+
Custom handler packs can be loaded from any directory via `registry.loadDir()`.
|
|
161
|
+
|
|
162
|
+
## Project Structure
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
symbiote-node/
|
|
166
|
+
├── index.js — public API (70+ exports)
|
|
167
|
+
├── core/ — Editor, Node, Connection, Socket, Portal
|
|
168
|
+
├── canvas/ — NodeCanvas, ConnectionRenderer, FlowSimulator, AutoLayout
|
|
169
|
+
├── node/ — GraphNode, PortItem, CtrlItem, NodeSocket
|
|
170
|
+
├── menu/ — ContextMenu
|
|
171
|
+
├── interactions/ — Drag, Zoom, Selector, SnapGrid, ConnectFlow
|
|
172
|
+
├── shapes/ — SVG shape system with 10 presets
|
|
173
|
+
├── themes/ — Theme, Palette, Skin (7 themes)
|
|
174
|
+
├── layout/ — BSP layout engine + LayoutSidebar + LayoutRouter
|
|
175
|
+
├── toolbar/ — QuickToolbar
|
|
176
|
+
├── inspector/ — InspectorPanel
|
|
177
|
+
├── palette/ — PaletteBrowser
|
|
178
|
+
├── plugins/ — Readonly, History
|
|
179
|
+
├── engine/ — Server-side graph runtime
|
|
180
|
+
│ ├── packs/ — Built-in handler packs (flow, data, transform, io, util)
|
|
181
|
+
│ └── cli.js — CLI runner
|
|
182
|
+
├── demo/ — Interactive demo
|
|
183
|
+
└── tests/ — 45 tests (geometry, serialization, topology)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Tests
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
node --test tests/*.test.js
|
|
190
|
+
# 45 tests: geometry, Bézier paths, serialization, socket compatibility,
|
|
191
|
+
# collapse/mute, subgraphs, execution, duck-typing
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Related Projects
|
|
195
|
+
|
|
196
|
+
- [project-graph-mcp](https://github.com/rnd-pro/project-graph-mcp) — MCP server for AI agents: project graph, code analysis, 18 tools
|
|
197
|
+
- [agent-pool-mcp](https://github.com/rnd-pro/agent-pool-mcp) — Multi-agent orchestration via Gemini CLI
|
|
198
|
+
- [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — Isomorphic Reactive Web Components framework
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT © [RND-PRO.com](https://rnd-pro.com)
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
**Made with ❤️ by the RND-PRO team**
|