@claude-flow/cli 3.56.6 → 3.56.7

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.
Files changed (56) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +109 -10
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/intelligence.cjs +57 -4
  4. package/bin/cli.js +11 -1
  5. package/bin/mcp-server.js +11 -1
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/doctor-native-addon.d.ts +23 -0
  8. package/dist/src/commands/doctor-native-addon.js +135 -0
  9. package/dist/src/commands/doctor.js +3 -0
  10. package/dist/src/commands/hooks.js +8 -1
  11. package/dist/src/commands/index.js +2 -0
  12. package/dist/src/commands/init.d.ts +2 -0
  13. package/dist/src/commands/init.js +37 -5
  14. package/dist/src/commands/mods.d.ts +3 -0
  15. package/dist/src/commands/mods.js +19 -5
  16. package/dist/src/commands/tui.d.ts +12 -0
  17. package/dist/src/commands/tui.js +98 -0
  18. package/dist/src/fs-secure.d.ts +25 -0
  19. package/dist/src/fs-secure.js +51 -2
  20. package/dist/src/init/executor.js +11 -14
  21. package/dist/src/init/helper-commands.d.ts +2 -0
  22. package/dist/src/init/helper-commands.js +2 -0
  23. package/dist/src/init/helpers-generator.js +4 -1
  24. package/dist/src/init/settings-generator.js +3 -3
  25. package/dist/src/mcp-server.js +10 -2
  26. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  27. package/dist/src/mcp-tools/capability-brain.js +1 -1
  28. package/dist/src/mcp-tools/hooks-tools.js +26 -4
  29. package/dist/src/mcp-tools/memory-tools.js +2 -1
  30. package/dist/src/memory/live-memory-row.d.ts +3 -1
  31. package/dist/src/memory/live-memory-row.js +3 -1
  32. package/dist/src/memory/memory-bridge.d.ts +4 -0
  33. package/dist/src/memory/memory-bridge.js +3 -2
  34. package/dist/src/memory/memory-initializer.d.ts +4 -0
  35. package/dist/src/memory/memory-initializer.js +12 -11
  36. package/dist/src/mods/claude-installs.js +7 -3
  37. package/dist/src/mods/install.js +32 -12
  38. package/dist/src/mods/plugin-resolve.js +3 -1
  39. package/dist/src/mods/probe.js +92 -4
  40. package/dist/src/services/command-recording.d.ts +17 -0
  41. package/dist/src/services/command-recording.js +91 -0
  42. package/node_modules/@claude-flow/codex/README.md +20 -3
  43. package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
  44. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  45. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  46. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  47. package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
  48. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  49. package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
  50. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  51. package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
  52. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  53. package/node_modules/@claude-flow/codex/package.json +1 -1
  54. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  55. package/package.json +2 -2
  56. package/plugins/ruflo-console/dist/tui.mjs +271 -0
@@ -11,8 +11,10 @@
11
11
  * node auto-memory-hook.mjs status # Show bridge status
12
12
  */
13
13
 
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
- import { join, dirname, resolve } from 'path';
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, rmdirSync, rmSync, statSync } from 'fs';
15
+ import { execFileSync } from 'child_process';
16
+ import { homedir } from 'os';
17
+ import { join, dirname, resolve, sep } from 'path';
16
18
  import { fileURLToPath } from 'url';
17
19
 
18
20
  const __filename = fileURLToPath(import.meta.url);
@@ -77,6 +79,66 @@ process.on('SIGINT', () => { gracefulExit('SIGINT'); });
77
79
  // Ensure data dir
78
80
  if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
79
81
 
