ripwire
Gives Claude Code a structural map of a codebase before it reads the code, and checks its changes for blast radius, broken contracts, and which tests to run. Includes skills for C++ output formatting and bug localization from symptoms.
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
/**
* 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(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/**
* 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: enveloRead the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
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.
Gives Claude Code a structural map of a codebase before it reads the code, and checks its changes for blast radius, broken contracts, and which tests to run. Includes skills for C++ output formatting and bug localization from symptoms.
Cuts AI coding agent token costs through terse prose, YAGNI-first code style, and compressed tool output. Adds audit, review, and a terse dev mode for Claude Code and other agents.