moflo 4.8.0 → 4.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.claude/workflow-state.json +3 -7
  3. package/README.md +3 -1
  4. package/bin/build-embeddings.mjs +59 -3
  5. package/bin/generate-code-map.mjs +3 -1
  6. package/bin/index-guidance.mjs +3 -1
  7. package/bin/lib/moflo-resolve.mjs +14 -0
  8. package/bin/semantic-search.mjs +10 -5
  9. package/bin/session-start-launcher.mjs +28 -0
  10. package/package.json +6 -6
  11. package/src/@claude-flow/cli/dist/src/appliance/ruvllm-bridge.js +3 -7
  12. package/src/@claude-flow/cli/dist/src/commands/doctor.js +116 -1
  13. package/src/@claude-flow/cli/dist/src/commands/embeddings.js +4 -3
  14. package/src/@claude-flow/cli/dist/src/commands/hooks.js +3 -2
  15. package/src/@claude-flow/cli/dist/src/commands/mcp.js +38 -22
  16. package/src/@claude-flow/cli/dist/src/commands/memory.js +2 -1
  17. package/src/@claude-flow/cli/dist/src/commands/neural.js +10 -5
  18. package/src/@claude-flow/cli/dist/src/index.js +12 -0
  19. package/src/@claude-flow/cli/dist/src/init/moflo-init.js +49 -0
  20. package/src/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +2 -2
  21. package/src/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +2 -1
  22. package/src/@claude-flow/cli/dist/src/memory/memory-bridge.js +5 -1
  23. package/src/@claude-flow/cli/dist/src/memory/memory-initializer.js +29 -24
  24. package/src/@claude-flow/cli/dist/src/ruvector/ast-analyzer.js +2 -1
  25. package/src/@claude-flow/cli/dist/src/ruvector/coverage-router.js +2 -1
  26. package/src/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +2 -1
  27. package/src/@claude-flow/cli/dist/src/ruvector/enhanced-model-router.js +3 -3
  28. package/src/@claude-flow/cli/dist/src/ruvector/index.js +6 -13
  29. package/src/@claude-flow/cli/dist/src/ruvector/q-learning-router.js +4 -1
  30. package/src/@claude-flow/cli/dist/src/services/learning-service.js +2 -1
  31. package/src/@claude-flow/cli/dist/src/services/moflo-require.d.ts +34 -0
  32. package/src/@claude-flow/cli/dist/src/services/moflo-require.js +67 -0
  33. package/src/@claude-flow/cli/dist/src/services/ruvector-training.js +8 -6
  34. package/src/@claude-flow/cli/package.json +6 -6
@@ -8,7 +8,10 @@
8
8
  "Bash(tasklist.exe /FI \"IMAGENAME eq node.exe\" /V)",
9
9
  "Bash(powershell.exe -NoProfile -Command \"Get-Process node -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,StartTime,CommandLine | Format-Table -AutoSize\")",
10
10
  "Bash(npm version:*)",
11
- "Bash(gh pr:*)"
11
+ "Bash(gh pr:*)",
12
+ "mcp__claude-flow__memory_store",
13
+ "mcp__claude-flow__memory_retrieve",
14
+ "mcp__claude-flow__memory_search"
12
15
  ]
13
16
  }
14
17
  }
@@ -1,9 +1,5 @@
1
1
  {
2
- "tasksCreated": false,
3
- "taskCount": 0,
4
- "memorySearched": false,
5
- "memoryRequired": true,
6
- "interactionCount": 10,
7
- "sessionStart": null,
8
- "lastBlockedAt": "2026-03-21T05:00:01.929Z"
2
+ "tasksCreated": true,
3
+ "taskCount": 1,
4
+ "memorySearched": true
9
5
  }
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  # MoFlo
6
6
 
7
+ **⚠️ MoFlo is experimental software. APIs, commands, and behavior may change without notice.**
8
+
7
9
  **An opinionated fork of [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo), optimized for local development.**
8
10
 
9
11
  MoFlo adds automatic code and guidance cataloging along with memory gating on top of the original Ruflo/Claude Flow orchestration engine. Where the upstream project provides raw building blocks, MoFlo ships opinionated defaults — workflow gates that enforce memory-first patterns, semantic indexing that runs at session start, and learned routing that improves over time — so you get a productive setup from `flo init` without manual tuning.