82
+ // ============================================================================
83
+ // Store write safety (#4058): intelligence.cjs read-modify-writes the same
84
+ // file from the session-end hook, so writes are atomic (temp + rename) and
85
+ // serialised by a short directory lock. The lock is advisory and fails open:
86
+ // a hook must never hang, so after ~2 s it proceeds without it.
87
+ // ============================================================================
88
+
89
+ const LOCK_WAIT_MS = 2000;
90
+ const LOCK_STALE_MS = 10000;
91
+ function sleepSync(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
92
+
93
+ function withStoreLock(storePath, fn) {
94
+ const lock = storePath + '.lock';
95
+ const deadline = Date.now() + LOCK_WAIT_MS;
96
+ let held = false;
97
+ for (;;) {
98
+ try { mkdirSync(lock); held = true; break; } catch (e) {
99
+ if (!e || e.code !== 'EEXIST') break;
100
+ try {
101
+ if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) { rmSync(lock, { recursive: true, force: true }); continue; }
102
+ } catch { /* lock vanished: retry */ }
103
+ if (Date.now() > deadline) break;
104
+ sleepSync(10);
105
+ }
106
+ }
107
+ try { return fn(); } finally { if (held) { try { rmdirSync(lock); } catch { /* already gone */ } } }
108
+ }
109
+
110
+ function writeFileAtomic(filePath, text) {
111
+ const tmp = filePath + '.' + process.pid + '.tmp';
112
+ try {
113
+ writeFileSync(tmp, text, 'utf-8');
114
+ renameSync(tmp, filePath);
115
+ } catch (e) {
116
+ try { unlinkSync(tmp); } catch { /* nothing to clean */ }
117
+ throw e;
118
+ }
119
+ }
120
+
121
+ // Claude Code shares one auto-memory directory across every worktree of a
122
+ // repository, but findGitRoot() in the memory package stops at a worktree's
123
+ // `.git` FILE and keys the directory by the worktree path (#4058). Resolve the
124
+ // main checkout through git's common dir so both agree. No shell is involved.
125
+ function mainCheckoutRoot(dir) {
126
+ try {
127
+ if (!statSync(join(dir, '.git')).isFile()) return null;
128
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: dir, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
129
+ if (!common) return null;
130
+ const abs = resolve(dir, common);
131
+ return abs.endsWith('/.git') || abs.endsWith('\\.git') ? dirname(abs) : null;
132
+ } catch { return null; }
133
+ }
134
+
135
+ function worktreeMemoryDir() {
136
+ const main = mainCheckoutRoot(PROJECT_ROOT);
137
+ if (!main) return undefined;
138
+ const key = main.split(sep).join('/').replace(/[\/_:]/g, '-');
139
+ return join(process.env.HOME || process.env.USERPROFILE || homedir(), '.claude', 'projects', key, 'memory');
140
+ }
141
+
80
142
  // ============================================================================
81
143
  // Simple JSON File Backend (implements IMemoryBackend interface)
82
144
  // ============================================================================
