claude-flow 3.59.6 → 3.59.8
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/proven-config.json +1 -1
- package/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/config-tools.js +4 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +3 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +25 -2
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +15 -2
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +29 -2
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -339,10 +339,12 @@ export const configTools = [
|
|
|
339
339
|
merged.set(key, { value, source: 'stored' });
|
|
340
340
|
}
|
|
341
341
|
// Always include keys from every scope so they're discoverable; the
|
|
342
|
-
// scope filter only narrows which set is used as the *winner*.
|
|
342
|
+
// scope filter only narrows which set is used as the *winner*. The
|
|
343
|
+
// default scope has no scope of its own, so stored values win there,
|
|
344
|
+
// as in config_get and config_export.
|
|
343
345
|
for (const [scopeName, scopeValues] of Object.entries(store.scopes)) {
|
|
344
346
|
for (const [key, value] of Object.entries(scopeValues)) {
|
|
345
|
-
if (scope === scopeName
|
|
347
|
+
if (scope === scopeName) {
|
|
346
348
|
merged.set(key, { value, source: `scope:${scopeName}` });
|
|
347
349
|
}
|
|
348
350
|
else if (!merged.has(key)) {
|
|
@@ -502,6 +502,8 @@ export const sessionTools = [
|
|
|
502
502
|
else if (sortBy === 'size') {
|
|
503
503
|
sessions.sort((a, b) => (b.stats?.totalSize ?? 0) - (a.stats?.totalSize ?? 0));
|
|
504
504
|
}
|
|
505
|
+
// Count every session before applying the limit so callers can tell when the list was truncated
|
|
506
|
+
const total = sessions.length;
|
|
505
507
|
// Apply limit
|
|
506
508
|
const limit = input.limit || 10;
|
|
507
509
|
sessions = sessions.slice(0, limit);
|
|
@@ -522,7 +524,7 @@ export const sessionTools = [
|
|
|
522
524
|
projection.metrics = s.metrics;
|
|
523
525
|
return projection;
|
|
524
526
|
}),
|
|
525
|
-
total
|
|
527
|
+
total,
|
|
526
528
|
limit,
|
|
527
529
|
};
|
|
528
530
|
},
|
|
@@ -1659,12 +1659,29 @@ export async function bridgeDeleteEntry(options) {
|
|
|
1659
1659
|
if (!guardResult.allowed) {
|
|
1660
1660
|
return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `MutationGuard rejected: ${guardResult.reason}` };
|
|
1661
1661
|
}
|
|
1662
|
-
//
|
|
1662
|
+
// #3785: secure_delete zeroes the old cells while the payload columns are
|
|
1663
|
+
// scrubbed; otherwise SQLite can leave the deleted text in free pages.
|
|
1664
|
+
try {
|
|
1665
|
+
ctx.db.pragma('secure_delete = ON');
|
|
1666
|
+
}
|
|
1667
|
+
catch { /* non-fatal: the logical scrub below still hides the row */ }
|
|
1668
|
+
// Soft delete using parameterized query. The tombstone must keep only
|
|
1669
|
+
// what later upserts/audits need: the payload columns are scrubbed here,
|
|
1670
|
+
// not merely hidden by the status filter. `content` is NOT NULL in the
|
|
1671
|
+
// schema, so "scrubbed" is the empty string rather than SQL NULL; that
|
|
1672
|
+
// still removes the deleted value from disk and from any raw SQLite read.
|
|
1663
1673
|
let changes = 0;
|
|
1664
1674
|
try {
|
|
1665
1675
|
const result = ctx.db.prepare(`
|
|
1666
1676
|
UPDATE memory_entries
|
|
1667
|
-
SET status = 'deleted',
|
|
1677
|
+
SET status = 'deleted',
|
|
1678
|
+
content = '',
|
|
1679
|
+
embedding = NULL,
|
|
1680
|
+
embedding_model = NULL,
|
|
1681
|
+
embedding_dimensions = NULL,
|
|
1682
|
+
tags = NULL,
|
|
1683
|
+
metadata = NULL,
|
|
1684
|
+
updated_at = ?
|
|
1668
1685
|
WHERE key = ? AND namespace = ? AND ${ACTIVE_MEMORY_ROW_SQL}
|
|
1669
1686
|
`).run(Date.now(), key, namespace);
|
|
1670
1687
|
changes = result?.changes ?? 0;
|
|
@@ -1737,6 +1754,12 @@ export async function bridgePurgeNamespace(options) {
|
|
|
1737
1754
|
}
|
|
1738
1755
|
let deletedCount = 0;
|
|
1739
1756
|
try {
|
|
1757
|
+
// #3785: a hard DELETE without secure_delete leaves the purged payload
|
|
1758
|
+
// in free pages; purge claims to be irreversible.
|
|
1759
|
+
try {
|
|
1760
|
+
ctx.db.pragma('secure_delete = ON');
|
|
1761
|
+
}
|
|
1762
|
+
catch { /* non-fatal: the rows are still removed below */ }
|
|
1740
1763
|
const result = ctx.db.prepare(`DELETE FROM memory_entries WHERE namespace = ?`).run(namespace);
|
|
1741
1764
|
deletedCount = result?.changes ?? 0;
|
|
1742
1765
|
}
|
|
@@ -3338,6 +3338,10 @@ export async function deleteEntry(options) {
|
|
|
3338
3338
|
const SQL = await initSqlJs();
|
|
3339
3339
|
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
3340
3340
|
const db = new SQL.Database(fileBuffer);
|
|
3341
|
+
// #3785: zero the old cells as well as the live row. Without
|
|
3342
|
+
// secure_delete, UPDATE leaves the previous text in SQLite free pages,
|
|
3343
|
+
// so the "deleted" value is still recoverable from the file image.
|
|
3344
|
+
db.run('PRAGMA secure_delete = ON');
|
|
3341
3345
|
// Check if entry exists first
|
|
3342
3346
|
const checkStmt = db.prepare(`
|
|
3343
3347
|
SELECT id FROM memory_entries
|
|
@@ -3367,12 +3371,19 @@ export async function deleteEntry(options) {
|
|
|
3367
3371
|
error: `Key '${key}' not found in namespace '${namespace}'`
|
|
3368
3372
|
};
|
|
3369
3373
|
}
|
|
3370
|
-
//
|
|
3371
|
-
//
|
|
3374
|
+
// Soft delete by setting status to 'deleted'. Scrub the payload, not
|
|
3375
|
+
// only the vector: a tombstone that keeps `content` on disk still
|
|
3376
|
+
// exposes deleted data to anyone who opens the SQLite file. `content`
|
|
3377
|
+
// is NOT NULL, so use the empty string rather than SQL NULL.
|
|
3372
3378
|
db.run(`
|
|
3373
3379
|
UPDATE memory_entries
|
|
3374
3380
|
SET status = 'deleted',
|
|
3381
|
+
content = '',
|
|
3375
3382
|
embedding = NULL,
|
|
3383
|
+
embedding_model = NULL,
|
|
3384
|
+
embedding_dimensions = NULL,
|
|
3385
|
+
tags = NULL,
|
|
3386
|
+
metadata = NULL,
|
|
3376
3387
|
updated_at = strftime('%s', 'now') * 1000
|
|
3377
3388
|
WHERE key = ?
|
|
3378
3389
|
AND namespace = ?
|
|
@@ -3547,6 +3558,8 @@ export async function purgeNamespace(options) {
|
|
|
3547
3558
|
const SQL = await initSqlJs();
|
|
3548
3559
|
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
3549
3560
|
const db = new SQL.Database(fileBuffer);
|
|
3561
|
+
// #3785: purge is advertised as irreversible; zero free pages too.
|
|
3562
|
+
db.run('PRAGMA secure_delete = ON');
|
|
3550
3563
|
const beforeResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE namespace = ?`, [namespace]);
|
|
3551
3564
|
const deletedCount = beforeResult[0]?.values?.[0]?.[0] || 0;
|
|
3552
3565
|
db.run(`DELETE FROM memory_entries WHERE namespace = ?`, [namespace]);
|
|
@@ -178,9 +178,13 @@ function reclaimDeadLock(lockPath) {
|
|
|
178
178
|
*/
|
|
179
179
|
export async function acquireLock(lockPath) {
|
|
180
180
|
const started = Date.now();
|
|
181
|
+
// O_NOFOLLOW closes the create-through-a-symlink path on platforms that
|
|
182
|
+
// support it; lstat below still rejects non-regular existing paths.
|
|
183
|
+
const createFlags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL
|
|
184
|
+
| (process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0));
|
|
181
185
|
while (Date.now() - started < LOCK_WAIT_MS) {
|
|
182
186
|
try {
|
|
183
|
-
const fd = openSync(lockPath,
|
|
187
|
+
const fd = openSync(lockPath, createFlags, 0o600);
|
|
184
188
|
const ownership = JSON.stringify({
|
|
185
189
|
pid: process.pid,
|
|
186
190
|
acquiredAt: Date.now(),
|
|
@@ -204,7 +208,30 @@ export async function acquireLock(lockPath) {
|
|
|
204
208
|
catch { /* already released */ }
|
|
205
209
|
};
|
|
206
210
|
}
|
|
207
|
-
catch {
|
|
211
|
+
catch (error) {
|
|
212
|
+
const code = error?.code;
|
|
213
|
+
// Only EEXIST is normal lock contention. Permission/path/IO failures
|
|
214
|
+
// must not masquerade as a five-second timeout on every MCP call.
|
|
215
|
+
// ELOOP is O_NOFOLLOW rejecting a symlink at the lock path.
|
|
216
|
+
if (code === 'ELOOP') {
|
|
217
|
+
throw new Error('policy-state-lock-error: lock path is not a regular file');
|
|
218
|
+
}
|
|
219
|
+
if (code !== 'EEXIST') {
|
|
220
|
+
throw new Error(`policy-state-lock-error: ${code ?? (error instanceof Error ? error.message : String(error))}`);
|
|
221
|
+
}
|
|
222
|
+
// EEXIST can also mean a directory/FIFO/symlink was planted at the lock
|
|
223
|
+
// path. Those are never a live lock: fail immediately instead of
|
|
224
|
+
// waiting, and never follow/remove the non-regular target.
|
|
225
|
+
try {
|
|
226
|
+
if (!lstatSync(lockPath).isFile()) {
|
|
227
|
+
throw new Error('policy-state-lock-error: lock path is not a regular file');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (statError) {
|
|
231
|
+
if (statError instanceof Error && statError.message.startsWith('policy-state-lock-error:'))
|
|
232
|
+
throw statError;
|
|
233
|
+
// The lock vanished between open() and lstat(); retry immediately.
|
|
234
|
+
}
|
|
208
235
|
reclaimDeadLock(lockPath);
|
|
209
236
|
await sleep(10);
|
|
210
237
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.59.
|
|
3
|
+
"version": "3.59.8",
|
|
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",
|