@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.
- package/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +30 -0
- package/.claude/helpers/intelligence.cjs +83 -5
- package/.claude/helpers/statusline.cjs +169 -6
- package/bin/cli.js +25 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/agent.js +1 -1
- package/dist/src/commands/doctor.js +159 -11
- package/dist/src/commands/hooks.js +39 -4
- package/dist/src/commands/init.js +52 -8
- package/dist/src/commands/mcp.js +1 -1
- package/dist/src/commands/memory-distill.js +1 -0
- package/dist/src/commands/swarm.js +28 -0
- package/dist/src/init/executor.js +45 -10
- package/dist/src/init/helpers-generator.js +15 -4
- package/dist/src/init/statusline-generator.js +18 -5
- package/dist/src/mcp-server.d.ts +25 -0
- package/dist/src/mcp-server.js +57 -2
- package/dist/src/mcp-tools/embeddings-tools.js +51 -10
- package/dist/src/mcp-tools/hooks-tools.js +34 -8
- package/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/dist/src/memory/memory-bridge.d.ts +12 -0
- package/dist/src/memory/memory-bridge.js +29 -6
- package/dist/src/memory/memory-initializer.js +11 -8
- package/dist/src/services/memory-distillation.d.ts +1 -0
- package/dist/src/services/memory-distillation.js +81 -5
- package/dist/src/services/worker-daemon.js +1 -0
- package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts +5 -0
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +54 -20
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +4 -2
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
- package/plugins/ruflo-metaharness/scripts/genome.mjs +29 -4
- package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
- package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
- package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +25 -1
- package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
- package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
- package/plugins/ruflo-metaharness/skills/harness-genome/SKILL.md +5 -2
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.37",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
|
|
6
|
-
"hook-handler.cjs": "
|
|
7
|
-
"intelligence.cjs": "
|
|
8
|
-
"statusline.cjs": "
|
|
6
|
+
"hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
|
|
7
|
+
"intelligence.cjs": "4e637e065b8f712ce6cf28cccab4244fe222dff2b984690d830bfc40d21fd050",
|
|
8
|
+
"statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"signature": "
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 = {
|
|
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 [
|
|
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 (
|
|
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.
|
|
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,
|
package/catalog-manifest.json
CHANGED
|
@@ -230,7 +230,7 @@ const listCommand = {
|
|
|
230
230
|
}
|
|
231
231
|
// Format for display
|
|
232
232
|
const displayAgents = result.agents.map(agent => ({
|
|
233
|
-
id: agent.
|
|
233
|
+
id: agent.agentId,
|
|
234
234
|
type: agent.agentType,
|
|
235
235
|
status: agent.status,
|
|
236
236
|
created: new Date(agent.createdAt).toLocaleTimeString(),
|