Skip to content
Automation
Hook

Hooks

What ai-maestro runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
ai-maestro
74415 hooks1 MCP

Where it lives

  • hooks/useAgentPlayback.tsGitHub
    Read the script
    'use client'
    
    import { useState, useEffect, useCallback, useRef } from 'react'
    import type { PlaybackState, PlaybackControl } from '@/types/playback'
    
    /**
     * Default playback speed
     */
    const DEFAULT_PLAYBACK_SPEED = 1.0
    
    /**
     * Valid playback speed options
     */
    export const PLAYBACK_SPEEDS = [0.5, 1.0, 1.5, 2.0] as const
    
    /**
     * Hook for managing agent transcript playback
     *
     * Handles playback state management, controls (play/pause/seek/speed),
     * and message loading for conversation transcript playback.
     *
     * @param agentId - Agent ID to manage playback for
     * @param sessionId - Session ID to play back (optional for cross-session)
     */
    export function useAgentPlayback(
      agentId: string,
      sessionId?: string
    ) {
      const [state, setState] = useState<PlaybackState | null>(null)
      const [loading, setLoading] = useState(true)
      const [error, setError] = useState<Error | null>(null)
      const [messages, setMessages] = useState<Array<{
        role: 'user' | 'assistant' | 'system'
        content: string
        timestamp?: number
        metadata?: Record<string, any>
      }>>([])
      const [autoSaveEnabled, setAutoSaveEnabled] = useState(true)
    
      // Refs for timers and tracking
      const autoSaveTimerRef = useRef<NodeJS.Timeout>()
      const isMountedRef = useRef(true)
    
      /**
       * Load playback state from API
       */
      const loadPlaybackState = useCallback(async () => {
        setLoading(true)
        setError(null)
    
        try {
          console.log(`[useAgentPlayback] Loading state for agent ${agentId}, session ${sessionId || 'all'}`)
    
          const queryParams = sessionId ? `?sessionId=${sessionId}` : ''
          const response = await fetch(`/api/agents/${agentId}/playback${queryParams}`)
    
          if (!response.ok) {
            const errorData = await response.json()
            throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`)
          }
    
          const data = await response.json()
    
          if (!isMountedRef.current) return
    
          if (data.success && data.playbackState) {
            setState(data.playbackState)
            console.log(`[useAgentPlayback] Loaded state: playing=${data.playbackState.isPlaying}, position=${data.playbackState.currentMessageIndex}`)
          } else {
            // Initialize default state if none exists
            setState({
              agentId,
              sessionId,
              isPlaying: false,
              currentMessageIndex: 0,
              speed: DEFAULT_PLAYBACK_SPEED,
              totalMessages: 0,
              createdAt: Date.now(),
              updatedAt: Date.now()
            })
          }
        } catch (err) {
          if (!isMountedRef.current) return
    
          console.error('[useAgentPlayback] Failed to load state:', err)
          setError(err instanceof Error ? err : new Error('Failed to load playback state'))
          
          // Set default state on error
          setState({
            agentId,
            sessionId,
            isPlaying: false,
            currentMessageIndex: 0,
            speed: DEFAULT_PLAYBACK_SPEED,
            totalMessages: 0,
            createdAt: Date.now(),
            updatedAt: Date.now()
          })
        } finally {
          if (isMountedRef.current) {
            setLoading(false)
          }
        }
      }, [agentId, sessionId])
    
      /**
       * Update playback state via API
       */
      const updatePlaybackState = useCallback(async (action: PlaybackControl) => {
        if (!state) return
    
        try {
          console.log(`[useAgentPlayback] Updating state: ${action.action}`, action.value ?? '')
    
          const response = await fetch(`/api/agents/${agentId}/playback`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              action: action.action,
              value: action.value,
              sessionId
            })
          })
    
          if (!response.ok) {
            const errorData = await response.json()
            throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`)
          }
    
          const data = await response.json()
    
          if (!isMountedRef.current) return
    
          if (data.success && data.playbackState) {
            setState(data.playbackState)
          }
        } catch (err) {
          if (!isMountedRef.current) return
    
          console.error('[useAgentPlayback] Failed to update state:', err)
          setError(err instanceof Error ? err : new Error('Failed to update playback state'))
        }
      }, [agentId, sessionId, state])
    
      /**
       * Start playback
       */
      const start = useCallback(() => {
        if (!state || state.isPlaying) return
        updatePlaybackState({ action: 'play' })
      }, [state, updatePlaybackState])
    
      /**
       * Pause playback
       */
      const pause = useCallback(() => {
        if (!state || !state.isPlaying) return
        updatePlaybackState({ action: 'pause' })
      }, [state, updatePlaybackState])
    
      /**
       * Toggle play/pause
       */
      const toggle = useCallback(() => {
        if (!state) return
        if (state.isPlaying) {
          pause()
        } else {
          start()
        }
      }, [state, start, pause])
    
      /**
       * Seek to specific message index
       */
      const seek = useCallback((position: number) => {
        if (!state || position < 0) return
        const maxPosition = state.totalMessages !== undefined 
          ? Math.min(position, state.totalMessages - 1) 
          : position
        updatePlaybackState({ action: 'seek', value: maxPosition })
      }, [state, updatePlaybackState])
    
      /**
       * Set playback speed
       */
      const setSpeed = useCallback((speed: number) => {
        if (!state || speed < 0.5 || speed > 2.0) return
        updatePlaybackState({ action: 'setSpeed', value: speed })
      }, [state, updatePlaybackState])
    
      /**
       * Reset playback to beginning
       */
      const reset = useCallback(() => {
        updatePlaybackState({ action: 'reset' })
      }, [updatePlaybackState])
    
      /**
       * Move to next message
       */
      const next = useCallback(() => {
        if (!state) return
        const newPosition = state.currentMessageIndex + 1
        seek(newPosition)
      }, [state, seek])
    
      /**
       * Move to previous message
       */
      const previous = useCallback(() => {
        if (!state) return
        const newPosition = Math.max(0, state.currentMessage
  • hooks/useAgentSearch.tsGitHub
    Read the script
    'use client'
    
    import { useState, useEffect, useCallback, useRef } from 'react'
    import type { SearchQuery, SearchResult, HighlightedSearchResult } from '@/types/search'
    
    /**
     * Debounce delay for search queries (in milliseconds)
     */
    const SEARCH_DEBOUNCE_MS = 300
    
    /**
     * Interface for search results with highlighting
     */
    export interface SearchResults {
      results: HighlightedSearchResult[]
      total: number
      query: string
      highlights: string[]
      timestamp: number
    }
    
    /**
     * Hook for searching agent conversation data
     *
     * Provides debounced search, results management, and error handling
     * for searching across agent messages, conversations, and code.
     *
     * @param agentId - Agent ID to search within
     */
    export function useAgentSearch(agentId: string) {
      const [query, setQuery] = useState<string>('')
      const [results, setResults] = useState<SearchResults | null>(null)
      const [loading, setLoading] = useState(false)
      const [error, setError] = useState<Error | null>(null)
      const [debouncedQuery, setDebouncedQuery] = useState<string>('')
    
      // Ref to track debounce timer
      const debounceTimerRef = useRef<NodeJS.Timeout>()
    
      // Ref to track if component is mounted (for async operations)
      const isMountedRef = useRef(true)
    
      /**
       * Perform search with the current debounced query
       */
      const performSearch = useCallback(async (searchQuery: string) => {
        if (!searchQuery.trim()) {
          setResults(null)
          setLoading(false)
          setError(null)
          return
        }
    
        setLoading(true)
        setError(null)
    
        try {
          console.log(`[useAgentSearch] Searching for: "${searchQuery}" in agent ${agentId}`)
    
          const queryParams = new URLSearchParams({
            q: searchQuery
          })
    
          const response = await fetch(`/api/agents/${agentId}/search?${queryParams.toString()}`)
    
          if (!response.ok) {
            const errorData = await response.json()
            throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`)
          }
    
          const data = await response.json()
    
          if (!isMountedRef.current) return
    
          const searchResults: HighlightedSearchResult[] = data.results || []
          const searchTerms = extractSearchTerms(searchQuery)
          const highlightedResults = highlightResults(searchResults, searchTerms)
    
          setResults({
            results: highlightedResults,
            total: data.count || highlightedResults.length,
            query: searchQuery,
            highlights: searchTerms,
            timestamp: Date.now()
          })
    
          console.log(`[useAgentSearch] Found ${data.count} results for query "${searchQuery}"`)
        } catch (err) {
          if (!isMountedRef.current) return
    
          console.error('[useAgentSearch] Search failed:', err)
          setError(err instanceof Error ? err : new Error('Unknown search error'))
          setResults(null)
        } finally {
          if (isMountedRef.current) {
            setLoading(false)
          }
        }
      }, [agentId])
    
      /**
       * Update search query with debouncing
       */
      useEffect(() => {
        if (debounceTimerRef.current) {
          clearTimeout(debounceTimerRef.current)
        }
    
        debounceTimerRef.current = setTimeout(() => {
          setDebouncedQuery(query)
        }, SEARCH_DEBOUNCE_MS)
    
        return () => {
          if (debounceTimerRef.current) {
            clearTimeout(debounceTimerRef.current)
          }
        }
      }, [query])
    
      /**
       * Perform search when debounced query changes
       */
      useEffect(() => {
        performSearch(debouncedQuery)
      }, [debouncedQuery, performSearch])
    
      /**
       * Clear search results and query
       */
      const clearSearch = useCallback(() => {
        setQuery('')
        setResults(null)
        setError(null)
        setLoading(false)
      }, [])
    
      /**
       * Retry the last search
       */
      const retrySearch = useCallback(() => {
        if (debouncedQuery) {
          performSearch(debouncedQuery)
        }
      }, [debouncedQuery, performSearch])
    
      /**
       * Extract search terms from query for highlighting
       * Removes common stop words and punctuation
       */
      function extractSearchTerms(queryText: string): string[] {
        const words = queryText
          .toLowerCase()
          .replace(/[^\w\s]/g, '')
          .split(/\s+/)
          .filter(word => word.length > 2)
    
        const stopWords = new Set([
          'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of',
          'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during',
          'before', 'after', 'above', 'below', 'between', 'under', 'again'
        ])
    
        return words.filter(word => !stopWords.has(word))
      }
    
      /**
       * Add highlighting to search results
       */
      function highlightResults(
        searchResults: SearchResult[],
        highlightTerms: string[]
      ): HighlightedSearchResult[] {
        return searchResults.map((result) => {
          const text = result.text
          const highlightRanges: Array<{ start: number; end: number }> = []
    
          for (const term of highlightTerms) {
            const lowerText = text.toLowerCase()
            const lowerTerm = term.toLowerCase()
            let position = lowerText.indexOf(lowerTerm)
    
            while (position !== -1) {
              highlightRanges.push({ start: position, end: position + term.length })
              position = lowerText.indexOf(lowerTerm, position + term.length)
            }
          }
    
          highlightRanges.sort((a, b) => a.start - b.start)
    
          let highlightedText = ''
          let lastIndex = 0
    
          for (const range of highlightRanges) {
            highlightedText += text.substring(lastIndex, range.start)
            highlightedText += `<mark>${text.substring(range.start, range.end)}</mark>`
            lastIndex = range.end
          }
    
          highlightedText += text.substring(lastIndex)
    
          return {
            ...result,
            highlightedText,
            highlightRanges
          }
        })
      }
    
      return {
        // State
        query,
        results,
        loading,
        error,
    
        // Actions
        setQuery,
        clearSearch,
        retrySearch
      }
    }
    
  • hooks/useAgents.tsGitHub
    Read the script
    'use client'
    
    import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
    import type { Agent, AgentsApiResponse, AgentStats, AgentHostInfo } from '@/types/agent'
    import type { Host } from '@/types/host'
    import { useHosts } from './useHosts'
    import { cacheRemoteAgents, getCachedAgents } from '@/lib/agent-cache'
    
    const REFRESH_INTERVAL = 10000 // 10 seconds
    const SELF_FETCH_TIMEOUT = 8000 // 8 seconds for self host (tmux queries can be slow)
    const PEER_FETCH_TIMEOUT = 3000 // 3 seconds for peer hosts (fail fast, use cache)
    
    /**
     * Check if a host URL points to localhost (the machine running this dashboard)
     * Used client-side since os.hostname() isn't available in browser
     */
    function isLocalhostUrl(url: string | undefined): boolean {
      if (!url) return true
      const lowered = url.toLowerCase()
      return lowered.includes('localhost') || lowered.includes('127.0.0.1')
    }
    
    /**
     * Aggregated stats across all hosts
     */
    interface AggregatedStats {
      total: number
      online: number
      offline: number
      orphans: number
      newlyRegistered: number
      cached: number // Number of agents loaded from cache
    }
    
    /**
     * Host fetch result
     */
    interface HostFetchResult {
      hostId: string
      success: boolean
      response?: AgentsApiResponse
      error?: Error
      fromCache?: boolean
    }
    
    /**
     * Fetch agents from a specific host
     */
    async function fetchHostAgents(host: Host): Promise<HostFetchResult> {
      const isSelf = host.isSelf || isLocalhostUrl(host.url)
      const baseUrl = isSelf ? '' : host.url
      const timeout = isSelf ? SELF_FETCH_TIMEOUT : PEER_FETCH_TIMEOUT
    
      try {
        const controller = new AbortController()
        const timeoutId = setTimeout(() => controller.abort(), timeout)
    
        const response = await fetch(`${baseUrl}/api/agents`, {
          signal: controller.signal
        })
    
        clearTimeout(timeoutId)
    
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
    
        const data: AgentsApiResponse = await response.json()
    
        // Inject host info directly onto agents (for remote hosts, ensure correct hostId/hostName/hostUrl)
        const agents = data.agents.map(agent => ({
          ...agent,
          hostId: host.id,
          hostName: host.name,
          hostUrl: host.url,
          isSelf,
        }))
    
        // Cache peer host agents for offline access (not self host)
        if (!isSelf) {
          cacheRemoteAgents(host.id, agents)
        }
    
        return {
          hostId: host.id,
          success: true,
          response: {
            ...data,
            agents,
            hostInfo: {
              ...data.hostInfo,
              id: host.id,
              name: host.name,
              isSelf,
            }
          }
        }
      } catch (error) {
        console.error(`[useAgents] Failed to fetch from ${host.name} (${host.url}):`, error)
    
        // Try to use cached data for peer hosts (not self)
        if (!isSelf) {
          const cachedAgents = getCachedAgents(host.id)
          if (cachedAgents && cachedAgents.length > 0) {
            console.log(`[useAgents] Using cached data for ${host.name}`)
            return {
              hostId: host.id,
              success: true,
              fromCache: true,
              response: {
                agents: cachedAgents,
                stats: {
                  total: cachedAgents.length,
                  online: cachedAgents.filter(a => a.session?.status === 'online').length,
                  offline: cachedAgents.filter(a => a.session?.status === 'offline').length,
                  orphans: cachedAgents.filter(a => a.isOrphan).length,
                  newlyRegistered: 0
                },
                hostInfo: {
                  id: host.id,
                  name: host.name,
                  url: host.url,
                  isSelf: false,
                }
              }
            }
          }
        }
    
        return {
          hostId: host.id,
          success: false,
          error: error instanceof Error ? error : new Error('Unknown error')
        }
      }
    }
    
    /**
     * Aggregate results from multiple hosts
     */
    function aggregateResults(results: HostFetchResult[]): {
      agents: Agent[]
      stats: AggregatedStats
      hostErrors: Record<string, Error>
    } {
      const allAgents: Agent[] = []
      const hostErrors: Record<string, Error> = {}
      let cachedCount = 0
    
      for (const result of results) {
        if (result.success && result.response) {
          allAgents.push(...result.response.agents)
          if (result.fromCache) {
            cachedCount += result.response.agents.length
          }
        } else if (result.error) {
          hostErrors[result.hostId] = result.error
        }
      }
    
      // Filter out system agents (prefixed with _aim-) from the public list
      const publicAgents = allAgents.filter(a => {
        const name = a.name || a.alias || ''
        return !name.startsWith('_aim-')
      })
    
      // OPTIMIZED: Use toSorted() for immutability instead of sort() which mutates
      // Sort: online first, then alphabetically by alias
      const sortedAgents = publicAgents.toSorted((a, b) => {
        // Online first
        if (a.session?.status === 'online' && b.session?.status !== 'online') return -1
        if (a.session?.status !== 'online' && b.session?.status === 'online') return 1
    
        // Then alphabetically by name (case-insensitive)
        const nameA = (a.name || a.alias || '').toLowerCase()
        const nameB = (b.name || b.alias || '').toLowerCase()
        return nameA.localeCompare(nameB)
      })
    
      // OPTIMIZED: Calculate stats in a single loop instead of multiple filter() calls
      // Reduces from 4 array iterations (3 filter + 1 length) to 1 iteration
      let online = 0
      let offline = 0
      let orphans = 0
      for (const agent of sortedAgents) {
        if (agent.session?.status === 'online') online++
        if (agent.session?.status === 'offline') offline++
        if (agent.isOrphan) orphans++
      }
    
      const stats: AggregatedStats = {
        total: sortedAgents.length,
        online,
        offline,
        orphans,
        newlyRegistered: results.reduce((sum, r) =>
          sum + (r.response?.stats.newlyRegistered || 0), 0),
        cached: cachedCount
      }
    
      return { agents: sortedAgents, stats, hostErrors }
    }
    
    /**
     * Hook to manage agents across multiple hosts
     *
     * Fetches agents from all confi
  • hooks/useCompanionWebSocket.tsGitHub
    Read the script
    'use client'
    
    import { useEffect, useRef, useCallback } from 'react'
    
    interface UseCompanionWebSocketOptions {
      agentId: string | null
      onSpeech: (text: string) => void
      onInterrupt?: () => void
    }
    
    /**
     * Hook for bidirectional communication with the server's cerebellum voice subsystem.
     * Connects to /companion-ws?agent={agentId}, receives speech events,
     * and can send user messages back to the voice subsystem.
     */
    export function useCompanionWebSocket({ agentId, onSpeech, onInterrupt }: UseCompanionWebSocketOptions) {
      const onSpeechRef = useRef(onSpeech)
      onSpeechRef.current = onSpeech
      const onInterruptRef = useRef(onInterrupt)
      onInterruptRef.current = onInterrupt
    
      const wsRef = useRef<WebSocket | null>(null)
    
      const send = useCallback((data: Record<string, unknown>) => {
        if (wsRef.current?.readyState === WebSocket.OPEN) {
          try {
            wsRef.current.send(JSON.stringify(data))
          } catch {
            // Ignore send errors
          }
        }
      }, [])
    
      useEffect(() => {
        if (!agentId) return
    
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
        const wsUrl = `${protocol}//${window.location.host}/companion-ws?agent=${encodeURIComponent(agentId)}`
    
        let ws: WebSocket | null = null
        let mounted = true
        let retryCount = 0
        const maxRetries = 5
        const retryDelays = [1000, 2000, 3000, 5000, 10000]
    
        function connect() {
          if (!mounted) return
    
          // Close any existing socket to prevent orphaned connections
          if (ws && ws.readyState !== WebSocket.CLOSED) {
            ws.close()
          }
    
          ws = new WebSocket(wsUrl)
          wsRef.current = ws
    
          ws.onopen = () => {
            retryCount = 0
            console.log('[CompanionWS] Connected for agent', agentId?.substring(0, 8))
          }
    
          ws.onmessage = (event) => {
            try {
              const data = JSON.parse(event.data)
              if (data.type === 'speech' && data.text) {
                onSpeechRef.current(data.text)
              } else if (data.type === 'interrupt') {
                onInterruptRef.current?.()
              }
            } catch {
              // Ignore non-JSON messages
            }
          }
    
          ws.onclose = () => {
            // Guard against stale closures from orphaned sockets
            if (wsRef.current !== ws) return
            wsRef.current = null
            if (mounted && retryCount < maxRetries) {
              const delay = retryDelays[retryCount] || retryDelays[retryDelays.length - 1]
              retryCount++
              setTimeout(connect, delay)
            }
          }
    
          ws.onerror = () => {
            // onclose will handle reconnection
          }
        }
    
        connect()
    
        return () => {
          mounted = false
          if (ws) {
            ws.close()
            ws = null
          }
          wsRef.current = null
        }
      }, [agentId])
    
      return { send }
    }
    
  • hooks/useDeviceType.tsGitHub
    Read the script
    'use client'
    
    import { useState, useEffect } from 'react'
    
    export type DeviceType = 'phone' | 'tablet' | 'desktop'
    
    interface DeviceInfo {
      deviceType: DeviceType
      isTouch: boolean
    }
    
    function detectTouch(): boolean {
      if (typeof window === 'undefined') return false
      // Primary check: CSS media query for coarse pointer (touch screens)
      if (window.matchMedia?.('(pointer: coarse)').matches) return true
      // Fallback: touch event support
      if ('ontouchstart' in window) return true
      // Fallback: navigator check
      if (navigator.maxTouchPoints > 0) return true
      return false
    }
    
    function classify(width: number, isTouch: boolean): DeviceType {
      if (width < 768) return 'phone'
      // Touch devices at any width >= 768 get tablet experience
      // Non-touch devices between 768-1023 also get tablet (small laptop screens are fine with it)
      if (isTouch) return 'tablet'
      if (width < 1024) return 'tablet'
      return 'desktop'
    }
    
    export function useDeviceType(): DeviceInfo {
      const [info, setInfo] = useState<DeviceInfo>(() => {
        if (typeof window === 'undefined') return { deviceType: 'desktop', isTouch: false }
        const isTouch = detectTouch()
        const deviceType = classify(window.innerWidth, isTouch)
        return { deviceType, isTouch }
      })
    
      useEffect(() => {
        const update = () => {
          const isTouch = detectTouch()
          const deviceType = classify(window.innerWidth, isTouch)
          setInfo(prev => {
            if (prev.deviceType === deviceType && prev.isTouch === isTouch) return prev
            return { deviceType, isTouch }
          })
        }
    
        // Listen for resize
        window.addEventListener('resize', update)
    
        // Listen for pointer capability changes (e.g. connecting/disconnecting mouse)
        const mql = window.matchMedia?.('(pointer: coarse)')
        if (mql?.addEventListener) {
          mql.addEventListener('change', update)
        }
    
        // Initial check
        update()
    
        return () => {
          window.removeEventListener('resize', update)
          if (mql?.removeEventListener) {
            mql.removeEventListener('change', update)
          }
        }
      }, [])
    
      return info
    }
    
  • hooks/useDocuments.tsGitHub
    Read the script
    'use client'
    
    import { useState, useEffect, useCallback, useRef } from 'react'
    import type { TeamDocument } from '@/types/document'
    
    interface UseDocumentsResult {
      documents: TeamDocument[]
      loading: boolean
      error: string | null
      createDocument: (data: { title: string; content: string; pinned?: boolean; tags?: string[] }) => Promise<void>
      updateDocument: (docId: string, updates: { title?: string; content?: string; pinned?: boolean; tags?: string[] }) => Promise<void>
      deleteDocument: (docId: string) => Promise<void>
      refreshDocuments: () => Promise<void>
    }
    
    export function useDocuments(teamId: string | null): UseDocumentsResult {
      const [documents, setDocuments] = useState<TeamDocument[]>([])
      const [loading, setLoading] = useState(false)
      const [error, setError] = useState<string | null>(null)
      const intervalRef = useRef<NodeJS.Timeout | null>(null)
    
      const fetchDocuments = useCallback(async () => {
        if (!teamId) return
        try {
          const res = await fetch(`/api/teams/${teamId}/documents`)
          if (!res.ok) throw new Error('Failed to fetch documents')
          const data = await res.json()
          setDocuments(data.documents || [])
          setError(null)
        } catch (err) {
          setError(err instanceof Error ? err.message : 'Failed to fetch documents')
        }
      }, [teamId])
    
      // Initial fetch
      useEffect(() => {
        if (!teamId) {
          setDocuments([])
          return
        }
        setLoading(true)
        fetchDocuments().finally(() => setLoading(false))
      }, [teamId, fetchDocuments])
    
      // Poll every 5s for multi-tab sync
      useEffect(() => {
        if (!teamId) return
        intervalRef.current = setInterval(fetchDocuments, 5000)
        return () => {
          if (intervalRef.current) clearInterval(intervalRef.current)
        }
      }, [teamId, fetchDocuments])
    
      const createDocument = useCallback(async (data: { title: string; content: string; pinned?: boolean; tags?: string[] }) => {
        if (!teamId) return
        const res = await fetch(`/api/teams/${teamId}/documents`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data),
        })
        if (!res.ok) throw new Error('Failed to create document')
        await fetchDocuments()
      }, [teamId, fetchDocuments])
    
      const updateDocument = useCallback(async (docId: string, updates: { title?: string; content?: string; pinned?: boolean; tags?: string[] }) => {
        if (!teamId) return
        // Optimistic update
        setDocuments(prev => prev.map(d => d.id === docId ? { ...d, ...updates, updatedAt: new Date().toISOString() } : d))
        const res = await fetch(`/api/teams/${teamId}/documents/${docId}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(updates),
        })
        if (!res.ok) {
          await fetchDocuments() // Revert optimistic update
          throw new Error('Failed to update document')
        }
        await fetchDocuments()
      }, [teamId, fetchDocuments])
    
      const deleteDocument = useCallback(async (docId: string) => {
        if (!teamId) return
        // Optimistic update
        setDocuments(prev => prev.filter(d => d.id !== docId))
        const res = await fetch(`/api/teams/${teamId}/documents/${docId}`, { method: 'DELETE' })
        if (!res.ok) {
          await fetchDocuments() // Revert
          throw new Error('Failed to delete document')
        }
      }, [teamId, fetchDocuments])
    
      return {
        documents,
        loading,
        error,
        createDocument,
        updateDocument,
        deleteDocument,
        refreshDocuments: fetchDocuments,
      }
    }
    
  • hooks/useHosts.tsGitHub
  • hooks/useMeetingMessages.tsGitHub
  • hooks/useSessionActivity.tsGitHub
  • hooks/useTTS.tsGitHub
  • hooks/useTasks.tsGitHub
  • hooks/useTeam.tsGitHub
  • hooks/useTerminal.tsGitHub
  • hooks/useTranscriptExport.tsGitHub
  • hooks/useWebSocket.tsGitHub

All 15 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

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 withai-maestro

I was running 35 AI agents across multiple terminals and became the human mailman between them. So I built AI Maestro. The OS for AI-first organizations — orchestrate any AI agent with persistent memory, agent-to-agent messaging, and multi-machine support.

Get the whole plugin
Stats
744
Stars
95
Forks
Active
Maintenance
TypeScript
Language
MIT
License
2d ago
Last commit
10mo ago
Created

Repo: 23blocks-OS/ai-maestro