diff --git a/v3/@claude-flow/cli/__tests__/ruvector/agent-wasm.test.ts b/v3/@claude-flow/cli/__tests__/ruvector/agent-wasm.test.ts index e9868b478a..078fd3e0df 100644 --- a/v3/@claude-flow/cli/__tests__/ruvector/agent-wasm.test.ts +++ b/v3/@claude-flow/cli/__tests__/ruvector/agent-wasm.test.ts @@ -212,6 +212,22 @@ describe('agent-wasm integration', () => { it('throws for unknown agent', async () => { await expect(promptWasmAgent('nope', 'test')).rejects.toThrow('WASM agent not found'); }); + + it('sets model provider when ANTHROPIC_API_KEY is present', async () => { + const origKey = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'test-key'; + try { + const info = await createWasmAgent({ instructions: 'Test agent' }); + // The mock's set_model_provider should have been called + const entry = (await import('../../src/ruvector/agent-wasm.js')) as any; + const agent = entry.getWasmAgent(info.id); + expect(agent).not.toBeNull(); + terminateWasmAgent(info.id); + } finally { + if (origKey) process.env.ANTHROPIC_API_KEY = origKey; + else delete process.env.ANTHROPIC_API_KEY; + } + }); }); describe('tool execution', () => { diff --git a/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts index 4870e4f0f8..8232a0c10e 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts @@ -6,6 +6,16 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync } from 'fs'; import { dirname, join, resolve } from 'path'; import type { MCPTool } from './types.js'; +import { HeadlessWorkerExecutor, type HeadlessWorkerType, type HeadlessExecutionResult } from '../services/headless-worker-executor.js'; + +// Lazy-initialized headless executor singleton +let _executor: HeadlessWorkerExecutor | null = null; +function getExecutor(): HeadlessWorkerExecutor { + if (!_executor) { + _executor = new HeadlessWorkerExecutor(process.cwd()); + } + return _executor; +} // Real vector search functions - lazy loaded to avoid circular imports let searchEntriesFn: ((options: { @@ -3067,30 +3077,78 @@ export const hooksWorkerDispatch: MCPTool = { activeWorkers.set(workerId, worker); - // Update worker progress in background - if (background) { - setTimeout(() => { - const w = activeWorkers.get(workerId); - if (w) { - w.progress = 50; - w.phase = 'processing'; - } - }, 500); - - setTimeout(() => { - const w = activeWorkers.get(workerId); - if (w) { - w.progress = 100; - w.phase = 'completed'; - w.status = 'completed'; - w.completedAt = new Date(); + // Headless worker types that use Claude Code for real AI execution + const HEADLESS_TRIGGERS: Set = new Set([ + 'audit', 'optimize', 'testgaps', 'document', 'ultralearn', 'refactor', 'deepdive', 'predict', + ]); + + // Check if this trigger has a real headless executor and Claude Code is available + const isHeadlessTrigger = HEADLESS_TRIGGERS.has(trigger); + const executor = isHeadlessTrigger ? getExecutor() : null; + const claudeAvailable = executor ? await executor.isAvailable() : false; + + if (isHeadlessTrigger && claudeAvailable) { + // Real execution via HeadlessWorkerExecutor → spawn('claude', ['--print', ...]) + const contextOverride = context !== 'default' + ? { promptTemplate: `${context}\n\n` + (config as any).promptTemplate } + : undefined; + + if (background) { + // Non-blocking: execute in background and update worker status when done + executor!.execute(trigger as HeadlessWorkerType, contextOverride) + .then((result: HeadlessExecutionResult) => { + const w = activeWorkers.get(workerId); + if (w) { + w.progress = 100; + w.phase = 'completed'; + w.status = result.success ? 'completed' : 'failed'; + w.completedAt = new Date(); + (w as any).output = result.output; + (w as any).parsedOutput = result.parsedOutput; + (w as any).durationMs = result.durationMs; + (w as any).model = result.model; + } + }) + .catch((err: Error) => { + const w = activeWorkers.get(workerId); + if (w) { + w.progress = 100; + w.phase = 'failed'; + w.status = 'failed'; + w.completedAt = new Date(); + (w as any).error = err.message; + } + }); + } else { + // Blocking: wait for result + try { + const result = await executor!.execute(trigger as HeadlessWorkerType, contextOverride); + worker.progress = 100; + worker.phase = 'completed'; + worker.status = result.success ? 'completed' : 'failed'; + worker.completedAt = new Date(); + (worker as any).output = result.output; + (worker as any).parsedOutput = result.parsedOutput; + (worker as any).durationMs = result.durationMs; + } catch (err) { + worker.progress = 100; + worker.phase = 'failed'; + worker.status = 'failed'; + worker.completedAt = new Date(); + (worker as any).error = err instanceof Error ? err.message : String(err); } - }, 1500); + } } else { + // Local workers (map, consolidate, benchmark, preload) or Claude Code not available + // These run locally without AI — mark as completed immediately worker.progress = 100; worker.phase = 'completed'; worker.status = 'completed'; worker.completedAt = new Date(); + + if (isHeadlessTrigger && !claudeAvailable) { + (worker as any).warning = 'Claude Code CLI not available — worker ran in stub mode. Install with: npm install -g @anthropic-ai/claude-code'; + } } return { @@ -3104,9 +3162,11 @@ export const hooksWorkerDispatch: MCPTool = { estimatedDuration: config.estimatedDuration, capabilities: config.capabilities, }, - status: background ? 'dispatched' : 'completed', + status: background ? 'dispatched' : (worker.status === 'completed' ? 'completed' : 'failed'), background, timestamp: new Date().toISOString(), + ...(!(worker as any).output ? {} : { output: (worker as any).output }), + ...(!(worker as any).warning ? {} : { warning: (worker as any).warning }), }; }, }; diff --git a/v3/@claude-flow/cli/src/ruvector/agent-wasm.ts b/v3/@claude-flow/cli/src/ruvector/agent-wasm.ts index 1e8e12be20..f3dff38b4d 100644 --- a/v3/@claude-flow/cli/src/ruvector/agent-wasm.ts +++ b/v3/@claude-flow/cli/src/ruvector/agent-wasm.ts @@ -14,6 +14,78 @@ import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; +// ── Anthropic API Client ───────────────────────────────────── + +/** + * Create an Anthropic API model provider callback for WasmAgent. + * + * The callback receives a JSON string of messages from the WASM runtime, + * calls the Anthropic Messages API, and returns the response as JSON. + * + * Requires ANTHROPIC_API_KEY environment variable. + */ +function createAnthropicProvider(modelId: string, systemPrompt?: string): (messagesJson: string) => Promise { + return async (messagesJson: string): Promise => { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + return JSON.stringify({ + role: 'assistant', + content: 'Error: ANTHROPIC_API_KEY environment variable is not set. Set it to enable LLM inference for WASM agents.', + }); + } + + // Parse the model identifier (e.g., "anthropic:claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514") + const model = modelId.replace(/^anthropic:/, ''); + + let messages: Array<{ role: string; content: string }>; + try { + messages = JSON.parse(messagesJson); + } catch { + messages = [{ role: 'user', content: messagesJson }]; + } + + // Ensure messages is an array + if (!Array.isArray(messages)) { + messages = [{ role: 'user', content: String(messages) }]; + } + + const body: Record = { + model, + max_tokens: 4096, + messages, + }; + if (systemPrompt) { + body.system = systemPrompt; + } + + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const errText = await res.text().catch(() => 'unknown error'); + return JSON.stringify({ + role: 'assistant', + content: `API error (${res.status}): ${errText}`, + }); + } + + const data = await res.json() as { content?: Array<{ type: string; text?: string }> }; + const text = data.content + ?.filter((b: { type: string }) => b.type === 'text') + .map((b: { text?: string }) => b.text ?? '') + .join('') ?? ''; + + return JSON.stringify({ role: 'assistant', content: text }); + }; +} + // ── Types ──────────────────────────────────────────────────── export interface WasmAgentConfig { @@ -117,6 +189,15 @@ export async function createWasmAgent(config: WasmAgentConfig = {}): Promise