@claude-flow/cli 3.71.3 → 3.72.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 +17 -0
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +23 -10
- package/.claude/helpers/intelligence.cjs +23 -1
- package/.claude/helpers/statusline.cjs +29 -14
- package/README.md +18 -3
- package/bin/cli.js +13 -18
- package/bin/mcp-server.js +13 -18
- package/catalog-manifest.json +2 -2
- package/dist/src/appliance/rvfa-distribution.js +2 -2
- package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
- package/dist/src/benchmarks/gaia-critic.js +1 -7
- package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
- package/dist/src/benchmarks/gaia-decomposer.js +1 -8
- package/dist/src/commands/agent-wasm.js +2 -2
- package/dist/src/commands/analyze.js +1 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/commands/doctor.js +3 -2
- package/dist/src/commands/guidance.js +9 -9
- package/dist/src/commands/hooks.js +13 -8
- package/dist/src/commands/init.js +14 -16
- package/dist/src/commands/memory.js +4 -3
- package/dist/src/commands/neural.js +8 -6
- package/dist/src/commands/route.js +1 -1
- package/dist/src/commands/ruvector/backup.js +7 -5
- package/dist/src/commands/ruvector/benchmark.js +6 -1
- package/dist/src/commands/ruvector/import.js +19 -5
- package/dist/src/commands/ruvector/init.js +1 -1
- package/dist/src/commands/start.js +1 -1
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/task.js +18 -1
- package/dist/src/commands/update.js +1 -1
- package/dist/src/funnel/local-signals.js +7 -2
- package/dist/src/init/helpers-generator.js +16 -1
- package/dist/src/mcp-protocol.d.ts +90 -0
- package/dist/src/mcp-protocol.js +179 -0
- package/dist/src/mcp-server.d.ts +1 -0
- package/dist/src/mcp-server.js +20 -22
- package/dist/src/mcp-tools/agent-execute-core.js +9 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
- package/dist/src/mcp-tools/agent-tools.js +63 -12
- package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
- package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
- package/dist/src/mcp-tools/browser-argv.d.ts +27 -0
- package/dist/src/mcp-tools/browser-argv.js +92 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
- package/dist/src/mcp-tools/browser-tools.js +25 -8
- package/dist/src/mcp-tools/coordination-tools.js +5 -4
- package/dist/src/mcp-tools/embeddings-tools.js +12 -4
- package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
- package/dist/src/mcp-tools/glob-exclusions.js +81 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
- package/dist/src/mcp-tools/hooks-tools.js +12 -5
- package/dist/src/mcp-tools/import-limits.d.ts +4 -0
- package/dist/src/mcp-tools/import-limits.js +28 -0
- package/dist/src/mcp-tools/memory-tools.js +68 -72
- package/dist/src/mcp-tools/neural-tools.js +8 -2
- package/dist/src/mcp-tools/path-containment.d.ts +34 -0
- package/dist/src/mcp-tools/path-containment.js +133 -0
- package/dist/src/mcp-tools/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +15 -0
- package/dist/src/mcp-tools/record-store.js +117 -3
- package/dist/src/mcp-tools/security-tools.d.ts +9 -0
- package/dist/src/mcp-tools/security-tools.js +39 -3
- package/dist/src/mcp-tools/session-tools.js +9 -41
- package/dist/src/mcp-tools/swarm-tools.js +36 -14
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +16 -2
- package/dist/src/mcp-tools/validate-input.d.ts +2 -0
- package/dist/src/mcp-tools/validate-input.js +14 -0
- package/dist/src/mcp-tools/workflow-tools.js +13 -12
- package/dist/src/mcp-tools/x-federation-join.js +55 -14
- package/dist/src/memory/intelligence.d.ts +11 -0
- package/dist/src/memory/intelligence.js +84 -6
- package/dist/src/memory/memory-initializer.d.ts +6 -2
- package/dist/src/memory/memory-initializer.js +15 -6
- package/dist/src/ruvector/model-router.d.ts +6 -1
- package/dist/src/ruvector/model-router.js +16 -4
- package/dist/src/services/worker-daemon.js +30 -12
- package/dist/src/transfer/storage/gcs.js +3 -2
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
export const MAX_IMPORT_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
/** Bound allocation and reads even if an import file grows after its size check. */
|
|
4
|
+
export function readImportFile(file) {
|
|
5
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
6
|
+
try {
|
|
7
|
+
const stat = fstatSync(fd);
|
|
8
|
+
if (!stat.isFile())
|
|
9
|
+
throw new Error('Import input must be a regular file');
|
|
10
|
+
if (stat.size > MAX_IMPORT_BYTES)
|
|
11
|
+
throw new Error('Import file exceeds the 16 MiB limit');
|
|
12
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
13
|
+
let total = 0;
|
|
14
|
+
while (total < buffer.length) {
|
|
15
|
+
const count = readSync(fd, buffer, total, buffer.length - total, null);
|
|
16
|
+
if (count === 0)
|
|
17
|
+
break;
|
|
18
|
+
total += count;
|
|
19
|
+
}
|
|
20
|
+
if (total > stat.size)
|
|
21
|
+
throw new Error('Import input changed size while reading');
|
|
22
|
+
return buffer.subarray(0, total);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=import-limits.js.map
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { basename, dirname, join, resolve } from 'path';
|
|
15
|
+
import { containedReadPath, containedWritePath, dbPathError, isLocalCliCall } from './path-containment.js';
|
|
15
16
|
import { execFileSync } from 'node:child_process';
|
|
16
17
|
import { createHash } from 'crypto';
|
|
17
18
|
import { getProjectCwd } from './types.js';
|
|
18
19
|
import { validateIdentifier } from './validate-input.js';
|
|
19
20
|
import { DEFAULT_SEARCH_THRESHOLD, nearMisses } from '../memory/search-defaults.js';
|
|
21
|
+
import { compileExclusionPatterns, MAX_EXCLUDE_PATTERNS, MAX_EXCLUDE_PATTERN_LENGTH } from './glob-exclusions.js';
|
|
22
|
+
import { readImportFile } from './import-limits.js';
|
|
20
23
|
// #1604: Align with memory-initializer.ts — single source of truth is .swarm/memory.db
|
|
21
24
|
const MEMORY_DIR = '.swarm';
|
|
22
25
|
const LEGACY_MEMORY_FILE = 'store.json';
|
|
@@ -27,10 +30,21 @@ const LEGACY_MEMORY_DIR = '.claude-flow/memory';
|
|
|
27
30
|
* deliberately not an implicit `CLAUDE_FLOW_DB_PATH` override because the
|
|
28
31
|
* native/encrypted CLI store and the AgentDB sibling are separate backends.
|
|
29
32
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Resolve an explicit `dbPath`. MCP callers may only name a SQLite database
|
|
35
|
+
* file, so the memory tools cannot be aimed at and overwrite an unrelated file;
|
|
36
|
+
* the local CLI (`--path`) keeps operator-chosen paths as-is.
|
|
37
|
+
*/
|
|
38
|
+
function resolveDbPathOption(inputDbPath, context) {
|
|
39
|
+
if (typeof inputDbPath !== 'string' || inputDbPath.trim().length === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
const dbPath = resolve(inputDbPath);
|
|
42
|
+
if (!isLocalCliCall(context)) {
|
|
43
|
+
const error = dbPathError(dbPath);
|
|
44
|
+
if (error)
|
|
45
|
+
throw new Error(error);
|
|
46
|
+
}
|
|
47
|
+
return dbPath;
|
|
34
48
|
}
|
|
35
49
|
const MIGRATION_MARKER = '.migrated-to-sqlite';
|
|
36
50
|
function getMemoryDir() {
|
|
@@ -112,37 +126,6 @@ function sanitizeMemoryKey(key) {
|
|
|
112
126
|
const safe = key.replace(DANGEROUS_KEY_CHARS, '_');
|
|
113
127
|
return safe.length > MAX_KEY_LENGTH ? safe.slice(0, MAX_KEY_LENGTH) : safe;
|
|
114
128
|
}
|
|
115
|
-
// #1937 — minimal glob → RegExp helper for memory_import_claude exclusion
|
|
116
|
-
// patterns. Anchored. Supports the three operators the issue's voice-fidelity
|
|
117
|
-
// workflow needs:
|
|
118
|
-
// `**` — any chars including path separators
|
|
119
|
-
// `*` — any chars except path separators
|
|
120
|
-
// `?` — exactly one char except a path separator
|
|
121
|
-
// Everything else is regex-escaped. Used to match absolute file paths.
|
|
122
|
-
function globToRegex(pattern) {
|
|
123
|
-
// Tokenize so we can replace `**` before `*` without overlap.
|
|
124
|
-
let out = '';
|
|
125
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
126
|
-
const c = pattern[i];
|
|
127
|
-
if (c === '*' && pattern[i + 1] === '*') {
|
|
128
|
-
out += '.*';
|
|
129
|
-
i++;
|
|
130
|
-
}
|
|
131
|
-
else if (c === '*') {
|
|
132
|
-
out += '[^/\\\\]*';
|
|
133
|
-
}
|
|
134
|
-
else if (c === '?') {
|
|
135
|
-
out += '[^/\\\\]';
|
|
136
|
-
}
|
|
137
|
-
else if (/[.+^$|(){}\[\]\\]/.test(c)) {
|
|
138
|
-
out += '\\' + c;
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
out += c;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return new RegExp('^' + out + '$');
|
|
145
|
-
}
|
|
146
129
|
// #1883 — resolve the Claude-Code project memory directory for the *current*
|
|
147
130
|
// project. Claude Code hashes the project path differently per host OS, and
|
|
148
131
|
// our previous logic only POSIX-slash-replaced cwd, which breaks for:
|
|
@@ -462,12 +445,12 @@ export const memoryTools = [
|
|
|
462
445
|
},
|
|
463
446
|
required: ['key', 'value'],
|
|
464
447
|
},
|
|
465
|
-
handler: async (input) => {
|
|
448
|
+
handler: async (input, context) => {
|
|
466
449
|
const missingKey = missingRequiredString(input, 'key', 'memory_store');
|
|
467
450
|
if (missingKey) {
|
|
468
451
|
return { success: false, key: input.key, stored: false, hasEmbedding: false, ...missingKey };
|
|
469
452
|
}
|
|
470
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
453
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
471
454
|
if (!dbPath)
|
|
472
455
|
await ensureInitialized();
|
|
473
456
|
const { storeEntry } = await getMemoryFunctions();
|
|
@@ -549,12 +532,12 @@ export const memoryTools = [
|
|
|
549
532
|
},
|
|
550
533
|
required: ['key'],
|
|
551
534
|
},
|
|
552
|
-
handler: async (input) => {
|
|
535
|
+
handler: async (input, context) => {
|
|
553
536
|
const missingKey = missingRequiredString(input, 'key', 'memory_retrieve');
|
|
554
537
|
if (missingKey) {
|
|
555
538
|
return { key: input.key, namespace: input.namespace, value: null, found: false, ...missingKey };
|
|
556
539
|
}
|
|
557
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
540
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
558
541
|
if (!dbPath)
|
|
559
542
|
await ensureInitialized();
|
|
560
543
|
const { getEntry } = await getMemoryFunctions();
|
|
@@ -624,12 +607,12 @@ export const memoryTools = [
|
|
|
624
607
|
},
|
|
625
608
|
required: ['query'],
|
|
626
609
|
},
|
|
627
|
-
handler: async (input) => {
|
|
610
|
+
handler: async (input, context) => {
|
|
628
611
|
const missingQuery = missingRequiredString(input, 'query', 'memory_search');
|
|
629
612
|
if (missingQuery) {
|
|
630
613
|
return { query: input.query, results: [], total: 0, ...missingQuery };
|
|
631
614
|
}
|
|
632
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
615
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
633
616
|
if (!dbPath)
|
|
634
617
|
await ensureInitialized();
|
|
635
618
|
const { searchEntries } = await getMemoryFunctions();
|
|
@@ -810,12 +793,12 @@ export const memoryTools = [
|
|
|
810
793
|
},
|
|
811
794
|
required: ['key'],
|
|
812
795
|
},
|
|
813
|
-
handler: async (input) => {
|
|
796
|
+
handler: async (input, context) => {
|
|
814
797
|
const missingKey = missingRequiredString(input, 'key', 'memory_delete');
|
|
815
798
|
if (missingKey) {
|
|
816
799
|
return { success: false, key: input.key, namespace: input.namespace, deleted: false, ...missingKey };
|
|
817
800
|
}
|
|
818
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
801
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
819
802
|
if (!dbPath)
|
|
820
803
|
await ensureInitialized();
|
|
821
804
|
const { deleteEntry } = await getMemoryFunctions();
|
|
@@ -862,8 +845,8 @@ export const memoryTools = [
|
|
|
862
845
|
dbPath: { type: 'string', description: 'Explicit SQLite store to read/write; omit to use the MCP default (the native/AgentDB sibling).' },
|
|
863
846
|
},
|
|
864
847
|
},
|
|
865
|
-
handler: async (input) => {
|
|
866
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
848
|
+
handler: async (input, context) => {
|
|
849
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
867
850
|
if (!dbPath)
|
|
868
851
|
await ensureInitialized();
|
|
869
852
|
const { listEntries } = await getMemoryFunctions();
|
|
@@ -939,11 +922,11 @@ export const memoryTools = [
|
|
|
939
922
|
dbPath: { type: 'string', description: 'Explicit SQLite store to read/write; omit to use the MCP default (the native/AgentDB sibling).' },
|
|
940
923
|
},
|
|
941
924
|
},
|
|
942
|
-
handler: async (input) => {
|
|
925
|
+
handler: async (input, context) => {
|
|
943
926
|
if (input.dbPath !== undefined && (typeof input.dbPath !== 'string' || !input.dbPath.trim())) {
|
|
944
927
|
return memoryStatsUnavailable('dbPath must be a non-empty string');
|
|
945
928
|
}
|
|
946
|
-
const dbPath = resolveDbPathOption(input.dbPath);
|
|
929
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
947
930
|
// An explicit read must not initialize or migrate the unrelated default store.
|
|
948
931
|
if (!dbPath)
|
|
949
932
|
await ensureInitialized();
|
|
@@ -1116,8 +1099,9 @@ export const memoryTools = [
|
|
|
1116
1099
|
projectPath: { type: 'string', description: '#1883 — explicit project path to hash, used when cwd does not match Claude Code\'s view (e.g. WSL bridge to Windows host). Pass the canonical project root as Claude Code sees it.' },
|
|
1117
1100
|
excludeFilePatterns: {
|
|
1118
1101
|
type: 'array',
|
|
1119
|
-
|
|
1120
|
-
|
|
1102
|
+
maxItems: MAX_EXCLUDE_PATTERNS,
|
|
1103
|
+
items: { type: 'string', maxLength: MAX_EXCLUDE_PATTERN_LENGTH },
|
|
1104
|
+
description: '#1937 — glob patterns matched against the absolute file path. Files matching ANY pattern are skipped. Supports `*` (any chars within a path segment), `**` (any chars including separators), and `?` (single char). At most 64 patterns, 1024 characters and 32 wildcard operators per pattern. Examples: `**/voice-*.md`, `**/persona-*.md`. Combine with excludeFiles for explicit paths.',
|
|
1121
1105
|
},
|
|
1122
1106
|
excludeFiles: {
|
|
1123
1107
|
type: 'array',
|
|
@@ -1127,6 +1111,13 @@ export const memoryTools = [
|
|
|
1127
1111
|
},
|
|
1128
1112
|
},
|
|
1129
1113
|
handler: async (input) => {
|
|
1114
|
+
let excludeMatchers;
|
|
1115
|
+
try {
|
|
1116
|
+
excludeMatchers = compileExclusionPatterns(input.excludeFilePatterns);
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
return { success: false, imported: 0, error: error.message };
|
|
1120
|
+
}
|
|
1130
1121
|
await ensureInitialized();
|
|
1131
1122
|
const { storeEntry } = await getMemoryFunctions();
|
|
1132
1123
|
const ns = input.namespace || 'claude-memories';
|
|
@@ -1139,13 +1130,11 @@ export const memoryTools = [
|
|
|
1139
1130
|
const projectPathOverride = input.projectPath;
|
|
1140
1131
|
const claudeProjectsDir = join(homedir(), '.claude', 'projects');
|
|
1141
1132
|
// #1937 — voice-fidelity / persona-restricted exclusion.
|
|
1142
|
-
const excludeFilePatterns = Array.isArray(input.excludeFilePatterns) ? input.excludeFilePatterns : [];
|
|
1143
1133
|
const excludeFilesList = Array.isArray(input.excludeFiles) ? new Set(input.excludeFiles) : new Set();
|
|
1144
|
-
const excludeRegexes = excludeFilePatterns.map(globToRegex);
|
|
1145
1134
|
const isExcluded = (absPath) => {
|
|
1146
1135
|
if (excludeFilesList.has(absPath))
|
|
1147
1136
|
return true;
|
|
1148
|
-
return
|
|
1137
|
+
return excludeMatchers.some(matches => matches(absPath));
|
|
1149
1138
|
};
|
|
1150
1139
|
// Find memory files
|
|
1151
1140
|
const memoryFiles = [];
|
|
@@ -1648,18 +1637,19 @@ export const memoryTools = [
|
|
|
1648
1637
|
},
|
|
1649
1638
|
required: ['outputPath'],
|
|
1650
1639
|
},
|
|
1651
|
-
handler: async (input) => {
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
:
|
|
1640
|
+
handler: async (input, context) => {
|
|
1641
|
+
const requestedOutput = String(input.outputPath ?? '');
|
|
1642
|
+
if (!requestedOutput)
|
|
1643
|
+
return { error: 'outputPath is required' };
|
|
1644
|
+
const outputPath = isLocalCliCall(context) ? resolve(requestedOutput) : containedWritePath(requestedOutput, ['.json']);
|
|
1645
|
+
if (!outputPath)
|
|
1646
|
+
return { error: 'outputPath must be a .json file inside the project directory (not a symlink, not in .git)' };
|
|
1647
|
+
const requestedDbPath = resolveDbPathOption(input.dbPath, context);
|
|
1655
1648
|
// An explicit file selection is a single-store read. Do not initialize or
|
|
1656
1649
|
// migrate the unrelated default store just to export a snapshot.
|
|
1657
1650
|
if (!requestedDbPath)
|
|
1658
1651
|
await ensureInitialized();
|
|
1659
1652
|
const { listEntries } = await getMemoryFunctions();
|
|
1660
|
-
const outputPath = String(input.outputPath ?? '');
|
|
1661
|
-
if (!outputPath)
|
|
1662
|
-
return { error: 'outputPath is required' };
|
|
1663
1653
|
const namespace = input.namespace ? String(input.namespace) : undefined;
|
|
1664
1654
|
if (namespace) {
|
|
1665
1655
|
const v = validateIdentifier(namespace, 'namespace');
|
|
@@ -1763,33 +1753,39 @@ export const memoryTools = [
|
|
|
1763
1753
|
inputSchema: {
|
|
1764
1754
|
type: 'object',
|
|
1765
1755
|
properties: {
|
|
1766
|
-
inputPath: { type: 'string', description: 'Path to the JSON export file' },
|
|
1756
|
+
inputPath: { type: 'string', description: 'Path to the JSON export file (maximum 16 MiB)' },
|
|
1767
1757
|
merge: { type: 'boolean', description: 'Merge into existing entries (upsert) vs. fail on conflict (default true)' },
|
|
1768
1758
|
namespace: { type: 'string', description: 'Override the namespace for all imported entries' },
|
|
1769
1759
|
dbPath: { type: 'string', description: 'Database file to import into (defaults to the MCP memory store)' },
|
|
1770
1760
|
},
|
|
1771
1761
|
required: ['inputPath'],
|
|
1772
1762
|
},
|
|
1773
|
-
handler: async (input) => {
|
|
1774
|
-
const dbPath =
|
|
1775
|
-
? resolve(input.dbPath)
|
|
1776
|
-
: undefined;
|
|
1777
|
-
await ensureInitialized(dbPath);
|
|
1778
|
-
const { storeEntry } = await getMemoryFunctions();
|
|
1763
|
+
handler: async (input, context) => {
|
|
1764
|
+
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
1779
1765
|
const t0 = Date.now();
|
|
1780
1766
|
// Values are re-embedded on import; count the vectors actually written
|
|
1781
1767
|
// rather than reporting a constant 0 next to entries that show a vector.
|
|
1782
1768
|
let vectors = 0;
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1769
|
+
const requestedInput = String(input.inputPath ?? '');
|
|
1770
|
+
const inputPath = !requestedInput ? null
|
|
1771
|
+
: isLocalCliCall(context) ? (existsSync(requestedInput) ? resolve(requestedInput) : null)
|
|
1772
|
+
: containedReadPath(requestedInput);
|
|
1773
|
+
if (!inputPath) {
|
|
1774
|
+
return { error: isLocalCliCall(context) || !requestedInput
|
|
1775
|
+
? `File not found: ${requestedInput || '(empty)'}`
|
|
1776
|
+
: 'inputPath must be an existing regular file inside the project directory (not a symlink)' };
|
|
1777
|
+
}
|
|
1786
1778
|
let doc;
|
|
1787
1779
|
try {
|
|
1788
|
-
doc = JSON.parse(
|
|
1780
|
+
doc = JSON.parse(readImportFile(inputPath).toString('utf-8'));
|
|
1789
1781
|
}
|
|
1790
|
-
catch (
|
|
1791
|
-
return { error:
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
return { error: error instanceof SyntaxError
|
|
1784
|
+
? 'inputPath is not a valid memory export (JSON parse failed)'
|
|
1785
|
+
: error instanceof Error ? error.message : 'Could not read memory import' };
|
|
1792
1786
|
}
|
|
1787
|
+
await ensureInitialized(dbPath);
|
|
1788
|
+
const { storeEntry } = await getMemoryFunctions();
|
|
1793
1789
|
const entries = Array.isArray(doc.entries) ? doc.entries : [];
|
|
1794
1790
|
const nsOverride = input.namespace ? String(input.namespace) : undefined;
|
|
1795
1791
|
if (nsOverride) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Note: For production neural features, use @claude-flow/neural module
|
|
13
13
|
*/
|
|
14
14
|
import { getProjectCwd } from './types.js';
|
|
15
|
+
import { nullPrototypeRecords } from './record-store.js';
|
|
15
16
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
18
|
import { join } from 'node:path';
|
|
@@ -223,13 +224,18 @@ function loadNeuralStore() {
|
|
|
223
224
|
try {
|
|
224
225
|
const path = getNeuralPath();
|
|
225
226
|
if (existsSync(path)) {
|
|
226
|
-
|
|
227
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
228
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
229
|
+
parsed.models = nullPrototypeRecords(parsed.models);
|
|
230
|
+
parsed.patterns = nullPrototypeRecords(parsed.patterns);
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
catch {
|
|
230
236
|
// Return empty store
|
|
231
237
|
}
|
|
232
|
-
return { models:
|
|
238
|
+
return { models: nullPrototypeRecords(undefined), patterns: nullPrototypeRecords(undefined), version: '3.0.0' };
|
|
233
239
|
}
|
|
234
240
|
/**
|
|
235
241
|
* ADR-176 flywheel: expose the stored patterns (id/name/content) so the
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path checks for MCP tools that read or write a caller-named file.
|
|
3
|
+
*
|
|
4
|
+
* Paths in tool input come from whoever drives the MCP server, including a
|
|
5
|
+
* model following injected instructions. A tool that writes or reads such a
|
|
6
|
+
* path must keep it inside the project directory, and must not follow a
|
|
7
|
+
* symlink out of it.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a caller-named output file inside the project. The real parent
|
|
11
|
+
* directory must be inside the real project root and outside `.git`, and an
|
|
12
|
+
* existing target must be a regular, non-executable file — not a symlink.
|
|
13
|
+
* `extensions`, when given, lists the allowed lower-case file extensions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function containedWritePath(p: string, extensions?: readonly string[]): string | null;
|
|
16
|
+
/** Resolve a caller-named input file inside the project: a regular file, not a symlink. */
|
|
17
|
+
export declare function containedReadPath(p: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Check a caller-named memory database path. It may live outside the project
|
|
20
|
+
* (the CLI's `--path` flag passes one), so this does not contain it; instead
|
|
21
|
+
* it refuses anything that is not plainly a SQLite database (plaintext, or
|
|
22
|
+
* encrypted at rest per ADR-096), so the memory tools cannot be pointed at and
|
|
23
|
+
* overwrite an unrelated file.
|
|
24
|
+
*/
|
|
25
|
+
export declare function dbPathError(p: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Marks a tool call made by the local CLI on the operator's behalf (for
|
|
28
|
+
* example `ruflo memory export --output ~/backup.json`). MCP requests arrive
|
|
29
|
+
* as JSON and cannot carry a symbol key, so this cannot be forged remotely.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LOCAL_CLI_CALL: unique symbol;
|
|
32
|
+
export declare function localCliContext(): Record<string, unknown>;
|
|
33
|
+
export declare function isLocalCliCall(context: unknown): boolean;
|
|
34
|
+
//# sourceMappingURL=path-containment.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path checks for MCP tools that read or write a caller-named file.
|
|
3
|
+
*
|
|
4
|
+
* Paths in tool input come from whoever drives the MCP server, including a
|
|
5
|
+
* model following injected instructions. A tool that writes or reads such a
|
|
6
|
+
* path must keep it inside the project directory, and must not follow a
|
|
7
|
+
* symlink out of it.
|
|
8
|
+
*/
|
|
9
|
+
import { closeSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
import { isNonProjectDir } from '@claude-flow/cli-core/mcp-tools/types';
|
|
12
|
+
import { getProjectCwd } from './types.js';
|
|
13
|
+
import { existingFileIsEncrypted } from '../fs-secure.js';
|
|
14
|
+
function lstatOrNull(p) {
|
|
15
|
+
try {
|
|
16
|
+
return lstatSync(p);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function projectRoot() {
|
|
23
|
+
const root = realpathSync(getProjectCwd());
|
|
24
|
+
// A server started in a home or filesystem root has no project to contain
|
|
25
|
+
// writes to; "inside the root" would mean the whole home directory.
|
|
26
|
+
return isNonProjectDir(root) ? null : root;
|
|
27
|
+
}
|
|
28
|
+
function insideRoot(root, target) {
|
|
29
|
+
const rel = relative(root, target);
|
|
30
|
+
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
31
|
+
return false;
|
|
32
|
+
return !rel.split(sep).includes('.git');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a caller-named output file inside the project. The real parent
|
|
36
|
+
* directory must be inside the real project root and outside `.git`, and an
|
|
37
|
+
* existing target must be a regular, non-executable file — not a symlink.
|
|
38
|
+
* `extensions`, when given, lists the allowed lower-case file extensions.
|
|
39
|
+
*/
|
|
40
|
+
export function containedWritePath(p, extensions) {
|
|
41
|
+
if (!p || p.includes('\0'))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const root = projectRoot();
|
|
45
|
+
if (!root)
|
|
46
|
+
return null;
|
|
47
|
+
const abs = resolve(root, p);
|
|
48
|
+
if (extensions && !extensions.includes(extname(abs).toLowerCase()))
|
|
49
|
+
return null;
|
|
50
|
+
const target = join(realpathSync(dirname(abs)), basename(abs));
|
|
51
|
+
if (!insideRoot(root, target))
|
|
52
|
+
return null;
|
|
53
|
+
const existing = lstatOrNull(target);
|
|
54
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile() || (existing.mode & 0o111) !== 0))
|
|
55
|
+
return null;
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Resolve a caller-named input file inside the project: a regular file, not a symlink. */
|
|
63
|
+
export function containedReadPath(p) {
|
|
64
|
+
if (!p || p.includes('\0'))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const root = projectRoot();
|
|
68
|
+
if (!root)
|
|
69
|
+
return null;
|
|
70
|
+
const abs = resolve(root, p);
|
|
71
|
+
const existing = lstatOrNull(abs);
|
|
72
|
+
if (!existing || existing.isSymbolicLink() || !existing.isFile())
|
|
73
|
+
return null;
|
|
74
|
+
const target = realpathSync(abs);
|
|
75
|
+
return insideRoot(root, target) ? target : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3'];
|
|
82
|
+
const SQLITE_HEADER = 'SQLite format 3\0';
|
|
83
|
+
/**
|
|
84
|
+
* Check a caller-named memory database path. It may live outside the project
|
|
85
|
+
* (the CLI's `--path` flag passes one), so this does not contain it; instead
|
|
86
|
+
* it refuses anything that is not plainly a SQLite database (plaintext, or
|
|
87
|
+
* encrypted at rest per ADR-096), so the memory tools cannot be pointed at and
|
|
88
|
+
* overwrite an unrelated file.
|
|
89
|
+
*/
|
|
90
|
+
export function dbPathError(p) {
|
|
91
|
+
if (p.includes('\0'))
|
|
92
|
+
return 'dbPath contains a null byte';
|
|
93
|
+
if (!DB_EXTENSIONS.includes(extname(p).toLowerCase())) {
|
|
94
|
+
return `dbPath must end in ${DB_EXTENSIONS.join(', ')}`;
|
|
95
|
+
}
|
|
96
|
+
const existing = lstatOrNull(p);
|
|
97
|
+
if (!existing)
|
|
98
|
+
return null;
|
|
99
|
+
if (existing.isSymbolicLink())
|
|
100
|
+
return 'dbPath must not be a symlink';
|
|
101
|
+
if (!existing.isFile())
|
|
102
|
+
return 'dbPath must be a regular file';
|
|
103
|
+
if (existing.size === 0 || existingFileIsEncrypted(p) === true)
|
|
104
|
+
return null;
|
|
105
|
+
const header = Buffer.alloc(SQLITE_HEADER.length);
|
|
106
|
+
let fd;
|
|
107
|
+
try {
|
|
108
|
+
fd = openSync(p, 'r');
|
|
109
|
+
readSync(fd, header, 0, header.length, 0);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return 'dbPath could not be read';
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (fd !== undefined)
|
|
116
|
+
closeSync(fd);
|
|
117
|
+
}
|
|
118
|
+
return header.toString('latin1') === SQLITE_HEADER ? null : 'dbPath is not a SQLite database';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Marks a tool call made by the local CLI on the operator's behalf (for
|
|
122
|
+
* example `ruflo memory export --output ~/backup.json`). MCP requests arrive
|
|
123
|
+
* as JSON and cannot carry a symbol key, so this cannot be forged remotely.
|
|
124
|
+
*/
|
|
125
|
+
export const LOCAL_CLI_CALL = Symbol('ruflo.localCliCall');
|
|
126
|
+
export function localCliContext() {
|
|
127
|
+
return { [LOCAL_CLI_CALL]: true };
|
|
128
|
+
}
|
|
129
|
+
export function isLocalCliCall(context) {
|
|
130
|
+
return !!context && typeof context === 'object'
|
|
131
|
+
&& context[LOCAL_CLI_CALL] === true;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=path-containment.js.map
|
|
@@ -21,6 +21,7 @@ const STORAGE_DIR = '.claude-flow';
|
|
|
21
21
|
const PERF_DIR = 'performance';
|
|
22
22
|
const METRICS_FILE = 'metrics.json';
|
|
23
23
|
const BENCHMARKS_FILE = 'benchmarks.json';
|
|
24
|
+
const MAX_BENCHMARK_ITERATIONS = 10_000;
|
|
24
25
|
function getPerfDir() {
|
|
25
26
|
return join(getProjectCwd(), STORAGE_DIR, PERF_DIR);
|
|
26
27
|
}
|
|
@@ -246,14 +247,18 @@ export const performanceTools = [
|
|
|
246
247
|
type: 'object',
|
|
247
248
|
properties: {
|
|
248
249
|
suite: { type: 'string', enum: ['all', 'memory', 'neural', 'swarm', 'io'], description: 'Benchmark suite' },
|
|
249
|
-
iterations: { type: '
|
|
250
|
+
iterations: { type: 'integer', minimum: 1, maximum: MAX_BENCHMARK_ITERATIONS, description: 'Number of iterations (maximum 10000)' },
|
|
250
251
|
warmup: { type: 'boolean', description: 'Include warmup phase' },
|
|
251
252
|
},
|
|
252
253
|
},
|
|
253
254
|
handler: async (input) => {
|
|
255
|
+
const requestedIterations = input.iterations === undefined ? 100 : input.iterations;
|
|
256
|
+
if (typeof requestedIterations !== 'number' || !Number.isSafeInteger(requestedIterations) || requestedIterations < 1) {
|
|
257
|
+
return { success: false, error: 'iterations must be a positive safe integer' };
|
|
258
|
+
}
|
|
259
|
+
const iterations = Math.min(requestedIterations, MAX_BENCHMARK_ITERATIONS);
|
|
254
260
|
const store = loadPerfStore();
|
|
255
261
|
const suite = input.suite || 'all';
|
|
256
|
-
const iterations = input.iterations || 100;
|
|
257
262
|
const warmup = input.warmup !== false;
|
|
258
263
|
// Synthetic data benchmarks — measures actual CPU/memory throughput
|
|
259
264
|
const benchmarkFunctions = {
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Give a record map a null prototype. Record keys come from MCP tool input; on
|
|
3
|
+
* a plain object `__proto__` or `constructor` resolve to Object.prototype or a
|
|
4
|
+
* built-in, which a handler would then write into. A null-prototype map only
|
|
5
|
+
* ever returns its own records.
|
|
6
|
+
*/
|
|
7
|
+
export declare function nullPrototypeRecords<M extends object>(map: M | undefined | null): M;
|
|
1
8
|
/** Only an absent store is empty. Failed reads must never authorize replacement. */
|
|
2
9
|
export declare function readRecordStore<T>(path: string, collection: string, empty: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* JSON object store read for workflow, hive-mind, and swarm (#4737).
|
|
12
|
+
* Same rule as {@link readRecordStore}: only a missing file is empty.
|
|
13
|
+
* A parse failure, a non-object document, or a document `accept` rejects is
|
|
14
|
+
* copied to `${path}.corrupt-<timestamp>` and thrown, so a later save cannot
|
|
15
|
+
* replace the original.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readPreservedJsonStore<T extends object>(path: string, label: string, empty: () => T, accept?: (store: T) => boolean): T;
|
|
3
18
|
//# sourceMappingURL=record-store.d.ts.map
|