@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.
Files changed (83) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +17 -0
  2. package/.claude/helpers/helpers.manifest.json +6 -6
  3. package/.claude/helpers/hook-handler.cjs +23 -10
  4. package/.claude/helpers/intelligence.cjs +23 -1
  5. package/.claude/helpers/statusline.cjs +29 -14
  6. package/README.md +18 -3
  7. package/bin/cli.js +13 -18
  8. package/bin/mcp-server.js +13 -18
  9. package/catalog-manifest.json +2 -2
  10. package/dist/src/appliance/rvfa-distribution.js +2 -2
  11. package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
  12. package/dist/src/benchmarks/gaia-critic.js +1 -7
  13. package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
  14. package/dist/src/benchmarks/gaia-decomposer.js +1 -8
  15. package/dist/src/commands/agent-wasm.js +2 -2
  16. package/dist/src/commands/analyze.js +1 -1
  17. package/dist/src/commands/daemon.js +1 -1
  18. package/dist/src/commands/doctor.js +3 -2
  19. package/dist/src/commands/guidance.js +9 -9
  20. package/dist/src/commands/hooks.js +13 -8
  21. package/dist/src/commands/init.js +14 -16
  22. package/dist/src/commands/memory.js +4 -3
  23. package/dist/src/commands/neural.js +8 -6
  24. package/dist/src/commands/route.js +1 -1
  25. package/dist/src/commands/ruvector/backup.js +7 -5
  26. package/dist/src/commands/ruvector/benchmark.js +6 -1
  27. package/dist/src/commands/ruvector/import.js +19 -5
  28. package/dist/src/commands/ruvector/init.js +1 -1
  29. package/dist/src/commands/start.js +1 -1
  30. package/dist/src/commands/status.js +1 -1
  31. package/dist/src/commands/task.js +18 -1
  32. package/dist/src/commands/update.js +1 -1
  33. package/dist/src/funnel/local-signals.js +7 -2
  34. package/dist/src/init/helpers-generator.js +16 -1
  35. package/dist/src/mcp-protocol.d.ts +90 -0
  36. package/dist/src/mcp-protocol.js +179 -0
  37. package/dist/src/mcp-server.d.ts +1 -0
  38. package/dist/src/mcp-server.js +20 -22
  39. package/dist/src/mcp-tools/agent-execute-core.js +9 -1
  40. package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
  41. package/dist/src/mcp-tools/agent-tools.js +63 -12
  42. package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
  43. package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
  44. package/dist/src/mcp-tools/browser-argv.d.ts +27 -0
  45. package/dist/src/mcp-tools/browser-argv.js +92 -0
  46. package/dist/src/mcp-tools/browser-session-tools.js +4 -0
  47. package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
  48. package/dist/src/mcp-tools/browser-tools.js +25 -8
  49. package/dist/src/mcp-tools/coordination-tools.js +5 -4
  50. package/dist/src/mcp-tools/embeddings-tools.js +12 -4
  51. package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
  52. package/dist/src/mcp-tools/glob-exclusions.js +81 -0
  53. package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
  54. package/dist/src/mcp-tools/hooks-tools.js +12 -5
  55. package/dist/src/mcp-tools/import-limits.d.ts +4 -0
  56. package/dist/src/mcp-tools/import-limits.js +28 -0
  57. package/dist/src/mcp-tools/memory-tools.js +68 -72
  58. package/dist/src/mcp-tools/neural-tools.js +8 -2
  59. package/dist/src/mcp-tools/path-containment.d.ts +34 -0
  60. package/dist/src/mcp-tools/path-containment.js +133 -0
  61. package/dist/src/mcp-tools/performance-tools.js +7 -2
  62. package/dist/src/mcp-tools/record-store.d.ts +15 -0
  63. package/dist/src/mcp-tools/record-store.js +117 -3
  64. package/dist/src/mcp-tools/security-tools.d.ts +9 -0
  65. package/dist/src/mcp-tools/security-tools.js +39 -3
  66. package/dist/src/mcp-tools/session-tools.js +9 -41
  67. package/dist/src/mcp-tools/swarm-tools.js +36 -14
  68. package/dist/src/mcp-tools/task-tools.js +68 -5
  69. package/dist/src/mcp-tools/terminal-tools.js +16 -2
  70. package/dist/src/mcp-tools/validate-input.d.ts +2 -0
  71. package/dist/src/mcp-tools/validate-input.js +14 -0
  72. package/dist/src/mcp-tools/workflow-tools.js +13 -12
  73. package/dist/src/mcp-tools/x-federation-join.js +55 -14
  74. package/dist/src/memory/intelligence.d.ts +11 -0
  75. package/dist/src/memory/intelligence.js +84 -6
  76. package/dist/src/memory/memory-initializer.d.ts +6 -2
  77. package/dist/src/memory/memory-initializer.js +15 -6
  78. package/dist/src/ruvector/model-router.d.ts +6 -1
  79. package/dist/src/ruvector/model-router.js +16 -4
  80. package/dist/src/services/worker-daemon.js +30 -12
  81. package/dist/src/transfer/storage/gcs.js +3 -2
  82. package/package.json +1 -1
  83. 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
- function resolveDbPathOption(inputDbPath) {
31
- return typeof inputDbPath === 'string' && inputDbPath.trim().length > 0
32
- ? resolve(inputDbPath)
33
- : undefined;
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
- items: { type: 'string' },
1120
- 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). Examples: `**/voice-*.md`, `**/persona-*.md`. Combine with excludeFiles for explicit paths.',
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 excludeRegexes.some(re => re.test(absPath));
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 requestedDbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
1653
- ? resolve(input.dbPath)
1654
- : undefined;
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 = typeof input.dbPath === 'string' && input.dbPath.trim()
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 inputPath = String(input.inputPath ?? '');
1784
- if (!inputPath || !existsSync(inputPath))
1785
- return { error: `File not found: ${inputPath || '(empty)'}` };
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(readFileSync(inputPath, 'utf-8'));
1780
+ doc = JSON.parse(readImportFile(inputPath).toString('utf-8'));
1789
1781
  }
1790
- catch (e) {
1791
- return { error: `Invalid export JSON: ${e.message}` };
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
- return JSON.parse(readFileSync(path, 'utf-8'));
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: {}, patterns: {}, version: '3.0.0' };
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: 'number', description: 'Number of iterations' },
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