@@ -30,7 +32,7 @@ MoFlo makes deliberate choices so you don't have to:
30
32
  - **Task registration before agents** — Sub-agents can't spawn until work is tracked. Prevents runaway agent proliferation.
31
33
  - **Learned routing** — Task outcomes feed back into the routing system automatically. No manual configuration needed — it gets smarter with use.
32
34
  - **Incremental indexing** — Guidance and code map indexes run on every session start but skip unchanged files. Fast after the first run.
33
- - **AI client agnostic** Works with any MCP-capable AI client. We develop and test with Claude Code, but the MCP tools, memory system, and hooks are client-independent.
35
+ - **Built for Claude Code, works with others** We develop and test exclusively with Claude Code. The MCP tools, memory system, and hooks are client-independent and should work with any MCP-capable AI client, but Claude Code is the only tested target.
34
36
  - **GitHub-oriented** — The `/flo` skill, PR workflows, and issue tracking are built around GitHub. With Claude's help, you can adapt them to your own issue tracker and source control system.
35
37
  - **Cross-platform** — Forward-slash path normalization, no `sh -c` shell commands, `windowsHide` on all spawn calls.
36
38
 
@@ -20,7 +20,8 @@
20
20
 
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
22
22
  import { resolve, dirname } from 'path';
23
- import initSqlJs from 'sql.js';
23
+ import { mofloResolveURL } from './lib/moflo-resolve.mjs';
24
+ const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
24
25
 
