moflo 4.10.7 → 4.10.9
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/guidance/shipped/moflo-cli-reference.md +1 -1
- package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -4
- package/.claude/skills/memory-optimization/SKILL.md +1 -1
- package/.claude/skills/memory-patterns/SKILL.md +3 -3
- package/.claude/skills/vector-search/SKILL.md +2 -2
- package/README.md +5 -5
- package/bin/hooks.mjs +3 -2
- package/bin/index-all.mjs +3 -2
- package/bin/index-guidance.mjs +4 -4
- package/bin/lib/daemon-port.mjs +66 -0
- package/bin/lib/process-manager.mjs +3 -3
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-config.js +182 -10
- package/dist/src/cli/commands/doctor-fixes.js +208 -3
- package/dist/src/cli/commands/doctor-registry.js +16 -1
- package/dist/src/cli/commands/memory.js +8 -8
- package/dist/src/cli/commands/neural.js +8 -6
- package/dist/src/cli/config/moflo-config.js +68 -3
- package/dist/src/cli/index.js +18 -19
- package/dist/src/cli/init/moflo-yaml-template.js +1 -1
- package/dist/src/cli/mcp-server.js +59 -10
- package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
- package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
- package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
- package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
- package/dist/src/cli/memory/daemon-write-client.js +178 -49
- package/dist/src/cli/memory/database-provider.js +58 -3
- package/dist/src/cli/memory/intelligence.js +54 -26
- package/dist/src/cli/memory/memory-initializer.js +21 -11
- package/dist/src/cli/movector/model-router.js +1 -1
- package/dist/src/cli/movector/q-learning-router.js +2 -2
- package/dist/src/cli/services/daemon-dashboard.js +94 -25
- package/dist/src/cli/services/daemon-lock.js +390 -3
- package/dist/src/cli/services/daemon-port.js +252 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/config-adapter.js +0 -182
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* shell-out where possible). Falls back to running the check's `fix` string
|
|
6
6
|
* if it looks like an `npx`/`npm`/`claude` command.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, unlinkSync, writeFileSync, readdirSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { output } from '../output.js';
|
|
11
11
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
@@ -13,6 +13,7 @@ import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
|
|
|
13
13
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
14
14
|
import { getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
15
15
|
import { findProjectRoot } from '../services/project-root.js';
|
|
16
|
+
import { legacyMemoryDbPath, legacyMemoryDbBakPath, memoryDbPath, mofloDir } from '../services/moflo-paths.js';
|
|
16
17
|
import { findZombieProcesses } from './doctor-zombies.js';
|
|
17
18
|
import { inspectMcpConfigs } from './doctor-checks-config.js';
|
|
18
19
|
import { installClaudeCode, runCommand } from './doctor-checks-runtime.js';
|
|
@@ -94,6 +95,131 @@ async function fixGateHealthHooks() {
|
|
|
94
95
|
}
|
|
95
96
|
return driftFixed && wiringFixed;
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Migrate `.swarm/` residue to its canonical home and remove the legacy directory.
|
|
100
|
+
*
|
|
101
|
+
* Three categories of artifact:
|
|
102
|
+
* 1. `memory.db` (+ `.bak`) — stale once `.moflo/moflo.db` exists; delete.
|
|
103
|
+
* 2. `q-learning-model.json` / `model-router-state.json` — live RL state.
|
|
104
|
+
* Rename into `.moflo/movector/`. If the canonical target already exists
|
|
105
|
+
* (consumer ran moflo on the new defaults before the auto-fix), the
|
|
106
|
+
* canonical wins and the `.swarm/` copy is unlinked.
|
|
107
|
+
* 3. `hooks.log` / `background.log` — diagnostic logs. Relocate to
|
|
108
|
+
* `.moflo/logs/`; append into the canonical file if it already exists
|
|
109
|
+
* so we don't drop history. Best-effort — log loss is acceptable.
|
|
110
|
+
*
|
|
111
|
+
* Finally `rmdir .swarm/` if and only if it's empty. Anything we didn't
|
|
112
|
+
* recognise is left in place rather than silently deleted.
|
|
113
|
+
*
|
|
114
|
+
* Cross-platform: uses `fs.rename`/`rmdir` (Node primitives), forward-slash-free
|
|
115
|
+
* joins via `path.join`. Works on Windows + POSIX.
|
|
116
|
+
*
|
|
117
|
+
* Returns true if the directory was fully retired OR if there was nothing to
|
|
118
|
+
* migrate; false if any relocation throws or `.swarm/` survives with unknown
|
|
119
|
+
* contents.
|
|
120
|
+
*/
|
|
121
|
+
async function fixSwarmLegacyResidue() {
|
|
122
|
+
const root = findProjectRoot();
|
|
123
|
+
const swarmDir = join(root, '.swarm');
|
|
124
|
+
if (!existsSync(swarmDir))
|
|
125
|
+
return true;
|
|
126
|
+
const canonicalDb = memoryDbPath(root);
|
|
127
|
+
const moflo = mofloDir(root);
|
|
128
|
+
const movectorDir = join(moflo, 'movector');
|
|
129
|
+
const logsDir = join(moflo, 'logs');
|
|
130
|
+
let allMigrated = true;
|
|
131
|
+
// (1) memory.db + .bak — both are migration artifacts of the launcher's
|
|
132
|
+
// copy-verify-rename step; if the canonical isn't yet in place neither
|
|
133
|
+
// source is safe to delete. The launcher creates the `.bak` only AFTER
|
|
134
|
+
// canonical exists, so this guard is conservative but correct.
|
|
135
|
+
const legacyDbPaths = [
|
|
136
|
+
['memory.db', legacyMemoryDbPath(root)],
|
|
137
|
+
['memory.db.bak', legacyMemoryDbBakPath(root)],
|
|
138
|
+
];
|
|
139
|
+
for (const [name, src] of legacyDbPaths) {
|
|
140
|
+
if (!existsSync(src))
|
|
141
|
+
continue;
|
|
142
|
+
if (!existsSync(canonicalDb)) {
|
|
143
|
+
output.writeln(output.warning(` Skipping ${name}: .moflo/moflo.db absent — run \`flo memory init\` first.`));
|
|
144
|
+
allMigrated = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
unlinkSync(src);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
output.writeln(output.warning(` Failed to delete ${name}: ${errorDetail(e)}`));
|
|
152
|
+
allMigrated = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// (2) router state JSONs — rename into .moflo/movector/.
|
|
156
|
+
const stateFiles = [
|
|
157
|
+
{ name: 'q-learning-model.json', dest: movectorDir },
|
|
158
|
+
{ name: 'model-router-state.json', dest: movectorDir },
|
|
159
|
+
];
|
|
160
|
+
for (const { name, dest } of stateFiles) {
|
|
161
|
+
const src = join(swarmDir, name);
|
|
162
|
+
if (!existsSync(src))
|
|
163
|
+
continue;
|
|
164
|
+
const target = join(dest, name);
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(dest, { recursive: true });
|
|
167
|
+
if (existsSync(target)) {
|
|
168
|
+
// Canonical already populated by a fresh save on the new defaults.
|
|
169
|
+
// Keep the canonical, drop the legacy copy.
|
|
170
|
+
unlinkSync(src);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
renameSync(src, target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
output.writeln(output.warning(` Failed to relocate ${name}: ${errorDetail(e)}`));
|
|
178
|
+
allMigrated = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// (3) logs — best-effort move. Append into canonical if it already exists
|
|
182
|
+
// (don't drop history). Hook + background logs are bounded to kilobytes in
|
|
183
|
+
// practice so the read-into-memory cost is acceptable.
|
|
184
|
+
const logFiles = ['hooks.log', 'background.log'];
|
|
185
|
+
for (const name of logFiles) {
|
|
186
|
+
const src = join(swarmDir, name);
|
|
187
|
+
if (!existsSync(src))
|
|
188
|
+
continue;
|
|
189
|
+
const target = join(logsDir, name);
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(logsDir, { recursive: true });
|
|
192
|
+
if (existsSync(target)) {
|
|
193
|
+
appendFileSync(target, readFileSync(src));
|
|
194
|
+
unlinkSync(src);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
renameSync(src, target);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
output.writeln(output.warning(` Failed to relocate ${name}: ${errorDetail(e)}`));
|
|
202
|
+
allMigrated = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// (4) rmdir .swarm/ if it's empty. Anything left is unrecognised — leave it
|
|
206
|
+
// for the user to inspect rather than silently delete.
|
|
207
|
+
try {
|
|
208
|
+
const remaining = readdirSync(swarmDir);
|
|
209
|
+
if (remaining.length === 0) {
|
|
210
|
+
rmdirSync(swarmDir);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
output.writeln(output.dim(` .swarm/ kept (${remaining.length} unrecognised entr${remaining.length === 1 ? 'y' : 'ies'}): ${remaining.join(', ')}`));
|
|
214
|
+
allMigrated = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
output.writeln(output.warning(` Failed to remove .swarm/: ${errorDetail(e)}`));
|
|
219
|
+
allMigrated = false;
|
|
220
|
+
}
|
|
221
|
+
return allMigrated;
|
|
222
|
+
}
|
|
97
223
|
/**
|
|
98
224
|
* Execute the fix for a failed/warned health check.
|
|
99
225
|
* Returns true if the fix succeeded (re-check should pass).
|
|
@@ -160,9 +286,29 @@ export async function autoFixCheck(check) {
|
|
|
160
286
|
return false;
|
|
161
287
|
}
|
|
162
288
|
},
|
|
289
|
+
// #1150 — SIGTERM the lock-holder BEFORE unlinking the lock. The old
|
|
290
|
+
// shape (`unlink lock; daemon start`) is the bug that produced orphan
|
|
291
|
+
// daemon accumulation: if the lock-holder PID was still alive, the
|
|
292
|
+
// unlink left it running and the respawn produced a second same-project
|
|
293
|
+
// daemon. Mirrors the 'Daemon Version Skew' / 'Daemon Identity Match'
|
|
294
|
+
// shape which got this right.
|
|
295
|
+
//
|
|
296
|
+
// Also reaps any same-project orphans whose PIDs aren't recorded in the
|
|
297
|
+
// lock — those are the daemons that survived prior buggy fixes.
|
|
163
298
|
'Daemon Status': async () => {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
299
|
+
const cwd = process.cwd();
|
|
300
|
+
const { getDaemonLockPayload, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
|
|
301
|
+
const payload = getDaemonLockPayload(cwd);
|
|
302
|
+
if (payload?.pid && payload.pid > 0) {
|
|
303
|
+
try {
|
|
304
|
+
process.kill(payload.pid, 'SIGTERM');
|
|
305
|
+
}
|
|
306
|
+
catch { /* already dead */ }
|
|
307
|
+
}
|
|
308
|
+
// Wipe other same-project daemons that the lock doesn't account for.
|
|
309
|
+
reapSameProjectOrphans(cwd);
|
|
310
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
311
|
+
const pidFile = join(cwd, '.moflo', 'daemon.pid');
|
|
166
312
|
try {
|
|
167
313
|
if (existsSync(lockFile))
|
|
168
314
|
unlinkSync(lockFile);
|
|
@@ -195,6 +341,59 @@ export async function autoFixCheck(check) {
|
|
|
195
341
|
catch { /* ok */ }
|
|
196
342
|
return runFixCommand('npx moflo daemon start');
|
|
197
343
|
},
|
|
344
|
+
// #1150 — terminate same-project orphan daemons. Keep the lock-holder
|
|
345
|
+
// alive if it shows up in the scan (it's the canonical daemon). If the
|
|
346
|
+
// lock-holder is missing/stale, kill all candidates and let the next
|
|
347
|
+
// session-start respawn a clean one. The pre-computed `pids` list is
|
|
348
|
+
// threaded into `reapSameProjectOrphans` so we don't re-run the
|
|
349
|
+
// OS process scan inside it.
|
|
350
|
+
'Daemon Orphan': async () => {
|
|
351
|
+
const cwd = process.cwd();
|
|
352
|
+
const { findProjectDaemonPids, getDaemonLockHolder, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
|
|
353
|
+
const pids = findProjectDaemonPids(cwd);
|
|
354
|
+
if (pids.length <= 1)
|
|
355
|
+
return true; // already healthy
|
|
356
|
+
const lockHolder = getDaemonLockHolder(cwd);
|
|
357
|
+
if (lockHolder != null && pids.includes(lockHolder)) {
|
|
358
|
+
const { survived } = reapSameProjectOrphans(cwd, process.pid, lockHolder, pids);
|
|
359
|
+
return survived.length === 0;
|
|
360
|
+
}
|
|
361
|
+
// No identifiable canonical daemon — kill them all, clear the lock,
|
|
362
|
+
// respawn fresh.
|
|
363
|
+
const { survived } = reapSameProjectOrphans(cwd, process.pid, undefined, pids);
|
|
364
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
365
|
+
try {
|
|
366
|
+
if (existsSync(lockFile))
|
|
367
|
+
unlinkSync(lockFile);
|
|
368
|
+
}
|
|
369
|
+
catch { /* ok */ }
|
|
370
|
+
if (survived.length > 0)
|
|
371
|
+
return false;
|
|
372
|
+
return runFixCommand('npx moflo daemon start');
|
|
373
|
+
},
|
|
374
|
+
// #1145 — daemon claims a different projectRoot than ours (or has no
|
|
375
|
+
// port in its lock so we can't verify). Same recycle pattern as version
|
|
376
|
+
// skew: SIGTERM the local daemon, clear the lock, respawn. Then the new
|
|
377
|
+
// daemon binds the per-project deterministic port and stamps it into
|
|
378
|
+
// the lock — clients can discover it without guessing.
|
|
379
|
+
'Daemon Identity Match': async () => {
|
|
380
|
+
const cwd = process.cwd();
|
|
381
|
+
const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
|
|
382
|
+
const payload = getDaemonLockPayload(cwd);
|
|
383
|
+
if (payload?.pid && payload.pid > 0) {
|
|
384
|
+
try {
|
|
385
|
+
process.kill(payload.pid, 'SIGTERM');
|
|
386
|
+
}
|
|
387
|
+
catch { /* already dead */ }
|
|
388
|
+
}
|
|
389
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
390
|
+
try {
|
|
391
|
+
if (existsSync(lockFile))
|
|
392
|
+
unlinkSync(lockFile);
|
|
393
|
+
}
|
|
394
|
+
catch { /* ok */ }
|
|
395
|
+
return runFixCommand('npx moflo daemon start');
|
|
396
|
+
},
|
|
198
397
|
'Embedding Coverage Truth': async () => {
|
|
199
398
|
// Same as the existing Embeddings fix — rebuild the cache by re-running
|
|
200
399
|
// the embeddings pipeline. Routes through `npx moflo` so the consumer
|
|
@@ -367,6 +566,12 @@ export async function autoFixCheck(check) {
|
|
|
367
566
|
return false;
|
|
368
567
|
}
|
|
369
568
|
},
|
|
569
|
+
// Migrate `.swarm/` residue (legacy memory.db, RL state JSONs, hook/bg logs)
|
|
570
|
+
// into their canonical `.moflo/` homes and rmdir the directory once empty.
|
|
571
|
+
// See `fixSwarmLegacyResidue` for the per-artifact policy.
|
|
572
|
+
'Swarm Residue': async () => {
|
|
573
|
+
return fixSwarmLegacyResidue();
|
|
574
|
+
},
|
|
370
575
|
'Status Line': async () => {
|
|
371
576
|
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
372
577
|
if (!existsSync(settingsPath))
|
|
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
|
|
|
12
12
|
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
13
13
|
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
14
14
|
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
15
|
-
import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
15
|
+
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
|
|
16
16
|
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
17
17
|
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
18
18
|
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
@@ -37,9 +37,15 @@ export const allChecks = [
|
|
|
37
37
|
checkStatusLine,
|
|
38
38
|
checkDaemonStatus,
|
|
39
39
|
checkDaemonVersionSkew,
|
|
40
|
+
checkDaemonIdentity,
|
|
41
|
+
checkDaemonOrphan,
|
|
40
42
|
checkDaemonWriteRouting,
|
|
41
43
|
checkWritersAudit,
|
|
42
44
|
checkMemoryDatabase,
|
|
45
|
+
// Surfaces leftover `.swarm/` artifacts (memory.db, router state, logs) so
|
|
46
|
+
// the auto-fix can relocate or delete them. Independent of the canonical
|
|
47
|
+
// DB checks — runs cheap (statSync only).
|
|
48
|
+
checkSwarmResidue,
|
|
43
49
|
// Owns the corruption signal so downstream checks (Embeddings, Semantic
|
|
44
50
|
// Quality, Memory Access Functional) don't surface it as the synthetic
|
|
45
51
|
// "Check" failure (doctor.ts:214). MUST run after checkMemoryDatabase
|
|
@@ -95,9 +101,18 @@ export const componentMap = {
|
|
|
95
101
|
'skew': checkDaemonVersionSkew,
|
|
96
102
|
'daemon-write-routing': checkDaemonWriteRouting,
|
|
97
103
|
'write-routing': checkDaemonWriteRouting,
|
|
104
|
+
'daemon-identity': checkDaemonIdentity,
|
|
105
|
+
'daemon-identity-match': checkDaemonIdentity,
|
|
106
|
+
'identity': checkDaemonIdentity,
|
|
107
|
+
'daemon-orphan': checkDaemonOrphan,
|
|
108
|
+
'daemon-orphans': checkDaemonOrphan,
|
|
109
|
+
'orphan': checkDaemonOrphan,
|
|
110
|
+
'orphans': checkDaemonOrphan,
|
|
98
111
|
'writers-audit': checkWritersAudit,
|
|
99
112
|
'writers': checkWritersAudit,
|
|
100
113
|
'memory': checkMemoryDatabase,
|
|
114
|
+
'swarm-residue': checkSwarmResidue,
|
|
115
|
+
'residue': checkSwarmResidue,
|
|
101
116
|
'memory-db-integrity': checkMemoryDbIntegrity,
|
|
102
117
|
'integrity': checkMemoryDbIntegrity,
|
|
103
118
|
'memory-integrity': checkMemoryDbIntegrity,
|
|
@@ -103,7 +103,7 @@ const storeCommand = {
|
|
|
103
103
|
size: Buffer.byteLength(value, 'utf8')
|
|
104
104
|
};
|
|
105
105
|
output.printInfo(`Storing in ${namespace}/${key}...`);
|
|
106
|
-
// Use direct
|
|
106
|
+
// Use direct memory-backend storage with automatic embedding generation
|
|
107
107
|
try {
|
|
108
108
|
const { storeEntry } = await import('../memory/memory-initializer.js');
|
|
109
109
|
if (asVector) {
|
|
@@ -175,7 +175,7 @@ const retrieveCommand = {
|
|
|
175
175
|
output.printError('Key is required');
|
|
176
176
|
return { success: false, exitCode: 1 };
|
|
177
177
|
}
|
|
178
|
-
// Use
|
|
178
|
+
// Use the memory backend directly for consistent data access
|
|
179
179
|
try {
|
|
180
180
|
const { getEntry } = await import('../memory/memory-initializer.js');
|
|
181
181
|
const result = await getEntry({ key, namespace });
|
|
@@ -302,7 +302,7 @@ const searchCommand = {
|
|
|
302
302
|
}
|
|
303
303
|
output.printInfo(`Searching: "${query}" (${searchType})`);
|
|
304
304
|
output.writeln();
|
|
305
|
-
// Use direct
|
|
305
|
+
// Use direct memory-backend search with vector similarity
|
|
306
306
|
try {
|
|
307
307
|
const { searchEntries } = await import('../memory/memory-initializer.js');
|
|
308
308
|
const searchResult = await searchEntries({
|
|
@@ -381,7 +381,7 @@ const listCommand = {
|
|
|
381
381
|
action: async (ctx) => {
|
|
382
382
|
const namespace = ctx.flags.namespace;
|
|
383
383
|
const limit = ctx.flags.limit;
|
|
384
|
-
// Use
|
|
384
|
+
// Use the memory backend directly for consistent data access
|
|
385
385
|
try {
|
|
386
386
|
const { listEntries } = await import('../memory/memory-initializer.js');
|
|
387
387
|
const listResult = await listEntries({ namespace, limit, offset: 0 });
|
|
@@ -499,7 +499,7 @@ const deleteCommand = {
|
|
|
499
499
|
return { success: true };
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
-
// Use
|
|
502
|
+
// Use the memory backend directly for consistent data access (Issue #980)
|
|
503
503
|
try {
|
|
504
504
|
const { deleteEntry } = await import('../memory/memory-initializer.js');
|
|
505
505
|
const result = await deleteEntry({ key, namespace });
|
|
@@ -1034,10 +1034,10 @@ const importCommand = {
|
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
1036
|
};
|
|
1037
|
-
// Init subcommand - initialize memory database using
|
|
1037
|
+
// Init subcommand - initialize memory database using node:sqlite
|
|
1038
1038
|
const initMemoryCommand = {
|
|
1039
1039
|
name: 'init',
|
|
1040
|
-
description: 'Initialize memory database with
|
|
1040
|
+
description: 'Initialize memory database with node:sqlite (Node 22+ built-in) - includes vector embeddings, pattern learning, temporal decay',
|
|
1041
1041
|
options: [
|
|
1042
1042
|
{
|
|
1043
1043
|
name: 'backend',
|
|
@@ -2637,7 +2637,7 @@ export const memoryCommand = {
|
|
|
2637
2637
|
output.writeln();
|
|
2638
2638
|
output.writeln('Subcommands:');
|
|
2639
2639
|
output.printList([
|
|
2640
|
-
`${output.highlight('init')} - Initialize memory database (
|
|
2640
|
+
`${output.highlight('init')} - Initialize memory database (node:sqlite)`,
|
|
2641
2641
|
`${output.highlight('store')} - Store data in memory`,
|
|
2642
2642
|
`${output.highlight('retrieve')} - Retrieve data from memory`,
|
|
2643
2643
|
`${output.highlight('search')} - Semantic/vector search`,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Created with ❤️ by motailz.com
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
|
+
import { findProjectRoot } from '../services/project-root.js';
|
|
9
|
+
import { MOFLO_DIR } from '../services/moflo-paths.js';
|
|
8
10
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
9
11
|
// Train subcommand - REAL WASM training with MoVector
|
|
10
12
|
const trainCommand = {
|
|
@@ -673,8 +675,8 @@ const optimizeCommand = {
|
|
|
673
675
|
await initializeIntelligence();
|
|
674
676
|
const patterns = await getAllPatterns();
|
|
675
677
|
const stats = getIntelligenceStats();
|
|
676
|
-
// Get actual pattern storage size
|
|
677
|
-
const patternDir = path.join(
|
|
678
|
+
// Get actual pattern storage size (#1152: anchor on projectRoot, not cwd)
|
|
679
|
+
const patternDir = path.join(findProjectRoot(), MOFLO_DIR, 'neural');
|
|
678
680
|
let beforeSize = 0;
|
|
679
681
|
try {
|
|
680
682
|
const patternFile = path.join(patternDir, 'patterns.json');
|
|
@@ -849,8 +851,8 @@ const exportCommand = {
|
|
|
849
851
|
totalUsage: 0,
|
|
850
852
|
},
|
|
851
853
|
};
|
|
852
|
-
// Load patterns from local storage
|
|
853
|
-
const memoryDir = path.join(
|
|
854
|
+
// Load patterns from local storage (#1152: projectRoot-anchored)
|
|
855
|
+
const memoryDir = path.join(findProjectRoot(), MOFLO_DIR, 'memory');
|
|
854
856
|
const patternsFile = path.join(memoryDir, 'patterns.json');
|
|
855
857
|
if (fs.existsSync(patternsFile)) {
|
|
856
858
|
const patterns = JSON.parse(fs.readFileSync(patternsFile, 'utf8'));
|
|
@@ -1242,8 +1244,8 @@ const importCommand = {
|
|
|
1242
1244
|
if (validPatterns.length < patterns.length) {
|
|
1243
1245
|
output.writeln(output.warning(`Filtered ${patterns.length - validPatterns.length} suspicious patterns`));
|
|
1244
1246
|
}
|
|
1245
|
-
// Save to local memory
|
|
1246
|
-
const memoryDir = path.join(
|
|
1247
|
+
// Save to local memory (#1152: projectRoot-anchored)
|
|
1248
|
+
const memoryDir = path.join(findProjectRoot(), MOFLO_DIR, 'memory');
|
|
1247
1249
|
if (!fs.existsSync(memoryDir)) {
|
|
1248
1250
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
1249
1251
|
}
|
|
@@ -41,7 +41,7 @@ const DEFAULT_CONFIG = {
|
|
|
41
41
|
code_map: true,
|
|
42
42
|
},
|
|
43
43
|
memory: {
|
|
44
|
-
backend: '
|
|
44
|
+
backend: 'node-sqlite',
|
|
45
45
|
embedding_model: 'Xenova/all-MiniLM-L6-v2',
|
|
46
46
|
namespace: 'default',
|
|
47
47
|
},
|
|
@@ -138,6 +138,39 @@ function findConfigFile(root) {
|
|
|
138
138
|
function positiveNumber(value, fallback) {
|
|
139
139
|
return typeof value === 'number' && isFinite(value) && value > 0 ? value : fallback;
|
|
140
140
|
}
|
|
141
|
+
const VALID_MEMORY_BACKENDS = new Set([
|
|
142
|
+
'node-sqlite',
|
|
143
|
+
'sql.js',
|
|
144
|
+
'rvf',
|
|
145
|
+
'json',
|
|
146
|
+
]);
|
|
147
|
+
// Dedupe stderr warnings — `loadMofloConfig()` runs on every
|
|
148
|
+
// `createDatabase()` call that omits an explicit provider, so a single
|
|
149
|
+
// typo in moflo.yaml would otherwise spam N lines per process. Keyed by
|
|
150
|
+
// the offending raw value so unrelated typos still each emit once.
|
|
151
|
+
const _seenUnknownBackendWarnings = new Set();
|
|
152
|
+
/**
|
|
153
|
+
* Validate a raw `memory.backend` value against the narrow union. Unknown
|
|
154
|
+
* values (previously: `agentdb`, future typos) fall back to the default
|
|
155
|
+
* with a one-line stderr warning so consumers don't silently get the wrong
|
|
156
|
+
* backend after a migration.
|
|
157
|
+
*
|
|
158
|
+
* `sql.js` is accepted here but normalised later by
|
|
159
|
+
* {@link resolveDatabaseProvider} — keeping it in the YAML-side type lets
|
|
160
|
+
* existing `moflo.yaml` files keep parsing across the upgrade.
|
|
161
|
+
*/
|
|
162
|
+
function coerceMemoryBackend(raw) {
|
|
163
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
164
|
+
return DEFAULT_CONFIG.memory.backend;
|
|
165
|
+
if (VALID_MEMORY_BACKENDS.has(raw))
|
|
166
|
+
return raw;
|
|
167
|
+
if (!_seenUnknownBackendWarnings.has(raw)) {
|
|
168
|
+
_seenUnknownBackendWarnings.add(raw);
|
|
169
|
+
process.stderr.write(`[moflo] WARNING: unknown memory.backend "${raw}" in moflo.yaml — ` +
|
|
170
|
+
`falling back to "${DEFAULT_CONFIG.memory.backend}". Valid: ${[...VALID_MEMORY_BACKENDS].join(', ')}.\n`);
|
|
171
|
+
}
|
|
172
|
+
return DEFAULT_CONFIG.memory.backend;
|
|
173
|
+
}
|
|
141
174
|
/**
|
|
142
175
|
* Parse raw config object into typed config, merging with defaults.
|
|
143
176
|
*/
|
|
@@ -166,7 +199,7 @@ function mergeConfig(raw, root) {
|
|
|
166
199
|
code_map: raw.auto_index?.code_map ?? raw.autoIndex?.code_map ?? DEFAULT_CONFIG.auto_index.code_map,
|
|
167
200
|
},
|
|
168
201
|
memory: {
|
|
169
|
-
backend: raw.memory?.backend
|
|
202
|
+
backend: coerceMemoryBackend(raw.memory?.backend),
|
|
170
203
|
embedding_model: raw.memory?.embedding_model || raw.memory?.embeddingModel || DEFAULT_CONFIG.memory.embedding_model,
|
|
171
204
|
namespace: raw.memory?.namespace || DEFAULT_CONFIG.memory.namespace,
|
|
172
205
|
},
|
|
@@ -361,7 +394,7 @@ auto_index:
|
|
|
361
394
|
|
|
362
395
|
# Memory backend
|
|
363
396
|
memory:
|
|
364
|
-
backend:
|
|
397
|
+
backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider.
|
|
365
398
|
embedding_model: Xenova/all-MiniLM-L6-v2
|
|
366
399
|
namespace: default
|
|
367
400
|
|
|
@@ -471,4 +504,36 @@ export function writeMofloConfig(projectRoot) {
|
|
|
471
504
|
fs.writeFileSync(configPath, content, 'utf-8');
|
|
472
505
|
return configPath;
|
|
473
506
|
}
|
|
507
|
+
// Dedupe stderr deprecation messages — one banner per (process, deprecated
|
|
508
|
+
// alias) keeps the noise to a single line per session even when multiple
|
|
509
|
+
// subsystems (daemon + indexer + statusline) all load the same config.
|
|
510
|
+
const _seenBackendDeprecations = new Set();
|
|
511
|
+
/**
|
|
512
|
+
* Map a YAML-side `memory.backend` value to the runtime
|
|
513
|
+
* {@link ResolvedMemoryBackend}. `sql.js` (deprecated since Phase 5 / #1084)
|
|
514
|
+
* is rewritten to `node-sqlite` with a one-time stderr deprecation note so
|
|
515
|
+
* consumers see they need to update their `moflo.yaml` but the run still
|
|
516
|
+
* succeeds.
|
|
517
|
+
*
|
|
518
|
+
* Centralising the mapping here is the whole point of #1144: every future
|
|
519
|
+
* caller that opens a DB based on the user's YAML knob goes through this
|
|
520
|
+
* helper, so the YAML schema and the runtime selection can never drift.
|
|
521
|
+
*/
|
|
522
|
+
export function resolveDatabaseProvider(backend) {
|
|
523
|
+
if (backend === 'sql.js') {
|
|
524
|
+
if (!_seenBackendDeprecations.has(backend)) {
|
|
525
|
+
_seenBackendDeprecations.add(backend);
|
|
526
|
+
process.stderr.write(`[moflo] DEPRECATED: memory.backend "sql.js" in moflo.yaml — ` +
|
|
527
|
+
`sql.js was removed in Phase 5 (#1084). Using "node-sqlite" instead. ` +
|
|
528
|
+
`Update your moflo.yaml to silence this warning.\n`);
|
|
529
|
+
}
|
|
530
|
+
return 'node-sqlite';
|
|
531
|
+
}
|
|
532
|
+
return backend;
|
|
533
|
+
}
|
|
534
|
+
/** @internal — test hook only; resets the dedupe sets between cases. */
|
|
535
|
+
export function _resetBackendDeprecations() {
|
|
536
|
+
_seenBackendDeprecations.clear();
|
|
537
|
+
_seenUnknownBackendWarnings.clear();
|
|
538
|
+
}
|
|
474
539
|
//# sourceMappingURL=moflo-config.js.map
|
package/dist/src/cli/index.js
CHANGED
|
@@ -519,30 +519,29 @@ export class CLI {
|
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
/**
|
|
522
|
-
* Load configuration
|
|
522
|
+
* Load moflo project configuration.
|
|
523
|
+
*
|
|
524
|
+
* Returns the user's `moflo.yaml` merged with defaults so command actions
|
|
525
|
+
* can read project settings directly. Prior versions invoked a v2→v3
|
|
526
|
+
* SystemConfig adapter — collapsed in #1144 because nothing actually read
|
|
527
|
+
* `ctx.config.*` and the parallel schema was a silent-drift bug class.
|
|
528
|
+
*
|
|
529
|
+
* `configPath` (the global `--config` flag) used to point at a
|
|
530
|
+
* claude-flow SystemConfig file. With the adapter gone, the flag has no
|
|
531
|
+
* runtime effect; we surface a one-line warning when a consumer passes
|
|
532
|
+
* it so the silent-no-op is visible rather than mysterious. A future
|
|
533
|
+
* flag-driven override can plumb a directory through to
|
|
534
|
+
* `loadMofloConfig()` without touching callers.
|
|
523
535
|
*/
|
|
524
536
|
async loadConfig(configPath) {
|
|
537
|
+
if (configPath) {
|
|
538
|
+
this.output.printWarning(`--config "${configPath}" is no longer honoured — moflo loads moflo.yaml ` +
|
|
539
|
+
`from the project root directly (#1144).`);
|
|
540
|
+
}
|
|
525
541
|
try {
|
|
526
|
-
|
|
527
|
-
const { loadConfig: loadSystemConfig } = await import('./shared/index.js');
|
|
528
|
-
const { systemConfigToV3Config } = await import('./config-adapter.js');
|
|
529
|
-
// Load configuration
|
|
530
|
-
const loaded = await loadSystemConfig({
|
|
531
|
-
file: configPath,
|
|
532
|
-
paths: configPath ? undefined : [process.cwd()],
|
|
533
|
-
});
|
|
534
|
-
// Convert to V3Config format
|
|
535
|
-
const v3Config = systemConfigToV3Config(loaded.config);
|
|
536
|
-
// Log warnings if any
|
|
537
|
-
if (loaded.warnings && loaded.warnings.length > 0) {
|
|
538
|
-
for (const warning of loaded.warnings) {
|
|
539
|
-
this.output.printWarning(warning);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return v3Config;
|
|
542
|
+
return loadMofloConfig();
|
|
543
543
|
}
|
|
544
544
|
catch (error) {
|
|
545
|
-
// Config loading is optional - don't fail if it doesn't exist
|
|
546
545
|
if (process.env.DEBUG) {
|
|
547
546
|
this.output.writeln(this.output.dim(`Config loading failed: ${error.message}`));
|
|
548
547
|
}
|
|
@@ -24,19 +24,44 @@ import * as os from 'os';
|
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
import { dirname } from 'path';
|
|
26
26
|
import { errorDetail } from './shared/utils/error-detail.js';
|
|
27
|
+
import { findProjectRoot } from './services/project-root.js';
|
|
27
28
|
// ESM-compatible __dirname
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const __dirname = dirname(__filename);
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Resolve the per-project `.moflo/` directory for MCP state files.
|
|
33
|
+
*
|
|
34
|
+
* Routed through the unified `findProjectRoot` so the launcher, daemon, healers
|
|
35
|
+
* and the MCP server all agree on the same anchor (see #1057, #1145).
|
|
36
|
+
* Replaces the prior `os.tmpdir()` location which was shared across every
|
|
37
|
+
* moflo consumer on the machine — concurrent projects overwrote each other's
|
|
38
|
+
* PID file and `flo mcp stop` could kill the wrong project's MCP server.
|
|
39
|
+
*/
|
|
40
|
+
function resolveMcpStateDir() {
|
|
41
|
+
return path.join(findProjectRoot(), '.moflo');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Legacy tmpdir paths (pre-#1151). Kept only so we can clean up dead PID
|
|
45
|
+
* files left behind by older moflo versions. Never written to.
|
|
32
46
|
*/
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const LEGACY_TMPDIR_PID_FILE = path.join(os.tmpdir(), 'claude-flow-mcp.pid');
|
|
48
|
+
const LEGACY_TMPDIR_LOG_FILE = path.join(os.tmpdir(), 'claude-flow-mcp.log');
|
|
49
|
+
/**
|
|
50
|
+
* Build default configuration at construction time. PID/log paths resolve
|
|
51
|
+
* against `findProjectRoot()` so each project gets its own MCP state files
|
|
52
|
+
* under `<projectRoot>/.moflo/`. Lazy so test code that sets
|
|
53
|
+
* `CLAUDE_PROJECT_DIR` per-test sees the override.
|
|
54
|
+
*/
|
|
55
|
+
function buildDefaultOptions() {
|
|
56
|
+
const stateDir = resolveMcpStateDir();
|
|
57
|
+
return {
|
|
58
|
+
transport: 'stdio',
|
|
59
|
+
pidFile: path.join(stateDir, 'mcp-server.pid'),
|
|
60
|
+
logFile: path.join(stateDir, 'mcp-server.log'),
|
|
61
|
+
tools: 'all',
|
|
62
|
+
daemonize: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
40
65
|
/**
|
|
41
66
|
* MCP Server Manager
|
|
42
67
|
*
|
|
@@ -49,7 +74,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
49
74
|
healthCheckInterval;
|
|
50
75
|
constructor(options = {}) {
|
|
51
76
|
super();
|
|
52
|
-
this.options = { ...
|
|
77
|
+
this.options = { ...buildDefaultOptions(), ...options };
|
|
53
78
|
}
|
|
54
79
|
/**
|
|
55
80
|
* Start the MCP server
|
|
@@ -461,11 +486,35 @@ export class MCPServerManager extends EventEmitter {
|
|
|
461
486
|
this.healthCheckInterval.unref();
|
|
462
487
|
}
|
|
463
488
|
/**
|
|
464
|
-
* Write PID file
|
|
489
|
+
* Write PID file. Ensures the per-project state directory exists and opportunistically
|
|
490
|
+
* cleans up an abandoned tmpdir PID file (pre-#1151 layout) when it points at a dead
|
|
491
|
+
* PID — abandoned dead-PID files belong to nobody so we can safely unlink them, but
|
|
492
|
+
* a live tmpdir PID is left alone since it may belong to another project on an
|
|
493
|
+
* older moflo version.
|
|
465
494
|
*/
|
|
466
495
|
async writePidFile() {
|
|
467
496
|
const pid = this.process?.pid || process.pid;
|
|
497
|
+
await fs.promises.mkdir(path.dirname(this.options.pidFile), { recursive: true });
|
|
468
498
|
await fs.promises.writeFile(this.options.pidFile, String(pid), 'utf8');
|
|
499
|
+
await this.cleanupAbandonedTmpdirPid();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Remove a stale `<tmpdir>/claude-flow-mcp.pid` left by a pre-#1151 moflo if
|
|
503
|
+
* the PID it points to is no longer running. Live PIDs are preserved so we
|
|
504
|
+
* don't break stop/status for another project still on the old layout.
|
|
505
|
+
*/
|
|
506
|
+
async cleanupAbandonedTmpdirPid() {
|
|
507
|
+
try {
|
|
508
|
+
const legacy = await fs.promises.readFile(LEGACY_TMPDIR_PID_FILE, 'utf8');
|
|
509
|
+
const legacyPid = parseInt(legacy.trim(), 10);
|
|
510
|
+
if (!Number.isNaN(legacyPid) && !this.isProcessRunning(legacyPid)) {
|
|
511
|
+
await fs.promises.unlink(LEGACY_TMPDIR_PID_FILE).catch(() => { });
|
|
512
|
+
await fs.promises.unlink(LEGACY_TMPDIR_LOG_FILE).catch(() => { });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// No legacy file (the common path post-upgrade) — nothing to do.
|
|
517
|
+
}
|
|
469
518
|
}
|
|
470
519
|
/**
|
|
471
520
|
* Read PID file
|