@duckmind/deepquark-darwin-arm64 0.9.83 → 0.9.90
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/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
- package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
- package/bin/deepquark +0 -0
- package/package.json +1 -1
- package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
- package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
- package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
- package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
- package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
- package/.deepquark/skills/bundled/ge-payroll/references/timeline-mandays.md +0 -64
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container Runtime Abstraction Layer
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for Podman and Docker container operations.
|
|
5
|
+
* Automatically detects available runtime and handles compatibility differences.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY HARDENING:
|
|
8
|
+
* - All container names validated against strict regex to prevent injection
|
|
9
|
+
* - Command arguments properly escaped before execution
|
|
10
|
+
* - Network names and volume names also validated
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
// Using Bun.spawn and Bun.spawnSync for container operations
|
|
16
|
+
|
|
17
|
+
// Container runtime types
|
|
18
|
+
export type ContainerRuntime = 'podman' | 'docker' | 'none';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Security: Strict pattern for container names, network names, volume names
|
|
22
|
+
* Prevents command injection via malicious names
|
|
23
|
+
*
|
|
24
|
+
* Pattern explanation:
|
|
25
|
+
* - ^[a-zA-Z0-9] - Start with alphanumeric
|
|
26
|
+
* - [a-zA-Z0-9_-]{0,63} - Up to 64 chars total, allowing alphanumeric, underscore, hyphen
|
|
27
|
+
* - $ - End of string (no trailing special chars)
|
|
28
|
+
*
|
|
29
|
+
* Examples of VALID names: "my-container", "Container123", "my_container"
|
|
30
|
+
* Examples of INVALID names: "my container", "my;container", "my-container && malicious", "../../etc/passwd"
|
|
31
|
+
*/
|
|
32
|
+
const CONTAINER_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
33
|
+
|
|
34
|
+
// Container status
|
|
35
|
+
export type ContainerStatus = 'running' | 'stopped' | 'not-found';
|
|
36
|
+
|
|
37
|
+
// Container info
|
|
38
|
+
export interface ContainerInfo {
|
|
39
|
+
name: string;
|
|
40
|
+
status: ContainerStatus;
|
|
41
|
+
exists: boolean;
|
|
42
|
+
ports: string | undefined;
|
|
43
|
+
uptime: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Network info
|
|
47
|
+
export interface NetworkInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
exists: boolean;
|
|
50
|
+
subnet: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Execution options
|
|
54
|
+
export interface ExecOptions {
|
|
55
|
+
silent?: boolean;
|
|
56
|
+
timeout?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Command result
|
|
60
|
+
export interface CommandResult {
|
|
61
|
+
success: boolean;
|
|
62
|
+
stdout: string;
|
|
63
|
+
stderr: string;
|
|
64
|
+
exitCode: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Database backend type
|
|
69
|
+
*/
|
|
70
|
+
export type DatabaseBackend = 'falkordb' | 'neo4j';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Security error for invalid container names
|
|
74
|
+
*/
|
|
75
|
+
export class ContainerValidationError extends Error {
|
|
76
|
+
constructor(message: string) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = 'ContainerValidationError';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a container/network/volume name to prevent injection attacks
|
|
84
|
+
*
|
|
85
|
+
* @param name - The name to validate
|
|
86
|
+
* @param context - Description of what is being validated (for error messages)
|
|
87
|
+
* @throws ContainerValidationError if name contains invalid characters
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* validateName('my-container', 'container') // OK
|
|
91
|
+
* validateName('my;container', 'container') // throws
|
|
92
|
+
* validateName('../../etc/passwd', 'volume') // throws
|
|
93
|
+
*/
|
|
94
|
+
export function validateName(name: string, context = 'resource'): void {
|
|
95
|
+
if (!name || typeof name !== 'string') {
|
|
96
|
+
throw new ContainerValidationError(`Invalid ${context} name: must be a non-empty string`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!CONTAINER_NAME_PATTERN.test(name)) {
|
|
100
|
+
throw new ContainerValidationError(
|
|
101
|
+
`Invalid ${context} name "${name}": must contain only alphanumeric characters, hyphens, and underscores, and must start with alphanumeric (max 64 characters)`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Container Manager class
|
|
108
|
+
*/
|
|
109
|
+
export class ContainerManager {
|
|
110
|
+
private runtime: ContainerRuntime;
|
|
111
|
+
private runtimeCommand: string;
|
|
112
|
+
|
|
113
|
+
// Default container names - FalkorDB backend
|
|
114
|
+
static readonly FALKORDB_CONTAINER = 'madeinoz-knowledge-falkordb';
|
|
115
|
+
static readonly MCP_CONTAINER = 'madeinoz-knowledge-graph-mcp';
|
|
116
|
+
static readonly NETWORK_NAME = 'madeinoz-knowledge-net';
|
|
117
|
+
static readonly VOLUME_NAME = 'madeinoz-knowledge-falkordb-data';
|
|
118
|
+
|
|
119
|
+
// Neo4j backend container names
|
|
120
|
+
static readonly NEO4J_CONTAINER = 'madeinoz-knowledge-neo4j';
|
|
121
|
+
static readonly NEO4J_VOLUME_DATA = 'madeinoz-knowledge-neo4j-data';
|
|
122
|
+
static readonly NEO4J_VOLUME_LOGS = 'madeinoz-knowledge-neo4j-logs';
|
|
123
|
+
|
|
124
|
+
// Container images per backend
|
|
125
|
+
static readonly IMAGES = {
|
|
126
|
+
falkordb: {
|
|
127
|
+
database: 'falkordb/falkordb:latest',
|
|
128
|
+
mcp: 'madeinoz-knowledge-system:fixed', // Custom image with patches
|
|
129
|
+
},
|
|
130
|
+
neo4j: {
|
|
131
|
+
database: 'neo4j:5.26.0',
|
|
132
|
+
mcp: 'madeinoz-knowledge-system:fixed', // Custom image with patches
|
|
133
|
+
},
|
|
134
|
+
} as const;
|
|
135
|
+
|
|
136
|
+
// Port mappings per backend
|
|
137
|
+
static readonly PORTS = {
|
|
138
|
+
falkordb: {
|
|
139
|
+
database: ['3000:3000'], // FalkorDB web UI
|
|
140
|
+
mcp: ['8000:8000'], // MCP HTTP endpoint
|
|
141
|
+
},
|
|
142
|
+
neo4j: {
|
|
143
|
+
database: ['7474:7474', '7687:7687'], // Neo4j Browser + Bolt
|
|
144
|
+
mcp: ['8000:8000'], // MCP HTTP endpoint
|
|
145
|
+
},
|
|
146
|
+
} as const;
|
|
147
|
+
|
|
148
|
+
constructor(runtime?: ContainerRuntime) {
|
|
149
|
+
if (runtime) {
|
|
150
|
+
this.runtime = runtime;
|
|
151
|
+
this.runtimeCommand = runtime;
|
|
152
|
+
} else {
|
|
153
|
+
const detected = this.detectRuntime();
|
|
154
|
+
this.runtime = detected;
|
|
155
|
+
this.runtimeCommand = detected;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect available container runtime (synchronous)
|
|
161
|
+
*/
|
|
162
|
+
detectRuntime(): ContainerRuntime {
|
|
163
|
+
// Use Bun.spawnSync for synchronous detection
|
|
164
|
+
try {
|
|
165
|
+
const podmanCheck = Bun.spawnSync(['which', 'podman']);
|
|
166
|
+
if (podmanCheck.exitCode === 0) {
|
|
167
|
+
return 'podman';
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Podman not found, try Docker
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const dockerCheck = Bun.spawnSync(['which', 'docker']);
|
|
175
|
+
if (dockerCheck.exitCode === 0) {
|
|
176
|
+
return 'docker';
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Docker not found
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return 'none';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the detected runtime
|
|
187
|
+
*/
|
|
188
|
+
getRuntime(): ContainerRuntime {
|
|
189
|
+
return this.runtime;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the runtime command for display purposes
|
|
194
|
+
*/
|
|
195
|
+
getRuntimeCommand(): string {
|
|
196
|
+
return this.runtimeCommand;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if runtime is available
|
|
201
|
+
*/
|
|
202
|
+
isRuntimeAvailable(): boolean {
|
|
203
|
+
return this.runtime !== 'none';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Execute a container command
|
|
208
|
+
*/
|
|
209
|
+
async exec(args: string[], _options: ExecOptions = {}): Promise<CommandResult> {
|
|
210
|
+
if (!this.isRuntimeAvailable()) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
stdout: '',
|
|
214
|
+
stderr: 'No container runtime found',
|
|
215
|
+
exitCode: 1,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Use Bun.spawn for proper async execution
|
|
221
|
+
const proc = Bun.spawn([this.runtimeCommand, ...args], {
|
|
222
|
+
stdout: 'pipe',
|
|
223
|
+
stderr: 'pipe',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const stdout = await new Response(proc.stdout).text();
|
|
227
|
+
const stderr = await new Response(proc.stderr).text();
|
|
228
|
+
const exitCode = await proc.exited;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: exitCode === 0,
|
|
232
|
+
stdout: stdout.trim(),
|
|
233
|
+
stderr: stderr.trim(),
|
|
234
|
+
exitCode,
|
|
235
|
+
};
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
stdout: '',
|
|
240
|
+
stderr: error?.message || 'Unknown error',
|
|
241
|
+
exitCode: error?.exitCode || 1,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if a network exists
|
|
248
|
+
*/
|
|
249
|
+
async networkExists(networkName: string): Promise<boolean> {
|
|
250
|
+
validateName(networkName, 'network');
|
|
251
|
+
const result = await this.exec(['network', 'inspect', networkName], {
|
|
252
|
+
silent: true,
|
|
253
|
+
});
|
|
254
|
+
return result.success;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a network
|
|
259
|
+
*/
|
|
260
|
+
async createNetwork(networkName: string, subnet?: string): Promise<CommandResult> {
|
|
261
|
+
validateName(networkName, 'network');
|
|
262
|
+
const args = ['network', 'create', '--driver', 'bridge'];
|
|
263
|
+
|
|
264
|
+
if (subnet) {
|
|
265
|
+
args.push('--subnet', subnet);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
args.push(networkName);
|
|
269
|
+
|
|
270
|
+
return await this.exec(args);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get network information
|
|
275
|
+
*/
|
|
276
|
+
async getNetworkInfo(networkName: string): Promise<NetworkInfo> {
|
|
277
|
+
validateName(networkName, 'network');
|
|
278
|
+
const exists = await this.networkExists(networkName);
|
|
279
|
+
|
|
280
|
+
if (!exists) {
|
|
281
|
+
return { name: networkName, exists: false, subnet: undefined };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Try to get subnet info
|
|
285
|
+
const result = await this.exec(
|
|
286
|
+
['network', 'inspect', '--format', '{{range .IPAM.Config}}{{.Subnet}}{{end}}', networkName],
|
|
287
|
+
{ silent: true }
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
name: networkName,
|
|
292
|
+
exists: true,
|
|
293
|
+
subnet: result.success ? result.stdout : undefined,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if a container exists
|
|
299
|
+
*/
|
|
300
|
+
async containerExists(containerName: string): Promise<boolean> {
|
|
301
|
+
validateName(containerName, 'container');
|
|
302
|
+
const result = await this.exec(['ps', '-a', '--format', '{{.Names}}'], {
|
|
303
|
+
silent: true,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!result.success) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const containers = result.stdout.split('\n').filter((line) => line.trim());
|
|
311
|
+
return containers.includes(containerName);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a container is running
|
|
316
|
+
*/
|
|
317
|
+
async isContainerRunning(containerName: string): Promise<boolean> {
|
|
318
|
+
validateName(containerName, 'container');
|
|
319
|
+
const result = await this.exec(['ps', '--format', '{{.Names}}'], {
|
|
320
|
+
silent: true,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const runningContainers = result.stdout.split('\n').filter((line) => line.trim());
|
|
328
|
+
return runningContainers.includes(containerName);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get container information
|
|
333
|
+
*/
|
|
334
|
+
async getContainerInfo(containerName: string): Promise<ContainerInfo> {
|
|
335
|
+
validateName(containerName, 'container');
|
|
336
|
+
const exists = await this.containerExists(containerName);
|
|
337
|
+
|
|
338
|
+
if (!exists) {
|
|
339
|
+
return {
|
|
340
|
+
name: containerName,
|
|
341
|
+
status: 'not-found',
|
|
342
|
+
exists: false,
|
|
343
|
+
ports: undefined,
|
|
344
|
+
uptime: undefined,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const isRunning = await this.isContainerRunning(containerName);
|
|
349
|
+
|
|
350
|
+
// Get detailed status
|
|
351
|
+
const statusResult = await this.exec(
|
|
352
|
+
['ps', '-a', '--filter', `name=${containerName}`, '--format', '{{.Status}}'],
|
|
353
|
+
{ silent: true }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Get port mappings
|
|
357
|
+
const portsResult = await this.exec(
|
|
358
|
+
['ps', '--filter', `name=${containerName}`, '--format', '{{.Ports}}'],
|
|
359
|
+
{ silent: true }
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
name: containerName,
|
|
364
|
+
status: isRunning ? 'running' : 'stopped',
|
|
365
|
+
exists: true,
|
|
366
|
+
ports: portsResult.success ? portsResult.stdout : undefined,
|
|
367
|
+
uptime: statusResult.success ? statusResult.stdout : undefined,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Start a container
|
|
373
|
+
*/
|
|
374
|
+
async startContainer(containerName: string): Promise<CommandResult> {
|
|
375
|
+
validateName(containerName, 'container');
|
|
376
|
+
return await this.exec(['start', containerName]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Stop a container
|
|
381
|
+
*/
|
|
382
|
+
async stopContainer(containerName: string): Promise<CommandResult> {
|
|
383
|
+
validateName(containerName, 'container');
|
|
384
|
+
return await this.exec(['stop', containerName]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Restart a container
|
|
389
|
+
*/
|
|
390
|
+
async restartContainer(containerName: string): Promise<CommandResult> {
|
|
391
|
+
validateName(containerName, 'container');
|
|
392
|
+
return await this.exec(['restart', containerName]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Remove a container
|
|
397
|
+
*/
|
|
398
|
+
async removeContainer(containerName: string): Promise<CommandResult> {
|
|
399
|
+
validateName(containerName, 'container');
|
|
400
|
+
return await this.exec(['rm', containerName]);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Stop and remove a container
|
|
405
|
+
*/
|
|
406
|
+
async stopAndRemoveContainer(containerName: string): Promise<CommandResult> {
|
|
407
|
+
validateName(containerName, 'container');
|
|
408
|
+
await this.stopContainer(containerName);
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
410
|
+
return await this.removeContainer(containerName);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Run a new container
|
|
415
|
+
*/
|
|
416
|
+
async runContainer(args: string[]): Promise<CommandResult> {
|
|
417
|
+
return await this.exec(['run', '-d', ...args]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get container logs
|
|
422
|
+
*/
|
|
423
|
+
async getLogs(containerName: string, follow = false): Promise<CommandResult> {
|
|
424
|
+
validateName(containerName, 'container');
|
|
425
|
+
const args = ['logs'];
|
|
426
|
+
if (follow) {
|
|
427
|
+
args.push('-f');
|
|
428
|
+
}
|
|
429
|
+
args.push(containerName);
|
|
430
|
+
|
|
431
|
+
// Note: For follow=true, this will hang until interrupted
|
|
432
|
+
// Caller should handle streaming appropriately
|
|
433
|
+
return await this.exec(args);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get container stats (resource usage)
|
|
438
|
+
*/
|
|
439
|
+
async getStats(containerName: string): Promise<CommandResult> {
|
|
440
|
+
validateName(containerName, 'container');
|
|
441
|
+
return await this.exec(
|
|
442
|
+
[
|
|
443
|
+
'stats',
|
|
444
|
+
containerName,
|
|
445
|
+
'--no-stream',
|
|
446
|
+
'--format',
|
|
447
|
+
'table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}',
|
|
448
|
+
],
|
|
449
|
+
{ silent: true }
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Create a volume
|
|
455
|
+
*/
|
|
456
|
+
async createVolume(volumeName: string): Promise<CommandResult> {
|
|
457
|
+
validateName(volumeName, 'volume');
|
|
458
|
+
return await this.exec(['volume', 'create', volumeName]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Check if a volume exists
|
|
463
|
+
*/
|
|
464
|
+
async volumeExists(volumeName: string): Promise<boolean> {
|
|
465
|
+
validateName(volumeName, 'volume');
|
|
466
|
+
const result = await this.exec(['volume', 'inspect', volumeName], {
|
|
467
|
+
silent: true,
|
|
468
|
+
});
|
|
469
|
+
return result.success;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* List all containers (including stopped)
|
|
474
|
+
*/
|
|
475
|
+
async listContainers(all = true): Promise<CommandResult> {
|
|
476
|
+
return await this.exec(['ps', all ? '-a' : '', '--format', '{{.Names}}'], {
|
|
477
|
+
silent: true,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Export a container to a tar file
|
|
483
|
+
*/
|
|
484
|
+
async exportContainer(containerName: string, outputPath: string): Promise<CommandResult> {
|
|
485
|
+
validateName(containerName, 'container');
|
|
486
|
+
// outputPath is not a container name, but we should still validate it's safe
|
|
487
|
+
// Only allow absolute paths or paths relative to current directory, no parent traversal
|
|
488
|
+
if (outputPath.includes('..')) {
|
|
489
|
+
throw new ContainerValidationError(
|
|
490
|
+
`Invalid export path "${outputPath}": parent directory traversal not allowed`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return await this.exec(['export', containerName, '-o', outputPath]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Parse container name from various formats
|
|
498
|
+
* SECURITY: Also validates the normalized name
|
|
499
|
+
*/
|
|
500
|
+
static normalizeContainerName(name: string): string {
|
|
501
|
+
const normalized = name.replace(/^[\/*]/, '').replace(/\/$/, '');
|
|
502
|
+
// Validate the normalized name before returning
|
|
503
|
+
try {
|
|
504
|
+
validateName(normalized, 'container');
|
|
505
|
+
return normalized;
|
|
506
|
+
} catch {
|
|
507
|
+
throw new ContainerValidationError(
|
|
508
|
+
`Invalid container name "${name}": after normalization, "${normalized}" is not a valid container name`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create a container manager instance with auto-detected runtime
|
|
516
|
+
*/
|
|
517
|
+
export function createContainerManager(): ContainerManager {
|
|
518
|
+
return new ContainerManager();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Create a container manager instance with specific runtime
|
|
523
|
+
*/
|
|
524
|
+
export function createContainerManagerWithRuntime(runtime: ContainerRuntime): ContainerManager {
|
|
525
|
+
if (runtime === 'none') {
|
|
526
|
+
throw new Error("Cannot create container manager with runtime 'none'");
|
|
527
|
+
}
|
|
528
|
+
return new ContainerManager(runtime);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Docker Compose Manager class
|
|
533
|
+
*
|
|
534
|
+
* Provides docker-compose orchestration for Madeinoz Knowledge System.
|
|
535
|
+
* This is the preferred way to manage containers (vs raw docker commands).
|
|
536
|
+
*/
|
|
537
|
+
export class ComposeManager {
|
|
538
|
+
private containerManager: ContainerManager;
|
|
539
|
+
private serverDir: string;
|
|
540
|
+
|
|
541
|
+
// Compose file paths relative to server directory
|
|
542
|
+
static readonly COMPOSE_FILES = {
|
|
543
|
+
neo4j: 'docker-compose-neo4j.yml',
|
|
544
|
+
falkordb: 'docker-compose-falkordb.yml',
|
|
545
|
+
neo4jDev: 'docker-compose-neo4j-dev.yml',
|
|
546
|
+
falkordbDev: 'docker-compose-falkordb-dev.yml',
|
|
547
|
+
} as const;
|
|
548
|
+
|
|
549
|
+
// Generated env file paths
|
|
550
|
+
static readonly ENV_FILES = {
|
|
551
|
+
neo4j: '/tmp/madeinoz-knowledge-neo4j.env',
|
|
552
|
+
mcp: '/tmp/madeinoz-knowledge-mcp.env',
|
|
553
|
+
neo4jDev: '/tmp/madeinoz-knowledge-neo4j-dev.env',
|
|
554
|
+
mcpDev: '/tmp/madeinoz-knowledge-mcp-dev.env',
|
|
555
|
+
} as const;
|
|
556
|
+
|
|
557
|
+
constructor(serverDir?: string) {
|
|
558
|
+
this.containerManager = createContainerManager();
|
|
559
|
+
// Default to src/server directory
|
|
560
|
+
this.serverDir = serverDir || join(import.meta.dir, '..');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get the container runtime command
|
|
565
|
+
*/
|
|
566
|
+
getRuntimeCommand(): string {
|
|
567
|
+
return this.containerManager.getRuntimeCommand();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Check if runtime is available
|
|
572
|
+
*/
|
|
573
|
+
isRuntimeAvailable(): boolean {
|
|
574
|
+
return this.containerManager.isRuntimeAvailable();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Get the compose file path for a database type
|
|
579
|
+
*/
|
|
580
|
+
getComposeFilePath(databaseType: DatabaseBackend, devMode = false): string {
|
|
581
|
+
if (devMode) {
|
|
582
|
+
const devKey = `${databaseType}Dev` as keyof typeof ComposeManager.COMPOSE_FILES;
|
|
583
|
+
return join(this.serverDir, ComposeManager.COMPOSE_FILES[devKey]);
|
|
584
|
+
}
|
|
585
|
+
return join(this.serverDir, ComposeManager.COMPOSE_FILES[databaseType]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get env file paths for a mode
|
|
590
|
+
*/
|
|
591
|
+
getEnvFilePaths(devMode = false): { neo4j: string; mcp: string } {
|
|
592
|
+
if (devMode) {
|
|
593
|
+
return {
|
|
594
|
+
neo4j: ComposeManager.ENV_FILES.neo4jDev,
|
|
595
|
+
mcp: ComposeManager.ENV_FILES.mcpDev,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
neo4j: ComposeManager.ENV_FILES.neo4j,
|
|
600
|
+
mcp: ComposeManager.ENV_FILES.mcp,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Execute a compose command
|
|
606
|
+
*/
|
|
607
|
+
async execCompose(composeFile: string, args: string[], envFile?: string): Promise<CommandResult> {
|
|
608
|
+
if (!this.containerManager.isRuntimeAvailable()) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
stdout: '',
|
|
612
|
+
stderr: 'No container runtime found',
|
|
613
|
+
exitCode: 1,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const runtime = this.containerManager.getRuntimeCommand();
|
|
618
|
+
const cmdArgs = ['compose', '-f', composeFile];
|
|
619
|
+
|
|
620
|
+
if (envFile) {
|
|
621
|
+
cmdArgs.push('--env-file', envFile);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
cmdArgs.push(...args);
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
const proc = Bun.spawn([runtime, ...cmdArgs], {
|
|
628
|
+
stdout: 'pipe',
|
|
629
|
+
stderr: 'pipe',
|
|
630
|
+
cwd: this.serverDir,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const stdout = await new Response(proc.stdout).text();
|
|
634
|
+
const stderr = await new Response(proc.stderr).text();
|
|
635
|
+
const exitCode = await proc.exited;
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
success: exitCode === 0,
|
|
639
|
+
stdout: stdout.trim(),
|
|
640
|
+
stderr: stderr.trim(),
|
|
641
|
+
exitCode,
|
|
642
|
+
};
|
|
643
|
+
} catch (error: any) {
|
|
644
|
+
return {
|
|
645
|
+
success: false,
|
|
646
|
+
stdout: '',
|
|
647
|
+
stderr: error?.message || 'Unknown error',
|
|
648
|
+
exitCode: error?.exitCode || 1,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Start containers using docker-compose up
|
|
655
|
+
*/
|
|
656
|
+
async up(
|
|
657
|
+
databaseType: DatabaseBackend,
|
|
658
|
+
devMode = false,
|
|
659
|
+
extraArgs: string[] = []
|
|
660
|
+
): Promise<CommandResult> {
|
|
661
|
+
const composeFile = this.getComposeFilePath(databaseType, devMode);
|
|
662
|
+
const envFiles = this.getEnvFilePaths(devMode);
|
|
663
|
+
|
|
664
|
+
return await this.execCompose(composeFile, ['up', '-d', ...extraArgs], envFiles.neo4j);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Stop containers using docker-compose down
|
|
669
|
+
* NOTE: Never uses -v flag to preserve data volumes
|
|
670
|
+
*/
|
|
671
|
+
async down(databaseType: DatabaseBackend, devMode = false): Promise<CommandResult> {
|
|
672
|
+
const composeFile = this.getComposeFilePath(databaseType, devMode);
|
|
673
|
+
const envFiles = this.getEnvFilePaths(devMode);
|
|
674
|
+
|
|
675
|
+
// CRITICAL: Never use -v flag - this destroys data volumes!
|
|
676
|
+
return await this.execCompose(composeFile, ['down'], envFiles.neo4j);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get container status using docker-compose ps
|
|
681
|
+
*/
|
|
682
|
+
async ps(databaseType: DatabaseBackend, devMode = false): Promise<CommandResult> {
|
|
683
|
+
const composeFile = this.getComposeFilePath(databaseType, devMode);
|
|
684
|
+
const envFiles = this.getEnvFilePaths(devMode);
|
|
685
|
+
|
|
686
|
+
// Don't use --format flag as it's not supported by all compose versions
|
|
687
|
+
return await this.execCompose(composeFile, ['ps'], envFiles.neo4j);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Get container logs using docker-compose logs
|
|
692
|
+
*/
|
|
693
|
+
async logs(
|
|
694
|
+
databaseType: DatabaseBackend,
|
|
695
|
+
service?: string,
|
|
696
|
+
follow = false,
|
|
697
|
+
devMode = false,
|
|
698
|
+
tail?: number
|
|
699
|
+
): Promise<CommandResult> {
|
|
700
|
+
const composeFile = this.getComposeFilePath(databaseType, devMode);
|
|
701
|
+
const envFiles = this.getEnvFilePaths(devMode);
|
|
702
|
+
|
|
703
|
+
const args = ['logs'];
|
|
704
|
+
if (follow) {
|
|
705
|
+
args.push('-f');
|
|
706
|
+
}
|
|
707
|
+
if (tail) {
|
|
708
|
+
args.push('--tail', tail.toString());
|
|
709
|
+
}
|
|
710
|
+
if (service) {
|
|
711
|
+
args.push(service);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return await this.execCompose(composeFile, args, envFiles.neo4j);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Restart containers using docker-compose restart
|
|
719
|
+
*/
|
|
720
|
+
async restart(
|
|
721
|
+
databaseType: DatabaseBackend,
|
|
722
|
+
service?: string,
|
|
723
|
+
devMode = false
|
|
724
|
+
): Promise<CommandResult> {
|
|
725
|
+
const composeFile = this.getComposeFilePath(databaseType, devMode);
|
|
726
|
+
const envFiles = this.getEnvFilePaths(devMode);
|
|
727
|
+
|
|
728
|
+
const args = ['restart'];
|
|
729
|
+
if (service) {
|
|
730
|
+
args.push(service);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return await this.execCompose(composeFile, args, envFiles.neo4j);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Check if compose services are running
|
|
738
|
+
*/
|
|
739
|
+
async isRunning(databaseType: DatabaseBackend, devMode = false): Promise<boolean> {
|
|
740
|
+
const result = await this.ps(databaseType, devMode);
|
|
741
|
+
if (!result.success) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
// Check if any services are running (not just headers)
|
|
745
|
+
const lines = result.stdout.split('\n').filter((line) => line.trim());
|
|
746
|
+
// More than just the header line means services exist
|
|
747
|
+
return lines.length > 1 && result.stdout.includes('running');
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Create a compose manager instance
|
|
753
|
+
*/
|
|
754
|
+
export function createComposeManager(serverDir?: string): ComposeManager {
|
|
755
|
+
return new ComposeManager(serverDir);
|
|
756
|
+
}
|