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 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: Development Install (From Source)
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
- git clone https://github.com/drewdrewH/code-graph-context.git
74
- cd code-graph-context
75
- ```
71
+ # Install globally
72
+ npm install -g code-graph-context
76
73
 
77
- 2. **Install dependencies:**
78
- ```bash
79
- npm install
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
- 3. **Set up Neo4j using Docker:**
83
- ```bash
84
- docker-compose up -d
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
- This will start Neo4j with:
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
- cp .env.example .env
95
- # Edit .env with your configuration
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
- 5. **Build the project:**
111
+ ### CLI Commands
112
+
113
+ The package includes a CLI for managing Neo4j:
114
+
99
115
  ```bash
100
- npm run build
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
- 6. **Add to Claude Code:**
104
- ```bash
105
- claude mcp add code-graph-context node /absolute/path/to/code-graph-context/dist/mcp/mcp.server.js
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
- #### Option 2: NPM Install (Global Package)
130
+ ### Alternative Neo4j Setup
109
131
 
110
- Best for: Easy setup and automatic updates
132
+ If you prefer not to use the CLI, you can set up Neo4j manually:
111
133
 
112
- 1. **Install the package globally:**
134
+ **Docker Compose:**
113
135
  ```bash
114
- npm install -g code-graph-context
136
+ docker-compose up -d
115
137
  ```
116
138
 
117
- 2. **Set up Neo4j** (choose one):
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='["apoc"]' \
145
+ -e 'NEO4J_PLUGINS=["apoc"]' \
126
146
  neo4j:5.23
127
147
  ```
128
148
 
129
- **Option B: Neo4j Desktop**
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
- **Option C: Neo4j Aura (Cloud)**
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
@@ -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
+ };