claude-flow 3.47.0 → 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/hive-mind-worker.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.js +242 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +24 -41
- 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/mcp-tools/task-tools.js +73 -53
- 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
|
@@ -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) {
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* AgentDB MCP Tools — Phase 6 of ADR-053
|
|
3
|
-
*
|
|
4
|
-
* Exposes AgentDB v3 controller operations as MCP tools.
|
|
5
|
-
* Provides direct access to ReasoningBank, CausalGraph, SkillLibrary,
|
|
6
|
-
* AttestationLog, and bridge health through the MCP protocol.
|
|
7
|
-
*
|
|
8
|
-
* Security: All handlers validate input types, enforce length bounds,
|
|
9
|
-
* and sanitize error messages before returning to MCP callers.
|
|
10
|
-
*
|
|
11
|
-
* @module v3/cli/mcp-tools/agentdb-tools
|
|
12
|
-
*/
|
|
1
|
+
import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
|
|
13
2
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
14
3
|
// ===== Shared validation helpers =====
|
|
15
4
|
const MAX_STRING_LENGTH = 100_000; // 100KB max for any string input
|
|
@@ -66,12 +55,6 @@ async function getBridge() {
|
|
|
66
55
|
// Lazy-cached modules used by graph-query / graph-pathfinder dispatch.
|
|
67
56
|
// Caching the resolved namespace avoids per-call dynamic-import overhead
|
|
68
57
|
// in the ADR-130 hot path (smoke harness measures elapsedMs of every call).
|
|
69
|
-
let graphBackendMod = null;
|
|
70
|
-
async function getGraphBackend() {
|
|
71
|
-
if (!graphBackendMod)
|
|
72
|
-
graphBackendMod = await import('../ruvector/graph-backend.js');
|
|
73
|
-
return graphBackendMod;
|
|
74
|
-
}
|
|
75
58
|
let graphEdgeWriterMod = null;
|
|
76
59
|
async function getGraphEdgeWriter() {
|
|
77
60
|
if (!graphEdgeWriterMod)
|
|
@@ -396,6 +379,7 @@ export const agentdbFeedback = {
|
|
|
396
379
|
type: 'object',
|
|
397
380
|
properties: {
|
|
398
381
|
taskId: { type: 'string', description: 'Task identifier' },
|
|
382
|
+
patterns: feedbackPatternsSchema,
|
|
399
383
|
success: { type: 'boolean', description: 'Whether task succeeded' },
|
|
400
384
|
quality: { type: 'number', description: 'Quality score (0-1)' },
|
|
401
385
|
agent: { type: 'string', description: 'Agent that performed the task' },
|
|
@@ -415,9 +399,11 @@ export const agentdbFeedback = {
|
|
|
415
399
|
const taskId = validateString(params.taskId, 'taskId', 500);
|
|
416
400
|
if (!taskId)
|
|
417
401
|
return { success: false, error: 'taskId is required (non-empty string, max 500 chars)' };
|
|
402
|
+
const patterns = validateFeedbackPatterns(params.patterns);
|
|
418
403
|
const bridge = await getBridge();
|
|
419
404
|
const result = await bridge.bridgeRecordFeedback({
|
|
420
405
|
taskId,
|
|
406
|
+
patterns,
|
|
421
407
|
success: params.success === true,
|
|
422
408
|
quality: validateScore(params.quality, 0.85),
|
|
423
409
|
agent: validateString(params.agent, 'agent', 200) ?? undefined,
|
|
@@ -802,21 +788,18 @@ export const agentdbHierarchicalRecall = {
|
|
|
802
788
|
// ===== agentdb_consolidate — Run memory consolidation =====
|
|
803
789
|
export const agentdbConsolidate = {
|
|
804
790
|
name: 'agentdb_consolidate',
|
|
805
|
-
description: '
|
|
791
|
+
description: 'Request memory consolidation. Use when checking whether AgentDB can consolidate retained memories; native file edits cannot perform controller consolidation. Currently returns unsupported when the installed controller is a no-op stub; no entries are promoted or compressed in that case.',
|
|
806
792
|
inputSchema: {
|
|
807
793
|
type: 'object',
|
|
808
|
-
properties: {
|
|
809
|
-
minAge: { type: 'number', description: 'Minimum age in hours since store (optional)' },
|
|
810
|
-
maxEntries: { type: 'number', description: 'Maximum entries to consolidate (optional)' },
|
|
811
|
-
},
|
|
794
|
+
properties: {},
|
|
812
795
|
},
|
|
813
796
|
handler: async (params) => {
|
|
814
797
|
try {
|
|
798
|
+
if (Object.keys(params).length > 0) {
|
|
799
|
+
return { success: false, status: 'unsupported', error: 'agentdb_consolidate does not support options' };
|
|
800
|
+
}
|
|
815
801
|
const bridge = await getBridge();
|
|
816
|
-
const result = await bridge.bridgeConsolidate({
|
|
817
|
-
minAge: typeof params.minAge === 'number' ? Math.max(0, params.minAge) : undefined,
|
|
818
|
-
maxEntries: validatePositiveInt(params.maxEntries, 1000, 10_000),
|
|
819
|
-
});
|
|
802
|
+
const result = await bridge.bridgeConsolidate({});
|
|
820
803
|
return result ?? { success: false, error: 'AgentDB bridge not available. Use memory_store/memory_search instead.' };
|
|
821
804
|
}
|
|
822
805
|
catch (error) {
|
|
@@ -1060,7 +1043,7 @@ export const agentdbCausalNodeDelete = {
|
|
|
1060
1043
|
};
|
|
1061
1044
|
export const agentdbGraphQuery = {
|
|
1062
1045
|
name: 'agentdb_graph-query',
|
|
1063
|
-
description: 'Unified graph traversal across the knowledge graph (ADR-130).
|
|
1046
|
+
description: 'Unified graph traversal across the knowledge graph (ADR-130). K-hop queries read committed graph_edges relationships through SQL; a separately populated native graph is not evidence of retained-history coverage. Inspect appliedDepth/truncated because SQL is bounded at 3 hops. maxNodesVisited bounds returned rows, not traversal work or elapsed time. Semantic and PageRank modes use their own backends. Use when you need relationship traversal beyond flat memory search.',
|
|
1064
1047
|
inputSchema: {
|
|
1065
1048
|
type: 'object',
|
|
1066
1049
|
properties: {
|
|
@@ -1101,53 +1084,38 @@ export const agentdbGraphQuery = {
|
|
|
1101
1084
|
}
|
|
1102
1085
|
const budgetRaw = (params.complexityBudget ?? {});
|
|
1103
1086
|
const budget = {
|
|
1104
|
-
maxNodesVisited: budgetRaw.maxNodesVisited
|
|
1105
|
-
maxDepth: budgetRaw.maxDepth
|
|
1087
|
+
maxNodesVisited: validatePositiveInt(budgetRaw.maxNodesVisited, 10_000, 10_000),
|
|
1088
|
+
maxDepth: validatePositiveInt(budgetRaw.maxDepth, 5, 5),
|
|
1106
1089
|
maxMillis: budgetRaw.maxMillis ?? 50,
|
|
1107
1090
|
maxMemoryMB: budgetRaw.maxMemoryMB ?? 32,
|
|
1108
1091
|
};
|
|
1109
|
-
const
|
|
1092
|
+
const requestedDepth = validatePositiveInt(params.depth, 2, 5);
|
|
1093
|
+
const depth = Math.min(requestedDepth, budget.maxDepth);
|
|
1110
1094
|
const topK = validatePositiveInt(params.topK, 10, MAX_TOP_K);
|
|
1111
1095
|
const relation = validateString(params.relation, 'relation', 200) ?? undefined;
|
|
1112
1096
|
// ── k-hop mode ──────────────────────────────────────────────────────────
|
|
1113
1097
|
if (mode === 'k-hop') {
|
|
1114
|
-
// graph-node
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
// unsupported native query instead of silently widening it.
|
|
1118
|
-
try {
|
|
1119
|
-
const graphBackend = await getGraphBackend();
|
|
1120
|
-
if (await graphBackend.isGraphBackendAvailable()) {
|
|
1121
|
-
if (relation) {
|
|
1122
|
-
return { success: false, error: 'Native graph-node k-hop does not support relation filtering', unsupported: 'relation', mode, nodeId, relation, backend: 'graph-node' };
|
|
1123
|
-
}
|
|
1124
|
-
const neighbors = await graphBackend.getNeighbors(nodeId, depth);
|
|
1125
|
-
return {
|
|
1126
|
-
success: true, mode, nodeId, depth, appliedDepth: depth,
|
|
1127
|
-
results: neighbors.map(id => ({ nodeId: id })),
|
|
1128
|
-
count: neighbors.length,
|
|
1129
|
-
backend: 'graph-node',
|
|
1130
|
-
elapsedMs: Date.now() - t0,
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
catch { /* fall through to SQL */ }
|
|
1135
|
-
// SQL CTE fallback is bounded at depth 3. Report that bound so callers
|
|
1136
|
-
// cannot mistake an incomplete depth-5 cycle check for a complete one.
|
|
1098
|
+
// #3315: graph-node and retained SQL are populated independently.
|
|
1099
|
+
// Native availability does not establish historical coverage. Query
|
|
1100
|
+
// the committed relationship source, and fail if it cannot be read.
|
|
1137
1101
|
try {
|
|
1138
1102
|
const { getBridgeDb } = await getGraphEdgeWriter();
|
|
1139
1103
|
const db = await getBridgeDb();
|
|
1140
1104
|
if (db) {
|
|
1141
1105
|
const appliedDepth = Math.min(depth, 3);
|
|
1142
|
-
const cteSql = buildKHopCTE(nodeId, appliedDepth, relation, budget.maxNodesVisited);
|
|
1106
|
+
const cteSql = buildKHopCTE(nodeId, appliedDepth, relation, budget.maxNodesVisited + 1);
|
|
1143
1107
|
// graph-edge-writer returns a better-sqlite3 Database after #2431.
|
|
1144
1108
|
// `db.exec(sql, params)` (sql.js style) is a runner with no result
|
|
1145
1109
|
// on better-sqlite3 — use `prepare(sql).raw().all(...)` to get the
|
|
1146
1110
|
// same array-of-arrays shape the downstream code expects.
|
|
1147
|
-
const
|
|
1111
|
+
const allRows = db.prepare(cteSql).raw().all();
|
|
1112
|
+
const resultLimitReached = allRows.length > budget.maxNodesVisited;
|
|
1113
|
+
const rows = allRows.slice(0, budget.maxNodesVisited);
|
|
1114
|
+
const depthLimited = appliedDepth < requestedDepth;
|
|
1148
1115
|
return {
|
|
1149
|
-
success: true, mode, nodeId, depth, appliedDepth,
|
|
1150
|
-
|
|
1116
|
+
success: true, mode, nodeId, depth, requestedDepth, appliedDepth,
|
|
1117
|
+
depthLimited, resultLimitReached, truncated: depthLimited || resultLimitReached,
|
|
1118
|
+
resultLimit: budget.maxNodesVisited,
|
|
1151
1119
|
results: rows.map((r) => ({ nodeId: r[0], depth: r[1] })),
|
|
1152
1120
|
count: rows.length,
|
|
1153
1121
|
backend: 'sql-cte',
|
|
@@ -1155,11 +1123,10 @@ export const agentdbGraphQuery = {
|
|
|
1155
1123
|
};
|
|
1156
1124
|
}
|
|
1157
1125
|
}
|
|
1158
|
-
catch {
|
|
1159
|
-
|
|
1160
|
-
return { success: false, error: 'Relation-filtered k-hop query requires the SQL graph_edges backend', mode, nodeId, relation };
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
return { success: false, error: `Retained SQL graph query failed: ${sanitizeError(error)}`, mode, nodeId };
|
|
1161
1128
|
}
|
|
1162
|
-
return { success: false, error: '
|
|
1129
|
+
return { success: false, error: 'Retained SQL graph_edges backend is unavailable for k-hop query', mode, nodeId };
|
|
1163
1130
|
}
|
|
1164
1131
|
// ── semantic mode ────────────────────────────────────────────────────────
|
|
1165
1132
|
if (mode === 'semantic') {
|