Development
Hook
Hooks
What claudable runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Where it lives
- hooks/useCLI.tsGitHub
Read the script
/** * CLI Hook * Manages CLI configuration and status */ import { useState, useCallback, useEffect } from 'react'; import { CLIOption, CLIStatus, CLIPreference, CLI_OPTIONS } from '@/types/cli'; import { getDefaultModelForCli } from '@/lib/constants/cliModels'; import { DEFAULT_ACTIVE_CLI, normalizeModelForCli, sanitizeActiveCli } from '@/lib/utils/cliOptions'; interface UseCLIOptions { projectId: string; } const buildOptimisticStatus = (): CLIStatus => CLI_OPTIONS.reduce((acc, option) => { acc[option.id] = { installed: true, checking: false, available: true, configured: true, models: option.models?.map((model) => model.id), }; return acc; }, {} as CLIStatus); export const createCliStatusFallback = (): CLIStatus => buildOptimisticStatus(); export async function fetchCliStatusSnapshot(): Promise<CLIStatus> { const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ''; try { const response = await fetch(`${API_BASE}/api/settings/cli-status`); if (!response.ok) { throw new Error(`Failed to fetch CLI status: ${response.status}`); } const payload = (await response.json()) as CLIStatus; const optimistic = buildOptimisticStatus(); for (const option of CLI_OPTIONS) { const entry = payload[option.id]; if (!entry) { continue; } optimistic[option.id] = { ...optimistic[option.id], ...entry, checking: false, available: entry.available ?? entry.installed ?? optimistic[option.id]?.available ?? false, configured: entry.configured ?? entry.installed ?? optimistic[option.id]?.configured ?? false, models: entry.models ?? option.models?.map((model) => model.id), }; } return optimistic; } catch (error) { console.warn('Failed to fetch CLI status from API:', error); return buildOptimisticStatus(); } } export function useCLI({ projectId }: UseCLIOptions) { const [cliOptions, setCLIOptions] = useState<CLIOption[]>(() => CLI_OPTIONS.map((option) => ({ ...option }))); const [preference, setPreference] = useState<CLIPreference | null>(null); const [statuses, setStatuses] = useState<CLIStatus>(() => createCliStatusFallback()); const [isLoading, setIsLoading] = useState(false); const parsePreference = useCallback((payload: unknown): CLIPreference => { const data = payload as Record<string, unknown> | null | undefined; const preferredRaw = typeof data?.preferredCli === 'string' ? data.preferredCli : typeof data?.preferred_cli === 'string' ? data.preferred_cli : DEFAULT_ACTIVE_CLI; const preferredCli = sanitizeActiveCli(preferredRaw, DEFAULT_ACTIVE_CLI); const fallbackEnabled = typeof data?.fallbackEnabled === 'boolean' ? data.fallbackEnabled : typeof data?.fallback_enabled === 'boolean' ? data.fallback_enabled : false; const rawModel = typeof data?.selectedModel === 'string' ? data.selectedModel : typeof data?.selected_model === 'string' ? data.selected_model : undefined; const normalizedModel = normalizeModelForCli(preferredCli, rawModel, preferredCli); return { preferredCli, fallbackEnabled, selectedModel: normalizedModel || getDefaultModelForCli(preferredCli), }; }, []); // Load CLI preference const loadPreference = useCallback(async () => { try { const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ''; const response = await fetch(`${API_BASE}/api/projects/${projectId}`); if (!response.ok) { throw new Error('Failed to load project preferences'); } const payload = await response.json(); const project = payload?.data ?? payload ?? {}; setPreference(parsePreference(project)); } catch (error) { console.error('Failed to load CLI preference:', error); setPreference({ preferredCli: DEFAULT_ACTIVE_CLI, fallbackEnabled: false, selectedModel: getDefaultModelForCli(DEFAULT_ACTIVE_CLI), }); } }, [projectId, parsePreference]); const applyStatusToState = useCallback((status: CLIStatus) => { setStatuses(status); setCLIOptions( CLI_OPTIONS.map((option) => { const entry = status[option.id]; return { ...option, available: Boolean(entry?.available ?? entry?.installed ?? option.available), configured: Boolean(entry?.configured ?? entry?.installed ?? option.configured), }; }) ); }, []); // Load all CLI statuses const loadStatuses = useCallback(async () => { try { setIsLoading(true); const status = await fetchCliStatusSnapshot(); applyStatusToState(status); } finally { setIsLoading(false); } }, [applyStatusToState]); // Check single CLI status const checkCLIStatus = useCallback(async (cliType: string) => { const status = await fetchCliStatusSnapshot(); applyStatusToState(status); return status[cliType]; }, [applyStatusToState]); // Update CLI preference const updatePreference = useCallback(async (preferredCliInput: string) => { const sanitizedInput = sanitizeActiveCli(preferredCliInput, DEFAULT_ACTIVE_CLI); try { const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ''; const response = await fetch(`${API_BASE}/api/projects/${projectId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ preferredCli: sanitizedInput }), }); if (!response.ok) throw new Error('Failed to update CLI preference'); const payload = await response.json(); const project = payload?.data ?? payload ?? {}; const responseCli = sanitizeActiveCli( project.preferredCli ?? project.preferred_cli ?? sanitizedInput, DEFAULT_ACTIVE_CLI ); const rawSelected = project.selectedModel ?? project.selected_model; setPreference( - hooks/useUserRequests.tsGitHub
Read the script
import { useState, useCallback, useEffect, useRef } from 'react'; interface UseUserRequestsOptions { projectId: string; } interface ActiveRequestsResponse { hasActiveRequests: boolean; activeCount: number; } export function useUserRequests({ projectId }: UseUserRequestsOptions) { const [hasActiveRequests, setHasActiveRequests] = useState(false); const [activeCount, setActiveCount] = useState(0); const [isTabVisible, setIsTabVisible] = useState(true); // Default to true const intervalRef = useRef<NodeJS.Timeout | null>(null); const previousActiveState = useRef(false); const activeRequestIdsRef = useRef<Set<string>>(new Set()); const setFromActiveSet = useCallback(() => { const size = activeRequestIdsRef.current.size; setActiveCount(size); setHasActiveRequests(size > 0); }, []); const registerActiveRequest = useCallback((requestId: string | null | undefined) => { if (!requestId) return; const set = activeRequestIdsRef.current; const before = set.size; set.add(requestId); if (set.size !== before) { setFromActiveSet(); } }, [setFromActiveSet]); const unregisterActiveRequest = useCallback((requestId: string | null | undefined) => { if (!requestId) return; const set = activeRequestIdsRef.current; if (set.delete(requestId)) { setFromActiveSet(); } }, [setFromActiveSet]); // Track tab visibility state useEffect(() => { // Execute only on client side if (typeof document !== 'undefined') { setIsTabVisible(!document.hidden); const handleVisibilityChange = () => { setIsTabVisible(!document.hidden); }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; } }, []); // Query active request status from DB const checkActiveRequests = useCallback(async (options?: { force?: boolean }) => { if (!options?.force && !isTabVisible) return; // Stop polling if tab is inactive unless forced try { const apiBase = process.env.NEXT_PUBLIC_API_BASE ?? ''; const response = await fetch(`${apiBase}/api/chat/${projectId}/requests/active`, { cache: 'no-store', }); if (response.status === 404) { if (previousActiveState.current) { console.log('๐ [UserRequests] Active requests endpoint unavailable; assuming no active requests.'); } if (activeRequestIdsRef.current.size > 0) { activeRequestIdsRef.current.clear(); } setHasActiveRequests(false); setActiveCount(0); previousActiveState.current = false; return; } if (response.ok) { const data: ActiveRequestsResponse = await response.json(); if (!data.hasActiveRequests && activeRequestIdsRef.current.size > 0) { activeRequestIdsRef.current.clear(); } setHasActiveRequests(data.hasActiveRequests); setActiveCount(data.activeCount); // Log only when active state changes if (data.hasActiveRequests !== previousActiveState.current) { console.log(`๐ [UserRequests] Active requests: ${data.hasActiveRequests} (count: ${data.activeCount})`); previousActiveState.current = data.hasActiveRequests; } } else { // Treat other statuses as no-op without logging noisy errors return; } } catch (error) { if (activeRequestIdsRef.current.size > 0) { activeRequestIdsRef.current.clear(); setFromActiveSet(); } else { setHasActiveRequests(false); setActiveCount(0); } previousActiveState.current = false; if (process.env.NODE_ENV === 'development') { console.warn('[UserRequests] Failed to check active requests (network issue):', error); } } }, [projectId, isTabVisible, setFromActiveSet]); // Adaptive polling configuration useEffect(() => { // Stop polling if tab is inactive if (!isTabVisible) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } return; } // Determine polling interval based on active request status const pollInterval = hasActiveRequests ? 500 : 5000; // 0.5s vs 5s // Clean up existing polling if (intervalRef.current) { clearInterval(intervalRef.current); } // Check immediately once checkActiveRequests(); // Start new polling intervalRef.current = setInterval(() => checkActiveRequests(), pollInterval); if (process.env.NODE_ENV === 'development') { console.log(`โฑ๏ธ [UserRequests] Polling interval: ${pollInterval}ms (active: ${hasActiveRequests})`); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [hasActiveRequests, isTabVisible, checkActiveRequests]); // Clean up on component unmount useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } activeRequestIdsRef.current.clear(); }; }, []); // Placeholder functions for WebSocket events (maintaining existing interface) const createRequest = useCallback(( requestId: string, messageId: string, instruction: string, type: 'act' | 'chat' = 'act' ) => { registerActiveRequest(requestId); // Check status immediately via polling checkActiveRequests({ force: true }); console.log(`๐ [UserRequests] Created request: ${requestId}`); }, [checkActiveRequests, registerActiveRequest]); const startRequest = useCallback((requestId: string) => { registerActiveRequest(requestId); // Check status immediately via polling checkActiveRequests({ force: true }); console.log(`โถ๏ธ [UserRequests] Started request: ${requestId}`); }, [checkActiveRequests, registerActiveRequest]); const complet - hooks/useWebSocket.tsGitHub
Read the script
/** * WebSocket Hook * Manages WebSocket connection for real-time updates */ import { useEffect, useRef, useCallback, useState } from 'react'; import { WEBSOCKET_CONFIG } from '@/lib/config/constants'; import type { ChatMessage, RealtimeEvent, RealtimeStatus } from '@/types'; interface WebSocketOptions { projectId: string; onMessage?: (message: ChatMessage) => void; onStatus?: (status: string, data?: RealtimeStatus | Record<string, unknown>, requestId?: string) => void; onConnect?: () => void; onDisconnect?: () => void; onError?: (error: Error) => void; } export function useWebSocket({ projectId, onMessage, onStatus, onConnect, onDisconnect, onError }: WebSocketOptions) { const wsRef = useRef<WebSocket | null>(null); const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null); const heartbeatIntervalRef = useRef<NodeJS.Timeout | null>(null); const connectionAttemptsRef = useRef(0); const shouldReconnectRef = useRef(true); const manualCloseRef = useRef(false); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const handlersRef = useRef({ onMessage, onStatus, onConnect, onDisconnect, onError, }); useEffect(() => { handlersRef.current = { onMessage, onStatus, onConnect, onDisconnect, onError, }; }, [onMessage, onStatus, onConnect, onDisconnect, onError]); const clearHeartbeat = useCallback(() => { if (heartbeatIntervalRef.current) { clearInterval(heartbeatIntervalRef.current); heartbeatIntervalRef.current = null; } }, []); const startHeartbeat = useCallback(() => { clearHeartbeat(); heartbeatIntervalRef.current = setInterval(() => { const socket = wsRef.current; if (!socket || socket.readyState !== WebSocket.OPEN) { return; } try { socket.send('ping'); } catch (error) { console.error('Failed to send WebSocket ping:', error); } }, 25000); }, [clearHeartbeat]); const connect = useCallback(() => { const existing = wsRef.current; if (existing) { if ( existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING ) { return; } try { existing.close(1000, 'Reconnecting'); } catch { // Ignore close errors; we'll replace the socket below. } wsRef.current = null; } // Don't reconnect if we're intentionally disconnecting if (!shouldReconnectRef.current) { return; } const resolveWebSocketUrl = () => { const rawBase = process.env.NEXT_PUBLIC_WS_BASE?.trim() ?? ''; const endpoint = `/api/ws/${projectId}`; if (rawBase.length > 0) { const normalizedBase = rawBase.replace(/\/+$/, ''); return `${normalizedBase}${endpoint}`; } if (typeof window !== 'undefined') { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}${endpoint}`; } throw new Error('WebSocket base URL is not available'); }; const resolveHttpWarmupUrl = () => { const rawBase = process.env.NEXT_PUBLIC_WS_BASE?.trim() ?? ''; const endpoint = `/api/ws/${projectId}`; if (rawBase.length > 0) { // Convert ws/wss to http/https for the warm-up fetch const normalizedBase = rawBase .replace(/\/+$/, '') .replace(/^ws:\/\//i, 'http://') .replace(/^wss:\/\//i, 'https://'); return `${normalizedBase}${endpoint}`; } if (typeof window !== 'undefined') { const httpProto = window.location.protocol === 'https:' ? 'https:' : 'http:'; return `${httpProto}//${window.location.host}${endpoint}`; } throw new Error('HTTP base URL is not available'); }; const openWebSocket = () => { setIsConnecting(true); const ws = new WebSocket(resolveWebSocketUrl()); manualCloseRef.current = false; ws.onopen = () => { setIsConnected(true); setIsConnecting(false); connectionAttemptsRef.current = 0; startHeartbeat(); handlersRef.current.onConnect?.(); }; ws.onmessage = (event) => { if (event.data === 'pong') { return; } try { const envelope = JSON.parse(event.data) as RealtimeEvent; const { onMessage: handleMessage, onStatus: handleStatus, onError: handleError } = handlersRef.current; switch (envelope.type) { case 'message': if (envelope.data && handleMessage) { handleMessage(envelope.data); } break; case 'status': if (envelope.data && handleStatus) { handleStatus(envelope.data.status, envelope.data, envelope.data.requestId); } break; case 'error': { const message = envelope.error ?? 'Realtime bridge error'; const rawData = envelope.data as Record<string, unknown> | undefined; const requestId = (() => { if (!rawData) return undefined; const direct = rawData.requestId ?? rawData.request_id; return typeof direct === 'string' ? direct : undefined; })(); const payload: RealtimeStatus = { status: 'error', message, ...(requestId ? { requestId } : {}), }; handleStatus?.('error', payload, requestId); handleError?.(new Error(message)); break; } case 'connected': if (handleStatus) { const payload: RealtimeStatus = { status: 'connected', message: 'Realtime channel connected', sessionId: envelo
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withclaudable
Claudable is an open-source web builder that leverages local CLI agents, such as Claude Code, Codex, Gemini CLI, Qwen Code, and Cursor Agent, to build and deploy products effortlessly.
Get the whole plugin
Stats
4,044
Stars
618
Forks
Maintained
Maintenance
TypeScript
Language
MIT
License
4mo ago
Last commit
11mo ago
Created
Repo: opactorai/Claudable

