Coverage for session_buddy / settings.py: 80.00%
144 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1"""MCPBaseSettings-based configuration for Session Buddy MCP Server.
3Configuration Loading:
4 Settings are loaded with layered priority (highest to lowest):
5 1. Environment variables SESSION_BUDDY_*
6 2. settings/local.yaml (local overrides, gitignored)
7 3. settings/session-buddy.yaml (base configuration)
8 4. Defaults from this class (lowest)
10Settings Directory Structure:
11 settings/
12 ├── session-buddy.yaml # Base configuration (committed)
13 └── local.yaml # Local overrides (gitignored)
14"""
16from __future__ import annotations
18import os
19import typing as t
20from pathlib import Path
22from mcp_common import MCPBaseSettings
23from pydantic import Field, field_validator, model_validator
26class SessionMgmtSettings(MCPBaseSettings):
27 """Unified MCPBaseSettings for session-buddy.
29 All configuration consolidated into a single flat structure
30 for Oneiric/mcp-common compatibility and simplicity.
31 """
33 # === Core MCP settings ===
34 server_name: str = Field(
35 default="Session Buddy MCP",
36 description="Display name for the MCP server",
37 )
38 server_description: str = Field(
39 default="Session management and tooling MCP server",
40 description="Brief description of server functionality",
41 )
42 log_level: t.Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
43 default="INFO",
44 description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
45 )
46 enable_debug_mode: bool = Field(
47 default=False,
48 description="Enable debug features (verbose logging, additional validation)",
49 )
51 # === Core Paths ===
52 data_dir: Path = Field(
53 default=Path("~/.claude/data"),
54 description="Base data directory",
55 )
56 log_dir: Path = Field(
57 default=Path("~/.claude/logs"),
58 description="Base log directory",
59 )
61 # === Database Settings ===
62 database_path: Path = Field(
63 default=Path("~/.claude/data/reflection.duckdb"),
64 description="Path to the DuckDB database file",
65 )
66 database_connection_timeout: int = Field(
67 default=30,
68 ge=1,
69 le=300,
70 description="Database connection timeout in seconds",
71 )
72 database_query_timeout: int = Field(
73 default=120,
74 ge=1,
75 le=3600,
76 description="Database query timeout in seconds",
77 )
78 database_max_connections: int = Field(
79 default=10,
80 ge=1,
81 le=100,
82 description="Maximum number of database connections",
83 )
85 # Multi-project settings
86 enable_multi_project: bool = Field(
87 default=True,
88 description="Enable multi-project coordination features",
89 )
90 auto_detect_projects: bool = Field(
91 default=True,
92 description="Auto-detect project relationships",
93 )
94 project_groups_enabled: bool = Field(
95 default=True,
96 description="Enable project grouping functionality",
97 )
99 # Database search settings
100 enable_full_text_search: bool = Field(
101 default=True,
102 description="Enable full-text search capabilities",
103 )
104 search_index_update_interval: int = Field(
105 default=3600,
106 ge=60,
107 le=86400,
108 description="Search index update interval in seconds",
109 )
110 max_search_results: int = Field(
111 default=100,
112 ge=1,
113 le=10000,
114 description="Maximum number of search results to return",
115 )
117 # === Search Settings ===
118 enable_semantic_search: bool = Field(
119 default=True,
120 description="Enable semantic search using embeddings",
121 )
122 embedding_model: str = Field(
123 default="all-MiniLM-L6-v2",
124 description="Embedding model for semantic search",
125 )
126 embedding_cache_size: int = Field(
127 default=1000,
128 ge=10,
129 le=100000,
130 description="Number of embeddings to cache in memory",
131 )
133 # Advanced search
134 enable_faceted_search: bool = Field(
135 default=True,
136 description="Enable faceted search capabilities",
137 )
138 max_facet_values: int = Field(
139 default=50,
140 ge=1,
141 le=1000,
142 description="Maximum number of facet values to return",
143 )
144 enable_search_suggestions: bool = Field(
145 default=True,
146 description="Enable search suggestions and autocomplete",
147 )
148 suggestion_limit: int = Field(
149 default=10,
150 ge=1,
151 le=100,
152 description="Maximum number of search suggestions",
153 )
154 enable_stemming: bool = Field(
155 default=True,
156 description="Enable word stemming in search",
157 )
158 enable_fuzzy_matching: bool = Field(
159 default=True,
160 description="Enable fuzzy matching for typos",
161 )
162 fuzzy_threshold: float = Field(
163 default=0.8,
164 ge=0.1,
165 le=1.0,
166 description="Fuzzy matching similarity threshold",
167 )
169 # === Token Optimization Settings ===
170 enable_token_optimization: bool = Field(
171 default=True,
172 description="Enable token optimization features",
173 )
174 default_max_tokens: int = Field(
175 default=4000,
176 ge=100,
177 le=200000,
178 description="Default maximum tokens for responses",
179 )
180 default_chunk_size: int = Field(
181 default=2000,
182 ge=50,
183 le=100000,
184 description="Default chunk size for response splitting",
185 )
186 optimization_strategy: str = Field(
187 default="auto",
188 description="Preferred optimization strategy (auto, truncate_old, summarize_content, compress)",
189 )
190 enable_response_chunking: bool = Field(
191 default=True,
192 description="Enable automatic response chunking for large outputs",
193 )
194 enable_duplicate_filtering: bool = Field(
195 default=True,
196 description="Filter out duplicate content in responses",
197 )
198 track_token_usage: bool = Field(
199 default=True,
200 description="Track token usage statistics",
201 )
202 usage_retention_days: int = Field(
203 default=90,
204 ge=1,
205 le=3650,
206 description="Number of days to retain usage statistics",
207 )
209 # === Session Management Settings ===
210 auto_checkpoint_interval: int = Field(
211 default=1800,
212 ge=60,
213 le=86400,
214 description="Auto-checkpoint interval in seconds (default: 30 minutes)",
215 )
216 enable_auto_commit: bool = Field(
217 default=True,
218 description="Enable automatic git commits during checkpoints",
219 )
220 commit_message_template: str = Field(
221 default="checkpoint: Session checkpoint - {timestamp}",
222 min_length=10,
223 description="Template for automatic commit messages",
224 )
225 enable_permission_system: bool = Field(
226 default=True,
227 description="Enable the permission system for trusted operations",
228 )
229 default_trusted_operations: list[str] = Field(
230 default_factory=lambda: ["git_commit", "uv_sync", "file_operations"],
231 description="List of operations that are trusted by default",
232 )
233 auto_cleanup_old_sessions: bool = Field(
234 default=True,
235 description="Automatically clean up old session data",
236 )
237 session_retention_days: int = Field(
238 default=365,
239 ge=1,
240 le=3650,
241 description="Number of days to retain session data",
242 )
244 # Selective auto-store
245 enable_auto_store_reflections: bool = Field(
246 default=True,
247 description="Enable automatic reflection storage at meaningful checkpoints",
248 )
249 auto_store_quality_delta_threshold: int = Field(
250 default=10,
251 ge=5,
252 le=50,
253 description="Minimum quality score change to trigger auto-store",
254 )
255 auto_store_exceptional_quality_threshold: int = Field(
256 default=90,
257 ge=70,
258 le=100,
259 description="Quality score threshold for exceptional sessions",
260 )
261 auto_store_manual_checkpoints: bool = Field(
262 default=True,
263 description="Always store reflections for manually-triggered checkpoints",
264 )
265 auto_store_session_end: bool = Field(
266 default=True,
267 description="Always store reflections at session end",
268 )
270 # === Integration Settings ===
271 enable_crackerjack: bool = Field(
272 default=True,
273 description="Enable Crackerjack code quality integration",
274 )
275 crackerjack_command: str = Field(
276 default="crackerjack",
277 min_length=1,
278 description="Command to run Crackerjack",
279 )
280 enable_git_integration: bool = Field(
281 default=True,
282 description="Enable Git integration features",
283 )
284 git_auto_stage: bool = Field(
285 default=False,
286 description="Automatically stage changes before commits",
287 )
288 global_workspace_path: Path = Field(
289 default=Path("~/Projects/claude"),
290 description="Path to global workspace directory",
291 )
292 enable_global_toolkits: bool = Field(
293 default=True,
294 description="Enable global toolkit discovery and usage",
295 )
297 # === LLM API Keys (optional, overrides env vars) ===
298 openai_api_key: str | None = Field(
299 default=None,
300 description="OpenAI API key (overrides OPENAI_API_KEY)",
301 )
302 anthropic_api_key: str | None = Field(
303 default=None,
304 description="Anthropic API key (overrides ANTHROPIC_API_KEY)",
305 )
306 gemini_api_key: str | None = Field(
307 default=None,
308 description="Gemini API key (overrides GEMINI_API_KEY/GOOGLE_API_KEY)",
309 )
311 # === Logging Settings ===
312 log_format: str = Field(
313 default="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
314 min_length=10,
315 description="Log message format string",
316 )
317 enable_file_logging: bool = Field(
318 default=True,
319 description="Enable logging to file",
320 )
321 log_file_path: Path = Field(
322 default=Path("~/.claude/logs/session-buddy.log"),
323 description="Path to log file",
324 )
325 log_file_max_size: int = Field(
326 default=10 * 1024 * 1024,
327 ge=1024,
328 le=1024 * 1024 * 1024,
329 description="Maximum log file size in bytes (default: 10MB)",
330 )
331 log_file_backup_count: int = Field(
332 default=5,
333 ge=0,
334 le=100,
335 description="Number of backup log files to keep",
336 )
337 enable_performance_logging: bool = Field(
338 default=False,
339 description="Enable detailed performance logging",
340 )
341 log_slow_queries: bool = Field(
342 default=True,
343 description="Log slow database queries",
344 )
345 slow_query_threshold: float = Field(
346 default=1.0,
347 ge=0.1,
348 le=60.0,
349 description="Threshold for slow query logging in seconds",
350 )
352 # === Security Settings ===
353 anonymize_paths: bool = Field(
354 default=False,
355 description="Anonymize file paths in logs and data",
356 )
357 enable_rate_limiting: bool = Field(
358 default=True,
359 description="Enable rate limiting for API requests",
360 )
361 max_requests_per_minute: int = Field(
362 default=100,
363 ge=1,
364 le=10000,
365 description="Maximum requests per minute per client",
366 )
367 max_query_length: int = Field(
368 default=10000,
369 ge=100,
370 le=1000000,
371 description="Maximum length for search queries",
372 )
373 max_content_length: int = Field(
374 default=1000000,
375 ge=1000,
376 le=100000000,
377 description="Maximum content length in bytes (default: 1MB)",
378 )
380 # === MCP Server Settings ===
381 server_host: str = Field(
382 default="localhost",
383 description="MCP server host address",
384 )
385 server_port: int = Field(
386 default=3000,
387 ge=1024,
388 le=65535,
389 description="MCP server port number",
390 )
391 enable_websockets: bool = Field(
392 default=True,
393 description="Enable WebSocket support for MCP server",
394 )
396 # === Development Settings ===
397 enable_hot_reload: bool = Field(
398 default=False,
399 description="Enable hot reloading during development",
400 )
402 # === Feature Flags (rollout) ===
403 # Default to False; enable gradually and flip to True post-rollout
404 use_schema_v2: bool = Field(
405 default=True,
406 description="Use enhanced schema v2 for memory tables",
407 )
408 enable_llm_entity_extraction: bool = Field(
409 default=True,
410 description="Enable multi-provider LLM entity extraction",
411 )
412 enable_anthropic: bool = Field(
413 default=True,
414 description="Enable Anthropic provider in cascade",
415 )
416 enable_ollama: bool = Field(
417 default=False,
418 description="Enable Ollama provider in cascade",
419 )
420 enable_conscious_agent: bool = Field(
421 default=True,
422 description="Enable background Conscious Agent",
423 )
424 enable_filesystem_extraction: bool = Field(
425 default=True,
426 description="Enable filesystem-triggered entity extraction",
427 )
429 # === Extraction Controls ===
430 llm_extraction_timeout: int = Field(
431 default=10,
432 ge=1,
433 le=120,
434 description="Timeout in seconds for LLM extraction requests",
435 )
436 llm_extraction_retries: int = Field(
437 default=1,
438 ge=0,
439 le=3,
440 description="Retry attempts per provider before cascading",
441 )
443 # === Filesystem Extraction Settings ===
444 filesystem_dedupe_ttl_seconds: int = Field(
445 default=120,
446 ge=10,
447 le=3600,
448 description="Time window to skip reprocessing the same file",
449 )
450 filesystem_max_file_size_bytes: int = Field(
451 default=1_000_000,
452 ge=10_000,
453 le=100_000_000,
454 description="Maximum file size to consider for extraction",
455 )
456 filesystem_ignore_dirs: list[str] = Field(
457 default_factory=lambda: [
458 ".git",
459 "__pycache__",
460 "node_modules",
461 ".venv",
462 "venv",
463 ".pytest_cache",
464 ".mypy_cache",
465 ".ruff_cache",
466 "dist",
467 "build",
468 ".DS_Store",
469 ".idea",
470 ".vscode",
471 ],
472 description="Directory names to ignore for extraction",
473 )
475 # === Field Validators ===
476 @model_validator(mode="before")
477 def map_legacy_debug_flag(self, data: t.Any) -> t.Any:
478 if (
479 isinstance(data, dict)
480 and "debug" in data
481 and "enable_debug_mode" not in data
482 ):
483 data = dict(data)
484 data["enable_debug_mode"] = bool(data["debug"])
485 return data
487 @field_validator(
488 "data_dir",
489 "log_dir",
490 "database_path",
491 "log_file_path",
492 "global_workspace_path",
493 )
494 @classmethod
495 def expand_user_paths(cls, v: Path | str) -> Path:
496 """Expand user paths (~ to home directory)."""
497 path = v if isinstance(v, Path) else Path(v)
498 return Path(os.path.expanduser(str(path)))
500 @field_validator("commit_message_template")
501 @classmethod
502 def validate_commit_template(cls, v: str) -> str:
503 """Ensure commit message template contains timestamp placeholder."""
504 if "{timestamp}" not in v:
505 msg = "Commit message template must contain {timestamp} placeholder"
506 raise ValueError(msg)
507 return v
510# Global settings instance
511_settings: SessionMgmtSettings | None = None
514def get_settings(reload: bool = False) -> SessionMgmtSettings:
515 """Get the global settings instance.
517 Args:
518 reload: Force reload settings from files
520 Returns:
521 Global SessionMgmtSettings instance
523 """
524 global _settings
526 if _settings is None or reload:
527 _settings = t.cast(
528 "SessionMgmtSettings", SessionMgmtSettings.load("session-buddy")
529 )
531 # _settings is guaranteed non-None here
532 assert _settings is not None
533 return _settings
536def reload_settings() -> SessionMgmtSettings:
537 """Force reload settings from files.
539 Returns:
540 Freshly loaded SessionMgmtSettings instance
542 """
543 return get_settings(reload=True)
546def get_database_path() -> Path:
547 settings = get_settings()
548 raw = settings.database_path
549 path = raw.expanduser() if isinstance(raw, Path) else Path(str(raw)).expanduser()
550 if not path.is_absolute(): 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true
551 data_dir_raw = settings.data_dir
552 data_dir = (
553 data_dir_raw.expanduser()
554 if isinstance(data_dir_raw, Path)
555 else Path(str(data_dir_raw)).expanduser()
556 )
557 path = data_dir / path
558 return path
561def get_log_file_path() -> Path:
562 settings = get_settings()
563 path = settings.log_file_path.expanduser()
564 if not path.is_absolute():
565 path = settings.log_dir.expanduser() / path
566 return path
569def get_llm_api_key(provider: str) -> str | None:
570 settings = get_settings()
571 field_map = {
572 "openai": "openai_api_key",
573 "anthropic": "anthropic_api_key",
574 "gemini": "gemini_api_key",
575 }
576 field = field_map.get(provider)
577 if field is None:
578 return None
579 raw = getattr(settings, field, None)
580 if not isinstance(raw, str) or not raw.strip(): 580 ↛ 582line 580 didn't jump to line 582 because the condition on line 580 was always true
581 return None
582 if provider in ("openai", "anthropic"):
583 return settings.get_api_key_secure(key_name=field, provider=provider)
584 return settings.get_api_key(key_name=field)
587__all__ = [
588 "SessionMgmtSettings",
589 "get_database_path",
590 "get_llm_api_key",
591 "get_log_file_path",
592 "get_settings",
593 "reload_settings",
594]