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.
- package/.claude/settings.local.json +4 -1
- package/.claude/workflow-state.json +3 -7
- package/README.md +3 -1
- package/bin/build-embeddings.mjs +59 -3
- package/bin/generate-code-map.mjs +3 -1
- package/bin/index-guidance.mjs +3 -1
- package/bin/lib/moflo-resolve.mjs +14 -0
- package/bin/semantic-search.mjs +10 -5
- package/bin/session-start-launcher.mjs +28 -0
- package/package.json +6 -6
- package/src/@claude-flow/cli/dist/src/appliance/ruvllm-bridge.js +3 -7
- package/src/@claude-flow/cli/dist/src/commands/doctor.js +116 -1
- package/src/@claude-flow/cli/dist/src/commands/embeddings.js +4 -3
- package/src/@claude-flow/cli/dist/src/commands/hooks.js +3 -2
- package/src/@claude-flow/cli/dist/src/commands/mcp.js +38 -22
- package/src/@claude-flow/cli/dist/src/commands/memory.js +2 -1
- package/src/@claude-flow/cli/dist/src/commands/neural.js +10 -5
- package/src/@claude-flow/cli/dist/src/index.js +12 -0
- package/src/@claude-flow/cli/dist/src/init/moflo-init.js +49 -0
- package/src/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +2 -2
- package/src/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +2 -1
- package/src/@claude-flow/cli/dist/src/memory/memory-bridge.js +5 -1
- package/src/@claude-flow/cli/dist/src/memory/memory-initializer.js +29 -24
- package/src/@claude-flow/cli/dist/src/ruvector/ast-analyzer.js +2 -1
- package/src/@claude-flow/cli/dist/src/ruvector/coverage-router.js +2 -1
- package/src/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +2 -1
- package/src/@claude-flow/cli/dist/src/ruvector/enhanced-model-router.js +3 -3
- package/src/@claude-flow/cli/dist/src/ruvector/index.js +6 -13
- package/src/@claude-flow/cli/dist/src/ruvector/q-learning-router.js +4 -1
- package/src/@claude-flow/cli/dist/src/services/learning-service.js +2 -1
- package/src/@claude-flow/cli/dist/src/services/moflo-require.d.ts +34 -0
- package/src/@claude-flow/cli/dist/src/services/moflo-require.js +67 -0
- package/src/@claude-flow/cli/dist/src/services/ruvector-training.js +8 -6
- 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":
|
|
3
|
-
"taskCount":
|
|
4
|
-
"memorySearched":
|
|
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
|
-
- **
|
|
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
|
|
package/bin/build-embeddings.mjs
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
22
22
|
import { resolve, dirname } from 'path';
|
|
23
|
-
import
|
|
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
|
|
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
|
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/bin/semantic-search.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync } from 'fs';
|
|
18
18
|
import { resolve, dirname } from 'path';
|
|
19
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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.
|
|
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.
|
|
66
|
-
"sql.js": "^1.
|
|
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.
|
|
84
|
+
"@types/node": "^20.19.37",
|
|
85
85
|
"eslint": "^8.0.0",
|
|
86
|
-
"moflo": "^4.
|
|
86
|
+
"moflo": "^4.8.0",
|
|
87
87
|
"tsx": "^4.21.0",
|
|
88
|
-
"typescript": "^5.
|
|
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
|
-
|
|
285
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|