claude-flow 3.5.24 → 3.5.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.5.24",
3
+ "version": "3.5.25",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -61,6 +61,8 @@ const commandLoaders = {
61
61
  guidance: () => import('./guidance.js'),
62
62
  // RVFA Appliance Management
63
63
  appliance: () => import('./appliance.js'),
64
+ 'appliance-advanced': () => import('./appliance-advanced.js'),
65
+ 'transfer-store': () => import('./transfer-store.js'),
64
66
  };
65
67
  // Cache for loaded commands
66
68
  const loadedCommands = new Map();
@@ -10,10 +10,10 @@ const browserSessions = new Map();
10
10
  * Execute agent-browser CLI command
11
11
  */
12
12
  async function execBrowserCommand(args, session = 'default') {
13
- const { execSync } = await import('child_process');
13
+ const { execFileSync } = await import('child_process');
14
14
  try {
15
15
  const fullArgs = ['--session', session, '--json', ...args];
16
- const result = execSync(`agent-browser ${fullArgs.join(' ')}`, {
16
+ const result = execFileSync('agent-browser', fullArgs, {
17
17
  encoding: 'utf-8',
18
18
  timeout: 30000,
19
19
  });
@@ -30,7 +30,7 @@ function getConfigPath() {
30
30
  function ensureConfigDir() {
31
31
  const dir = getConfigDir();
32
32
  if (!existsSync(dir)) {
33
- mkdirSync(dir, { recursive: true });
33
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
34
34
  }
35
35
  }
36
36
  function loadConfigStore() {
@@ -80,7 +80,16 @@ function filterDangerousKeys(obj) {
80
80
  return filtered;
81
81
  }
82
82
  function setNestedValue(obj, key, value) {
83
+ const MAX_NESTING_DEPTH = 10;
83
84
  const parts = key.split('.');
85
+ if (parts.length > MAX_NESTING_DEPTH) {
86
+ throw new Error(`Key exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`);
87
+ }
88
+ for (const part of parts) {
89
+ if (DANGEROUS_KEYS.has(part)) {
90
+ throw new Error(`Dangerous key segment rejected: ${part}`);
91
+ }
92
+ }
84
93
  let current = obj;
85
94
  for (let i = 0; i < parts.length - 1; i++) {
86
95
  const part = parts[i];
@@ -213,6 +213,7 @@ export const memoryTools = [
213
213
  const { getEntry } = await getMemoryFunctions();
214
214
  const key = input.key;
215
215
  const namespace = input.namespace || 'default';
216
+ validateMemoryInput(key);
216
217
  try {
217
218
  const result = await getEntry({ key, namespace });
218
219
  if (result.found && result.entry) {
@@ -337,6 +338,7 @@ export const memoryTools = [
337
338
  const { deleteEntry } = await getMemoryFunctions();
338
339
  const key = input.key;
339
340
  const namespace = input.namespace || 'default';
341
+ validateMemoryInput(key);
340
342
  try {
341
343
  const result = await deleteEntry({ key, namespace });
342
344
  return {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Swarm MCP Tools for CLI
3
3
  *
4
- * Tool definitions for swarm coordination.
4
+ * Tool definitions for swarm coordination with file-based state persistence.
5
+ * Replaces previous stub implementations with real state tracking.
5
6
  */
6
7
  import type { MCPTool } from './types.js';
7
8
  export declare const swarmTools: MCPTool[];
@@ -1,82 +1,212 @@
1
1
  /**
2
2
  * Swarm MCP Tools for CLI
3
3
  *
4
- * Tool definitions for swarm coordination.
4
+ * Tool definitions for swarm coordination with file-based state persistence.
5
+ * Replaces previous stub implementations with real state tracking.
5
6
  */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ // Swarm state persistence
10
+ const SWARM_DIR = '.claude-flow/swarm';
11
+ const SWARM_STATE_FILE = 'swarm-state.json';
12
+ function getSwarmDir() {
13
+ return join(process.cwd(), SWARM_DIR);
14
+ }
15
+ function getSwarmStatePath() {
16
+ return join(getSwarmDir(), SWARM_STATE_FILE);
17
+ }
18
+ function ensureSwarmDir() {
19
+ const dir = getSwarmDir();
20
+ if (!existsSync(dir)) {
21
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
22
+ }
23
+ }
24
+ function loadSwarmStore() {
25
+ try {
26
+ const path = getSwarmStatePath();
27
+ if (existsSync(path)) {
28
+ return JSON.parse(readFileSync(path, 'utf-8'));
29
+ }
30
+ }
31
+ catch { /* return default */ }
32
+ return { swarms: {}, version: '3.0.0' };
33
+ }
34
+ function saveSwarmStore(store) {
35
+ ensureSwarmDir();
36
+ writeFileSync(getSwarmStatePath(), JSON.stringify(store, null, 2), 'utf-8');
37
+ }
38
+ // Input validation
39
+ const VALID_TOPOLOGIES = new Set([
40
+ 'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive',
41
+ ]);
6
42
  export const swarmTools = [
7
43
  {
8
44
  name: 'swarm_init',
9
- description: 'Initialize a swarm',
45
+ description: 'Initialize a swarm with persistent state tracking',
10
46
  category: 'swarm',
11
47
  inputSchema: {
12
48
  type: 'object',
13
49
  properties: {
14
- topology: { type: 'string', description: 'Swarm topology type' },
15
- maxAgents: { type: 'number', description: 'Maximum number of agents' },
16
- config: { type: 'object', description: 'Swarm configuration' },
50
+ topology: { type: 'string', description: 'Swarm topology type (hierarchical, mesh, hierarchical-mesh, ring, star, hybrid, adaptive)' },
51
+ maxAgents: { type: 'number', description: 'Maximum number of agents (1-50)' },
52
+ strategy: { type: 'string', description: 'Agent strategy (specialized, balanced, adaptive)' },
53
+ config: { type: 'object', description: 'Additional swarm configuration' },
17
54
  },
18
55
  },
19
56
  handler: async (input) => {
20
57
  const topology = input.topology || 'hierarchical-mesh';
21
- const maxAgents = input.maxAgents || 15;
58
+ const maxAgents = Math.min(Math.max(input.maxAgents || 15, 1), 50);
59
+ const strategy = input.strategy || 'specialized';
22
60
  const config = (input.config || {});
23
- return {
24
- success: true,
25
- swarmId: `swarm-${Date.now()}`,
61
+ if (!VALID_TOPOLOGIES.has(topology)) {
62
+ return {
63
+ success: false,
64
+ error: `Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`,
65
+ };
66
+ }
67
+ const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
68
+ const now = new Date().toISOString();
69
+ const swarmState = {
70
+ swarmId,
26
71
  topology,
27
- initializedAt: new Date().toISOString(),
72
+ maxAgents,
73
+ status: 'running',
74
+ agents: [],
75
+ tasks: [],
28
76
  config: {
29
77
  topology,
30
78
  maxAgents,
31
- currentAgents: 0,
79
+ strategy,
32
80
  communicationProtocol: config.communicationProtocol || 'message-bus',
33
81
  autoScaling: config.autoScaling ?? true,
34
82
  consensusMechanism: config.consensusMechanism || 'majority',
35
83
  },
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ };
87
+ const store = loadSwarmStore();
88
+ store.swarms[swarmId] = swarmState;
89
+ saveSwarmStore(store);
90
+ return {
91
+ success: true,
92
+ swarmId,
93
+ topology,
94
+ strategy,
95
+ maxAgents,
96
+ initializedAt: now,
97
+ config: swarmState.config,
98
+ persisted: true,
36
99
  };
37
100
  },
38
101
  },
39
102
  {
40
103
  name: 'swarm_status',
41
- description: 'Get swarm status',
104
+ description: 'Get swarm status from persistent state',
42
105
  category: 'swarm',
43
106
  inputSchema: {
44
107
  type: 'object',
45
108
  properties: {
46
- swarmId: { type: 'string', description: 'Swarm ID' },
109
+ swarmId: { type: 'string', description: 'Swarm ID (omit for most recent)' },
47
110
  },
48
111
  },
49
112
  handler: async (input) => {
113
+ const store = loadSwarmStore();
114
+ const swarmId = input.swarmId;
115
+ if (swarmId && store.swarms[swarmId]) {
116
+ const swarm = store.swarms[swarmId];
117
+ return {
118
+ swarmId: swarm.swarmId,
119
+ status: swarm.status,
120
+ topology: swarm.topology,
121
+ maxAgents: swarm.maxAgents,
122
+ agentCount: swarm.agents.length,
123
+ taskCount: swarm.tasks.length,
124
+ config: swarm.config,
125
+ createdAt: swarm.createdAt,
126
+ updatedAt: swarm.updatedAt,
127
+ };
128
+ }
129
+ // Return most recent swarm if no ID specified
130
+ const swarmIds = Object.keys(store.swarms);
131
+ if (swarmIds.length === 0) {
132
+ return {
133
+ status: 'no_swarm',
134
+ message: 'No active swarms. Use swarm_init to create one.',
135
+ totalSwarms: 0,
136
+ };
137
+ }
138
+ const latest = swarmIds
139
+ .map(id => store.swarms[id])
140
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
50
141
  return {
51
- swarmId: input.swarmId,
52
- status: 'running',
53
- agentCount: 0,
54
- taskCount: 0,
142
+ swarmId: latest.swarmId,
143
+ status: latest.status,
144
+ topology: latest.topology,
145
+ maxAgents: latest.maxAgents,
146
+ agentCount: latest.agents.length,
147
+ taskCount: latest.tasks.length,
148
+ config: latest.config,
149
+ createdAt: latest.createdAt,
150
+ updatedAt: latest.updatedAt,
151
+ totalSwarms: swarmIds.length,
55
152
  };
56
153
  },
57
154
  },
58
155
  {
59
156
  name: 'swarm_shutdown',
60
- description: 'Shutdown a swarm',
157
+ description: 'Shutdown a swarm and update persistent state',
61
158
  category: 'swarm',
62
159
  inputSchema: {
63
160
  type: 'object',
64
161
  properties: {
65
- swarmId: { type: 'string', description: 'Swarm ID' },
66
- graceful: { type: 'boolean', description: 'Graceful shutdown' },
162
+ swarmId: { type: 'string', description: 'Swarm ID to shutdown' },
163
+ graceful: { type: 'boolean', description: 'Graceful shutdown (default: true)' },
67
164
  },
68
165
  },
69
166
  handler: async (input) => {
167
+ const store = loadSwarmStore();
168
+ const swarmId = input.swarmId;
169
+ // Find the swarm
170
+ let target;
171
+ if (swarmId && store.swarms[swarmId]) {
172
+ target = store.swarms[swarmId];
173
+ }
174
+ else {
175
+ // Shutdown most recent running swarm
176
+ const running = Object.values(store.swarms)
177
+ .filter(s => s.status === 'running')
178
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
179
+ target = running[0];
180
+ }
181
+ if (!target) {
182
+ return {
183
+ success: false,
184
+ error: swarmId ? `Swarm ${swarmId} not found` : 'No running swarms to shutdown',
185
+ };
186
+ }
187
+ if (target.status === 'terminated') {
188
+ return {
189
+ success: false,
190
+ swarmId: target.swarmId,
191
+ error: 'Swarm already terminated',
192
+ };
193
+ }
194
+ target.status = 'terminated';
195
+ target.updatedAt = new Date().toISOString();
196
+ saveSwarmStore(store);
70
197
  return {
71
198
  success: true,
72
- swarmId: input.swarmId,
199
+ swarmId: target.swarmId,
73
200
  terminated: true,
201
+ graceful: input.graceful ?? true,
202
+ agentsTerminated: target.agents.length,
203
+ terminatedAt: target.updatedAt,
74
204
  };
75
205
  },
76
206
  },
77
207
  {
78
208
  name: 'swarm_health',
79
- description: 'Check swarm health status',
209
+ description: 'Check swarm health status with real state inspection',
80
210
  category: 'swarm',
81
211
  inputSchema: {
82
212
  type: 'object',
@@ -85,15 +215,71 @@ export const swarmTools = [
85
215
  },
86
216
  },
87
217
  handler: async (input) => {
218
+ const store = loadSwarmStore();
219
+ const swarmId = input.swarmId;
220
+ // Find the swarm
221
+ let target;
222
+ if (swarmId) {
223
+ target = store.swarms[swarmId];
224
+ if (!target) {
225
+ return {
226
+ status: 'not_found',
227
+ healthy: false,
228
+ checks: [
229
+ { name: 'swarm_exists', status: 'fail', message: `Swarm ${swarmId} not found` },
230
+ ],
231
+ checkedAt: new Date().toISOString(),
232
+ };
233
+ }
234
+ }
235
+ else {
236
+ const running = Object.values(store.swarms)
237
+ .filter(s => s.status === 'running')
238
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
239
+ target = running[0];
240
+ }
241
+ if (!target) {
242
+ return {
243
+ status: 'no_swarm',
244
+ healthy: false,
245
+ checks: [
246
+ { name: 'swarm_exists', status: 'fail', message: 'No active swarm found' },
247
+ ],
248
+ checkedAt: new Date().toISOString(),
249
+ };
250
+ }
251
+ const isRunning = target.status === 'running';
252
+ const stateFileExists = existsSync(getSwarmStatePath());
253
+ const checks = [
254
+ {
255
+ name: 'coordinator',
256
+ status: isRunning ? 'ok' : 'warn',
257
+ message: isRunning ? 'Coordinator active' : `Swarm status: ${target.status}`,
258
+ },
259
+ {
260
+ name: 'agents',
261
+ status: target.agents.length > 0 ? 'ok' : 'info',
262
+ message: `${target.agents.length} agents registered (max: ${target.maxAgents})`,
263
+ },
264
+ {
265
+ name: 'persistence',
266
+ status: stateFileExists ? 'ok' : 'warn',
267
+ message: stateFileExists ? 'State file persisted' : 'State file missing',
268
+ },
269
+ {
270
+ name: 'topology',
271
+ status: 'ok',
272
+ message: `Topology: ${target.topology}`,
273
+ },
274
+ ];
275
+ const healthy = isRunning && stateFileExists;
88
276
  return {
89
- status: 'healthy',
90
- swarmId: input.swarmId || 'default',
91
- checks: [
92
- { name: 'coordinator', status: 'ok', message: 'Coordinator responding' },
93
- { name: 'agents', status: 'ok', message: 'Agent pool healthy' },
94
- { name: 'memory', status: 'ok', message: 'Memory backend connected' },
95
- { name: 'messaging', status: 'ok', message: 'Message bus active' },
96
- ],
277
+ status: healthy ? 'healthy' : 'degraded',
278
+ healthy,
279
+ swarmId: target.swarmId,
280
+ topology: target.topology,
281
+ agentCount: target.agents.length,
282
+ checks,
97
283
  checkedAt: new Date().toISOString(),
98
284
  };
99
285
  },
@@ -78,6 +78,10 @@ export async function uploadToGCS(content, options = {}) {
78
78
  const contentId = generateContentId(content);
79
79
  const checksum = crypto.createHash('sha256').update(content).digest('hex');
80
80
  const fileName = options.name || `${contentId}.cfp.json`;
81
+ // Validate filename to prevent path traversal
82
+ if (!/^[a-zA-Z0-9._\-]+$/.test(fileName) || fileName.includes('..')) {
83
+ throw new Error(`Invalid filename: ${fileName}`);
84
+ }
81
85
  const objectPath = config.prefix ? `${config.prefix}/${fileName}` : fileName;
82
86
  // S-1: Validate bucket name and object path to prevent command injection
83
87
  if (!isValidBucketName(config.bucket)) {
@@ -111,8 +115,11 @@ export async function uploadToGCS(content, options = {}) {
111
115
  // Metadata update failed, but upload succeeded
112
116
  }
113
117
  }
114
- // Clean up temp file
115
- fs.unlinkSync(tempFile);
118
+ // Clean up temp file (validate path is within temp dir)
119
+ const resolvedTemp = path.resolve(tempFile);
120
+ if (resolvedTemp.startsWith(path.resolve(tempDir))) {
121
+ fs.unlinkSync(tempFile);
122
+ }
116
123
  const uri = `gs://${config.bucket}/${objectPath}`;
117
124
  const publicUrl = `https://storage.googleapis.com/${config.bucket}/${objectPath}`;
118
125
  console.log(`[GCS] Upload complete: ${uri}`);
@@ -126,9 +133,12 @@ export async function uploadToGCS(content, options = {}) {
126
133
  };
127
134
  }
128
135
  catch (error) {
129
- // Clean up temp file on error
136
+ // Clean up temp file on error (validate path is within temp dir)
130
137
  try {
131
- fs.unlinkSync(tempFile);
138
+ const resolvedTemp = path.resolve(tempFile);
139
+ if (resolvedTemp.startsWith(path.resolve(tempDir))) {
140
+ fs.unlinkSync(tempFile);
141
+ }
132
142
  }
133
143
  catch { /* ignore */ }
134
144
  throw new Error(`GCS upload failed: ${error}`);
@@ -150,13 +160,19 @@ export async function downloadFromGCS(uri, config) {
150
160
  downloadArgs.push(`--project=${cfg.projectId}`);
151
161
  execFileSync('gcloud', downloadArgs, { encoding: 'utf-8', stdio: 'pipe' });
152
162
  const content = fs.readFileSync(tempFile);
153
- fs.unlinkSync(tempFile);
163
+ const resolvedTemp = path.resolve(tempFile);
164
+ if (resolvedTemp.startsWith(path.resolve(tempDir))) {
165
+ fs.unlinkSync(tempFile);
166
+ }
154
167
  console.log(`[GCS] Downloaded ${content.length} bytes`);
155
168
  return content;
156
169
  }
157
170
  catch (error) {
158
171
  try {
159
- fs.unlinkSync(tempFile);
172
+ const resolvedTemp = path.resolve(tempFile);
173
+ if (resolvedTemp.startsWith(path.resolve(tempDir))) {
174
+ fs.unlinkSync(tempFile);
175
+ }
160
176
  }
161
177
  catch { /* ignore */ }
162
178
  console.error(`[GCS] Download failed: ${error}`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.5.24",
3
+ "version": "3.5.25",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",