claude-flow 3.47.1 → 3.49.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/.proven-config-version +1 -0
- package/.claude/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/hook-handler.cjs +7 -4
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/memory.cjs +1 -1
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/.claude/helpers/router.cjs +1 -1
- package/.claude/helpers/session.cjs +1 -1
- package/.claude/proven-config.json +42 -0
- package/.claude-plugin/marketplace.json +16 -1
- package/README.md +1 -53
- package/README.zh-CN.md +1 -53
- 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/dist/policy/engine.d.ts +2 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +1 -53
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- 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/doctor.d.ts +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/index.js +10 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
- 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 +16 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
- 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.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
- 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 +47 -34
- package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
- package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
- 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/policy-runtime.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
- 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
|
@@ -13,6 +13,7 @@ import { createBuiltinAIDefence } from '../security/builtin-aidefence.js';
|
|
|
13
13
|
// for both validation and the traversal-depth maps below.
|
|
14
14
|
const SCAN_DEPTHS = ['quick', 'standard', 'deep'];
|
|
15
15
|
const SCAN_TYPES = ['code', 'deps', 'all'];
|
|
16
|
+
const SCAN_OUTPUTS = ['text', 'json', 'sarif'];
|
|
16
17
|
// Real type predicates, not `as` casts. Array.includes() does not narrow a
|
|
17
18
|
// string to a literal union on its own, so without these the depth-map lookups
|
|
18
19
|
// below need an unchecked assertion — and an unchecked assertion would let a
|
|
@@ -23,6 +24,37 @@ const SCAN_TYPES = ['code', 'deps', 'all'];
|
|
|
23
24
|
// is being fixed for.
|
|
24
25
|
const isScanDepth = (v) => SCAN_DEPTHS.includes(v);
|
|
25
26
|
const isScanType = (v) => SCAN_TYPES.includes(v);
|
|
27
|
+
const isScanOutput = (v) => SCAN_OUTPUTS.includes(v);
|
|
28
|
+
function scanSarif(findings) {
|
|
29
|
+
const ruleId = (type) => `RUFLO-${type.toUpperCase().replace(/[^A-Z0-9]+/g, '-')}`;
|
|
30
|
+
const level = (severity) => severity === 'critical' || severity === 'high' ? 'error' :
|
|
31
|
+
severity === 'medium' ? 'warning' : 'note';
|
|
32
|
+
const rules = [...new Map(findings.map(finding => [finding.type, {
|
|
33
|
+
id: ruleId(finding.type),
|
|
34
|
+
name: finding.type,
|
|
35
|
+
shortDescription: { text: finding.type },
|
|
36
|
+
}])).values()];
|
|
37
|
+
return {
|
|
38
|
+
version: '2.1.0',
|
|
39
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
40
|
+
runs: [{
|
|
41
|
+
tool: { driver: { name: 'Ruflo Security Scan', rules } },
|
|
42
|
+
results: findings.map(finding => {
|
|
43
|
+
const lineMatch = /^(.*):(\d+)$/.exec(finding.location);
|
|
44
|
+
const file = lineMatch?.[1] ?? (finding.location.startsWith('package.json:') ? 'package.json' : finding.location);
|
|
45
|
+
return {
|
|
46
|
+
ruleId: ruleId(finding.type),
|
|
47
|
+
level: level(finding.severity),
|
|
48
|
+
message: { text: finding.description },
|
|
49
|
+
locations: [{ physicalLocation: {
|
|
50
|
+
artifactLocation: { uri: file.replace(/\\/g, '/') },
|
|
51
|
+
...(lineMatch ? { region: { startLine: Number(lineMatch[2]) } } : {}),
|
|
52
|
+
} }],
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
26
58
|
// `full` was never a supported depth, but the CLI itself printed it — the
|
|
27
59
|
// statusline insight, the announcement, the release-notes blurb, the CLAUDE.md
|
|
28
60
|
// that `init` generates, and two shipped agent definitions all tell people to
|
|
@@ -62,6 +94,7 @@ const scanCommand = {
|
|
|
62
94
|
const target = ctx.flags.target || '.';
|
|
63
95
|
const requestedDepth = ctx.flags.depth || 'standard';
|
|
64
96
|
const scanType = ctx.flags.type || 'all';
|
|
97
|
+
const requestedOutput = ctx.flags.output || 'text';
|
|
65
98
|
const fix = ctx.flags.fix;
|
|
66
99
|
// A security scanner must never silently degrade on an unrecognised enum
|
|
67
100
|
// value. An unknown --depth used to fall through to the shallowest
|
|
@@ -91,6 +124,11 @@ const scanCommand = {
|
|
|
91
124
|
output.printError(`Invalid --type '${scanType}'. Expected one of: ${SCAN_TYPES.join(', ')}.`);
|
|
92
125
|
return { success: false, exitCode: 1 };
|
|
93
126
|
}
|
|
127
|
+
if (!isScanOutput(requestedOutput)) {
|
|
128
|
+
output.printError(`Invalid --output '${requestedOutput}'. Expected one of: ${SCAN_OUTPUTS.join(', ')}.`);
|
|
129
|
+
return { success: false, exitCode: 1 };
|
|
130
|
+
}
|
|
131
|
+
const outputFormat = requestedOutput;
|
|
94
132
|
// --target names WHAT gets scanned, and was never validated. A path that
|
|
95
133
|
// does not exist (or is a file, not a directory) made every phase read
|
|
96
134
|
// nothing, and the swallowed dir-read catches turned that into zero
|
|
@@ -106,11 +144,14 @@ const scanCommand = {
|
|
|
106
144
|
output.printError(`Target is not a directory: ${resolvedTarget}`);
|
|
107
145
|
return { success: false, exitCode: 1 };
|
|
108
146
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
147
|
+
if (outputFormat === 'text') {
|
|
148
|
+
output.writeln();
|
|
149
|
+
output.writeln(output.bold('Security Scan'));
|
|
150
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
151
|
+
}
|
|
112
152
|
const spinner = output.createSpinner({ text: `Scanning ${target}...`, spinner: 'dots' });
|
|
113
|
-
|
|
153
|
+
if (outputFormat === 'text')
|
|
154
|
+
spinner.start();
|
|
114
155
|
const findings = [];
|
|
115
156
|
let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
|
|
116
157
|
try {
|
|
@@ -151,9 +192,9 @@ const scanCommand = {
|
|
|
151
192
|
else
|
|
152
193
|
lowCount++;
|
|
153
194
|
findings.push({
|
|
154
|
-
severity: sev === 'critical' ?
|
|
155
|
-
sev === 'high' ?
|
|
156
|
-
sev === 'moderate' || sev === 'medium' ?
|
|
195
|
+
severity: sev === 'critical' ? 'critical' :
|
|
196
|
+
sev === 'high' ? 'high' :
|
|
197
|
+
sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
|
|
157
198
|
type: 'Dependency CVE',
|
|
158
199
|
location: `package.json:${pkg}`,
|
|
159
200
|
description: title.substring(0, 35),
|
|
@@ -206,7 +247,7 @@ const scanCommand = {
|
|
|
206
247
|
if (pattern.test(lines[i])) {
|
|
207
248
|
highCount++;
|
|
208
249
|
findings.push({
|
|
209
|
-
severity:
|
|
250
|
+
severity: 'high',
|
|
210
251
|
type: 'Hardcoded Secret',
|
|
211
252
|
location: `${path.relative(target, fullPath)}:${i + 1}`,
|
|
212
253
|
description: type,
|
|
@@ -261,7 +302,7 @@ const scanCommand = {
|
|
|
261
302
|
else
|
|
262
303
|
mediumCount++;
|
|
263
304
|
findings.push({
|
|
264
|
-
severity: severity === 'high' ?
|
|
305
|
+
severity: severity === 'high' ? 'high' : 'medium',
|
|
265
306
|
type,
|
|
266
307
|
location: `${path.relative(target, fullPath)}:${i + 1}`,
|
|
267
308
|
description: desc,
|
|
@@ -280,35 +321,57 @@ const scanCommand = {
|
|
|
280
321
|
const scanDepth = CODE_SCAN_DEPTH[depth];
|
|
281
322
|
scanCodeDir(path.resolve(target), scanDepth);
|
|
282
323
|
}
|
|
283
|
-
|
|
324
|
+
if (outputFormat === 'text')
|
|
325
|
+
spinner.succeed('Scan complete');
|
|
284
326
|
// Display results
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
327
|
+
if (outputFormat === 'text') {
|
|
328
|
+
output.writeln();
|
|
329
|
+
if (findings.length > 0) {
|
|
330
|
+
output.printTable({
|
|
331
|
+
columns: [
|
|
332
|
+
{ key: 'severity', header: 'Severity', width: 12 },
|
|
333
|
+
{ key: 'type', header: 'Type', width: 18 },
|
|
334
|
+
{ key: 'location', header: 'Location', width: 25 },
|
|
335
|
+
{ key: 'description', header: 'Description', width: 35 },
|
|
336
|
+
],
|
|
337
|
+
data: findings.slice(0, 20).map(finding => ({
|
|
338
|
+
...finding,
|
|
339
|
+
severity: finding.severity === 'critical' ? output.error('CRITICAL') :
|
|
340
|
+
finding.severity === 'high' ? output.warning('HIGH') :
|
|
341
|
+
finding.severity === 'medium' ? output.warning('MEDIUM') : output.info('LOW'),
|
|
342
|
+
})), // Show first 20
|
|
343
|
+
});
|
|
344
|
+
if (findings.length > 20) {
|
|
345
|
+
output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
|
|
346
|
+
}
|
|
298
347
|
}
|
|
348
|
+
else {
|
|
349
|
+
output.writeln(output.success('No security issues found!'));
|
|
350
|
+
}
|
|
351
|
+
output.writeln();
|
|
352
|
+
output.printBox([
|
|
353
|
+
`Target: ${target}`,
|
|
354
|
+
`Depth: ${depth}`,
|
|
355
|
+
`Type: ${scanType}`,
|
|
356
|
+
``,
|
|
357
|
+
`Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
|
|
358
|
+
`Total Issues: ${findings.length}`,
|
|
359
|
+
].join('\n'), 'Scan Summary');
|
|
299
360
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
const record = {
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
363
|
+
target,
|
|
364
|
+
depth,
|
|
365
|
+
type: scanType,
|
|
366
|
+
summary: {
|
|
367
|
+
critical: criticalCount,
|
|
368
|
+
high: highCount,
|
|
369
|
+
medium: mediumCount,
|
|
370
|
+
low: lowCount,
|
|
371
|
+
total: findings.length,
|
|
372
|
+
},
|
|
373
|
+
findings,
|
|
374
|
+
};
|
|
312
375
|
// Persist the scan result so downstream consumers (the statusline's
|
|
313
376
|
// getSecurityStatus in funnel/local-signals.ts, which reads
|
|
314
377
|
// `.claude/security-scans/*.json`) reflect real scan state. Best-effort:
|
|
@@ -316,20 +379,6 @@ const scanCommand = {
|
|
|
316
379
|
try {
|
|
317
380
|
const scanDirOut = path.join(path.resolve(target), '.claude', 'security-scans');
|
|
318
381
|
fs.mkdirSync(scanDirOut, { recursive: true });
|
|
319
|
-
const record = {
|
|
320
|
-
timestamp: new Date().toISOString(),
|
|
321
|
-
target,
|
|
322
|
-
depth,
|
|
323
|
-
type: scanType,
|
|
324
|
-
summary: {
|
|
325
|
-
critical: criticalCount,
|
|
326
|
-
high: highCount,
|
|
327
|
-
medium: mediumCount,
|
|
328
|
-
low: lowCount,
|
|
329
|
-
total: findings.length,
|
|
330
|
-
},
|
|
331
|
-
findings,
|
|
332
|
-
};
|
|
333
382
|
// Deterministic name keyed on scan config so repeated runs overwrite
|
|
334
383
|
// rather than accumulate stale reports.
|
|
335
384
|
const outFile = path.join(scanDirOut, `scan-${scanType}-${depth}.json`);
|
|
@@ -340,24 +389,33 @@ const scanCommand = {
|
|
|
340
389
|
}
|
|
341
390
|
// Auto-fix if requested
|
|
342
391
|
if (fix && criticalCount + highCount > 0) {
|
|
343
|
-
|
|
392
|
+
if (outputFormat === 'text')
|
|
393
|
+
output.writeln();
|
|
344
394
|
const fixSpinner = output.createSpinner({ text: 'Attempting to fix vulnerabilities...', spinner: 'dots' });
|
|
345
|
-
|
|
395
|
+
if (outputFormat === 'text')
|
|
396
|
+
fixSpinner.start();
|
|
346
397
|
try {
|
|
347
398
|
try {
|
|
348
399
|
execSync('npm audit fix', { cwd: path.resolve(target), encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
349
400
|
}
|
|
350
401
|
catch { /* npm audit fix may exit non-zero */ }
|
|
351
|
-
|
|
402
|
+
if (outputFormat === 'text')
|
|
403
|
+
fixSpinner.succeed('Applied available fixes (run scan again to verify)');
|
|
352
404
|
}
|
|
353
405
|
catch {
|
|
354
|
-
|
|
406
|
+
if (outputFormat === 'text')
|
|
407
|
+
fixSpinner.fail('Some fixes could not be applied automatically');
|
|
355
408
|
}
|
|
356
409
|
}
|
|
410
|
+
if (outputFormat === 'json')
|
|
411
|
+
output.printJson(record);
|
|
412
|
+
else if (outputFormat === 'sarif')
|
|
413
|
+
output.printJson(scanSarif(findings));
|
|
357
414
|
return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
|
|
358
415
|
}
|
|
359
416
|
catch (error) {
|
|
360
|
-
|
|
417
|
+
if (outputFormat === 'text')
|
|
418
|
+
spinner.fail('Scan failed');
|
|
361
419
|
output.printError(`Error: ${error}`);
|
|
362
420
|
return { success: false };
|
|
363
421
|
}
|
|
@@ -1039,7 +1097,7 @@ const defendCommand = {
|
|
|
1039
1097
|
medium: output.info,
|
|
1040
1098
|
low: output.dim,
|
|
1041
1099
|
}[threat.severity] || output.dim;
|
|
1042
|
-
output.writeln(` ${severityColor(`[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
|
|
1100
|
+
output.writeln(` ${severityColor.call(output, `[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
|
|
1043
1101
|
output.writeln(` ${output.dim(threat.description)}`);
|
|
1044
1102
|
output.writeln(` Confidence: ${(threat.confidence * 100).toFixed(1)}%`);
|
|
1045
1103
|
output.writeln();
|
|
@@ -28,6 +28,33 @@ function formatDate(dateStr) {
|
|
|
28
28
|
// Otherwise show date
|
|
29
29
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* #3575: the session MCP tools report failure as `{ error }` (or
|
|
33
|
+
* `success: false`) rather than throwing, so every call site must check before
|
|
34
|
+
* printing a success line or reading result fields.
|
|
35
|
+
*/
|
|
36
|
+
function toolError(result) {
|
|
37
|
+
if (!result || typeof result !== 'object')
|
|
38
|
+
return 'the tool returned no result';
|
|
39
|
+
const r = result;
|
|
40
|
+
if (typeof r.error === 'string' && r.error.length > 0)
|
|
41
|
+
return r.error;
|
|
42
|
+
if (r.success === false)
|
|
43
|
+
return 'the operation failed';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** #3573: say what happened to memory instead of printing an invented 0. */
|
|
47
|
+
function memoryCaptureLabel(capture, fallback) {
|
|
48
|
+
if (!capture)
|
|
49
|
+
return fallback ?? 'unknown';
|
|
50
|
+
if (!capture.requested)
|
|
51
|
+
return 'not included';
|
|
52
|
+
if (capture.status === 'error')
|
|
53
|
+
return `not captured (${capture.error || 'error'})`;
|
|
54
|
+
if (capture.status === 'no-store')
|
|
55
|
+
return '0 (no memory store found)';
|
|
56
|
+
return capture.entries;
|
|
57
|
+
}
|
|
31
58
|
// Format session status
|
|
32
59
|
function formatStatus(status) {
|
|
33
60
|
switch (status) {
|
|
@@ -186,6 +213,12 @@ const saveCommand = {
|
|
|
186
213
|
includeAgents: ctx.flags['include-agents'] !== false,
|
|
187
214
|
includeTasks: ctx.flags['include-tasks'] !== false
|
|
188
215
|
});
|
|
216
|
+
const failure = toolError(result);
|
|
217
|
+
if (failure) {
|
|
218
|
+
spinner.fail('Failed to save session');
|
|
219
|
+
output.printError(failure);
|
|
220
|
+
return { success: false, exitCode: 1 };
|
|
221
|
+
}
|
|
189
222
|
spinner.succeed('Session saved');
|
|
190
223
|
output.writeln();
|
|
191
224
|
const stats = result.stats || {};
|
|
@@ -201,10 +234,13 @@ const saveCommand = {
|
|
|
201
234
|
{ property: 'Saved At', value: new Date(result.savedAt).toLocaleString() },
|
|
202
235
|
{ property: 'Agents', value: stats.agentCount ?? stats.agents ?? 0 },
|
|
203
236
|
{ property: 'Tasks', value: stats.taskCount ?? stats.tasks ?? 0 },
|
|
204
|
-
{ property: 'Memory Entries', value: stats.memoryEntries
|
|
237
|
+
{ property: 'Memory Entries', value: memoryCaptureLabel(result.memoryCapture, stats.memoryEntries) },
|
|
205
238
|
{ property: 'Total Size', value: formatSize(stats.totalSize ?? 0) }
|
|
206
239
|
]
|
|
207
240
|
});
|
|
241
|
+
if (result.memoryCapture?.status === 'error') {
|
|
242
|
+
output.printWarning(`Memory was requested but could not be captured: ${result.memoryCapture.error}`);
|
|
243
|
+
}
|
|
208
244
|
output.writeln();
|
|
209
245
|
output.printSuccess(`Session saved: ${result.sessionId}`);
|
|
210
246
|
output.printInfo(`Restore with: claude-flow session restore ${result.sessionId}`);
|
|
@@ -313,8 +349,25 @@ const restoreCommand = {
|
|
|
313
349
|
restoreAgents,
|
|
314
350
|
restoreTasks
|
|
315
351
|
});
|
|
316
|
-
|
|
352
|
+
const failure = toolError(result) ?? (result.restored === false ? 'Session not found' : null);
|
|
353
|
+
if (failure) {
|
|
354
|
+
spinner.fail('Failed to restore session');
|
|
355
|
+
output.printError(failure);
|
|
356
|
+
return { success: false, exitCode: 1 };
|
|
357
|
+
}
|
|
358
|
+
const memoryRestore = result.memoryRestore;
|
|
359
|
+
const memoryPartial = !!memoryRestore && memoryRestore.failed > 0;
|
|
360
|
+
if (memoryPartial) {
|
|
361
|
+
spinner.fail('Session restored with memory errors');
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
spinner.succeed('Session restored');
|
|
365
|
+
}
|
|
317
366
|
output.writeln();
|
|
367
|
+
// #3573: distinguish "not requested" from "the session holds no memory".
|
|
368
|
+
const memoryStatus = result.restoredComponents.memory
|
|
369
|
+
? (memoryPartial ? output.error('Partial') : output.success('Restored'))
|
|
370
|
+
: restoreMemory ? output.dim('Not in session') : output.dim('Skipped');
|
|
318
371
|
output.printTable({
|
|
319
372
|
columns: [
|
|
320
373
|
{ key: 'component', header: 'Component', width: 20 },
|
|
@@ -324,27 +377,37 @@ const restoreCommand = {
|
|
|
324
377
|
data: [
|
|
325
378
|
{
|
|
326
379
|
component: 'Memory',
|
|
327
|
-
status:
|
|
328
|
-
count: result.
|
|
380
|
+
status: memoryStatus,
|
|
381
|
+
count: result.restoredComponents.memory
|
|
382
|
+
? (result.stats.memoryEntriesRestored ?? result.stats.memoryEntries)
|
|
383
|
+
: 0
|
|
329
384
|
},
|
|
330
385
|
{
|
|
331
386
|
component: 'Agents',
|
|
332
|
-
status: result.
|
|
333
|
-
count: result.stats.
|
|
387
|
+
status: result.restoredComponents.agents ? output.success('Restored') : output.dim('Skipped'),
|
|
388
|
+
count: result.restoredComponents.agents ? result.stats.agents : 0
|
|
334
389
|
},
|
|
335
390
|
{
|
|
336
391
|
component: 'Tasks',
|
|
337
|
-
status: result.
|
|
338
|
-
count: result.stats.
|
|
392
|
+
status: result.restoredComponents.tasks ? output.success('Restored') : output.dim('Skipped'),
|
|
393
|
+
count: result.restoredComponents.tasks ? result.stats.tasks : 0
|
|
339
394
|
}
|
|
340
395
|
]
|
|
341
396
|
});
|
|
342
397
|
output.writeln();
|
|
343
|
-
|
|
398
|
+
if (memoryPartial) {
|
|
399
|
+
output.printWarning(`${memoryRestore.failed} memory entries could not be restored` +
|
|
400
|
+
(memoryRestore.errors.length ? `: ${memoryRestore.errors.join('; ')}` : ''));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
output.printSuccess(`Session ${sessionId} restored successfully`);
|
|
404
|
+
}
|
|
344
405
|
if (ctx.flags.format === 'json') {
|
|
345
406
|
output.printJson(result);
|
|
346
407
|
}
|
|
347
|
-
return
|
|
408
|
+
return memoryPartial
|
|
409
|
+
? { success: false, exitCode: 1, data: result }
|
|
410
|
+
: { success: true, data: result };
|
|
348
411
|
}
|
|
349
412
|
catch (error) {
|
|
350
413
|
spinner.fail('Failed to restore session');
|
|
@@ -391,6 +454,11 @@ const deleteCommand = {
|
|
|
391
454
|
}
|
|
392
455
|
try {
|
|
393
456
|
const result = await callMCPTool('session_delete', { sessionId });
|
|
457
|
+
const failure = toolError(result) ?? (result.deleted === false ? 'Session not found' : null);
|
|
458
|
+
if (failure) {
|
|
459
|
+
output.printError(`Failed to delete session: ${failure}`);
|
|
460
|
+
return { success: false, exitCode: 1 };
|
|
461
|
+
}
|
|
394
462
|
output.writeln();
|
|
395
463
|
output.printSuccess(`Session ${sessionId} deleted`);
|
|
396
464
|
if (ctx.flags.format === 'json') {
|
|
@@ -412,7 +480,7 @@ const deleteCommand = {
|
|
|
412
480
|
// Export subcommand
|
|
413
481
|
const exportCommand = {
|
|
414
482
|
name: 'export',
|
|
415
|
-
description: 'Export session to file',
|
|
483
|
+
description: 'Export a saved session to a file. Usage: session export <session-id> [-o file] (or --latest)',
|
|
416
484
|
options: [
|
|
417
485
|
{
|
|
418
486
|
name: 'output',
|
|
@@ -420,6 +488,12 @@ const exportCommand = {
|
|
|
420
488
|
description: 'Output file path',
|
|
421
489
|
type: 'string'
|
|
422
490
|
},
|
|
491
|
+
{
|
|
492
|
+
name: 'latest',
|
|
493
|
+
description: 'Export the most recently saved session when no session id is given',
|
|
494
|
+
type: 'boolean',
|
|
495
|
+
default: false
|
|
496
|
+
},
|
|
423
497
|
{
|
|
424
498
|
name: 'format',
|
|
425
499
|
short: 'f',
|
|
@@ -446,14 +520,23 @@ const exportCommand = {
|
|
|
446
520
|
let outputPath = ctx.flags.output;
|
|
447
521
|
const exportFormat = ctx.flags.format;
|
|
448
522
|
const compress = ctx.flags.compress;
|
|
449
|
-
//
|
|
523
|
+
// #3575: like `session delete`, refuse to guess which session to act on.
|
|
524
|
+
// Exporting the most recent one is available, but only when asked for.
|
|
450
525
|
if (!sessionId) {
|
|
526
|
+
if (!ctx.flags.latest) {
|
|
527
|
+
output.printError('Session ID is required. Pass a session id, or --latest to export the most recently saved session.');
|
|
528
|
+
return { success: false, exitCode: 1 };
|
|
529
|
+
}
|
|
451
530
|
try {
|
|
452
531
|
const current = await callMCPTool('session_current', {});
|
|
532
|
+
if (toolError(current) || !current.sessionId) {
|
|
533
|
+
output.printError('No saved sessions to export.');
|
|
534
|
+
return { success: false, exitCode: 1 };
|
|
535
|
+
}
|
|
453
536
|
sessionId = current.sessionId;
|
|
454
537
|
}
|
|
455
538
|
catch {
|
|
456
|
-
output.printError('No
|
|
539
|
+
output.printError('No saved sessions to export.');
|
|
457
540
|
return { success: false, exitCode: 1 };
|
|
458
541
|
}
|
|
459
542
|
}
|
|
@@ -469,6 +552,12 @@ const exportCommand = {
|
|
|
469
552
|
sessionId,
|
|
470
553
|
includeMemory: ctx.flags['include-memory'] !== false
|
|
471
554
|
});
|
|
555
|
+
const failure = toolError(result);
|
|
556
|
+
if (failure || result.data === undefined) {
|
|
557
|
+
spinner.fail('Failed to export session');
|
|
558
|
+
output.printError(failure || 'The export returned no session data');
|
|
559
|
+
return { success: false, exitCode: 1 };
|
|
560
|
+
}
|
|
472
561
|
// Format output
|
|
473
562
|
let content;
|
|
474
563
|
if (exportFormat === 'yaml') {
|
|
@@ -484,7 +573,8 @@ const exportCommand = {
|
|
|
484
573
|
fs.writeFileSync(absolutePath, content, 'utf-8');
|
|
485
574
|
spinner.succeed('Session exported');
|
|
486
575
|
output.writeln();
|
|
487
|
-
|
|
576
|
+
// session_export returns the whole record under `data`; its stats live there.
|
|
577
|
+
const exportStats = result.stats || result.data?.stats || {};
|
|
488
578
|
output.printTable({
|
|
489
579
|
columns: [
|
|
490
580
|
{ key: 'property', header: 'Property', width: 18 },
|
|
@@ -522,7 +612,7 @@ const exportCommand = {
|
|
|
522
612
|
// Import subcommand
|
|
523
613
|
const importCommand = {
|
|
524
614
|
name: 'import',
|
|
525
|
-
description: 'Import session from file',
|
|
615
|
+
description: 'Import a session from a file written by session export. Usage: session import <file> [--name n] [--activate]',
|
|
526
616
|
options: [
|
|
527
617
|
{
|
|
528
618
|
name: 'name',
|
|
@@ -555,21 +645,32 @@ const importCommand = {
|
|
|
555
645
|
const spinner = output.createSpinner({ text: 'Importing session...' });
|
|
556
646
|
spinner.start();
|
|
557
647
|
try {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// Parse based on extension
|
|
648
|
+
// The YAML written by `session export --format yaml` is display-only;
|
|
649
|
+
// there is no YAML parser here, so say so instead of failing obscurely.
|
|
561
650
|
if (absolutePath.endsWith('.yaml') || absolutePath.endsWith('.yml')) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
651
|
+
try {
|
|
652
|
+
JSON.parse(fs.readFileSync(absolutePath, 'utf-8'));
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
spinner.fail('Failed to import session');
|
|
656
|
+
output.printError('YAML session import is not supported. Export with --format json and import that file.');
|
|
657
|
+
return { success: false, exitCode: 1 };
|
|
658
|
+
}
|
|
567
659
|
}
|
|
660
|
+
// #3575: session_import reads the file itself from `inputPath`. Passing a
|
|
661
|
+
// parsed `data` object used to hit its required-argument check, and the
|
|
662
|
+
// CLI then printed "Session imported" before crashing on the error result.
|
|
568
663
|
const result = await callMCPTool('session_import', {
|
|
569
|
-
|
|
664
|
+
inputPath: absolutePath,
|
|
570
665
|
name: sessionName,
|
|
571
666
|
activate
|
|
572
667
|
});
|
|
668
|
+
const failure = toolError(result);
|
|
669
|
+
if (failure) {
|
|
670
|
+
spinner.fail('Failed to import session');
|
|
671
|
+
output.printError(failure);
|
|
672
|
+
return { success: false, exitCode: 1 };
|
|
673
|
+
}
|
|
573
674
|
spinner.succeed('Session imported');
|
|
574
675
|
output.writeln();
|
|
575
676
|
output.printTable({
|
|
@@ -619,6 +720,11 @@ const currentCommand = {
|
|
|
619
720
|
action: async (ctx) => {
|
|
620
721
|
try {
|
|
621
722
|
const result = await callMCPTool('session_current', { includeStats: true });
|
|
723
|
+
if (toolError(result)) {
|
|
724
|
+
output.printWarning('No active session');
|
|
725
|
+
output.printInfo('Save one with "claude-flow session save"');
|
|
726
|
+
return { success: true, data: { active: false } };
|
|
727
|
+
}
|
|
622
728
|
if (ctx.flags.format === 'json') {
|
|
623
729
|
output.printJson(result);
|
|
624
730
|
return { success: true, data: result };
|
|
@@ -727,7 +833,8 @@ export const sessionCommand = {
|
|
|
727
833
|
{ command: 'claude-flow session save -n "checkpoint-1"', description: 'Save current session' },
|
|
728
834
|
{ command: 'claude-flow session restore session-123', description: 'Restore a session' },
|
|
729
835
|
{ command: 'claude-flow session delete session-123', description: 'Delete a session' },
|
|
730
|
-
{ command: 'claude-flow session export -o backup.json', description: 'Export session to file' },
|
|
836
|
+
{ command: 'claude-flow session export session-123 -o backup.json', description: 'Export a session to file' },
|
|
837
|
+
{ command: 'claude-flow session export --latest -o backup.json', description: 'Export the most recently saved session' },
|
|
731
838
|
{ command: 'claude-flow session import backup.json', description: 'Import session from file' },
|
|
732
839
|
{ command: 'claude-flow session current', description: 'Show current session' }
|
|
733
840
|
],
|
|
@@ -196,17 +196,14 @@ function getSwarmStatus(swarmId) {
|
|
|
196
196
|
// Ignore
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
-
//
|
|
200
|
-
//
|
|
199
|
+
// Progress is completed / total tasks. With no tasks there is nothing to be
|
|
200
|
+
// a fraction of, so it is `null` with a reason — never an invented number
|
|
201
|
+
// (#3572: a hard-coded 5% was shown for a swarm with no tasks, forever).
|
|
201
202
|
const totalTasks = completedTasks + inProgressTasks + pendingTasks;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
else if (swarmState) {
|
|
207
|
-
// Swarm initialized but no tasks yet
|
|
208
|
-
progress = 5;
|
|
209
|
-
}
|
|
203
|
+
const progress = totalTasks > 0
|
|
204
|
+
? Math.round((completedTasks / totalTasks) * 100)
|
|
205
|
+
: null;
|
|
206
|
+
const progressReason = totalTasks > 0 ? null : 'no tasks';
|
|
210
207
|
// Determine status
|
|
211
208
|
let status = 'idle';
|
|
212
209
|
if (inProgressTasks > 0 || activeAgents > 0) {
|
|
@@ -243,6 +240,7 @@ function getSwarmStatus(swarmId) {
|
|
|
243
240
|
completed: 0
|
|
244
241
|
},
|
|
245
242
|
progress,
|
|
243
|
+
progressReason,
|
|
246
244
|
tasks: {
|
|
247
245
|
total: totalTasks,
|
|
248
246
|
completed: completedTasks,
|
|
@@ -738,7 +736,9 @@ const statusCommand = {
|
|
|
738
736
|
output.writeln(output.bold(`Swarm Status: ${status.id ?? output.dim('unknown id')}`));
|
|
739
737
|
output.writeln();
|
|
740
738
|
// Progress bar
|
|
741
|
-
output.writeln(
|
|
739
|
+
output.writeln(status.progress === null
|
|
740
|
+
? `Overall Progress: ${output.dim(`n/a (${status.progressReason})`)}`
|
|
741
|
+
: `Overall Progress: ${output.progressBar(status.progress, 100, 40)}`);
|
|
742
742
|
output.writeln();
|
|
743
743
|
// Agent status
|
|
744
744
|
output.writeln(output.bold('Agents'));
|
|
@@ -154,7 +154,7 @@ const createCommand = {
|
|
|
154
154
|
type: taskType,
|
|
155
155
|
description,
|
|
156
156
|
priority: priority || 'normal',
|
|
157
|
-
|
|
157
|
+
assignTo: ctx.flags.assign ? String(ctx.flags.assign).split(',').map(id => id.trim()).filter(Boolean) : undefined,
|
|
158
158
|
parentId: ctx.flags.parent,
|
|
159
159
|
dependencies,
|
|
160
160
|
tags,
|
|
@@ -245,14 +245,15 @@ const listCommand = {
|
|
|
245
245
|
}
|
|
246
246
|
],
|
|
247
247
|
action: async (ctx) => {
|
|
248
|
-
const
|
|
248
|
+
const requestedStatus = ctx.flags.all ? 'all' : ctx.flags.status || 'pending,in_progress';
|
|
249
|
+
const status = requestedStatus === 'all' ? undefined : requestedStatus.replace(/\brunning\b/g, 'in_progress');
|
|
249
250
|
const limit = ctx.flags.limit;
|
|
250
251
|
try {
|
|
251
252
|
const result = await callMCPTool('task_list', {
|
|
252
253
|
status,
|
|
253
254
|
type: ctx.flags.type,
|
|
254
255
|
priority: ctx.flags.priority,
|
|
255
|
-
|
|
256
|
+
assignedTo: ctx.flags.agent,
|
|
256
257
|
limit,
|
|
257
258
|
offset: 0
|
|
258
259
|
});
|
|
@@ -277,7 +278,7 @@ const listCommand = {
|
|
|
277
278
|
{ key: 'progress', header: 'Progress', width: 10 }
|
|
278
279
|
],
|
|
279
280
|
data: result.tasks.map(t => ({
|
|
280
|
-
id: t.
|
|
281
|
+
id: t.taskId,
|
|
281
282
|
type: t.type,
|
|
282
283
|
description: t.description.length > 27
|
|
283
284
|
? t.description.slice(0, 27) + '...'
|
|
@@ -155,10 +155,19 @@ export class CLI {
|
|
|
155
155
|
// loudly (not silent) — the existing project helpers were left intact.
|
|
156
156
|
this.output.printWarning(`Skipped helper auto-refresh — ${r.blocked}. Reinstall @claude-flow/cli from a trusted source.`);
|
|
157
157
|
}
|
|
158
|
+
else if (r.healed) {
|
|
159
|
+
// #3565: a critical helper's stamp matched but its on-disk content
|
|
160
|
+
// didn't hash-match the signed manifest — restored, but always
|
|
161
|
+
// surfaced (not verbose-gated like a routine version-bump refresh).
|
|
162
|
+
this.output.printWarning(`Detected tampered critical helper(s) in .claude/helpers (${(r.tampered || []).join(', ')}) — restored verified content from the installed package. If this wasn't expected, find out what modified them.`);
|
|
163
|
+
}
|
|
158
164
|
else if (r.refreshed && this.output.isVerbose()) {
|
|
159
165
|
this.output.printDebug(`Refreshed .claude/helpers (${r.from} → ${r.to})`);
|
|
160
166
|
}
|
|
161
|
-
if (r.global?.
|
|
167
|
+
if (r.global?.healed) {
|
|
168
|
+
this.output.printWarning(`Detected tampered critical helper(s) in ~/.claude/helpers (${(r.global.tampered || []).join(', ')}) — restored verified content.`);
|
|
169
|
+
}
|
|
170
|
+
else if (r.global?.refreshed && this.output.isVerbose()) {
|
|
162
171
|
this.output.printDebug(`Refreshed ~/.claude/helpers (${r.global.from} → ${r.global.to})`);
|
|
163
172
|
}
|
|
164
173
|
else if (r.global?.blocked && r.global.blocked !== r.blocked) {
|