claude-flow 3.47.0 → 3.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +6 -3
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +0 -2
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.js +242 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +24 -41
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +73 -53
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
- package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
- package/v3/@claude-flow/cli/package.json +9 -9
- package/v3/@claude-flow/guidance/package.json +5 -6
- package/v3/@claude-flow/shared/package.json +6 -3
|
@@ -205,7 +205,9 @@ export interface MemoryInitResult {
|
|
|
205
205
|
* Ensure memory_entries table has all required columns
|
|
206
206
|
* Adds missing columns for older databases (e.g., 'content' column)
|
|
207
207
|
*/
|
|
208
|
-
export declare function ensureSchemaColumns(dbPath: string
|
|
208
|
+
export declare function ensureSchemaColumns(dbPath: string, options?: {
|
|
209
|
+
encryptWrites?: boolean;
|
|
210
|
+
}): Promise<{
|
|
209
211
|
success: boolean;
|
|
210
212
|
columnsAdded: string[];
|
|
211
213
|
error?: string;
|
|
@@ -496,6 +498,8 @@ export declare function listEntries(options: {
|
|
|
496
498
|
limit?: number;
|
|
497
499
|
offset?: number;
|
|
498
500
|
dbPath?: string;
|
|
501
|
+
/** Internal native-mirror writes must remain plaintext, including schema migration. */
|
|
502
|
+
encryptWrites?: boolean;
|
|
499
503
|
/** #2073: When true, include the entry's full `content` string in each result. */
|
|
500
504
|
includeContent?: boolean;
|
|
501
505
|
/** ADR-323: restrict rows to these provenance types. */
|
|
@@ -549,6 +553,8 @@ export declare function deleteEntry(options: {
|
|
|
549
553
|
key: string;
|
|
550
554
|
namespace?: string;
|
|
551
555
|
dbPath?: string;
|
|
556
|
+
/** Internal native-mirror writes must remain plaintext, including schema migration. */
|
|
557
|
+
encryptWrites?: boolean;
|
|
552
558
|
}): Promise<{
|
|
553
559
|
success: boolean;
|
|
554
560
|
deleted: boolean;
|
|
@@ -573,6 +579,8 @@ export declare function withMemoryDbLock<T>(dbPath: string, fn: () => Promise<T>
|
|
|
573
579
|
export declare function purgeNamespace(options: {
|
|
574
580
|
namespace: string;
|
|
575
581
|
dbPath?: string;
|
|
582
|
+
/** Internal native-mirror writes must remain plaintext, including schema migration. */
|
|
583
|
+
encryptWrites?: boolean;
|
|
576
584
|
}): Promise<{
|
|
577
585
|
success: boolean;
|
|
578
586
|
deletedCount: number;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module v3/cli/memory-initializer
|
|
10
10
|
*/
|
|
11
|
+
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -208,14 +209,14 @@ async function getBridge() {
|
|
|
208
209
|
* missing better-sqlite3. Appending the recorded reason turns an unactionable
|
|
209
210
|
* message into a diagnosis.
|
|
210
211
|
*/
|
|
211
|
-
async function walRefusalError(operation) {
|
|
212
|
+
async function walRefusalError(operation, bridgeDbPath) {
|
|
212
213
|
const base = 'memory database has an active native WAL connection '
|
|
213
214
|
+ '(found -wal/-shm sidecar files) — refusing an unsafe sql.js '
|
|
214
215
|
+ `whole-image ${operation}. Retry once the native writer completes, or `
|
|
215
216
|
+ 'restore the native better-sqlite3 bridge.';
|
|
216
217
|
try {
|
|
217
218
|
const bridge = await getBridge();
|
|
218
|
-
const reason = bridge?.getBridgeFailureReason?.();
|
|
219
|
+
const reason = bridge?.getBridgeFailureReason?.(bridgeDbPath);
|
|
219
220
|
if (reason)
|
|
220
221
|
return `${base} Bridge unavailable: ${reason}`;
|
|
221
222
|
}
|
|
@@ -1141,7 +1142,7 @@ INSERT OR IGNORE INTO vector_indexes (id, name, dimensions) VALUES
|
|
|
1141
1142
|
* Ensure memory_entries table has all required columns
|
|
1142
1143
|
* Adds missing columns for older databases (e.g., 'content' column)
|
|
1143
1144
|
*/
|
|
1144
|
-
export async function ensureSchemaColumns(dbPath) {
|
|
1145
|
+
export async function ensureSchemaColumns(dbPath, options = {}) {
|
|
1145
1146
|
const columnsAdded = [];
|
|
1146
1147
|
try {
|
|
1147
1148
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -1213,7 +1214,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
1213
1214
|
if (modified) {
|
|
1214
1215
|
// Save updated database
|
|
1215
1216
|
const data = db.export();
|
|
1216
|
-
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
1217
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
|
|
1217
1218
|
}
|
|
1218
1219
|
db.close();
|
|
1219
1220
|
return { success: true, columnsAdded };
|
|
@@ -1312,13 +1313,12 @@ async function activateControllerRegistry(verbose) {
|
|
|
1312
1313
|
if (!bridge) {
|
|
1313
1314
|
return { activated, failed, initTimeMs: performance.now() - startTime };
|
|
1314
1315
|
}
|
|
1315
|
-
const
|
|
1316
|
-
if (!
|
|
1316
|
+
const controllers = await bridge.bridgeListControllers();
|
|
1317
|
+
if (!controllers) {
|
|
1317
1318
|
return { activated, failed, initTimeMs: performance.now() - startTime };
|
|
1318
1319
|
}
|
|
1319
1320
|
// Collect controller status from the registry
|
|
1320
|
-
if (
|
|
1321
|
-
const controllers = registry.listControllers();
|
|
1321
|
+
if (controllers) {
|
|
1322
1322
|
for (const ctrl of controllers) {
|
|
1323
1323
|
if (ctrl.enabled) {
|
|
1324
1324
|
activated.push(ctrl.name);
|
|
@@ -1879,7 +1879,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
1879
1879
|
// Try to load with sql.js
|
|
1880
1880
|
const initSqlJs = (await import('sql.js')).default;
|
|
1881
1881
|
const SQL = await initSqlJs();
|
|
1882
|
-
const fileBuffer =
|
|
1882
|
+
const fileBuffer = readFileMaybeEncrypted(path_, null);
|
|
1883
1883
|
db = new SQL.Database(fileBuffer);
|
|
1884
1884
|
// Check for metadata table
|
|
1885
1885
|
const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
|
|
@@ -2579,7 +2579,7 @@ export async function storeEntry(options) {
|
|
|
2579
2579
|
return {
|
|
2580
2580
|
success: false,
|
|
2581
2581
|
id: '',
|
|
2582
|
-
error: await walRefusalError('write'),
|
|
2582
|
+
error: await walRefusalError('write', options.dbPath),
|
|
2583
2583
|
};
|
|
2584
2584
|
}
|
|
2585
2585
|
const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
@@ -2751,8 +2751,8 @@ export async function searchEntries(options) {
|
|
|
2751
2751
|
// query (no extra round-trip) so a provenance-filtered search
|
|
2752
2752
|
// still gets RaBitQ's speedup instead of falling back to brute
|
|
2753
2753
|
// force.
|
|
2754
|
-
const stmt = db.prepare(
|
|
2755
|
-
stmt.bind([candidate.id
|
|
2754
|
+
const stmt = db.prepare(`SELECT content, embedding, provenance_type FROM memory_entries WHERE id = ? AND ${liveMemoryRowSql()}`);
|
|
2755
|
+
stmt.bind([candidate.id]);
|
|
2756
2756
|
if (stmt.step()) {
|
|
2757
2757
|
const [content, embeddingJson, provenanceTypeVal] = stmt.get();
|
|
2758
2758
|
if (provenanceFilter?.length && !provenanceFilter.includes(provenanceTypeVal || 'unknown')) {
|
|
@@ -2812,29 +2812,28 @@ export async function searchEntries(options) {
|
|
|
2812
2812
|
const db = new SQL.Database(fileBuffer);
|
|
2813
2813
|
const provenanceByKey = new Map();
|
|
2814
2814
|
for (const r of filtered) {
|
|
2815
|
-
const stmt = db.prepare(
|
|
2815
|
+
const stmt = db.prepare(`SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? AND ${liveMemoryRowSql()} LIMIT 1`);
|
|
2816
2816
|
stmt.bind([r.namespace, r.key]);
|
|
2817
2817
|
if (stmt.step()) {
|
|
2818
|
-
provenanceByKey.set(
|
|
2818
|
+
provenanceByKey.set(JSON.stringify([r.namespace, r.key]), stmt.get()[0] || 'unknown');
|
|
2819
2819
|
}
|
|
2820
2820
|
stmt.free();
|
|
2821
2821
|
}
|
|
2822
2822
|
db.close();
|
|
2823
2823
|
filtered = filtered
|
|
2824
|
-
.
|
|
2824
|
+
.filter(r => provenanceByKey.has(JSON.stringify([r.namespace, r.key])))
|
|
2825
|
+
.map(r => ({ ...r, provenanceType: provenanceByKey.get(JSON.stringify([r.namespace, r.key])) || 'unknown' }));
|
|
2825
2826
|
if (provenanceFilter?.length) {
|
|
2826
2827
|
filtered = filtered.filter(r => provenanceFilter.includes(r.provenanceType));
|
|
2827
2828
|
}
|
|
2828
2829
|
}
|
|
2829
2830
|
catch {
|
|
2830
|
-
//
|
|
2831
|
-
//
|
|
2832
|
-
filtered =
|
|
2833
|
-
? []
|
|
2834
|
-
: filtered.map(r => ({ ...r, provenanceType: 'unknown' }));
|
|
2831
|
+
// An ANN hit alone cannot prove the row is still live. Fall back
|
|
2832
|
+
// to the authoritative SQL scan if liveness cannot be checked.
|
|
2833
|
+
filtered = [];
|
|
2835
2834
|
}
|
|
2836
2835
|
}
|
|
2837
|
-
if (
|
|
2836
|
+
if (filtered.length >= limit) {
|
|
2838
2837
|
return {
|
|
2839
2838
|
success: true,
|
|
2840
2839
|
results: filtered.slice(0, limit),
|
|
@@ -2853,7 +2852,7 @@ export async function searchEntries(options) {
|
|
|
2853
2852
|
// Get entries with embeddings
|
|
2854
2853
|
// ADR-323: build the WHERE clause incrementally so namespace and
|
|
2855
2854
|
// provenance filters compose (both, either, or neither).
|
|
2856
|
-
const whereClauses = [
|
|
2855
|
+
const whereClauses = [liveMemoryRowSql()];
|
|
2857
2856
|
const whereParams = [];
|
|
2858
2857
|
if (effectiveNamespace !== 'all') {
|
|
2859
2858
|
whereClauses.push('namespace = ?');
|
|
@@ -2980,8 +2979,14 @@ export async function listEntries(options) {
|
|
|
2980
2979
|
if (!fs.existsSync(dbPath)) {
|
|
2981
2980
|
return { success: false, entries: [], total: 0, error: 'Database not found' };
|
|
2982
2981
|
}
|
|
2982
|
+
// Listing can migrate/backfill the schema, so it is also a whole-image
|
|
2983
|
+
// writer. The newly selected native mirror may still have a live WAL.
|
|
2984
|
+
await releaseOwnNativeHandle(dbPath);
|
|
2985
|
+
if (hasNativeWalSidecars(dbPath)) {
|
|
2986
|
+
return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
|
|
2987
|
+
}
|
|
2983
2988
|
// Ensure schema has all required columns (migration for older DBs)
|
|
2984
|
-
await ensureSchemaColumns(dbPath);
|
|
2989
|
+
await ensureSchemaColumns(dbPath, options);
|
|
2985
2990
|
const initSqlJs = (await import('sql.js')).default;
|
|
2986
2991
|
const SQL = await initSqlJs();
|
|
2987
2992
|
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
@@ -2990,7 +2995,7 @@ export async function listEntries(options) {
|
|
|
2990
2995
|
// that predate the status column may have NULL after migration.
|
|
2991
2996
|
// See memory-bridge.ts:bridgeListEntries for full context.
|
|
2992
2997
|
// Get total count
|
|
2993
|
-
const whereClauses = [
|
|
2998
|
+
const whereClauses = [liveMemoryRowSql()];
|
|
2994
2999
|
const whereParams = [];
|
|
2995
3000
|
if (namespace) {
|
|
2996
3001
|
whereClauses.push('namespace = ?');
|
|
@@ -3088,7 +3093,7 @@ export async function getEntry(options) {
|
|
|
3088
3093
|
return {
|
|
3089
3094
|
success: false,
|
|
3090
3095
|
found: false,
|
|
3091
|
-
error: await walRefusalError('read/write'),
|
|
3096
|
+
error: await walRefusalError('read/write', options.dbPath),
|
|
3092
3097
|
};
|
|
3093
3098
|
}
|
|
3094
3099
|
// #2878: this is a mutator, not a reader — the access_count bump below
|
|
@@ -3107,7 +3112,7 @@ export async function getEntry(options) {
|
|
|
3107
3112
|
const getStmt = db.prepare(`
|
|
3108
3113
|
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
|
|
3109
3114
|
FROM memory_entries
|
|
3110
|
-
WHERE ${
|
|
3115
|
+
WHERE ${liveMemoryRowSql()}
|
|
3111
3116
|
AND key = ?
|
|
3112
3117
|
AND namespace = ?
|
|
3113
3118
|
LIMIT 1
|
|
@@ -3211,14 +3216,14 @@ export async function deleteEntry(options) {
|
|
|
3211
3216
|
key,
|
|
3212
3217
|
namespace,
|
|
3213
3218
|
remainingEntries: 0,
|
|
3214
|
-
error: await walRefusalError('write'),
|
|
3219
|
+
error: await walRefusalError('write', options.dbPath),
|
|
3215
3220
|
};
|
|
3216
3221
|
}
|
|
3217
3222
|
// #2878: whole-image read-modify-write — without the lock a concurrent
|
|
3218
3223
|
// writer's flush resurrects the row this call just tombstoned.
|
|
3219
3224
|
return await withMemoryDbLock(dbPath, async () => {
|
|
3220
3225
|
// Ensure schema has all required columns (migration for older DBs)
|
|
3221
|
-
await ensureSchemaColumns(dbPath);
|
|
3226
|
+
await ensureSchemaColumns(dbPath, options);
|
|
3222
3227
|
const initSqlJs = (await import('sql.js')).default;
|
|
3223
3228
|
const SQL = await initSqlJs();
|
|
3224
3229
|
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
@@ -3268,7 +3273,7 @@ export async function deleteEntry(options) {
|
|
|
3268
3273
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3269
3274
|
// Save updated database
|
|
3270
3275
|
const data = db.export();
|
|
3271
|
-
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
3276
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
|
|
3272
3277
|
db.close();
|
|
3273
3278
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
3274
3279
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
@@ -3420,7 +3425,12 @@ export async function purgeNamespace(options) {
|
|
|
3420
3425
|
if (!fs.existsSync(dbPath)) {
|
|
3421
3426
|
return { success: false, deletedCount: 0, remainingEntries: 0, error: 'Database not found' };
|
|
3422
3427
|
}
|
|
3423
|
-
|
|
3428
|
+
// Recheck at mutation time even when the CLI already read a preview.
|
|
3429
|
+
await releaseOwnNativeHandle(dbPath);
|
|
3430
|
+
if (hasNativeWalSidecars(dbPath)) {
|
|
3431
|
+
return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
|
|
3432
|
+
}
|
|
3433
|
+
await ensureSchemaColumns(dbPath, options);
|
|
3424
3434
|
const initSqlJs = (await import('sql.js')).default;
|
|
3425
3435
|
const SQL = await initSqlJs();
|
|
3426
3436
|
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
@@ -3431,7 +3441,7 @@ export async function purgeNamespace(options) {
|
|
|
3431
3441
|
const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
|
|
3432
3442
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3433
3443
|
const data = db.export();
|
|
3434
|
-
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
3444
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
|
|
3435
3445
|
db.close();
|
|
3436
3446
|
if (deletedCount > 0 && hnswIndex?.entries) {
|
|
3437
3447
|
for (const [id, entry] of hnswIndex.entries) {
|
|
@@ -96,7 +96,10 @@ async function loadRuVector() {
|
|
|
96
96
|
function extractImports(content, _filePath) {
|
|
97
97
|
const imports = [];
|
|
98
98
|
// ES6 import statements
|
|
99
|
-
|
|
99
|
+
// Keep the import clause's shape finite. Repeating an optional separator
|
|
100
|
+
// around alternatives lets ordinary prose such as "import one two three ..."
|
|
101
|
+
// backtrack exponentially when no `from` clause follows.
|
|
102
|
+
const esImportRegex = /\bimport\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?(?:\{[^}]*\}|\*\s+as\s+[\w$]+|[\w$]+)\s+from\s*['"]([^'"]+)['"]/g;
|
|
100
103
|
let match;
|
|
101
104
|
while ((match = esImportRegex.exec(content)) !== null) {
|
|
102
105
|
imports.push({ path: match[1], type: 'import' });
|
|
@@ -80,12 +80,23 @@ export class ConfigFileManager {
|
|
|
80
80
|
}
|
|
81
81
|
try {
|
|
82
82
|
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
83
|
-
|
|
83
|
+
const parsed = JSON.parse(content);
|
|
84
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
85
|
+
throw new Error('Config file must contain a JSON object');
|
|
86
|
+
}
|
|
87
|
+
this.config = parsed;
|
|
84
88
|
return this.config;
|
|
85
89
|
}
|
|
86
|
-
catch {
|
|
90
|
+
catch (error) {
|
|
87
91
|
this.config = null;
|
|
88
|
-
|
|
92
|
+
if (error.code === 'ENOENT') {
|
|
93
|
+
this.configPath = null;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// A parse/read failure is not a missing config. In particular, set()
|
|
97
|
+
// must never replace a recoverable file with fallback defaults.
|
|
98
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new Error(`Failed to load config ${this.configPath}: ${detail}`);
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
/** Get the current config, loading if needed */
|
|
@@ -98,12 +109,23 @@ export class ConfigFileManager {
|
|
|
98
109
|
/** Get a nested config value by dot-separated key */
|
|
99
110
|
get(cwd, key) {
|
|
100
111
|
const config = this.getConfig(cwd);
|
|
101
|
-
return getNestedValue(config, key);
|
|
112
|
+
return Object.hasOwn(config, key) ? config[key] : getNestedValue(config, key);
|
|
102
113
|
}
|
|
103
114
|
/** Set a nested config value by dot-separated key */
|
|
104
115
|
set(cwd, key, value) {
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
// Reject the entire path before touching cached config or disk.
|
|
117
|
+
if (key.split('.').some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
|
|
118
|
+
throw new Error(`Unsafe configuration key: ${key}`);
|
|
119
|
+
}
|
|
120
|
+
// A targeted update must not persist unrelated defaults (notably the
|
|
121
|
+
// default memory path, which would relocate an existing memory store).
|
|
122
|
+
const config = this.config ?? this.load(cwd) ?? {};
|
|
123
|
+
if (Object.hasOwn(config, key)) {
|
|
124
|
+
config[key] = value;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
setNestedValue(config, key, value);
|
|
128
|
+
}
|
|
107
129
|
this.config = config;
|
|
108
130
|
const targetPath = this.configPath ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
|
|
109
131
|
this.writeAtomic(targetPath, config);
|
|
@@ -181,7 +203,7 @@ function getNestedValue(obj, key) {
|
|
|
181
203
|
const parts = key.split('.');
|
|
182
204
|
let current = obj;
|
|
183
205
|
for (const part of parts) {
|
|
184
|
-
if (current === null || current === undefined || typeof current !== 'object') {
|
|
206
|
+
if (current === null || current === undefined || typeof current !== 'object' || !Object.hasOwn(current, part)) {
|
|
185
207
|
return undefined;
|
|
186
208
|
}
|
|
187
209
|
current = current[part];
|
|
@@ -194,7 +216,7 @@ function setNestedValue(obj, key, value) {
|
|
|
194
216
|
let current = obj;
|
|
195
217
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
196
218
|
const part = parts[i];
|
|
197
|
-
if (!(part
|
|
219
|
+
if (!Object.hasOwn(current, part) || typeof current[part] !== 'object' || current[part] === null) {
|
|
198
220
|
current[part] = {};
|
|
199
221
|
}
|
|
200
222
|
current = current[part];
|
|
@@ -25,7 +25,7 @@ export interface RestoreResult {
|
|
|
25
25
|
restored: boolean;
|
|
26
26
|
/** The backup file that was restored. */
|
|
27
27
|
from?: string;
|
|
28
|
-
/** memory_entries count in the restored DB
|
|
28
|
+
/** Verified memory_entries count in the restored DB. */
|
|
29
29
|
rows?: number;
|
|
30
30
|
/** Where the corrupt live DB was parked before the swap. */
|
|
31
31
|
corruptBackupPath?: string;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { readFileMaybeEncrypted } from '../fs-secure.js';
|
|
17
18
|
export function defaultMemoryDbPath(cwd = process.cwd()) {
|
|
18
19
|
return path.join(cwd, '.swarm', 'memory.db');
|
|
19
20
|
}
|
|
@@ -21,6 +22,19 @@ export function defaultMemoryDbPath(cwd = process.cwd()) {
|
|
|
21
22
|
function fileStamp(ms) {
|
|
22
23
|
return new Date(ms).toISOString().replace(/[:.]/g, '-');
|
|
23
24
|
}
|
|
25
|
+
/** Keep legacy memory.db names; give every other basename its own snapshot set. */
|
|
26
|
+
function snapshotPrefix(dbPath) {
|
|
27
|
+
const name = path.basename(dbPath);
|
|
28
|
+
// A distinct namespace prevents an extensionless `memory` database from
|
|
29
|
+
// colliding with the historical `memory.db` prefix. Encoding is injective
|
|
30
|
+
// within the nondefault namespace, including names that contain `%`.
|
|
31
|
+
return name === 'memory.db' ? 'memory-' : `store-${encodeURIComponent(name)}-`;
|
|
32
|
+
}
|
|
33
|
+
function isSnapshotFor(file, dbPath) {
|
|
34
|
+
const prefix = snapshotPrefix(dbPath);
|
|
35
|
+
return file.startsWith(prefix)
|
|
36
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.db$/.test(file.slice(prefix.length));
|
|
37
|
+
}
|
|
24
38
|
export async function backupMemoryDb(opts = {}) {
|
|
25
39
|
const dbPath = opts.dbPath ?? defaultMemoryDbPath();
|
|
26
40
|
if (!dbPath || !fs.existsSync(dbPath))
|
|
@@ -38,7 +52,7 @@ export async function backupMemoryDb(opts = {}) {
|
|
|
38
52
|
fs.mkdirSync(destDir, { recursive: true });
|
|
39
53
|
}
|
|
40
54
|
catch { /* */ }
|
|
41
|
-
const destPath = path.join(destDir,
|
|
55
|
+
const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
|
|
42
56
|
// WAL-safe online backup: read-only source, consistent snapshot to destPath.
|
|
43
57
|
let db;
|
|
44
58
|
try {
|
|
@@ -68,7 +82,7 @@ export async function backupMemoryDb(opts = {}) {
|
|
|
68
82
|
const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
|
|
69
83
|
const rotatedAway = [];
|
|
70
84
|
try {
|
|
71
|
-
const snaps = fs.readdirSync(destDir).filter(f =>
|
|
85
|
+
const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
|
|
72
86
|
while (snaps.length > keep) {
|
|
73
87
|
const old = snaps.shift();
|
|
74
88
|
try {
|
|
@@ -107,7 +121,7 @@ export async function backupMemoryDb(opts = {}) {
|
|
|
107
121
|
const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
|
|
108
122
|
const rotatedAway = [];
|
|
109
123
|
try {
|
|
110
|
-
const snaps = fs.readdirSync(destDir).filter(f =>
|
|
124
|
+
const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
|
|
111
125
|
while (snaps.length > keep) {
|
|
112
126
|
const old = snaps.shift();
|
|
113
127
|
try {
|
|
@@ -136,6 +150,48 @@ export async function backupMemoryDb(opts = {}) {
|
|
|
136
150
|
}
|
|
137
151
|
return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri };
|
|
138
152
|
}
|
|
153
|
+
/** Verify a snapshot without modifying it, including RFE1 images and WASM-only hosts. */
|
|
154
|
+
async function verifiedBackupRows(file, Database) {
|
|
155
|
+
if (Database) {
|
|
156
|
+
let db;
|
|
157
|
+
try {
|
|
158
|
+
db = new Database(file, { readonly: true });
|
|
159
|
+
if (String(db.pragma('integrity_check', { simple: true })).toLowerCase() !== 'ok')
|
|
160
|
+
return null;
|
|
161
|
+
const rows = Number(db.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0);
|
|
162
|
+
return rows > 0 ? rows : null;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Native SQLite cannot open encrypted RFE1 images. Verify the decrypted
|
|
166
|
+
// bytes below; a failed native open is never evidence of a good backup.
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
try {
|
|
170
|
+
db?.close();
|
|
171
|
+
}
|
|
172
|
+
catch { /* best effort */ }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let db;
|
|
176
|
+
try {
|
|
177
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
178
|
+
const SQL = await initSqlJs();
|
|
179
|
+
db = new SQL.Database(readFileMaybeEncrypted(file, null));
|
|
180
|
+
const integrity = db.exec('PRAGMA integrity_check')[0]?.values?.[0]?.[0];
|
|
181
|
+
const rows = Number(db.exec('SELECT COUNT(*) FROM memory_entries')[0]?.values?.[0]?.[0] ?? 0);
|
|
182
|
+
return String(integrity).toLowerCase() === 'ok' && rows > 0 ? rows : null;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// No verifier, wrong key, failed authentication, or invalid SQLite image.
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
db?.close();
|
|
191
|
+
}
|
|
192
|
+
catch { /* best effort */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
139
195
|
/**
|
|
140
196
|
* Restore the newest integrity-ok backup over a corrupt/malformed memory DB
|
|
141
197
|
* (issue #2584).
|
|
@@ -158,7 +214,7 @@ export async function restoreMemoryDbFromBackup(dbPath, opts = {}) {
|
|
|
158
214
|
try {
|
|
159
215
|
snaps = fs
|
|
160
216
|
.readdirSync(destDir)
|
|
161
|
-
.filter(f =>
|
|
217
|
+
.filter(f => isSnapshotFor(f, dbPath))
|
|
162
218
|
.map(f => path.join(destDir, f))
|
|
163
219
|
.sort() // ISO-stamped names sort chronologically
|
|
164
220
|
.reverse(); // newest first
|
|
@@ -168,39 +224,23 @@ export async function restoreMemoryDbFromBackup(dbPath, opts = {}) {
|
|
|
168
224
|
}
|
|
169
225
|
if (!snaps.length)
|
|
170
226
|
return { restored: false, skipped: 'no-backups' };
|
|
171
|
-
//
|
|
172
|
-
//
|
|
227
|
+
// Prefer native verification for plaintext snapshots. sql.js verifies
|
|
228
|
+
// encrypted snapshots and provides the same checks on WASM-only hosts.
|
|
173
229
|
let Database = null;
|
|
174
230
|
try {
|
|
175
231
|
const mod = 'better-sqlite3';
|
|
176
232
|
Database = (await import(mod)).default;
|
|
177
233
|
}
|
|
178
234
|
catch {
|
|
179
|
-
/*
|
|
235
|
+
/* use the sql.js verifier */
|
|
180
236
|
}
|
|
181
237
|
let chosen = null;
|
|
182
238
|
for (const file of snaps) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let rows = 0;
|
|
188
|
-
try {
|
|
189
|
-
rows = db.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0;
|
|
190
|
-
}
|
|
191
|
-
catch { /* no entries table — not a usable memory DB */ }
|
|
192
|
-
db.close();
|
|
193
|
-
if (integ.toLowerCase() === 'ok' && rows > 0) {
|
|
194
|
-
chosen = { file, rows };
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else if (fs.statSync(file).size > 0) {
|
|
199
|
-
chosen = { file, rows: -1 };
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
239
|
+
const rows = await verifiedBackupRows(file, Database);
|
|
240
|
+
if (rows !== null) {
|
|
241
|
+
chosen = { file, rows };
|
|
242
|
+
break;
|
|
202
243
|
}
|
|
203
|
-
catch { /* unreadable snapshot — try the next-older one */ }
|
|
204
244
|
}
|
|
205
245
|
if (!chosen)
|
|
206
246
|
return { restored: false, skipped: 'no-integrity-ok-backup' };
|
|
@@ -124,8 +124,8 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
124
124
|
static isWslEnvironment(): boolean;
|
|
125
125
|
static getEffectiveCpuCount(): number;
|
|
126
126
|
/**
|
|
127
|
-
* Read daemon-specific config from .claude-flow/config.{
|
|
128
|
-
* Supports
|
|
127
|
+
* Read daemon-specific config from CLI JSON paths or .claude-flow/config.{yaml,yml}.
|
|
128
|
+
* Supports dotted and nested keys like 'daemon.resourceThresholds.maxCpuLoad'.
|
|
129
129
|
* #1844: prefer JSON when both exist (existing behavior) but fall back
|
|
130
130
|
* to YAML so operators using the v3 canonical YAML format aren't silently
|
|
131
131
|
* ignored. The chosen path (or a parse warning) is returned rather than
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
|
|
11
11
|
* - testgaps: Test coverage analysis (20 min interval)
|
|
12
12
|
*/
|
|
13
|
+
import { ConfigFileManager } from './config-file-manager.js';
|
|
13
14
|
import { EventEmitter } from 'events';
|
|
14
15
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
|
|
15
16
|
import { cpus } from 'os';
|
|
@@ -339,8 +340,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
339
340
|
return cpus().length || 1;
|
|
340
341
|
}
|
|
341
342
|
/**
|
|
342
|
-
* Read daemon-specific config from .claude-flow/config.{
|
|
343
|
-
* Supports
|
|
343
|
+
* Read daemon-specific config from CLI JSON paths or .claude-flow/config.{yaml,yml}.
|
|
344
|
+
* Supports dotted and nested keys like 'daemon.resourceThresholds.maxCpuLoad'.
|
|
344
345
|
* #1844: prefer JSON when both exist (existing behavior) but fall back
|
|
345
346
|
* to YAML so operators using the v3 canonical YAML format aren't silently
|
|
346
347
|
* ignored. The chosen path (or a parse warning) is returned rather than
|
|
@@ -352,13 +353,14 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
352
353
|
* The caller logs these once `this.config` exists.
|
|
353
354
|
*/
|
|
354
355
|
readDaemonConfigFromFile(claudeFlowDir) {
|
|
355
|
-
|
|
356
|
+
// Share the CLI's JSON search order, including root and explicit config files.
|
|
357
|
+
const jsonPath = new ConfigFileManager().findConfig(join(claudeFlowDir, '..'));
|
|
356
358
|
const yamlPath = join(claudeFlowDir, 'config.yaml');
|
|
357
359
|
const ymlPath = join(claudeFlowDir, 'config.yml');
|
|
358
360
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
359
361
|
let raw;
|
|
360
362
|
let chosenPath;
|
|
361
|
-
if (
|
|
363
|
+
if (jsonPath) {
|
|
362
364
|
try {
|
|
363
365
|
raw = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
364
366
|
chosenPath = jsonPath;
|
|
@@ -390,21 +392,28 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
390
392
|
return {};
|
|
391
393
|
}
|
|
392
394
|
try {
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
// The CLI writes nested objects; legacy files use dotted keys. Scoped
|
|
396
|
+
// values retain precedence, including explicit false and zero.
|
|
397
|
+
const read = (obj, key) => {
|
|
398
|
+
if (!obj)
|
|
399
|
+
return undefined;
|
|
400
|
+
return obj[key] ?? key.split('.').reduce((value, part) => value && typeof value === 'object' && Object.hasOwn(value, part) ? value[part] : undefined, obj);
|
|
401
|
+
};
|
|
402
|
+
const get = (key) => read(raw?.scopes?.project, key) ?? read(raw, key);
|
|
403
|
+
const rawAutoStart = get('daemon.autoStart');
|
|
404
|
+
const rawCpuLoad = get('daemon.resourceThresholds.maxCpuLoad');
|
|
405
|
+
const rawMinMem = get('daemon.resourceThresholds.minFreeMemoryPercent');
|
|
406
|
+
const rawMaxConcurrent = get('daemon.maxConcurrent');
|
|
407
|
+
const rawTimeout = get('daemon.workerTimeoutMs');
|
|
399
408
|
// #2356 — lifecycle limits are configured in SECONDS in config.json
|
|
400
409
|
// (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
|
|
401
410
|
// and env var; stored internally as ms. An explicit 0 disables.
|
|
402
|
-
const rawTtl =
|
|
403
|
-
const rawIdle =
|
|
411
|
+
const rawTtl = get('daemon.ttlSecs');
|
|
412
|
+
const rawIdle = get('daemon.idleSecs');
|
|
404
413
|
// #2661 — explicit opt-in for scheduled AI workers.
|
|
405
|
-
const rawAiEnabled =
|
|
414
|
+
const rawAiEnabled = get('daemon.aiWorkers.enabled');
|
|
406
415
|
return {
|
|
407
|
-
autoStart: typeof
|
|
416
|
+
autoStart: typeof rawAutoStart === 'boolean' ? rawAutoStart : undefined,
|
|
408
417
|
maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
|
|
409
418
|
workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
|
|
410
419
|
maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -98,13 +98,13 @@
|
|
|
98
98
|
"vitest": "^4.1.0"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@claude-flow/cli-core": "3.7.0
|
|
101
|
+
"@claude-flow/cli-core": "3.7.0",
|
|
102
102
|
"@claude-flow/codex": "3.0.3",
|
|
103
|
-
"@claude-flow/mcp": "3.0.0
|
|
104
|
-
"@claude-flow/neural": "3.0.0
|
|
105
|
-
"@claude-flow/plugin-agent-federation": "1.0.0
|
|
106
|
-
"@claude-flow/security": "3.0.0
|
|
107
|
-
"@claude-flow/shared": "3.0.0
|
|
103
|
+
"@claude-flow/mcp": "3.0.0",
|
|
104
|
+
"@claude-flow/neural": "3.0.0",
|
|
105
|
+
"@claude-flow/plugin-agent-federation": "1.0.0",
|
|
106
|
+
"@claude-flow/security": "3.0.0",
|
|
107
|
+
"@claude-flow/shared": "3.0.0",
|
|
108
108
|
"@iarna/toml": "^2.2.5",
|
|
109
109
|
"@noble/ed25519": "2.3.0",
|
|
110
110
|
"@ruvector/rabitq-wasm": "0.1.0",
|
|
@@ -125,11 +125,11 @@
|
|
|
125
125
|
"ws": "^8.21.0",
|
|
126
126
|
"yaml": "^2.8.0",
|
|
127
127
|
"zod": "^3.22.0",
|
|
128
|
-
"@claude-flow/memory": "3.0.0
|
|
128
|
+
"@claude-flow/memory": "3.0.0"
|
|
129
129
|
},
|
|
130
130
|
"optionalDependencies": {
|
|
131
131
|
"@agntcy/slim-bindings": "2.0.0-alpha.5",
|
|
132
|
-
"@claude-flow/memory": "3.0.0
|
|
132
|
+
"@claude-flow/memory": "3.0.0",
|
|
133
133
|
"@metaharness/darwin": "~0.10.2",
|
|
134
134
|
"@metaharness/flywheel": "~0.1.10",
|
|
135
135
|
"@metaharness/radio": "~0.1.0",
|