@claude-flow/cli 3.32.35 β†’ 3.32.36

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 (48) 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 +47 -4
  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/mcp.js +1 -1
  11. package/dist/src/commands/memory-distill.js +1 -0
  12. package/dist/src/commands/swarm.js +28 -0
  13. package/dist/src/init/executor.js +16 -10
  14. package/dist/src/init/helpers-generator.js +15 -4
  15. package/dist/src/init/statusline-generator.js +18 -5
  16. package/dist/src/mcp-server.d.ts +25 -0
  17. package/dist/src/mcp-server.js +57 -2
  18. package/dist/src/mcp-tools/embeddings-tools.js +51 -10
  19. package/dist/src/mcp-tools/hooks-tools.js +34 -8
  20. package/dist/src/memory/memory-bridge.d.ts +2 -0
  21. package/dist/src/memory/memory-bridge.js +5 -1
  22. package/dist/src/services/memory-distillation.d.ts +1 -0
  23. package/dist/src/services/memory-distillation.js +81 -5
  24. package/dist/src/services/worker-daemon.js +1 -0
  25. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  26. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  27. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  28. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  29. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  30. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  31. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  32. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  33. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  34. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  35. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  36. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  37. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  38. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  39. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  40. package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
  41. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  42. package/package.json +1 -1
  43. package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
  44. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
  45. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
  46. package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
  47. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
  48. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.32.35",
3
+ "version": "3.32.36",
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": "792c24a12704267e6b45ed0794e128131fb16e4174a43e65a5afe757bc0230af",
8
+ "statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
9
9
  }
10
10
  },
11
- "signature": "7hAABpKaZablL78ug8UW506P+ppwYlDb2qws0D4BbCeugfGQTdDEOYsndbZMc4+Wzbx10oQbnubqh8jHZnHvBQ==",
11
+ "signature": "qGxpst4ul6cizCQw1hjdJ5sBXj7uN7Fm1Dux2sKC7BwcPA66Nr9RzI8c8BJuKEUyPgaDtSvaY/gZTX2hWNqXBg==",
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,12 +17,30 @@
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) ─────────────────────────────────────
@@ -47,6 +65,22 @@ const STOP_WORDS = new Set([
47
65
 
48
66
  function ensureDataDir() {
49
67
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
68
+ // Recover signal written by older helpers under a subdirectory cwd. Keep
69
+ // the legacy file intact and append only lines not already present.
70
+ if (path.resolve(LEGACY_PENDING_PATH) !== path.resolve(PENDING_PATH) &&
71
+ fs.existsSync(LEGACY_PENDING_PATH)) {
72
+ try {
73
+ const existing = fs.existsSync(PENDING_PATH)
74
+ ? new Set(fs.readFileSync(PENDING_PATH, 'utf-8').split('\n').filter(Boolean))
75
+ : new Set();
76
+ const recovered = fs.readFileSync(LEGACY_PENDING_PATH, 'utf-8')
77
+ .split('\n')
78
+ .filter(line => line && !existing.has(line));
79
+ if (recovered.length > 0) {
80
+ fs.appendFileSync(PENDING_PATH, recovered.join('\n') + '\n', 'utf-8');
81
+ }
82
+ } catch { /* migration is best-effort; never block hook execution */ }
83
+ }
50
84
  }
51
85
 
52
86
  function readJSON(filePath) {
@@ -300,7 +334,7 @@ function buildEdges(entries) {
300
334
  */
301
335
  function bootstrapFromMemoryFiles() {
302
336
  const entries = [];
303
- const cwd = process.cwd();
337
+ const cwd = PROJECT_ROOT;
304
338
 
305
339
  // Search for auto-memory directories
306
340
  const candidates = [
@@ -1032,7 +1066,16 @@ function stats(outputJson) {
1032
1066
  return report;
1033
1067
  }
1034
1068
 
1035
- module.exports = { init, getContext, recordEdit, feedback, consolidate, stats };
1069
+ module.exports = {
1070
+ init,
1071
+ getContext,
1072
+ recordEdit,
1073
+ feedback,
1074
+ consolidate,
1075
+ stats,
1076
+ resolveProjectRoot,
1077
+ projectRoot: PROJECT_ROOT,
1078
+ };
1036
1079
 
1037
1080
  // ── CLI entrypoint ──────────────────────────────────────────────────────────
1038
1081
  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:08:51.000Z",
5
+ "gitSha": "67ff9898",
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(),