claude-flow 3.7.0-alpha.76 → 3.7.0-alpha.78

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.7.0-alpha.76",
3
+ "version": "3.7.0-alpha.78",
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",
@@ -145,7 +145,13 @@ const initAction = async (ctx) => {
145
145
  const full = ctx.flags.full;
146
146
  const skipClaude = ctx.flags['skip-claude'];
147
147
  const onlyClaude = ctx.flags['only-claude'];
148
- const noGlobal = ctx.flags['no-global'];
148
+ // #2098A the parser handles `--no-foo` by stripping the prefix and
149
+ // storing `flags.foo = false` (parser.ts:291-294), not by storing
150
+ // `flags['no-foo'] = true`. So `--no-global` lands as
151
+ // `ctx.flags.global === false`. The old read of `flags['no-global']`
152
+ // was always undefined and silently no-op'd — every user with the flag
153
+ // set still got `~/.claude/CLAUDE.md` modified. Read the real key.
154
+ const noGlobal = ctx.flags['no-global'] === true || ctx.flags['global'] === false;
149
155
  const allAgents = ctx.flags['all-agents'];
150
156
  const codexMode = ctx.flags.codex;
151
157
  const dualMode = ctx.flags.dual;
@@ -15,6 +15,7 @@ function behavioralRules() {
15
15
  - NEVER save working files or tests to root — use \`/src\`, \`/tests\`, \`/docs\`, \`/config\`, \`/scripts\`
16
16
  - ALWAYS read a file before editing it
17
17
  - NEVER commit secrets, credentials, or .env files
18
+ - NEVER add a \`Co-Authored-By\` trailer to user commits unless this project's \`.claude/settings.json\` has \`attribution.commit\` set (#2078). The Claude Code Bash tool may suggest one in its default commit-message template — ignore it. \`Co-Authored-By\` is semantic authorship attribution under git/GitHub convention; the tool is the facilitator, not a co-author.
18
19
  - Keep files under 500 lines
19
20
  - Validate input at system boundaries`;
20
21
  }
@@ -60,14 +60,31 @@ export async function callAnthropicMessages(input) {
60
60
  const explicitProvider = (process.env.RUFLO_PROVIDER || '').toLowerCase();
61
61
  const ollamaKey = process.env.OLLAMA_API_KEY;
62
62
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
63
- const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !!ollamaKey);
63
+ // #2042 OpenRouter is an OpenAI-compat endpoint that fronts dozens of
64
+ // providers. Reporter (@ummcke00) had `providers.openrouter.apiKey` in
65
+ // their config.yaml but agent_execute hardcoded Anthropic. Detect via
66
+ // explicit RUFLO_PROVIDER=openrouter OR presence of OPENROUTER_API_KEY
67
+ // when no Anthropic key is available (same precedence as the Ollama
68
+ // branch above).
69
+ const openrouterKey = process.env.OPENROUTER_API_KEY;
70
+ const useOpenRouter = explicitProvider === 'openrouter' || (!anthropicKey && !!openrouterKey);
71
+ const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !!ollamaKey && !openrouterKey);
72
+ if (useOpenRouter && openrouterKey) {
73
+ return callOpenAICompat({
74
+ ...input,
75
+ apiKey: openrouterKey,
76
+ baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api',
77
+ providerLabel: 'openrouter',
78
+ defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-3.5-sonnet',
79
+ });
80
+ }
64
81
  if (useOllama && ollamaKey) {
65
82
  return callOllamaCompat({ ...input, apiKey: ollamaKey });
66
83
  }
67
84
  if (!anthropicKey) {
68
85
  return {
69
86
  success: false,
70
- error: 'No LLM provider configured. Set ANTHROPIC_API_KEY (Tier-3) or OLLAMA_API_KEY (Tier-2 Ollama Cloud see issue #1725).',
87
+ error: 'No LLM provider configured. Set ANTHROPIC_API_KEY (Tier-3), OPENROUTER_API_KEY (#2042), or OLLAMA_API_KEY (Tier-2 — #1725).',
71
88
  };
72
89
  }
73
90
  const model = input.model || DEFAULT_ANTHROPIC_MODEL;
@@ -202,6 +219,88 @@ async function callOllamaCompat(input) {
202
219
  };
