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
159
160
//! Debug filter utilities
//!
//! Translated from openclaudecode/src/utils/debugFilter.ts
use once_cell::sync::Lazy;
use regex::Regex;
/// Debug filter configuration
#[derive(Debug, Clone)]
pub struct DebugFilter {
pub include: Vec<String>,
pub exclude: Vec<String>,
pub is_exclusive: bool,
}
/// Parse debug filter string into a filter configuration
/// Examples:
/// - "api,hooks" -> include only api and hooks categories
/// - "!1p,!file" -> exclude logging and file categories
/// - None/empty -> no filtering (show all)
pub fn parse_debug_filter(filter_string: Option<&str>) -> Option<DebugFilter> {
let filter_string = filter_string?.trim();
if filter_string.is_empty() {
return None;
}
let filters: Vec<&str> = filter_string
.split(',')
.map(|f| f.trim())
.filter(|f| !f.is_empty())
.collect();
// If no valid filters remain, return None
if filters.is_empty() {
return None;
}
// Check for mixed inclusive/exclusive filters
let has_exclusive: bool = filters.iter().any(|f| f.starts_with('!'));
let has_inclusive: bool = filters.iter().any(|f| !f.starts_with('!'));
if has_exclusive && has_inclusive {
// Mixed filters - show all messages
return None;
}
// Clean up filters (remove ! prefix) and normalize
let clean_filters: Vec<String> = filters
.iter()
.map(|f| f.trim_start_matches('!').to_lowercase())
.collect();
Some(DebugFilter {
include: if has_exclusive {
vec![]
} else {
clean_filters.clone()
},
exclude: if has_exclusive { clean_filters } else { vec![] },
is_exclusive: has_exclusive,
})
}
/// 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
pub fn extract_debug_categories(message: &str) -> Vec<String> {
let mut categories: Vec<String> = Vec::new();
// Pattern 3: MCP server "servername" - Check this first to avoid false positives
static MCP_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^MCP server ["']([^"']+)["']"#).unwrap());
if let Some(mcp_match) = MCP_REGEX.captures(message) {
if let Some(mcp_name) = mcp_match.get(1) {
categories.push("mcp".to_string());
categories.push(mcp_name.as_str().to_lowercase());
}
} else {
// Pattern 1: "category: message" (simple prefix) - only if not MCP pattern
static PREFIX_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^([^:[]+):").unwrap());
if let Some(prefix_match) = PREFIX_REGEX.captures(message) {
if let Some(prefix) = prefix_match.get(1) {
categories.push(prefix.as_str().trim().to_lowercase());
}
}
}
// Pattern 2: [CATEGORY] at the start
static BRACKET_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\[([^\]]+)]").unwrap());
if let Some(bracket_match) = BRACKET_REGEX.captures(message) {
if let Some(bracket) = bracket_match.get(1) {
categories.push(bracket.as_str().trim().to_lowercase());
}
}
// Pattern 4: Check for additional categories in the message
if message.to_lowercase().contains("1p event:") {
categories.push("1p".to_string());
}
// Pattern 5: Look for secondary categories after the first pattern
static SECONDARY_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r":\s*([^:]+?)(?:\s+(?:type|mode|status|event))?:").unwrap());
if let Some(secondary_match) = SECONDARY_REGEX.captures(message) {
if let Some(secondary) = secondary_match.get(1) {
let secondary = secondary.as_str().trim().to_lowercase();
// Only add if it's a reasonable category name (not too long, no spaces)
if secondary.len() < 30 && !secondary.contains(' ') {
categories.push(secondary);
}
}
}
// Remove duplicates
categories.sort();
categories.dedup();
categories
}
/// Check if debug message should be shown based on filter
pub fn should_show_debug_categories(categories: &[String], filter: &Option<DebugFilter>) -> bool {
// No filter means show everything
let filter = match filter {
Some(f) => f,
None => return true,
};
// If no categories found, handle based on filter mode
if categories.is_empty() {
// 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.is_exclusive {
// Exclusive mode: show if none of the categories are in the exclude list
!categories.iter().any(|cat| filter.exclude.contains(cat))
} else {
// Inclusive mode: show if any of the categories are in the include list
categories.iter().any(|cat| filter.include.contains(cat))
}
}
/// Main function to check if a debug message should be shown
/// Combines extraction and filtering
pub fn should_show_debug_message(message: &str, filter: &Option<DebugFilter>) -> bool {
// Fast path: no filter means show everything
if filter.is_none() {
return true;
}
// Only extract categories if we have a filter
let categories = extract_debug_categories(message);
should_show_debug_categories(&categories, filter)
}