@claude-flow/cli 3.32.35 β†’ 3.32.37

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/helpers.manifest.json +5 -5
  2. package/.claude/helpers/hook-handler.cjs +30 -0
  3. package/.claude/helpers/intelligence.cjs +83 -5
  4. package/.claude/helpers/statusline.cjs +169 -6
  5. package/bin/cli.js +25 -1
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/agent.js +1 -1
  8. package/dist/src/commands/doctor.js +159 -11
  9. package/dist/src/commands/hooks.js +39 -4
  10. package/dist/src/commands/init.js +52 -8
  11. package/dist/src/commands/mcp.js +1 -1
  12. package/dist/src/commands/memory-distill.js +1 -0
  13. package/dist/src/commands/swarm.js +28 -0
  14. package/dist/src/init/executor.js +45 -10
  15. package/dist/src/init/helpers-generator.js +15 -4
  16. package/dist/src/init/statusline-generator.js +18 -5
  17. package/dist/src/mcp-server.d.ts +25 -0
  18. package/dist/src/mcp-server.js +57 -2
  19. package/dist/src/mcp-tools/embeddings-tools.js +51 -10
  20. package/dist/src/mcp-tools/hooks-tools.js +34 -8
  21. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  22. package/dist/src/memory/memory-bridge.d.ts +12 -0
  23. package/dist/src/memory/memory-bridge.js +29 -6
  24. package/dist/src/memory/memory-initializer.js +11 -8
  25. package/dist/src/services/memory-distillation.d.ts +1 -0
  26. package/dist/src/services/memory-distillation.js +81 -5
  27. package/dist/src/services/worker-daemon.js +1 -0
  28. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  29. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  30. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  31. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  32. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  33. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  34. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  35. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  36. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  37. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  38. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  39. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  40. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  41. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  42. package/node_modules/@claude-flow/codex/dist/initializer.d.ts +5 -0
  43. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  44. package/node_modules/@claude-flow/codex/dist/initializer.js +54 -20
  45. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  46. package/package.json +1 -1
  47. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +4 -2
  48. package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
  49. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
  50. package/plugins/ruflo-metaharness/scripts/genome.mjs +29 -4
  51. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
  52. package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
  53. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +25 -1
  54. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
  55. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
  56. package/plugins/ruflo-metaharness/skills/harness-genome/SKILL.md +5 -2
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.32.35",
3
+ "version": "3.32.37",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
6
- "hook-handler.cjs": "50ea92a72651bdc95634f7588d56a5870963168eef5226f66ed14af3b47c8d9a",
7
- "intelligence.cjs": "bd1f8e4b034944aee1df0391dc47ac2e8cc4b3aa542c65407a59d49620bbf76b",
8
- "statusline.cjs": "d2a0eac56d1267d8dbed2d70b09feb592299469196ec4b215909f2b052144882"
6
+ "hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
7
+ "intelligence.cjs": "4e637e065b8f712ce6cf28cccab4244fe222dff2b984690d830bfc40d21fd050",
8
+ "statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
9
9
  }
10
10
  },
11
- "signature": "7hAABpKaZablL78ug8UW506P+ppwYlDb2qws0D4BbCeugfGQTdDEOYsndbZMc4+Wzbx10oQbnubqh8jHZnHvBQ==",
11
+ "signature": "4aXDfffQBBhj8kE+NJo5s/G4exfj/xdhqCwWIJxyGijINFL2GleKUam1oIYH9iyDeAZ1FKWrFAUzZ2WNEwMCDg==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -286,6 +286,31 @@ async function readStdin() {
286
286
  });
287
287
  }
288
288
 
