source dump of claude code
1import memoize from 'lodash-es/memoize.js'
2
3export type DebugFilter = {
4 include: string[]
5 exclude: string[]
6 isExclusive: boolean
7}
8
9/**
10 * Parse debug filter string into a filter configuration
11 * Examples:
12 * - "api,hooks" -> include only api and hooks categories
13 * - "!1p,!file" -> exclude logging and file categories
14 * - undefined/empty -> no filtering (show all)
15 */
16export const parseDebugFilter = memoize(
17 (filterString?: string): DebugFilter | null => {
18 if (!filterString || filterString.trim() === '') {
19 return null
20 }
21
22 const filters = filterString
23 .split(',')
24 .map(f => f.trim())
25 .filter(Boolean)
26
27 // If no valid filters remain, return null
28 if (filters.length === 0) {
29 return null
30 }
31
32 // Check for mixed inclusive/exclusive filters
33 const hasExclusive = filters.some(f => f.startsWith('!'))
34 const hasInclusive = filters.some(f => !f.startsWith('!'))
35
36 if (hasExclusive && hasInclusive) {
37 // For now, we'll treat this as an error case and show all messages
38 // Log error using logForDebugging to avoid console.error lint rule
39 // We'll import and use it later when the circular dependency is resolved
40 // For now, just return null silently
41 return null
42 }
43
44 // Clean up filters (remove ! prefix) and normalize
45 const cleanFilters = filters.map(f => f.replace(/^!/, '').toLowerCase())
46
47 return {
48 include: hasExclusive ? [] : cleanFilters,
49 exclude: hasExclusive ? cleanFilters : [],
50 isExclusive: hasExclusive,
51 }
52 },
53)
54
55/**
56 * Extract debug categories from a message
57 * Supports multiple patterns:
58 * - "category: message" -> ["category"]
59 * - "[CATEGORY] message" -> ["category"]
60 * - "MCP server \"name\": message" -> ["mcp", "name"]
61 * - "[ANT-ONLY] 1P event: tengu_timer" -> ["ant-only", "1p"]
62 *
63 * Returns lowercase categories for case-insensitive matching
64 */
65export function extractDebugCategories(message: string): string[] {
66 const categories: string[] = []
67
68 // Pattern 3: MCP server "servername" - Check this first to avoid false positives
69 const mcpMatch = message.match(/^MCP server ["']([^"']+)["']/)
70 if (mcpMatch && mcpMatch[1]) {
71 categories.push('mcp')
72 categories.push(mcpMatch[1].toLowerCase())
73 } else {
74 // Pattern 1: "category: message" (simple prefix) - only if not MCP pattern
75 const prefixMatch = message.match(/^([^:[]+):/)
76 if (prefixMatch && prefixMatch[1]) {
77 categories.push(prefixMatch[1].trim().toLowerCase())
78 }
79 }
80
81 // Pattern 2: [CATEGORY] at the start
82 const bracketMatch = message.match(/^\[([^\]]+)]/)
83 if (bracketMatch && bracketMatch[1]) {
84 categories.push(bracketMatch[1].trim().toLowerCase())
85 }
86
87 // Pattern 4: Check for additional categories in the message
88 // e.g., "[ANT-ONLY] 1P event: tengu_timer" should match both "ant-only" and "1p"
89 if (message.toLowerCase().includes('1p event:')) {
90 categories.push('1p')
91 }
92
93 // Pattern 5: Look for secondary categories after the first pattern
94 // e.g., "AutoUpdaterWrapper: Installation type: development"
95 const secondaryMatch = message.match(
96 /:\s*([^:]+?)(?:\s+(?:type|mode|status|event))?:/,
97 )
98 if (secondaryMatch && secondaryMatch[1]) {
99 const secondary = secondaryMatch[1].trim().toLowerCase()
100 // Only add if it's a reasonable category name (not too long, no spaces)
101 if (secondary.length < 30 && !secondary.includes(' ')) {
102 categories.push(secondary)
103 }
104 }
105
106 // If no categories found, return empty array (uncategorized)
107 return Array.from(new Set(categories)) // Remove duplicates
108}
109
110/**
111 * Check if debug message should be shown based on filter
112 * @param categories - Categories extracted from the message
113 * @param filter - Parsed filter configuration
114 * @returns true if message should be shown
115 */
116export function shouldShowDebugCategories(
117 categories: string[],
118 filter: DebugFilter | null,
119): boolean {
120 // No filter means show everything
121 if (!filter) {
122 return true
123 }
124
125 // If no categories found, handle based on filter mode
126 if (categories.length === 0) {
127 // In exclusive mode, uncategorized messages are excluded by default for security
128 // In inclusive mode, uncategorized messages are excluded (must match a category)
129 return false
130 }
131
132 if (filter.isExclusive) {
133 // Exclusive mode: show if none of the categories are in the exclude list
134 return !categories.some(cat => filter.exclude.includes(cat))
135 } else {
136 // Inclusive mode: show if any of the categories are in the include list
137 return categories.some(cat => filter.include.includes(cat))
138 }
139}
140
141/**
142 * Main function to check if a debug message should be shown
143 * Combines extraction and filtering
144 */
145export function shouldShowDebugMessage(
146 message: string,
147 filter: DebugFilter | null,
148): boolean {
149 // Fast path: no filter means show everything
150 if (!filter) {
151 return true
152 }
153
154 // Only extract categories if we have a filter
155 const categories = extractDebugCategories(message)
156 return shouldShowDebugCategories(categories, filter)
157}