25
26
  function findProjectRoot() {
26
27
  let dir = process.cwd();
@@ -227,7 +228,7 @@ async function loadTransformersModel() {
227
228
  log('Attempting to load Transformers.js neural model...');
228
229
 
229
230
  try {
230
- const { env, pipeline: createPipeline } = await import('@xenova/transformers');
231
+ const { env, pipeline: createPipeline } = await import(mofloResolveURL('@xenova/transformers'));
231
232
  env.allowLocalModels = false;
232
233
  env.backends.onnx.wasm.numThreads = 1;
233
234
 
@@ -310,6 +311,25 @@ function updateEmbedding(db, id, embedding, model) {
310
311
  stmt.free();
311
312
  }
312
313
 
314
+ function getNamespaceStats(db) {
315
+ const stmt = db.prepare(`
316
+ SELECT
317
+ namespace,
318
+ COUNT(*) as total,
319
+ SUM(CASE WHEN embedding IS NOT NULL AND embedding != '' AND embedding_model != 'domain-aware-hash-v1' THEN 1 ELSE 0 END) as vectorized,
320
+ SUM(CASE WHEN embedding IS NULL OR embedding = '' THEN 1 ELSE 0 END) as missing,
321
+ SUM(CASE WHEN embedding_model = 'domain-aware-hash-v1' THEN 1 ELSE 0 END) as hash_only
322
+ FROM memory_entries
323
+ WHERE status = 'active'
324
+ GROUP BY namespace
325
+ ORDER BY namespace
326
+ `);
327
+ const results = [];
328
+ while (stmt.step()) results.push(stmt.getAsObject());
329
+ stmt.free();
330
+ return results;
331
+ }
332
+
313
333
  function getEmbeddingStats(db) {
314
334
  const stmtTotal = db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`);
315
335
  const total = stmtTotal.step() ? stmtTotal.getAsObject() : { cnt: 0 };
@@ -426,7 +446,6 @@ async function main() {
426
446
  }
427
447
  }
428
448
  }
429
- db.close();
430
449
 
431
450
  console.log('');
432
451
  log('═══════════════════════════════════════════════════════════');
@@ -445,7 +464,44 @@ async function main() {
445
464
  log(` - ${m.embedding_model}: ${m.cnt}`);
446
465
  }
447
466
  }
467
+ log('');
468
+
469
+ // Per-namespace health report
470
+ const nsStats = getNamespaceStats(db);
471
+ if (nsStats.length > 0) {
472
+ log(' Namespace Health:');
473
+ log(' ┌─────────────────┬───────┬────────────┬─────────┬───────────┐');
474
+ log(' │ Namespace │ Total │ Vectorized │ Missing │ Hash-Only │');
475
+ log(' ├─────────────────┼───────┼────────────┼─────────┼───────────┤');
476
+ let hasWarnings = false;
477
+ for (const ns of nsStats) {
478
+ const name = String(ns.namespace).padEnd(15);
479
+ const total = String(ns.total).padStart(5);
480
+ const vectorized = String(ns.vectorized).padStart(10);
481
+ const missing = String(ns.missing).padStart(7);
482
+ const hashOnly = String(ns.hash_only).padStart(9);
483
+ const warn = (ns.missing > 0 || ns.hash_only > 0) ? ' ⚠' : ' ';
484
+ log(` │ ${name} │${total} │${vectorized} │${missing} │${hashOnly} │${warn}`);
485
+ if (ns.missing > 0 || ns.hash_only > 0) hasWarnings = true;
486
+ }
487
+ log(' └─────────────────┴───────┴────────────┴─────────┴───────────┘');
488
+ if (hasWarnings) {
489
+ log('');
490
+ log(' ⚠ Some namespaces have entries without Xenova embeddings.');
491
+ log(' Run with --force to re-embed all entries:');
492
+ log(' node node_modules/moflo/bin/build-embeddings.mjs --force');
493
+ if (!useTransformers) {
494
+ log('');
495
+ log(' ⚠ Xenova model not available — using hash fallback.');
496
+ log(' Install @xenova/transformers for neural embeddings:');
497
+ log(' npm install @xenova/transformers');
498
+ }
499
+ }
500
+ }
501
+
448
502
  log('═══════════════════════════════════════════════════════════');
503
+
504
+ db.close();
449
505
  }
450
506
 
451
507
  main().catch(err => {
@@ -30,7 +30,9 @@ import { resolve, dirname, relative, basename, extname } from 'path';
30
30
  import { fileURLToPath } from 'url';
31
31
  import { createHash } from 'crypto';
32
32
  import { execSync, spawn } from 'child_process';
33
- import initSqlJs from 'sql.js';
33
+ import { mofloResolveURL } from './lib/moflo-resolve.mjs';
34
+ const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
35
+
34
36
 
35
37
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
38
 
@@ -25,7 +25,9 @@
25
25
  import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'fs';
26
26
  import { resolve, dirname, basename, extname } from 'path';
27
27
  import { fileURLToPath } from 'url';
28
- import initSqlJs from 'sql.js';
28
+ import { mofloResolveURL } from './lib/moflo-resolve.mjs';
29
+ const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
30
+
29
31
 
30
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
31
33
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared dependency resolver for moflo bin scripts.
3
+ * Resolves packages from moflo's own node_modules (not the consuming project's).
4
+ * On Windows, converts native paths to file:// URLs required by ESM import().
5
+ */
6
+
7
+ import { createRequire } from 'module';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+
10
+ const __require = createRequire(fileURLToPath(import.meta.url));
11
+
12
+ export function mofloResolveURL(specifier) {
13
+ return pathToFileURL(__require.resolve(specifier)).href;
14
+ }
@@ -16,7 +16,8 @@
16
16
 
17
17
  import { existsSync, readFileSync } from 'fs';
18
18
  import { resolve, dirname } from 'path';
19
- import initSqlJs from 'sql.js';
19
+ import { mofloResolveURL } from './lib/moflo-resolve.mjs';
20
+ const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
20
21
 
21
22
  function findProjectRoot() {
22
23
  let dir = process.cwd();
@@ -34,6 +35,8 @@ const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
34
35
  const EMBEDDING_DIMS = 384;
35
36
  const EMBEDDING_MODEL_NEURAL = 'Xenova/all-MiniLM-L6-v2';
36
37
  const EMBEDDING_MODEL_HASH = 'domain-aware-hash-v1';
38
+ // 'onnx' is a legacy alias for the Xenova model — treat them as compatible vector spaces
39
+ const NEURAL_ALIASES = new Set([EMBEDDING_MODEL_NEURAL, 'onnx']);
37
40
 
38
41
  // Parse args
39
42
  const args = process.argv.slice(2);
@@ -58,7 +61,7 @@ let useTransformers = false;
58
61
 
59
62
  async function loadTransformersModel() {
60
63
  try {
61
- const { env, pipeline: createPipeline } = await import('@xenova/transformers');
64
+ const { env, pipeline: createPipeline } = await import(mofloResolveURL('@xenova/transformers'));
62
65
  env.allowLocalModels = false;
63
66
  env.backends.onnx.wasm.numThreads = 1;
64
67
 
@@ -251,7 +254,8 @@ async function generateQueryEmbedding(queryText, db) {
251
254
  if (debug) console.error(`[semantic-search] Stored model: ${storedModel}`);
252
255
 
253
256
  // If stored embeddings are neural, try to use neural for query too
254
- if (storedModel === EMBEDDING_MODEL_NEURAL) {
257
+ // Accept both canonical name and legacy 'onnx' tag (both use the same Xenova pipeline)
258
+ if (storedModel === EMBEDDING_MODEL_NEURAL || storedModel === 'onnx') {
255
259
  await loadTransformersModel();
256
260
  if (useTransformers) {
257
261
  const neuralEmb = await generateNeuralEmbedding(queryText);
@@ -325,8 +329,9 @@ async function semanticSearch(queryText, options = {}) {
325
329
  while (stmt.step()) {
326
330
  const entry = stmt.getAsObject();
327
331
  try {
328
- // Skip entries with mismatched embedding model (incompatible vector spaces)
329
- if (entry.embedding_model && entry.embedding_model !== queryModel) continue;
332
+ const storedIsNeural = NEURAL_ALIASES.has(entry.embedding_model);
333
+ const queryIsNeural = NEURAL_ALIASES.has(queryModel);
334
+ if (entry.embedding_model && entry.embedding_model !== queryModel && !(storedIsNeural && queryIsNeural)) continue;
330
335
 
331
336
  const embedding = JSON.parse(entry.embedding);
332
337
  if (!Array.isArray(embedding) || embedding.length !== EMBEDDING_DIMS) continue;
@@ -120,6 +120,20 @@ try {
120
120
  }
121
121
  }
122
122
 
123
+ // Sync guidance bootstrap file (moflo-bootstrap.md)
124
+ // Ensures subagents can read guidance directly from disk
125
+ const bootstrapSrc = resolve(projectRoot, 'node_modules/moflo/.claude/guidance/agent-bootstrap.md');
126
+ const guidanceDir = resolve(projectRoot, '.claude/guidance');
127
+ const bootstrapDest = resolve(guidanceDir, 'moflo-bootstrap.md');
128
+ if (existsSync(bootstrapSrc)) {
129
+ try {
130
+ if (!existsSync(guidanceDir)) mkdirSync(guidanceDir, { recursive: true });
131
+ const header = '<!-- AUTO-GENERATED by moflo session-start. Do not edit — changes will be overwritten. -->\n<!-- Source: node_modules/moflo/.claude/guidance/agent-bootstrap.md -->\n\n';
132
+ const content = readFileSync(bootstrapSrc, 'utf-8');
133
+ writeFileSync(bootstrapDest, header + content);
134
+ } catch { /* non-fatal */ }
135
+ }
136
+
123
137
  // Write version stamp
124
138
  try {
125
139
  const cfDir = resolve(projectRoot, '.claude-flow');
@@ -132,6 +146,20 @@ try {
132
146
  // Non-fatal — scripts will still work, just may be stale
133
147
  }
134
148
 
149
+ // ── 3b. Ensure guidance bootstrap file exists (even without version change) ──
150
+ // Subagents need this file on disk for direct reads without memory search.
151
+ try {
152
+ const bootstrapSrc = resolve(projectRoot, 'node_modules/moflo/.claude/guidance/agent-bootstrap.md');
153
+ const guidanceDir = resolve(projectRoot, '.claude/guidance');
154
+ const bootstrapDest = resolve(guidanceDir, 'moflo-bootstrap.md');
155
+ if (existsSync(bootstrapSrc) && !existsSync(bootstrapDest)) {
156
+ if (!existsSync(guidanceDir)) mkdirSync(guidanceDir, { recursive: true });
157
+ const header = '<!-- AUTO-GENERATED by moflo session-start. Do not edit — changes will be overwritten. -->\n<!-- Source: node_modules/moflo/.claude/guidance/agent-bootstrap.md -->\n\n';
158
+ const content = readFileSync(bootstrapSrc, 'utf-8');
159
+ writeFileSync(bootstrapDest, header + content);
160
+ }
161
+ } catch { /* non-fatal */ }
162
+
135
163
  // ── 4. Spawn background tasks ───────────────────────────────────────────────
136
164
  const localCli = resolve(projectRoot, 'node_modules/moflo/src/@claude-flow/cli/bin/cli.js');
137
165
  const hasLocalCli = existsSync(localCli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.8.0",
3
+ "version": "4.8.1",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -62,8 +62,8 @@
62
62
  "dependencies": {
63
63
  "@ruvector/learning-wasm": "^0.1.29",
64
64
  "js-yaml": "^4.1.1",
65
- "semver": "^7.6.0",
66
- "sql.js": "^1.12.0",
65
+ "semver": "^7.7.4",
66
+ "sql.js": "^1.14.1",
67
67
  "zod": "^3.22.4"
68
68
  },
69
69
  "optionalDependencies": {
@@ -81,11 +81,11 @@
81
81
  },
82
82
  "devDependencies": {
83
83
  "@types/bcrypt": "^5.0.2",
84
- "@types/node": "^20.0.0",
84
+ "@types/node": "^20.19.37",
85
85
  "eslint": "^8.0.0",
86
- "moflo": "^4.7.4",
86
+ "moflo": "^4.8.0",
87
87
  "tsx": "^4.21.0",
88
- "typescript": "^5.0.0",
88
+ "typescript": "^5.9.3",
89
89
  "vitest": "^4.0.0"
90
90
  },
91
91
  "engines": {
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { readdir, stat } from 'node:fs/promises';
16
16
  import { join, extname, basename } from 'node:path';
17
+ import { mofloImport } from '../services/moflo-require.js';
17
18
  const DEFAULT_CONFIG = {
18
19
  modelsDir: './models', defaultModel: '', maxTokens: 512,
19
20
  temperature: 0.7, contextSize: 4096, kvCachePath: '', verbose: false,
@@ -281,12 +282,7 @@ export function getRuvllmBridge(config) {
281
282
  export function resetRuvllmBridge() { instance = null; }
282
283
  /** Check whether @ruvector/core is importable without loading the bridge. */
283
284
  export async function isRuvllmAvailable() {
284
- try {
285
- await import('@ruvector/core');
286
- return true;
287
- }
288
- catch {
289
- return false;
290
- }
285
+ const mod = await mofloImport('@ruvector/core');
286
+ return mod !== null;
291
287
  }
292
288
  //# sourceMappingURL=ruvllm-bridge.js.map
@@ -10,6 +10,7 @@ import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { execSync, exec } from 'child_process';
12
12
  import { promisify } from 'util';
13
+ import { getDaemonLockHolder, releaseDaemonLock } from '../services/daemon-lock.js';
13
14
  // Promisified exec with proper shell and env inheritance for cross-platform support
14
15
  const execAsync = promisify(exec);
15
16
  /**
@@ -406,6 +407,69 @@ async function checkAgenticFlow() {
406
407
  return { name: 'agentic-flow', status: 'warn', message: 'Check failed' };
407
408
  }
408
409
  }
410
+ // Find and optionally kill orphaned moflo/claude-flow node processes
411
+ async function findZombieProcesses(kill = false) {
412
+ const legitimatePid = getDaemonLockHolder(process.cwd());
413
+ const currentPid = process.pid;
414
+ const parentPid = process.ppid;
415
+ const found = [];
416
+ let killed = 0;
417
+ try {
418
+ if (process.platform === 'win32') {
419
+ // Windows: use WMIC to find node processes with moflo/claude-flow in command line
420
+ const result = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Select-Object ProcessId,CommandLine | Format-Table -AutoSize -Wrap"', { encoding: 'utf-8', timeout: 10000, windowsHide: true });
421
+ const lines = result.split('\n');
422
+ for (const line of lines) {
423
+ if (/moflo|claude-flow|flo\s+(hooks|gate|mcp|daemon)/i.test(line)) {
424
+ const pidMatch = line.match(/^\s*(\d+)/);
425
+ if (pidMatch) {
426
+ const pid = parseInt(pidMatch[1], 10);
427
+ // Skip our own process, parent, and the legitimate daemon
428
+ if (pid === currentPid || pid === parentPid || pid === legitimatePid)
429
+ continue;
430
+ found.push(pid);
431
+ }
432
+ }
433
+ }
434
+ }
435
+ else {
436
+ // Unix/macOS: use ps to find node processes
437
+ const result = execSync('ps aux | grep -E "node.*(moflo|claude-flow)" | grep -v grep', { encoding: 'utf-8', timeout: 5000 });
438
+ const lines = result.trim().split('\n');
439
+ for (const line of lines) {
440
+ const parts = line.trim().split(/\s+/);
441
+ const pid = parseInt(parts[1], 10);
442
+ if (pid === currentPid || pid === parentPid || pid === legitimatePid)
443
+ continue;
444
+ found.push(pid);
445
+ }
446
+ }
447
+ }
448
+ catch {
449
+ // No matches found (grep exits non-zero) or command failed
450
+ }
451
+ if (kill && found.length > 0) {
452
+ for (const pid of found) {
453
+ try {
454
+ if (process.platform === 'win32') {
455
+ execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
456
+ }
457
+ else {
458
+ process.kill(pid, 'SIGKILL');
459
+ }
460
+ killed++;
461
+ }
462
+ catch {
463
+ // Process may have already exited
464
+ }
465
+ }
466
+ // Clean up stale daemon lock if we killed the holder
467
+ if (legitimatePid && found.includes(legitimatePid)) {
468
+ releaseDaemonLock(process.cwd(), legitimatePid, true);
469
+ }
470
+ }
471
+ return { found: found.length, killed, pids: found };
472
+ }
409
473
  // Format health check result
410
474
  function formatCheck(check) {
411
475
  const icon = check.status === 'pass' ? output.success('✓') :
@@ -444,12 +508,20 @@ export const doctorCommand = {
444
508
  description: 'Verbose output',
445
509
  type: 'boolean',
446
510
  default: false
511
+ },
512
+ {
513
+ name: 'kill-zombies',
514
+ short: 'k',
515
+ description: 'Find and kill orphaned moflo/claude-flow node processes',
516
+ type: 'boolean',
517
+ default: false
447
518
  }
448
519
  ],
449
520
  examples: [
450
521
  { command: 'claude-flow doctor', description: 'Run full health check' },
451
522
  { command: 'claude-flow doctor --fix', description: 'Show fixes for issues' },
452
523
  { command: 'claude-flow doctor --install', description: 'Auto-install missing dependencies' },
524
+ { command: 'claude-flow doctor --kill-zombies', description: 'Find and kill zombie processes' },
453
525
  { command: 'claude-flow doctor -c version', description: 'Check for stale npx cache' },
454
526
  { command: 'claude-flow doctor -c claude', description: 'Check Claude Code CLI only' }
455
527
  ],
@@ -458,11 +530,53 @@ export const doctorCommand = {
458
530
  const autoInstall = ctx.flags.install;
459
531
  const component = ctx.flags.component;
460
532
  const verbose = ctx.flags.verbose;
533
+ const killZombies = ctx.flags['kill-zombies'];
461
534
  output.writeln();
462
535
  output.writeln(output.bold('MoFlo Doctor'));
463
536
  output.writeln(output.dim('System diagnostics and health check'));
464
537
  output.writeln(output.dim('─'.repeat(50)));
465
538
  output.writeln();
539
+ // Handle --kill-zombies early
540
+ if (killZombies) {
541
+ output.writeln(output.bold('Zombie Process Scan'));
542
+ output.writeln();
543
+ // First scan without killing to show what would be killed
544
+ const scan = await findZombieProcesses(false);
545
+ if (scan.found === 0) {
546
+ output.writeln(output.success(' No orphaned moflo processes found'));
547
+ }
548
+ else {
549
+ output.writeln(output.warning(` Found ${scan.found} orphaned process(es): PIDs ${scan.pids.join(', ')}`));
550
+ // Kill them
551
+ const result = await findZombieProcesses(true);
552
+ if (result.killed > 0) {
553
+ output.writeln(output.success(` Killed ${result.killed} zombie process(es)`));
554
+ }
555
+ if (result.killed < result.found) {
556
+ output.writeln(output.warning(` ${result.found - result.killed} process(es) could not be killed`));
557
+ }
558
+ }
559
+ output.writeln();
560
+ output.writeln(output.dim('─'.repeat(50)));
561
+ output.writeln();
562
+ }
563
+ const checkZombieProcesses = async () => {
564
+ try {
565
+ const scan = await findZombieProcesses(false);
566
+ if (scan.found === 0) {
567
+ return { name: 'Zombie Processes', status: 'pass', message: 'No orphaned processes' };
568
+ }
569
+ return {
570
+ name: 'Zombie Processes',
571
+ status: 'warn',
572
+ message: `${scan.found} orphaned process(es) (PIDs: ${scan.pids.join(', ')})`,
573
+ fix: 'moflo doctor --kill-zombies'
574
+ };
575
+ }
576
+ catch {
577
+ return { name: 'Zombie Processes', status: 'pass', message: 'Check skipped' };
578
+ }
579
+ };
466
580
  const allChecks = [
467
581
  checkVersionFreshness,
468
582
  checkNodeVersion,
@@ -476,7 +590,8 @@ export const doctorCommand = {
476
590
  checkMcpServers,
477
591
  checkDiskSpace,
478
592
  checkBuildTools,
479
- checkAgenticFlow
593
+ checkAgenticFlow,
594
+ checkZombieProcesses
480
595
  ];
481
596
  const componentMap = {
482
597
  'version': checkVersionFreshness,
@@ -13,6 +13,7 @@
13
13
  * Created with ❤️ by motailz.com
14
14
  */
15
15
  import { output } from '../output.js';
16
+ import { mofloImport } from '../services/moflo-require.js';
16
17
  // Dynamic imports for embeddings package (optional — may not be installed)
17
18
  async function getEmbeddings() {
18
19
  try {
@@ -135,7 +136,7 @@ const searchCommand = {
135
136
  return { success: false, exitCode: 1 };
136
137
  }
137
138
  // Load sql.js
138
- const initSqlJs = (await import('sql.js')).default;
139
+ const initSqlJs = (await mofloImport('sql.js')).default;
139
140
  const SQL = await initSqlJs();
140
141
  const fileBuffer = fs.readFileSync(fullDbPath);
141
142
  const db = new SQL.Database(fileBuffer);
@@ -378,7 +379,7 @@ const collectionsCommand = {
378
379
  return { success: true, data: [] };
379
380
  }
380
381
  // Load sql.js and query real data
381
- const initSqlJs = (await import('sql.js')).default;
382
+ const initSqlJs = (await mofloImport('sql.js')).default;
382
383
  const SQL = await initSqlJs();
383
384
  const fileBuffer = fs.readFileSync(fullDbPath);
384
385
  const db = new SQL.Database(fileBuffer);
@@ -1200,7 +1201,7 @@ const cacheCommand = {
1200
1201
  }
1201
1202
  // Try to count real entries via sql.js
1202
1203
  try {
1203
- const initSqlJs = (await import('sql.js')).default;
1204
+ const initSqlJs = (await mofloImport('sql.js')).default;
1204
1205
  const SQL = await initSqlJs();
1205
1206
  const fileBuffer = fs.readFileSync(resolvedDbPath);
1206
1207
  const db = new SQL.Database(fileBuffer);
@@ -6,6 +6,7 @@ import { output } from '../output.js';
6
6
  import { confirm } from '../prompt.js';
7
7
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import { storeCommand } from './transfer-store.js';
9
+ import { mofloImport } from '../services/moflo-require.js';
9
10
  // Hook types
10
11
  const HOOK_TYPES = [
11
12
  { value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
@@ -3074,7 +3075,7 @@ const tokenOptimizeCommand = {
3074
3075
  let reasoningBank = null;
3075
3076
  try {
3076
3077
  // Check if agentic-flow v3 is available
3077
- const rb = await import('agentic-flow/reasoningbank').catch(() => null);
3078
+ const rb = await mofloImport('agentic-flow/reasoningbank');
3078
3079
  if (rb) {
3079
3080
  agenticFlowAvailable = true;
3080
3081
  if (typeof rb.retrieveMemories === 'function') {
@@ -3083,7 +3084,7 @@ const tokenOptimizeCommand = {
3083
3084
  }
3084
3085
  else {
3085
3086
  // Legacy check for older agentic-flow
3086
- const af = await import('agentic-flow').catch(() => null);
3087
+ const af = await mofloImport('agentic-flow');
3087
3088
  if (af)
3088
3089
  agenticFlowAvailable = true;
3089
3090
  }
@@ -9,6 +9,7 @@ import { output } from '../output.js';
9
9
  import { confirm } from '../prompt.js';
10
10
  import { getServerManager, getMCPServerStatus, } from '../mcp-server.js';
11
11
  import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
12
+ import { acquireDaemonLock, releaseDaemonLock, getDaemonLockHolder } from '../services/daemon-lock.js';
12
13
  // MCP tools categories
13
14
  const TOOL_CATEGORIES = [
14
15
  { value: 'coordination', label: 'Coordination', hint: 'Swarm and agent coordination tools' },
@@ -97,14 +98,19 @@ const startCommand = {
97
98
  output.writeln();
98
99
  output.printInfo('Starting MCP Server...');
99
100
  output.writeln();
100
- // Check if already running
101
+ // Check daemon lock first — prevents duplicate MCP servers across all platforms
102
+ const projectRoot = process.cwd();
103
+ const lockHolder = getDaemonLockHolder(projectRoot);
104
+ if (lockHolder && lockHolder !== process.pid && !force) {
105
+ output.printWarning(`MCP Server already running (PID: ${lockHolder}, detected via daemon lock)`);
106
+ output.writeln(output.dim('Use --force to override, or stop the existing server first'));
107
+ return { success: true };
108
+ }
109
+ // Check if already running via server status
101
110
  const existingStatus = await getMCPServerStatus();
102
111
  if (existingStatus.running) {
103
- // For stdio transport, always force restart since we can't health check it
104
- // For other transports, check health unless --force is specified
105
- const shouldForceRestart = force || transport === 'stdio';
106
- if (!shouldForceRestart) {
107
- // Verify the server is actually healthy/responsive
112
+ // For non-stdio transports, check health unless --force is specified
113
+ if (!force && transport !== 'stdio') {
108
114
  const manager = getServerManager();
109
115
  const health = await manager.checkHealth();
110
116
  if (health.healthy) {
@@ -113,26 +119,36 @@ const startCommand = {
113
119
  return { success: false, exitCode: 1 };
114
120
  }
115
121
  }
116
- // Force restart or unresponsive - auto-recover
117
- output.printWarning(`MCP Server (PID: ${existingStatus.pid}) - restarting...`);
118
- try {
119
- // Force kill the existing process
120
- if (existingStatus.pid) {
121
- try {
122
- process.kill(existingStatus.pid, 'SIGKILL');
123
- }
124
- catch {
125
- // Process may already be dead
122
+ if (force) {
123
+ // Force restart kill existing and continue
124
+ output.printWarning(`MCP Server (PID: ${existingStatus.pid}) - restarting...`);
125
+ try {
126
+ if (existingStatus.pid) {
127
+ try {
128
+ process.kill(existingStatus.pid, 'SIGKILL');
129
+ }
130
+ catch {
131
+ // Process may already be dead
132
+ }
126
133
  }
134
+ const manager = getServerManager();
135
+ await manager.stop();
136
+ // Release stale daemon lock from old process
137
+ releaseDaemonLock(projectRoot, existingStatus.pid || 0, true);
138
+ output.writeln(output.dim(' Cleaned up existing server'));
139
+ }
140
+ catch {
141
+ // Continue anyway - the stop/cleanup may partially fail
127
142
  }
128
- const manager = getServerManager();
129
- await manager.stop();
130
- output.writeln(output.dim(' Cleaned up existing server'));
131
- }
132
- catch {
133
- // Continue anyway - the stop/cleanup may partially fail
134
143
  }
135
144
  }
145
+ // Acquire daemon lock for the new server
146
+ const lockResult = acquireDaemonLock(projectRoot);
147
+ if (!lockResult.acquired) {
148
+ output.printWarning(`Cannot acquire daemon lock (held by PID: ${lockResult.holder})`);
149
+ output.writeln(output.dim('Use --force to override'));
150
+ return { success: true };
151
+ }
136
152
  const options = {
137
153
  transport,
138
154
  host,