moflo 4.0.3 → 4.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -59,7 +59,8 @@ const preEditCommand = {
59
59
  output.printInfo(`Analyzing context for: ${output.highlight(filePath)}`);
60
60
  try {
61
61
  // Call MCP tool for pre-edit hook
62
- const result = await callMCPTool('hooks_pre-edit', {
62
+ // Wrap MCP call in timeout to prevent indefinite hang (#13)
63
+ const mcpPromise = callMCPTool('hooks_pre-edit', {
63
64
  filePath,
64
65
  operation,
65
66
  context: ctx.flags.context,
@@ -183,6 +184,8 @@ const postEditCommand = {
183
184
  metrics,
184
185
  timestamp: Date.now(),
185
186
  });
187
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('session-end timed out after 3s')), 3000));
188
+ const result = await Promise.race([mcpPromise, timeoutPromise]);
186
189
  if (ctx.flags.format === 'json') {
187
190
  output.printJson(result);
188
191
  return { success: true, data: result };
@@ -17,11 +17,10 @@
17
17
  * @module v3/cli/memory-bridge
18
18
  */
19
19
  import * as path from 'path';
20
- import * as fs from 'fs';
21
20
  import * as crypto from 'crypto';
22
- import { createRequire } from 'module';
23
- import { pathToFileURL } from 'url';
24
- // Project root resolution — handles npx running from node_modules/moflo
21
+ // ===== Project root resolution =====
22
+ // When run via npx, CWD may be node_modules/moflo — walk up to find actual project
23
+ import * as fs from 'fs';
25
24
  let _projectRoot;
26
25
  function getProjectRoot() {
27
26
  if (_projectRoot)
@@ -41,11 +40,41 @@ function getProjectRoot() {
41
40
  _projectRoot = dir;
42
41
  return _projectRoot;
43
42
  }
43
+ // Skip node_modules directories
44
+ if (path.basename(dir) === 'node_modules') {
45
+ dir = path.dirname(dir);
46
+ continue;
47
+ }
44
48
  dir = path.dirname(dir);
45
49
  }
46
50
  _projectRoot = process.cwd();
47
51
  return _projectRoot;
48
52
  }
53
+ // ===== Transformers.js fallback embedder =====
54
+ let _tfEmbedder = null;
55
+ let _tfFailed = false;
56
+ async function getFallbackEmbedder() {
57
+ if (_tfFailed)
58
+ return null;
59
+ if (_tfEmbedder)
60
+ return _tfEmbedder;
61
+ try {
62
+ const { pipeline } = await import('@xenova/transformers');
63
+ _tfEmbedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
64
+ return _tfEmbedder;
65
+ }
66
+ catch {
67
+ _tfFailed = true;
68
+ return null;
69
+ }
70
+ }
71
+ async function fallbackEmbed(text) {
72
+ const embedder = await getFallbackEmbedder();
73
+ if (!embedder)
74
+ return null;
75
+ const result = await embedder(text, { pooling: 'mean', normalize: true });
76
+ return Array.from(result.data);
77
+ }
49
78
  // ===== Lazy singleton =====
50
79
  let registryPromise = null;
51
80
  let registryInstance = null;
@@ -56,15 +85,15 @@ let bridgeAvailable = null;
56
85
  * or the special ':memory:' path.
57
86
  */
58
87
  function getDbPath(customPath) {
59
- const projectRoot = getProjectRoot();
60
- const swarmDir = path.resolve(projectRoot, '.swarm');
88
+ const swarmDir = path.resolve(getProjectRoot(), '.swarm');
61
89
  if (!customPath)
62
90
  return path.join(swarmDir, 'memory.db');
63
91
  if (customPath === ':memory:')
64
92
  return ':memory:';
65
93
  const resolved = path.resolve(customPath);
66
- // Ensure the path doesn't escape the project directory
67
- if (!resolved.startsWith(projectRoot)) {
94
+ // Ensure the path doesn't escape the working directory
95
+ const cwd = getProjectRoot();
96
+ if (!resolved.startsWith(cwd)) {
68
97
  return path.join(swarmDir, 'memory.db'); // fallback to safe default
69
98
  }
70
99
  return resolved;
@@ -76,10 +105,8 @@ function generateId(prefix) {
76
105
  return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
77
106
  }
78
107
  /**
79
- * Lazily initialize the ControllerRegistry singleton via @claude-flow/memory.
80
- * Falls back to direct agentdb import if @claude-flow/memory is unavailable.
81
- * The registry exposes .get(name), .getAgentDB(), .listControllers(), and .initialize()
82
- * to maintain compatibility with all bridge consumers.
108
+ * Lazily initialize the ControllerRegistry singleton.
109
+ * Returns null if @claude-flow/memory is not available.
83
110
  */
84
111
  async function getRegistry(dbPath) {
85
112
  if (bridgeAvailable === false)
@@ -89,81 +116,13 @@ async function getRegistry(dbPath) {
89
116
  if (!registryPromise) {
90
117
  registryPromise = (async () => {
91
118
  try {
92
- // Try @claude-flow/memory ControllerRegistry first (bundled in moflo)
93
- let memoryModule = null;
94
- const memoryRelPath = '../../../../memory/dist/index.js';
95
- try {
96
- memoryModule = await import(memoryRelPath);
97
- }
98
- catch {
99
- try {
100
- const req = createRequire(path.join(getProjectRoot(), 'package.json'));
101
- const resolved = req.resolve('@claude-flow/memory');
102
- memoryModule = await import(pathToFileURL(resolved).href);
103
- }
104
- catch {
105
- // @claude-flow/memory not available
106
- }
107
- }
108
- // If @claude-flow/memory has ControllerRegistry, use it directly
109
- if (memoryModule?.ControllerRegistry) {
110
- const { ControllerRegistry } = memoryModule;
111
- const registry = new ControllerRegistry();
112
- // Suppress noisy console.log during init
113
- const origLog = console.log;
114
- console.log = (...args) => {
115
- const msg = String(args[0] ?? '');
116
- if (msg.includes('Transformers.js') ||
117
- msg.includes('better-sqlite3') ||
118
- msg.includes('[AgentDB]') ||
119
- msg.includes('[HNSWLibBackend]') ||
120
- msg.includes('RuVector graph'))
121
- return;
122
- origLog.apply(console, args);
123
- };
124
- try {
125
- await registry.initialize({
126
- dbPath: dbPath || getDbPath(),
127
- dimension: 384,
128
- });
129
- }
130
- finally {
131
- console.log = origLog;
132
- }
133
- registryInstance = registry;
134
- return registry;
135
- }
136
- // Fallback: direct agentdb import with manual adapter
137
- let agentdbModule = null;
138
- try {
139
- agentdbModule = await import('agentdb');
140
- }
141
- catch {
142
- try {
143
- const req = createRequire(path.join(getProjectRoot(), 'package.json'));
144
- const resolved = req.resolve('agentdb');
145
- agentdbModule = await import(pathToFileURL(resolved).href);
146
- }
147
- catch {
148
- // Not available
149
- }
150
- }
151
- if (!agentdbModule) {
152
- bridgeAvailable = false;
153
- registryPromise = null;
154
- return null;
155
- }
156
- const { AgentDB } = agentdbModule;
157
- const agentdb = new AgentDB({
158
- dbPath: dbPath || getDbPath(),
159
- dimension: 384,
160
- });
119
+ const { ControllerRegistry } = await import('@claude-flow/memory');
120
+ const registry = new ControllerRegistry();
161
121
  // Suppress noisy console.log during init
162
122
  const origLog = console.log;
163
123
  console.log = (...args) => {
164
124
  const msg = String(args[0] ?? '');
165
125
  if (msg.includes('Transformers.js') ||
166
- msg.includes('better-sqlite3') ||
167
126
  msg.includes('[AgentDB]') ||
168
127
  msg.includes('[HNSWLibBackend]') ||
169
128
  msg.includes('RuVector graph'))
@@ -171,38 +130,22 @@ async function getRegistry(dbPath) {
171
130
  origLog.apply(console, args);
172
131
  };
173
132
  try {
174
- await agentdb.initialize();
133
+ await registry.initialize({
134
+ dbPath: dbPath || getDbPath(),
135
+ dimension: 384,
136
+ controllers: {
137
+ reasoningBank: true,
138
+ learningBridge: false,
139
+ tieredCache: true,
140
+ hierarchicalMemory: true,
141
+ memoryConsolidation: true,
142
+ memoryGraph: true, // issue #1214: enable MemoryGraph for graph-aware ranking
143
+ },
144
+ });
175
145
  }
176
146
  finally {
177
147
  console.log = origLog;
178
148
  }
179
- // Build a registry-compatible adapter over AgentDB
180
- const registry = {
181
- _agentdb: agentdb,
182
- get(name) {
183
- try {
184
- return agentdb.getController(name);
185
- }
186
- catch {
187
- return null;
188
- }
189
- },
190
- getAgentDB() {
191
- return agentdb;
192
- },
193
- listControllers() {
194
- return [
195
- 'reflexion', 'skills', 'reasoningBank',
196
- 'causalGraph', 'causalRecall', 'learningSystem',
197
- 'explainableRecall', 'nightlyLearner',
198
- 'graphTransformer', 'mutationGuard',
199
- 'attestationLog', 'vectorBackend'
200
- ].filter(name => {
201
- try { return agentdb.getController(name) != null; }
202
- catch { return false; }
203
- });
204
- },
205
- };
206
149
  registryInstance = registry;
207
150
  bridgeAvailable = true;
208
151
  return registry;
@@ -352,25 +295,25 @@ function getDb(registry) {
352
295
  const db = agentdb.database;
353
296
  // Ensure memory_entries table exists (idempotent)
354
297
  try {
355
- db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
356
- id TEXT PRIMARY KEY,
357
- key TEXT NOT NULL,
358
- namespace TEXT DEFAULT 'default',
359
- content TEXT NOT NULL,
360
- type TEXT DEFAULT 'semantic',
361
- embedding TEXT,
362
- embedding_model TEXT DEFAULT 'local',
363
- embedding_dimensions INTEGER,
364
- tags TEXT,
365
- metadata TEXT,
366
- owner_id TEXT,
367
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
368
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
369
- expires_at INTEGER,
370
- last_accessed_at INTEGER,
371
- access_count INTEGER DEFAULT 0,
372
- status TEXT DEFAULT 'active',
373
- UNIQUE(namespace, key)
298
+ db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
299
+ id TEXT PRIMARY KEY,
300
+ key TEXT NOT NULL,
301
+ namespace TEXT DEFAULT 'default',
302
+ content TEXT NOT NULL,
303
+ type TEXT DEFAULT 'semantic',
304
+ embedding TEXT,
305
+ embedding_model TEXT DEFAULT 'local',
306
+ embedding_dimensions INTEGER,
307
+ tags TEXT,
308
+ metadata TEXT,
309
+ owner_id TEXT,
310
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
311
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
312
+ expires_at INTEGER,
313
+ last_accessed_at INTEGER,
314
+ access_count INTEGER DEFAULT 0,
315
+ status TEXT DEFAULT 'active',
316
+ UNIQUE(namespace, key)
374
317
  )`);
375
318
  // Ensure indexes
376
319
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
@@ -424,17 +367,17 @@ export async function bridgeStoreEntry(options) {
424
367
  // Embedding failed — store without
425
368
  }
426
369
  }
427
- // better-sqlite3 uses synchronous .run() with positional params
370
+ // sql.js uses synchronous .run() with positional params
428
371
  const insertSql = options.upsert
429
- ? `INSERT OR REPLACE INTO memory_entries (
430
- id, key, namespace, content, type,
431
- embedding, embedding_dimensions, embedding_model,
432
- tags, metadata, created_at, updated_at, expires_at, status
372
+ ? `INSERT OR REPLACE INTO memory_entries (
373
+ id, key, namespace, content, type,
374
+ embedding, embedding_dimensions, embedding_model,
375
+ tags, metadata, created_at, updated_at, expires_at, status
433
376
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
434
- : `INSERT INTO memory_entries (
435
- id, key, namespace, content, type,
436
- embedding, embedding_dimensions, embedding_model,
437
- tags, metadata, created_at, updated_at, expires_at, status
377
+ : `INSERT INTO memory_entries (
378
+ id, key, namespace, content, type,
379
+ embedding, embedding_dimensions, embedding_model,
380
+ tags, metadata, created_at, updated_at, expires_at, status
438
381
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
439
382
  const stmt = ctx.db.prepare(insertSql);
440
383
  stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
@@ -485,17 +428,17 @@ export async function bridgeSearchEntries(options) {
485
428
  catch {
486
429
  // Fall back to keyword search
487
430
  }
488
- // better-sqlite3: .prepare().all() returns array of objects
431
+ // Prepare/all returns array of objects
489
432
  const nsFilter = namespace !== 'all'
490
433
  ? `AND namespace = ?`
491
434
  : '';
492
435
  let rows;
493
436
  try {
494
- const stmt = ctx.db.prepare(`
495
- SELECT id, key, namespace, content, embedding
496
- FROM memory_entries
497
- WHERE status = 'active' ${nsFilter}
498
- LIMIT 1000
437
+ const stmt = ctx.db.prepare(`
438
+ SELECT id, key, namespace, content, embedding
439
+ FROM memory_entries
440
+ WHERE status = 'active' ${nsFilter}
441
+ LIMIT 1000
499
442
  `);
500
443
  rows = namespace !== 'all' ? stmt.all(namespace) : stmt.all();
501
444
  }
@@ -585,12 +528,12 @@ export async function bridgeListEntries(options) {
585
528
  // List
586
529
  const entries = [];
587
530
  try {
588
- const stmt = ctx.db.prepare(`
589
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
590
- FROM memory_entries
591
- WHERE status = 'active' ${nsFilter}
592
- ORDER BY updated_at DESC
593
- LIMIT ? OFFSET ?
531
+ const stmt = ctx.db.prepare(`
532
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
533
+ FROM memory_entries
534
+ WHERE status = 'active' ${nsFilter}
535
+ ORDER BY updated_at DESC
536
+ LIMIT ? OFFSET ?
594
537
  `);
595
538
  const rows = stmt.all(...nsParams, limit, offset);
596
539
  for (const row of rows) {
@@ -653,11 +596,11 @@ export async function bridgeGetEntry(options) {
653
596
  }
654
597
  let row;
655
598
  try {
656
- const stmt = ctx.db.prepare(`
657
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
658
- FROM memory_entries
659
- WHERE status = 'active' AND key = ? AND namespace = ?
660
- LIMIT 1
599
+ const stmt = ctx.db.prepare(`
600
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
601
+ FROM memory_entries
602
+ WHERE status = 'active' AND key = ? AND namespace = ?
603
+ LIMIT 1
661
604
  `);
662
605
  row = stmt.get(key, namespace);
663
606
  }
@@ -721,10 +664,10 @@ export async function bridgeDeleteEntry(options) {
721
664
  // Soft delete using parameterized query
722
665
  let changes = 0;
723
666
  try {
724
- const result = ctx.db.prepare(`
725
- UPDATE memory_entries
726
- SET status = 'deleted', updated_at = ?
727
- WHERE key = ? AND namespace = ? AND status = 'active'
667
+ const result = ctx.db.prepare(`
668
+ UPDATE memory_entries
669
+ SET status = 'deleted', updated_at = ?
670
+ WHERE key = ? AND namespace = ? AND status = 'active'
728
671
  `).run(Date.now(), key, namespace);
729
672
  changes = result?.changes ?? 0;
730
673
  }
@@ -869,11 +812,11 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
869
812
  : '';
870
813
  let rows;
871
814
  try {
872
- const stmt = ctx.db.prepare(`
873
- SELECT id, key, namespace, content, embedding
874
- FROM memory_entries
875
- WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
876
- LIMIT 10000
815
+ const stmt = ctx.db.prepare(`
816
+ SELECT id, key, namespace, content, embedding
817
+ FROM memory_entries
818
+ WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
819
+ LIMIT 10000
877
820
  `);
878
821
  rows = nsFilter
879
822
  ? stmt.all(options.namespace)
@@ -925,12 +868,12 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
925
868
  try {
926
869
  const now = Date.now();
927
870
  const embeddingJson = JSON.stringify(embedding);
928
- ctx.db.prepare(`
929
- INSERT OR REPLACE INTO memory_entries (
930
- id, key, namespace, content, type,
931
- embedding, embedding_dimensions, embedding_model,
932
- created_at, updated_at, status
933
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
871
+ ctx.db.prepare(`
872
+ INSERT OR REPLACE INTO memory_entries (
873
+ id, key, namespace, content, type,
874
+ embedding, embedding_dimensions, embedding_model,
875
+ created_at, updated_at, status
876
+ ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
934
877
  `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, now, now);
935
878
  return true;
936
879
  }
@@ -1198,9 +1141,9 @@ export async function bridgeRecordCausalEdge(options) {
1198
1141
  const ctx = getDb(registry);
1199
1142
  if (ctx) {
1200
1143
  try {
1201
- ctx.db.prepare(`
1202
- INSERT OR REPLACE INTO memory_entries (id, key, namespace, content, type, created_at, updated_at, status)
1203
- VALUES (?, ?, 'causal-edges', ?, 'procedural', ?, ?, 'active')
1144
+ ctx.db.prepare(`
1145
+ INSERT OR REPLACE INTO memory_entries (id, key, namespace, content, type, created_at, updated_at, status)
1146
+ VALUES (?, ?, 'causal-edges', ?, 'procedural', ?, ?, 'active')
1204
1147
  `).run(generateId('edge'), `${options.sourceId}→${options.targetId}`, JSON.stringify(options), Date.now(), Date.now());
1205
1148
  return { success: true, controller: 'bridge-fallback' };
1206
1149
  }
@@ -10,6 +10,7 @@
10
10
  * @module @claude-flow/memory/controller-registry
11
11
  */
12
12
  import { EventEmitter } from 'node:events';
13
+ import * as path from 'node:path';
13
14
  import { LearningBridge } from './learning-bridge.js';
14
15
  import { MemoryGraph } from './memory-graph.js';
15
16
  import { TieredCacheManager } from './cache-manager.js';
@@ -290,7 +291,7 @@ export class ControllerRegistry extends EventEmitter {
290
291
  // Validate dbPath to prevent path traversal
291
292
  const dbPath = config.dbPath || ':memory:';
292
293
  if (dbPath !== ':memory:') {
293
- const resolved = require('path').resolve(dbPath);
294
+ const resolved = path.resolve(dbPath);
294
295
  if (resolved.includes('..')) {
295
296
  this.emit('agentdb:unavailable', { reason: 'Invalid dbPath' });
296
297
  return;
@@ -309,7 +310,6 @@ export class ControllerRegistry extends EventEmitter {
309
310
  const suppressFilter = (args) => {
310
311
  const msg = String(args[0] ?? '');
311
312
  return msg.includes('Transformers.js') ||
312
- msg.includes('better-sqlite3') ||
313
313
  msg.includes('[AgentDB]');
314
314
  };
315
315
  console.log = (...args) => {
@@ -12,7 +12,7 @@ import { IMemoryBackend } from './types.js';
12
12
  /**
13
13
  * Available database provider types
14
14
  */
15
- export type DatabaseProvider = 'better-sqlite3' | 'sql.js' | 'json' | 'rvf' | 'auto';
15
+ export type DatabaseProvider = 'sql.js' | 'json' | 'rvf' | 'auto';
16
16
  /**
17
17
  * Database creation options
18
18
  */
@@ -10,7 +10,6 @@
10
10
  */
11
11
  import { platform } from 'node:os';
12
12
  import { existsSync } from 'node:fs';
13
- import { SQLiteBackend } from './sqlite-backend.js';
14
13
  import { SqlJsBackend } from './sqljs-backend.js';
15
14
  /**
16
15
  * Detect platform and recommend provider
@@ -21,7 +20,7 @@ function detectPlatform() {
21
20
  const isMacOS = os === 'darwin';
22
21
  const isLinux = os === 'linux';
23
22
  // Recommend better-sqlite3 for Unix-like systems, sql.js for Windows
24
- const recommendedProvider = isWindows ? 'sql.js' : 'better-sqlite3';
23
+ const recommendedProvider = 'sql.js';
25
24
  return {
26
25
  os,
27
26
  isWindows,
@@ -36,19 +35,9 @@ function detectPlatform() {
36
35
  async function testRvf() {
37
36
  return true;
38
37
  }
39
- /**
40
- * Test if better-sqlite3 is available and working
41
- */
38
+ /** better-sqlite3 removed — always returns false */
42
39
  async function testBetterSqlite3() {
43
- try {
44
- const Database = (await import('better-sqlite3')).default;
45
- const testDb = new Database(':memory:');
46
- testDb.close();
47
- return true;
48
- }
49
- catch (error) {
50
- return false;
51
- }
40
+ return false;
52
41
  }
53
42
  /**
54
43
  * Test if sql.js is available and working
@@ -87,19 +76,7 @@ async function selectProvider(preferred, verbose = false) {
87
76
  }
88
77
  return 'rvf';
89
78
  }
90
- // Try recommended provider
91
- if (platformInfo.recommendedProvider === 'better-sqlite3') {
92
- if (await testBetterSqlite3()) {
93
- if (verbose) {
94
- console.log('[DatabaseProvider] better-sqlite3 available and working');
95
- }
96
- return 'better-sqlite3';
97
- }
98
- else if (verbose) {
99
- console.log('[DatabaseProvider] better-sqlite3 not available, trying sql.js');
100
- }
101
- }
102
- // Try sql.js as fallback
79
+ // Try sql.js (moflo: sql.js is the only SQLite backend)
103
80
  if (await testSqlJs()) {
104
81
  if (verbose) {
105
82
  console.log('[DatabaseProvider] sql.js available and working');
@@ -147,18 +124,6 @@ export async function createDatabase(path, options = {}) {
147
124
  }
148
125
  let backend;
149
126
  switch (selectedProvider) {
150
- case 'better-sqlite3': {
151
- const config = {
152
- databasePath: path,
153
- walMode,
154
- optimize,
155
- defaultNamespace,
156
- maxEntries,
157
- verbose,
158
- };
159
- backend = new SQLiteBackend(config);
160
- break;
161
- }
162
127
  case 'sql.js': {
163
128
  const config = {
164
129
  databasePath: path,
@@ -33,7 +33,7 @@ describe('DatabaseProvider', () => {
33
33
  expect(info.recommendedProvider).toBe('sql.js');
34
34
  }
35
35
  else {
36
- expect(info.recommendedProvider).toBe('better-sqlite3');
36
+ expect(info.recommendedProvider).toBe('sql.js');
37
37
  }
38
38
  });
39
39
  });
@@ -100,7 +100,7 @@ describe('DatabaseProvider', () => {
100
100
  return;
101
101
  }
102
102
  const db = await createDatabase(':memory:', {
103
- provider: 'better-sqlite3',
103
+ provider: 'sql.js',
104
104
  verbose: false,
105
105
  });
106
106
  expect(db).toBeDefined();
@@ -142,7 +142,7 @@ describe('DatabaseProvider', () => {
142
142
  const available = await getAvailableProviders();
143
143
  const providers = [];
144
144
  if (available.betterSqlite3)
145
- providers.push('better-sqlite3');
145
+ providers.push('sql.js');
146
146
  if (available.sqlJs)
147
147
  providers.push('sql.js');
148
148
  providers.push('json'); // Always available
@@ -63,7 +63,6 @@ export { AgentDBAdapter } from './agentdb-adapter.js';
63
63
  export type { AgentDBAdapterConfig } from './agentdb-adapter.js';
64
64
  export { AgentDBBackend } from './agentdb-backend.js';
65
65
  export type { AgentDBBackendConfig } from './agentdb-backend.js';
66
- export { SQLiteBackend } from './sqlite-backend.js';
67
66
  export type { SQLiteBackendConfig } from './sqlite-backend.js';
68
67
  export { SqlJsBackend } from './sqljs-backend.js';
69
68
  export type { SqlJsBackendConfig } from './sqljs-backend.js';
@@ -61,7 +61,6 @@ export { ControllerRegistry, INIT_LEVELS } from './controller-registry.js';
61
61
  // ===== Core Components =====
62
62
  export { AgentDBAdapter } from './agentdb-adapter.js';
63
63
  export { AgentDBBackend } from './agentdb-backend.js';
64
- export { SQLiteBackend } from './sqlite-backend.js';
65
64
  export { SqlJsBackend } from './sqljs-backend.js';
66
65
  export { HybridBackend } from './hybrid-backend.js';
67
66
  export { RvfBackend } from './rvf-backend.js';
@@ -90,38 +90,18 @@ function normalizeSqliteRow(row) {
90
90
  return out;
91
91
  }
92
92
  async function readSqliteRows(dbPath) {
93
- // Attempt better-sqlite3
93
+ // Use sql.js (WASM) for SQLite reading
94
94
  try {
95
- const mod = await import('better-sqlite3');
96
- const Database = mod.default ?? mod;
97
- const db = new Database(dbPath, { readonly: true });
98
- try {
99
- return db.prepare('SELECT * FROM memory_entries').all();
100
- }
101
- finally {
102
- db.close();
103
- }
95
+ const initSqlJs = (await import('sql.js')).default;
96
+ const SQL = await initSqlJs();
97
+ const fs = await import('node:fs');
98
+ const buf = fs.readFileSync(dbPath);
99
+ const db = new SQL.Database(buf);
100
+ return { exec: (sql) => db.exec(sql), close: () => db.close() };
104
101
  }
105
- catch { /* unavailable */ }
106
- // Attempt sql.js
107
- try {
108
- const mod = await import('sql.js');
109
- const SQL = await (mod.default ?? mod)();
110
- const db = new SQL.Database(await readFile(dbPath));
111
- try {
112
- const stmt = db.prepare('SELECT * FROM memory_entries');
113
- const rows = [];
114
- while (stmt.step())
115
- rows.push(stmt.getAsObject());
116
- stmt.free();
117
- return rows;
118
- }
119
- finally {
120
- db.close();
121
- }
102
+ catch {
103
+ throw new Error('Cannot read SQLite: install sql.js');
122
104
  }
123
- catch { /* unavailable */ }
124
- throw new Error('Cannot read SQLite: install better-sqlite3 or sql.js');
125
105
  }
126
106
  // -- Batch migration helper -------------------------------------------------
127
107
  async function migrateBatches(items, rvfPath, options, normalize) {
@@ -8,7 +8,15 @@
8
8
  * @module v3/memory/sqlite-backend
9
9
  */
10
10
  import { EventEmitter } from 'node:events';
11
- import Database from 'better-sqlite3';
11
+ // Lazy import better-sqlite3 is optional (requires native build tools)
12
+ let Database = null;
13
+ async function loadBetterSqlite3() {
14
+ if (!Database) {
15
+ const mod = await import('better-sqlite3');
16
+ Database = mod.default;
17
+ }
18
+ return Database;
19
+ }
12
20
  /**
13
21
  * Default configuration values
14
22
  */
@@ -91,19 +99,19 @@ export class SQLiteBackend extends EventEmitter {
91
99
  async store(entry) {
92
100
  this.ensureInitialized();
93
101
  const startTime = performance.now();
94
- const stmt = this.db.prepare(`
95
- INSERT OR REPLACE INTO memory_entries (
96
- id, key, content, type, namespace, tags, metadata,
97
- owner_id, access_level, created_at, updated_at, expires_at,
98
- version, "references", access_count, last_accessed_at
99
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
102
+ const stmt = this.db.prepare(`
103
+ INSERT OR REPLACE INTO memory_entries (
104
+ id, key, content, type, namespace, tags, metadata,
105
+ owner_id, access_level, created_at, updated_at, expires_at,
106
+ version, "references", access_count, last_accessed_at
107
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
100
108
  `);
101
109
  stmt.run(entry.id, entry.key, entry.content, entry.type, entry.namespace, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.ownerId || null, entry.accessLevel, entry.createdAt, entry.updatedAt, entry.expiresAt || null, entry.version, JSON.stringify(entry.references), entry.accessCount, entry.lastAccessedAt);
102
110
  // Store embedding separately (as BLOB)
103
111
  if (entry.embedding) {
104
- const embeddingStmt = this.db.prepare(`
105
- INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding)
106
- VALUES (?, ?)
112
+ const embeddingStmt = this.db.prepare(`
113
+ INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding)
114
+ VALUES (?, ?)
107
115
  `);
108
116
  embeddingStmt.run(entry.id, Buffer.from(entry.embedding.buffer));
109
117
  }
@@ -128,9 +136,9 @@ export class SQLiteBackend extends EventEmitter {
128
136
  */
129
137
  async getByKey(namespace, key) {
130
138
  this.ensureInitialized();
131
- const stmt = this.db.prepare(`
132
- SELECT * FROM memory_entries
133
- WHERE namespace = ? AND key = ?
139
+ const stmt = this.db.prepare(`
140
+ SELECT * FROM memory_entries
141
+ WHERE namespace = ? AND key = ?
134
142
  `);
135
143
  const row = stmt.get(namespace, key);
136
144
  if (!row)
@@ -244,9 +252,9 @@ export class SQLiteBackend extends EventEmitter {
244
252
  }
245
253
  // Use parameterized query with JSON functions
246
254
  const tagPlaceholders = query.tags.map(() => '?').join(', ');
247
- sql += ` AND EXISTS (
248
- SELECT 1 FROM json_each(tags) AS t
249
- WHERE t.value IN (${tagPlaceholders})
255
+ sql += ` AND EXISTS (
256
+ SELECT 1 FROM json_each(tags) AS t
257
+ WHERE t.value IN (${tagPlaceholders})
250
258
  )`;
251
259
  params.push(...query.tags);
252
260
  }
@@ -342,9 +350,9 @@ export class SQLiteBackend extends EventEmitter {
342
350
  const deleteEntries = this.db.prepare('DELETE FROM memory_entries WHERE namespace = ?');
343
351
  const result = deleteEntries.run(namespace);
344
352
  // Clean up orphaned embeddings
345
- this.db.prepare(`
346
- DELETE FROM memory_embeddings
347
- WHERE entry_id NOT IN (SELECT id FROM memory_entries)
353
+ this.db.prepare(`
354
+ DELETE FROM memory_embeddings
355
+ WHERE entry_id NOT IN (SELECT id FROM memory_entries)
348
356
  `).run();
349
357
  return result.changes;
350
358
  }
@@ -354,10 +362,10 @@ export class SQLiteBackend extends EventEmitter {
354
362
  async getStats() {
355
363
  this.ensureInitialized();
356
364
  // Count by namespace
357
- const namespaceStmt = this.db.prepare(`
358
- SELECT namespace, COUNT(*) as count
359
- FROM memory_entries
360
- GROUP BY namespace
365
+ const namespaceStmt = this.db.prepare(`
366
+ SELECT namespace, COUNT(*) as count
367
+ FROM memory_entries
368
+ GROUP BY namespace
361
369
  `);
362
370
  const namespaceRows = namespaceStmt.all();
363
371
  const entriesByNamespace = {};
@@ -365,10 +373,10 @@ export class SQLiteBackend extends EventEmitter {
365
373
  entriesByNamespace[row.namespace] = row.count;
366
374
  }
367
375
  // Count by type
368
- const typeStmt = this.db.prepare(`
369
- SELECT type, COUNT(*) as count
370
- FROM memory_entries
371
- GROUP BY type
376
+ const typeStmt = this.db.prepare(`
377
+ SELECT type, COUNT(*) as count
378
+ FROM memory_entries
379
+ GROUP BY type
372
380
  `);
373
381
  const typeRows = typeStmt.all();
374
382
  const entriesByType = {
@@ -475,40 +483,40 @@ export class SQLiteBackend extends EventEmitter {
475
483
  if (!this.db)
476
484
  return;
477
485
  // Main entries table
478
- this.db.exec(`
479
- CREATE TABLE IF NOT EXISTS memory_entries (
480
- id TEXT PRIMARY KEY,
481
- key TEXT NOT NULL,
482
- content TEXT NOT NULL,
483
- type TEXT NOT NULL,
484
- namespace TEXT NOT NULL,
485
- tags TEXT NOT NULL,
486
- metadata TEXT NOT NULL,
487
- owner_id TEXT,
488
- access_level TEXT NOT NULL,
489
- created_at INTEGER NOT NULL,
490
- updated_at INTEGER NOT NULL,
491
- expires_at INTEGER,
492
- version INTEGER NOT NULL,
493
- "references" TEXT NOT NULL,
494
- access_count INTEGER NOT NULL,
495
- last_accessed_at INTEGER NOT NULL
496
- );
497
-
498
- CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace);
499
- CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key);
500
- CREATE INDEX IF NOT EXISTS idx_namespace_key ON memory_entries(namespace, key);
501
- CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type);
502
- CREATE INDEX IF NOT EXISTS idx_owner_id ON memory_entries(owner_id);
503
- CREATE INDEX IF NOT EXISTS idx_created_at ON memory_entries(created_at);
504
- CREATE INDEX IF NOT EXISTS idx_updated_at ON memory_entries(updated_at);
505
- CREATE INDEX IF NOT EXISTS idx_expires_at ON memory_entries(expires_at);
506
-
507
- CREATE TABLE IF NOT EXISTS memory_embeddings (
508
- entry_id TEXT PRIMARY KEY,
509
- embedding BLOB,
510
- FOREIGN KEY (entry_id) REFERENCES memory_entries(id) ON DELETE CASCADE
511
- );
486
+ this.db.exec(`
487
+ CREATE TABLE IF NOT EXISTS memory_entries (
488
+ id TEXT PRIMARY KEY,
489
+ key TEXT NOT NULL,
490
+ content TEXT NOT NULL,
491
+ type TEXT NOT NULL,
492
+ namespace TEXT NOT NULL,
493
+ tags TEXT NOT NULL,
494
+ metadata TEXT NOT NULL,
495
+ owner_id TEXT,
496
+ access_level TEXT NOT NULL,
497
+ created_at INTEGER NOT NULL,
498
+ updated_at INTEGER NOT NULL,
499
+ expires_at INTEGER,
500
+ version INTEGER NOT NULL,
501
+ "references" TEXT NOT NULL,
502
+ access_count INTEGER NOT NULL,
503
+ last_accessed_at INTEGER NOT NULL
504
+ );
505
+
506
+ CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace);
507
+ CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key);
508
+ CREATE INDEX IF NOT EXISTS idx_namespace_key ON memory_entries(namespace, key);
509
+ CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type);
510
+ CREATE INDEX IF NOT EXISTS idx_owner_id ON memory_entries(owner_id);
511
+ CREATE INDEX IF NOT EXISTS idx_created_at ON memory_entries(created_at);
512
+ CREATE INDEX IF NOT EXISTS idx_updated_at ON memory_entries(updated_at);
513
+ CREATE INDEX IF NOT EXISTS idx_expires_at ON memory_entries(expires_at);
514
+
515
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
516
+ entry_id TEXT PRIMARY KEY,
517
+ embedding BLOB,
518
+ FOREIGN KEY (entry_id) REFERENCES memory_entries(id) ON DELETE CASCADE
519
+ );
512
520
  `);
513
521
  }
514
522
  rowToEntry(row) {
@@ -543,18 +551,18 @@ export class SQLiteBackend extends EventEmitter {
543
551
  * Synchronous store for use in transactions
544
552
  */
545
553
  storeSync(entry) {
546
- const stmt = this.db.prepare(`
547
- INSERT OR REPLACE INTO memory_entries (
548
- id, key, content, type, namespace, tags, metadata,
549
- owner_id, access_level, created_at, updated_at, expires_at,
550
- version, "references", access_count, last_accessed_at
551
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
554
+ const stmt = this.db.prepare(`
555
+ INSERT OR REPLACE INTO memory_entries (
556
+ id, key, content, type, namespace, tags, metadata,
557
+ owner_id, access_level, created_at, updated_at, expires_at,
558
+ version, "references", access_count, last_accessed_at
559
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
552
560
  `);
553
561
  stmt.run(entry.id, entry.key, entry.content, entry.type, entry.namespace, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.ownerId || null, entry.accessLevel, entry.createdAt, entry.updatedAt, entry.expiresAt || null, entry.version, JSON.stringify(entry.references), entry.accessCount, entry.lastAccessedAt);
554
562
  if (entry.embedding) {
555
- const embeddingStmt = this.db.prepare(`
556
- INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding)
557
- VALUES (?, ?)
563
+ const embeddingStmt = this.db.prepare(`
564
+ INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding)
565
+ VALUES (?, ?)
558
566
  `);
559
567
  embeddingStmt.run(entry.id, Buffer.from(entry.embedding.buffer));
560
568
  }
@@ -1,46 +1,42 @@
1
- {
2
- "name": "@claude-flow/memory",
3
- "version": "3.0.0-alpha.11",
4
- "type": "module",
5
- "description": "Memory module - AgentDB unification, HNSW indexing, vector search, hybrid SQLite+AgentDB backend (ADR-009)",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": "./dist/index.js",
10
- "./*": "./dist/*.js"
11
- },
12
- "scripts": {
13
- "test": "vitest run",
14
- "bench": "vitest bench",
15
- "build": "tsc"
16
- },
17
- "dependencies": {
18
- "agentdb": "^3.0.0-alpha.10",
19
- "sql.js": "^1.10.3"
20
- },
21
- "optionalDependencies": {
22
- "better-sqlite3": "^11.0.0"
23
- },
24
- "devDependencies": {
25
- "@types/better-sqlite3": "^7.6.11",
26
- "@types/sql.js": "^1.4.9",
27
- "vitest": "^4.0.16"
28
- },
29
- "files": [
30
- "dist",
31
- "README.md"
32
- ],
33
- "publishConfig": {
34
- "access": "public",
35
- "tag": "v3alpha"
36
- },
37
- "os": [
38
- "darwin",
39
- "linux",
40
- "win32"
41
- ],
42
- "cpu": [
43
- "x64",
44
- "arm64"
45
- ]
46
- }
1
+ {
2
+ "name": "@claude-flow/memory",
3
+ "version": "3.0.0-alpha.11",
4
+ "type": "module",
5
+ "description": "Memory module - AgentDB unification, HNSW indexing, vector search, hybrid SQLite+AgentDB backend (ADR-009)",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./*": "./dist/*.js"
11
+ },
12
+ "scripts": {
13
+ "test": "vitest run",
14
+ "bench": "vitest bench",
15
+ "build": "tsc"
16
+ },
17
+ "dependencies": {
18
+ "agentdb": "^3.0.0-alpha.10",
19
+ "sql.js": "^1.10.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/sql.js": "^1.4.9",
23
+ "vitest": "^4.0.16"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "tag": "v3alpha"
32
+ },
33
+ "os": [
34
+ "darwin",
35
+ "linux",
36
+ "win32"
37
+ ],
38
+ "cpu": [
39
+ "x64",
40
+ "arm64"
41
+ ]
42
+ }