πŸ“„ File detail

utils/debugFilter.ts

🧩 .tsπŸ“ 158 linesπŸ’Ύ 5,069 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes DebugFilter, parseDebugFilter, extractDebugCategories, shouldShowDebugCategories, and shouldShowDebugMessage β€” mainly functions, hooks, or classes. Dependencies touch lodash-es.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

import memoize from 'lodash-es/memoize.js' export type DebugFilter = { include: string[] exclude: string[]

πŸ“€ Exports (heuristic)

  • DebugFilter
  • parseDebugFilter
  • extractDebugCategories
  • shouldShowDebugCategories
  • shouldShowDebugMessage

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • lodash-es

πŸ–₯️ Source preview

import memoize from 'lodash-es/memoize.js'

export type DebugFilter = {
  include: string[]
  exclude: string[]
  isExclusive: boolean
}

/**
 * Parse debug filter string into a filter configuration
 * Examples:
 * - "api,hooks" -> include only api and hooks categories
 * - "!1p,!file" -> exclude logging and file categories
 * - undefined/empty -> no filtering (show all)
 */
export const parseDebugFilter = memoize(
  (filterString?: string): DebugFilter | null => {
    if (!filterString || filterString.trim() === '') {
      return null
    }

    const filters = filterString
      .split(',')
      .map(f => f.trim())
      .filter(Boolean)

    // If no valid filters remain, return null
    if (filters.length === 0) {
      return null
    }

    // Check for mixed inclusive/exclusive filters
    const hasExclusive = filters.some(f => f.startsWith('!'))
    const hasInclusive = filters.some(f => !f.startsWith('!'))

    if (hasExclusive && hasInclusive) {
      // For now, we'll treat this as an error case and show all messages
      // Log error using logForDebugging to avoid console.error lint rule
      // We'll import and use it later when the circular dependency is resolved
      // For now, just return null silently
      return null
    }

    // Clean up filters (remove ! prefix) and normalize
    const cleanFilters = filters.map(f => f.replace(/^!/, '').toLowerCase())

    return {
      include: hasExclusive ? [] : cleanFilters,
      exclude: hasExclusive ? cleanFilters : [],
      isExclusive: hasExclusive,
    }
  },
)

/**
 * Extract debug categories from a message
 * Supports multiple patterns:
 * - "category: message" -> ["category"]
 * - "[CATEGORY] message" -> ["category"]
 * - "MCP server \"name\": message" -> ["mcp", "name"]
 * - "[ANT-ONLY] 1P event: tengu_timer" -> ["ant-only", "1p"]
 *
 * Returns lowercase categories for case-insensitive matching
 */
export function extractDebugCategories(message: string): string[] {
  const categories: string[] = []

  // Pattern 3: MCP server "servername" - Check this first to avoid false positives
  const mcpMatch = message.match(/^MCP server ["']([^"']+)["']/)
  if (mcpMatch && mcpMatch[1]) {
    categories.push('mcp')
    categories.push(mcpMatch[1].toLowerCase())
  } else {
    // Pattern 1: "category: message" (simple prefix) - only if not MCP pattern
    const prefixMatch = message.match(/^([^:[]+):/)
    if (prefixMatch && prefixMatch[1]) {
      categories.push(prefixMatch[1].trim().toLowerCase())
    }
  }

  // Pattern 2: [CATEGORY] at the start
  const bracketMatch = message.match(/^\[([^\]]+)]/)
  if (bracketMatch && bracketMatch[1]) {
    categories.push(bracketMatch[1].trim().toLowerCase())
  }

  // Pattern 4: Check for additional categories in the message
  // e.g., "[ANT-ONLY] 1P event: tengu_timer" should match both "ant-only" and "1p"
  if (message.toLowerCase().includes('1p event:')) {
    categories.push('1p')
  }

  // Pattern 5: Look for secondary categories after the first pattern
  // e.g., "AutoUpdaterWrapper: Installation type: development"
  const secondaryMatch = message.match(
    /:\s*([^:]+?)(?:\s+(?:type|mode|status|event))?:/,
  )
  if (secondaryMatch && secondaryMatch[1]) {
    const secondary = secondaryMatch[1].trim().toLowerCase()
    // Only add if it's a reasonable category name (not too long, no spaces)
    if (secondary.length < 30 && !secondary.includes(' ')) {
      categories.push(secondary)
    }
  }

  // If no categories found, return empty array (uncategorized)
  return Array.from(new Set(categories)) // Remove duplicates
}

/**
 * Check if debug message should be shown based on filter
 * @param categories - Categories extracted from the message
 * @param filter - Parsed filter configuration
 * @returns true if message should be shown
 */
export function shouldShowDebugCategories(
  categories: string[],
  filter: DebugFilter | null,
): boolean {
  // No filter means show everything
  if (!filter) {
    return true
  }

  // If no categories found, handle based on filter mode
  if (categories.length === 0) {
    // In exclusive mode, uncategorized messages are excluded by default for security
    // In inclusive mode, uncategorized messages are excluded (must match a category)
    return false
  }

  if (filter.isExclusive) {
    // Exclusive mode: show if none of the categories are in the exclude list
    return !categories.some(cat => filter.exclude.includes(cat))
  } else {
    // Inclusive mode: show if any of the categories are in the include list
    return categories.some(cat => filter.include.includes(cat))
  }
}

/**
 * Main function to check if a debug message should be shown
 * Combines extraction and filtering
 */
export function shouldShowDebugMessage(
  message: string,
  filter: DebugFilter | null,
): boolean {
  // Fast path: no filter means show everything
  if (!filter) {
    return true
  }

  // Only extract categories if we have a filter
  const categories = extractDebugCategories(message)
  return shouldShowDebugCategories(categories, filter)
}