moflo 4.9.0-rc.1 → 4.9.0-rc.2
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/helpers/statusline.cjs +11 -8
- package/bin/build-embeddings.mjs +5 -5
- package/bin/generate-code-map.mjs +4 -3
- package/bin/index-guidance.mjs +4 -3
- package/bin/index-patterns.mjs +3 -2
- package/bin/index-tests.mjs +3 -2
- package/bin/lib/moflo-paths.mjs +154 -3
- package/bin/semantic-search.mjs +2 -1
- package/bin/session-start-launcher.mjs +20 -1
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +2 -11
- package/dist/src/cli/commands/doctor.js +27 -20
- package/dist/src/cli/commands/embeddings.js +9 -7
- package/dist/src/cli/commands/hooks.js +4 -9
- package/dist/src/cli/commands/swarm.js +2 -4
- package/dist/src/cli/embeddings/migration/sqljs-helpers.js +1 -1
- package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
- package/dist/src/cli/memory/bridge-core.js +43 -21
- package/dist/src/cli/memory/memory-initializer.js +31 -41
- package/dist/src/cli/memory/migration.js +1 -0
- package/dist/src/cli/services/embeddings-migration.js +4 -3
- package/dist/src/cli/services/learning-service.js +7 -8
- package/dist/src/cli/services/moflo-paths.js +179 -3
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -256,13 +256,15 @@ function formatModelName(modelId) {
|
|
|
256
256
|
return 'Claude Code';
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
// Get learning stats from memory database (pure stat calls)
|
|
259
|
+
// Get learning stats from memory database (pure stat calls).
|
|
260
|
+
// Canonical first (post-#727), then legacy paths. Keep in lockstep with
|
|
261
|
+
// `memoryDbCandidatePaths` in src/cli/services/moflo-paths.ts.
|
|
260
262
|
function getLearningStats() {
|
|
261
263
|
const memoryPaths = [
|
|
264
|
+
path.join(CWD, '.moflo', 'moflo.db'),
|
|
262
265
|
path.join(CWD, '.swarm', 'memory.db'),
|
|
263
|
-
path.join(CWD, '.moflo', 'memory.db'),
|
|
264
|
-
path.join(CWD, '.claude', 'memory.db'),
|
|
265
266
|
path.join(CWD, 'data', 'memory.db'),
|
|
267
|
+
path.join(CWD, '.claude', 'memory.db'),
|
|
266
268
|
path.join(CWD, '.agentdb', 'memory.db'),
|
|
267
269
|
];
|
|
268
270
|
|
|
@@ -506,12 +508,13 @@ function getAgentDBStats() {
|
|
|
506
508
|
return { vectorCount, dbSizeKB, namespaces, hasHnsw };
|
|
507
509
|
}
|
|
508
510
|
|
|
509
|
-
// Fallback: estimate from DB file size (no subprocess)
|
|
511
|
+
// Fallback: estimate from DB file size (no subprocess). Same probe order
|
|
512
|
+
// as `getLearningStats` above — see comment there.
|
|
510
513
|
const dbFiles = [
|
|
514
|
+
path.join(CWD, '.moflo', 'moflo.db'),
|
|
511
515
|
path.join(CWD, '.swarm', 'memory.db'),
|
|
512
|
-
path.join(CWD, '.moflo', 'memory.db'),
|
|
513
|
-
path.join(CWD, '.claude', 'memory.db'),
|
|
514
516
|
path.join(CWD, 'data', 'memory.db'),
|
|
517
|
+
path.join(CWD, '.claude', 'memory.db'),
|
|
515
518
|
];
|
|
516
519
|
for (const f of dbFiles) {
|
|
517
520
|
const stat = safeStat(f);
|
|
@@ -524,8 +527,8 @@ function getAgentDBStats() {
|
|
|
524
527
|
}
|
|
525
528
|
|
|
526
529
|
const hnswPaths = [
|
|
527
|
-
path.join(CWD, '.swarm', 'hnsw.index'),
|
|
528
530
|
path.join(CWD, '.moflo', 'hnsw.index'),
|
|
531
|
+
path.join(CWD, '.swarm', 'hnsw.index'), // legacy pre-#727
|
|
529
532
|
];
|
|
530
533
|
for (const p of hnswPaths) {
|
|
531
534
|
if (safeStat(p)) {
|
|
@@ -592,7 +595,7 @@ function getIntegrationStatus() {
|
|
|
592
595
|
}
|
|
593
596
|
}
|
|
594
597
|
|
|
595
|
-
const hasDatabase = ['.
|
|
598
|
+
const hasDatabase = ['.moflo/moflo.db', '.swarm/memory.db', 'data/memory.db']
|
|
596
599
|
.some(p => fs.existsSync(path.join(CWD, p)));
|
|
597
600
|
const hasApi = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
598
601
|
|
package/bin/build-embeddings.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
20
20
|
import { resolve, dirname } from 'path';
|
|
21
21
|
import { mofloResolveURL, mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
22
|
+
import { memoryDbPath, hnswIndexPath } from './lib/moflo-paths.mjs';
|
|
22
23
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
23
24
|
const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
|
|
24
25
|
const BRIDGE_CORE = 'dist/src/cli/memory/bridge-core.js';
|
|
@@ -35,7 +36,7 @@ function findProjectRoot() {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const projectRoot = findProjectRoot();
|
|
38
|
-
const DB_PATH =
|
|
39
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
39
40
|
|
|
40
41
|
// Canonical model name emitted into `memory_entries.embedding_model`. The
|
|
41
42
|
// semantic-search bin script accepts this plus a short set of legacy aliases
|
|
@@ -182,8 +183,7 @@ function getEmbeddingStats(db) {
|
|
|
182
183
|
function writeVectorStatsCache(stats, nsStats) {
|
|
183
184
|
try {
|
|
184
185
|
const dbSizeKB = Math.floor(readFileSync(DB_PATH).length / 1024);
|
|
185
|
-
const hasHnsw = existsSync(
|
|
186
|
-
|| existsSync(resolve(projectRoot, '.moflo', 'hnsw.index'));
|
|
186
|
+
const hasHnsw = existsSync(hnswIndexPath(projectRoot));
|
|
187
187
|
const missing = nsStats.reduce((sum, ns) => sum + (ns.missing || 0), 0);
|
|
188
188
|
writeVectorStatsJson(projectRoot, {
|
|
189
189
|
vectorCount: stats.withEmbeddings,
|
|
@@ -263,8 +263,8 @@ async function main() {
|
|
|
263
263
|
if (embedded > 0) {
|
|
264
264
|
saveDb(db);
|
|
265
265
|
for (const p of [
|
|
266
|
-
|
|
267
|
-
resolve(projectRoot, '.
|
|
266
|
+
hnswIndexPath(projectRoot),
|
|
267
|
+
resolve(projectRoot, '.moflo', 'hnsw.metadata.json'),
|
|
268
268
|
]) {
|
|
269
269
|
if (existsSync(p)) {
|
|
270
270
|
unlinkSync(p);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Generate structural code map for a monorepo or project.
|
|
4
4
|
*
|
|
5
|
-
* Produces five chunk types stored in the `code-map` namespace of .
|
|
5
|
+
* Produces five chunk types stored in the `code-map` namespace of .moflo/moflo.db:
|
|
6
6
|
* 1. project: — one per top-level project directory (bird's-eye overview)
|
|
7
7
|
* 2. dir: — one per directory with 2+ exported types (drill-down detail)
|
|
8
8
|
* 3. iface-map: — batched interface-to-implementation mappings
|
|
@@ -31,6 +31,7 @@ import { fileURLToPath } from 'url';
|
|
|
31
31
|
import { createHash } from 'crypto';
|
|
32
32
|
import { execSync, execFileSync, spawn } from 'child_process';
|
|
33
33
|
import { mofloResolveURL } from './lib/moflo-resolve.mjs';
|
|
34
|
+
import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
|
|
34
35
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
35
36
|
|
|
36
37
|
|
|
@@ -49,8 +50,8 @@ function findProjectRoot() {
|
|
|
49
50
|
|
|
50
51
|
const projectRoot = findProjectRoot();
|
|
51
52
|
const NAMESPACE = 'code-map';
|
|
52
|
-
const DB_PATH =
|
|
53
|
-
const HASH_CACHE_PATH = resolve(projectRoot, '
|
|
53
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
54
|
+
const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'code-map-hash.txt');
|
|
54
55
|
|
|
55
56
|
// Directories to exclude from indexing
|
|
56
57
|
const EXCLUDE_DIRS = [
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSy
|
|
|
26
26
|
import { resolve, relative, dirname, basename, extname } from 'path';
|
|
27
27
|
import { fileURLToPath } from 'url';
|
|
28
28
|
import { mofloResolveURL } from './lib/moflo-resolve.mjs';
|
|
29
|
+
import { memoryDbPath } from './lib/moflo-paths.mjs';
|
|
29
30
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
30
31
|
|
|
31
32
|
|
|
@@ -47,7 +48,7 @@ const projectRoot = findProjectRoot();
|
|
|
47
48
|
const mofloRoot = resolve(__dirname, '..');
|
|
48
49
|
|
|
49
50
|
const NAMESPACE = 'guidance';
|
|
50
|
-
const DB_PATH =
|
|
51
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
51
52
|
|
|
52
53
|
// ============================================================================
|
|
53
54
|
// Load guidance directories from moflo.yaml, falling back to defaults
|
|
@@ -883,7 +884,7 @@ if (!skipEmbeddings && needsEmbeddings) {
|
|
|
883
884
|
const embeddingArgs = ['--namespace', NAMESPACE];
|
|
884
885
|
|
|
885
886
|
// Create log file for background process output
|
|
886
|
-
const logDir = resolve(projectRoot, '.
|
|
887
|
+
const logDir = resolve(projectRoot, '.moflo/logs');
|
|
887
888
|
if (!existsSync(logDir)) {
|
|
888
889
|
mkdirSync(logDir, { recursive: true });
|
|
889
890
|
}
|
|
@@ -902,7 +903,7 @@ if (!skipEmbeddings && needsEmbeddings) {
|
|
|
902
903
|
proc.unref(); // Allow parent to exit independently
|
|
903
904
|
|
|
904
905
|
log(`Background embedding started (PID: ${proc.pid})`);
|
|
905
|
-
log(`Log file: .
|
|
906
|
+
log(`Log file: .moflo/logs/embeddings.log`);
|
|
906
907
|
} else {
|
|
907
908
|
log('⚠️ Embedding script not found, skipping embedding generation');
|
|
908
909
|
}
|
package/bin/index-patterns.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { fileURLToPath } from 'url';
|
|
|
30
30
|
import { createHash } from 'crypto';
|
|
31
31
|
import { spawn } from 'child_process';
|
|
32
32
|
import { mofloResolveURL } from './lib/moflo-resolve.mjs';
|
|
33
|
+
import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
|
|
33
34
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -46,8 +47,8 @@ function findProjectRoot() {
|
|
|
46
47
|
|
|
47
48
|
const projectRoot = findProjectRoot();
|
|
48
49
|
const NAMESPACE = 'patterns';
|
|
49
|
-
const DB_PATH =
|
|
50
|
-
const HASH_CACHE_PATH = resolve(projectRoot, '
|
|
50
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
51
|
+
const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'patterns-hash.txt');
|
|
51
52
|
|
|
52
53
|
const args = process.argv.slice(2);
|
|
53
54
|
const force = args.includes('--force');
|
package/bin/index-tests.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { fileURLToPath } from 'url';
|
|
|
28
28
|
import { createHash } from 'crypto';
|
|
29
29
|
import { execSync, execFileSync, spawn } from 'child_process';
|
|
30
30
|
import { mofloResolveURL } from './lib/moflo-resolve.mjs';
|
|
31
|
+
import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
|
|
31
32
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
32
33
|
|
|
33
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -44,8 +45,8 @@ function findProjectRoot() {
|
|
|
44
45
|
|
|
45
46
|
const projectRoot = findProjectRoot();
|
|
46
47
|
const NAMESPACE = 'tests';
|
|
47
|
-
const DB_PATH =
|
|
48
|
-
const HASH_CACHE_PATH = resolve(projectRoot, '
|
|
48
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
49
|
+
const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'tests-hash.txt');
|
|
49
50
|
|
|
50
51
|
// Parse args
|
|
51
52
|
const args = process.argv.slice(2);
|
package/bin/lib/moflo-paths.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure-JS counterpart to src/cli/services/moflo-paths.ts (#699).
|
|
2
|
+
* Pure-JS counterpart to src/cli/services/moflo-paths.ts (#699, #727).
|
|
3
3
|
*
|
|
4
4
|
* Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
|
|
5
5
|
* run before any TS compilation has happened — they can't import the .ts
|
|
@@ -8,11 +8,30 @@
|
|
|
8
8
|
* launcher path. Algorithm parity is enforced by the parity case in
|
|
9
9
|
* src/cli/__tests__/services/moflo-paths-migration.test.ts.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
closeSync,
|
|
13
|
+
copyFileSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
openSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
readSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmdirSync,
|
|
22
|
+
statSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
13
26
|
|
|
14
27
|
export const MOFLO_DIR = '.moflo';
|
|
28
|
+
export const MEMORY_DB_FILE = 'moflo.db';
|
|
29
|
+
export const HNSW_INDEX_FILE = 'hnsw.index';
|
|
30
|
+
|
|
15
31
|
export const LEGACY_CLAUDE_FLOW_DIR = '.claude-flow';
|
|
32
|
+
export const LEGACY_SWARM_DIR = '.swarm';
|
|
33
|
+
export const LEGACY_MEMORY_DB_FILE = 'memory.db';
|
|
34
|
+
export const LEGACY_MEMORY_DB_BAK_SUFFIX = '.bak';
|
|
16
35
|
|
|
17
36
|
export function mofloDir(projectRoot) {
|
|
18
37
|
return join(projectRoot, MOFLO_DIR);
|
|
@@ -22,6 +41,35 @@ export function legacyClaudeFlowDir(projectRoot) {
|
|
|
22
41
|
return join(projectRoot, LEGACY_CLAUDE_FLOW_DIR);
|
|
23
42
|
}
|
|
24
43
|
|
|
44
|
+
export function memoryDbPath(projectRoot) {
|
|
45
|
+
return join(projectRoot, MOFLO_DIR, MEMORY_DB_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hnswIndexPath(projectRoot) {
|
|
49
|
+
return join(projectRoot, MOFLO_DIR, HNSW_INDEX_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function legacyMemoryDbPath(projectRoot) {
|
|
53
|
+
return join(projectRoot, LEGACY_SWARM_DIR, LEGACY_MEMORY_DB_FILE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function legacyHnswIndexPath(projectRoot) {
|
|
57
|
+
return join(projectRoot, LEGACY_SWARM_DIR, HNSW_INDEX_FILE);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function legacyMemoryDbBakPath(projectRoot) {
|
|
61
|
+
return join(projectRoot, LEGACY_SWARM_DIR, `${LEGACY_MEMORY_DB_FILE}${LEGACY_MEMORY_DB_BAK_SUFFIX}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function memoryDbCandidatePaths(projectRoot) {
|
|
65
|
+
return [
|
|
66
|
+
memoryDbPath(projectRoot),
|
|
67
|
+
legacyMemoryDbPath(projectRoot),
|
|
68
|
+
join(projectRoot, 'data', LEGACY_MEMORY_DB_FILE),
|
|
69
|
+
join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
/**
|
|
26
74
|
* One-time migration of `.claude-flow/` → `.moflo/`. Idempotent — safe to call
|
|
27
75
|
* on every session start. See moflo-paths.ts for the full contract.
|
|
@@ -66,3 +114,106 @@ export function migrateClaudeFlowToMoflo(projectRoot) {
|
|
|
66
114
|
|
|
67
115
|
return moved > 0 ? { migrated: true } : { migrated: false, reason: 'merged-nothing' };
|
|
68
116
|
}
|
|
117
|
+
|
|
118
|
+
const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
|
|
119
|
+
|
|
120
|
+
function looksLikeSqliteFile(filePath) {
|
|
121
|
+
let fd = null;
|
|
122
|
+
try {
|
|
123
|
+
fd = openSync(filePath, 'r');
|
|
124
|
+
const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
|
|
125
|
+
const read = readSync(fd, buf, 0, buf.length, 0);
|
|
126
|
+
if (read < SQLITE_MAGIC_HEADER.length) return false;
|
|
127
|
+
return buf.equals(SQLITE_MAGIC_HEADER);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
} finally {
|
|
131
|
+
if (fd !== null) try { closeSync(fd); } catch { /* non-fatal */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function verifyByteEqual(srcPath, dstPath) {
|
|
136
|
+
try {
|
|
137
|
+
const srcStat = statSync(srcPath);
|
|
138
|
+
const dstStat = statSync(dstPath);
|
|
139
|
+
if (srcStat.size !== dstStat.size) return false;
|
|
140
|
+
const srcBuf = readFileSync(srcPath);
|
|
141
|
+
const dstBuf = readFileSync(dstPath);
|
|
142
|
+
return srcBuf.equals(dstBuf);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function tryUnlink(filePath) {
|
|
149
|
+
try { unlinkSync(filePath); } catch { /* non-fatal */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function migrateHnswIndex(projectRoot) {
|
|
153
|
+
const src = legacyHnswIndexPath(projectRoot);
|
|
154
|
+
const dst = hnswIndexPath(projectRoot);
|
|
155
|
+
|
|
156
|
+
if (!existsSync(src)) return false;
|
|
157
|
+
if (existsSync(dst)) return false;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
161
|
+
copyFileSync(src, dst);
|
|
162
|
+
} catch {
|
|
163
|
+
tryUnlink(dst);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!verifyByteEqual(src, dst)) {
|
|
168
|
+
tryUnlink(dst);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
tryUnlink(src);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One-time relocation of memory DB from `.swarm/memory.db` → `.moflo/moflo.db`
|
|
178
|
+
* (story #727). Idempotent. See moflo-paths.ts for the full contract and the
|
|
179
|
+
* SQLite-header / byte-equal verification rationale.
|
|
180
|
+
*
|
|
181
|
+
* MUST run before any long-lived sql.js consumer (MCP server, daemon) opens
|
|
182
|
+
* the DB — sql.js dumps the whole snapshot on every flush and would clobber
|
|
183
|
+
* the relocated file. Launcher section 0b is the safe boundary.
|
|
184
|
+
*/
|
|
185
|
+
export function migrateMemoryDbToMoflo(projectRoot) {
|
|
186
|
+
const target = memoryDbPath(projectRoot);
|
|
187
|
+
if (existsSync(target)) return { migrated: false, reason: 'target-exists' };
|
|
188
|
+
|
|
189
|
+
const source = legacyMemoryDbPath(projectRoot);
|
|
190
|
+
if (!existsSync(source)) return { migrated: false, reason: 'no-legacy' };
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
194
|
+
} catch {
|
|
195
|
+
return { migrated: false, reason: 'copy-failed' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
copyFileSync(source, target);
|
|
200
|
+
} catch {
|
|
201
|
+
tryUnlink(target);
|
|
202
|
+
return { migrated: false, reason: 'copy-failed' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
|
|
206
|
+
tryUnlink(target);
|
|
207
|
+
return { migrated: false, reason: 'verify-failed' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const hnswMoved = migrateHnswIndex(projectRoot);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
renameSync(source, legacyMemoryDbBakPath(projectRoot));
|
|
214
|
+
} catch {
|
|
215
|
+
return { migrated: true, reason: 'rename-failed', hnswMoved };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { migrated: true, hnswMoved };
|
|
219
|
+
}
|
package/bin/semantic-search.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { existsSync, readFileSync } from 'fs';
|
|
21
21
|
import { resolve, dirname } from 'path';
|
|
22
22
|
import { mofloResolveURL, mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
23
|
+
import { memoryDbPath } from './lib/moflo-paths.mjs';
|
|
23
24
|
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
24
25
|
const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
|
|
25
26
|
|
|
@@ -34,7 +35,7 @@ function findProjectRoot() {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const projectRoot = findProjectRoot();
|
|
37
|
-
const DB_PATH =
|
|
38
|
+
const DB_PATH = memoryDbPath(projectRoot);
|
|
38
39
|
|
|
39
40
|
const EMBEDDING_MODEL = 'fast-all-MiniLM-L6-v2';
|
|
40
41
|
const EMBEDDING_DIMS = 384;
|
|
@@ -11,7 +11,7 @@ import { spawn } from 'child_process';
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
|
|
12
12
|
import { resolve, dirname } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import { migrateClaudeFlowToMoflo } from './lib/moflo-paths.mjs';
|
|
14
|
+
import { migrateClaudeFlowToMoflo, migrateMemoryDbToMoflo } from './lib/moflo-paths.mjs';
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
|
|
@@ -72,6 +72,25 @@ try {
|
|
|
72
72
|
// again next session. Better to keep launching than to block on it.
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// ── 0b. LEGACY memory DB relocation (#727) ──────────────────────────────────
|
|
76
|
+
// Run BEFORE long-lived sql.js consumers (MCP server, daemon) — see the
|
|
77
|
+
// `migrateMemoryDbToMoflo` JSDoc for the copy-verify-delete contract and
|
|
78
|
+
// the sql.js write-back hazard.
|
|
79
|
+
try {
|
|
80
|
+
const dbMigration = migrateMemoryDbToMoflo(projectRoot);
|
|
81
|
+
if (dbMigration?.migrated) {
|
|
82
|
+
const detail = dbMigration.hnswMoved
|
|
83
|
+
? '.swarm/memory.db → .moflo/moflo.db (with hnsw.index)'
|
|
84
|
+
: '.swarm/memory.db → .moflo/moflo.db';
|
|
85
|
+
emitMutation('relocated memory db', detail);
|
|
86
|
+
if (dbMigration.reason === 'rename-failed') {
|
|
87
|
+
emitMutation('legacy .swarm/memory.db remains', 'rename to .bak failed — flo doctor will warn');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Non-fatal — failed migration leaves both DBs in place; next session retries.
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
// ── 1. Helper: fire-and-forget a background process ─────────────────────────
|
|
76
95
|
function fireAndForget(cmd, args, label) {
|
|
77
96
|
try {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
25
25
|
import { existsSync } from 'fs';
|
|
26
|
-
import { join } from 'path';
|
|
27
26
|
import { CANONICAL_EMBEDDING_MODEL } from '../embeddings/migration/types.js';
|
|
28
27
|
import { mofloImport } from '../services/moflo-require.js';
|
|
28
|
+
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
29
29
|
/**
|
|
30
30
|
* Known neural-model labels that all share the all-MiniLM-L6-v2 384-dim
|
|
31
31
|
* vector space. The Story-2 migration retags any of these to the
|
|
@@ -124,16 +124,7 @@ export async function checkEmbeddingHygiene() {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
function resolveMemoryDb() {
|
|
127
|
-
|
|
128
|
-
join(process.cwd(), '.swarm', 'memory.db'),
|
|
129
|
-
join(process.cwd(), '.moflo', 'memory.db'),
|
|
130
|
-
join(process.cwd(), 'data', 'memory.db'),
|
|
131
|
-
];
|
|
132
|
-
for (const p of candidates) {
|
|
133
|
-
if (existsSync(p))
|
|
134
|
-
return p;
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
127
|
+
return memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p)) ?? null;
|
|
137
128
|
}
|
|
138
129
|
async function loadModelGroups(dbPath) {
|
|
139
130
|
const fs = await import('fs');
|
|
@@ -16,6 +16,7 @@ import { getDaemonLockHolder, releaseDaemonLock, isDaemonProcess } from '../serv
|
|
|
16
16
|
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkMofloDbBridge, getMofloRoot, } from './doctor-checks-deep.js';
|
|
17
17
|
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
18
18
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
19
|
+
import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
|
|
19
20
|
// Promisified exec with proper shell and env inheritance for cross-platform support
|
|
20
21
|
const execAsync = promisify(exec);
|
|
21
22
|
/**
|
|
@@ -168,22 +169,33 @@ async function checkDaemonStatus() {
|
|
|
168
169
|
}
|
|
169
170
|
// Check memory database
|
|
170
171
|
async function checkMemoryDatabase() {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
const root = process.cwd();
|
|
173
|
+
const canonical = memoryDbPath(root);
|
|
174
|
+
for (const dbPath of memoryDbCandidatePaths(root)) {
|
|
175
|
+
let stats;
|
|
176
|
+
try {
|
|
177
|
+
stats = statSync(dbPath);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
183
|
+
if (dbPath === canonical) {
|
|
184
|
+
let message = `.moflo/moflo.db (${sizeMB} MB)`;
|
|
185
|
+
// Unfinished migration tail: source still present means the launcher's
|
|
186
|
+
// rename-to-.bak step failed (Windows lock most often). Flag so the user
|
|
187
|
+
// knows to clear the stale source.
|
|
188
|
+
if (existsSync(legacyMemoryDbPath(root))) {
|
|
189
|
+
message += ' — legacy .swarm/memory.db still present (delete it after confirming canonical is healthy)';
|
|
185
190
|
}
|
|
191
|
+
return { name: 'Memory Database', status: 'pass', message };
|
|
186
192
|
}
|
|
193
|
+
return {
|
|
194
|
+
name: 'Memory Database',
|
|
195
|
+
status: 'warn',
|
|
196
|
+
message: `${dbPath} (${sizeMB} MB) — legacy location, will migrate to .moflo/moflo.db on next session start`,
|
|
197
|
+
fix: 'restart claude code session',
|
|
198
|
+
};
|
|
187
199
|
}
|
|
188
200
|
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
189
201
|
}
|
|
@@ -474,12 +486,7 @@ const VECTOR_STATS_SKEW_WARN_THRESHOLD = 0.2;
|
|
|
474
486
|
// Check embeddings / vector index health.
|
|
475
487
|
// Exported so the #639 stale-cache regression test can invoke it directly.
|
|
476
488
|
export async function checkEmbeddings() {
|
|
477
|
-
const
|
|
478
|
-
join(process.cwd(), '.swarm', 'memory.db'),
|
|
479
|
-
join(process.cwd(), '.moflo', 'memory.db'),
|
|
480
|
-
join(process.cwd(), 'data', 'memory.db'),
|
|
481
|
-
];
|
|
482
|
-
const liveDbPath = dbPaths.find((p) => existsSync(p));
|
|
489
|
+
const liveDbPath = memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p));
|
|
483
490
|
// 1. Fast path: read cached vector-stats.json if available
|
|
484
491
|
const statsPath = join(process.cwd(), '.moflo', 'vector-stats.json');
|
|
485
492
|
try {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import { output } from '../output.js';
|
|
16
16
|
import { mofloImport } from '../services/moflo-require.js';
|
|
17
17
|
import { runEmbeddingsMigrationIfNeeded } from '../services/embeddings-migration.js';
|
|
18
|
+
import { memoryDbPath, MEMORY_DB_FILE, MOFLO_DIR } from '../services/moflo-paths.js';
|
|
18
19
|
import * as embeddings from '../embeddings/index.js';
|
|
20
|
+
const DEFAULT_DB_PATH_FLAG = `${MOFLO_DIR}/${MEMORY_DB_FILE}`;
|
|
19
21
|
// Generate subcommand - REAL implementation
|
|
20
22
|
const generateCommand = {
|
|
21
23
|
name: 'generate',
|
|
@@ -96,7 +98,7 @@ const searchCommand = {
|
|
|
96
98
|
{ name: 'collection', short: 'c', type: 'string', description: 'Namespace to search', default: 'default' },
|
|
97
99
|
{ name: 'limit', short: 'l', type: 'number', description: 'Max results', default: '10' },
|
|
98
100
|
{ name: 'threshold', short: 't', type: 'number', description: 'Similarity threshold (0-1)', default: '0.5' },
|
|
99
|
-
{ name: 'db-path', type: 'string', description: 'Database path', default:
|
|
101
|
+
{ name: 'db-path', type: 'string', description: 'Database path', default: DEFAULT_DB_PATH_FLAG },
|
|
100
102
|
],
|
|
101
103
|
examples: [
|
|
102
104
|
{ command: 'claude-flow embeddings search -q "error handling"', description: 'Search for similar' },
|
|
@@ -107,7 +109,7 @@ const searchCommand = {
|
|
|
107
109
|
const namespace = ctx.flags.collection || 'default';
|
|
108
110
|
const limit = parseInt(ctx.flags.limit || '10', 10);
|
|
109
111
|
const threshold = parseFloat(ctx.flags.threshold || '0.5');
|
|
110
|
-
const dbPath = ctx.flags['db-path'] ||
|
|
112
|
+
const dbPath = ctx.flags['db-path'] || memoryDbPath(process.cwd());
|
|
111
113
|
if (!query) {
|
|
112
114
|
output.printError('Query is required');
|
|
113
115
|
return { success: false, exitCode: 1 };
|
|
@@ -347,7 +349,7 @@ const collectionsCommand = {
|
|
|
347
349
|
options: [
|
|
348
350
|
{ name: 'action', short: 'a', type: 'string', description: 'Action: list, stats', default: 'list' },
|
|
349
351
|
{ name: 'name', short: 'n', type: 'string', description: 'Namespace name' },
|
|
350
|
-
{ name: 'db-path', type: 'string', description: 'Database path', default:
|
|
352
|
+
{ name: 'db-path', type: 'string', description: 'Database path', default: DEFAULT_DB_PATH_FLAG },
|
|
351
353
|
],
|
|
352
354
|
examples: [
|
|
353
355
|
{ command: 'claude-flow embeddings collections', description: 'List collections' },
|
|
@@ -355,7 +357,7 @@ const collectionsCommand = {
|
|
|
355
357
|
],
|
|
356
358
|
action: async (ctx) => {
|
|
357
359
|
const action = ctx.flags.action || 'list';
|
|
358
|
-
const dbPath = ctx.flags['db-path'] ||
|
|
360
|
+
const dbPath = ctx.flags['db-path'] || memoryDbPath(process.cwd());
|
|
359
361
|
output.writeln();
|
|
360
362
|
output.writeln(output.bold('Embedding Collections (Namespaces)'));
|
|
361
363
|
output.writeln(output.dim('─'.repeat(60)));
|
|
@@ -1489,16 +1491,16 @@ const migrateCommand = {
|
|
|
1489
1491
|
name: 'migrate',
|
|
1490
1492
|
description: 'Re-embed existing vectors using the current neural model (runs automatically on session start if needed)',
|
|
1491
1493
|
options: [
|
|
1492
|
-
{ name: 'db', short: 'd', type: 'string', description: 'Path to memory DB', default:
|
|
1494
|
+
{ name: 'db', short: 'd', type: 'string', description: 'Path to memory DB', default: DEFAULT_DB_PATH_FLAG },
|
|
1493
1495
|
{ name: 'batch-size', short: 'b', type: 'number', description: 'Batch size', default: '128' },
|
|
1494
1496
|
{ name: 'verbose', short: 'v', type: 'boolean', description: 'Verbose output', default: 'false' },
|
|
1495
1497
|
],
|
|
1496
1498
|
examples: [
|
|
1497
|
-
{ command: 'claude-flow embeddings migrate', description:
|
|
1499
|
+
{ command: 'claude-flow embeddings migrate', description: `Migrate ${DEFAULT_DB_PATH_FLAG} to current version` },
|
|
1498
1500
|
{ command: 'claude-flow embeddings migrate -d .custom/mem.db', description: 'Migrate a custom DB' },
|
|
1499
1501
|
],
|
|
1500
1502
|
action: async (ctx) => {
|
|
1501
|
-
const dbPath = ctx.flags.db ||
|
|
1503
|
+
const dbPath = ctx.flags.db || memoryDbPath(process.cwd());
|
|
1502
1504
|
try {
|
|
1503
1505
|
const ran = await runEmbeddingsMigrationIfNeeded({ dbPath });
|
|
1504
1506
|
return { success: true, data: { ran } };
|
|
@@ -7,6 +7,7 @@ import { formatStatus } from '../services/cli-formatters.js';
|
|
|
7
7
|
import { confirm } from '../prompt.js';
|
|
8
8
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
9
9
|
import { storeCommand } from './transfer-store.js';
|
|
10
|
+
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
10
11
|
// Hook types
|
|
11
12
|
const HOOK_TYPES = [
|
|
12
13
|
{ value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
|
|
@@ -2544,10 +2545,7 @@ const statuslineCommand = {
|
|
|
2544
2545
|
const { execSync } = await import('child_process');
|
|
2545
2546
|
// Get learning stats from memory database
|
|
2546
2547
|
function getLearningStats() {
|
|
2547
|
-
const memoryPaths =
|
|
2548
|
-
path.join(process.cwd(), '.swarm', 'memory.db'),
|
|
2549
|
-
path.join(process.cwd(), '.claude', 'memory.db'),
|
|
2550
|
-
];
|
|
2548
|
+
const memoryPaths = memoryDbCandidatePaths(process.cwd());
|
|
2551
2549
|
let patterns = 0;
|
|
2552
2550
|
let sessions = 0;
|
|
2553
2551
|
let trajectories = 0;
|
|
@@ -2798,12 +2796,9 @@ const statuslineCommand = {
|
|
|
2798
2796
|
}
|
|
2799
2797
|
// Get AgentDB stats (matching .claude/helpers/statusline.cjs paths)
|
|
2800
2798
|
const agentdbStats = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
|
|
2801
|
-
// Check for direct database files first
|
|
2799
|
+
// Check for direct database files first. Canonical first, legacies after.
|
|
2802
2800
|
const dbPaths = [
|
|
2803
|
-
|
|
2804
|
-
path.join(process.cwd(), '.moflo', 'memory.db'),
|
|
2805
|
-
path.join(process.cwd(), '.claude', 'memory.db'),
|
|
2806
|
-
path.join(process.cwd(), 'data', 'memory.db'),
|
|
2801
|
+
...memoryDbCandidatePaths(process.cwd()),
|
|
2807
2802
|
path.join(process.cwd(), 'memory.db'),
|
|
2808
2803
|
path.join(process.cwd(), '.agentdb', 'memory.db'),
|
|
2809
2804
|
path.join(process.cwd(), '.moflo', 'memory', 'agentdb.db'),
|
|
@@ -7,14 +7,12 @@ import { select, confirm } from '../prompt.js';
|
|
|
7
7
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
+
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
10
11
|
// Get dynamic swarm status from memory/session files
|
|
11
12
|
function getSwarmStatus(swarmId) {
|
|
12
13
|
const swarmDir = path.join(process.cwd(), '.swarm');
|
|
13
14
|
const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
|
|
14
|
-
const memoryPaths =
|
|
15
|
-
path.join(process.cwd(), '.swarm', 'memory.db'),
|
|
16
|
-
path.join(process.cwd(), '.claude', 'memory.db'),
|
|
17
|
-
];
|
|
15
|
+
const memoryPaths = memoryDbCandidatePaths(process.cwd());
|
|
18
16
|
// Check for active swarm state file
|
|
19
17
|
const swarmStateFile = path.join(swarmDir, 'state.json');
|
|
20
18
|
let swarmState = null;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 2. An `embeddings_migration_cursor` table that persists the resume
|
|
11
11
|
* cursor per-store (so multiple stores sharing a DB file — e.g.
|
|
12
|
-
* `.
|
|
12
|
+
* `.moflo/moflo.db` with both `memory_entries` and `patterns` —
|
|
13
13
|
* each get their own row).
|
|
14
14
|
*
|
|
15
15
|
* The helpers are framework-agnostic: they accept a minimal `SqlJsDatabase`
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
16
|
import { TestDeterministicEmbedding } from './__mocks__/test-embedding-service.js';
|
|
17
|
+
import { MEMORY_DB_FILE, MOFLO_DIR } from '../../services/moflo-paths.js';
|
|
17
18
|
// Dynamic imports for optional dependencies
|
|
18
19
|
let MofloDbAdapter = null;
|
|
19
20
|
let HNSWIndex = null;
|
|
@@ -28,7 +29,7 @@ const DEFAULT_CONFIG = {
|
|
|
28
29
|
promotionThreshold: 3,
|
|
29
30
|
qualityThreshold: 0.6,
|
|
30
31
|
dedupThreshold: 0.95,
|
|
31
|
-
dbPath:
|
|
32
|
+
dbPath: `${MOFLO_DIR}/${MEMORY_DB_FILE}`,
|
|
32
33
|
useMockEmbeddings: false,
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
@@ -8,6 +8,7 @@ import * as path from 'path';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
10
|
import { atomicWriteFileSync } from '../services/atomic-file-write.js';
|
|
11
|
+
import { legacyMemoryDbPath, memoryDbPath, MOFLO_DIR, } from '../services/moflo-paths.js';
|
|
11
12
|
// When run via npx, CWD may be node_modules/moflo — walk up to find actual project
|
|
12
13
|
let _projectRoot;
|
|
13
14
|
/**
|
|
@@ -28,7 +29,10 @@ function getProjectRoot() {
|
|
|
28
29
|
let dir = process.cwd();
|
|
29
30
|
const root = path.parse(dir).root;
|
|
30
31
|
while (dir !== root) {
|
|
31
|
-
|
|
32
|
+
// `.moflo/moflo.db` is the canonical post-#727 marker. Older consumers
|
|
33
|
+
// mid-migration may still only have `.swarm/memory.db`; recognise both
|
|
34
|
+
// so the bridge can find the project root either way.
|
|
35
|
+
if (fs.existsSync(memoryDbPath(dir)) || fs.existsSync(legacyMemoryDbPath(dir))) {
|
|
32
36
|
_projectRoot = dir;
|
|
33
37
|
return _projectRoot;
|
|
34
38
|
}
|
|
@@ -69,22 +73,42 @@ function logBridgeError(context, err) {
|
|
|
69
73
|
const msg = err instanceof Error ? err.message : String(err);
|
|
70
74
|
console.error(`[moflo] ${context}: ${msg}`);
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the on-disk DB path the bridge should read/write.
|
|
78
|
+
*
|
|
79
|
+
* Default to `.moflo/moflo.db`, but during the post-#727 migration window —
|
|
80
|
+
* after a consumer upgrades but before the next session-start launcher fires
|
|
81
|
+
* — prefer `.swarm/memory.db` if only the legacy file exists. Without this
|
|
82
|
+
* preference, any CLI command (e.g. `moflo doctor`) that opens the bridge
|
|
83
|
+
* before the launcher runs would create an empty canonical, defeating the
|
|
84
|
+
* launcher's `target-exists` short-circuit and stranding the user's data in
|
|
85
|
+
* `.swarm/memory.db`.
|
|
86
|
+
*
|
|
87
|
+
* Exported for test access; production callers go through the no-arg
|
|
88
|
+
* `getDbPath()` wrapper below.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveBridgeDbPath(root, customPath) {
|
|
91
|
+
const canonical = memoryDbPath(root);
|
|
92
|
+
if (!customPath) {
|
|
93
|
+
if (!fs.existsSync(canonical) && fs.existsSync(legacyMemoryDbPath(root))) {
|
|
94
|
+
return legacyMemoryDbPath(root);
|
|
95
|
+
}
|
|
96
|
+
return canonical;
|
|
97
|
+
}
|
|
76
98
|
if (customPath === ':memory:')
|
|
77
99
|
return ':memory:';
|
|
78
100
|
const resolved = path.resolve(customPath);
|
|
79
|
-
const root = getProjectRoot();
|
|
80
101
|
const rel = path.relative(root, resolved);
|
|
81
102
|
// Reject anything that escapes the project root or is an absolute path
|
|
82
103
|
// outside it (path.relative returns an absolute path on different drives).
|
|
83
104
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
84
|
-
return
|
|
105
|
+
return canonical;
|
|
85
106
|
}
|
|
86
107
|
return resolved;
|
|
87
108
|
}
|
|
109
|
+
function getDbPath(customPath) {
|
|
110
|
+
return resolveBridgeDbPath(getProjectRoot(), customPath);
|
|
111
|
+
}
|
|
88
112
|
export function generateId(prefix) {
|
|
89
113
|
return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
90
114
|
}
|
|
@@ -163,9 +187,11 @@ export function execRows(db, sql, params) {
|
|
|
163
187
|
* when the process exits, which breaks store→retrieve across CLI commands.
|
|
164
188
|
*/
|
|
165
189
|
export function persistBridgeDb(db, dbPath) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
// Mirror the read-side resolution so writes land where reads come from.
|
|
191
|
+
// Important during the migration window (#727): if we read from
|
|
192
|
+
// `.swarm/memory.db` because the canonical doesn't exist yet, writing back
|
|
193
|
+
// there keeps the legacy file fresh until the launcher relocates it.
|
|
194
|
+
const target = dbPath ? path.resolve(dbPath) : getDbPath();
|
|
169
195
|
if (target === ':memory:')
|
|
170
196
|
return;
|
|
171
197
|
try {
|
|
@@ -286,19 +312,15 @@ export function writeVectorStatsJson(rootDir, stats) {
|
|
|
286
312
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
287
313
|
fs.writeFileSync(path.join(cacheDir, 'vector-stats.json'), JSON.stringify({ ...stats, updatedAt: Date.now() }));
|
|
288
314
|
}
|
|
289
|
-
/** Probe
|
|
315
|
+
/** Probe for the HNSW index sidecar at its canonical post-#727 location. */
|
|
290
316
|
function detectHnswIndex(rootDir) {
|
|
291
|
-
|
|
292
|
-
path.join(rootDir,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
catch { /* nope */ }
|
|
317
|
+
try {
|
|
318
|
+
fs.statSync(path.join(rootDir, MOFLO_DIR, 'hnsw.index'));
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
300
323
|
}
|
|
301
|
-
return false;
|
|
302
324
|
}
|
|
303
325
|
/**
|
|
304
326
|
* Read the existing on-disk vector-stats cache. Returns null when missing
|
|
@@ -17,6 +17,7 @@ import { HnswLite } from './hnsw-lite.js';
|
|
|
17
17
|
import { EMBEDDING_MODEL_OPT_OUT, getBridgeEmbedder } from './bridge-embedder.js';
|
|
18
18
|
import { toFloat32 } from './controllers/_shared.js';
|
|
19
19
|
import { writeVectorStatsJson } from './bridge-core.js';
|
|
20
|
+
import { MOFLO_DIR, hnswIndexPath, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-paths.js';
|
|
20
21
|
/**
|
|
21
22
|
* Write vector-stats.json cache for the statusline (no subprocess needed).
|
|
22
23
|
* Called after memory store in the raw-sql.js fallback path. The bridge path
|
|
@@ -31,19 +32,13 @@ function writeVectorStatsCache(dbPath, stats) {
|
|
|
31
32
|
const dbSizeKB = Math.floor(fileStat.size / 1024);
|
|
32
33
|
const { vectorCount, namespaces, missing = 0 } = stats;
|
|
33
34
|
const dbDir = path.dirname(dbPath);
|
|
34
|
-
const projectDir = path.dirname(dbDir); // .swarm -> project root
|
|
35
|
+
const projectDir = path.dirname(dbDir); // .moflo (or legacy .swarm) -> project root
|
|
35
36
|
let hasHnsw = false;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
]) {
|
|
40
|
-
try {
|
|
41
|
-
fs.statSync(p);
|
|
42
|
-
hasHnsw = true;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
catch { /* nope */ }
|
|
37
|
+
try {
|
|
38
|
+
fs.statSync(hnswIndexPath(projectDir));
|
|
39
|
+
hasHnsw = true;
|
|
46
40
|
}
|
|
41
|
+
catch { /* nope */ }
|
|
47
42
|
writeVectorStatsJson(projectDir, { vectorCount, missing, dbSizeKB, namespaces, hasHnsw });
|
|
48
43
|
}
|
|
49
44
|
catch { /* Non-fatal */ }
|
|
@@ -381,13 +376,13 @@ export async function getHNSWIndex(options) {
|
|
|
381
376
|
hnswInitializing = true;
|
|
382
377
|
try {
|
|
383
378
|
// Use HnswLite pure TS implementation (no native dependencies).
|
|
384
|
-
// Persistent storage paths
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
379
|
+
// Persistent storage paths — colocated with the canonical memory DB.
|
|
380
|
+
const dbPath = options?.dbPath || memoryDbPath(process.cwd());
|
|
381
|
+
const dbDir = path.dirname(dbPath);
|
|
382
|
+
if (!fs.existsSync(dbDir)) {
|
|
383
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
388
384
|
}
|
|
389
|
-
const metadataPath = path.join(
|
|
390
|
-
const dbPath = options?.dbPath || path.join(swarmDir, 'memory.db');
|
|
385
|
+
const metadataPath = path.join(dbDir, 'hnsw.metadata.json');
|
|
391
386
|
// Create HnswLite index and wrap it with the expected db interface
|
|
392
387
|
const hnsw = new HnswLite(dimensions, 16, 200, 'cosine');
|
|
393
388
|
const db = {
|
|
@@ -478,8 +473,7 @@ function saveHNSWMetadata() {
|
|
|
478
473
|
if (!hnswIndex?.entries)
|
|
479
474
|
return;
|
|
480
475
|
try {
|
|
481
|
-
const
|
|
482
|
-
const metadataPath = path.join(swarmDir, 'hnsw.metadata.json');
|
|
476
|
+
const metadataPath = path.join(path.dirname(memoryDbPath(process.cwd())), 'hnsw.metadata.json');
|
|
483
477
|
const metadata = Array.from(hnswIndex.entries.entries());
|
|
484
478
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
|
485
479
|
}
|
|
@@ -893,12 +887,17 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
893
887
|
*/
|
|
894
888
|
export async function checkAndMigrateLegacy(options) {
|
|
895
889
|
const { dbPath, verbose = false } = options;
|
|
896
|
-
// Check for legacy locations
|
|
890
|
+
// Check for legacy locations. `.swarm/memory.db` is the pre-#727 layout
|
|
891
|
+
// primarily handled by the launcher's copy-verify-delete migration; this
|
|
892
|
+
// probe still catches consumers whose launcher migration deferred. The
|
|
893
|
+
// bare `memory.db` and `.claude/memory.db`/`data/memory.db` entries are
|
|
894
|
+
// older still.
|
|
897
895
|
const legacyPaths = [
|
|
896
|
+
legacyMemoryDbPath(process.cwd()),
|
|
897
|
+
path.join(process.cwd(), MOFLO_DIR, 'memory.db'),
|
|
898
898
|
path.join(process.cwd(), 'memory.db'),
|
|
899
|
-
path.join(process.cwd(), '.claude
|
|
900
|
-
path.join(process.cwd(), 'data
|
|
901
|
-
path.join(process.cwd(), '.moflo/memory.db')
|
|
899
|
+
path.join(process.cwd(), '.claude', 'memory.db'),
|
|
900
|
+
path.join(process.cwd(), 'data', 'memory.db'),
|
|
902
901
|
];
|
|
903
902
|
for (const legacyPath of legacyPaths) {
|
|
904
903
|
if (fs.existsSync(legacyPath) && legacyPath !== dbPath) {
|
|
@@ -983,8 +982,7 @@ async function activateControllerRegistry(dbPath, verbose) {
|
|
|
983
982
|
*/
|
|
984
983
|
export async function initializeMemoryDatabase(options) {
|
|
985
984
|
const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
|
|
986
|
-
const
|
|
987
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
985
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
988
986
|
const dbDir = path.dirname(dbPath);
|
|
989
987
|
try {
|
|
990
988
|
// Create directory if needed
|
|
@@ -1165,8 +1163,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1165
1163
|
* Check if memory database is properly initialized
|
|
1166
1164
|
*/
|
|
1167
1165
|
export async function checkMemoryInitialization(dbPath) {
|
|
1168
|
-
const
|
|
1169
|
-
const path_ = dbPath || path.join(swarmDir, 'memory.db');
|
|
1166
|
+
const path_ = dbPath || memoryDbPath(process.cwd());
|
|
1170
1167
|
if (!fs.existsSync(path_)) {
|
|
1171
1168
|
return { initialized: false };
|
|
1172
1169
|
}
|
|
@@ -1214,8 +1211,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
1214
1211
|
* Reduces confidence of patterns that haven't been used recently
|
|
1215
1212
|
*/
|
|
1216
1213
|
export async function applyTemporalDecay(dbPath) {
|
|
1217
|
-
const
|
|
1218
|
-
const path_ = dbPath || path.join(swarmDir, 'memory.db');
|
|
1214
|
+
const path_ = dbPath || memoryDbPath(process.cwd());
|
|
1219
1215
|
try {
|
|
1220
1216
|
const initSqlJs = (await mofloImport('sql.js')).default;
|
|
1221
1217
|
const SQL = await initSqlJs();
|
|
@@ -1579,8 +1575,7 @@ export async function storeEntry(options) {
|
|
|
1579
1575
|
}
|
|
1580
1576
|
// Fallback: raw sql.js
|
|
1581
1577
|
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
|
|
1582
|
-
const
|
|
1583
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
1578
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1584
1579
|
try {
|
|
1585
1580
|
if (!fs.existsSync(dbPath)) {
|
|
1586
1581
|
return { success: false, id: '', error: 'Database not initialized. Run: claude-flow memory init' };
|
|
@@ -1718,8 +1713,7 @@ export async function searchEntries(options) {
|
|
|
1718
1713
|
}
|
|
1719
1714
|
// Fallback: raw sql.js
|
|
1720
1715
|
const { query, namespace = 'default', limit = 10, threshold = 0.3, dbPath: customPath } = options;
|
|
1721
|
-
const
|
|
1722
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
1716
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1723
1717
|
const startTime = Date.now();
|
|
1724
1718
|
try {
|
|
1725
1719
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -1837,8 +1831,7 @@ export async function listEntries(options) {
|
|
|
1837
1831
|
}
|
|
1838
1832
|
// Fallback: raw sql.js
|
|
1839
1833
|
const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
|
|
1840
|
-
const
|
|
1841
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
1834
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1842
1835
|
try {
|
|
1843
1836
|
if (!fs.existsSync(dbPath)) {
|
|
1844
1837
|
return { success: false, entries: [], total: 0, error: 'Database not found' };
|
|
@@ -1906,8 +1899,7 @@ export async function getEntry(options) {
|
|
|
1906
1899
|
}
|
|
1907
1900
|
// Fallback: raw sql.js
|
|
1908
1901
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
1909
|
-
const
|
|
1910
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
1902
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1911
1903
|
try {
|
|
1912
1904
|
if (!fs.existsSync(dbPath)) {
|
|
1913
1905
|
return { success: false, found: false, error: 'Database not found' };
|
|
@@ -1987,8 +1979,7 @@ export async function deleteEntry(options) {
|
|
|
1987
1979
|
}
|
|
1988
1980
|
// Fallback: raw sql.js
|
|
1989
1981
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
1990
|
-
const
|
|
1991
|
-
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
1982
|
+
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1992
1983
|
try {
|
|
1993
1984
|
if (!fs.existsSync(dbPath)) {
|
|
1994
1985
|
return {
|
|
@@ -2065,8 +2056,7 @@ export async function deleteEntry(options) {
|
|
|
2065
2056
|
* Returns { namespaces: Record<string, number>, total: number }.
|
|
2066
2057
|
*/
|
|
2067
2058
|
export async function getNamespaceCounts(dbPath) {
|
|
2068
|
-
const
|
|
2069
|
-
const resolvedPath = dbPath || path.join(swarmDir, 'memory.db');
|
|
2059
|
+
const resolvedPath = dbPath || memoryDbPath(process.cwd());
|
|
2070
2060
|
try {
|
|
2071
2061
|
if (!fs.existsSync(resolvedPath)) {
|
|
2072
2062
|
return { namespaces: {}, total: 0 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idempotent embeddings-version migration for moflo's memory.db.
|
|
2
|
+
* Idempotent embeddings-version migration for moflo's memory DB (`.moflo/moflo.db`).
|
|
3
3
|
*
|
|
4
4
|
* Runs the story-2 driver (`runUpgrade`) with the story-3 UX on any DB whose
|
|
5
5
|
* stored `embeddings_version` is below the current runtime version OR whose
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
18
18
|
import { mofloImport } from './moflo-require.js';
|
|
19
19
|
import { atomicWriteFileSync } from './atomic-file-write.js';
|
|
20
|
+
import { memoryDbPath } from './moflo-paths.js';
|
|
20
21
|
// EMBEDDINGS_VERSION is a number constant in a leaf types module — pulling it
|
|
21
22
|
// eagerly is cheap. The heavy imports (fastembed wrapper, upgrade renderer,
|
|
22
23
|
// etc.) stay deferred behind the early returns so session-start stays fast.
|
|
@@ -33,7 +34,7 @@ import { CANONICAL_EMBEDDING_MODEL, EMBEDDINGS_VERSION, } from '../embeddings/mi
|
|
|
33
34
|
export async function runEmbeddingsMigrationIfNeeded(options = {}) {
|
|
34
35
|
const fs = await import('fs');
|
|
35
36
|
const path = await import('path');
|
|
36
|
-
const dbPath = path.resolve(options.dbPath ??
|
|
37
|
+
const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
|
|
37
38
|
if (!fs.existsSync(dbPath))
|
|
38
39
|
return false;
|
|
39
40
|
const initSqlJs = (await mofloImport('sql.js'))?.default;
|
|
@@ -106,7 +107,7 @@ export async function runEmbeddingsMigrationIfNeeded(options = {}) {
|
|
|
106
107
|
if (summary.status === 'completed') {
|
|
107
108
|
// `db.export()` returns a Uint8Array; `writeFileSync` accepts it directly,
|
|
108
109
|
// so no Buffer.from() copy. Atomic temp-file + rename so SIGINT mid-write
|
|
109
|
-
// cannot truncate
|
|
110
|
+
// cannot truncate moflo.db — see atomic-file-write.ts.
|
|
110
111
|
atomicWriteFileSync(dbPath, db.export());
|
|
111
112
|
options.onMigrationComplete?.(summary.totalItemsMigrated);
|
|
112
113
|
return true;
|
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
* promotion, deduplication, and consolidation.
|
|
7
7
|
*
|
|
8
8
|
* Features:
|
|
9
|
-
* - Pattern storage/search with sql.js backend (.
|
|
9
|
+
* - Pattern storage/search with sql.js backend (.moflo/moflo.db)
|
|
10
10
|
* - Short-term -> long-term pattern promotion (promote after 3 uses)
|
|
11
11
|
* - Quality thresholds: minimum quality 0.6, dedup threshold 0.95
|
|
12
12
|
* - Consolidation: max 500 short-term, 2000 long-term, prune after 30 days
|
|
13
13
|
* - HNSW indexing for fast similarity search
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
16
|
-
import {
|
|
16
|
+
import { dirname } from 'path';
|
|
17
17
|
import { mofloImport } from './moflo-require.js';
|
|
18
18
|
import { atomicWriteFileSync } from './atomic-file-write.js';
|
|
19
|
+
import { memoryDbPath } from './moflo-paths.js';
|
|
19
20
|
import { createNeuralEmbeddingProvider, } from './neural-embedding-provider.js';
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Configuration
|
|
@@ -262,12 +263,9 @@ export class LearningService {
|
|
|
262
263
|
longTermIndex = new HNSWIndex();
|
|
263
264
|
dirty = false;
|
|
264
265
|
dbPath;
|
|
265
|
-
dataDir;
|
|
266
266
|
embedderPromise = null;
|
|
267
267
|
constructor(projectRoot, embedder) {
|
|
268
|
-
|
|
269
|
-
this.dataDir = join(root, '.swarm');
|
|
270
|
-
this.dbPath = join(this.dataDir, 'memory.db');
|
|
268
|
+
this.dbPath = memoryDbPath(projectRoot || process.cwd());
|
|
271
269
|
if (embedder)
|
|
272
270
|
this.embedderPromise = Promise.resolve(embedder);
|
|
273
271
|
}
|
|
@@ -281,8 +279,9 @@ export class LearningService {
|
|
|
281
279
|
async initialize() {
|
|
282
280
|
if (this.db)
|
|
283
281
|
return;
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
const dataDir = dirname(this.dbPath);
|
|
283
|
+
if (!existsSync(dataDir)) {
|
|
284
|
+
mkdirSync(dataDir, { recursive: true });
|
|
286
285
|
}
|
|
287
286
|
const SQL = await loadSqlJs();
|
|
288
287
|
if (existsSync(this.dbPath)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MoFlo runtime state directory constants + legacy
|
|
2
|
+
* MoFlo runtime state directory constants + legacy migrations (#699, #727).
|
|
3
3
|
*
|
|
4
4
|
* MoFlo owns its state under `.moflo/` at the project root. The upstream Ruflo
|
|
5
5
|
* fork used `.claude-flow/`; consumers upgrading from older moflo builds (which
|
|
@@ -16,20 +16,66 @@
|
|
|
16
16
|
* been compiled. The parity test in moflo-paths-migration.test.ts catches
|
|
17
17
|
* algorithm divergence between the two.
|
|
18
18
|
*/
|
|
19
|
-
import { existsSync, readdirSync, renameSync, rmdirSync } from 'node:fs';
|
|
20
|
-
import { join } from 'node:path';
|
|
19
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
21
|
export const MOFLO_DIR = '.moflo';
|
|
22
|
+
/** Canonical memory DB filename (post-#727). Lives at `<root>/.moflo/moflo.db`. */
|
|
23
|
+
export const MEMORY_DB_FILE = 'moflo.db';
|
|
24
|
+
/** HNSW persisted index sidecar. Lives next to the DB at `<root>/.moflo/hnsw.index`. */
|
|
25
|
+
export const HNSW_INDEX_FILE = 'hnsw.index';
|
|
22
26
|
/**
|
|
23
27
|
* Legacy runtime directory inherited from upstream Ruflo. Only referenced from
|
|
24
28
|
* migration code paths — production code should use {@link MOFLO_DIR}.
|
|
25
29
|
*/
|
|
26
30
|
export const LEGACY_CLAUDE_FLOW_DIR = '.claude-flow';
|
|
31
|
+
/** Legacy `.swarm/` directory used by Ruflo + pre-#727 moflo for the memory DB. */
|
|
32
|
+
export const LEGACY_SWARM_DIR = '.swarm';
|
|
33
|
+
/** Legacy memory DB filename — only ever inside `.swarm/`. Pre-#727. */
|
|
34
|
+
export const LEGACY_MEMORY_DB_FILE = 'memory.db';
|
|
35
|
+
/** Suffix appended to `.swarm/memory.db` once migrated, retained one upgrade cycle. */
|
|
36
|
+
export const LEGACY_MEMORY_DB_BAK_SUFFIX = '.bak';
|
|
27
37
|
export function mofloDir(projectRoot) {
|
|
28
38
|
return join(projectRoot, MOFLO_DIR);
|
|
29
39
|
}
|
|
30
40
|
export function legacyClaudeFlowDir(projectRoot) {
|
|
31
41
|
return join(projectRoot, LEGACY_CLAUDE_FLOW_DIR);
|
|
32
42
|
}
|
|
43
|
+
/** Canonical memory DB path: `<root>/.moflo/moflo.db`. */
|
|
44
|
+
export function memoryDbPath(projectRoot) {
|
|
45
|
+
return join(projectRoot, MOFLO_DIR, MEMORY_DB_FILE);
|
|
46
|
+
}
|
|
47
|
+
/** Canonical HNSW index sidecar path: `<root>/.moflo/hnsw.index`. */
|
|
48
|
+
export function hnswIndexPath(projectRoot) {
|
|
49
|
+
return join(projectRoot, MOFLO_DIR, HNSW_INDEX_FILE);
|
|
50
|
+
}
|
|
51
|
+
/** Legacy memory DB path: `<root>/.swarm/memory.db`. Migration source only. */
|
|
52
|
+
export function legacyMemoryDbPath(projectRoot) {
|
|
53
|
+
return join(projectRoot, LEGACY_SWARM_DIR, LEGACY_MEMORY_DB_FILE);
|
|
54
|
+
}
|
|
55
|
+
/** Legacy HNSW index path: `<root>/.swarm/hnsw.index`. Migration source only. */
|
|
56
|
+
export function legacyHnswIndexPath(projectRoot) {
|
|
57
|
+
return join(projectRoot, LEGACY_SWARM_DIR, HNSW_INDEX_FILE);
|
|
58
|
+
}
|
|
59
|
+
/** Backup sentinel kept for one upgrade cycle: `<root>/.swarm/memory.db.bak`. */
|
|
60
|
+
export function legacyMemoryDbBakPath(projectRoot) {
|
|
61
|
+
return join(projectRoot, LEGACY_SWARM_DIR, `${LEGACY_MEMORY_DB_FILE}${LEGACY_MEMORY_DB_BAK_SUFFIX}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Memory-DB probe order used by every reader that does best-effort detection
|
|
65
|
+
* (statusline, doctor, swarm status, hooks aggregator). Canonical first so
|
|
66
|
+
* the early-break stops at the post-#727 location; legacy paths kept so a
|
|
67
|
+
* partially-migrated consumer still surfaces a result.
|
|
68
|
+
*
|
|
69
|
+
* Keep in sync with the pure-JS twin in `bin/lib/moflo-paths.mjs`.
|
|
70
|
+
*/
|
|
71
|
+
export function memoryDbCandidatePaths(projectRoot) {
|
|
72
|
+
return [
|
|
73
|
+
memoryDbPath(projectRoot),
|
|
74
|
+
legacyMemoryDbPath(projectRoot),
|
|
75
|
+
join(projectRoot, 'data', LEGACY_MEMORY_DB_FILE),
|
|
76
|
+
join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
33
79
|
/**
|
|
34
80
|
* One-time migration of `.claude-flow/` → `.moflo/`.
|
|
35
81
|
*
|
|
@@ -80,4 +126,134 @@ export function migrateClaudeFlowToMoflo(projectRoot) {
|
|
|
80
126
|
}
|
|
81
127
|
return moved > 0 ? { migrated: true } : { migrated: false, reason: 'merged-nothing' };
|
|
82
128
|
}
|
|
129
|
+
const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
|
|
130
|
+
function looksLikeSqliteFile(filePath) {
|
|
131
|
+
let fd = null;
|
|
132
|
+
try {
|
|
133
|
+
fd = openSync(filePath, 'r');
|
|
134
|
+
const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
|
|
135
|
+
const read = readSync(fd, buf, 0, buf.length, 0);
|
|
136
|
+
if (read < SQLITE_MAGIC_HEADER.length)
|
|
137
|
+
return false;
|
|
138
|
+
return buf.equals(SQLITE_MAGIC_HEADER);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (fd !== null)
|
|
145
|
+
try {
|
|
146
|
+
closeSync(fd);
|
|
147
|
+
}
|
|
148
|
+
catch { /* non-fatal */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function verifyByteEqual(srcPath, dstPath) {
|
|
152
|
+
try {
|
|
153
|
+
const srcStat = statSync(srcPath);
|
|
154
|
+
const dstStat = statSync(dstPath);
|
|
155
|
+
if (srcStat.size !== dstStat.size)
|
|
156
|
+
return false;
|
|
157
|
+
const srcBuf = readFileSync(srcPath);
|
|
158
|
+
const dstBuf = readFileSync(dstPath);
|
|
159
|
+
return srcBuf.equals(dstBuf);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function tryUnlink(filePath) {
|
|
166
|
+
try {
|
|
167
|
+
unlinkSync(filePath);
|
|
168
|
+
}
|
|
169
|
+
catch { /* non-fatal */ }
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Move `.swarm/hnsw.index` → `.moflo/hnsw.index` using the same
|
|
173
|
+
* copy-verify-delete pattern as the DB. Returns true on success, false when
|
|
174
|
+
* source absent or copy/verify failed (caller treats as best-effort).
|
|
175
|
+
*/
|
|
176
|
+
function migrateHnswIndex(projectRoot) {
|
|
177
|
+
const src = legacyHnswIndexPath(projectRoot);
|
|
178
|
+
const dst = hnswIndexPath(projectRoot);
|
|
179
|
+
if (!existsSync(src))
|
|
180
|
+
return false;
|
|
181
|
+
if (existsSync(dst))
|
|
182
|
+
return false; // already there — leave both alone
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
185
|
+
copyFileSync(src, dst);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
tryUnlink(dst);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (!verifyByteEqual(src, dst)) {
|
|
192
|
+
tryUnlink(dst);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
tryUnlink(src); // sidecar can be regenerated; no .bak retention needed
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* One-time relocation of the memory DB from the upstream `.swarm/memory.db`
|
|
200
|
+
* layout to the canonical `.moflo/moflo.db` (story #727).
|
|
201
|
+
*
|
|
202
|
+
* Algorithm — copy-verify-delete, never `mv`:
|
|
203
|
+
* 1. If `.moflo/moflo.db` exists → no-op (`target-exists`).
|
|
204
|
+
* 2. If `.swarm/memory.db` absent → no-op (`no-legacy`).
|
|
205
|
+
* 3. Ensure `.moflo/` exists.
|
|
206
|
+
* 4. `copyFileSync(.swarm/memory.db, .moflo/moflo.db)`.
|
|
207
|
+
* 5. Verify byte-equal (size + content) AND SQLite header magic.
|
|
208
|
+
* 6. Move `.swarm/hnsw.index` → `.moflo/hnsw.index` (best-effort).
|
|
209
|
+
* 7. Rename `.swarm/memory.db` → `.swarm/memory.db.bak` (kept one upgrade cycle).
|
|
210
|
+
*
|
|
211
|
+
* Any failure between 4–7 deletes the partial target and returns failure;
|
|
212
|
+
* next session-start retries.
|
|
213
|
+
*
|
|
214
|
+
* MUST run BEFORE any long-lived consumer (MCP server, daemon) opens the DB —
|
|
215
|
+
* sql.js holds a full snapshot in RAM and would clobber the relocated file
|
|
216
|
+
* on its next flush. The launcher's section before the fire-and-forget block
|
|
217
|
+
* is the safe boundary; see feedback memory `feedback_sqljs_writeback_clobber`.
|
|
218
|
+
*
|
|
219
|
+
* Idempotent.
|
|
220
|
+
*/
|
|
221
|
+
export function migrateMemoryDbToMoflo(projectRoot) {
|
|
222
|
+
const target = memoryDbPath(projectRoot);
|
|
223
|
+
if (existsSync(target))
|
|
224
|
+
return { migrated: false, reason: 'target-exists' };
|
|
225
|
+
const source = legacyMemoryDbPath(projectRoot);
|
|
226
|
+
if (!existsSync(source))
|
|
227
|
+
return { migrated: false, reason: 'no-legacy' };
|
|
228
|
+
try {
|
|
229
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return { migrated: false, reason: 'copy-failed' };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
copyFileSync(source, target);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
tryUnlink(target);
|
|
239
|
+
return { migrated: false, reason: 'copy-failed' };
|
|
240
|
+
}
|
|
241
|
+
if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
|
|
242
|
+
tryUnlink(target);
|
|
243
|
+
return { migrated: false, reason: 'verify-failed' };
|
|
244
|
+
}
|
|
245
|
+
const hnswMoved = migrateHnswIndex(projectRoot);
|
|
246
|
+
// Final step: retire the source by renaming to .bak. Only after the new
|
|
247
|
+
// file is verified — never lose data. If this rename fails we leave the
|
|
248
|
+
// source in place; a stale `.swarm/memory.db` next to a healthy
|
|
249
|
+
// `.moflo/moflo.db` is harmless (the bridge reads only the new path) and
|
|
250
|
+
// surfaces as a `flo doctor` warning.
|
|
251
|
+
try {
|
|
252
|
+
renameSync(source, legacyMemoryDbBakPath(projectRoot));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return { migrated: true, reason: 'rename-failed', hnswMoved };
|
|
256
|
+
}
|
|
257
|
+
return { migrated: true, hnswMoved };
|
|
258
|
+
}
|
|
83
259
|
//# sourceMappingURL=moflo-paths.js.map
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.0-rc.
|
|
3
|
+
"version": "4.9.0-rc.2",
|
|
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/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
79
79
|
"@typescript-eslint/parser": "^7.18.0",
|
|
80
80
|
"eslint": "^8.0.0",
|
|
81
|
-
"moflo": "^4.
|
|
81
|
+
"moflo": "^4.9.0-rc.1",
|
|
82
82
|
"tsx": "^4.21.0",
|
|
83
83
|
"typescript": "^5.9.3",
|
|
84
84
|
"vitest": "^4.0.0"
|