/** * handStore.ts - Hand, Trigger, and Approval management store * * Extracted from gatewayStore.ts for Phase 11 Store Refactoring. * Manages OpenFang Hands, Triggers, and Approval workflows. */ import { create } from 'zustand'; import type { GatewayClient } from '../lib/gateway-client'; // === Re-exported Types (from gatewayStore for compatibility) === export interface HandRequirement { description: string; met: boolean; details?: string; } export interface Hand { id: string; name: string; description: string; status: 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed'; currentRunId?: string; requirements_met?: boolean; category?: string; icon?: string; provider?: string; model?: string; requirements?: HandRequirement[]; tools?: string[]; metrics?: string[]; toolCount?: number; metricCount?: number; } export interface HandRun { runId: string; status: string; startedAt: string; completedAt?: string; result?: unknown; error?: string; } export interface Trigger { id: string; type: string; enabled: boolean; } export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired'; export interface Approval { id: string; handName: string; runId?: string; status: ApprovalStatus; requestedAt: string; requestedBy?: string; reason?: string; action?: string; params?: Record; respondedAt?: string; respondedBy?: string; responseReason?: string; } // === Raw API Response Types (for mapping) === interface RawHandRequirement { description?: string; name?: string; met?: boolean; satisfied?: boolean; details?: string; hint?: string; } interface RawHandRun { runId?: string; run_id?: string; id?: string; status?: string; startedAt?: string; started_at?: string; created_at?: string; completedAt?: string; completed_at?: string; finished_at?: string; result?: unknown; output?: unknown; error?: string; message?: string; } interface RawApproval { id?: string; approvalId?: string; approval_id?: string; type?: string; request_type?: string; handId?: string; hand_id?: string; hand_name?: string; handName?: string; run_id?: string; runId?: string; requester?: string; requested_by?: string; requestedAt?: string; requested_at?: string; status?: string; reason?: string; description?: string; action?: string; params?: Record; responded_at?: string; respondedAt?: string; responded_by?: string; respondedBy?: string; response_reason?: string; responseReason?: string; } // === Store Interface === interface HandClient { listHands: () => Promise<{ hands?: Array> } | null>; getHand: (name: string) => Promise | null>; listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>; triggerHand: (name: string, params?: Record) => Promise<{ runId?: string; status?: string } | null>; approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise; cancelHand: (name: string, runId: string) => Promise; listTriggers: () => Promise<{ triggers?: Trigger[] } | null>; getTrigger: (id: string) => Promise; createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise<{ id?: string } | null>; updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise; deleteTrigger: (id: string) => Promise; listApprovals: (status?: ApprovalStatus) => Promise<{ approvals?: RawApproval[] } | null>; respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise; } interface HandStore { // State hands: Hand[]; handRuns: Record; triggers: Trigger[]; approvals: Approval[]; isLoading: boolean; error: string | null; // Client reference (injected via setHandStoreClient) client: HandClient | null; // Actions setHandStoreClient: (client: HandClient) => void; loadHands: () => Promise; getHandDetails: (name: string) => Promise; triggerHand: (name: string, params?: Record) => Promise; loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise; approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise; cancelHand: (name: string, runId: string) => Promise; loadTriggers: () => Promise; getTrigger: (id: string) => Promise; createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise; updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise; deleteTrigger: (id: string) => Promise; loadApprovals: (status?: ApprovalStatus) => Promise; respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise; clearError: () => void; } export const useHandStore = create((set, get) => ({ // Initial State hands: [], handRuns: {}, triggers: [], approvals: [], isLoading: false, error: null, client: null, // Client injection setHandStoreClient: (client: HandClient) => { set({ client }); }, // === Hand Actions === loadHands: async () => { const client = get().client; if (!client) return; set({ isLoading: true }); try { const result = await client.listHands(); const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const; const hands: Hand[] = (result?.hands || []).map((h: Record) => { const status = validStatuses.includes(h.status as Hand['status']) ? h.status as Hand['status'] : (h.requirements_met ? 'idle' : 'setup_needed'); return { id: String(h.id || h.name), name: String(h.name || ''), description: String(h.description || ''), status, requirements_met: Boolean(h.requirements_met), category: h.category as string | undefined, icon: h.icon as string | undefined, toolCount: (h.tool_count as number) || ((h.tools as unknown[])?.length), metricCount: (h.metric_count as number) || ((h.metrics as unknown[])?.length), }; }); set({ hands, isLoading: false }); } catch { set({ isLoading: false }); } }, getHandDetails: async (name: string) => { const client = get().client; if (!client) return undefined; try { const result = await client.getHand(name); if (!result) return undefined; const getStringFromConfig = (key: string): string | undefined => { const val = (result.config as Record)?.[key]; return typeof val === 'string' ? val : undefined; }; const getArrayFromConfig = (key: string): string[] | undefined => { const val = (result.config as Record)?.[key]; return Array.isArray(val) ? val : undefined; }; const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const; const status = validStatuses.includes(result.status as Hand['status']) ? result.status as Hand['status'] : (result.requirements_met ? 'idle' : 'setup_needed'); const hand: Hand = { id: String(result.id || result.name || name), name: String(result.name || name), description: String(result.description || ''), status, requirements_met: Boolean(result.requirements_met), category: result.category as string | undefined, icon: result.icon as string | undefined, provider: (result.provider as string) || getStringFromConfig('provider'), model: (result.model as string) || getStringFromConfig('model'), requirements: ((result.requirements as RawHandRequirement[]) || []).map((r) => ({ description: r.description || r.name || String(r), met: r.met ?? r.satisfied ?? true, details: r.details || r.hint, })), tools: (result.tools as string[]) || getArrayFromConfig('tools'), metrics: (result.metrics as string[]) || getArrayFromConfig('metrics'), toolCount: (result.tool_count as number) || ((result.tools as unknown[])?.length) || 0, metricCount: (result.metric_count as number) || ((result.metrics as unknown[])?.length) || 0, }; set(state => ({ hands: state.hands.map(h => h.name === name ? { ...h, ...hand } : h), })); return hand; } catch { return undefined; } }, loadHandRuns: async (name: string, opts?: { limit?: number; offset?: number }) => { const client = get().client; if (!client) return []; try { const result = await client.listHandRuns(name, opts); const runs: HandRun[] = (result?.runs || []).map((r: RawHandRun) => ({ runId: r.runId || r.run_id || r.id || '', status: r.status || 'unknown', startedAt: r.startedAt || r.started_at || r.created_at || new Date().toISOString(), completedAt: r.completedAt || r.completed_at || r.finished_at, result: r.result || r.output, error: r.error || r.message, })); set(state => ({ handRuns: { ...state.handRuns, [name]: runs }, })); return runs; } catch { return []; } }, triggerHand: async (name: string, params?: Record) => { const client = get().client; if (!client) return undefined; try { const result = await client.triggerHand(name, params); if (!result) return undefined; const run: HandRun = { runId: result.runId || '', status: result.status || 'running', startedAt: new Date().toISOString(), }; // Add run to local state set(state => ({ handRuns: { ...state.handRuns, [name]: [run, ...(state.handRuns[name] || [])], }, })); // Refresh hands to update status await get().loadHands(); return run; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, approveHand: async (name: string, runId: string, approved: boolean, reason?: string) => { const client = get().client; if (!client) return; try { await client.approveHand(name, runId, approved, reason); await get().loadHands(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, cancelHand: async (name: string, runId: string) => { const client = get().client; if (!client) return; try { await client.cancelHand(name, runId); await get().loadHands(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, // === Trigger Actions === loadTriggers: async () => { const client = get().client; if (!client) return; try { const result = await client.listTriggers(); set({ triggers: result?.triggers || [] }); } catch { // ignore if triggers API not available } }, getTrigger: async (id: string) => { const client = get().client; if (!client) return undefined; try { const result = await client.getTrigger(id); if (!result) return undefined; return { id: result.id, type: result.type, enabled: result.enabled, } as Trigger; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, createTrigger: async (trigger) => { const client = get().client; if (!client) return undefined; try { const result = await client.createTrigger(trigger); if (!result?.id) return undefined; await get().loadTriggers(); return get().triggers.find(t => t.id === result.id); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateTrigger: async (id: string, updates) => { const client = get().client; if (!client) return undefined; try { await client.updateTrigger(id, updates); set(state => ({ triggers: state.triggers.map(t => t.id === id ? { ...t, ...updates } : t ), })); return get().triggers.find(t => t.id === id); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteTrigger: async (id: string) => { const client = get().client; if (!client) return; try { await client.deleteTrigger(id); set(state => ({ triggers: state.triggers.filter(t => t.id !== id), })); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, // === Approval Actions === loadApprovals: async (status?: ApprovalStatus) => { const client = get().client; if (!client) return; try { const result = await client.listApprovals(status); const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({ id: a.id || a.approval_id || a.approvalId || '', handName: a.hand_name || a.handName || '', runId: a.run_id || a.runId, status: (a.status || 'pending') as ApprovalStatus, requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(), requestedBy: a.requested_by || a.requester, reason: a.reason || a.description, action: a.action || 'execute', params: a.params, respondedAt: a.responded_at || a.respondedAt, respondedBy: a.responded_by || a.respondedBy, responseReason: a.response_reason || a.responseReason, })); set({ approvals }); } catch { // ignore if approvals API not available } }, respondToApproval: async (approvalId: string, approved: boolean, reason?: string) => { const client = get().client; if (!client) return; try { await client.respondToApproval(approvalId, approved, reason); await get().loadApprovals(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, clearError: () => set({ error: null }), })); /** * Helper to create a HandClient adapter from a GatewayClient. * Use this to inject the client into handStore. */ export function createHandClientFromGateway(client: GatewayClient): HandClient { return { listHands: () => client.listHands(), getHand: (name) => client.getHand(name), listHandRuns: (name, opts) => client.listHandRuns(name, opts), triggerHand: (name, params) => client.triggerHand(name, params), approveHand: (name, runId, approved, reason) => client.approveHand(name, runId, approved, reason), cancelHand: (name, runId) => client.cancelHand(name, runId), listTriggers: () => client.listTriggers(), getTrigger: (id) => client.getTrigger(id), createTrigger: (trigger) => client.createTrigger(trigger), updateTrigger: (id, updates) => client.updateTrigger(id, updates), deleteTrigger: (id) => client.deleteTrigger(id), listApprovals: (status) => client.listApprovals(status), respondToApproval: (approvalId, approved, reason) => client.respondToApproval(approvalId, approved, reason), }; }