moflo 4.8.80 → 4.8.82

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.8.80",
3
+ "version": "4.8.82",
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",
@@ -117,6 +117,7 @@
117
117
  "@types/node": "^24.12.2",
118
118
  "@xenova/transformers": "^2.17.0",
119
119
  "eslint": "^8.0.0",
120
+ "moflo": "^4.8.81",
120
121
  "tsx": "^4.21.0",
121
122
  "typescript": "^5.9.3",
122
123
  "vitest": "^4.0.0"
@@ -409,6 +409,54 @@ export async function checkMcpSpellIntegration() {
409
409
  }
410
410
  }
411
411
  // ============================================================================
412
+ // MofloDb Bridge Check
413
+ // ============================================================================
414
+ /**
415
+ * Verify the moflodb bridge (v3 ControllerRegistry) actually loads and
416
+ * returns real controllers. If it fails, every moflodb_* MCP tool degrades
417
+ * to a stub response.
418
+ */
419
+ export async function checkMofloDbBridge() {
420
+ try {
421
+ const modulePath = findModule('src/modules/cli/dist/src/memory/memory-bridge.js');
422
+ if (!modulePath) {
423
+ return { name: 'MofloDb Bridge', status: 'warn', message: 'memory-bridge module not found', fix: 'npm run build' };
424
+ }
425
+ const bridge = await import(toImportUrl(modulePath));
426
+ const health = await bridge.bridgeHealthCheck?.();
427
+ if (!health) {
428
+ const err = bridge.getBridgeLastError?.();
429
+ const reason = err?.message ? err.message.slice(0, 200) : 'bridge unavailable';
430
+ return {
431
+ name: 'MofloDb Bridge',
432
+ status: 'fail',
433
+ message: `init failed: ${reason}`,
434
+ fix: 'Check that sql.js and @moflo/memory are installed; rebuild: npm run build',
435
+ };
436
+ }
437
+ const controllers = Array.isArray(health.controllers) ? health.controllers : [];
438
+ const required = bridge.REQUIRED_BRIDGE_CONTROLLERS ?? [];
439
+ const present = new Set(controllers.map((c) => c.name));
440
+ const missing = required.filter(r => !present.has(r));
441
+ if (missing.length > 0) {
442
+ return {
443
+ name: 'MofloDb Bridge',
444
+ status: 'warn',
445
+ message: `loaded but missing controllers: ${missing.join(', ')}`,
446
+ };
447
+ }
448
+ return {
449
+ name: 'MofloDb Bridge',
450
+ status: 'pass',
451
+ message: `${controllers.length} controllers loaded`,
452
+ };
453
+ }
454
+ catch (err) {
455
+ const msg = err instanceof Error ? err.message.split(/\r?\n/)[0] : String(err);
456
+ return { name: 'MofloDb Bridge', status: 'fail', message: `check error: ${msg}`, fix: 'npm run build' };
457
+ }
458
+ }
459
+ // ============================================================================
412
460
  // Gate Health Check
413
461
  // ============================================================================
414
462
  /** Required gate cases that must exist in gate.cjs for full enforcement. */
@@ -13,7 +13,7 @@ import { execSync, exec } from 'child_process';
13
13
  import { promisify } from 'util';
14
14
  import os from 'os';
15
15
  import { getDaemonLockHolder, releaseDaemonLock, isDaemonProcess } from '../services/daemon-lock.js';
16
- import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, getMofloRoot, } from './doctor-checks-deep.js';
16
+ import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkMofloDbBridge, getMofloRoot, } from './doctor-checks-deep.js';
17
17
  import { repairHookWiring } from '../services/hook-wiring.js';
18
18
  // Promisified exec with proper shell and env inheritance for cross-platform support
19
19
  const execAsync = promisify(exec);
@@ -1394,6 +1394,7 @@ export const doctorCommand = {
1394
1394
  checkMcpSpellIntegration,
1395
1395
  checkHookExecution,
1396
1396
  checkGateHealth,
1397
+ checkMofloDbBridge,
1397
1398
  checkSandboxTier,
1398
1399
  ];