@@ -85,6 +147,8 @@ class JsonFileBackend {
85
147
  constructor(filePath) {
86
148
  this.filePath = filePath;
87
149
  this.entries = new Map();
150
+ this.loadedIds = new Set();
151
+ this.deletedIds = new Set();
88
152
  }
89
153
 
90
154
  async initialize() {
@@ -92,7 +156,7 @@ class JsonFileBackend {
92
156
  try {
93
157
  const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
94
158
  if (Array.isArray(data)) {
95
- for (const entry of data) this.entries.set(entry.id, entry);
159
+ for (const entry of data) { this.entries.set(entry.id, entry); this.loadedIds.add(entry.id); }
96
160
  }
97
161
  } catch { /* start fresh */ }
98
162
  }
@@ -117,17 +181,26 @@ class JsonFileBackend {
117
181
  this._persist();
118
182
  return e;
119
183
  }
120
- async delete(id) { return this.entries.delete(id); }
184
+ async delete(id) { this.deletedIds.add(id); return this.entries.delete(id); }
121
185
  async query(opts) {
122
186
  let results = [...this.entries.values()];
123
187
  if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
124
- if (opts?.type) results = results.filter(e => e.type === opts.type);
188
+ // MemoryQuery.type is the query mode (semantic|exact|prefix|tag|hybrid), not
189
+ // the entry's memory type: the bridge asks for { type: 'hybrid' } and a strict
190
+ // entry-type match returned nothing, so sync always reported 0 (#4058).
191
+ const MODES = ['semantic', 'exact', 'prefix', 'tag', 'hybrid'];
192
+ if (opts?.type && !MODES.includes(opts.type)) results = results.filter(e => e.type === opts.type);
193
+ if (opts?.memoryType) results = results.filter(e => e.type === opts.memoryType);
194
+ if (opts?.key) results = results.filter(e => e.key === opts.key);
195
+ if (opts?.keyPrefix) results = results.filter(e => typeof e.key === 'string' && e.key.startsWith(opts.keyPrefix));
196
+ if (Array.isArray(opts?.tags) && opts.tags.length) results = results.filter(e => opts.tags.every(t => (e.tags || []).includes(t)));
197
+ if (opts?.updatedAfter) results = results.filter(e => (e.updatedAt || 0) > opts.updatedAfter);
125
198
  if (opts?.limit) results = results.slice(0, opts.limit);
126
199
  return results;
127
200
  }
128
201
  async search() { return []; } // No vector search in JSON backend
129
202
  async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
130
- async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
203
+ async bulkDelete(ids) { let n = 0; for (const id of ids) { this.deletedIds.add(id); if (this.entries.delete(id)) n++; } this._persist(); return n; }
131
204
  async count() { return this.entries.size; }
132
205
  async listNamespaces() {
133
206
  const ns = new Set();
@@ -137,7 +210,7 @@ class JsonFileBackend {
137
210
  async clearNamespace(ns) {
138
211
  let n = 0;
139
212
  for (const [id, e] of this.entries) {
140
- if (e.namespace === ns) { this.entries.delete(id); n++; }
213
+ if (e.namespace === ns) { this.deletedIds.add(id); this.entries.delete(id); n++; }
141
214
  }
142
215
  this._persist();
143
216
  return n;
@@ -164,7 +237,22 @@ class JsonFileBackend {
164
237
 
165
238
  _persist() {
166
239
  try {
167
- writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
240
+ withStoreLock(this.filePath, () => {
241
+ // Keep entries another writer (intelligence.cjs, a parallel hook) added
242
+ // after we loaded; ours win on the same id, and our own deletes stay deleted.
243
+ const merged = new Map(this.entries);
244
+ try {
245
+ const onDisk = JSON.parse(readFileSync(this.filePath, 'utf-8'));
246
+ if (Array.isArray(onDisk)) {
247
+ for (const e of onDisk) {
248
+ if (e && !merged.has(e.id) && !this.loadedIds.has(e.id) && !this.deletedIds.has(e.id)) merged.set(e.id, e);
249
+ }
250
+ }
251
+ } catch { /* missing or unreadable: write ours */ }
252
+ writeFileAtomic(this.filePath, JSON.stringify([...merged.values()], null, 2));
253
+ for (const id of merged.keys()) this.loadedIds.add(id);
254
+ this.entries = merged;
255
+ });
168
256
  } catch { /* best effort */ }
169
257
  }
170
258
  }
@@ -284,6 +372,8 @@ async function doImport() {
284
372
  workingDir: PROJECT_ROOT,
285
373
  syncMode: 'on-session-end',
286
374
  };
375
+ const wtDir = worktreeMemoryDir();
376
+ if (wtDir) bridgeConfig.memoryDir = wtDir;
287
377
 
288
378
  // Wire learning if enabled and available
289
379
  if (config.learningBridge.enabled && memPkg.LearningBridge) {
@@ -344,6 +434,8 @@ async function doSync() {
344
434
  workingDir: PROJECT_ROOT,
345
435
  syncMode: 'on-session-end',
346
436
  };
437
+ const wtDir = worktreeMemoryDir();
438
+ if (wtDir) bridgeConfig.memoryDir = wtDir;
347
439
 
348
440
  if (config.learningBridge.enabled && memPkg.LearningBridge) {
349
441
  bridgeConfig.learning = {
@@ -369,8 +461,11 @@ async function doSync() {
369
461
  dim(`└─ Backend entries: ${entryCount}`);
370
462
 
371
463
  // Curate MEMORY.md index with graph-aware ordering
464
+ let indexSkipped = null;
465
+ if (typeof bridge.on === 'function') bridge.on('index:skipped', (info) => { indexSkipped = info; });
372
466
  await bridge.curateIndex();
373
- success('Curated MEMORY.md index');
467
+ if (indexSkipped) dim(`MEMORY.md index unchanged (${indexSkipped.reason || 'nothing to curate'})`);
468
+ else success('Curated MEMORY.md index');
374
469
  } catch (err) {
375
470
  dim(`Sync failed (non-critical): ${err.message}`);
376
471
  }
@@ -408,7 +503,11 @@ async function doStatus() {
408
503
  // Main
409
504
  // ============================================================================
410
505
 
411
- const command = process.argv[2] || 'status';
506
+ // Older settings.json files run `node -e "…import(file)" import`, where the
507
+ // subcommand lands in argv[1] and argv[2] is empty, so both hooks fell through
508
+ // to `status` and never imported or synced (#4058). Accept that shape too.
509
+ const COMMANDS = ['import', 'sync', 'status'];
510
+ const command = process.argv[2] || (COMMANDS.includes(process.argv[1]) ? process.argv[1] : 'status');
412
511
 
413
512
  // Dynamic import() failures can surface as unhandled rejections on a later
414
513
  // microtask even when the awaiting call site already caught them, which would
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.56.6",
3
+ "version": "3.56.7",
4
4
  "files": {
5
- "auto-memory-hook.mjs": "381412515fb490def7d821ddfe1ebf8d9bee76e49a86e182806c0274f568bf15",
5
+ "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "0f855c667b8d2d04cb4fe91b14b351d14c3dfb30516dcd9d7ae156acbe0a689d",
7
- "intelligence.cjs": "ac5033a77bcea9796856629d5d319bde9ab6b7edf0dcd0d4af187ce30af90836",
7
+ "intelligence.cjs": "f486be296eb2f0518f34b8f4453d9d0a988845e4aec6d18d8d47725aca6dd38b",
8
8
  "statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "3wmXMrroE4WEmOLGOLYSxXlcB2GITYHa8xpd3Jwu/xLCyFsf+saWI4Vq8EsvCzuwC8u3XpKX7aWrvQchQK3oAg==",
12
+ "signature": "15NFpUZ8+naP4v/xuCBCl7IZ//YJPXQYotOg48V/Xw/uyCY1FXYeeL4a3Rwa+Bji0+CV4FRiZg61N6Zt+EStBg==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -108,9 +108,60 @@ function readJSON(filePath) {
108
108
  return null;
109
109
  }
110
110
 
111
+ // Atomic (temp + rename): auto-memory-hook.mjs reads and writes the same store
112
+ // from the session hooks, and a reader must never see a half-written file (#4058).
111
113
  function writeJSON(filePath, data) {
112
114
  ensureDataDir();
113
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
115
+ const tmp = filePath + '.' + process.pid + '.tmp';
116
+ try {
117
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
118
+ fs.renameSync(tmp, filePath);
119
+ } catch (e) {
120
+ try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ }
121
+ throw e;
122
+ }
123
+ }
124
+
125
+ // Short advisory directory lock shared with auto-memory-hook.mjs. It fails open
126
+ // after ~2 s so a stuck lock can never hang a hook.
127
+ function withStoreLock(filePath, fn) {
128
+ const lock = filePath + '.lock';
129
+ const deadline = Date.now() + 2000;
130
+ let held = false;
131
+ for (;;) {
132
+ try { fs.mkdirSync(lock); held = true; break; } catch (e) {
133
+ if (!e || e.code !== 'EEXIST') break;
134
+ try {
135
+ if (Date.now() - fs.statSync(lock).mtimeMs > 10000) { fs.rmSync(lock, { recursive: true, force: true }); continue; }
136
+ } catch { /* lock vanished: retry */ }
137
+ if (Date.now() > deadline) break;
138
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
139
+ }
140
+ }
141
+ try { return fn(); } finally { if (held) { try { fs.rmdirSync(lock); } catch { /* already gone */ } } }
142
+ }
143
+
144
+ // IDs present when the store was read. Take this BEFORE deduping: those passes
145
+ // assign IDs to entries in place, and an entry that had none must still count as
146
+ // ours (not as something another writer added).
147
+ function storeIds(loaded) {
148
+ return new Set((Array.isArray(loaded) ? loaded : []).map((e) => e && e.id));
149
+ }
150
+
151
+ // Write the store, keeping entries another writer added after we read it.
152
+ function writeStoreMerged(filePath, next, loadedIds) {
153
+ withStoreLock(filePath, () => {
154
+ let out = next;
155
+ try {
156
+ const current = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
157
+ if (Array.isArray(current)) {
158
+ const have = new Set(next.map((e) => e && e.id));
159
+ const added = current.filter((e) => e && e.id != null && !loadedIds.has(e.id) && !have.has(e.id));
160
+ if (added.length) out = next.concat(added);
161
+ }
162
+ } catch { /* missing or unreadable: write ours */ }
163
+ writeJSON(filePath, out);
164
+ });
114
165
  }
115
166
 
116
167
  function tokenize(text) {
@@ -475,13 +526,14 @@ function init() {
475
526
  // Check if graph-state.json is fresh (within 60s of store)
476
527
  const graphState = readJSON(GRAPH_PATH);
477
528
  let store = readJSON(STORE_PATH);
529
+ const loadedIds = storeIds(store);
478
530
 
479
531
  // Bootstrap from MEMORY.md files if store is empty
480
532
  if (!store || !Array.isArray(store) || store.length === 0) {
481
533
  const bootstrapped = bootstrapFromMemoryFiles();
482
534
  if (bootstrapped.length > 0) {
483
535
  store = bootstrapped;
484
- writeJSON(STORE_PATH, store);
536
+ writeStoreMerged(STORE_PATH, store, loadedIds);
485
537
  } else {
486
538
  return { nodes: 0, edges: 0, message: 'No memory entries to index' };
487
539
  }
@@ -503,7 +555,7 @@ function init() {
503
555
  `[INTELLIGENCE] Deduped store: ${store.length} -> ${deduped.length} entries ` +
504
556
  `(by-id: ${store.length - beforeContentDedup} dropped, by-content: ${beforeContentDedup - deduped.length} dropped)\n`
505
557
  );
506
- writeJSON(STORE_PATH, deduped);
558
+ writeStoreMerged(STORE_PATH, deduped, loadedIds);
507
559
  }
508
560
 
509
561
  // Skip rebuild if graph is fresh and store hasn't changed. #2920: nodeCount
@@ -834,6 +886,7 @@ function consolidate() {
834
886
  return { entries: 0, edges: 0, newEntries: 0, message: 'No store to consolidate' };
835
887
  }
836
888
 
889
+ const loadedIds = storeIds(store);
837
890
  // Deduplicate store entries by ID before processing (fixes #1518)
838
891
  const preDedupCount = store.length;
839
892
  store = deduplicateById(store);
@@ -979,7 +1032,7 @@ function consolidate() {
979
1032
  // RANKED_PATH are rewritten unconditionally just above; writing the
980
1033
  // (already deduped, size-bounded per ADR-095 G6) store alongside them
981
1034
  // costs nothing near the <500ms budget and removes the whole bug class.
982
- writeJSON(STORE_PATH, store);
1035
+ writeStoreMerged(STORE_PATH, store, loadedIds);
983
1036
 
984
1037
  // 9. Save snapshot for delta tracking
985
1038
  const updatedGraph = readJSON(GRAPH_PATH);
package/bin/cli.js CHANGED
@@ -257,7 +257,7 @@ if (isMCPMode) {
257
257
  serverInfo: { name: 'ruflo', version: VERSION },
258
258
  capabilities: {
259
259
  tools: { listChanged: true },
260
- resources: { subscribe: true, listChanged: true },
260
+ resources: {},
261
261
  },
262
262
  },
263
263
  };
@@ -322,6 +322,16 @@ if (isMCPMode) {
322
322
  }
323
323
  }
324
324
 
325
+ // A declared capability must be served (ADR-490): no resources are exposed, so list answers are empty and a read is "not found".
326
+ case 'resources/list':
327
+ return { jsonrpc: '2.0', id: message.id, result: { resources: [] } };
328
+
329
+ case 'resources/templates/list':
330
+ return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
331
+
332
+ case 'resources/read':
333
+ return { jsonrpc: '2.0', id: message.id, error: { code: -32002, message: 'Resource not found' } };
334
+
325
335
  case 'notifications/initialized':
326
336
  return null;
327
337
 
package/bin/mcp-server.js CHANGED
@@ -146,7 +146,7 @@ async function handleMessage(message) {
146
146
  serverInfo: { name: 'ruflo', version: VERSION },
147
147
  capabilities: {
148
148
  tools: { listChanged: true },
149
- resources: { subscribe: true, listChanged: true },
149
+ resources: {},
150
150
  },
151
151
  },
152
152
  };
@@ -208,6 +208,16 @@ async function handleMessage(message) {
208
208
  }
209
209
  }
210
210
 
211
+ // A declared capability must be served (ADR-490): no resources are exposed, so list answers are empty and a read is "not found".
212
+ case 'resources/list':
213
+ return { jsonrpc: '2.0', id: message.id, result: { resources: [] } };
214
+
215
+ case 'resources/templates/list':
216
+ return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
217
+
218
+ case 'resources/read':
219
+ return { jsonrpc: '2.0', id: message.id, error: { code: -32002, message: 'Resource not found' } };
220
+
211
221
  case 'notifications/initialized':
212
222
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] Client initialized`);
213
223
  return null; // No response for notifications
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-11T16:40:50.905Z",
5
- "gitSha": "5905d40b",
4
+ "generatedAt": "2026-10-11T22:43:38.354Z",
5
+ "gitSha": "aac16fbd",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -0,0 +1,23 @@
1
+ export interface NativeAddonCheck {
2
+ name: string;
3
+ status: 'pass' | 'warn' | 'fail';
4
+ message: string;
5
+ fix?: string;
6
+ }
7
+ /** Node 24.19.0 up to, but not including, 24.22.0. Re-check this bound when a release ships the fix. */
8
+ export declare function isAffectedNodeVersion(version: string): boolean;
9
+ /** The Node version of the headers a copy was built against, from its config.gypi. */
10
+ export declare function headersVersionFromGypi(gypi: string): string | null;
11
+ /** `better-sqlite3` copies in node_modules next to, or one level under, each ancestor of the start dirs. */
12
+ export declare function findBetterSqlite3Copies(startDirs: string[], maxDepth?: number): string[];
13
+ export interface NativeAddonCheckDeps {
14
+ /** Runtime version, default process.versions.node. */
15
+ nodeVersion?: string;
16
+ /** Exact better-sqlite3 package directories to inspect (skips discovery). */
17
+ copies?: string[];
18
+ /** Directories to walk up from when discovering copies. */
19
+ startDirs?: string[];
20
+ }
21
+ /** Read-only: warn when a locally compiled better-sqlite3 was built against Node >= 24.19 headers on an affected runtime. */
22
+ export declare function checkNativeAddonHeaders(deps?: NativeAddonCheckDeps): Promise<NativeAddonCheck>;
23
+ //# sourceMappingURL=doctor-native-addon.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * #4055: native addons compiled against Node >= 24.19 headers abort when a
3
+ * garbage-collected ObjectWrap (a better-sqlite3 Statement) calls
4
+ * RemoveEnvironmentCleanupHook() with no Environment (nodejs/node#65446).
5
+ * The fix (abe1b673e9b, PR #65943) was in no release through v24.21.0.
6
+ *
7
+ * Only copies that were COMPILED LOCALLY carry a build/config.gypi naming the
8
+ * headers they were built against; prebuilt binaries (prebuild-install) do not,
9
+ * so they are never flagged. The check is read-only and never loads an addon.
10
+ */
11
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ const NAME = 'Native addon headers (Node 24)';
15
+ function parse(v) {
16
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v);
17
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
18
+ }
19
+ /** Node 24.19.0 up to, but not including, 24.22.0. Re-check this bound when a release ships the fix. */
20
+ export function isAffectedNodeVersion(version) {
21
+ const v = parse(version);
22
+ if (!v || v[0] !== 24)
23
+ return false;
24
+ return v[1] >= 19 && v[1] <= 21;
25
+ }
26
+ /** The Node version of the headers a copy was built against, from its config.gypi. */
27
+ export function headersVersionFromGypi(gypi) {
28
+ const field = /["']node_version["']\s*:\s*["']v?(\d+\.\d+\.\d+)["']/.exec(gypi);
29
+ if (field)
30
+ return field[1];
31
+ const dir = /["']nodedir["']\s*:\s*["']([^"']*)["']/.exec(gypi);
32
+ if (!dir)
33
+ return null;
34
+ const all = [...dir[1].matchAll(/(\d+)\.(\d+)\.(\d+)/g)];
35
+ const last = all[all.length - 1];
36
+ return last ? `${last[1]}.${last[2]}.${last[3]}` : null;
37
+ }
38
+ function readJson(path) {
39
+ try {
40
+ return JSON.parse(readFileSync(path, 'utf8'));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function listDir(path) {
47
+ try {
48
+ return readdirSync(path);
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ /** `better-sqlite3` copies in node_modules next to, or one level under, each ancestor of the start dirs. */
55
+ export function findBetterSqlite3Copies(startDirs, maxDepth = 12) {
56
+ const found = new Set();
57
+ const add = (dir) => {
58
+ if (!existsSync(join(dir, 'package.json')))
59
+ return;
60
+ try {
61
+ found.add(realpathSync(dir));
62
+ }
63
+ catch { /* unreadable */ }
64
+ };
65
+ const scanNodeModules = (nm) => {
66
+ add(join(nm, 'better-sqlite3'));
67
+ for (const entry of listDir(nm)) {
68
+ if (entry.startsWith('.') || entry === 'better-sqlite3')
69
+ continue;
70
+ if (entry.startsWith('@')) {
71
+ for (const sub of listDir(join(nm, entry)))
72
+ add(join(nm, entry, sub, 'node_modules', 'better-sqlite3'));
73
+ }
74
+ else {
75
+ add(join(nm, entry, 'node_modules', 'better-sqlite3'));
76
+ }
77
+ }
78
+ };
79
+ for (const start of startDirs) {
80
+ let dir = resolve(start);
81
+ for (let i = 0; i < maxDepth; i++) {
82
+ scanNodeModules(join(dir, 'node_modules'));
83
+ const parent = dirname(dir);
84
+ if (parent === dir)
85
+ break;
86
+ dir = parent;
87
+ }
88
+ }
89
+ return [...found].sort();
90
+ }
91
+ const FIX = [
92
+ 'Use a Node release that contains nodejs/node#65943 (not in any release through 24.21.0), or Node 22 / 24.18.1;',
93
+ 'or rebuild each listed copy against 24.18.1 headers:',
94
+ 'cd <copy> && npx node-gyp rebuild --release --nodedir=<path to node 24.18.1 headers> ;',
95
+ 'or install the 12.x prebuilt (npm install better-sqlite3@12 in the project).',
96
+ 'ruflo itself opens AgentDB through its own prebuilt 12.x copy, so a flagged nested copy matters only to code that loads it directly.',
97
+ ].join(' ');
98
+ /** Read-only: warn when a locally compiled better-sqlite3 was built against Node >= 24.19 headers on an affected runtime. */
99
+ export async function checkNativeAddonHeaders(deps = {}) {
100
+ const nodeVersion = deps.nodeVersion ?? process.versions.node;
101
+ if (!isAffectedNodeVersion(nodeVersion)) {
102
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion} is not in the affected range (24.19.0-24.21.x)` };
103
+ }
104
+ const here = dirname(fileURLToPath(import.meta.url));
105
+ const copies = deps.copies ?? findBetterSqlite3Copies(deps.startDirs ?? [here, process.cwd()]);
106
+ if (copies.length === 0) {
107
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: no better-sqlite3 copy found to inspect` };
108
+ }
109
+ const bad = [];
110
+ for (const copy of copies) {
111
+ let gypi;
112
+ try {
113
+ gypi = readFileSync(join(copy, 'build', 'config.gypi'), 'utf8');
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ const headers = headersVersionFromGypi(gypi);
119
+ const parsed = headers ? parse(headers) : null;
120
+ if (!parsed || parsed[0] !== 24 || parsed[1] < 19)
121
+ continue;
122
+ const version = readJson(join(copy, 'package.json'))?.version;
123
+ bad.push(`${copy} (better-sqlite3 ${typeof version === 'string' ? version : '?'}, built against Node ${headers} headers)`);
124
+ }
125
+ if (bad.length === 0) {
126
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: ${copies.length} better-sqlite3 cop${copies.length === 1 ? 'y' : 'ies'} checked, none built against 24.19+ headers` };
127
+ }
128
+ return {
129
+ name: NAME,
130
+ status: 'warn',
131
+ message: `Node ${nodeVersion} can abort ("Assertion failed: (env) != nullptr", nodejs/node#65446) in: ${bad.join('; ')}`,
132
+ fix: FIX,
133
+ };
134
+ }
135
+ //# sourceMappingURL=doctor-native-addon.js.map
@@ -10,6 +10,7 @@ import { join, dirname } from 'path';
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
11
  import { createHash } from 'crypto';
12
12
  import { ownInstallAncestors } from '../plugins/own-install.js';
13
+ import { checkNativeAddonHeaders } from './doctor-native-addon.js';
13
14
  import { execSync, exec, execFile } from 'child_process';
14
15
  import { promisify } from 'util';
15
16
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
@@ -2607,6 +2608,7 @@ export const doctorCommand = {
2607
2608
  ];
2608
2609
  },
2609
2610
  'memory-package': checkMemoryPackageVersion, // #3392
2611
+ 'native-addon': () => checkNativeAddonHeaders(), // #4055
2610
2612
  'learning': checkLearningBridge, // #2545
2611
2613
  'learning-bridge': checkLearningBridge, // #2545
2612
2614
  'api': checkApiKeys,
@@ -2722,6 +2724,7 @@ export const doctorCommand = {
2722
2724
  checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
2723
2725
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
2724
2726
  checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
2727
+ () => checkNativeAddonHeaders(), // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
2725
2728
  checkApiKeys,
2726
2729
  checkMcpServers,
2727
2730
  checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
@@ -599,9 +599,16 @@ const postCommandCommand = {
599
599
  ttl: ctx.flags.ttl,
600
600
  timestamp: Date.now(),
601
601
  });
602
+ // #4056: recording switched off on purpose is not a failure.
603
+ const recordingOff = result.disabled === true;
602
604
  if (ctx.flags.format === 'json') {
603
605
  output.printJson(result);
604
- return { success: result.recorded, exitCode: result.recorded ? 0 : 1, data: result };
606
+ const ok = result.recorded || recordingOff;
607
+ return { success: ok, exitCode: ok ? 0 : 1, data: result };
608
+ }
609
+ if (recordingOff) {
610
+ output.printInfo('Command recording is switched off (CLAUDE_FLOW_HOOKS_RECORD_COMMANDS / CLAUDE_FLOW_HOOKS_ENABLED / hooks.recordCommands); nothing was stored.');
611
+ return { success: true, data: result };
605
612
  }
606
613
  if (!result.recorded) {
607
614
  output.printError('Command outcome could not be recorded');
@@ -90,6 +90,8 @@ const commandLoaders = {
90
90
  spinner: () => import('./spinner.js'),
91
91
  // ruflo as a Claude Code mod: function hooks, early access (ADR-404)
92
92
  mods: () => import('./mods.js'),
93
+ // ADR-489: the console in a terminal of its own (read-only Overview and Cost); loads a bundle from this CLI's own install.
94
+ tui: () => import('./tui.js'),
93
95
  // ADR-406 command catalog and mission control (interim homes until mods.ts follow-up)
94
96
  catalog: () => import('./catalog.js'),
95
97
  mission: () => import('./mission.js'),
@@ -3,10 +3,12 @@
3
3
  * Comprehensive initialization for Claude Flow with Claude Code integration
4
4
  */
5
5
  import type { Command } from '../types.js';
6
+ export declare function readCodexTrustFlag(flags: Record<string, unknown>): 'standard' | 'full' | undefined | null;
6
7
  export declare function runCodexInitializerCli(cwd: string, options: {
7
8
  template: string;
8
9
  force: boolean;
9
10
  dual: boolean;
11
+ codexTrust?: 'standard' | 'full';
10
12
  }): boolean;
11
13
  export declare const initCommand: Command;
12
14
  export default initCommand;