Swadhin Biswas feat: implement mervelas cli engine, built-in tools, and reactive UI · 4/3/2026
0358986
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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)
}