@claude-flow/cli 3.71.4 → 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/helpers.manifest.json +4 -4
- package/.claude/helpers/hook-handler.cjs +23 -10
- 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/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/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 +59 -8
- package/dist/src/mcp-tools/browser-argv.d.ts +9 -0
- package/dist/src/mcp-tools/browser-argv.js +33 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.js +12 -2
- 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 +21 -42
- package/dist/src/mcp-tools/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +8 -0
- package/dist/src/mcp-tools/record-store.js +98 -1
- 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 +5 -3
- package/dist/src/mcp-tools/swarm-tools.js +35 -17
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +9 -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,81 @@
|
|
|
1
|
+
/** Bounded glob matching for caller-provided memory-import exclusions. */
|
|
2
|
+
export const MAX_EXCLUDE_PATTERNS = 64;
|
|
3
|
+
export const MAX_EXCLUDE_PATTERN_LENGTH = 1024;
|
|
4
|
+
export const MAX_EXCLUDE_WILDCARDS = 32;
|
|
5
|
+
function tokenize(pattern) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let wildcards = 0;
|
|
8
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
9
|
+
const char = pattern[i];
|
|
10
|
+
if (char === '*') {
|
|
11
|
+
let end = i + 1;
|
|
12
|
+
while (pattern[end] === '*')
|
|
13
|
+
end++;
|
|
14
|
+
tokens.push({ kind: end - i > 1 ? 'globstar' : 'star' });
|
|
15
|
+
// Preserve the trailing segment-star in odd runs, including its newline behavior.
|
|
16
|
+
if (end - i > 1 && (end - i) % 2 === 1) {
|
|
17
|
+
tokens.push({ kind: 'star' });
|
|
18
|
+
wildcards++;
|
|
19
|
+
}
|
|
20
|
+
i = end - 1;
|
|
21
|
+
wildcards++;
|
|
22
|
+
}
|
|
23
|
+
else if (char === '?') {
|
|
24
|
+
tokens.push({ kind: 'question' });
|
|
25
|
+
wildcards++;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
tokens.push({ kind: 'literal', value: char });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (wildcards > MAX_EXCLUDE_WILDCARDS) {
|
|
32
|
+
throw new Error(`excludeFilePatterns entries may contain at most ${MAX_EXCLUDE_WILDCARDS} wildcard operators`);
|
|
33
|
+
}
|
|
34
|
+
return tokens;
|
|
35
|
+
}
|
|
36
|
+
function matches(tokens, path) {
|
|
37
|
+
// Each path character visits each token once; wildcard placement cannot cause backtracking.
|
|
38
|
+
let previous = new Uint8Array(tokens.length + 1);
|
|
39
|
+
let current = new Uint8Array(tokens.length + 1);
|
|
40
|
+
previous[0] = 1;
|
|
41
|
+
for (let j = 1; j <= tokens.length; j++) {
|
|
42
|
+
const token = tokens[j - 1];
|
|
43
|
+
if (token.kind === 'star' || token.kind === 'globstar')
|
|
44
|
+
previous[j] = previous[j - 1];
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < path.length; i++) {
|
|
47
|
+
current[0] = 0;
|
|
48
|
+
const char = path[i];
|
|
49
|
+
const inSegment = char !== '/' && char !== '\\';
|
|
50
|
+
for (let j = 1; j <= tokens.length; j++) {
|
|
51
|
+
const token = tokens[j - 1];
|
|
52
|
+
if (token.kind === 'star' || token.kind === 'globstar') {
|
|
53
|
+
const canConsume = token.kind === 'globstar'
|
|
54
|
+
? !'\n\r\u2028\u2029'.includes(char)
|
|
55
|
+
: inSegment;
|
|
56
|
+
current[j] = current[j - 1] || (canConsume ? previous[j] : 0);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const canConsume = token.kind === 'literal' ? token.value === char : inSegment;
|
|
60
|
+
current[j] = canConsume ? previous[j - 1] : 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
[previous, current] = [current, previous];
|
|
64
|
+
}
|
|
65
|
+
return previous[tokens.length] === 1;
|
|
66
|
+
}
|
|
67
|
+
export function compileExclusionPatterns(input) {
|
|
68
|
+
if (input === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
if (!Array.isArray(input) || input.length > MAX_EXCLUDE_PATTERNS) {
|
|
71
|
+
throw new Error(`excludeFilePatterns must be an array of at most ${MAX_EXCLUDE_PATTERNS} patterns`);
|
|
72
|
+
}
|
|
73
|
+
return Array.from(input, (pattern) => {
|
|
74
|
+
if (typeof pattern !== 'string' || pattern.length > MAX_EXCLUDE_PATTERN_LENGTH) {
|
|
75
|
+
throw new Error(`excludeFilePatterns entries must be strings of at most ${MAX_EXCLUDE_PATTERN_LENGTH} characters`);
|
|
76
|
+
}
|
|
77
|
+
const tokens = tokenize(pattern);
|
|
78
|
+
return (path) => matches(tokens, path);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=glob-exclusions.js.map
|
|
@@ -8,6 +8,7 @@ import { join } from 'node:path';
|
|
|
8
8
|
import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
9
9
|
import { getProjectCwd } from './types.js';
|
|
10
10
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
11
|
+
import { readPreservedJsonStore } from './record-store.js';
|
|
11
12
|
// Storage paths
|
|
12
13
|
const STORAGE_DIR = '.claude-flow';
|
|
13
14
|
const HIVE_DIR = 'hive-mind';
|
|
@@ -199,17 +200,10 @@ function ensureHiveDir() {
|
|
|
199
200
|
catch { /* not ours / unsupported */ }
|
|
200
201
|
}
|
|
201
202
|
function loadHiveState() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return JSON.parse(data);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
// Return default state on error
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
203
|
+
// #4737: a parse error used to return a fresh hive, and the next save
|
|
204
|
+
// overwrote state.json (including its capability token). Only a missing
|
|
205
|
+
// file is empty; corrupt bytes are copied aside and the error is reported.
|
|
206
|
+
return readPreservedJsonStore(getHivePath(), 'hive-mind', () => ({
|
|
213
207
|
initialized: false,
|
|
214
208
|
topology: 'mesh',
|
|
215
209
|
workers: [],
|
|
@@ -217,7 +211,7 @@ function loadHiveState() {
|
|
|
217
211
|
sharedMemory: {},
|
|
218
212
|
createdAt: new Date().toISOString(),
|
|
219
213
|
updatedAt: new Date().toISOString(),
|
|
220
|
-
};
|
|
214
|
+
}));
|
|
221
215
|
}
|
|
222
216
|
function saveHiveState(state) {
|
|
223
217
|
ensureHiveDir();
|
|
@@ -243,24 +237,50 @@ function saveHiveState(state) {
|
|
|
243
237
|
throw err;
|
|
244
238
|
}
|
|
245
239
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
240
|
+
function getHiveAgentPath() {
|
|
241
|
+
return join(getProjectCwd(), STORAGE_DIR, 'agents.json');
|
|
242
|
+
}
|
|
243
|
+
function loadAgentStore(storePath = getHiveAgentPath()) {
|
|
244
|
+
let text;
|
|
250
245
|
try {
|
|
251
|
-
|
|
252
|
-
return JSON.parse(readAgentStore(storePath, 'utf-8'));
|
|
253
|
-
}
|
|
246
|
+
text = readFileSync(storePath, 'utf-8');
|
|
254
247
|
}
|
|
255
|
-
catch {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (!agentStoreExists(storeDir)) {
|
|
261
|
-
mkdirAgentStore(storeDir, { recursive: true });
|
|
248
|
+
catch (error) {
|
|
249
|
+
// Only an absent file is empty: never replace corrupt/unreadable data.
|
|
250
|
+
if (error.code === 'ENOENT')
|
|
251
|
+
return { agents: {} };
|
|
252
|
+
throw error;
|
|
262
253
|
}
|
|
263
|
-
|
|
254
|
+
const store = JSON.parse(text);
|
|
255
|
+
if (!store || typeof store !== 'object' || Array.isArray(store)
|
|
256
|
+
|| !store.agents || typeof store.agents !== 'object'
|
|
257
|
+
|| Object.values(store.agents).some(agent => !agent || typeof agent !== 'object' || Array.isArray(agent))) {
|
|
258
|
+
throw new Error(`Invalid agent store at ${storePath}; existing data was preserved`);
|
|
259
|
+
}
|
|
260
|
+
return store;
|
|
261
|
+
}
|
|
262
|
+
function saveAgentStore(store, storePath = getHiveAgentPath()) {
|
|
263
|
+
// A new hive store lives directly in STORAGE_DIR; join only writes stores
|
|
264
|
+
// that already contain its agent, so their parent directories already exist.
|
|
265
|
+
mkdirSync(join(getProjectCwd(), STORAGE_DIR), { recursive: true });
|
|
266
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
267
|
+
}
|
|
268
|
+
function findAgent(store, agentId) {
|
|
269
|
+
if (Array.isArray(store.agents))
|
|
270
|
+
return store.agents.find(agent => agent.agentId === agentId);
|
|
271
|
+
return Object.prototype.hasOwnProperty.call(store.agents, agentId) ? store.agents[agentId] : undefined;
|
|
272
|
+
}
|
|
273
|
+
const HIVE_ROLES = new Set(['worker', 'specialist', 'scout']);
|
|
274
|
+
function capacityError(state, additionalWorkers) {
|
|
275
|
+
const maxAgents = state.maxAgents ?? 15;
|
|
276
|
+
if (state.workers.length + additionalWorkers <= maxAgents)
|
|
277
|
+
return null;
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
error: `Hive-mind capacity exceeded: ${state.workers.length}/${maxAgents} workers; cannot add ${additionalWorkers}. Leave the hive or initialize with a larger maxAgents.`,
|
|
281
|
+
totalWorkers: state.workers.length,
|
|
282
|
+
maxAgents,
|
|
283
|
+
};
|
|
264
284
|
}
|
|
265
285
|
export const hiveMindTools = [
|
|
266
286
|
{
|
|
@@ -303,8 +323,19 @@ export const hiveMindTools = [
|
|
|
303
323
|
if (!v.valid)
|
|
304
324
|
return { success: false, error: v.error };
|
|
305
325
|
}
|
|
306
|
-
const
|
|
326
|
+
const requestedCount = input.count === undefined ? 1 : input.count;
|
|
327
|
+
if (typeof requestedCount !== 'number' || !Number.isInteger(requestedCount) || requestedCount < 1) {
|
|
328
|
+
return { success: false, error: 'count must be a positive integer' };
|
|
329
|
+
}
|
|
330
|
+
const count = Math.min(requestedCount, 20); // Preserve the per-call cap.
|
|
331
|
+
// Reject the entire batch before creating agents or mutating the roster.
|
|
332
|
+
const full = capacityError(state, count);
|
|
333
|
+
if (full)
|
|
334
|
+
return full;
|
|
307
335
|
const role = input.role || 'worker';
|
|
336
|
+
if (!HIVE_ROLES.has(role)) {
|
|
337
|
+
return { success: false, error: `role must be one of: ${[...HIVE_ROLES].join(', ')}` };
|
|
338
|
+
}
|
|
308
339
|
const agentType = input.agentType || 'worker';
|
|
309
340
|
const prefix = input.prefix || 'hive-worker';
|
|
310
341
|
const agentStore = loadAgentStore();
|
|
@@ -312,7 +343,7 @@ export const hiveMindTools = [
|
|
|
312
343
|
for (let i = 0; i < count; i++) {
|
|
313
344
|
const agentId = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
314
345
|
// Create agent record (like agent/spawn)
|
|
315
|
-
|
|
346
|
+
const agent = {
|
|
316
347
|
agentId,
|
|
317
348
|
agentType,
|
|
318
349
|
status: 'idle',
|
|
@@ -322,6 +353,10 @@ export const hiveMindTools = [
|
|
|
322
353
|
createdAt: new Date().toISOString(),
|
|
323
354
|
domain: 'hive-mind',
|
|
324
355
|
};
|
|
356
|
+
if (Array.isArray(agentStore.agents))
|
|
357
|
+
agentStore.agents.push(agent);
|
|
358
|
+
else
|
|
359
|
+
agentStore.agents[agentId] = agent;
|
|
325
360
|
// Join to hive-mind (like hive-mind/join)
|
|
326
361
|
if (!state.workers.includes(agentId)) {
|
|
327
362
|
state.workers.push(agentId);
|
|
@@ -521,7 +556,7 @@ export const hiveMindTools = [
|
|
|
521
556
|
term: state.queen.term,
|
|
522
557
|
} : { id: 'N/A', status: 'offline', load: 0, tasksQueued: 0 },
|
|
523
558
|
workers: state.workers.map(w => {
|
|
524
|
-
const agent = agentStore
|
|
559
|
+
const agent = findAgent(agentStore, w);
|
|
525
560
|
return {
|
|
526
561
|
id: w,
|
|
527
562
|
type: agent?.agentType || 'worker',
|
|
@@ -601,14 +636,43 @@ export const hiveMindTools = [
|
|
|
601
636
|
if (tokenError) {
|
|
602
637
|
return { success: false, agentId, error: tokenError };
|
|
603
638
|
}
|
|
604
|
-
|
|
639
|
+
const alreadyJoined = state.workers.includes(agentId);
|
|
640
|
+
// Retrying a registered member does not consume another slot.
|
|
641
|
+
if (!alreadyJoined) {
|
|
642
|
+
const full = capacityError(state, 1);
|
|
643
|
+
if (full)
|
|
644
|
+
return { ...full, agentId };
|
|
645
|
+
}
|
|
646
|
+
// Both registration paths are legitimate (#1916). Update each existing
|
|
647
|
+
// record in place, preserving metadata and map/array representation.
|
|
648
|
+
const paths = [
|
|
649
|
+
join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json'),
|
|
650
|
+
getHiveAgentPath(),
|
|
651
|
+
];
|
|
652
|
+
const records = paths.flatMap(path => {
|
|
653
|
+
const store = loadAgentStore(path);
|
|
654
|
+
const agent = findAgent(store, agentId);
|
|
655
|
+
return agent ? [{ path, store, agent }] : [];
|
|
656
|
+
});
|
|
657
|
+
if (records.length === 0) {
|
|
658
|
+
return { success: false, agentId, error: 'Agent not found. Register it with agent_spawn or hive-mind_spawn first.' };
|
|
659
|
+
}
|
|
660
|
+
const role = input.role || 'worker';
|
|
661
|
+
if (!HIVE_ROLES.has(role)) {
|
|
662
|
+
return { success: false, agentId, error: `role must be one of: ${[...HIVE_ROLES].join(', ')}` };
|
|
663
|
+
}
|
|
664
|
+
for (const { path, store, agent } of records) {
|
|
665
|
+
agent.config = { ...agent.config, role, hiveRole: role };
|
|
666
|
+
saveAgentStore(store, path);
|
|
667
|
+
}
|
|
668
|
+
if (!alreadyJoined) {
|
|
605
669
|
state.workers.push(agentId);
|
|
606
670
|
saveHiveState(state);
|
|
607
671
|
}
|
|
608
672
|
return {
|
|
609
673
|
success: true,
|
|
610
674
|
agentId,
|
|
611
|
-
role
|
|
675
|
+
role,
|
|
612
676
|
totalWorkers: state.workers.length,
|
|
613
677
|
joinedAt: new Date().toISOString(),
|
|
614
678
|
};
|
|
@@ -1073,9 +1137,14 @@ export const hiveMindTools = [
|
|
|
1073
1137
|
}
|
|
1074
1138
|
// Clear workers from agent store
|
|
1075
1139
|
const agentStore = loadAgentStore();
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1140
|
+
if (Array.isArray(agentStore.agents)) {
|
|
1141
|
+
agentStore.agents = agentStore.agents.filter(agent => !state.workers.includes(agent.agentId));
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
for (const workerId of state.workers) {
|
|
1145
|
+
if (Object.prototype.hasOwnProperty.call(agentStore.agents, workerId)) {
|
|
1146
|
+
delete agentStore.agents[workerId];
|
|
1147
|
+
}
|
|
1079
1148
|
}
|
|
1080
1149
|
}
|
|
1081
1150
|
saveAgentStore(agentStore);
|
|
@@ -2288,7 +2288,8 @@ export const hooksTransfer = {
|
|
|
2288
2288
|
},
|
|
2289
2289
|
handler: async (params) => {
|
|
2290
2290
|
const sourcePath = params.sourcePath;
|
|
2291
|
-
|
|
2291
|
+
// #4728: an explicit 0 must not fall through to 0.7 (`0 || 0.7` is 0.7).
|
|
2292
|
+
const minConfidence = params.minConfidence ?? 0.7;
|
|
2292
2293
|
const filter = params.filter;
|
|
2293
2294
|
{
|
|
2294
2295
|
const v = validatePath(sourcePath, 'sourcePath');
|
|
@@ -3474,7 +3475,8 @@ export const hooksPatternStore = {
|
|
|
3474
3475
|
handler: async (params) => {
|
|
3475
3476
|
const pattern = params.pattern;
|
|
3476
3477
|
const type = params.type || 'general';
|
|
3477
|
-
|
|
3478
|
+
// #4728: an explicit 0 must not fall through to 0.8 (`0 || 0.8` is 0.8).
|
|
3479
|
+
const confidence = params.confidence ?? 0.8;
|
|
3478
3480
|
const metadata = params.metadata;
|
|
3479
3481
|
const timestamp = new Date().toISOString();
|
|
3480
3482
|
{
|
|
@@ -3559,7 +3561,8 @@ export const hooksPatternSearch = {
|
|
|
3559
3561
|
handler: async (params) => {
|
|
3560
3562
|
const query = params.query;
|
|
3561
3563
|
const topK = params.topK || 5;
|
|
3562
|
-
|
|
3564
|
+
// #4728: an explicit 0 must not fall through to 0.3 (`0 || 0.3` is 0.3).
|
|
3565
|
+
const minConfidence = params.minConfidence ?? 0.3;
|
|
3563
3566
|
const namespace = params.namespace || 'pattern';
|
|
3564
3567
|
const strategy = params.strategy || 'semantic';
|
|
3565
3568
|
{
|
|
@@ -4865,7 +4868,8 @@ export const hooksWorkerDetect = {
|
|
|
4865
4868
|
handler: async (params) => {
|
|
4866
4869
|
const prompt = params.prompt;
|
|
4867
4870
|
const autoDispatch = params.autoDispatch;
|
|
4868
|
-
|
|
4871
|
+
// #4728: an explicit 0 must not fall through to 0.5 (`0 || 0.5` is 0.5).
|
|
4872
|
+
const minConfidence = params.minConfidence ?? 0.5;
|
|
4869
4873
|
{
|
|
4870
4874
|
const v = validateText(prompt, 'prompt');
|
|
4871
4875
|
if (!v.valid)
|
|
@@ -4968,7 +4972,10 @@ export const hooksModelRoute = {
|
|
|
4968
4972
|
implementation: 'fallback',
|
|
4969
4973
|
};
|
|
4970
4974
|
}
|
|
4971
|
-
const result = await router.route(task
|
|
4975
|
+
const result = await router.route(task, undefined, {
|
|
4976
|
+
preferCost: typeof params.preferCost === 'boolean' ? params.preferCost : undefined,
|
|
4977
|
+
preferSpeed: typeof params.preferSpeed === 'boolean' ? params.preferSpeed : undefined,
|
|
4978
|
+
});
|
|
4972
4979
|
return {
|
|
4973
4980
|
model: result.model,
|
|
4974
4981
|
confidence: result.confidence,
|
|
@@ -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
|
|
@@ -18,6 +18,8 @@ import { createHash } from 'crypto';
|
|
|
18
18
|
import { getProjectCwd } from './types.js';
|
|
19
19
|
import { validateIdentifier } from './validate-input.js';
|
|
20
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';
|
|
21
23
|
// #1604: Align with memory-initializer.ts — single source of truth is .swarm/memory.db
|
|
22
24
|
const MEMORY_DIR = '.swarm';
|
|
23
25
|
const LEGACY_MEMORY_FILE = 'store.json';
|
|
@@ -124,37 +126,6 @@ function sanitizeMemoryKey(key) {
|
|
|
124
126
|
const safe = key.replace(DANGEROUS_KEY_CHARS, '_');
|
|
125
127
|
return safe.length > MAX_KEY_LENGTH ? safe.slice(0, MAX_KEY_LENGTH) : safe;
|
|
126
128
|
}
|
|
127
|
-
// #1937 — minimal glob → RegExp helper for memory_import_claude exclusion
|
|
128
|
-
// patterns. Anchored. Supports the three operators the issue's voice-fidelity
|
|
129
|
-
// workflow needs:
|
|
130
|
-
// `**` — any chars including path separators
|
|
131
|
-
// `*` — any chars except path separators
|
|
132
|
-
// `?` — exactly one char except a path separator
|
|
133
|
-
// Everything else is regex-escaped. Used to match absolute file paths.
|
|
134
|
-
function globToRegex(pattern) {
|
|
135
|
-
// Tokenize so we can replace `**` before `*` without overlap.
|
|
136
|
-
let out = '';
|
|
137
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
138
|
-
const c = pattern[i];
|
|
139
|
-
if (c === '*' && pattern[i + 1] === '*') {
|
|
140
|
-
out += '.*';
|
|
141
|
-
i++;
|
|
142
|
-
}
|
|
143
|
-
else if (c === '*') {
|
|
144
|
-
out += '[^/\\\\]*';
|
|
145
|
-
}
|
|
146
|
-
else if (c === '?') {
|
|
147
|
-
out += '[^/\\\\]';
|
|
148
|
-
}
|
|
149
|
-
else if (/[.+^$|(){}\[\]\\]/.test(c)) {
|
|
150
|
-
out += '\\' + c;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
out += c;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return new RegExp('^' + out + '$');
|
|
157
|
-
}
|
|
158
129
|
// #1883 — resolve the Claude-Code project memory directory for the *current*
|
|
159
130
|
// project. Claude Code hashes the project path differently per host OS, and
|
|
160
131
|
// our previous logic only POSIX-slash-replaced cwd, which breaks for:
|
|
@@ -1128,8 +1099,9 @@ export const memoryTools = [
|
|
|
1128
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.' },
|
|
1129
1100
|
excludeFilePatterns: {
|
|
1130
1101
|
type: 'array',
|
|
1131
|
-
|
|
1132
|
-
|
|
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.',
|
|
1133
1105
|
},
|
|
1134
1106
|
excludeFiles: {
|
|
1135
1107
|
type: 'array',
|
|
@@ -1139,6 +1111,13 @@ export const memoryTools = [
|
|
|
1139
1111
|
},
|
|
1140
1112
|
},
|
|
1141
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
|
+
}
|
|
1142
1121
|
await ensureInitialized();
|
|
1143
1122
|
const { storeEntry } = await getMemoryFunctions();
|
|
1144
1123
|
const ns = input.namespace || 'claude-memories';
|
|
@@ -1151,13 +1130,11 @@ export const memoryTools = [
|
|
|
1151
1130
|
const projectPathOverride = input.projectPath;
|
|
1152
1131
|
const claudeProjectsDir = join(homedir(), '.claude', 'projects');
|
|
1153
1132
|
// #1937 — voice-fidelity / persona-restricted exclusion.
|
|
1154
|
-
const excludeFilePatterns = Array.isArray(input.excludeFilePatterns) ? input.excludeFilePatterns : [];
|
|
1155
1133
|
const excludeFilesList = Array.isArray(input.excludeFiles) ? new Set(input.excludeFiles) : new Set();
|
|
1156
|
-
const excludeRegexes = excludeFilePatterns.map(globToRegex);
|
|
1157
1134
|
const isExcluded = (absPath) => {
|
|
1158
1135
|
if (excludeFilesList.has(absPath))
|
|
1159
1136
|
return true;
|
|
1160
|
-
return
|
|
1137
|
+
return excludeMatchers.some(matches => matches(absPath));
|
|
1161
1138
|
};
|
|
1162
1139
|
// Find memory files
|
|
1163
1140
|
const memoryFiles = [];
|
|
@@ -1776,7 +1753,7 @@ export const memoryTools = [
|
|
|
1776
1753
|
inputSchema: {
|
|
1777
1754
|
type: 'object',
|
|
1778
1755
|
properties: {
|
|
1779
|
-
inputPath: { type: 'string', description: 'Path to the JSON export file' },
|
|
1756
|
+
inputPath: { type: 'string', description: 'Path to the JSON export file (maximum 16 MiB)' },
|
|
1780
1757
|
merge: { type: 'boolean', description: 'Merge into existing entries (upsert) vs. fail on conflict (default true)' },
|
|
1781
1758
|
namespace: { type: 'string', description: 'Override the namespace for all imported entries' },
|
|
1782
1759
|
dbPath: { type: 'string', description: 'Database file to import into (defaults to the MCP memory store)' },
|
|
@@ -1785,8 +1762,6 @@ export const memoryTools = [
|
|
|
1785
1762
|
},
|
|
1786
1763
|
handler: async (input, context) => {
|
|
1787
1764
|
const dbPath = resolveDbPathOption(input.dbPath, context);
|
|
1788
|
-
await ensureInitialized(dbPath);
|
|
1789
|
-
const { storeEntry } = await getMemoryFunctions();
|
|
1790
1765
|
const t0 = Date.now();
|
|
1791
1766
|
// Values are re-embedded on import; count the vectors actually written
|
|
1792
1767
|
// rather than reporting a constant 0 next to entries that show a vector.
|
|
@@ -1802,11 +1777,15 @@ export const memoryTools = [
|
|
|
1802
1777
|
}
|
|
1803
1778
|
let doc;
|
|
1804
1779
|
try {
|
|
1805
|
-
doc = JSON.parse(
|
|
1780
|
+
doc = JSON.parse(readImportFile(inputPath).toString('utf-8'));
|
|
1806
1781
|
}
|
|
1807
|
-
catch {
|
|
1808
|
-
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' };
|
|
1809
1786
|
}
|
|
1787
|
+
await ensureInitialized(dbPath);
|
|
1788
|
+
const { storeEntry } = await getMemoryFunctions();
|
|
1810
1789
|
const entries = Array.isArray(doc.entries) ? doc.entries : [];
|
|
1811
1790
|
const nsOverride = input.namespace ? String(input.namespace) : undefined;
|
|
1812
1791
|
if (nsOverride) {
|
|
@@ -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 = {
|
|
@@ -7,4 +7,12 @@
|
|
|
7
7
|
export declare function nullPrototypeRecords<M extends object>(map: M | undefined | null): M;
|
|
8
8
|
/** Only an absent store is empty. Failed reads must never authorize replacement. */
|
|
9
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;
|
|
10
18
|
//# sourceMappingURL=record-store.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
2
3
|
/**
|
|
3
4
|
* Give a record map a null prototype. Record keys come from MCP tool input; on
|
|
4
5
|
* a plain object `__proto__` or `constructor` resolve to Object.prototype or a
|
|
@@ -35,4 +36,100 @@ export function readRecordStore(path, collection, empty) {
|
|
|
35
36
|
nullPrototypeRecords(records);
|
|
36
37
|
return store;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* JSON object store read for workflow, hive-mind, and swarm (#4737).
|
|
41
|
+
* Same rule as {@link readRecordStore}: only a missing file is empty.
|
|
42
|
+
* A parse failure, a non-object document, or a document `accept` rejects is
|
|
43
|
+
* copied to `${path}.corrupt-<timestamp>` and thrown, so a later save cannot
|
|
44
|
+
* replace the original.
|
|
45
|
+
*/
|
|
46
|
+
export function readPreservedJsonStore(path, label, empty, accept = () => true) {
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(path, 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === 'ENOENT')
|
|
53
|
+
return empty();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
62
|
+
refuseCorruptStore(path, label, why);
|
|
63
|
+
}
|
|
64
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !accept(parsed)) {
|
|
65
|
+
refuseCorruptStore(path, label);
|
|
66
|
+
}
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
function refuseCorruptStore(path, label, detail) {
|
|
70
|
+
let where = 'existing data was preserved';
|
|
71
|
+
try {
|
|
72
|
+
where = `corrupt file backed up to ${backupCorruptStore(path)}; existing data was preserved`;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
76
|
+
where = `backup failed (${why}); existing data was preserved`;
|
|
77
|
+
}
|
|
78
|
+
const suffix = detail ? ` (${detail})` : '';
|
|
79
|
+
throw new Error(`Invalid ${label} store at ${path}${suffix}; ${where}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Copy `path` to `${path}.corrupt-<timestamp>` without removing the original.
|
|
83
|
+
* A backup with identical bytes is reused, so polling a corrupt store does not
|
|
84
|
+
* add a copy on every read.
|
|
85
|
+
*/
|
|
86
|
+
function backupCorruptStore(path) {
|
|
87
|
+
const existing = existingIdenticalBackup(path);
|
|
88
|
+
if (existing)
|
|
89
|
+
return existing;
|
|
90
|
+
const stamp = Date.now();
|
|
91
|
+
let backup = `${path}.corrupt-${stamp}`;
|
|
92
|
+
let n = 0;
|
|
93
|
+
while (existsSync(backup)) {
|
|
94
|
+
n += 1;
|
|
95
|
+
backup = `${path}.corrupt-${stamp}-${n}`;
|
|
96
|
+
}
|
|
97
|
+
copyFileSync(path, backup);
|
|
98
|
+
try {
|
|
99
|
+
chmodSync(backup, statSync(path).mode & 0o777);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* content is already copied; mode is best-effort */
|
|
103
|
+
}
|
|
104
|
+
return backup;
|
|
105
|
+
}
|
|
106
|
+
function existingIdenticalBackup(path) {
|
|
107
|
+
const dir = dirname(path);
|
|
108
|
+
const prefix = `${basename(path)}.corrupt-`;
|
|
109
|
+
let current;
|
|
110
|
+
try {
|
|
111
|
+
current = readFileSync(path);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = readdirSync(dir).filter((name) => name.startsWith(prefix));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const candidate = join(dir, name);
|
|
125
|
+
try {
|
|
126
|
+
if (statSync(candidate).size === current.length && readFileSync(candidate).equals(current))
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* unreadable candidate: keep looking */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
38
135
|
//# sourceMappingURL=record-store.js.map
|