289
+ function claimSideEffectEvent(family, stdinData, event) {
290
+ if (/^(1|true|yes|on)$/i.test(process.env.RUFLO_DISABLE_HOOK_DEDUP || '')) return true;
291
+ try {
292
+ const crypto = require('crypto');
293
+ const eventId = event?.tool_use_id || event?.toolUseId ||
294
+ event?.session_id || event?.sessionId || event?.hook_event_id;
295
+ const payloadIdentity = eventId
296
+ ? `event:${eventId}`
297
+ : `payload:${(stdinData || '').trim()}|bucket:${Math.floor(Date.now() / 2000)}`;
298
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
299
+ const digest = crypto.createHash('sha256')
300
+ .update(`ruflo-hook-dedup-v1\0${path.resolve(projectRoot)}\0${family}\0${payloadIdentity}`)
301
+ .digest('hex');
302
+ const dir = process.env.RUFLO_HOOK_DEDUP_DIR ||
303
+ path.join(os.tmpdir(), 'ruflo-hook-dedup-v1');
304
+ fs.mkdirSync(dir, { recursive: true });
305
+ const fd = fs.openSync(path.join(dir, digest), 'wx', 0o600);
306
+ fs.writeFileSync(fd, String(Date.now()));
307
+ fs.closeSync(fd);
308
+ return true;
309
+ } catch (error) {
310
+ return error?.code === 'EEXIST' ? false : true;
311
+ }
312
+ }
313
+
289
314
  async function main() {
290
315
  // Global safety timeout: hooks must NEVER hang (#1530, #1531)
291
316
  const safetyTimer = setTimeout(() => {
@@ -302,6 +327,11 @@ async function main() {
302
327
  try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
303
328
  }
304
329
 
330
+ if ((command === 'post-edit' || command === 'session-end') &&
331
+ !claimSideEffectEvent(command, stdinData, hookInput)) {
332
+ return;
333
+ }
334
+
305
335
  // Normalize snake_case/camelCase: Claude Code sends tool_input/tool_name (snake_case)
306
336
  const toolInput = hookInput.toolInput || hookInput.tool_input || {};
307
337
  const toolName = hookInput.toolName || hookInput.tool_name || '';
@@ -17,17 +17,40 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
 
20
- const DATA_DIR = path.join(process.cwd(), '.claude-flow', 'data');
20
+ function resolveProjectRoot(startDir) {
21
+ if (process.env.CLAUDE_PROJECT_DIR) {
22
+ return path.resolve(process.env.CLAUDE_PROJECT_DIR);
23
+ }
24
+ let dir = path.resolve(startDir || process.cwd());
25
+ while (true) {
26
+ if (fs.existsSync(path.join(dir, '.git')) ||
27
+ fs.existsSync(path.join(dir, '.claude-flow'))) {
28
+ return dir;
29
+ }
30
+ const parent = path.dirname(dir);
31
+ if (parent === dir) return path.resolve(startDir || process.cwd());
32
+ dir = parent;
33
+ }
34
+ }
35
+
36
+ const PROJECT_ROOT = resolveProjectRoot(process.cwd());
37
+ const DATA_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'data');
21
38
  const STORE_PATH = path.join(DATA_DIR, 'auto-memory-store.json');
22
39
  const GRAPH_PATH = path.join(DATA_DIR, 'graph-state.json');
23
40
  const RANKED_PATH = path.join(DATA_DIR, 'ranked-context.json');
24
41
  const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl');
25
- const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');
42
+ const LEGACY_PENDING_PATH = path.join(process.cwd(), '.claude-flow', 'data', 'pending-insights.jsonl');
43
+ const SESSION_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'sessions');
26
44
  const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
27
45
 
28
46
  // ── Safety limits (fixes #1530, #1531) ─────────────────────────────────────
29
47
  const MAX_DATA_FILE_SIZE = 10 * 1024 * 1024; // 10 MB β€” skip files larger than this
30
48
  const MAX_GRAPH_NODES = 5000; // skip PageRank if graph exceeds this
49
+ // #2628: similarity edges used to compare every pair in every category.
50
+ // Keep exact graph behavior for normal stores, but never let a session-end
51
+ // hook enter an unbounded O(nΒ²) pass. Temporal edges remain linear and are
52
+ // always retained when the similarity pass is skipped.
53
+ const MAX_SIMILARITY_COMPARISONS = 100000;
31
54
 
32
55
  // ── Stop words for trigram matching ──────────────────────────────────────────
33
56
 
@@ -47,6 +70,22 @@ const STOP_WORDS = new Set([
47
70
 
48
71
  function ensureDataDir() {
49
72
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
73
+ // Recover signal written by older helpers under a subdirectory cwd. Keep
74
+ // the legacy file intact and append only lines not already present.
75
+ if (path.resolve(LEGACY_PENDING_PATH) !== path.resolve(PENDING_PATH) &&
76
+ fs.existsSync(LEGACY_PENDING_PATH)) {
77
+ try {
78
+ const existing = fs.existsSync(PENDING_PATH)
79
+ ? new Set(fs.readFileSync(PENDING_PATH, 'utf-8').split('\n').filter(Boolean))
80
+ : new Set();
81
+ const recovered = fs.readFileSync(LEGACY_PENDING_PATH, 'utf-8')
82
+ .split('\n')
83
+ .filter(line => line && !existing.has(line));
84
+ if (recovered.length > 0) {
85
+ fs.appendFileSync(PENDING_PATH, recovered.join('\n') + '\n', 'utf-8');
86
+ }
87
+ } catch { /* migration is best-effort; never block hook execution */ }
88
+ }
50
89
  }
51
90
 
52
91
  function readJSON(filePath) {
@@ -135,7 +174,14 @@ function deduplicateByContent(entries) {
135
174
  const seen = new Map();
136
175
  for (const entry of entries) {
137
176
  const content = entry.content || entry.summary || entry.value || '';
138
- const fp = fingerprintContent(typeof content === 'string' ? content : JSON.stringify(content));
177
+ const normalizedContent = typeof content === 'string' ? content : JSON.stringify(content);
178
+ // Content-less records can still represent distinct graph nodes. There is
179
+ // no content identity to prove they are duplicates, so preserve them.
180
+ if (!normalizedContent || !normalizedContent.trim()) {
181
+ seen.set(`__no_content_${seen.size}`, entry);
182
+ continue;
183
+ }
184
+ const fp = fingerprintContent(normalizedContent);
139
185
  if (!seen.has(fp)) {
140
186
  seen.set(fp, entry);
141
187
  } else {
@@ -258,10 +304,28 @@ function buildEdges(entries) {
258
304
  }
259
305
  }
260
306
 
307
+ let similarityComparisons = 0;
308
+ for (const group of Object.values(byCategory)) {
309
+ similarityComparisons += (group.length * (group.length - 1)) / 2;
310
+ if (similarityComparisons > MAX_SIMILARITY_COMPARISONS) break;
311
+ }
312
+
261
313
  // Similarity edges within categories (Jaccard > 0.3).
262
314
  // ADR-095 G6 perf: hoist the trigram computation outside the inner
263
315
  // loop. Previously we re-tokenized + re-trigrammed group[j] for every
264
316
  // i β€” O(nΒ²) extra work for nothing. Now compute once per entry.
317
+ // #2628: the old unconditional nested loop blocked session exit for tens
318
+ // of seconds on accumulated stores. Skip only the quadratic similarity
319
+ // layer when its deterministic pair count exceeds the budget; the linear
320
+ // temporal graph above is still complete.
321
+ if (similarityComparisons > MAX_SIMILARITY_COMPARISONS) {
322
+ process.stderr.write(
323
+ `[INTELLIGENCE] WARN: Similarity graph needs >${MAX_SIMILARITY_COMPARISONS} comparisons; ` +
324
+ 'skipping similarity edges (temporal edges retained)\n'
325
+ );
326
+ return edges;
327
+ }
328
+
265
329
  for (const cat of Object.keys(byCategory)) {
266
330
  const group = byCategory[cat];
267
331
  if (group.length < 2) continue;
@@ -300,7 +364,7 @@ function buildEdges(entries) {
300
364
  */
301
365
  function bootstrapFromMemoryFiles() {
302
366
  const entries = [];
303
- const cwd = process.cwd();
367
+ const cwd = PROJECT_ROOT;
304
368
 
305
369
  // Search for auto-memory directories
306
370
  const candidates = [
@@ -654,6 +718,11 @@ function consolidate() {
654
718
  // Deduplicate store entries by ID before processing (fixes #1518)
655
719
  const preDedupCount = store.length;
656
720
  store = deduplicateById(store);
721
+ // #2628: imports assign fresh IDs to repeated MEMORY.md content, so ID
722
+ // dedup alone never shrinks the store. Consolidate is the session-end path:
723
+ // content-dedup here before edge construction and persist the compacted
724
+ // store so subsequent sessions stay bounded.
725
+ store = deduplicateByContent(store);
657
726
 
658
727
  // 1. Process pending insights
659
728
  let newEntries = 0;
@@ -1032,7 +1101,16 @@ function stats(outputJson) {
1032
1101
  return report;
1033
1102
  }
1034
1103
 
1035
- module.exports = { init, getContext, recordEdit, feedback, consolidate, stats };
1104
+ module.exports = {
1105
+ init,
1106
+ getContext,
1107
+ recordEdit,
1108
+ feedback,
1109
+ consolidate,
1110
+ stats,
1111
+ resolveProjectRoot,
1112
+ projectRoot: PROJECT_ROOT,
1113
+ };
1036
1114
 
1037
1115
  // ── CLI entrypoint ──────────────────────────────────────────────────────────
1038
1116
  if (require.main === module) {
@@ -42,7 +42,11 @@ const CONFIG = {
42
42
  hideCost: /^(1|true|yes|on)$/i.test(process.env.RUFLO_STATUSLINE_HIDE_COST || ''),
43
43
  };
44
44
 
45
- const CWD = process.cwd();
45
+ const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
+ // Replaced by statusline-generator with the package root of the CLI that
47
+ // installed this helper. This survives custom npm prefixes and bundled Node
48
+ // runtimes whose process.execPath belongs to a different tree (#2811).
49
+ const BAKED_INSTALL_ROOT = "";
46
50
 
47
51
  // ─── Delegation cache ───────────────────────────────────────────
48
52
  // Cache the CLI JSON result so rapid prompt re-renders (Claude Code
@@ -404,6 +408,154 @@ function getLocalIntegration() {
404
408
  return integration;
405
409
  }
406
410
 
411
+ // ─── Security freshness overlay (ruvnet/ruflo#2776) ──────────────
412
+ // The shipped CLI producer (dist/src/funnel/local-signals.js getSecurityStatus)
413
+ // only ever emits PENDING / CLEAN / ISSUES β€” it captures `scannedAt` but never
414
+ // inspects it, so a year-old scan renders πŸ›‘ βœ“ forever and the renderer's
415
+ // STALE / IN_PROGRESS branches are unreachable. Worse, when CLI delegation
416
+ // fails, the stale-while-revalidate cache (readCache() below) keeps serving
417
+ // the pre-scan PENDING pill indefinitely, so a user who runs the advertised
418
+ // `ruflo security scan` sees no change β€” the pill freezes at "scan pending".
419
+ //
420
+ // This overlay recomputes the security block from disk on EVERY render (same
421
+ // pattern as adrs/agentdb/tests/hooks above), which:
422
+ // 1) Makes STALE reachable β€” when the newest scan is older than
423
+ // RUFLO_SCAN_STALE_HOURS (default 24h β€” matches the CVE feed refresh
424
+ // cadence), report STALE regardless of what the cached CLI JSON says.
425
+ // 2) Makes IN_PROGRESS reachable β€” when a `scan-in-progress` marker file
426
+ // exists and is younger than SECURITY_IN_PROGRESS_MAX_MIN (guards against
427
+ // a crashed scan leaving the marker behind).
428
+ // 3) Caps the "scan pending" display window β€” if PENDING has been shown for
429
+ // >RUFLO_SCAN_PENDING_CAP_MIN (default 30) without a completion write,
430
+ // switch to STALE and stop rendering the yellow indicator. The tracker
431
+ // lives in ~/.ruflo/statusline-scan-pending-since.json, keyed by CWD
432
+ // hash so multiple project checkouts don't collide.
433
+ // 4) Since this runs AFTER readCache() serves stale data, it bypasses the
434
+ // "pill freezes at PENDING" freeze in defect 2 β€” the overlay reads
435
+ // fresh disk state even when the CLI delegation is broken.
436
+ const SECURITY_STALE_HOURS = Math.max(1, parseInt(process.env.RUFLO_SCAN_STALE_HOURS || '24', 10) || 24);
437
+ const SECURITY_PENDING_CAP_MIN = Math.max(1, parseInt(process.env.RUFLO_SCAN_PENDING_CAP_MIN || '30', 10) || 30);
438
+ const SECURITY_IN_PROGRESS_MAX_MIN = 30; // marker older than this = crashed scan; treat as absent
439
+ const PENDING_TRACK_FILE = path.join(os.homedir(), '.ruflo', 'statusline-scan-pending-since.json');
440
+ const CWD_KEY = require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 12);
441
+
442
+ function readPendingSince() {
443
+ try {
444
+ if (!fs.existsSync(PENDING_TRACK_FILE)) return null;
445
+ const raw = JSON.parse(fs.readFileSync(PENDING_TRACK_FILE, 'utf-8'));
446
+ if (raw && typeof raw === 'object' && typeof raw[CWD_KEY] === 'number') return raw[CWD_KEY];
447
+ } catch { /* ignore */ }
448
+ return null;
449
+ }
450
+
451
+ function writePendingSince(ts) {
452
+ try {
453
+ let obj = {};
454
+ if (fs.existsSync(PENDING_TRACK_FILE)) {
455
+ try { obj = JSON.parse(fs.readFileSync(PENDING_TRACK_FILE, 'utf-8')) || {}; } catch { obj = {}; }
456
+ }
457
+ if (ts === null) { delete obj[CWD_KEY]; } else { obj[CWD_KEY] = ts; }
458
+ fs.mkdirSync(path.dirname(PENDING_TRACK_FILE), { recursive: true, mode: 0o700 });
459
+ fs.writeFileSync(PENDING_TRACK_FILE, JSON.stringify(obj), { encoding: 'utf-8', mode: 0o600 });
460
+ } catch { /* ignore */ }
461
+ }
462
+
463
+ function getLocalSecurity(cliSecurity) {
464
+ const base = (cliSecurity && typeof cliSecurity === 'object')
465
+ ? Object.assign({}, cliSecurity)
466
+ : { status: 'NONE', findings: 0, cvesFixed: 0, totalCves: 0 };
467
+ base.findings = Math.max(0, base.findings || 0);
468
+
469
+ const scanDir = path.join(CWD, '.claude', 'security-scans');
470
+
471
+ // Detect a live in-progress marker (writer opts-in by writing this file).
472
+ let inProgress = false;
473
+ try {
474
+ const marker = path.join(scanDir, 'scan-in-progress');
475
+ if (fs.existsSync(marker)) {
476
+ const ageMin = (Date.now() - fs.statSync(marker).mtimeMs) / 60000;
477
+ if (ageMin < SECURITY_IN_PROGRESS_MAX_MIN) inProgress = true;
478
+ }
479
+ } catch { /* ignore */ }
480
+
481
+ // Find newest scan-*.json by mtime and read its findings/timestamp.
482
+ let newestPath = null;
483
+ let newestMtime = 0;
484
+ try {
485
+ if (fs.existsSync(scanDir)) {
486
+ for (const name of fs.readdirSync(scanDir)) {
487
+ if (!name.startsWith('scan-') || !name.endsWith('.json')) continue;
488
+ try {
489
+ const st = fs.statSync(path.join(scanDir, name));
490
+ if (st.mtimeMs > newestMtime) { newestMtime = st.mtimeMs; newestPath = path.join(scanDir, name); }
491
+ } catch { /* ignore */ }
492
+ }
493
+ }
494
+ } catch { /* ignore */ }
495
+
496
+ if (newestPath) {
497
+ // We have a scan on disk β€” the never-scanned pending tracker is no longer
498
+ // relevant. Clear it so a re-created directory can start a fresh window.
499
+ writePendingSince(null);
500
+
501
+ let scannedAtMs = newestMtime;
502
+ let findings = base.findings;
503
+ try {
504
+ const j = JSON.parse(fs.readFileSync(newestPath, 'utf-8'));
505
+ // scannedAt (CLI producer's field name) OR timestamp (writer's field name).
506
+ const isoStr = (j && (j.scannedAt || j.timestamp)) || null;
507
+ if (isoStr) {
508
+ const t = Date.parse(isoStr);
509
+ if (!isNaN(t)) scannedAtMs = t;
510
+ }
511
+ // findings may be a number, an array, or nested in summary.total.
512
+ if (j) {
513
+ if (typeof j.findings === 'number') findings = j.findings;
514
+ else if (Array.isArray(j.findings)) findings = j.findings.length;
515
+ else if (j.summary && typeof j.summary.total === 'number') findings = j.summary.total;
516
+ }
517
+ } catch { /* ignore parse β€” fall back to mtime + cached findings */ }
518
+
519
+ base.findings = Math.max(0, findings || 0);
520
+ base.scannedAt = new Date(scannedAtMs).toISOString();
521
+
522
+ const ageHours = (Date.now() - scannedAtMs) / 3600000;
523
+ if (ageHours >= SECURITY_STALE_HOURS) {
524
+ // Stale but findings still render red (a year-old ISSUES scan is still bad).
525
+ base.status = 'STALE';
526
+ } else if (inProgress) {
527
+ base.status = 'IN_PROGRESS';
528
+ } else if (base.findings > 0) {
529
+ base.status = 'ISSUES';
530
+ } else {
531
+ base.status = 'CLEAN';
532
+ }
533
+ return base;
534
+ }
535
+
536
+ // No scan file. If a live marker exists, we're mid-scan.
537
+ if (inProgress) {
538
+ base.status = 'IN_PROGRESS';
539
+ // Reset the pending tracker so, if the scan crashes mid-flight, the next
540
+ // render starts a fresh N-minute pending window instead of an already-expired one.
541
+ writePendingSince(null);
542
+ return base;
543
+ }
544
+
545
+ // Truly never-scanned: track how long we've shown PENDING. After the cap,
546
+ // escalate to STALE with the dim/gray glyph so the pill visibly stops
547
+ // shouting for attention β€” the user has either ignored it for 30 min or
548
+ // the scan is silently failing to write.
549
+ let pendingSince = readPendingSince();
550
+ if (pendingSince === null || typeof pendingSince !== 'number') {
551
+ pendingSince = Date.now();
552
+ writePendingSince(pendingSince);
553
+ }
554
+ const pendingAgeMin = (Date.now() - pendingSince) / 60000;
555
+ base.status = (pendingAgeMin >= SECURITY_PENDING_CAP_MIN) ? 'STALE' : 'PENDING';
556
+ return base;
557
+ }
558
+
407
559
  // Overlay every locally-derived block onto the CLI data (mutates in place).
408
560
  function applyLocalOverlays(data) {
409
561
  data.adrs = getLocalADRCount();
@@ -411,6 +563,9 @@ function applyLocalOverlays(data) {
411
563
  data.tests = getLocalTests();
412
564
  data.hooks = getLocalHooks();
413
565
  data.integration = getLocalIntegration();
566
+ // Security overlay: recompute freshness from disk on every render so cached
567
+ // CLI JSON can never freeze the pill at PENDING. See getLocalSecurity() above.
568
+ data.security = getLocalSecurity(data.security);
414
569
  return data;
415
570
  }
416
571
 
@@ -685,6 +840,7 @@ function getPkgVersion() {
685
840
  try {
686
841
  const home = os.homedir();
687
842
  const pkgPaths = [
843
+ ...(BAKED_INSTALL_ROOT ? [path.join(BAKED_INSTALL_ROOT, 'package.json')] : []),
688
844
  path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'package.json'),
689
845
  path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'package.json'),
690
846
  path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
@@ -712,7 +868,12 @@ function getPkgVersion() {
712
868
  // #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
713
869
  // the node binary location, so the binDir-derived probes above all miss. Also
714
870
  // probe the npm prefix from the environment and the common ~/.npm-global default.
715
- for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
871
+ for (const prefix of [
872
+ process.env.npm_config_prefix,
873
+ process.env.PREFIX,
874
+ path.join(home, '.local'),
875
+ path.join(home, '.npm-global'),
876
+ ]) {
716
877
  if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
717
878
  }
718
879
  for (const gm of globalModuleDirs) {
@@ -731,14 +892,12 @@ function getPkgVersion() {
731
892
  // right after a publish). Taking the first EXISTING candidate meant the
732
893
  // header could show a stale version even when a newer install (e.g.
733
894
  // node_modules/@claude-flow/cli from a plain npm install) was sitting right there.
734
- let found = false;
735
895
  for (const p of pkgPaths) {
736
896
  if (!fs.existsSync(p)) continue;
737
897
  try {
738
898
  const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
739
899
  if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) {
740
- if (!found || compareVersions(pkg.version, ver) > 0) ver = pkg.version;
741
- found = true;
900
+ if (compareVersions(pkg.version, ver) > 0) ver = pkg.version;
742
901
  }
743
902
  } catch { /* ignore */ }
744
903
  }
@@ -851,10 +1010,14 @@ function generateStatusline() {
851
1010
  if (healthAllGreen) {
852
1011
  opsParts.push(c.brightGreen + 'πŸ›‘ βœ“' + c.reset);
853
1012
  } else {
1013
+ // #2776: STALE gets dim/gray (distinct from the actionable yellow of
1014
+ // PENDING/IN_PROGRESS) so a stale pill visibly stops shouting for
1015
+ // attention β€” the user can act on the "run ruflo security scan" prompt or
1016
+ // ignore it without a permanently-yellow indicator.
854
1017
  if (secStatus === 'PENDING') opsParts.push(c.brightYellow + 'πŸ›‘ scan pending' + c.reset);
855
1018
  else if (secStatus === 'IN_PROGRESS') opsParts.push(c.brightYellow + 'πŸ›‘ scanning…' + c.reset);
856
1019
  else if (secStatus === 'ISSUES') opsParts.push(c.brightRed + 'πŸ›‘ findings' + c.reset);
857
- else if (secStatus === 'STALE') opsParts.push(c.brightYellow + 'πŸ›‘ scan stale' + c.reset);
1020
+ else if (secStatus === 'STALE') opsParts.push(c.dim + 'πŸ›‘ scan stale' + c.reset);
858
1021
  else if (secStatus !== 'NONE' && secStatus !== 'CLEAN') opsParts.push(c.brightRed + 'πŸ›‘ ' + secStatus.toLowerCase() + c.reset);
859
1022
  if (findings > 0) {
860
1023
  opsParts.push(c.brightRed + '⚠ ' + findings + ' finding' + (findings === 1 ? '' : 's') + c.reset);
package/bin/cli.js CHANGED
@@ -34,6 +34,30 @@ import { dirname, join } from 'path';
34
34
  const _origWarn = console.warn;
35
35
  const _origLog = console.log;
36
36
  const _origError = console.error;
37
+
38
+ // #2726 β€” Some MCP clients place every advertised schema in every model
39
+ // request. Keep "all" as the backwards-compatible default, but honor the
40
+ // same category/namespace/exact-name selector as `mcp start --tools`.
41
+ function _filterAdvertisedMcpTools(tools) {
42
+ const argv = process.argv.slice(2);
43
+ const toolsIndex = argv.findIndex((arg) => arg === '--tools');
44
+ const inlineTools = argv.find((arg) => arg.startsWith('--tools='));
45
+ const configured = process.env.CLAUDE_FLOW_MCP_TOOLS
46
+ || (toolsIndex >= 0 ? argv[toolsIndex + 1] : undefined)
47
+ || (inlineTools ? inlineTools.slice('--tools='.length) : undefined);
48
+ if (!configured || configured.trim().toLowerCase() === 'all') return tools;
49
+ const selectors = new Set(
50
+ configured.split(',').map((item) => item.trim().toLowerCase()).filter(Boolean),
51
+ );
52
+ if (selectors.size === 0) return tools;
53
+ return tools.filter((tool) => {
54
+ const name = tool.name.toLowerCase();
55
+ const category = typeof tool.category === 'string' ? tool.category.toLowerCase() : '';
56
+ return selectors.has(name)
57
+ || selectors.has(category)
58
+ || Array.from(selectors).some((selector) => name.startsWith(`${selector}_`));
59
+ });
60
+ }
37
61
  const _isCosmeticAgentdbPatchNoise = (msg) =>
38
62
  msg.includes('[AgentDB Patch]') && msg.includes('Controller index not found');
39
63
  const _STDERR_REDIRECT_PREFIXES = [
@@ -237,7 +261,7 @@ if (isMCPMode) {
237
261
  };
238
262
 
239
263
  case 'tools/list': {
240
- const tools = listMCPTools();
264
+ const tools = _filterAdvertisedMcpTools(listMCPTools());
241
265
  return {
242
266
  jsonrpc: '2.0',
243
267
  id: message.id,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 4,
4
- "generatedAt": "2026-07-29T17:56:33.000Z",
5
- "gitSha": "3d9bca6d",
4
+ "generatedAt": "2026-07-29T19:40:17.000Z",
5
+ "gitSha": "401e02d5",
6
6
  "catalog": {
7
7
  "agents": 164,
8
8
  "tools": 397,
@@ -230,7 +230,7 @@ const listCommand = {
230
230
  }
231
231
  // Format for display
232
232
  const displayAgents = result.agents.map(agent => ({
233
- id: agent.id,
233
+ id: agent.agentId,
234
234
  type: agent.agentType,
235
235
  status: agent.status,
236
236
  created: new Date(agent.createdAt).toLocaleTimeString(),