1399
1400
  const componentMap = {
@@ -1429,6 +1430,8 @@ export const doctorCommand = {
1429
1430
  'gate': checkGateHealth,
1430
1431
  'sandbox': checkSandboxTier,
1431
1432
  'sandbox-tier': checkSandboxTier,
1433
+ 'moflodb': checkMofloDbBridge,
1434
+ 'bridge': checkMofloDbBridge,
1432
1435
  };
1433
1436
  let checksToRun = allChecks;
1434
1437
  if (component && componentMap[component]) {
@@ -116,15 +116,40 @@ steps:
116
116
  timeout: 600000
117
117
  failOnError: true
118
118
 
119
- # 2b: Check off story in epic body
119
+ # 2b: Flip `- [ ] #<story>` to `- [x] #<story>` on the epic body.
120
+ # The github `comment` action only appends a comment — it does NOT
121
+ # touch the task-list checkboxes. We read the body, substitute, and
122
+ # write it back so the epic reflects progress at a glance.
120
123
  - id: check-off-story
121
- type: github
124
+ type: bash
125
+ permissionLevel: elevated
126
+ config:
127
+ command: |
128
+ set -e
129
+ STORY={loop.story_number}
130
+ EPIC={args.epic_number}
131
+ BODY=$(gh issue view "$EPIC" --json body -q .body)
132
+ UPDATED=$(printf '%s' "$BODY" | sed -E "s/- \[ \] #${STORY}\b/- [x] #${STORY}/")
133
+ if [ "$BODY" != "$UPDATED" ]; then
134
+ printf '%s' "$UPDATED" | gh issue edit "$EPIC" --body-file -
135
+ fi
136
+ timeout: 60000
137
+ failOnError: true
138
+
139
+ # 2c: Close the story issue explicitly. Single-branch defers to the
140
+ # consolidated PR's Closes-keywords on merge; if the PR never lands,
141
+ # stories would stay OPEN forever. Closing here makes the issue state
142
+ # track the commit, not the eventual merge.
143
+ - id: close-story
144
+ type: bash
145
+ permissionLevel: elevated
122
146
  config:
123
- action: "comment"
124
- issue: "{args.epic_number}"
125
- body: "Story #{loop.story_number} committed to epic branch"
147
+ command: |
148
+ gh issue close {loop.story_number} --reason completed --comment "Closed by epic #{args.epic_number}: commits landed on epic branch."
149
+ timeout: 60000
150
+ failOnError: true
126
151
 
127
- # 2c: Record completion in memory
152
+ # 2d: Record completion in memory
128
153
  - id: record-completion
129
154
  type: memory
130
155
  config:
@@ -149,17 +174,29 @@ steps:
149
174
  timeout: 120000
150
175
  failOnError: true
151
176
 
152
- # Step 4: Create consolidated PR
177
+ # Step 4: Build the 'Closes #<story>' fragment for the PR body so that
178
+ # merging the consolidated PR auto-closes each individual story issue,
179
+ # not just the epic. Without this, only the epic would close on merge
180
+ # and story issues would linger open indefinitely.
181
+ - id: build-closes-list
182
+ type: bash
183
+ config:
184
+ command: |
185
+ echo {args.stories} | tr ',' '\n' | awk 'NF {printf "Closes #%s ", $0}'
186
+ timeout: 30000
187
+ failOnError: true
188
+
189
+ # Step 5: Create consolidated PR
153
190
  - id: create-pr
154
191
  type: github
155
192
  config:
156
193
  action: "pr-create"
157
194
  title: "epic(#{args.epic_number}): consolidated epic PR"
158
- body: "## Epic #{args.epic_number}\n\nConsolidated PR for all stories.\n\nCloses #{args.epic_number}"
195
+ body: "## Epic #{args.epic_number}\n\nConsolidated PR for all stories.\n\n{build-closes-list.stdout} Closes #{args.epic_number}"
159
196
  base: "{args.base_branch}"
160
197
  head: "epic/{args.epic_number}-{args.epic_slug}"
161
198
 
162
- # Step 5: Mark epic as completed in memory
199
+ # Step 6: Mark epic as completed in memory
163
200
  - id: finalize-state
164
201
  type: memory
165
202
  config:
@@ -47,6 +47,17 @@ async function getBridge() {
47
47
  }
48
48
  return bridgeModule;
49
49
  }
50
+ /**
51
+ * When the bridge returns null, surface the underlying init error instead of
52
+ * the generic "bridge not available" stub.
53
+ */
54
+ async function bridgeUnavailableReason(fallback) {
55
+ const bridge = await getBridge();
56
+ const err = bridge.getBridgeLastError?.();
57
+ if (err)
58
+ return `MofloDb bridge init failed: ${sanitizeError(err)}`;
59
+ return fallback;
60
+ }
50
61
  // ===== moflodb_health — Controller health check =====
51
62
  export const moflodbHealth = {
52
63
  name: 'moflodb_health',
@@ -59,8 +70,9 @@ export const moflodbHealth = {
59
70
  try {
60
71
  const bridge = await getBridge();
61
72
  const health = await bridge.bridgeHealthCheck();
62
- if (!health)
63
- return { available: false, error: 'MofloDb bridge not available' };
73
+ if (!health) {
74
+ return { available: false, error: await bridgeUnavailableReason('MofloDb bridge not available') };
75
+ }
64
76
  return health;
65
77
  }
66
78
  catch (error) {
@@ -80,8 +92,13 @@ export const moflodbControllers = {
80
92
  try {
81
93
  const bridge = await getBridge();
82
94
  const controllers = await bridge.bridgeListControllers();
83
- if (!controllers)
84
- return { available: false, controllers: [], error: 'MofloDb bridge not available — @moflo/memory not installed or missing controller-registry. Use memory_store/memory_search tools instead.' };
95
+ if (!controllers) {
96
+ return {
97
+ available: false,
98
+ controllers: [],
99
+ error: await bridgeUnavailableReason('MofloDb bridge not available — controllers could not be listed. Use memory_store/memory_search tools instead.'),
100
+ };
101
+ }
85
102
  return {
86
103
  available: true,
87
104
  controllers,
@@ -36,11 +36,30 @@ function getProjectRoot() {
36
36
  _projectRoot = process.cwd();
37
37
  return _projectRoot;
38
38
  }
39
+ import { importMofloMemory } from '../services/moflo-require.js';
39
40
  let registryPromise = null;
40
41
  // Sync handle populated once the promise resolves. Lets sync callers
41
42
  // (refreshVectorStatsCache) read the registry without awaiting.
42
43
  let resolvedRegistry = null;
44
+ let lastBridgeError = null;
43
45
  const schemaInitialized = new WeakSet();
46
+ /** Controllers every moflodb_* MCP tool assumes are present when the bridge is available. */
47
+ export const REQUIRED_BRIDGE_CONTROLLERS = Object.freeze([
48
+ 'hierarchicalMemory',
49
+ 'tieredCache',
50
+ 'memoryConsolidation',
51
+ 'memoryGraph',
52
+ ]);
53
+ /** Last error thrown during bridge init, or null after a successful init. */
54
+ export function getBridgeLastError() {
55
+ return lastBridgeError;
56
+ }
57
+ function logBridgeError(context, err) {
58
+ if (process.env.MOFLO_BRIDGE_QUIET)
59
+ return;
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ console.error(`[moflo] ${context}: ${msg}`);
62
+ }
44
63
  function getDbPath(customPath) {
45
64
  const swarmDir = path.resolve(getProjectRoot(), '.swarm');
46
65
  if (!customPath)
@@ -68,7 +87,7 @@ export async function getRegistry(dbPath) {
68
87
  if (!registryPromise) {
69
88
  registryPromise = (async () => {
70
89
  try {
71
- const { ControllerRegistry } = await import('@moflo/memory');
90
+ const { ControllerRegistry } = await importMofloMemory(import.meta.url);
72
91
  const registry = new ControllerRegistry();
73
92
  // Suppress noisy init logs
74
93
  const origLog = console.log;
@@ -98,9 +117,12 @@ export async function getRegistry(dbPath) {
98
117
  console.log = origLog;
99
118
  }
100
119
  resolvedRegistry = registry;
120
+ lastBridgeError = null;
101
121
  return registry;
102
122
  }
103
- catch {
123
+ catch (err) {
124
+ lastBridgeError = err instanceof Error ? err : new Error(String(err));
125
+ logBridgeError('MofloDb bridge init failed', lastBridgeError);
104
126
  registryPromise = null;
105
127
  return null;
106
128
  }
@@ -108,6 +130,45 @@ export async function getRegistry(dbPath) {
108
130
  }
109
131
  return registryPromise;
110
132
  }
133
+ /**
134
+ * Read rows from sql.js as an array of column-keyed objects. sql.js doesn't
135
+ * have a `.all()` / `.get()` → object API — the native `Statement.get()`
136
+ * returns a positional array, and `.all()` doesn't exist at all. This is a
137
+ * thin wrapper around `db.exec(sql, bindings)` that converts the
138
+ * `{ columns, values }` shape into objects.
139
+ */
140
+ export function execRows(db, sql, params) {
141
+ const result = params && params.length > 0 ? db.exec(sql, params) : db.exec(sql);
142
+ if (!result || result.length === 0)
143
+ return [];
144
+ const { columns, values } = result[0];
145
+ return values.map((row) => {
146
+ const obj = {};
147
+ for (let i = 0; i < columns.length; i++)
148
+ obj[columns[i]] = row[i];
149
+ return obj;
150
+ });
151
+ }
152
+ /**
153
+ * Persist the in-memory sql.js DB back to disk. sql.js is purely in-memory —
154
+ * without an explicit export+writeFileSync after each mutation, writes vanish
155
+ * when the process exits, which breaks store→retrieve across CLI commands.
156
+ */
157
+ export function persistBridgeDb(db, dbPath) {
158
+ const target = dbPath
159
+ ? path.resolve(dbPath)
160
+ : path.join(getProjectRoot(), '.swarm', 'memory.db');
161
+ if (target === ':memory:')
162
+ return;
163
+ try {
164
+ const data = db.export();
165
+ fs.mkdirSync(path.dirname(target), { recursive: true });
166
+ fs.writeFileSync(target, Buffer.from(data));
167
+ }
168
+ catch (err) {
169
+ logBridgeError('bridge persist failed', err);
170
+ }
171
+ }
111
172
  // Kept in sync with MEMORY_SCHEMA_V3.memory_entries in memory-initializer.ts.
112
173
  // Running `CREATE TABLE IF NOT EXISTS` is a no-op if the initializer already
113
174
  // ran; when the bridge runs first, matching CHECKs here prevents drift.
@@ -152,7 +213,8 @@ export function getDb(registry) {
152
213
  }
153
214
  /**
154
215
  * Resolve registry + db, run fn, return null on any unexpected failure so
155
- * the caller falls back to raw sql.js.
216
+ * the caller falls back to raw sql.js. Errors are logged to stderr —
217
+ * silently swallowing them previously masked real bugs in bridge-entries.ts.
156
218
  */
157
219
  export async function withDb(dbPath, fn) {
158
220
  const registry = await getRegistry(dbPath);
@@ -164,7 +226,8 @@ export async function withDb(dbPath, fn) {
164
226
  try {
165
227
  return await fn(ctx, registry);
166
228
  }
167
- catch {
229
+ catch (err) {
230
+ logBridgeError('bridge operation failed', err);
168
231
  return null;
169
232
  }
170
233
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module v3/cli/bridge-entries
9
9
  */
10
- import { cosineSim, generateId, refreshVectorStatsCache, withDb } from './bridge-core.js';
10
+ import { cosineSim, execRows, generateId, persistBridgeDb, refreshVectorStatsCache, withDb } from './bridge-core.js';
11
11
  function makeEntryCacheKey(namespace, key) {
12
12
  const safeNs = String(namespace).replace(/:/g, '_');
13
13
  const safeKey = String(key).replace(/:/g, '_');
@@ -34,7 +34,7 @@ function computeTermDocFreqs(queryTerms, rows) {
34
34
  const termDocFreqs = new Map();
35
35
  let totalLength = 0;
36
36
  for (const row of rows) {
37
- const content = (row.content || '').toLowerCase();
37
+ const content = String(row.content || '').toLowerCase();
38
38
  const words = content.split(/\s+/);
39
39
  totalLength += words.length;
40
40
  for (const term of queryTerms) {
@@ -123,8 +123,17 @@ export async function bridgeStoreEntry(options) {
123
123
  embedding, embedding_dimensions, embedding_model,
124
124
  tags, metadata, created_at, updated_at, expires_at, status
125
125
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
126
+ // sql.js Statement.run takes an array of bindings — not varargs.
126
127
  const stmt = ctx.db.prepare(insertSql);
127
- stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
128
+ stmt.run([
129
+ id, key, namespace, value,
130
+ embeddingJson, dimensions || null, model,
131
+ tags.length > 0 ? JSON.stringify(tags) : null,
132
+ '{}',
133
+ now, now,
134
+ ttl ? now + (ttl * 1000) : null,
135
+ ]);
136
+ persistBridgeDb(ctx.db, options.dbPath);
128
137
  const cacheKey = makeEntryCacheKey(namespace, key);
129
138
  await cacheSet(registry, cacheKey, { id, key, namespace, content: value, embedding: embeddingJson });
130
139
  await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
@@ -161,13 +170,13 @@ export async function bridgeSearchEntries(options) {
161
170
  const nsFilter = namespace !== 'all' ? `AND namespace = ?` : '';
162
171
  let rows;
163
172
  try {
164
- const stmt = ctx.db.prepare(`
173
+ const sql = `
165
174
  SELECT id, key, namespace, content, embedding
166
175
  FROM memory_entries
167
176
  WHERE status = 'active' ${nsFilter}
168
177
  LIMIT 1000
169
- `);
170
- rows = namespace !== 'all' ? stmt.all(namespace) : stmt.all();
178
+ `;
179
+ rows = namespace !== 'all' ? execRows(ctx.db, sql, [namespace]) : execRows(ctx.db, sql);
171
180
  }
172
181
  catch {
173
182
  return null;
@@ -179,17 +188,18 @@ export async function bridgeSearchEntries(options) {
179
188
  for (const row of rows) {
180
189
  let semanticScore = 0;
181
190
  let bm25ScoreVal = 0;
191
+ const rowContent = String(row.content || '');
182
192
  if (queryEmbedding && row.embedding) {
183
193
  try {
184
- const embedding = JSON.parse(row.embedding);
194
+ const embedding = JSON.parse(String(row.embedding));
185
195
  semanticScore = cosineSim(queryEmbedding, embedding);
186
196
  }
187
197
  catch {
188
198
  // Invalid embedding
189
199
  }
190
200
  }
191
- if (queryTerms.length > 0 && row.content) {
192
- bm25ScoreVal = bm25Score(queryTerms, row.content, avgDocLength, docCount, termDocFreqs);
201
+ if (queryTerms.length > 0 && rowContent) {
202
+ bm25ScoreVal = bm25Score(queryTerms, rowContent, avgDocLength, docCount, termDocFreqs);
193
203
  bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
194
204
  }
195
205
  const usedSemantic = queryEmbedding != null;
@@ -200,10 +210,10 @@ export async function bridgeSearchEntries(options) {
200
210
  : `bm25:${bm25ScoreVal.toFixed(3)}`;
201
211
  results.push({
202
212
  id: String(row.id).substring(0, 12),
203
- key: row.key || String(row.id).substring(0, 15),
204
- content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
213
+ key: String(row.key || row.id).substring(0, 15),
214
+ content: rowContent.substring(0, 60) + (rowContent.length > 60 ? '...' : ''),
205
215
  score,
206
- namespace: row.namespace || 'default',
216
+ namespace: String(row.namespace || 'default'),
207
217
  provenance,
208
218
  });
209
219
  }
@@ -224,30 +234,26 @@ export async function bridgeListEntries(options) {
224
234
  const nsParams = namespace ? [namespace] : [];
225
235
  let total = 0;
226
236
  try {
227
- const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' ${nsFilter}`);
228
- const countRow = countStmt.get(...nsParams);
229
- total = countRow?.cnt ?? 0;
237
+ const countRows = execRows(ctx.db, `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' ${nsFilter}`, nsParams);
238
+ total = Number(countRows[0]?.cnt ?? 0);
230
239
  }
231
240
  catch {
232
241
  return null;
233
242
  }
234
243
  const entries = [];
235
244
  try {
236
- const stmt = ctx.db.prepare(`
237
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
238
- FROM memory_entries
239
- WHERE status = 'active' ${nsFilter}
240
- ORDER BY updated_at DESC
241
- LIMIT ? OFFSET ?
242
- `);
243
- const rows = stmt.all(...nsParams, limit, offset);
245
+ const rows = execRows(ctx.db, `SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
246
+ FROM memory_entries
247
+ WHERE status = 'active' ${nsFilter}
248
+ ORDER BY updated_at DESC
249
+ LIMIT ? OFFSET ?`, [...nsParams, limit, offset]);
244
250
  for (const row of rows) {
245
251
  entries.push({
246
252
  id: String(row.id).substring(0, 20),
247
253
  key: row.key || String(row.id).substring(0, 15),
248
254
  namespace: row.namespace || 'default',
249
- size: (row.content || '').length,
250
- accessCount: row.access_count ?? 0,
255
+ size: String(row.content || '').length,
256
+ accessCount: Number(row.access_count ?? 0),
251
257
  createdAt: row.created_at || new Date().toISOString(),
252
258
  updatedAt: row.updated_at || new Date().toISOString(),
253
259
  hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
@@ -294,7 +300,13 @@ export async function bridgeGetEntry(options) {
294
300
  WHERE status = 'active' AND key = ? AND namespace = ?
295
301
  LIMIT 1
296
302
  `);
297
- row = stmt.get(key, namespace);
303
+ // sql.js: Statement.get returns a positional array, not an object.
304
+ // Use getAsObject to read columns by name downstream. Bindings are
305
+ // passed as a single array — varargs are silently ignored.
306
+ row = stmt.getAsObject([key, namespace]);
307
+ // getAsObject returns {} when no row matches; treat as null.
308
+ if (!row || Object.keys(row).length === 0)
309
+ row = null;
298
310
  }
299
311
  catch {
300
312
  return null;
@@ -302,7 +314,7 @@ export async function bridgeGetEntry(options) {
302
314
  if (!row)
303
315
  return { success: true, found: false };
304
316
  try {
305
- ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run(Date.now(), row.id);
317
+ ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run([Date.now(), row.id]);
306
318
  }
307
319
  catch {
308
320
  // Non-fatal
@@ -341,24 +353,28 @@ export async function bridgeDeleteEntry(options) {
341
353
  }
342
354
  let changes = 0;
343
355
  try {
344
- const result = ctx.db.prepare(`
356
+ ctx.db.prepare(`
345
357
  UPDATE memory_entries
346
358
  SET status = 'deleted', updated_at = ?
347
359
  WHERE key = ? AND namespace = ? AND status = 'active'
348
- `).run(Date.now(), key, namespace);
349
- changes = result?.changes ?? 0;
360
+ `).run([Date.now(), key, namespace]);
361
+ // sql.js Statement.run returns true/false, not { changes }. Use
362
+ // db.getRowsModified() to read the row count from the last statement.
363
+ changes = ctx.db.getRowsModified?.() ?? 0;
350
364
  }
351
365
  catch {
352
366
  return null;
353
367
  }
368
+ if (changes > 0)
369
+ persistBridgeDb(ctx.db, options.dbPath);
354
370
  await cacheInvalidate(registry, makeEntryCacheKey(namespace, key));
355
371
  if (changes > 0) {
356
372
  await logAttestation(registry, 'delete', key, { namespace });
357
373
  }
358
374
  let remaining = 0;
359
375
  try {
360
- const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`).get();
361
- remaining = row?.cnt ?? 0;
376
+ const result = ctx.db.exec(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`);
377
+ remaining = result[0]?.values?.[0]?.[0] ?? 0;
362
378
  }
363
379
  catch {
364
380
  // Non-fatal
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * @module v3/cli/memory-bridge
11
11
  */
12
- import { cosineSim, generateId, getRegistry, withDb, } from './bridge-core.js';
12
+ import { cosineSim, execRows, generateId, getRegistry, persistBridgeDb, withDb, } from './bridge-core.js';
13
13
  import { bridgeSearchEntries, bridgeStoreEntry, } from './bridge-entries.js';
14
14
  // ===== Re-exports: primitives =====
15
- export { getControllerRegistry, isBridgeAvailable, refreshVectorStatsCache, shutdownBridge, } from './bridge-core.js';
15
+ export { REQUIRED_BRIDGE_CONTROLLERS, getBridgeLastError, getControllerRegistry, isBridgeAvailable, refreshVectorStatsCache, shutdownBridge, } from './bridge-core.js';
16
16
  // ===== Re-exports: entries store =====
17
17
  export { bridgeDeleteEntry, bridgeGetEntry, bridgeListEntries, bridgeSearchEntries, bridgeStoreEntry, } from './bridge-entries.js';
18
18
  // ===== Embedding bridge =====
@@ -67,8 +67,8 @@ export async function bridgeGetHNSWStatus(dbPath) {
67
67
  return withDb(dbPath, async (ctx) => {
68
68
  let entryCount = 0;
69
69
  try {
70
- const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`).get();
71
- entryCount = row?.cnt ?? 0;
70
+ const rows = execRows(ctx.db, `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`);
71
+ entryCount = Number(rows[0]?.cnt ?? 0);
72
72
  }
73
73
  catch {
74
74
  // Table might not exist
@@ -85,13 +85,13 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
85
85
  : '';
86
86
  let rows;
87
87
  try {
88
- const stmt = ctx.db.prepare(`
88
+ const sql = `
89
89
  SELECT id, key, namespace, content, embedding
90
90
  FROM memory_entries
91
91
  WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
92
92
  LIMIT 10000
93
- `);
94
- rows = nsFilter ? stmt.all(options.namespace) : stmt.all();
93
+ `;
94
+ rows = nsFilter ? execRows(ctx.db, sql, [options.namespace]) : execRows(ctx.db, sql);
95
95
  }
96
96
  catch {
97
97
  return null;
@@ -101,16 +101,16 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
101
101
  if (!row.embedding)
102
102
  continue;
103
103
  try {
104
- const emb = JSON.parse(row.embedding);
104
+ const emb = JSON.parse(String(row.embedding));
105
105
  const score = cosineSim(queryEmbedding, emb);
106
106
  if (score >= threshold) {
107
+ const content = String(row.content || '');
107
108
  results.push({
108
109
  id: String(row.id).substring(0, 12),
109
- key: row.key || String(row.id).substring(0, 15),
110
- content: (row.content || '').substring(0, 60) +
111
- ((row.content || '').length > 60 ? '...' : ''),
110
+ key: String(row.key || row.id).substring(0, 15),
111
+ content: content.substring(0, 60) + (content.length > 60 ? '...' : ''),
112
112
  score,
113
- namespace: row.namespace || 'default',
113
+ namespace: String(row.namespace || 'default'),
114
114
  });
115
115
  }
116
116
  }
@@ -132,7 +132,12 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
132
132
  embedding, embedding_dimensions, embedding_model,
133
133
  created_at, updated_at, status
134
134
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
135
- `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, now, now);
135
+ `).run([
136
+ id, entry.key, entry.namespace, entry.content,
137
+ embeddingJson, embedding.length,
138
+ now, now,
139
+ ]);
140
+ persistBridgeDb(ctx.db, dbPath);
136
141
  return true;
137
142
  });
138
143
  }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
- import { mofloImport } from '../services/moflo-require.js';
13
+ import { mofloImport, importMofloMemory } from '../services/moflo-require.js';
14
14
  /**
15
15
  * Write vector-stats.json cache for the statusline (no subprocess needed).
16
16
  * Called after memory store/delete to keep the cache fresh.
@@ -378,8 +378,10 @@ export async function getHNSWIndex(options) {
378
378
  }
379
379
  hnswInitializing = true;
380
380
  try {
381
- // Use HnswLite pure TS implementation (no native dependencies)
382
- const memoryModule = await import('@moflo/memory');
381
+ // Use HnswLite pure TS implementation (no native dependencies). The
382
+ // shared resolver handles the consumer case where @moflo/memory is not
383
+ // a declared dep and must be loaded via a relative URL fallback.
384
+ const memoryModule = await importMofloMemory(import.meta.url);
383
385
  if (!('HnswLite' in memoryModule) || memoryModule.HnswLite === undefined) {
384
386
  // Shape-check (issue #482): warn loudly and bail — the outer catch
385
387
  // would otherwise swallow a cryptic "undefined is not a constructor".
@@ -78,4 +78,21 @@ export function mofloResolve(specifier) {
78
78
  return null;
79
79
  }
80
80
  }
81
+ /**
82
+ * Import `@moflo/memory` from within a moflo source module. The root `moflo`
83
+ * package ships @moflo/memory as a source folder rather than a declared
84
+ * dependency, so `mofloImport('@moflo/memory')` fails in consumer installs
85
+ * (node_modules/@moflo/memory/ doesn't exist). Fall back to a URL resolved
86
+ * relative to the caller's file — the same src/modules/memory/dist/index.js
87
+ * layout holds in both dev and consumer.
88
+ *
89
+ * @param callerUrl `import.meta.url` of the file that needs @moflo/memory
90
+ */
91
+ export async function importMofloMemory(callerUrl) {
92
+ const viaRequire = await mofloImport('@moflo/memory');
93
+ if (viaRequire)
94
+ return viaRequire;
95
+ const memoryUrl = new URL('../../../../memory/dist/index.js', callerUrl);
96
+ return import(memoryUrl.href);
97
+ }
81
98
  //# sourceMappingURL=moflo-require.js.map
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.8.80';
5
+ export const VERSION = '4.8.82';
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moflo/cli",
3
- "version": "4.8.80",
3
+ "version": "4.8.82",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -41,8 +41,8 @@ async function testRvf() {
41
41
  */
42
42
  async function testSqlJs() {
43
43
  try {
44
- const initSqlJs = (await import('sql.js')).default;
45
- const SQL = await initSqlJs();
44
+ const { initSqlJsForNode } = await import('./sqljs-backend.js');
45
+ const SQL = await initSqlJsForNode();
46
46
  const testDb = new SQL.Database();
47
47
  testDb.close();
48
48
  return true;
@@ -92,8 +92,8 @@ function normalizeSqliteRow(row) {
92
92
  async function readSqliteRows(dbPath) {
93
93
  // Use sql.js (WASM) for SQLite reading
94
94
  try {
95
- const initSqlJs = (await import('sql.js')).default;
96
- const SQL = await initSqlJs();
95
+ const { initSqlJsForNode } = await import('./sqljs-backend.js');
96
+ const SQL = await initSqlJsForNode();
97
97
  const fs = await import('node:fs');
98
98
  const buf = fs.readFileSync(dbPath);
99
99
  const db = new SQL.Database(buf);
@@ -8,17 +8,57 @@
8
8
  */
9
9
  import { EventEmitter } from 'node:events';
10
10
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { dirname, join as joinPath } from 'node:path';
11
13
  import initSqlJs from 'sql.js';
14
+ /**
15
+ * Resolve the directory that bundles sql-wasm.wasm alongside sql.js. In Node
16
+ * the default `locateFile` (sql.js.org CDN) is treated as a file path and
17
+ * fails with ENOENT — sql.js then crashes on WASM cleanup. Walk to the
18
+ * installed sql.js package and point at its local dist/ instead.
19
+ */
20
+ let cachedSqlJsWasmDir = null;
21
+ function resolveSqlJsWasmDir() {
22
+ if (cachedSqlJsWasmDir !== null)
23
+ return cachedSqlJsWasmDir;
24
+ try {
25
+ // Resolve sql.js's main entry (sql-wasm.js lives next to it in dist/).
26
+ // Can't use require.resolve('sql.js/package.json') because sql.js's
27
+ // `exports` field doesn't expose it. `require.resolve` returns an OS-
28
+ // native absolute path — works on Windows (backslashes) and POSIX.
29
+ const require = createRequire(import.meta.url);
30
+ const mainEntry = require.resolve('sql.js');
31
+ cachedSqlJsWasmDir = dirname(mainEntry);
32
+ }
33
+ catch {
34
+ cachedSqlJsWasmDir = null;
35
+ }
36
+ return cachedSqlJsWasmDir;
37
+ }
38
+ /**
39
+ * Initialize sql.js with a Node-aware `locateFile` that points at the
40
+ * installed sql.js package's own dist/ directory. Prefer this over bare
41
+ * `initSqlJs()` anywhere sql.js is used in Node.
42
+ */
43
+ export async function initSqlJsForNode(wasmPath) {
44
+ return initSqlJs({ locateFile: buildLocateFile(wasmPath) });
45
+ }
46
+ function buildLocateFile(wasmPath) {
47
+ if (wasmPath)
48
+ return () => wasmPath;
49
+ const localWasmDir = resolveSqlJsWasmDir();
50
+ if (localWasmDir)
51
+ return (file) => joinPath(localWasmDir, file);
52
+ // Browser/unbundled fallback — sql.js can fetch over HTTP when running in
53
+ // environments where require.resolve('sql.js') can't find the package.
54
+ return (file) => `https://sql.js.org/dist/${file}`;
55
+ }
12
56
  /**
13
57
  * Load sql.js WASM and open a Database — from disk if `dbPath` exists,
14
58
  * otherwise in-memory. Shared between `SqlJsBackend` and `ControllerRegistry`.
15
59
  */
16
60
  export async function openSqlJsDatabase(dbPath, wasmPath) {
17
- const SQL = await initSqlJs({
18
- locateFile: wasmPath
19
- ? () => wasmPath
20
- : (file) => `https://sql.js.org/dist/${file}`,
21
- });
61
+ const SQL = await initSqlJsForNode(wasmPath);
22
62
  if (dbPath !== ':memory:' && existsSync(dbPath)) {
23
63
  return new SQL.Database(new Uint8Array(readFileSync(dbPath)));
24
64
  }