code-graph-context 2.2.0 → 2.4.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/README.md +119 -64
- package/dist/cli/cli.js +266 -0
- package/dist/cli/neo4j-docker.js +159 -0
- package/dist/mcp/constants.js +120 -0
- package/dist/mcp/handlers/incremental-parse.handler.js +19 -0
- package/dist/mcp/mcp.server.js +76 -1
- package/dist/mcp/service-init.js +53 -0
- package/dist/mcp/services/watch-manager.js +57 -7
- package/dist/mcp/tools/hello.tool.js +16 -2
- package/dist/mcp/tools/index.js +33 -0
- package/dist/mcp/tools/swarm-cleanup.tool.js +157 -0
- package/dist/mcp/tools/swarm-constants.js +35 -0
- package/dist/mcp/tools/swarm-pheromone.tool.js +196 -0
- package/dist/mcp/tools/swarm-sense.tool.js +212 -0
- package/dist/storage/neo4j/neo4j.service.js +8 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ A Model Context Protocol (MCP) server that builds rich code graphs to provide de
|
|
|
30
30
|
- **Impact Analysis**: Assess refactoring risk with dependency analysis (LOW/MEDIUM/HIGH/CRITICAL scoring)
|
|
31
31
|
- **Dead Code Detection**: Find unreferenced exports, uncalled private methods, unused interfaces with confidence scoring
|
|
32
32
|
- **Duplicate Code Detection**: Identify structural duplicates (identical AST) and semantic duplicates (similar logic via embeddings)
|
|
33
|
+
- **Swarm Coordination**: Multi-agent stigmergic coordination through pheromone markers with exponential decay
|
|
33
34
|
- **High Performance**: Optimized Neo4j storage with vector indexing for fast retrieval
|
|
34
35
|
- **MCP Integration**: Seamless integration with Claude Code and other MCP-compatible tools
|
|
35
36
|
|
|
@@ -64,100 +65,90 @@ The system uses a dual-schema approach:
|
|
|
64
65
|
|
|
65
66
|
Choose the installation method that works best for you:
|
|
66
67
|
|
|
67
|
-
#### Option 1:
|
|
68
|
+
#### Option 1: NPM Install (Recommended)
|
|
68
69
|
|
|
69
|
-
Best for: Contributing to the project or customizing the code
|
|
70
|
-
|
|
71
|
-
1. **Clone the repository:**
|
|
72
70
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```
|
|
71
|
+
# Install globally
|
|
72
|
+
npm install -g code-graph-context
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
# Set up Neo4j (requires Docker)
|
|
75
|
+
code-graph-context init
|
|
76
|
+
|
|
77
|
+
# Add to Claude Code
|
|
78
|
+
claude mcp add code-graph-context code-graph-context
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
|
|
81
|
+
Then configure your OpenAI API key in `~/.config/claude/config.json`:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"code-graph-context": {
|
|
86
|
+
"command": "code-graph-context",
|
|
87
|
+
"env": {
|
|
88
|
+
"OPENAI_API_KEY": "sk-your-key-here"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
- Web interface: http://localhost:7474
|
|
89
|
-
- Bolt connection: bolt://localhost:7687
|
|
90
|
-
- Username: `neo4j`, Password: `PASSWORD`
|
|
95
|
+
#### Option 2: From Source
|
|
91
96
|
|
|
92
|
-
4. **Configure environment variables:**
|
|
93
97
|
```bash
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
# Clone and build
|
|
99
|
+
git clone https://github.com/drewdrewH/code-graph-context.git
|
|
100
|
+
cd code-graph-context
|
|
101
|
+
npm install
|
|
102
|
+
npm run build
|
|
103
|
+
|
|
104
|
+
# Set up Neo4j
|
|
105
|
+
code-graph-context init
|
|
106
|
+
|
|
107
|
+
# Add to Claude Code (use absolute path)
|
|
108
|
+
claude mcp add code-graph-context node /absolute/path/to/code-graph-context/dist/cli/cli.js
|
|
96
109
|
```
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
### CLI Commands
|
|
112
|
+
|
|
113
|
+
The package includes a CLI for managing Neo4j:
|
|
114
|
+
|
|
99
115
|
```bash
|
|
100
|
-
|
|
116
|
+
code-graph-context init [options] # Set up Neo4j container
|
|
117
|
+
code-graph-context status # Check Docker/Neo4j status
|
|
118
|
+
code-graph-context stop # Stop Neo4j container
|
|
101
119
|
```
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
|
|
121
|
+
**Init options:**
|
|
122
|
+
```
|
|
123
|
+
-p, --port <port> Bolt port (default: 7687)
|
|
124
|
+
--http-port <port> Browser port (default: 7474)
|
|
125
|
+
--password <password> Neo4j password (default: PASSWORD)
|
|
126
|
+
-m, --memory <size> Heap memory (default: 2G)
|
|
127
|
+
-f, --force Recreate container
|
|
106
128
|
```
|
|
107
129
|
|
|
108
|
-
|
|
130
|
+
### Alternative Neo4j Setup
|
|
109
131
|
|
|
110
|
-
|
|
132
|
+
If you prefer not to use the CLI, you can set up Neo4j manually:
|
|
111
133
|
|
|
112
|
-
|
|
134
|
+
**Docker Compose:**
|
|
113
135
|
```bash
|
|
114
|
-
|
|
136
|
+
docker-compose up -d
|
|
115
137
|
```
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**Option A: Docker (Recommended)**
|
|
139
|
+
**Docker Run:**
|
|
120
140
|
```bash
|
|
121
141
|
docker run -d \
|
|
122
142
|
--name code-graph-neo4j \
|
|
123
143
|
-p 7474:7474 -p 7687:7687 \
|
|
124
144
|
-e NEO4J_AUTH=neo4j/PASSWORD \
|
|
125
|
-
-e NEO4J_PLUGINS=
|
|
145
|
+
-e 'NEO4J_PLUGINS=["apoc"]' \
|
|
126
146
|
neo4j:5.23
|
|
127
147
|
```
|
|
128
148
|
|
|
129
|
-
**
|
|
130
|
-
- Download from [neo4j.com/download](https://neo4j.com/download/)
|
|
131
|
-
- Install APOC plugin
|
|
132
|
-
- Start database
|
|
149
|
+
**Neo4j Desktop:** Download from [neo4j.com/download](https://neo4j.com/download/) and install APOC plugin.
|
|
133
150
|
|
|
134
|
-
**
|
|
135
|
-
- Create free account at [neo4j.com/cloud/aura](https://neo4j.com/cloud/platform/aura-graph-database/)
|
|
136
|
-
- Note your connection URI and credentials
|
|
137
|
-
|
|
138
|
-
3. **Add to Claude Code:**
|
|
139
|
-
```bash
|
|
140
|
-
claude mcp add code-graph-context code-graph-context
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Then configure in your MCP config file (`~/.config/claude/config.json`):
|
|
144
|
-
```json
|
|
145
|
-
{
|
|
146
|
-
"mcpServers": {
|
|
147
|
-
"code-graph-context": {
|
|
148
|
-
"command": "code-graph-context",
|
|
149
|
-
"env": {
|
|
150
|
-
"OPENAI_API_KEY": "sk-your-key-here",
|
|
151
|
-
"NEO4J_URI": "bolt://localhost:7687",
|
|
152
|
-
"NEO4J_USER": "neo4j",
|
|
153
|
-
"NEO4J_PASSWORD": "PASSWORD"
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Note:** The env vars can be configured for any Neo4j instance - local, Docker, cloud (Aura), or enterprise.
|
|
151
|
+
**Neo4j Aura (Cloud):** Create account at [neo4j.com/cloud/aura](https://neo4j.com/cloud/platform/aura-graph-database/) and configure connection URI in env vars.
|
|
161
152
|
|
|
162
153
|
### Verify Installation
|
|
163
154
|
|
|
@@ -255,6 +246,9 @@ npm run build
|
|
|
255
246
|
| `natural_language_to_cypher` | Convert natural language to Cypher | **Advanced queries** - complex graph queries |
|
|
256
247
|
| `detect_dead_code` | Find unreferenced exports, uncalled methods, unused interfaces | **Code cleanup** - identify potentially removable code |
|
|
257
248
|
| `detect_duplicate_code` | Find structural and semantic code duplicates | **Refactoring** - identify DRY violations |
|
|
249
|
+
| `swarm_pheromone` | Leave pheromone markers on code nodes | **Multi-agent** - stigmergic coordination |
|
|
250
|
+
| `swarm_sense` | Query pheromones in the code graph | **Multi-agent** - sense what other agents are doing |
|
|
251
|
+
| `swarm_cleanup` | Bulk delete pheromones | **Multi-agent** - cleanup after swarm completion |
|
|
258
252
|
| `test_neo4j_connection` | Verify database connectivity | **Health check** - troubleshooting |
|
|
259
253
|
|
|
260
254
|
> **Note**: All query tools (`search_codebase`, `traverse_from_node`, `impact_analysis`, `natural_language_to_cypher`) require a `projectId` parameter. Use `list_projects` to discover available projects.
|
|
@@ -755,6 +749,67 @@ await mcp.call('stop_watch_project', {
|
|
|
755
749
|
- 1000 pending events per watcher
|
|
756
750
|
- Graceful cleanup on server shutdown
|
|
757
751
|
|
|
752
|
+
#### 8. Swarm Coordination Tools
|
|
753
|
+
**Purpose**: Enable multiple parallel agents to coordinate work through stigmergic pheromone markers in the code graph—no direct messaging needed.
|
|
754
|
+
|
|
755
|
+
**Core Concepts:**
|
|
756
|
+
- **Pheromones**: Markers attached to graph nodes that decay over time
|
|
757
|
+
- **swarmId**: Groups related agents for bulk cleanup when done
|
|
758
|
+
- **Workflow States**: `exploring`, `claiming`, `modifying`, `completed`, `blocked` (mutually exclusive per agent+node)
|
|
759
|
+
- **Flags**: `warning`, `proposal`, `needs_review` (can coexist with workflow states)
|
|
760
|
+
|
|
761
|
+
**Pheromone Types & Decay:**
|
|
762
|
+
| Type | Half-Life | Use |
|
|
763
|
+
|------|-----------|-----|
|
|
764
|
+
| `exploring` | 2 min | Browsing/reading |
|
|
765
|
+
| `modifying` | 10 min | Active work |
|
|
766
|
+
| `claiming` | 1 hour | Ownership |
|
|
767
|
+
| `completed` | 24 hours | Done |
|
|
768
|
+
| `warning` | Never | Danger |
|
|
769
|
+
| `blocked` | 5 min | Stuck |
|
|
770
|
+
| `proposal` | 1 hour | Awaiting approval |
|
|
771
|
+
| `needs_review` | 30 min | Review requested |
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
// Orchestrator: Generate swarm ID and spawn agents
|
|
775
|
+
const swarmId = `swarm_${Date.now()}`;
|
|
776
|
+
|
|
777
|
+
// Agent: Check what's claimed before working
|
|
778
|
+
await mcp.call('swarm_sense', {
|
|
779
|
+
projectId: 'my-backend',
|
|
780
|
+
swarmId,
|
|
781
|
+
types: ['claiming', 'modifying']
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Agent: Claim a node before working
|
|
785
|
+
await mcp.call('swarm_pheromone', {
|
|
786
|
+
projectId: 'my-backend',
|
|
787
|
+
nodeId: 'proj_xxx:ClassDeclaration:abc123', // From search_codebase or traverse_from_node
|
|
788
|
+
type: 'claiming',
|
|
789
|
+
agentId: 'agent_1',
|
|
790
|
+
swarmId
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Agent: Mark complete when done
|
|
794
|
+
await mcp.call('swarm_pheromone', {
|
|
795
|
+
projectId: 'my-backend',
|
|
796
|
+
nodeId: 'proj_xxx:ClassDeclaration:abc123',
|
|
797
|
+
type: 'completed',
|
|
798
|
+
agentId: 'agent_1',
|
|
799
|
+
swarmId,
|
|
800
|
+
data: { summary: 'Added soft delete support' }
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// Orchestrator: Clean up when swarm is done
|
|
804
|
+
await mcp.call('swarm_cleanup', {
|
|
805
|
+
projectId: 'my-backend',
|
|
806
|
+
swarmId,
|
|
807
|
+
keepTypes: ['warning'] // Preserve warnings
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Important**: Node IDs must come from graph tool responses (`search_codebase`, `traverse_from_node`). Never fabricate node IDs—they are hash-based strings like `proj_xxx:ClassDeclaration:abc123`.
|
|
812
|
+
|
|
758
813
|
### Workflow Examples
|
|
759
814
|
|
|
760
815
|
#### Example 1: Understanding Authentication Flow
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI Entry Point for Code Graph Context
|
|
4
|
+
*
|
|
5
|
+
* Handles CLI commands (init, status, stop) and delegates to MCP server
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { NEO4J_CONFIG, createContainer, getContainerStatus, getFullStatus, isApocAvailable, isDockerInstalled, isDockerRunning, removeContainer, startContainer, stopContainer, waitForNeo4j, } from './neo4j-docker.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
// ANSI colors
|
|
15
|
+
const c = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
red: '\x1b[31m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
blue: '\x1b[34m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
};
|
|
25
|
+
const sym = {
|
|
26
|
+
ok: `${c.green}✓${c.reset}`,
|
|
27
|
+
err: `${c.red}✗${c.reset}`,
|
|
28
|
+
warn: `${c.yellow}⚠${c.reset}`,
|
|
29
|
+
info: `${c.blue}ℹ${c.reset}`,
|
|
30
|
+
};
|
|
31
|
+
const log = (symbol, msg) => {
|
|
32
|
+
console.log(` ${symbol} ${msg}`);
|
|
33
|
+
};
|
|
34
|
+
const header = (text) => {
|
|
35
|
+
console.log(`\n${c.bold}${text}${c.reset}\n`);
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Spinner for async operations
|
|
39
|
+
*/
|
|
40
|
+
const spinner = (msg) => {
|
|
41
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
42
|
+
let i = 0;
|
|
43
|
+
const interval = setInterval(() => {
|
|
44
|
+
process.stdout.write(`\r ${c.blue}${frames[i]}${c.reset} ${msg}`);
|
|
45
|
+
i = (i + 1) % frames.length;
|
|
46
|
+
}, 80);
|
|
47
|
+
return {
|
|
48
|
+
stop: (ok, finalMsg) => {
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
process.stdout.write(`\r ${ok ? sym.ok : sym.err} ${finalMsg || msg}\n`);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Print config instructions
|
|
56
|
+
*/
|
|
57
|
+
const printConfigInstructions = (password, boltPort) => {
|
|
58
|
+
console.log(`
|
|
59
|
+
${c.bold}Next steps:${c.reset}
|
|
60
|
+
|
|
61
|
+
1. Add to Claude Code:
|
|
62
|
+
${c.dim}claude mcp add code-graph-context code-graph-context${c.reset}
|
|
63
|
+
|
|
64
|
+
2. Configure in ${c.cyan}~/.config/claude/config.json${c.reset}:
|
|
65
|
+
|
|
66
|
+
${c.dim}{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"code-graph-context": {
|
|
69
|
+
"command": "code-graph-context",
|
|
70
|
+
"env": {
|
|
71
|
+
"OPENAI_API_KEY": "sk-..."${password !== NEO4J_CONFIG.defaultPassword
|
|
72
|
+
? `,
|
|
73
|
+
"NEO4J_PASSWORD": "${password}"`
|
|
74
|
+
: ''}${boltPort !== NEO4J_CONFIG.boltPort
|
|
75
|
+
? `,
|
|
76
|
+
"NEO4J_URI": "bolt://localhost:${boltPort}"`
|
|
77
|
+
: ''}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}${c.reset}
|
|
82
|
+
|
|
83
|
+
${c.yellow}Get your OpenAI API key:${c.reset} https://platform.openai.com/api-keys
|
|
84
|
+
|
|
85
|
+
3. Restart Claude Code
|
|
86
|
+
`);
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Init command - set up Neo4j
|
|
90
|
+
*/
|
|
91
|
+
const runInit = async (options) => {
|
|
92
|
+
const boltPort = options.port ? parseInt(options.port, 10) : NEO4J_CONFIG.boltPort;
|
|
93
|
+
const httpPort = options.httpPort ? parseInt(options.httpPort, 10) : NEO4J_CONFIG.httpPort;
|
|
94
|
+
const password = options.password || NEO4J_CONFIG.defaultPassword;
|
|
95
|
+
const memory = options.memory || '4G';
|
|
96
|
+
header('Code Graph Context Setup');
|
|
97
|
+
// Check Docker
|
|
98
|
+
if (!isDockerInstalled()) {
|
|
99
|
+
log(sym.err, 'Docker is not installed');
|
|
100
|
+
console.log(`\n Install Docker: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}\n`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
log(sym.ok, 'Docker installed');
|
|
104
|
+
if (!isDockerRunning()) {
|
|
105
|
+
log(sym.err, 'Docker daemon is not running');
|
|
106
|
+
console.log(`\n Start Docker Desktop or run: ${c.dim}sudo systemctl start docker${c.reset}\n`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
log(sym.ok, 'Docker daemon running');
|
|
110
|
+
// Handle existing container
|
|
111
|
+
const status = getContainerStatus();
|
|
112
|
+
if (status === 'running' && !options.force) {
|
|
113
|
+
log(sym.ok, 'Neo4j container already running');
|
|
114
|
+
const apocOk = isApocAvailable(NEO4J_CONFIG.containerName, password);
|
|
115
|
+
log(apocOk ? sym.ok : sym.warn, apocOk ? 'APOC plugin available' : 'APOC plugin not detected');
|
|
116
|
+
console.log(`\n ${c.dim}Use --force to recreate the container${c.reset}`);
|
|
117
|
+
printConfigInstructions(password, boltPort);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (status !== 'not-found' && options.force) {
|
|
121
|
+
const s = spinner('Removing existing container...');
|
|
122
|
+
stopContainer();
|
|
123
|
+
removeContainer();
|
|
124
|
+
s.stop(true, 'Removed existing container');
|
|
125
|
+
}
|
|
126
|
+
if (status === 'stopped' && !options.force) {
|
|
127
|
+
const s = spinner('Starting existing container...');
|
|
128
|
+
const started = startContainer();
|
|
129
|
+
if (!started) {
|
|
130
|
+
s.stop(false, 'Failed to start container');
|
|
131
|
+
console.log(`\n Try: ${c.dim}code-graph-context init --force${c.reset}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
s.stop(true, 'Container started');
|
|
135
|
+
}
|
|
136
|
+
else if (status === 'not-found' || options.force) {
|
|
137
|
+
const s = spinner('Creating Neo4j container...');
|
|
138
|
+
const created = createContainer({ httpPort, boltPort, password, memory });
|
|
139
|
+
if (!created) {
|
|
140
|
+
s.stop(false, 'Failed to create container');
|
|
141
|
+
console.log(`
|
|
142
|
+
Check if ports are in use:
|
|
143
|
+
${c.dim}lsof -i :${httpPort}${c.reset}
|
|
144
|
+
${c.dim}lsof -i :${boltPort}${c.reset}
|
|
145
|
+
`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
s.stop(true, 'Container created');
|
|
149
|
+
}
|
|
150
|
+
// Wait for Neo4j
|
|
151
|
+
const healthSpinner = spinner('Waiting for Neo4j to be ready (this may take a minute)...');
|
|
152
|
+
const ready = await waitForNeo4j(NEO4J_CONFIG.containerName, password);
|
|
153
|
+
healthSpinner.stop(ready, ready ? 'Neo4j is ready' : 'Neo4j failed to start');
|
|
154
|
+
if (!ready) {
|
|
155
|
+
console.log(`\n Check logs: ${c.dim}docker logs ${NEO4J_CONFIG.containerName}${c.reset}\n`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
// Check APOC
|
|
159
|
+
const apocOk = isApocAvailable(NEO4J_CONFIG.containerName, password);
|
|
160
|
+
log(apocOk ? sym.ok : sym.warn, apocOk ? 'APOC plugin verified' : 'APOC still loading (should be ready shortly)');
|
|
161
|
+
// Print connection info
|
|
162
|
+
console.log(`
|
|
163
|
+
${c.bold}Neo4j is ready${c.reset}
|
|
164
|
+
|
|
165
|
+
Browser: ${c.cyan}http://localhost:${httpPort}${c.reset}
|
|
166
|
+
Bolt URI: ${c.cyan}bolt://localhost:${boltPort}${c.reset}
|
|
167
|
+
Credentials: ${c.dim}neo4j / ${password}${c.reset}`);
|
|
168
|
+
printConfigInstructions(password, boltPort);
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Status command
|
|
172
|
+
*/
|
|
173
|
+
const runStatus = () => {
|
|
174
|
+
header('Code Graph Context Status');
|
|
175
|
+
const status = getFullStatus();
|
|
176
|
+
log(status.dockerInstalled ? sym.ok : sym.err, `Docker installed: ${status.dockerInstalled ? 'yes' : 'no'}`);
|
|
177
|
+
if (!status.dockerInstalled) {
|
|
178
|
+
console.log(`\n Install: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
log(status.dockerRunning ? sym.ok : sym.err, `Docker running: ${status.dockerRunning ? 'yes' : 'no'}`);
|
|
182
|
+
if (!status.dockerRunning) {
|
|
183
|
+
console.log(`\n Start Docker Desktop or: ${c.dim}sudo systemctl start docker${c.reset}\n`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const containerIcon = status.containerStatus === 'running' ? sym.ok : status.containerStatus === 'stopped' ? sym.warn : sym.err;
|
|
187
|
+
log(containerIcon, `Container: ${status.containerStatus}`);
|
|
188
|
+
if (status.containerStatus === 'running') {
|
|
189
|
+
log(status.neo4jReady ? sym.ok : sym.warn, `Neo4j responding: ${status.neo4jReady ? 'yes' : 'no'}`);
|
|
190
|
+
log(status.apocAvailable ? sym.ok : sym.warn, `APOC plugin: ${status.apocAvailable ? 'available' : 'not available'}`);
|
|
191
|
+
}
|
|
192
|
+
console.log('');
|
|
193
|
+
if (status.containerStatus !== 'running') {
|
|
194
|
+
console.log(` Run ${c.dim}code-graph-context init${c.reset} to start Neo4j\n`);
|
|
195
|
+
}
|
|
196
|
+
else if (!status.apocAvailable) {
|
|
197
|
+
console.log(` APOC may still be loading. Wait a moment and check again.\n`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Stop command
|
|
202
|
+
*/
|
|
203
|
+
const runStop = () => {
|
|
204
|
+
const status = getContainerStatus();
|
|
205
|
+
if (status === 'not-found') {
|
|
206
|
+
log(sym.info, 'No Neo4j container found');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (status === 'stopped') {
|
|
210
|
+
log(sym.info, 'Container already stopped');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const s = spinner('Stopping Neo4j...');
|
|
214
|
+
const stopped = stopContainer();
|
|
215
|
+
s.stop(stopped, stopped ? 'Neo4j stopped' : 'Failed to stop container');
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Start MCP server
|
|
219
|
+
*/
|
|
220
|
+
const startMcpServer = async () => {
|
|
221
|
+
// The MCP server is in a sibling directory after build
|
|
222
|
+
// cli/cli.js -> mcp/mcp.server.js
|
|
223
|
+
const mcpPath = join(__dirname, '..', 'mcp', 'mcp.server.js');
|
|
224
|
+
await import(mcpPath);
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Get package version
|
|
228
|
+
*/
|
|
229
|
+
const getVersion = () => {
|
|
230
|
+
try {
|
|
231
|
+
// Go up from dist/cli to root
|
|
232
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
233
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
234
|
+
return pkg.version;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return 'unknown';
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
// Build CLI
|
|
241
|
+
const program = new Command();
|
|
242
|
+
program.name('code-graph-context').description('MCP server for code graph analysis with Neo4j').version(getVersion());
|
|
243
|
+
program
|
|
244
|
+
.command('init')
|
|
245
|
+
.description('Set up Neo4j container and show configuration steps')
|
|
246
|
+
.option('-p, --port <port>', 'Neo4j Bolt port', '7687')
|
|
247
|
+
.option('--http-port <port>', 'Neo4j Browser port', '7474')
|
|
248
|
+
.option('--password <password>', 'Neo4j password', 'PASSWORD')
|
|
249
|
+
.option('-m, --memory <size>', 'Max heap memory (e.g., 2G, 4G)', '2G')
|
|
250
|
+
.option('-f, --force', 'Recreate container even if exists')
|
|
251
|
+
.action(runInit);
|
|
252
|
+
program.command('status').description('Check Neo4j and Docker status').action(runStatus);
|
|
253
|
+
program.command('stop').description('Stop the Neo4j container').action(runStop);
|
|
254
|
+
// Default action: start MCP server if no command given
|
|
255
|
+
const knownCommands = ['init', 'status', 'stop', 'help'];
|
|
256
|
+
const args = process.argv.slice(2);
|
|
257
|
+
const hasCommand = args.some((arg) => knownCommands.includes(arg) || arg.startsWith('-'));
|
|
258
|
+
if (args.length === 0 || !hasCommand) {
|
|
259
|
+
startMcpServer().catch((err) => {
|
|
260
|
+
console.error('Failed to start MCP server:', err);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
program.parse();
|
|
266
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j Docker Management
|
|
3
|
+
*
|
|
4
|
+
* Handles Docker container lifecycle for Neo4j
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
// Container configuration
|
|
8
|
+
export const NEO4J_CONFIG = {
|
|
9
|
+
containerName: 'code-graph-neo4j',
|
|
10
|
+
image: 'neo4j:5.23',
|
|
11
|
+
httpPort: 7474,
|
|
12
|
+
boltPort: 7687,
|
|
13
|
+
defaultPassword: 'PASSWORD',
|
|
14
|
+
defaultUser: 'neo4j',
|
|
15
|
+
healthCheckTimeoutMs: 120000,
|
|
16
|
+
healthCheckIntervalMs: 2000,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Execute a command and return stdout, or null if failed
|
|
20
|
+
*/
|
|
21
|
+
const exec = (command) => {
|
|
22
|
+
try {
|
|
23
|
+
return execSync(command, {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Check if Docker CLI is available
|
|
34
|
+
*/
|
|
35
|
+
export const isDockerInstalled = () => exec('docker --version') !== null;
|
|
36
|
+
/**
|
|
37
|
+
* Check if Docker daemon is running
|
|
38
|
+
*/
|
|
39
|
+
export const isDockerRunning = () => exec('docker info') !== null;
|
|
40
|
+
/**
|
|
41
|
+
* Get container status
|
|
42
|
+
*/
|
|
43
|
+
export const getContainerStatus = (containerName = NEO4J_CONFIG.containerName) => {
|
|
44
|
+
const result = exec(`docker inspect --format='{{.State.Running}}' ${containerName} 2>/dev/null`);
|
|
45
|
+
if (result === null)
|
|
46
|
+
return 'not-found';
|
|
47
|
+
return result === 'true' ? 'running' : 'stopped';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Start an existing stopped container
|
|
51
|
+
*/
|
|
52
|
+
export const startContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker start ${containerName}`) !== null;
|
|
53
|
+
/**
|
|
54
|
+
* Stop a running container
|
|
55
|
+
*/
|
|
56
|
+
export const stopContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker stop ${containerName}`) !== null;
|
|
57
|
+
/**
|
|
58
|
+
* Remove a container
|
|
59
|
+
*/
|
|
60
|
+
export const removeContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker rm ${containerName}`) !== null;
|
|
61
|
+
/**
|
|
62
|
+
* Create and start a new Neo4j container
|
|
63
|
+
*/
|
|
64
|
+
export const createContainer = (options = {}) => {
|
|
65
|
+
const { containerName = NEO4J_CONFIG.containerName, httpPort = NEO4J_CONFIG.httpPort, boltPort = NEO4J_CONFIG.boltPort, password = NEO4J_CONFIG.defaultPassword, memory = '2G', } = options;
|
|
66
|
+
const cmd = [
|
|
67
|
+
'docker run -d',
|
|
68
|
+
`--name ${containerName}`,
|
|
69
|
+
`--restart unless-stopped`,
|
|
70
|
+
`-p ${httpPort}:7474`,
|
|
71
|
+
`-p ${boltPort}:7687`,
|
|
72
|
+
`-e NEO4J_AUTH=neo4j/${password}`,
|
|
73
|
+
`-e 'NEO4J_PLUGINS=["apoc"]'`,
|
|
74
|
+
`-e NEO4J_dbms_security_procedures_unrestricted=apoc.*`,
|
|
75
|
+
`-e NEO4J_server_memory_heap_initial__size=1G`,
|
|
76
|
+
`-e NEO4J_server_memory_heap_max__size=${memory}`,
|
|
77
|
+
`-e NEO4J_server_memory_pagecache_size=2G`,
|
|
78
|
+
NEO4J_CONFIG.image,
|
|
79
|
+
].join(' ');
|
|
80
|
+
return exec(cmd) !== null;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Check if Neo4j is accepting connections
|
|
84
|
+
*/
|
|
85
|
+
export const isNeo4jReady = (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword) => {
|
|
86
|
+
const result = exec(`docker exec ${containerName} cypher-shell -u neo4j -p ${password} "RETURN 1" 2>/dev/null`);
|
|
87
|
+
return result !== null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Check if APOC plugin is available
|
|
91
|
+
*/
|
|
92
|
+
export const isApocAvailable = (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword) => {
|
|
93
|
+
const result = exec(`docker exec ${containerName} cypher-shell -u neo4j -p ${password} "CALL apoc.help('apoc') YIELD name RETURN count(name)" 2>/dev/null`);
|
|
94
|
+
return result !== null && !result.includes('error');
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Wait for Neo4j to be ready
|
|
98
|
+
*/
|
|
99
|
+
export const waitForNeo4j = async (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword, timeoutMs = NEO4J_CONFIG.healthCheckTimeoutMs) => {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
102
|
+
if (isNeo4jReady(containerName, password)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, NEO4J_CONFIG.healthCheckIntervalMs));
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Ensure Neo4j is running - start if needed
|
|
111
|
+
*/
|
|
112
|
+
export const ensureNeo4jRunning = async (options = {}) => {
|
|
113
|
+
const containerName = options.containerName ?? NEO4J_CONFIG.containerName;
|
|
114
|
+
const status = getContainerStatus(containerName);
|
|
115
|
+
if (status === 'running') {
|
|
116
|
+
return { success: true, action: 'already-running' };
|
|
117
|
+
}
|
|
118
|
+
if (!isDockerInstalled()) {
|
|
119
|
+
return { success: false, action: 'failed', error: 'Docker not installed' };
|
|
120
|
+
}
|
|
121
|
+
if (!isDockerRunning()) {
|
|
122
|
+
return { success: false, action: 'failed', error: 'Docker daemon not running' };
|
|
123
|
+
}
|
|
124
|
+
// Start existing container
|
|
125
|
+
if (status === 'stopped') {
|
|
126
|
+
if (startContainer(containerName)) {
|
|
127
|
+
const ready = await waitForNeo4j(containerName, options.password);
|
|
128
|
+
return ready
|
|
129
|
+
? { success: true, action: 'started' }
|
|
130
|
+
: { success: false, action: 'failed', error: 'Container started but Neo4j not responding' };
|
|
131
|
+
}
|
|
132
|
+
return { success: false, action: 'failed', error: 'Failed to start existing container' };
|
|
133
|
+
}
|
|
134
|
+
// Create new container
|
|
135
|
+
if (createContainer(options)) {
|
|
136
|
+
const ready = await waitForNeo4j(containerName, options.password);
|
|
137
|
+
return ready
|
|
138
|
+
? { success: true, action: 'created' }
|
|
139
|
+
: { success: false, action: 'failed', error: 'Container created but Neo4j not responding' };
|
|
140
|
+
}
|
|
141
|
+
return { success: false, action: 'failed', error: 'Failed to create container' };
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Get full status for diagnostics
|
|
145
|
+
*/
|
|
146
|
+
export const getFullStatus = () => {
|
|
147
|
+
const dockerInstalled = isDockerInstalled();
|
|
148
|
+
const dockerRunning = dockerInstalled && isDockerRunning();
|
|
149
|
+
const containerStatus = dockerRunning ? getContainerStatus() : 'not-found';
|
|
150
|
+
const neo4jReady = containerStatus === 'running' && isNeo4jReady();
|
|
151
|
+
const apocAvailable = neo4jReady && isApocAvailable();
|
|
152
|
+
return {
|
|
153
|
+
dockerInstalled,
|
|
154
|
+
dockerRunning,
|
|
155
|
+
containerStatus,
|
|
156
|
+
neo4jReady,
|
|
157
|
+
apocAvailable,
|
|
158
|
+
};
|
|
159
|
+
};
|