@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.
Files changed (70) hide show
  1. package/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
  2. package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
  3. package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
  4. package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
  5. package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
  6. package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
  7. package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
  8. package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
  9. package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
  10. package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
  11. package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
  12. package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
  13. package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
  14. package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
  15. package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
  16. package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
  17. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
  18. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
  19. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
  20. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
  21. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
  22. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
  23. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
  24. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
  25. package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
  26. package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
  27. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
  28. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
  29. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
  30. package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
  31. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
  32. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
  33. package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
  34. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
  35. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
  36. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
  37. package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
  38. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
  39. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
  40. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
  41. package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
  42. package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
  43. package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
  44. package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
  45. package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
  46. package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
  47. package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
  48. package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
  49. package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
  50. package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
  51. package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
  52. package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
  53. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
  54. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
  55. package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
  56. package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
  57. package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
  58. package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
  59. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
  60. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
  61. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
  62. package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
  63. package/bin/deepquark +0 -0
  64. package/package.json +1 -1
  65. package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
  66. package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
  67. package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
  68. package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
  69. package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
  70. 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
+ }