claude-flow 3.5.69 → 3.5.70

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.69",
3
+ "version": "3.5.70",
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",
@@ -287,7 +287,7 @@ const predictCommand = {
287
287
  else {
288
288
  output.writeln(`Action: ${prediction?.action || 'unknown'}`);
289
289
  output.writeln(`Confidence: ${prediction?.confidence || 0}`);
290
- if (prediction?.alternatives?.length > 0)
290
+ if (prediction?.alternatives && prediction.alternatives.length > 0)
291
291
  output.writeln(`Alternatives: ${prediction.alternatives.join(', ')}`);
292
292
  }
293
293
  return { success: true };
@@ -3763,7 +3763,7 @@ const postBashCommand = {
3763
3763
  // Token Optimizer command - integrates agentic-flow Agent Booster
3764
3764
  const tokenOptimizeCommand = {
3765
3765
  name: 'token-optimize',
3766
- description: 'Token optimization via agentic-flow Agent Booster (30-50% savings)',
3766
+ description: 'Token optimization via agentic-flow Agent Booster integration',
3767
3767
  options: [
3768
3768
  { name: 'query', short: 'q', type: 'string', description: 'Query for compact context retrieval' },
3769
3769
  { name: 'agents', short: 'A', type: 'number', description: 'Agent count for optimal config', default: '6' },
@@ -3791,7 +3791,6 @@ const tokenOptimizeCommand = {
3791
3791
  memoriesRetrieved: 0,
3792
3792
  };
3793
3793
  let agenticFlowAvailable = false;
3794
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3795
3794
  let reasoningBank = null;
3796
3795
  try {
3797
3796
  // Check if agentic-flow v3 is available
@@ -3846,10 +3845,8 @@ const tokenOptimizeCommand = {
3846
3845
  output.writeln();
3847
3846
  output.printInfo('ReasoningBank not available - query skipped');
3848
3847
  }
3849
- // Simulate some token savings for demo
3850
- stats.totalTokensSaved += 200;
3851
- stats.cacheHits = 2;
3852
- stats.cacheMisses = 1;
3848
+ // Note: stats reflect only actual measured values from this session.
3849
+ // No simulated/fabricated data is added.
3853
3850
  // Show stats
3854
3851
  if (showStats || showReport) {
3855
3852
  output.writeln();
@@ -4413,7 +4410,7 @@ export const hooksCommand = {
4413
4410
  `${output.highlight('coverage-route')} - Route tasks based on coverage gaps (ruvector)`,
4414
4411
  `${output.highlight('coverage-suggest')}- Suggest coverage improvements`,
4415
4412
  `${output.highlight('coverage-gaps')} - List all coverage gaps with agents`,
4416
- `${output.highlight('token-optimize')} - Token optimization (30-50% savings)`,
4413
+ `${output.highlight('token-optimize')} - Token optimization (agentic-flow integration)`,
4417
4414
  `${output.highlight('model-route')} - Route to optimal model (haiku/sonnet/opus)`,
4418
4415
  `${output.highlight('model-outcome')} - Record model routing outcome`,
4419
4416
  `${output.highlight('model-stats')} - View model routing statistics`,
@@ -18,7 +18,6 @@ async function initCodexAction(ctx, options) {
18
18
  const spinner = output.createSpinner({ text: 'Initializing Codex project...' });
19
19
  spinner.start();
20
20
  try {
21
- // Dynamic import of the Codex initializer with lazy loading fallback
22
21
  let CodexInitializer;
23
22
  // Try multiple resolution strategies for the @claude-flow/codex package
24
23
  // Use a variable to prevent TypeScript from statically resolving the optional module
@@ -1333,6 +1333,7 @@ const benchmarkCommand = {
1333
1333
  const spinner = output.createSpinner({ text: 'Running benchmarks...', spinner: 'dots' });
1334
1334
  spinner.start();
1335
1335
  try {
1336
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional native WASM module with no type declarations
1336
1337
  const attention = await import('@ruvector/attention');
1337
1338
  // Manual benchmark since benchmarkAttention has a binding bug
1338
1339
  const benchmarkMechanism = async (name, mechanism) => {
@@ -6,6 +6,91 @@
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  import { configManager } from '../services/config-file-manager.js';
9
+ const PROVIDER_CATALOG = [
10
+ { name: 'Anthropic', type: 'LLM', models: 'claude-3.5-sonnet, opus', envVar: 'ANTHROPIC_API_KEY', configName: 'anthropic' },
11
+ { name: 'OpenAI', type: 'LLM', models: 'gpt-4o, gpt-4-turbo', envVar: 'OPENAI_API_KEY', configName: 'openai' },
12
+ { name: 'OpenAI', type: 'Embedding', models: 'text-embedding-3-small/large', envVar: 'OPENAI_API_KEY', configName: 'openai' },
13
+ { name: 'Google', type: 'LLM', models: 'gemini-pro, gemini-ultra', envVar: 'GOOGLE_API_KEY', configName: 'google' },
14
+ { name: 'Transformers.js', type: 'Embedding', models: 'Xenova/all-MiniLM-L6-v2' },
15
+ { name: 'Agentic Flow', type: 'Embedding', models: 'ONNX optimized' },
16
+ { name: 'Mock', type: 'All', models: 'mock-*' },
17
+ ];
18
+ /**
19
+ * Resolve the API key for a provider by checking the config file first,
20
+ * then falling back to well-known environment variables.
21
+ */
22
+ function resolveApiKey(providerName, configuredProviders) {
23
+ // Check config file entry
24
+ const entry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === providerName.toLowerCase());
25
+ if (entry?.apiKey && typeof entry.apiKey === 'string') {
26
+ return entry.apiKey;
27
+ }
28
+ // Check environment variable
29
+ const envMapping = {
30
+ anthropic: 'ANTHROPIC_API_KEY',
31
+ openai: 'OPENAI_API_KEY',
32
+ google: 'GOOGLE_API_KEY',
33
+ };
34
+ const envVar = envMapping[providerName.toLowerCase()];
35
+ if (envVar && process.env[envVar]) {
36
+ return process.env[envVar];
37
+ }
38
+ return undefined;
39
+ }
40
+ /**
41
+ * Make a lightweight HTTP request to verify provider API key validity.
42
+ * Uses a 5-second timeout. Returns { ok, reason }.
43
+ */
44
+ async function testProviderConnectivity(providerName, apiKey) {
45
+ const endpoints = {
46
+ anthropic: {
47
+ url: 'https://api.anthropic.com/v1/models',
48
+ headers: {
49
+ 'x-api-key': apiKey,
50
+ 'anthropic-version': '2023-06-01',
51
+ },
52
+ },
53
+ openai: {
54
+ url: 'https://api.openai.com/v1/models',
55
+ headers: {
56
+ 'Authorization': `Bearer ${apiKey}`,
57
+ },
58
+ },
59
+ google: {
60
+ url: `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
61
+ headers: {},
62
+ },
63
+ };
64
+ const endpointConfig = endpoints[providerName.toLowerCase()];
65
+ if (!endpointConfig) {
66
+ return { ok: false, reason: 'No test endpoint available for this provider' };
67
+ }
68
+ try {
69
+ const controller = new AbortController();
70
+ const timeout = setTimeout(() => controller.abort(), 5000);
71
+ const res = await fetch(endpointConfig.url, {
72
+ method: 'GET',
73
+ headers: endpointConfig.headers,
74
+ signal: controller.signal,
75
+ });
76
+ clearTimeout(timeout);
77
+ if (res.ok || res.status === 200) {
78
+ return { ok: true, reason: 'Connected successfully' };
79
+ }
80
+ if (res.status === 401 || res.status === 403) {
81
+ return { ok: false, reason: `Authentication failed (HTTP ${res.status})` };
82
+ }
83
+ // A non-auth error but the server responded — key format may be fine
84
+ return { ok: false, reason: `Unexpected response (HTTP ${res.status})` };
85
+ }
86
+ catch (err) {
87
+ if (err instanceof Error && err.name === 'AbortError') {
88
+ return { ok: false, reason: 'Connection timed out (5s)' };
89
+ }
90
+ const msg = err instanceof Error ? err.message : String(err);
91
+ return { ok: false, reason: `Connection failed: ${msg}` };
92
+ }
93
+ }
9
94
  // List subcommand
10
95
  const listCommand = {
11
96
  name: 'list',
@@ -20,26 +105,88 @@ const listCommand = {
20
105
  ],
21
106
  action: async (ctx) => {
22
107
  const type = ctx.flags.type || 'all';
23
- // Note: Static provider catalog — does not reflect user's configured providers
108
+ const activeOnly = ctx.flags.active;
109
+ // Load user configuration
110
+ const cwd = process.cwd();
111
+ const config = configManager.getConfig(cwd);
112
+ const agents = (config.agents ?? {});
113
+ const configuredProviders = (agents.providers ?? []);
114
+ // Build table rows from the catalog, enriched with configuration status
115
+ const rows = [];
116
+ for (const entry of PROVIDER_CATALOG) {
117
+ // Apply type filter
118
+ if (type !== 'all' && entry.type.toLowerCase() !== type.toLowerCase()) {
119
+ continue;
120
+ }
121
+ let status;
122
+ let keySource = '';
123
+ if (entry.configName) {
124
+ const apiKey = resolveApiKey(entry.configName, configuredProviders);
125
+ if (apiKey) {
126
+ // Determine the source for the key
127
+ const configEntry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === entry.configName.toLowerCase());
128
+ if (configEntry?.apiKey && typeof configEntry.apiKey === 'string') {
129
+ keySource = 'config';
130
+ }
131
+ else {
132
+ keySource = 'env';
133
+ }
134
+ status = output.success(`Configured (${keySource})`);
135
+ }
136
+ else {
137
+ status = output.warning('Not configured');
138
+ }
139
+ }
140
+ else if (entry.name === 'Mock') {
141
+ status = output.dim('Dev only');
142
+ }
143
+ else {
144
+ // Local-only providers (Transformers.js, Agentic Flow) — always available
145
+ status = output.success('Available (local)');
146
+ }
147
+ if (activeOnly && !status.includes('Configured') && !status.includes('Available')) {
148
+ continue;
149
+ }
150
+ rows.push({
151
+ provider: entry.name,
152
+ type: entry.type,
153
+ models: entry.models,
154
+ status,
155
+ });
156
+ }
157
+ // Also show any providers in config that are not in the static catalog
158
+ for (const cp of configuredProviders) {
159
+ const cpName = cp.name || '';
160
+ const alreadyListed = PROVIDER_CATALOG.some((e) => e.configName?.toLowerCase() === cpName.toLowerCase() || e.name.toLowerCase() === cpName.toLowerCase());
161
+ if (!alreadyListed && cpName) {
162
+ const hasKey = !!(cp.apiKey || resolveApiKey(cpName, configuredProviders));
163
+ rows.push({
164
+ provider: cpName,
165
+ type: cp.type || 'Custom',
166
+ models: cp.model || output.dim('(not specified)'),
167
+ status: hasKey ? output.success('Configured (config)') : output.warning('Not configured'),
168
+ });
169
+ }
170
+ }
24
171
  output.writeln();
25
- output.writeln(output.bold('Available Providers'));
172
+ output.writeln(output.bold('Providers'));
26
173
  output.writeln(output.dim('─'.repeat(60)));
27
- output.printTable({
28
- columns: [
29
- { key: 'provider', header: 'Provider', width: 18 },
30
- { key: 'type', header: 'Type', width: 12 },
31
- { key: 'models', header: 'Models', width: 25 },
32
- { key: 'status', header: 'Status', width: 12 },
33
- ],
34
- data: [
35
- { provider: 'Anthropic', type: 'LLM', models: 'claude-3.5-sonnet, opus', status: output.success('Active') },
36
- { provider: 'OpenAI', type: 'LLM', models: 'gpt-4o, gpt-4-turbo', status: output.success('Active') },
37
- { provider: 'OpenAI', type: 'Embedding', models: 'text-embedding-3-small/large', status: output.success('Active') },
38
- { provider: 'Transformers.js', type: 'Embedding', models: 'Xenova/all-MiniLM-L6-v2', status: output.success('Active') },
39
- { provider: 'Agentic Flow', type: 'Embedding', models: 'ONNX optimized', status: output.success('Active') },
40
- { provider: 'Mock', type: 'All', models: 'mock-*', status: output.dim('Dev only') },
41
- ],
42
- });
174
+ if (rows.length === 0) {
175
+ output.writeln(output.dim(' No providers match the current filter.'));
176
+ }
177
+ else {
178
+ output.printTable({
179
+ columns: [
180
+ { key: 'provider', header: 'Provider', width: 18 },
181
+ { key: 'type', header: 'Type', width: 12 },
182
+ { key: 'models', header: 'Models', width: 25 },
183
+ { key: 'status', header: 'Status', width: 20 },
184
+ ],
185
+ data: rows,
186
+ });
187
+ }
188
+ output.writeln();
189
+ output.writeln(output.dim('Tip: Use "providers configure -p <name> -k <key>" to set API keys.'));
43
190
  return { success: true };
44
191
  },
45
192
  };
@@ -133,98 +280,83 @@ const testCommand = {
133
280
  const config = configManager.getConfig(cwd);
134
281
  const agents = (config.agents ?? {});
135
282
  const configuredProviders = (agents.providers ?? []);
136
- const getConfigApiKey = (name) => {
137
- const entry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === name.toLowerCase());
138
- return entry?.apiKey;
139
- };
140
- const knownChecks = [
141
- {
142
- name: 'Anthropic',
143
- test: async () => {
144
- const key = process.env.ANTHROPIC_API_KEY || getConfigApiKey('anthropic');
145
- if (key)
146
- return { pass: true, reason: 'API key found' };
147
- return { pass: false, reason: 'ANTHROPIC_API_KEY not set and no apiKey in config' };
148
- },
149
- },
150
- {
151
- name: 'OpenAI',
152
- test: async () => {
153
- const key = process.env.OPENAI_API_KEY || getConfigApiKey('openai');
154
- if (key)
155
- return { pass: true, reason: 'API key found' };
156
- return { pass: false, reason: 'OPENAI_API_KEY not set and no apiKey in config' };
157
- },
158
- },
159
- {
160
- name: 'Google',
161
- test: async () => {
162
- const key = process.env.GOOGLE_API_KEY || getConfigApiKey('google');
163
- if (key)
164
- return { pass: true, reason: 'API key found' };
165
- return { pass: false, reason: 'GOOGLE_API_KEY not set and no apiKey in config' };
166
- },
167
- },
168
- {
169
- name: 'Ollama',
170
- test: async () => {
171
- const entry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === 'ollama');
172
- const baseUrl = entry?.baseUrl || 'http://localhost:11434';
173
- try {
174
- const controller = new AbortController();
175
- const timeout = setTimeout(() => controller.abort(), 3000);
176
- const res = await fetch(baseUrl, { signal: controller.signal });
177
- clearTimeout(timeout);
178
- if (res.ok)
179
- return { pass: true, reason: `Reachable at ${baseUrl}` };
180
- return { pass: false, reason: `HTTP ${res.status} from ${baseUrl}` };
181
- }
182
- catch {
183
- return { pass: false, reason: `Unreachable at ${baseUrl}` };
184
- }
185
- },
186
- },
283
+ const knownTargets = [
284
+ { name: 'Anthropic', configName: 'anthropic' },
285
+ { name: 'OpenAI', configName: 'openai' },
286
+ { name: 'Google', configName: 'google' },
187
287
  ];
188
- // Filter to requested provider or test all
189
- let checksToRun;
288
+ // Add Ollama as a special case (endpoint-based, no API key)
289
+ const ollamaEntry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === 'ollama');
290
+ let targets;
190
291
  if (testAll || !provider) {
191
- checksToRun = knownChecks;
292
+ targets = [...knownTargets];
192
293
  }
193
294
  else {
194
- const match = knownChecks.find((c) => c.name.toLowerCase() === provider.toLowerCase());
195
- if (match) {
196
- checksToRun = [match];
295
+ const match = knownTargets.find((t) => t.name.toLowerCase() === provider.toLowerCase() || t.configName === provider.toLowerCase());
296
+ targets = match ? [match] : [{ name: provider, configName: provider.toLowerCase() }];
297
+ }
298
+ const results = [];
299
+ // Test API-key-based providers with real connectivity checks
300
+ for (const target of targets) {
301
+ const apiKey = resolveApiKey(target.configName, configuredProviders);
302
+ if (!apiKey) {
303
+ results.push({ name: target.name, pass: false, reason: 'Not configured (no API key found)' });
304
+ continue;
197
305
  }
198
- else {
199
- // Unknown provider -- check if it has a config entry with an apiKey
200
- checksToRun = [
201
- {
202
- name: provider,
203
- test: async () => {
204
- const key = getConfigApiKey(provider);
205
- if (key)
206
- return { pass: true, reason: 'API key found in config' };
207
- return { pass: false, reason: 'No API key in environment or config' };
208
- },
209
- },
210
- ];
306
+ output.writeln(output.dim(` Testing ${target.name}...`));
307
+ const result = await testProviderConnectivity(target.name, apiKey);
308
+ results.push({ name: target.name, pass: result.ok, reason: result.reason });
309
+ }
310
+ // Test Ollama separately (endpoint-based, no API key needed)
311
+ if (testAll || !provider || provider.toLowerCase() === 'ollama') {
312
+ const baseUrl = ollamaEntry?.baseUrl || 'http://localhost:11434';
313
+ output.writeln(output.dim(` Testing Ollama at ${baseUrl}...`));
314
+ try {
315
+ const controller = new AbortController();
316
+ const timeout = setTimeout(() => controller.abort(), 5000);
317
+ const res = await fetch(baseUrl, { signal: controller.signal });
318
+ clearTimeout(timeout);
319
+ if (res.ok) {
320
+ results.push({ name: 'Ollama', pass: true, reason: `Connected at ${baseUrl}` });
321
+ }
322
+ else {
323
+ results.push({ name: 'Ollama', pass: false, reason: `HTTP ${res.status} from ${baseUrl}` });
324
+ }
325
+ }
326
+ catch {
327
+ results.push({ name: 'Ollama', pass: false, reason: `Unreachable at ${baseUrl}` });
211
328
  }
212
329
  }
213
- let anyPassed = false;
214
- const results = [];
215
- for (const check of checksToRun) {
216
- const result = await check.test();
217
- results.push({ name: check.name, ...result });
218
- if (result.pass)
219
- anyPassed = true;
330
+ // Also test any custom providers from config that were not in the known list
331
+ if (testAll || !provider) {
332
+ for (const cp of configuredProviders) {
333
+ const cpName = cp.name || '';
334
+ const alreadyTested = results.some((r) => r.name.toLowerCase() === cpName.toLowerCase());
335
+ if (alreadyTested || !cpName)
336
+ continue;
337
+ const apiKey = resolveApiKey(cpName, configuredProviders);
338
+ if (!apiKey) {
339
+ results.push({ name: cpName, pass: false, reason: 'No API key found' });
340
+ }
341
+ else {
342
+ // For custom providers we can only verify the key exists
343
+ results.push({ name: cpName, pass: true, reason: 'API key found (no test endpoint available)' });
344
+ }
345
+ }
220
346
  }
347
+ let anyPassed = false;
221
348
  output.writeln();
222
349
  for (const r of results) {
223
350
  const icon = r.pass ? output.success('PASS') : output.error('FAIL');
224
351
  output.writeln(` ${icon} ${r.name}: ${r.reason}`);
352
+ if (r.pass)
353
+ anyPassed = true;
225
354
  }
226
355
  output.writeln();
227
- if (anyPassed) {
356
+ if (results.length === 0) {
357
+ output.writeln(output.warning('No providers to test. Use "providers configure" to add providers.'));
358
+ }
359
+ else if (anyPassed) {
228
360
  output.writeln(output.success(`${results.filter((r) => r.pass).length}/${results.length} provider(s) passed.`));
229
361
  }
230
362
  else {
@@ -53,7 +53,7 @@ const scanCommand = {
53
53
  }
54
54
  catch (auditErr) {
55
55
  // npm audit exits non-zero when vulnerabilities found — stdout still has JSON
56
- auditResult = auditErr.stdout || '{}';
56
+ auditResult = (auditErr instanceof Error && 'stdout' in auditErr ? auditErr.stdout : undefined) || '{}';
57
57
  }
58
58
  try {
59
59
  const audit = JSON.parse(auditResult);
@@ -6,6 +6,7 @@
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { getProjectCwd } from './types.js';
9
+ import { validateIdentifier, validateText } from './validate-input.js';
9
10
  // Storage paths
10
11
  const STORAGE_DIR = '.claude-flow';
11
12
  const CONFIG_FILE = 'config.json';
@@ -115,6 +116,15 @@ export const configTools = [
115
116
  required: ['key'],
116
117
  },
117
118
  handler: async (input) => {
119
+ // Validate user-provided input (#1425)
120
+ const vKey = validateText(input.key, 'key', 256);
121
+ if (!vKey.valid)
122
+ return { success: false, error: vKey.error };
123
+ if (input.scope) {
124
+ const v = validateIdentifier(input.scope, 'scope');
125
+ if (!v.valid)
126
+ return { success: false, error: v.error };
127
+ }
118
128
  const store = loadConfigStore();
119
129
  const key = input.key;
120
130
  const scope = input.scope || 'default';
@@ -152,6 +162,15 @@ export const configTools = [
152
162
  required: ['key', 'value'],
153
163
  },
154
164
  handler: async (input) => {
165
+ // Validate user-provided input (#1425)
166
+ const vKey = validateText(input.key, 'key', 256);
167
+ if (!vKey.valid)
168
+ return { success: false, error: vKey.error };
169
+ if (input.scope) {
170
+ const v = validateIdentifier(input.scope, 'scope');
171
+ if (!v.valid)
172
+ return { success: false, error: v.error };
173
+ }
155
174
  const store = loadConfigStore();
156
175
  const key = input.key;
157
176
  const value = input.value;
@@ -190,6 +209,17 @@ export const configTools = [
190
209
  },
191
210
  },
192
211
  handler: async (input) => {
212
+ // Validate user-provided input (#1425)
213
+ if (input.scope) {
214
+ const v = validateIdentifier(input.scope, 'scope');
215
+ if (!v.valid)
216
+ return { success: false, error: v.error };
217
+ }
218
+ if (input.prefix) {
219
+ const v = validateText(input.prefix, 'prefix', 256);
220
+ if (!v.valid)
221
+ return { success: false, error: v.error };
222
+ }
193
223
  const store = loadConfigStore();
194
224
  const scope = input.scope || 'default';
195
225
  const prefix = input.prefix;
@@ -236,6 +266,17 @@ export const configTools = [
236
266
  },
237
267
  },
238
268
  handler: async (input) => {
269
+ // Validate user-provided input (#1425)
270
+ if (input.scope) {
271
+ const v = validateIdentifier(input.scope, 'scope');
272
+ if (!v.valid)
273
+ return { success: false, error: v.error };
274
+ }
275
+ if (input.key) {
276
+ const v = validateText(input.key, 'key', 256);
277
+ if (!v.valid)
278
+ return { success: false, error: v.error };
279
+ }
239
280
  const store = loadConfigStore();
240
281
  const scope = input.scope || 'default';
241
282
  const key = input.key;
@@ -286,6 +327,12 @@ export const configTools = [
286
327
  },
287
328
  },
288
329
  handler: async (input) => {
330
+ // Validate user-provided input (#1425)
331
+ if (input.scope) {
332
+ const v = validateIdentifier(input.scope, 'scope');
333
+ if (!v.valid)
334
+ return { success: false, error: v.error };
335
+ }
289
336
  const store = loadConfigStore();
290
337
  const scope = input.scope || 'default';
291
338
  const includeDefaults = input.includeDefaults !== false;
@@ -320,6 +367,12 @@ export const configTools = [
320
367
  required: ['config'],
321
368
  },
322
369
  handler: async (input) => {
370
+ // Validate user-provided input (#1425)
371
+ if (input.scope) {
372
+ const v = validateIdentifier(input.scope, 'scope');
373
+ if (!v.valid)
374
+ return { success: false, error: v.error };
375
+ }
323
376
  const store = loadConfigStore();
324
377
  const config = filterDangerousKeys(input.config);
325
378
  const scope = input.scope || 'default';
@@ -6,6 +6,7 @@
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { getProjectCwd } from './types.js';
9
+ import { validateIdentifier, validateText } from './validate-input.js';
9
10
  // Storage paths
10
11
  const STORAGE_DIR = '.claude-flow';
11
12
  const SESSION_DIR = 'sessions';
@@ -105,6 +106,15 @@ export const sessionTools = [
105
106
  required: ['name'],
106
107
  },
107
108
  handler: async (input) => {
109
+ // Validate user-provided input (#1425)
110
+ const vName = validateText(input.name, 'name', 256);
111
+ if (!vName.valid)
112
+ return { success: false, error: vName.error };
113
+ if (input.description) {
114
+ const v = validateText(input.description, 'description');
115
+ if (!v.valid)
116
+ return { success: false, error: v.error };
117
+ }
108
118
  const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
109
119
  // Load related data based on options
110
120
  const data = loadRelatedStores({
@@ -152,6 +162,17 @@ export const sessionTools = [
152
162
  },
153
163
  },
154
164
  handler: async (input) => {
165
+ // Validate user-provided input (#1425)
166
+ if (input.sessionId) {
167
+ const v = validateIdentifier(input.sessionId, 'sessionId');
168
+ if (!v.valid)
169
+ return { success: false, error: v.error };
170
+ }
171
+ if (input.name) {
172
+ const v = validateText(input.name, 'name', 256);
173
+ if (!v.valid)
174
+ return { success: false, error: v.error };
175
+ }
155
176
  let session = null;
156
177
  // Try to find by sessionId first
157
178
  if (input.sessionId) {
@@ -279,6 +300,10 @@ export const sessionTools = [
279
300
  required: ['sessionId'],
280
301
  },
281
302
  handler: async (input) => {
303
+ // Validate user-provided input (#1425)
304
+ const vId = validateIdentifier(input.sessionId, 'sessionId');
305
+ if (!vId.valid)
306
+ return { success: false, error: vId.error };
282
307
  const sessionId = input.sessionId;
283
308
  const path = getSessionPath(sessionId);
284
309
  if (existsSync(path)) {
@@ -308,6 +333,10 @@ export const sessionTools = [
308
333
  required: ['sessionId'],
309
334
  },
310
335
  handler: async (input) => {
336
+ // Validate user-provided input (#1425)
337
+ const vId = validateIdentifier(input.sessionId, 'sessionId');
338
+ if (!vId.valid)
339
+ return { success: false, error: vId.error };
311
340
  const sessionId = input.sessionId;
312
341
  const session = loadSession(sessionId);
313
342
  if (session) {
@@ -7,6 +7,7 @@
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { getProjectCwd } from './types.js';
10
+ import { validateIdentifier } from './validate-input.js';
10
11
  // Swarm state persistence
11
12
  const SWARM_DIR = '.claude-flow/swarm';
12
13
  const SWARM_STATE_FILE = 'swarm-state.json';
@@ -55,6 +56,17 @@ export const swarmTools = [
55
56
  },
56
57
  },
57
58
  handler: async (input) => {
59
+ // Validate user-provided input (#1425)
60
+ if (input.topology) {
61
+ const v = validateIdentifier(input.topology, 'topology');
62
+ if (!v.valid)
63
+ return { success: false, error: v.error };
64
+ }
65
+ if (input.strategy) {
66
+ const v = validateIdentifier(input.strategy, 'strategy');
67
+ if (!v.valid)
68
+ return { success: false, error: v.error };
69
+ }
58
70
  const topology = input.topology || 'hierarchical-mesh';
59
71
  const maxAgents = Math.min(Math.max(input.maxAgents || 15, 1), 50);
60
72
  const strategy = input.strategy || 'specialized';
@@ -111,6 +123,12 @@ export const swarmTools = [
111
123
  },
112
124
  },
113
125
  handler: async (input) => {
126
+ // Validate user-provided input (#1425)
127
+ if (input.swarmId) {
128
+ const v = validateIdentifier(input.swarmId, 'swarmId');
129
+ if (!v.valid)
130
+ return { success: false, error: v.error };
131
+ }
114
132
  const store = loadSwarmStore();
115
133
  const swarmId = input.swarmId;
116
134
  if (swarmId && store.swarms[swarmId]) {
@@ -165,6 +183,12 @@ export const swarmTools = [
165
183
  },
166
184
  },
167
185
  handler: async (input) => {
186
+ // Validate user-provided input (#1425)
187
+ if (input.swarmId) {
188
+ const v = validateIdentifier(input.swarmId, 'swarmId');
189
+ if (!v.valid)
190
+ return { success: false, error: v.error };
191
+ }
168
192
  const store = loadSwarmStore();
169
193
  const swarmId = input.swarmId;
170
194
  // Find the swarm
@@ -216,6 +240,12 @@ export const swarmTools = [
216
240
  },
217
241
  },
218
242
  handler: async (input) => {
243
+ // Validate user-provided input (#1425)
244
+ if (input.swarmId) {
245
+ const v = validateIdentifier(input.swarmId, 'swarmId');
246
+ if (!v.valid)
247
+ return { success: false, error: v.error };
248
+ }
219
249
  const store = loadSwarmStore();
220
250
  const swarmId = input.swarmId;
221
251
  // Find the swarm
@@ -6,6 +6,7 @@
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { getProjectCwd } from './types.js';
9
+ import { validateIdentifier, validateText } from './validate-input.js';
9
10
  // Storage paths
10
11
  const STORAGE_DIR = '.claude-flow';
11
12
  const TASK_DIR = 'tasks';
@@ -56,6 +57,13 @@ export const taskTools = [
56
57
  required: ['type', 'description'],
57
58
  },
58
59
  handler: async (input) => {
60
+ // Validate user-provided input (#1425)
61
+ const vType = validateIdentifier(input.type, 'type');
62
+ if (!vType.valid)
63
+ return { success: false, error: vType.error };
64
+ const vDesc = validateText(input.description, 'description');
65
+ if (!vDesc.valid)
66
+ return { success: false, error: vDesc.error };
59
67
  const store = loadTaskStore();
60
68
  const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
61
69
  const task = {
@@ -97,6 +105,10 @@ export const taskTools = [
97
105
  required: ['taskId'],
98
106
  },
99
107
  handler: async (input) => {
108
+ // Validate user-provided input (#1425)
109
+ const vId = validateIdentifier(input.taskId, 'taskId');
110
+ if (!vId.valid)
111
+ return { success: false, error: vId.error };
100
112
  const store = loadTaskStore();
101
113
  const taskId = input.taskId;
102
114
  const task = store.tasks[taskId];
@@ -194,6 +206,10 @@ export const taskTools = [
194
206
  required: ['taskId'],
195
207
  },
196
208
  handler: async (input) => {
209
+ // Validate user-provided input (#1425)
210
+ const vId = validateIdentifier(input.taskId, 'taskId');
211
+ if (!vId.valid)
212
+ return { success: false, error: vId.error };
197
213
  const store = loadTaskStore();
198
214
  const taskId = input.taskId;
199
215
  const task = store.tasks[taskId];
@@ -254,6 +270,10 @@ export const taskTools = [
254
270
  required: ['taskId'],
255
271
  },
256
272
  handler: async (input) => {
273
+ // Validate user-provided input (#1425)
274
+ const vId = validateIdentifier(input.taskId, 'taskId');
275
+ if (!vId.valid)
276
+ return { success: false, error: vId.error };
257
277
  const store = loadTaskStore();
258
278
  const taskId = input.taskId;
259
279
  const task = store.tasks[taskId];
@@ -301,6 +321,10 @@ export const taskTools = [
301
321
  required: ['taskId'],
302
322
  },
303
323
  handler: async (input) => {
324
+ // Validate user-provided input (#1425)
325
+ const vId = validateIdentifier(input.taskId, 'taskId');
326
+ if (!vId.valid)
327
+ return { success: false, error: vId.error };
304
328
  const store = loadTaskStore();
305
329
  const taskId = input.taskId;
306
330
  const task = store.tasks[taskId];
@@ -380,6 +404,15 @@ export const taskTools = [
380
404
  required: ['taskId'],
381
405
  },
382
406
  handler: async (input) => {
407
+ // Validate user-provided input (#1425)
408
+ const vId = validateIdentifier(input.taskId, 'taskId');
409
+ if (!vId.valid)
410
+ return { success: false, error: vId.error };
411
+ if (input.reason) {
412
+ const v = validateText(input.reason, 'reason');
413
+ if (!v.valid)
414
+ return { success: false, error: v.error };
415
+ }
383
416
  const store = loadTaskStore();
384
417
  const taskId = input.taskId;
385
418
  const task = store.tasks[taskId];
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { getProjectCwd } from './types.js';
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { validateIdentifier, validatePath, validateText } from './validate-input.js';
8
9
  import { join } from 'node:path';
9
10
  import { execSync } from 'node:child_process';
10
11
  // Storage paths
@@ -53,6 +54,17 @@ export const terminalTools = [
53
54
  },
54
55
  },
55
56
  handler: async (input) => {
57
+ // Validate user-provided input (#1425)
58
+ if (input.name) {
59
+ const v = validateText(input.name, 'name', 256);
60
+ if (!v.valid)
61
+ return { success: false, error: v.error };
62
+ }
63
+ if (input.workingDir) {
64
+ const v = validatePath(input.workingDir, 'workingDir');
65
+ if (!v.valid)
66
+ return { success: false, error: v.error };
67
+ }
56
68
  const store = loadTerminalStore();
57
69
  const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
58
70
  const session = {
@@ -92,6 +104,15 @@ export const terminalTools = [
92
104
  required: ['command'],
93
105
  },
94
106
  handler: async (input) => {
107
+ // Validate user-provided input (#1425)
108
+ const vCmd = validateText(input.command, 'command', 10_000);
109
+ if (!vCmd.valid)
110
+ return { success: false, error: vCmd.error };
111
+ if (input.sessionId) {
112
+ const v = validateIdentifier(input.sessionId, 'sessionId');
113
+ if (!v.valid)
114
+ return { success: false, error: v.error };
115
+ }
95
116
  const store = loadTerminalStore();
96
117
  const sessionId = input.sessionId;
97
118
  const command = input.command;
@@ -201,6 +222,10 @@ export const terminalTools = [
201
222
  required: ['sessionId'],
202
223
  },
203
224
  handler: async (input) => {
225
+ // Validate user-provided input (#1425)
226
+ const vId = validateIdentifier(input.sessionId, 'sessionId');
227
+ if (!vId.valid)
228
+ return { success: false, error: vId.error };
204
229
  const store = loadTerminalStore();
205
230
  const sessionId = input.sessionId;
206
231
  const session = store.sessions[sessionId];
@@ -229,6 +254,12 @@ export const terminalTools = [
229
254
  },
230
255
  },
231
256
  handler: async (input) => {
257
+ // Validate user-provided input (#1425)
258
+ if (input.sessionId) {
259
+ const v = validateIdentifier(input.sessionId, 'sessionId');
260
+ if (!v.valid)
261
+ return { success: false, error: v.error };
262
+ }
232
263
  const store = loadTerminalStore();
233
264
  const sessionId = input.sessionId;
234
265
  const limit = input.limit || 50;
@@ -6,6 +6,7 @@
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { getProjectCwd } from './types.js';
9
+ import { validateIdentifier, validatePath, validateText } from './validate-input.js';
9
10
  // Storage paths
10
11
  const STORAGE_DIR = '.claude-flow';
11
12
  const WORKFLOW_DIR = 'workflows';
@@ -63,6 +64,22 @@ export const workflowTools = [
63
64
  },
64
65
  },
65
66
  handler: async (input) => {
67
+ // Validate user-provided input (#1425)
68
+ if (input.template) {
69
+ const v = validateIdentifier(input.template, 'template');
70
+ if (!v.valid)
71
+ return { success: false, error: v.error };
72
+ }
73
+ if (input.file) {
74
+ const v = validatePath(input.file, 'file');
75
+ if (!v.valid)
76
+ return { success: false, error: v.error };
77
+ }
78
+ if (input.task) {
79
+ const v = validateText(input.task, 'task');
80
+ if (!v.valid)
81
+ return { success: false, error: v.error };
82
+ }
66
83
  const store = loadWorkflowStore();
67
84
  const template = input.template;
68
85
  const task = input.task;
@@ -157,6 +174,15 @@ export const workflowTools = [
157
174
  required: ['name'],
158
175
  },
159
176
  handler: async (input) => {
177
+ // Validate user-provided input (#1425)
178
+ const vName = validateText(input.name, 'name', 256);
179
+ if (!vName.valid)
180
+ return { success: false, error: vName.error };
181
+ if (input.description) {
182
+ const v = validateText(input.description, 'description');
183
+ if (!v.valid)
184
+ return { success: false, error: v.error };
185
+ }
160
186
  const store = loadWorkflowStore();
161
187
  const workflowId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
162
188
  const steps = (input.steps || []).map((s, i) => ({
@@ -201,6 +227,10 @@ export const workflowTools = [
201
227
  required: ['workflowId'],
202
228
  },
203
229
  handler: async (input) => {
230
+ // Validate user-provided input (#1425)
231
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
232
+ if (!vId.valid)
233
+ return { success: false, error: vId.error };
204
234
  const store = loadWorkflowStore();
205
235
  const workflowId = input.workflowId;
206
236
  const workflow = store.workflows[workflowId];
@@ -252,6 +282,10 @@ export const workflowTools = [
252
282
  required: ['workflowId'],
253
283
  },
254
284
  handler: async (input) => {
285
+ // Validate user-provided input (#1425)
286
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
287
+ if (!vId.valid)
288
+ return { success: false, error: vId.error };
255
289
  const store = loadWorkflowStore();
256
290
  const workflowId = input.workflowId;
257
291
  const workflow = store.workflows[workflowId];
@@ -303,6 +337,12 @@ export const workflowTools = [
303
337
  },
304
338
  },
305
339
  handler: async (input) => {
340
+ // Validate user-provided input (#1425)
341
+ if (input.status) {
342
+ const v = validateIdentifier(input.status, 'status');
343
+ if (!v.valid)
344
+ return { success: false, error: v.error };
345
+ }
306
346
  const store = loadWorkflowStore();
307
347
  let workflows = Object.values(store.workflows);
308
348
  // Apply filters
@@ -340,6 +380,10 @@ export const workflowTools = [
340
380
  required: ['workflowId'],
341
381
  },
342
382
  handler: async (input) => {
383
+ // Validate user-provided input (#1425)
384
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
385
+ if (!vId.valid)
386
+ return { success: false, error: vId.error };
343
387
  const store = loadWorkflowStore();
344
388
  const workflowId = input.workflowId;
345
389
  const workflow = store.workflows[workflowId];
@@ -371,6 +415,10 @@ export const workflowTools = [
371
415
  required: ['workflowId'],
372
416
  },
373
417
  handler: async (input) => {
418
+ // Validate user-provided input (#1425)
419
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
420
+ if (!vId.valid)
421
+ return { success: false, error: vId.error };
374
422
  const store = loadWorkflowStore();
375
423
  const workflowId = input.workflowId;
376
424
  const workflow = store.workflows[workflowId];
@@ -413,6 +461,15 @@ export const workflowTools = [
413
461
  required: ['workflowId'],
414
462
  },
415
463
  handler: async (input) => {
464
+ // Validate user-provided input (#1425)
465
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
466
+ if (!vId.valid)
467
+ return { success: false, error: vId.error };
468
+ if (input.reason) {
469
+ const v = validateText(input.reason, 'reason');
470
+ if (!v.valid)
471
+ return { success: false, error: v.error };
472
+ }
416
473
  const store = loadWorkflowStore();
417
474
  const workflowId = input.workflowId;
418
475
  const workflow = store.workflows[workflowId];
@@ -451,6 +508,10 @@ export const workflowTools = [
451
508
  required: ['workflowId'],
452
509
  },
453
510
  handler: async (input) => {
511
+ // Validate user-provided input (#1425)
512
+ const vId = validateIdentifier(input.workflowId, 'workflowId');
513
+ if (!vId.valid)
514
+ return { success: false, error: vId.error };
454
515
  const store = loadWorkflowStore();
455
516
  const workflowId = input.workflowId;
456
517
  if (!store.workflows[workflowId]) {
@@ -485,6 +546,27 @@ export const workflowTools = [
485
546
  required: ['action'],
486
547
  },
487
548
  handler: async (input) => {
549
+ // Validate user-provided input (#1425)
550
+ if (input.workflowId) {
551
+ const v = validateIdentifier(input.workflowId, 'workflowId');
552
+ if (!v.valid)
553
+ return { success: false, error: v.error };
554
+ }
555
+ if (input.templateId) {
556
+ const v = validateIdentifier(input.templateId, 'templateId');
557
+ if (!v.valid)
558
+ return { success: false, error: v.error };
559
+ }
560
+ if (input.templateName) {
561
+ const v = validateText(input.templateName, 'templateName', 256);
562
+ if (!v.valid)
563
+ return { success: false, error: v.error };
564
+ }
565
+ if (input.newName) {
566
+ const v = validateText(input.newName, 'newName', 256);
567
+ if (!v.valid)
568
+ return { success: false, error: v.error };
569
+ }
488
570
  const store = loadWorkflowStore();
489
571
  const action = input.action;
490
572
  if (action === 'save') {
@@ -157,7 +157,9 @@ declare class LocalReasoningBank {
157
157
  persistence?: boolean;
158
158
  });
159
159
  /**
160
- * Load patterns from disk
160
+ * Load patterns from disk, deduplicating by content.
161
+ * When multiple patterns share identical content, keeps the one with
162
+ * highest confidence (ties broken by most recent lastUsedAt).
161
163
  */
162
164
  private loadFromDisk;
163
165
  /**
@@ -170,6 +172,9 @@ declare class LocalReasoningBank {
170
172
  flushToDisk(): void;
171
173
  /**
172
174
  * Store a pattern - O(1)
175
+ * Deduplicates by content: if a pattern with the same content already
176
+ * exists, the existing entry is updated (bumped usageCount, higher
177
+ * confidence wins, refreshed lastUsedAt) instead of adding a duplicate.
173
178
  */
174
179
  store(pattern: Omit<StoredPattern, 'usageCount' | 'createdAt' | 'lastUsedAt'> & Partial<StoredPattern>): void;
175
180
  /**
@@ -327,7 +327,9 @@ class LocalReasoningBank {
327
327
  }
328
328
  }
329
329
  /**
330
- * Load patterns from disk
330
+ * Load patterns from disk, deduplicating by content.
331
+ * When multiple patterns share identical content, keeps the one with
332
+ * highest confidence (ties broken by most recent lastUsedAt).
331
333
  */
332
334
  loadFromDisk() {
333
335
  try {
@@ -335,10 +337,41 @@ class LocalReasoningBank {
335
337
  if (existsSync(path)) {
336
338
  const data = JSON.parse(readFileSync(path, 'utf-8'));
337
339
  if (Array.isArray(data)) {
340
+ const totalLoaded = data.length;
341
+ // Group by content to deduplicate
342
+ const byContent = new Map();
338
343
  for (const pattern of data) {
344
+ const key = pattern.content;
345
+ const existing = byContent.get(key);
346
+ if (!existing) {
347
+ byContent.set(key, pattern);
348
+ }
349
+ else {
350
+ // Keep the one with higher confidence; break ties by lastUsedAt
351
+ if (pattern.confidence > existing.confidence ||
352
+ (pattern.confidence === existing.confidence &&
353
+ (pattern.lastUsedAt ?? 0) > (existing.lastUsedAt ?? 0))) {
354
+ // Merge: adopt the higher usageCount sum
355
+ pattern.usageCount = (pattern.usageCount ?? 0) + (existing.usageCount ?? 0);
356
+ byContent.set(key, pattern);
357
+ }
358
+ else {
359
+ existing.usageCount = (existing.usageCount ?? 0) + (pattern.usageCount ?? 0);
360
+ }
361
+ }
362
+ }
363
+ // Populate the bank from deduplicated entries
364
+ for (const pattern of byContent.values()) {
339
365
  this.patterns.set(pattern.id, pattern);
340
366
  this.patternList.push(pattern);
341
367
  }
368
+ const removed = totalLoaded - byContent.size;
369
+ if (removed > 0) {
370
+ console.log(`Deduplicated ${removed} patterns (${byContent.size} unique)`);
371
+ // Persist the compacted set immediately so the file shrinks on disk
372
+ this.dirty = true;
373
+ this.flushToDisk();
374
+ }
342
375
  }
343
376
  }
344
377
  }
@@ -380,6 +413,9 @@ class LocalReasoningBank {
380
413
  }
381
414
  /**
382
415
  * Store a pattern - O(1)
416
+ * Deduplicates by content: if a pattern with the same content already
417
+ * exists, the existing entry is updated (bumped usageCount, higher
418
+ * confidence wins, refreshed lastUsedAt) instead of adding a duplicate.
383
419
  */
384
420
  store(pattern) {
385
421
  const now = Date.now();
@@ -401,6 +437,20 @@ class LocalReasoningBank {
401
437
  }
402
438
  }
403
439
  else {
440
+ // Check for content-duplicate before inserting a new entry
441
+ const contentDupe = this.patternList.find(p => p.content === pattern.content);
442
+ if (contentDupe) {
443
+ // Merge into the existing pattern instead of adding a duplicate
444
+ contentDupe.usageCount++;
445
+ contentDupe.lastUsedAt = now;
446
+ if (stored.confidence > contentDupe.confidence) {
447
+ contentDupe.confidence = stored.confidence;
448
+ }
449
+ // Keep the Map in sync with the mutated object
450
+ this.patterns.set(contentDupe.id, contentDupe);
451
+ this.saveToDisk();
452
+ return;
453
+ }
404
454
  // Evict oldest if at capacity
405
455
  if (this.patterns.size >= this.maxSize) {
406
456
  const oldest = this.patternList.shift();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.5.69",
3
+ "version": "3.5.70",
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",