203
220
  }
204
221
  }
222
+ /**
223
+ * Generic OpenAI-compat caller for OpenRouter and other OpenAI-shaped
224
+ * endpoints. #2042 — reporter (@ummcke00) configured OpenRouter via
225
+ * config.yaml but agent_execute hardcoded the Anthropic fetch. This is
226
+ * the same shape as `callOllamaCompat` but routes to a configurable
227
+ * baseUrl + sends an OpenRouter-friendly default model when none is
228
+ * specified. Logical model names (haiku/sonnet/opus) pass through —
229
+ * OpenRouter accepts vendor-prefixed names like `anthropic/claude-3.5-sonnet`.
230
+ */
231
+ async function callOpenAICompat(input) {
232
+ const model = resolveOpenAICompatModel(input.model, input.defaultModel);
233
+ const startedAt = Date.now();
234
+ const base = input.baseUrl.replace(/\/+$/, '');
235
+ const url = `${base}/v1/chat/completions`;
236
+ try {
237
+ const controller = new AbortController();
238
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
239
+ const messages = [];
240
+ if (input.systemPrompt)
241
+ messages.push({ role: 'system', content: input.systemPrompt });
242
+ messages.push({ role: 'user', content: input.prompt });
243
+ const res = await fetch(url, {
244
+ method: 'POST',
245
+ headers: {
246
+ Authorization: `Bearer ${input.apiKey}`,
247
+ 'content-type': 'application/json',
248
+ // OpenRouter convention: identify the integrating app for analytics
249
+ // and rate-limit tiering. Harmless on other OpenAI-compat backends.
250
+ 'HTTP-Referer': 'https://github.com/ruvnet/ruflo',
251
+ 'X-Title': 'Ruflo',
252
+ },
253
+ body: JSON.stringify({
254
+ model,
255
+ max_tokens: input.maxTokens || 1024,
256
+ temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
257
+ messages,
258
+ }),
259
+ signal: controller.signal,
260
+ });
261
+ clearTimeout(timer);
262
+ if (!res.ok) {
263
+ const errText = await res.text().catch(() => '<unreadable error body>');
264
+ return { success: false, model, error: `${input.providerLabel} API error ${res.status}: ${errText.slice(0, 400)}` };
265
+ }
266
+ const data = await res.json();
267
+ const textOut = data.choices?.[0]?.message?.content ?? '';
268
+ const usage = data.usage ?? {};
269
+ return {
270
+ success: true,
271
+ model: data.model || model,
272
+ messageId: data.id,
273
+ stopReason: data.choices?.[0]?.finish_reason ?? 'end_turn',
274
+ output: textOut,
275
+ usage: {
276
+ inputTokens: usage.prompt_tokens ?? 0,
277
+ outputTokens: usage.completion_tokens ?? 0,
278
+ totalTokens: usage.total_tokens ?? 0,
279
+ },
280
+ durationMs: Date.now() - startedAt,
281
+ };
282
+ }
283
+ catch (err) {
284
+ return {
285
+ success: false,
286
+ model,
287
+ error: err instanceof Error ? err.message : String(err),
288
+ durationMs: Date.now() - startedAt,
289
+ };
290
+ }
291
+ }
292
+ function resolveOpenAICompatModel(input, fallback) {
293
+ if (!input)
294
+ return fallback;
295
+ // Logical Claude names → OpenRouter Anthropic-vendored names
296
+ if (input === 'haiku')
297
+ return 'anthropic/claude-3.5-haiku';
298
+ if (input === 'sonnet' || input === 'inherit')
299
+ return 'anthropic/claude-3.5-sonnet';
300
+ if (input === 'opus')
301
+ return 'anthropic/claude-3-opus';
302
+ return input;
303
+ }
205
304
  function resolveOllamaModel(input) {
206
305
  const DEFAULT = 'gpt-oss:120b-cloud';
207
306
  if (!input)
@@ -232,15 +331,6 @@ export function resolveAnthropicModel(input) {
232
331
  return input;
233
332
  }
234
333
  export async function executeAgentTask(input) {
235
- const apiKey = process.env.ANTHROPIC_API_KEY;
236
- if (!apiKey) {
237
- return {
238
- success: false,
239
- agentId: input.agentId,
240
- error: 'ANTHROPIC_API_KEY not set in environment',
241
- remediation: 'Set the env var and re-run. The key is read at call time.',
242
- };
243
- }
244
334
  const store = loadAgentStore();
245
335
  const agent = store.agents[input.agentId];
246
336
  if (!agent)
@@ -256,73 +346,50 @@ export async function executeAgentTask(input) {
256
346
  agent.taskCount = (agent.taskCount || 0) + 1;
257
347
  saveAgentStore(store);
258
348
  const startedAt = Date.now();
259
- try {
260
- const controller = new AbortController();
261
- const timeoutMs = input.timeoutMs || 60000;
262
- const timer = setTimeout(() => controller.abort(), timeoutMs);
263
- const res = await fetch('https://api.anthropic.com/v1/messages', {
264
- method: 'POST',
265
- headers: {
266
- 'x-api-key': apiKey,
267
- 'anthropic-version': '2023-06-01',
268
- 'content-type': 'application/json',
269
- },
270
- body: JSON.stringify({
271
- model: anthropicModel,
272
- max_tokens: input.maxTokens || 1024,
273
- temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
274
- system: systemPrompt,
275
- messages: [{ role: 'user', content: input.prompt }],
276
- }),
277
- signal: controller.signal,
278
- });
279
- clearTimeout(timer);
280
- if (!res.ok) {
281
- const errText = await res.text().catch(() => '<unreadable error body>');
282
- agent.status = 'idle';
283
- saveAgentStore(store);
284
- return {
285
- success: false,
286
- agentId: input.agentId,
287
- model: anthropicModel,
288
- error: `Anthropic API error ${res.status}: ${errText.slice(0, 400)}`,
289
- };
290
- }
291
- const data = await res.json();
292
- const textOut = data.content
293
- .filter(c => c.type === 'text' && typeof c.text === 'string')
294
- .map(c => c.text)
295
- .join('');
296
- const result = {
349
+ // #2042 — delegate to callAnthropicMessages so the v3 provider router
350
+ // (Anthropic / Ollama / OpenRouter) governs which backend is hit. The
351
+ // previous inline `fetch('https://api.anthropic.com/...')` bypassed
352
+ // the router entirely and forced an ANTHROPIC_API_KEY error for every
353
+ // non-Anthropic deployment. Reporter (@ummcke00) had OpenRouter
354
+ // configured but the bypass made the agent unreachable.
355
+ const result = await callAnthropicMessages({
356
+ model: anthropicModel,
357
+ prompt: input.prompt,
358
+ systemPrompt,
359
+ maxTokens: input.maxTokens,
360
+ temperature: input.temperature,
361
+ timeoutMs: input.timeoutMs,
362
+ });
363
+ agent.status = 'idle';
364
+ if (result.success) {
365
+ const out = {
297
366
  success: true,
298
367
  agentId: input.agentId,
299
- messageId: data.id,
300
- model: data.model,
301
- stopReason: data.stop_reason,
302
- output: textOut,
303
- usage: {
304
- inputTokens: data.usage.input_tokens,
305
- outputTokens: data.usage.output_tokens,
306
- totalTokens: data.usage.input_tokens + data.usage.output_tokens,
307
- },
308
- durationMs: Date.now() - startedAt,
368
+ messageId: result.messageId,
369
+ model: result.model,
370
+ stopReason: result.stopReason,
371
+ output: result.output,
372
+ usage: result.usage,
373
+ durationMs: result.durationMs ?? Date.now() - startedAt,
309
374
  };
310
- agent.status = 'idle';
311
- agent.lastResult = result;
312
- saveAgentStore(store);
313
- return result;
314
- }
315
- catch (err) {
316
- agent.status = 'idle';
375
+ agent.lastResult = out;
317
376
  saveAgentStore(store);
318
- const msg = err instanceof Error ? err.message : String(err);
319
- return {
320
- success: false,
321
- agentId: input.agentId,
322
- model: anthropicModel,
323
- error: `agent_execute failed: ${msg}`,
324
- durationMs: Date.now() - startedAt,
325
- };
377
+ return out;
326
378
  }
379
+ saveAgentStore(store);
380
+ // No-provider-configured error → surface the same actionable message
381
+ // the router built, with a #2042-aware remediation pointer.
382
+ const noProvider = (result.error || '').includes('No LLM provider configured');
383
+ return {
384
+ success: false,
385
+ agentId: input.agentId,
386
+ model: anthropicModel,
387
+ error: result.error || 'agent_execute failed',
388
+ durationMs: result.durationMs ?? Date.now() - startedAt,
389
+ ...(noProvider && {
390
+ remediation: 'Set one of ANTHROPIC_API_KEY, OPENROUTER_API_KEY (+ optional OPENROUTER_BASE_URL), or OLLAMA_API_KEY. ' +
391
+ 'Or set RUFLO_PROVIDER=openrouter|ollama to force a specific provider.',
392
+ }),
393
+ };
327
394
  }
328
395
  //# sourceMappingURL=agent-execute-core.js.map
@@ -174,6 +174,10 @@ export const agentTools = [
174
174
  properties: {
175
175
  agentType: { type: 'string', description: 'Type of agent to spawn' },
176
176
  agentId: { type: 'string', description: 'Optional custom agent ID' },
177
+ // #2085 — accept swarmId so spawned agents register in the
178
+ // swarm.agents array that swarm_status reports. Omit to register
179
+ // with the most-recently-created swarm.
180
+ swarmId: { type: 'string', description: 'Optional swarm to register the agent with (defaults to most-recent swarm)' },
177
181
  config: { type: 'object', description: 'Agent configuration' },
178
182
  domain: { type: 'string', description: 'Agent domain' },
179
183
  model: {
@@ -217,6 +221,33 @@ export const agentTools = [
217
221
  };
218
222
  store.agents[agentId] = agent;
219
223
  saveAgentStore(store);
224
+ // #2085 — also push to the swarm store's agents array so that
225
+ // swarm_status reports the new agent. Without this, agent_spawn
226
+ // and swarm_status read/write separate stores and agents added
227
+ // post-init never show up in swarm_status.agents — confirmed for
228
+ // all topologies (hierarchical, mesh, etc.).
229
+ try {
230
+ const { loadSwarmStore: _loadSwarmStore, saveSwarmStore: _saveSwarmStore } = await import('./swarm-tools.js');
231
+ const swarmStore = _loadSwarmStore();
232
+ let targetSwarmId = input.swarmId || '';
233
+ if (!targetSwarmId) {
234
+ // Default to the most-recently-created swarm.
235
+ const all = Object.values(swarmStore.swarms);
236
+ const latest = all.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
237
+ targetSwarmId = latest?.swarmId || '';
238
+ }
239
+ if (targetSwarmId && swarmStore.swarms[targetSwarmId]) {
240
+ const swarm = swarmStore.swarms[targetSwarmId];
241
+ if (!Array.isArray(swarm.agents))
242
+ swarm.agents = [];
243
+ // Idempotent — don't duplicate if agent_spawn is retried.
244
+ if (!swarm.agents.includes(agentId)) {
245
+ swarm.agents.push(agentId);
246
+ _saveSwarmStore(swarmStore);
247
+ }
248
+ }
249
+ }
250
+ catch { /* swarm store unavailable — agent still registered globally */ }
220
251
  // Record agent in graph database (ADR-087, best-effort)
221
252
  try {
222
253
  const { addNode } = await import('../ruvector/graph-backend.js');
@@ -5,5 +5,33 @@
5
5
  * Replaces previous stub implementations with real state tracking.
6
6
  */
7
7
  import { type MCPTool } from './types.js';
8
+ interface SwarmState {
9
+ swarmId: string;
10
+ topology: string;
11
+ maxAgents: number;
12
+ status: 'initializing' | 'running' | 'paused' | 'shutting_down' | 'terminated';
13
+ agents: string[];
14
+ tasks: string[];
15
+ config: Record<string, unknown>;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ /**
19
+ * #1799 — process that initialized this swarm. Used by reconciliation
20
+ * on `loadSwarmStore()` to detect orphan entries whose host process has
21
+ * already exited (common on Windows where backgrounded daemons don't
22
+ * always survive shell exit). Optional for backward compat with
23
+ * pre-#1799 stores.
24
+ */
25
+ pid?: number;
26
+ /** Reason set when status was forced to 'terminated' by reconciliation. */
27
+ terminationReason?: string;
28
+ }
29
+ interface SwarmStore {
30
+ swarms: Record<string, SwarmState>;
31
+ version: string;
32
+ }
33
+ export declare function loadSwarmStore(): SwarmStore;
34
+ export declare function saveSwarmStore(store: SwarmStore): void;
8
35
  export declare const swarmTools: MCPTool[];
36
+ export {};
9
37
  //# sourceMappingURL=swarm-tools.d.ts.map
@@ -78,7 +78,9 @@ function reconcileOrphanSwarms(store) {
78
78
  }
79
79
  return reconciled;
80
80
  }
81
- function loadSwarmStore() {
81
+ // #2085 — exported so `agent-tools.ts agent_spawn` can push into
82
+ // `swarm.agents` (the field `swarm_status` reads).
83
+ export function loadSwarmStore() {
82
84
  let store = { swarms: {}, version: '3.0.0' };
83
85
  try {
84
86
  const path = getSwarmStatePath();
@@ -99,7 +101,7 @@ function loadSwarmStore() {
99
101
  }
100
102
  return store;
101
103
  }
102
- function saveSwarmStore(store) {
104
+ export function saveSwarmStore(store) {
103
105
  ensureSwarmDir();
104
106
  writeFileSync(getSwarmStatePath(), JSON.stringify(store, null, 2), 'utf-8');
105
107
  }
@@ -859,11 +859,19 @@ Analyze the above codebase context and provide your response following the forma
859
859
  // writes the prompt and closes stdin atomically — the EOF still
860
860
  // unblocks `claude --print` (the original concern in #1395) but no
861
861
  // shell tokenization touches the prompt.
862
+ // #2098B / #2093 — `claude --print` can spawn grandchildren (MCP
863
+ // server stdio bridges, plugin tools). When the head times out a
864
+ // plain `child.kill()` only signals the head; grandchildren get
865
+ // reparented to init and survive — the symptom @maxstefanakis1114
866
+ // diagnosed as a 5-second redispatch + subprocess-table growth.
867
+ // `detached: true` puts the child in its own process group so we
868
+ // can signal the whole tree with `process.kill(-pid, sig)`.
862
869
  const child = spawn('claude', ['--print'], {
863
870
  cwd: this.projectRoot,
864
871
  env,
865
872
  stdio: ['pipe', 'pipe', 'pipe'],
866
873
  windowsHide: true, // Prevent phantom console windows on Windows
874
+ detached: process.platform !== 'win32',
867
875
  });
868
876
  try {
869
877
  child.stdin?.end(prompt);
@@ -872,14 +880,29 @@ Analyze the above codebase context and provide your response following the forma
872
880
  // stdin already closed (e.g. spawn failed) — `error` handler below
873
881
  // will surface the real cause.
874
882
  }
883
+ // Kill the whole process group on POSIX, fall back to the child on
884
+ // Windows (where setsid-style detach isn't available the same way).
885
+ const killTree = (signal) => {
886
+ if (process.platform !== 'win32' && typeof child.pid === 'number') {
887
+ try {
888
+ process.kill(-child.pid, signal);
889
+ return;
890
+ }
891
+ catch { /* fall through */ }
892
+ }
893
+ try {
894
+ child.kill(signal);
895
+ }
896
+ catch { /* already dead */ }
897
+ };
875
898
  // Setup timeout
876
899
  const timeoutHandle = setTimeout(() => {
877
900
  if (this.processPool.has(options.executionId)) {
878
- child.kill('SIGTERM');
901
+ killTree('SIGTERM');
879
902
  // Give it a moment to terminate gracefully
880
903
  setTimeout(() => {
881
904
  if (!child.killed) {
882
- child.kill('SIGKILL');
905
+ killTree('SIGKILL');
883
906
  }
884
907
  }, 5000);
885
908
  }
@@ -947,7 +970,7 @@ Analyze the above codebase context and provide your response following the forma
947
970
  if (!this.processPool.has(options.executionId))
948
971
  return;
949
972
  resolved = true;
950
- child.kill('SIGTERM');
973
+ killTree('SIGTERM');
951
974
  cleanup();
952
975
  resolve({
953
976
  success: false,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.7.0-alpha.76",
3
+ "version": "3.7.0-alpha.78",
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",