Skip to content
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.

From plugin
claudable
4k3 hooks

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