claude-flow 3.47.1 → 3.48.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/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/metrics-db.mjs +4 -2
- 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/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +0 -2
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- 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/hooks.js +15 -5
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
- 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 +0 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
- 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.js +34 -8
- 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 +41 -31
- 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/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
|
@@ -9,6 +9,8 @@ import { distillCommand } from './memory-distill.js';
|
|
|
9
9
|
import { backupCommand } from './memory-backup.js';
|
|
10
10
|
import { countSiblingStoreRows } from '../memory/sibling-store.js';
|
|
11
11
|
import { resolveDbPath } from '../memory/memory-initializer.js';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { siblingAgentDbPath } from '../memory/memory-bridge.js';
|
|
12
14
|
/**
|
|
13
15
|
* #3228: a miss in one store is not a miss in the memory.
|
|
14
16
|
*
|
|
@@ -26,6 +28,24 @@ async function warnIfSiblingHasRows(pathFlag) {
|
|
|
26
28
|
`That store is written by the MCP/AgentDB path; read it with --path ${unread.path}.`);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
/** Default CLI writes may be mirrored into AgentDB; remove both copies. */
|
|
32
|
+
function removalDbTargets(pathFlag) {
|
|
33
|
+
const primary = resolveDbPath(pathFlag);
|
|
34
|
+
// An explicit file selection is a single-store operation, as documented by
|
|
35
|
+
// --path. Do not widen a caller's requested destructive scope.
|
|
36
|
+
if (pathFlag || process.env.CLAUDE_FLOW_DB_PATH)
|
|
37
|
+
return [{ dbPath: primary }];
|
|
38
|
+
const sibling = siblingAgentDbPath(primary);
|
|
39
|
+
// The AgentDB mirror is consumed by native SQLite and must stay plaintext.
|
|
40
|
+
// Pass this per operation, including preview migrations; never toggle the
|
|
41
|
+
// process encryption setting while asynchronous operations are in flight.
|
|
42
|
+
const targets = [
|
|
43
|
+
{ dbPath: primary },
|
|
44
|
+
...(sibling ? [{ dbPath: sibling, encryptWrites: false }] : []),
|
|
45
|
+
].filter(target => existsSync(target.dbPath));
|
|
46
|
+
// Preserve the existing missing-database error when neither store exists.
|
|
47
|
+
return targets.length ? targets : [{ dbPath: primary }];
|
|
48
|
+
}
|
|
29
49
|
// Memory backends
|
|
30
50
|
const BACKENDS = [
|
|
31
51
|
{ value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing (150x-12,500x faster)' },
|
|
@@ -820,7 +840,7 @@ function formatRelativeTime(isoDate) {
|
|
|
820
840
|
const deleteCommand = {
|
|
821
841
|
name: 'delete',
|
|
822
842
|
aliases: ['rm'],
|
|
823
|
-
description: 'Delete memory entry',
|
|
843
|
+
description: 'Delete a memory entry from the default store and its AgentDB mirror (use --path to select one store)',
|
|
824
844
|
options: [
|
|
825
845
|
{
|
|
826
846
|
name: 'key',
|
|
@@ -870,16 +890,24 @@ const deleteCommand = {
|
|
|
870
890
|
}
|
|
871
891
|
// Use sql.js directly for consistent data access (Issue #980)
|
|
872
892
|
try {
|
|
873
|
-
const { deleteEntry
|
|
874
|
-
const
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
893
|
+
const { deleteEntry } = await import('../memory/memory-initializer.js');
|
|
894
|
+
const paths = removalDbTargets(ctx.flags.path);
|
|
895
|
+
const stores = [];
|
|
896
|
+
for (const target of paths) {
|
|
897
|
+
const { dbPath } = target;
|
|
898
|
+
const entry = await deleteEntry({ key, namespace, ...target });
|
|
899
|
+
if (!entry.success)
|
|
900
|
+
throw new Error(`${dbPath}: ${entry.error || 'Failed to delete'}`);
|
|
901
|
+
stores.push({ dbPath, ...entry });
|
|
879
902
|
}
|
|
903
|
+
const result = {
|
|
904
|
+
success: true, key, namespace, stores,
|
|
905
|
+
deleted: stores.some(entry => entry.deleted),
|
|
906
|
+
remainingEntries: stores.reduce((total, entry) => total + entry.remainingEntries, 0),
|
|
907
|
+
};
|
|
880
908
|
if (result.deleted) {
|
|
881
909
|
output.printSuccess(`Deleted "${key}" from namespace "${namespace}"`);
|
|
882
|
-
output.printInfo(`Remaining entries: ${result.remainingEntries}`);
|
|
910
|
+
output.printInfo(`Remaining entries across ${paths.length} store(s): ${result.remainingEntries}`);
|
|
883
911
|
}
|
|
884
912
|
else {
|
|
885
913
|
output.printWarning(`Key not found: "${key}" in namespace "${namespace}"`);
|
|
@@ -901,7 +929,7 @@ const deleteCommand = {
|
|
|
901
929
|
// either interactive confirmation or --force.
|
|
902
930
|
const purgeCommand = {
|
|
903
931
|
name: 'purge',
|
|
904
|
-
description: 'Permanently delete
|
|
932
|
+
description: 'Permanently delete a namespace from the default store and its AgentDB mirror (use --path to select one store)',
|
|
905
933
|
options: [
|
|
906
934
|
{
|
|
907
935
|
name: 'namespace',
|
|
@@ -939,17 +967,22 @@ const purgeCommand = {
|
|
|
939
967
|
return { success: false, exitCode: 1 };
|
|
940
968
|
}
|
|
941
969
|
try {
|
|
942
|
-
const { listEntries, purgeNamespace
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
const
|
|
970
|
+
const { listEntries, purgeNamespace } = await import('../memory/memory-initializer.js');
|
|
971
|
+
const paths = removalDbTargets(dbPath);
|
|
972
|
+
let previewCount = 0;
|
|
973
|
+
for (const target of paths) {
|
|
974
|
+
const preview = await listEntries({ namespace, limit: 1, ...target });
|
|
975
|
+
if (!preview.success)
|
|
976
|
+
throw new Error(`${target.dbPath}: ${preview.error || 'Failed to read purge target'}`);
|
|
977
|
+
previewCount += preview.total ?? preview.entries?.length ?? 0;
|
|
978
|
+
}
|
|
946
979
|
if (dryRun) {
|
|
947
|
-
output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" (dry run — nothing deleted)`);
|
|
980
|
+
output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" across ${paths.length} store(s) (dry run — nothing deleted)`);
|
|
948
981
|
return { success: true, data: { namespace, wouldDelete: previewCount } };
|
|
949
982
|
}
|
|
950
983
|
if (!force && ctx.interactive) {
|
|
951
984
|
const confirmed = await confirm({
|
|
952
|
-
message: `Permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}"? This is a hard delete — not reversible with \`memory delete\`'s soft-undo.`,
|
|
985
|
+
message: `Permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" across ${paths.length} store(s)? This is a hard delete — not reversible with \`memory delete\`'s soft-undo.`,
|
|
953
986
|
default: false
|
|
954
987
|
});
|
|
955
988
|
if (!confirmed) {
|
|
@@ -961,13 +994,20 @@ const purgeCommand = {
|
|
|
961
994
|
output.printError(`Refusing to purge namespace "${namespace}" without --force in non-interactive mode`);
|
|
962
995
|
return { success: false, exitCode: 1 };
|
|
963
996
|
}
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
997
|
+
const stores = [];
|
|
998
|
+
for (const target of paths) {
|
|
999
|
+
const purged = await purgeNamespace({ namespace, ...target });
|
|
1000
|
+
if (!purged.success)
|
|
1001
|
+
throw new Error(`${target.dbPath}: ${purged.error || 'Failed to purge'}`);
|
|
1002
|
+
stores.push({ dbPath: target.dbPath, ...purged });
|
|
968
1003
|
}
|
|
1004
|
+
const result = {
|
|
1005
|
+
success: true, stores,
|
|
1006
|
+
deletedCount: stores.reduce((total, store) => total + store.deletedCount, 0),
|
|
1007
|
+
remainingEntries: stores.reduce((total, store) => total + store.remainingEntries, 0),
|
|
1008
|
+
};
|
|
969
1009
|
output.printSuccess(`Purged ${result.deletedCount} entr${result.deletedCount === 1 ? 'y' : 'ies'} from namespace "${namespace}"`);
|
|
970
|
-
output.printInfo(`Remaining entries (all namespaces): ${result.remainingEntries}`);
|
|
1010
|
+
output.printInfo(`Remaining entries (all namespaces, ${paths.length} store(s)): ${result.remainingEntries}`);
|
|
971
1011
|
return { success: true, data: result };
|
|
972
1012
|
}
|
|
973
1013
|
catch (error) {
|
|
@@ -984,8 +1024,16 @@ const statsCommand = {
|
|
|
984
1024
|
action: async (ctx) => {
|
|
985
1025
|
// Call MCP memory/stats tool for real statistics
|
|
986
1026
|
try {
|
|
987
|
-
const
|
|
1027
|
+
const dbPath = resolveDbPath(ctx.flags.path);
|
|
1028
|
+
const statsResult = await callMCPTool('memory_stats', { dbPath });
|
|
1029
|
+
if (statsResult.available === false) {
|
|
1030
|
+
output.printError(`Failed to get stats: ${statsResult.error || 'Store unavailable'}`);
|
|
1031
|
+
return { success: false, exitCode: 1 };
|
|
1032
|
+
}
|
|
1033
|
+
const unreadStore = await countSiblingStoreRows(dbPath);
|
|
988
1034
|
const stats = {
|
|
1035
|
+
...(unreadStore ? { unreadStore } : {}),
|
|
1036
|
+
...(statsResult.truncated ? { truncated: true } : {}),
|
|
989
1037
|
backend: statsResult.backend,
|
|
990
1038
|
entries: {
|
|
991
1039
|
total: statsResult.totalEntries,
|
|
@@ -993,7 +1041,7 @@ const statsCommand = {
|
|
|
993
1041
|
text: statsResult.totalEntries
|
|
994
1042
|
},
|
|
995
1043
|
storage: {
|
|
996
|
-
total: statsResult.totalSize,
|
|
1044
|
+
total: statsResult.totalSize ?? 'unknown',
|
|
997
1045
|
location: statsResult.location
|
|
998
1046
|
},
|
|
999
1047
|
version: statsResult.version,
|
|
@@ -1004,6 +1052,9 @@ const statsCommand = {
|
|
|
1004
1052
|
output.printJson(stats);
|
|
1005
1053
|
return { success: true, data: stats };
|
|
1006
1054
|
}
|
|
1055
|
+
if (unreadStore) {
|
|
1056
|
+
output.printWarning(`${unreadStore.rows} entries are in ${unreadStore.path} and were not read here. Read them with --path ${unreadStore.path}.`);
|
|
1057
|
+
}
|
|
1007
1058
|
output.writeln();
|
|
1008
1059
|
output.writeln(output.bold('Memory Statistics'));
|
|
1009
1060
|
output.writeln();
|
|
@@ -1245,19 +1296,27 @@ const cleanupCommand = {
|
|
|
1245
1296
|
{ command: 'claude-flow memory cleanup --expired-only', description: 'Clean expired entries' }
|
|
1246
1297
|
],
|
|
1247
1298
|
action: async (ctx) => {
|
|
1248
|
-
const dryRun = ctx.flags.dryRun;
|
|
1249
|
-
const force = ctx.flags.force;
|
|
1299
|
+
const dryRun = ctx.flags.dryRun === true;
|
|
1300
|
+
const force = ctx.flags.force === true;
|
|
1301
|
+
if (ctx.flags.format === 'json' && !dryRun && !force) {
|
|
1302
|
+
output.printJson({ success: false, error: 'Use --dry-run to preview or --force to authorize cleanup with --format json.' });
|
|
1303
|
+
return { success: false, exitCode: 1 };
|
|
1304
|
+
}
|
|
1250
1305
|
if (dryRun) {
|
|
1251
1306
|
output.writeln(output.warning('DRY RUN - No changes will be made'));
|
|
1252
1307
|
}
|
|
1253
1308
|
output.printInfo('Analyzing memory for cleanup...');
|
|
1254
1309
|
try {
|
|
1255
|
-
const
|
|
1256
|
-
dryRun,
|
|
1310
|
+
const selectors = {
|
|
1257
1311
|
olderThan: ctx.flags.olderThan,
|
|
1258
1312
|
expiredOnly: ctx.flags.expiredOnly,
|
|
1259
1313
|
lowQualityThreshold: ctx.flags.lowQuality,
|
|
1260
1314
|
namespace: ctx.flags.namespace,
|
|
1315
|
+
};
|
|
1316
|
+
// Preview before asking: dryRun:false executes deletion inside the tool.
|
|
1317
|
+
let result = await callMCPTool('memory_cleanup', {
|
|
1318
|
+
...selectors,
|
|
1319
|
+
dryRun: dryRun || !force,
|
|
1261
1320
|
});
|
|
1262
1321
|
if (ctx.flags.format === 'json') {
|
|
1263
1322
|
output.printJson(result);
|
|
@@ -1286,6 +1345,7 @@ const cleanupCommand = {
|
|
|
1286
1345
|
output.printInfo('Cleanup cancelled');
|
|
1287
1346
|
return { success: true, data: result };
|
|
1288
1347
|
}
|
|
1348
|
+
result = await callMCPTool('memory_cleanup', { ...selectors, dryRun: false });
|
|
1289
1349
|
}
|
|
1290
1350
|
if (!dryRun) {
|
|
1291
1351
|
output.writeln();
|
|
@@ -1525,7 +1585,8 @@ const importCommand = {
|
|
|
1525
1585
|
short: 'n',
|
|
1526
1586
|
description: 'Import into specific namespace',
|
|
1527
1587
|
type: 'string'
|
|
1528
|
-
}
|
|
1588
|
+
},
|
|
1589
|
+
DB_PATH_OPTION
|
|
1529
1590
|
],
|
|
1530
1591
|
examples: [
|
|
1531
1592
|
{ command: 'claude-flow memory import -i ./backup.json', description: 'Import from file' },
|
|
@@ -1543,6 +1604,7 @@ const importCommand = {
|
|
|
1543
1604
|
inputPath,
|
|
1544
1605
|
merge: ctx.flags.merge ?? true,
|
|
1545
1606
|
namespace: ctx.flags.namespace,
|
|
1607
|
+
dbPath: resolveDbPath(ctx.flags.path),
|
|
1546
1608
|
});
|
|
1547
1609
|
output.printSuccess(`Imported from ${result.inputPath}`);
|
|
1548
1610
|
output.printList([
|
|
@@ -279,6 +279,7 @@ export const metaharnessCommand = {
|
|
|
279
279
|
'verbose', 'v',
|
|
280
280
|
'quiet', 'q',
|
|
281
281
|
'help', 'h',
|
|
282
|
+
'interactive', 'i', // CLI-only default; plugin scripts are non-interactive
|
|
282
283
|
]);
|
|
283
284
|
// Parser normalizes kebab-case → camelCase (--per-dimension → perDimension).
|
|
284
285
|
// Plugin scripts expect kebab-case at argv, so we re-kebab here.
|
|
@@ -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();
|
|
@@ -324,18 +324,18 @@ const restoreCommand = {
|
|
|
324
324
|
data: [
|
|
325
325
|
{
|
|
326
326
|
component: 'Memory',
|
|
327
|
-
status: result.
|
|
328
|
-
count: result.stats.
|
|
327
|
+
status: result.restoredComponents.memory ? output.success('Restored') : output.dim('Skipped'),
|
|
328
|
+
count: result.restoredComponents.memory ? result.stats.memoryEntries : 0
|
|
329
329
|
},
|
|
330
330
|
{
|
|
331
331
|
component: 'Agents',
|
|
332
|
-
status: result.
|
|
333
|
-
count: result.stats.
|
|
332
|
+
status: result.restoredComponents.agents ? output.success('Restored') : output.dim('Skipped'),
|
|
333
|
+
count: result.restoredComponents.agents ? result.stats.agents : 0
|
|
334
334
|
},
|
|
335
335
|
{
|
|
336
336
|
component: 'Tasks',
|
|
337
|
-
status: result.
|
|
338
|
-
count: result.stats.
|
|
337
|
+
status: result.restoredComponents.tasks ? output.success('Restored') : output.dim('Skipped'),
|
|
338
|
+
count: result.restoredComponents.tasks ? result.stats.tasks : 0
|
|
339
339
|
}
|
|
340
340
|
]
|
|
341
341
|
});
|
|
@@ -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) + '...'
|
|
@@ -1005,12 +1005,14 @@ export function generateAutoMemoryHook() {
|
|
|
1005
1005
|
*/
|
|
1006
1006
|
|
|
1007
1007
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1008
|
-
import { join, dirname } from 'path';
|
|
1008
|
+
import { join, dirname, resolve } from 'path';
|
|
1009
1009
|
import { fileURLToPath } from 'url';
|
|
1010
1010
|
|
|
1011
1011
|
const __filename = fileURLToPath(import.meta.url);
|
|
1012
1012
|
const __dirname = dirname(__filename);
|
|
1013
|
-
const PROJECT_ROOT =
|
|
1013
|
+
const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
|
|
1014
|
+
? resolve(process.env.CLAUDE_PROJECT_DIR)
|
|
1015
|
+
: join(__dirname, '../..');
|
|
1014
1016
|
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
|
1015
1017
|
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
|
1016
1018
|
|
|
@@ -27,6 +27,12 @@ import { fileURLToPath } from 'url';
|
|
|
27
27
|
import { dirname } from 'path';
|
|
28
28
|
import { trackRequest } from './mcp-tools/request-tracker.js';
|
|
29
29
|
import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
|
|
30
|
+
/** A failed task can be valid data; only an explicit error denotes tool failure. */
|
|
31
|
+
function hasToolError(result) {
|
|
32
|
+
return result !== null && typeof result === 'object'
|
|
33
|
+
&& typeof result.error === 'string'
|
|
34
|
+
&& result.error.trim().length > 0;
|
|
35
|
+
}
|
|
30
36
|
// ESM-compatible __dirname
|
|
31
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
32
38
|
const __dirname = dirname(__filename);
|
|
@@ -666,11 +672,12 @@ export class MCPServerManager extends EventEmitter {
|
|
|
666
672
|
}
|
|
667
673
|
try {
|
|
668
674
|
const result = await callMCPTool(toolName, toolParams, { sessionId });
|
|
669
|
-
|
|
675
|
+
const isError = hasToolError(result);
|
|
676
|
+
trackRequest(toolName, !isError);
|
|
670
677
|
return {
|
|
671
678
|
jsonrpc: '2.0',
|
|
672
679
|
id: message.id,
|
|
673
|
-
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
|
|
680
|
+
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError },
|
|
674
681
|
};
|
|
675
682
|
}
|
|
676
683
|
catch (error) {
|
|
@@ -741,7 +748,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
741
748
|
handler: async (input, context) => {
|
|
742
749
|
try {
|
|
743
750
|
const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId });
|
|
744
|
-
trackRequest(tool.name,
|
|
751
|
+
trackRequest(tool.name, !hasToolError(result));
|
|
745
752
|
return result;
|
|
746
753
|
}
|
|
747
754
|
catch (error) {
|