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

1"""MCPBaseSettings-based configuration for Session Buddy MCP Server. 

2 

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) 

9 

10Settings Directory Structure: 

11 settings/ 

12 ├── session-buddy.yaml # Base configuration (committed) 

13 └── local.yaml # Local overrides (gitignored) 

14""" 

15 

16from __future__ import annotations 

17 

18import os 

19import typing as t 

20from pathlib import Path 

21 

22from mcp_common import MCPBaseSettings 

23from pydantic import Field, field_validator, model_validator 

24 

25 

26class SessionMgmtSettings(MCPBaseSettings): 

27 """Unified MCPBaseSettings for session-buddy. 

28 

29 All configuration consolidated into a single flat structure 

30 for Oneiric/mcp-common compatibility and simplicity. 

31 """ 

32 

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 ) 

50 

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 ) 

60 

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 ) 

84 

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 ) 

98 

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 ) 

116 

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 ) 

132 

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 ) 

168 

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 ) 

208 

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 ) 

243 

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 ) 

269 

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 ) 

296 

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 ) 

310 

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 ) 

351 

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 ) 

379 

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 ) 

395 

396 # === Development Settings === 

397 enable_hot_reload: bool = Field( 

398 default=False, 

399 description="Enable hot reloading during development", 

400 ) 

401 

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 ) 

428 

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 ) 

442 

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 ) 

474 

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 

486 

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))) 

499 

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 

508 

509 

510# Global settings instance 

511_settings: SessionMgmtSettings | None = None 

512 

513 

514def get_settings(reload: bool = False) -> SessionMgmtSettings: 

515 """Get the global settings instance. 

516 

517 Args: 

518 reload: Force reload settings from files 

519 

520 Returns: 

521 Global SessionMgmtSettings instance 

522 

523 """ 

524 global _settings 

525 

526 if _settings is None or reload: 

527 _settings = t.cast( 

528 "SessionMgmtSettings", SessionMgmtSettings.load("session-buddy") 

529 ) 

530 

531 # _settings is guaranteed non-None here 

532 assert _settings is not None 

533 return _settings 

534 

535 

536def reload_settings() -> SessionMgmtSettings: 

537 """Force reload settings from files. 

538 

539 Returns: 

540 Freshly loaded SessionMgmtSettings instance 

541 

542 """ 

543 return get_settings(reload=True) 

544 

545 

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 

559 

560 

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 

567 

568 

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) 

585 

586 

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]