1use serde::Serialize;
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct ConfigSchema {
11 pub version: u32,
12 pub sections: BTreeMap<String, SectionSchema>,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct SectionSchema {
17 pub description: String,
18 pub keys: BTreeMap<String, KeySchema>,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct KeySchema {
23 #[serde(rename = "type")]
24 pub ty: String,
25 pub default: serde_json::Value,
26 pub description: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub values: Option<Vec<String>>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub env_override: Option<String>,
31}
32
33fn clean_f32(v: f32) -> serde_json::Value {
34 let clean: f64 = format!("{v}").parse().unwrap_or(v as f64);
35 serde_json::json!(clean)
36}
37
38fn key(ty: &str, default: serde_json::Value, desc: &str) -> KeySchema {
39 KeySchema {
40 ty: ty.to_string(),
41 default,
42 description: desc.to_string(),
43 values: None,
44 env_override: None,
45 }
46}
47
48fn key_enum(values: &[&str], default: &str, desc: &str) -> KeySchema {
49 KeySchema {
50 ty: "enum".to_string(),
51 default: serde_json::Value::String(default.to_string()),
52 description: desc.to_string(),
53 values: Some(values.iter().map(ToString::to_string).collect()),
54 env_override: None,
55 }
56}
57
58fn key_with_env(ty: &str, default: serde_json::Value, desc: &str, env: &str) -> KeySchema {
59 KeySchema {
60 ty: ty.to_string(),
61 default,
62 description: desc.to_string(),
63 values: None,
64 env_override: Some(env.to_string()),
65 }
66}
67
68fn key_enum_with_env(values: &[&str], default: &str, desc: &str, env: &str) -> KeySchema {
69 KeySchema {
70 ty: "enum".to_string(),
71 default: serde_json::Value::String(default.to_string()),
72 description: desc.to_string(),
73 values: Some(values.iter().map(ToString::to_string).collect()),
74 env_override: Some(env.to_string()),
75 }
76}
77
78impl ConfigSchema {
79 pub fn generate() -> Self {
80 let cfg = super::Config::default();
81 let mut sections = BTreeMap::new();
82
83 let mut root = BTreeMap::new();
84 root.insert(
85 "ultra_compact".into(),
86 key(
87 "bool",
88 serde_json::json!(false),
89 "Legacy flag for maximum compression (use compression_level instead)",
90 ),
91 );
92 root.insert(
93 "tee_mode".into(),
94 key_enum(
95 &["never", "failures", "always"],
96 "failures",
97 "Controls when shell output is tee'd to disk for later retrieval",
98 ),
99 );
100 root.insert(
101 "output_density".into(),
102 key_enum_with_env(
103 &["normal", "terse", "ultra"],
104 "normal",
105 "Controls how dense/compact MCP tool output is formatted",
106 "LEAN_CTX_OUTPUT_DENSITY",
107 ),
108 );
109 root.insert(
110 "checkpoint_interval".into(),
111 key(
112 "u32",
113 serde_json::json!(cfg.checkpoint_interval),
114 "Session checkpoint interval in minutes",
115 ),
116 );
117 root.insert(
118 "excluded_commands".into(),
119 key(
120 "string[]",
121 serde_json::json!(cfg.excluded_commands),
122 "Commands to exclude from shell hook interception",
123 ),
124 );
125 root.insert(
126 "passthrough_urls".into(),
127 key(
128 "string[]",
129 serde_json::json!(cfg.passthrough_urls),
130 "URLs to pass through without proxy interception",
131 ),
132 );
133 root.insert("slow_command_threshold_ms".into(), key("u64", serde_json::json!(cfg.slow_command_threshold_ms), "Commands taking longer than this (ms) are recorded in the slow log. Set to 0 to disable"));
134 root.insert(
135 "theme".into(),
136 key(
137 "string",
138 serde_json::json!(cfg.theme),
139 "Dashboard color theme",
140 ),
141 );
142 root.insert(
143 "buddy_enabled".into(),
144 key(
145 "bool",
146 serde_json::json!(cfg.buddy_enabled),
147 "Enable the buddy system for multi-agent coordination",
148 ),
149 );
150 root.insert(
151 "enable_wakeup_ctx".into(),
152 key(
153 "bool",
154 serde_json::json!(cfg.enable_wakeup_ctx),
155 "Append wakeup briefing (facts, session summary) to ctx_overview output. Set false to reduce context bloat when calling ctx_overview frequently.",
156 ),
157 );
158 root.insert(
159 "redirect_exclude".into(),
160 key(
161 "string[]",
162 serde_json::json!(cfg.redirect_exclude),
163 "URL patterns to exclude from proxy redirection",
164 ),
165 );
166 root.insert(
167 "disabled_tools".into(),
168 key(
169 "string[]",
170 serde_json::json!(cfg.disabled_tools),
171 "Tools to exclude from the MCP tool list",
172 ),
173 );
174 root.insert(
175 "default_tool_categories".into(),
176 key(
177 "string[]",
178 serde_json::json!(cfg.default_tool_categories),
179 "Tool categories active by default (core, arch, debug, memory, metrics, session). Override via LCTX_DEFAULT_CATEGORIES",
180 ),
181 );
182 root.insert(
183 "no_degrade".into(),
184 key(
185 "boolean",
186 serde_json::json!(cfg.no_degrade),
187 "Disable all automatic read-mode degradation. Override via LCTX_NO_DEGRADE=1",
188 ),
189 );
190 root.insert(
191 "profile".into(),
192 key(
193 "string",
194 serde_json::json!(cfg.profile.as_deref().unwrap_or("")),
195 "Persistent profile name. Checked after LEAN_CTX_PROFILE env var. Set via: lean-ctx config set profile passthrough",
196 ),
197 );
198 root.insert(
199 "rules_scope".into(),
200 key_enum(
201 &["both", "global", "project"],
202 "both",
203 "Where agent rule files are installed. Override via LEAN_CTX_RULES_SCOPE",
204 ),
205 );
206 root.insert(
207 "extra_ignore_patterns".into(),
208 key(
209 "string[]",
210 serde_json::json!(cfg.extra_ignore_patterns),
211 "Extra glob patterns to ignore in graph/overview/preload",
212 ),
213 );
214 root.insert(
215 "terse_agent".into(),
216 key_enum_with_env(
217 &["off", "lite", "full", "ultra"],
218 "off",
219 "Controls agent output verbosity via instructions injection",
220 "LEAN_CTX_TERSE_AGENT",
221 ),
222 );
223 root.insert(
224 "compression_level".into(),
225 key_enum_with_env(
226 &["off", "lite", "standard", "max"],
227 "off",
228 "Unified compression level for all output",
229 "LEAN_CTX_COMPRESSION",
230 ),
231 );
232 root.insert(
233 "allow_paths".into(),
234 key_with_env(
235 "string[]",
236 serde_json::json!(cfg.allow_paths),
237 "Additional paths allowed by PathJail (absolute)",
238 "LEAN_CTX_ALLOW_PATH",
239 ),
240 );
241 root.insert(
242 "extra_roots".into(),
243 key_with_env(
244 "string[]",
245 serde_json::json!(cfg.extra_roots),
246 "Extra project roots for multi-root workspaces (auto-added to PathJail allow-list)",
247 "LEAN_CTX_EXTRA_ROOTS",
248 ),
249 );
250 root.insert(
251 "content_defined_chunking".into(),
252 key(
253 "bool",
254 serde_json::json!(false),
255 "Enable Rabin-Karp chunking for cache-optimal output ordering",
256 ),
257 );
258 root.insert(
259 "minimal_overhead".into(),
260 key_with_env(
261 "bool",
262 serde_json::json!(false),
263 "Skip session/knowledge/gotcha blocks in MCP instructions",
264 "LEAN_CTX_MINIMAL",
265 ),
266 );
267 root.insert(
268 "shell_hook_disabled".into(),
269 key_with_env(
270 "bool",
271 serde_json::json!(false),
272 "Disable shell hook injection",
273 "LEAN_CTX_NO_HOOK",
274 ),
275 );
276 root.insert(
277 "shell_activation".into(),
278 key_enum_with_env(
279 &["always", "agents-only", "off"],
280 "always",
281 "Controls when the shell hook auto-activates aliases",
282 "LEAN_CTX_SHELL_ACTIVATION",
283 ),
284 );
285 root.insert(
286 "update_check_disabled".into(),
287 key_with_env(
288 "bool",
289 serde_json::json!(false),
290 "Disable the daily version check",
291 "LEAN_CTX_NO_UPDATE_CHECK",
292 ),
293 );
294 root.insert(
295 "bm25_max_cache_mb".into(),
296 key_with_env(
297 "u64",
298 serde_json::json!(cfg.bm25_max_cache_mb),
299 "Maximum BM25 cache file size in MB",
300 "LEAN_CTX_BM25_MAX_CACHE_MB",
301 ),
302 );
303 root.insert(
304 "graph_index_max_files".into(),
305 key(
306 "u64",
307 serde_json::json!(cfg.graph_index_max_files),
308 "Maximum files in graph index. 0 = unlimited (default). Set >0 to cap for constrained systems",
309 ),
310 );
311 root.insert(
312 "memory_profile".into(),
313 key_enum_with_env(
314 &["low", "balanced", "performance"],
315 "balanced",
316 "Controls RAM vs feature trade-off",
317 "LEAN_CTX_MEMORY_PROFILE",
318 ),
319 );
320 root.insert(
321 "memory_cleanup".into(),
322 key_enum_with_env(
323 &["aggressive", "shared"],
324 "aggressive",
325 "Controls how aggressively memory is freed when idle",
326 "LEAN_CTX_MEMORY_CLEANUP",
327 ),
328 );
329 root.insert(
330 "savings_footer".into(),
331 key_enum_with_env(
332 &["auto", "always", "never"],
333 "never",
334 "Controls visibility of token savings footers: never (default, suppress everywhere), always, auto (context-dependent)",
335 "LEAN_CTX_SAVINGS_FOOTER",
336 ),
337 );
338 root.insert(
339 "max_ram_percent".into(),
340 key_with_env(
341 "u8",
342 serde_json::json!(cfg.max_ram_percent),
343 "Maximum percentage of system RAM that lean-ctx may use (1-50, default 5)",
344 "LEAN_CTX_MAX_RAM_PERCENT",
345 ),
346 );
347 root.insert(
348 "max_disk_mb".into(),
349 key_with_env(
350 "u64",
351 serde_json::json!(cfg.max_disk_mb),
352 "Simplified disk budget in MB (0 = disabled). Distributes: archive ~25%, BM25 ~10%",
353 "LEAN_CTX_MAX_DISK_MB",
354 ),
355 );
356 root.insert(
357 "max_staleness_days".into(),
358 key_with_env(
359 "u32",
360 serde_json::json!(cfg.max_staleness_days),
361 "Auto-purge data older than N days (0 = disabled). Flows into archive.max_age_hours",
362 "LEAN_CTX_MAX_STALENESS_DAYS",
363 ),
364 );
365 root.insert(
366 "project_root".into(),
367 key_with_env(
368 "string?",
369 serde_json::json!(null),
370 "Explicit project root directory. Prevents accidental home-directory scans",
371 "LEAN_CTX_PROJECT_ROOT",
372 ),
373 );
374 root.insert(
375 "proxy_enabled".into(),
376 key(
377 "bool?",
378 serde_json::json!(null),
379 "Enable/disable the proxy layer. null = auto-detect, true = force on, false = force off",
380 ),
381 );
382 root.insert(
383 "proxy_port".into(),
384 key(
385 "u16?",
386 serde_json::json!(null),
387 "Custom proxy port (default: 4444). Useful for multi-user systems. Env: LEAN_CTX_PROXY_PORT",
388 ),
389 );
390 root.insert(
391 "proxy_timeout_ms".into(),
392 key(
393 "u64?",
394 serde_json::json!(null),
395 "Proxy reachability timeout in ms (default: 200). Override via LEAN_CTX_PROXY_TIMEOUT_MS",
396 ),
397 );
398 root.insert(
399 "response_verbosity".into(),
400 key_enum_with_env(
401 &["normal", "compact", "minimal"],
402 "normal",
403 "Controls how verbose tool responses are",
404 "LEAN_CTX_RESPONSE_VERBOSITY",
405 ),
406 );
407 root.insert(
408 "allow_auto_reroot".into(),
409 key_with_env(
410 "bool",
411 serde_json::json!(false),
412 "Allow automatic project-root re-rooting when absolute paths outside the jail are seen",
413 "LEAN_CTX_ALLOW_REROOT",
414 ),
415 );
416 root.insert(
417 "sandbox_level".into(),
418 key_with_env(
419 "u8",
420 serde_json::json!(0),
421 "Sandbox strictness level (0=default, 1=strict, 2=paranoid)",
422 "LEAN_CTX_SANDBOX_LEVEL",
423 ),
424 );
425 root.insert(
426 "reference_results".into(),
427 key_with_env(
428 "bool",
429 serde_json::json!(false),
430 "Store large tool outputs as references instead of inline content",
431 "LEAN_CTX_REFERENCE_RESULTS",
432 ),
433 );
434 root.insert(
435 "agent_token_budget".into(),
436 key(
437 "usize",
438 serde_json::json!(0),
439 "Default per-agent token budget. 0 = unlimited",
440 ),
441 );
442 root.insert(
443 "shell_allowlist".into(),
444 key_with_env(
445 "array",
446 serde_json::json!([]),
447 "Optional shell command allowlist. When non-empty, only listed binaries are permitted",
448 "LEAN_CTX_SHELL_ALLOWLIST",
449 ),
450 );
451 root.insert(
452 "shell_strict_mode".into(),
453 key(
454 "bool",
455 serde_json::json!(false),
456 "Block $(), backticks, <() in shell arguments. Default false = warn only.",
457 ),
458 );
459
460 sections.insert(
461 "root".into(),
462 SectionSchema {
463 description: "Top-level configuration keys".into(),
464 keys: root,
465 },
466 );
467
468 sections.insert(
469 "ide_paths".into(),
470 SectionSchema {
471 description: "Per-IDE allowed paths. Keys are agent names (cursor, codex, opencode, antigravity, etc.), values are arrays of paths to index for that agent".into(),
472 keys: BTreeMap::new(),
473 },
474 );
475
476 let mut lsp_keys = BTreeMap::new();
477 lsp_keys.insert(
478 "rust".into(),
479 key(
480 "string?",
481 serde_json::json!(null),
482 "Custom path to rust-analyzer binary",
483 ),
484 );
485 lsp_keys.insert(
486 "typescript".into(),
487 key(
488 "string?",
489 serde_json::json!(null),
490 "Custom path to typescript-language-server binary",
491 ),
492 );
493 lsp_keys.insert(
494 "python".into(),
495 key(
496 "string?",
497 serde_json::json!(null),
498 "Custom path to pylsp binary",
499 ),
500 );
501 lsp_keys.insert(
502 "go".into(),
503 key(
504 "string?",
505 serde_json::json!(null),
506 "Custom path to gopls binary",
507 ),
508 );
509 sections.insert(
510 "lsp".into(),
511 SectionSchema {
512 description: "LSP server binary overrides. Map language name to custom binary path"
513 .into(),
514 keys: lsp_keys,
515 },
516 );
517
518 let mut archive = BTreeMap::new();
519 archive.insert(
520 "enabled".into(),
521 key(
522 "bool",
523 serde_json::json!(cfg.archive.enabled),
524 "Enable zero-loss compression archive",
525 ),
526 );
527 archive.insert(
528 "threshold_chars".into(),
529 key(
530 "usize",
531 serde_json::json!(cfg.archive.threshold_chars),
532 "Minimum output size (chars) to trigger archiving",
533 ),
534 );
535 archive.insert(
536 "max_age_hours".into(),
537 key(
538 "u64",
539 serde_json::json!(cfg.archive.max_age_hours),
540 "Maximum age of archived entries before cleanup",
541 ),
542 );
543 archive.insert(
544 "max_disk_mb".into(),
545 key(
546 "u64",
547 serde_json::json!(cfg.archive.max_disk_mb),
548 "Maximum total disk usage for the archive",
549 ),
550 );
551 sections.insert("archive".into(), SectionSchema {
552 description: "Settings for the zero-loss compression archive (large tool outputs saved to disk)".into(),
553 keys: archive,
554 });
555
556 let mut autonomy = BTreeMap::new();
557 autonomy.insert(
558 "enabled".into(),
559 key(
560 "bool",
561 serde_json::json!(cfg.autonomy.enabled),
562 "Enable autonomous background behaviors",
563 ),
564 );
565 autonomy.insert(
566 "auto_preload".into(),
567 key(
568 "bool",
569 serde_json::json!(cfg.autonomy.auto_preload),
570 "Auto-preload related files on first read",
571 ),
572 );
573 autonomy.insert(
574 "auto_dedup".into(),
575 key(
576 "bool",
577 serde_json::json!(cfg.autonomy.auto_dedup),
578 "Auto-deduplicate repeated reads",
579 ),
580 );
581 autonomy.insert(
582 "auto_related".into(),
583 key(
584 "bool",
585 serde_json::json!(cfg.autonomy.auto_related),
586 "Auto-load graph-related files",
587 ),
588 );
589 autonomy.insert(
590 "auto_consolidate".into(),
591 key(
592 "bool",
593 serde_json::json!(cfg.autonomy.auto_consolidate),
594 "Auto-consolidate knowledge periodically",
595 ),
596 );
597 autonomy.insert(
598 "silent_preload".into(),
599 key(
600 "bool",
601 serde_json::json!(cfg.autonomy.silent_preload),
602 "Suppress preload notifications in output",
603 ),
604 );
605 autonomy.insert(
606 "dedup_threshold".into(),
607 key(
608 "usize",
609 serde_json::json!(cfg.autonomy.dedup_threshold),
610 "Number of repeated reads before dedup triggers",
611 ),
612 );
613 autonomy.insert(
614 "consolidate_every_calls".into(),
615 key(
616 "u32",
617 serde_json::json!(cfg.autonomy.consolidate_every_calls),
618 "Consolidate knowledge every N tool calls",
619 ),
620 );
621 autonomy.insert(
622 "consolidate_cooldown_secs".into(),
623 key(
624 "u64",
625 serde_json::json!(cfg.autonomy.consolidate_cooldown_secs),
626 "Minimum seconds between consolidation runs",
627 ),
628 );
629 sections.insert(
630 "autonomy".into(),
631 SectionSchema {
632 description:
633 "Controls autonomous background behaviors (preload, dedup, consolidation)"
634 .into(),
635 keys: autonomy,
636 },
637 );
638
639 let mut providers = BTreeMap::new();
640 providers.insert(
641 "enabled".into(),
642 key(
643 "bool",
644 serde_json::json!(cfg.providers.enabled),
645 "Master switch for the provider subsystem (GitHub, GitLab, etc.)",
646 ),
647 );
648 providers.insert(
649 "auto_index".into(),
650 key(
651 "bool",
652 serde_json::json!(cfg.providers.auto_index),
653 "Auto-ingest provider results into BM25/embedding indexes",
654 ),
655 );
656 providers.insert(
657 "cache_ttl_secs".into(),
658 key(
659 "u64",
660 serde_json::json!(cfg.providers.cache_ttl_secs),
661 "Default cache TTL for provider results (seconds)",
662 ),
663 );
664 providers.insert(
665 "github.enabled".into(),
666 key(
667 "bool",
668 serde_json::json!(cfg.providers.github.enabled),
669 "Enable/disable GitHub provider",
670 ),
671 );
672 providers.insert(
673 "github.api_url".into(),
674 key(
675 "string",
676 serde_json::json!(cfg.providers.github.api_url),
677 "GitHub API base URL (for GitHub Enterprise)",
678 ),
679 );
680 providers.insert(
681 "gitlab.enabled".into(),
682 key(
683 "bool",
684 serde_json::json!(cfg.providers.gitlab.enabled),
685 "Enable/disable GitLab provider",
686 ),
687 );
688 providers.insert(
689 "gitlab.api_url".into(),
690 key(
691 "string",
692 serde_json::json!(cfg.providers.gitlab.api_url),
693 "GitLab API base URL (for self-hosted instances)",
694 ),
695 );
696 providers.insert(
697 "mcp_bridges.<name>.url".into(),
698 key(
699 "string",
700 serde_json::json!(null),
701 "HTTP/SSE URL for a remote MCP server",
702 ),
703 );
704 providers.insert(
705 "mcp_bridges.<name>.command".into(),
706 key(
707 "string",
708 serde_json::json!(null),
709 "Command to spawn a local MCP server (stdio transport)",
710 ),
711 );
712 providers.insert(
713 "mcp_bridges.<name>.args".into(),
714 key(
715 "array",
716 serde_json::json!([]),
717 "Arguments for the MCP server command",
718 ),
719 );
720 providers.insert(
721 "mcp_bridges.<name>.auth_env".into(),
722 key(
723 "string",
724 serde_json::json!(null),
725 "Environment variable name containing auth token for MCP server",
726 ),
727 );
728 sections.insert(
729 "providers".into(),
730 SectionSchema {
731 description:
732 "External context providers (GitHub, GitLab, Jira, MCP bridges, etc.). Set tokens via env vars (GITHUB_TOKEN, GITLAB_TOKEN). MCP bridges connect external MCP servers as context sources."
733 .into(),
734 keys: providers,
735 },
736 );
737
738 let mut loop_det = BTreeMap::new();
739 loop_det.insert(
740 "normal_threshold".into(),
741 key(
742 "u32",
743 serde_json::json!(cfg.loop_detection.normal_threshold),
744 "Repetitions before reducing output",
745 ),
746 );
747 loop_det.insert(
748 "reduced_threshold".into(),
749 key(
750 "u32",
751 serde_json::json!(cfg.loop_detection.reduced_threshold),
752 "Repetitions before further reducing output",
753 ),
754 );
755 loop_det.insert(
756 "blocked_threshold".into(),
757 key(
758 "u32",
759 serde_json::json!(cfg.loop_detection.blocked_threshold),
760 "Repetitions before blocking. 0 = disabled",
761 ),
762 );
763 loop_det.insert(
764 "window_secs".into(),
765 key(
766 "u64",
767 serde_json::json!(cfg.loop_detection.window_secs),
768 "Time window in seconds for loop detection",
769 ),
770 );
771 loop_det.insert(
772 "search_group_limit".into(),
773 key(
774 "u32",
775 serde_json::json!(cfg.loop_detection.search_group_limit),
776 "Maximum unique searches within a loop window",
777 ),
778 );
779 loop_det.insert(
780 "tool_total_limits".into(),
781 key(
782 "table",
783 serde_json::json!({"ctx_read": 100, "ctx_search": 80, "ctx_shell": 50, "ctx_semantic_search": 60}),
784 "Per-tool total call limits within a session. Keys are tool names, values are max calls",
785 ),
786 );
787 sections.insert(
788 "loop_detection".into(),
789 SectionSchema {
790 description: "Loop detection settings for preventing repeated identical tool calls"
791 .into(),
792 keys: loop_det,
793 },
794 );
795
796 let mut updates = BTreeMap::new();
797 updates.insert(
798 "auto_update".into(),
799 key(
800 "bool",
801 serde_json::json!(cfg.updates.auto_update),
802 "Enable automatic updates (requires explicit opt-in)",
803 ),
804 );
805 updates.insert(
806 "check_interval_hours".into(),
807 key(
808 "u64",
809 serde_json::json!(cfg.updates.check_interval_hours),
810 "How often to check for updates (hours)",
811 ),
812 );
813 updates.insert(
814 "notify_only".into(),
815 key(
816 "bool",
817 serde_json::json!(cfg.updates.notify_only),
818 "Only notify about updates, don't install automatically",
819 ),
820 );
821 sections.insert(
822 "updates".into(),
823 SectionSchema {
824 description: "Automatic update configuration".into(),
825 keys: updates,
826 },
827 );
828
829 let mut boundary = BTreeMap::new();
830 boundary.insert(
831 "cross_project_search".into(),
832 key(
833 "bool",
834 serde_json::json!(cfg.boundary_policy.cross_project_search),
835 "Allow searching across project boundaries",
836 ),
837 );
838 boundary.insert(
839 "cross_project_import".into(),
840 key(
841 "bool",
842 serde_json::json!(cfg.boundary_policy.cross_project_import),
843 "Allow importing knowledge from other projects",
844 ),
845 );
846 boundary.insert(
847 "audit_cross_access".into(),
848 key(
849 "bool",
850 serde_json::json!(cfg.boundary_policy.audit_cross_access),
851 "Log audit events when cross-project access occurs",
852 ),
853 );
854 boundary.insert(
855 "universal_gotchas_enabled".into(),
856 key(
857 "bool",
858 serde_json::json!(cfg.boundary_policy.universal_gotchas_enabled),
859 "Load universal (cross-project) gotchas",
860 ),
861 );
862 sections.insert(
863 "boundary_policy".into(),
864 SectionSchema {
865 description: "Cross-project boundary and access control policies".into(),
866 keys: boundary,
867 },
868 );
869
870 let mut secret_det = BTreeMap::new();
871 secret_det.insert(
872 "enabled".into(),
873 key(
874 "bool",
875 serde_json::json!(cfg.secret_detection.enabled),
876 "Enable secret/credential detection in tool outputs",
877 ),
878 );
879 secret_det.insert(
880 "redact".into(),
881 key(
882 "bool",
883 serde_json::json!(cfg.secret_detection.redact),
884 "Redact detected secrets from output",
885 ),
886 );
887 secret_det.insert(
888 "custom_patterns".into(),
889 key(
890 "array",
891 serde_json::json!(cfg.secret_detection.custom_patterns),
892 "Additional regex patterns to detect as secrets",
893 ),
894 );
895 sections.insert(
896 "secret_detection".into(),
897 SectionSchema {
898 description: "Secret/credential detection and redaction settings".into(),
899 keys: secret_det,
900 },
901 );
902
903 let mut cloud = BTreeMap::new();
904 cloud.insert(
905 "contribute_enabled".into(),
906 key(
907 "bool",
908 serde_json::json!(cfg.cloud.contribute_enabled),
909 "Enable contributing anonymized stats to lean-ctx cloud",
910 ),
911 );
912 sections.insert(
913 "cloud".into(),
914 SectionSchema {
915 description: "Cloud feature settings".into(),
916 keys: cloud,
917 },
918 );
919
920 let mut proxy = BTreeMap::new();
921 proxy.insert(
922 "anthropic_upstream".into(),
923 key(
924 "string?",
925 serde_json::json!(cfg.proxy.anthropic_upstream),
926 "Custom upstream URL for Anthropic API proxy",
927 ),
928 );
929 proxy.insert(
930 "openai_upstream".into(),
931 key(
932 "string?",
933 serde_json::json!(cfg.proxy.openai_upstream),
934 "Custom upstream URL for OpenAI API proxy",
935 ),
936 );
937 proxy.insert(
938 "gemini_upstream".into(),
939 key(
940 "string?",
941 serde_json::json!(cfg.proxy.gemini_upstream),
942 "Custom upstream URL for Gemini API proxy",
943 ),
944 );
945 sections.insert(
946 "proxy".into(),
947 SectionSchema {
948 description: "Proxy upstream configuration for API routing".into(),
949 keys: proxy,
950 },
951 );
952
953 let mem = &cfg.memory;
954 let mut mem_knowledge = BTreeMap::new();
955 mem_knowledge.insert(
956 "max_facts".into(),
957 key(
958 "usize",
959 serde_json::json!(mem.knowledge.max_facts),
960 "Maximum number of knowledge facts stored per project",
961 ),
962 );
963 mem_knowledge.insert(
964 "max_patterns".into(),
965 key(
966 "usize",
967 serde_json::json!(mem.knowledge.max_patterns),
968 "Maximum number of patterns stored",
969 ),
970 );
971 mem_knowledge.insert(
972 "max_history".into(),
973 key(
974 "usize",
975 serde_json::json!(mem.knowledge.max_history),
976 "Maximum history entries retained",
977 ),
978 );
979 mem_knowledge.insert(
980 "contradiction_threshold".into(),
981 key(
982 "f32",
983 clean_f32(mem.knowledge.contradiction_threshold),
984 "Confidence threshold for contradiction detection",
985 ),
986 );
987 mem_knowledge.insert(
988 "recall_facts_limit".into(),
989 key(
990 "usize",
991 serde_json::json!(mem.knowledge.recall_facts_limit),
992 "Maximum facts returned per recall query",
993 ),
994 );
995 mem_knowledge.insert(
996 "rooms_limit".into(),
997 key(
998 "usize",
999 serde_json::json!(mem.knowledge.rooms_limit),
1000 "Maximum number of rooms returned",
1001 ),
1002 );
1003 mem_knowledge.insert(
1004 "timeline_limit".into(),
1005 key(
1006 "usize",
1007 serde_json::json!(mem.knowledge.timeline_limit),
1008 "Maximum number of timeline entries returned",
1009 ),
1010 );
1011 mem_knowledge.insert(
1012 "relations_limit".into(),
1013 key(
1014 "usize",
1015 serde_json::json!(mem.knowledge.relations_limit),
1016 "Maximum number of relations returned",
1017 ),
1018 );
1019 sections.insert(
1020 "memory.knowledge".into(),
1021 SectionSchema {
1022 description: "Knowledge memory budgets (facts, patterns, gotchas)".into(),
1023 keys: mem_knowledge,
1024 },
1025 );
1026
1027 let mut mem_episodic = BTreeMap::new();
1028 mem_episodic.insert(
1029 "max_episodes".into(),
1030 key(
1031 "usize",
1032 serde_json::json!(mem.episodic.max_episodes),
1033 "Maximum number of episodes retained",
1034 ),
1035 );
1036 mem_episodic.insert(
1037 "max_actions_per_episode".into(),
1038 key(
1039 "usize",
1040 serde_json::json!(mem.episodic.max_actions_per_episode),
1041 "Maximum actions tracked per episode",
1042 ),
1043 );
1044 mem_episodic.insert(
1045 "summary_max_chars".into(),
1046 key(
1047 "usize",
1048 serde_json::json!(mem.episodic.summary_max_chars),
1049 "Maximum characters in episode summary",
1050 ),
1051 );
1052 sections.insert(
1053 "memory.episodic".into(),
1054 SectionSchema {
1055 description: "Episodic memory budgets (session episodes)".into(),
1056 keys: mem_episodic,
1057 },
1058 );
1059
1060 let mut mem_procedural = BTreeMap::new();
1061 mem_procedural.insert(
1062 "max_procedures".into(),
1063 key(
1064 "usize",
1065 serde_json::json!(mem.procedural.max_procedures),
1066 "Maximum number of learned procedures stored",
1067 ),
1068 );
1069 mem_procedural.insert(
1070 "min_repetitions".into(),
1071 key(
1072 "usize",
1073 serde_json::json!(mem.procedural.min_repetitions),
1074 "Minimum repetitions before a pattern is stored",
1075 ),
1076 );
1077 mem_procedural.insert(
1078 "min_sequence_len".into(),
1079 key(
1080 "usize",
1081 serde_json::json!(mem.procedural.min_sequence_len),
1082 "Minimum sequence length for procedure detection",
1083 ),
1084 );
1085 mem_procedural.insert(
1086 "max_window_size".into(),
1087 key(
1088 "usize",
1089 serde_json::json!(mem.procedural.max_window_size),
1090 "Maximum window size for pattern analysis",
1091 ),
1092 );
1093 sections.insert(
1094 "memory.procedural".into(),
1095 SectionSchema {
1096 description: "Procedural memory budgets (learned patterns)".into(),
1097 keys: mem_procedural,
1098 },
1099 );
1100
1101 let mut mem_lifecycle = BTreeMap::new();
1102 mem_lifecycle.insert(
1103 "decay_rate".into(),
1104 key(
1105 "f32",
1106 clean_f32(mem.lifecycle.decay_rate),
1107 "Rate at which knowledge confidence decays over time",
1108 ),
1109 );
1110 mem_lifecycle.insert(
1111 "low_confidence_threshold".into(),
1112 key(
1113 "f32",
1114 clean_f32(mem.lifecycle.low_confidence_threshold),
1115 "Threshold below which facts are considered low-confidence",
1116 ),
1117 );
1118 mem_lifecycle.insert(
1119 "stale_days".into(),
1120 key(
1121 "i64",
1122 serde_json::json!(mem.lifecycle.stale_days),
1123 "Days after which unused facts are considered stale",
1124 ),
1125 );
1126 mem_lifecycle.insert(
1127 "similarity_threshold".into(),
1128 key(
1129 "f32",
1130 clean_f32(mem.lifecycle.similarity_threshold),
1131 "Similarity threshold for deduplication",
1132 ),
1133 );
1134 sections.insert(
1135 "memory.lifecycle".into(),
1136 SectionSchema {
1137 description: "Knowledge lifecycle policy (decay, staleness, dedup)".into(),
1138 keys: mem_lifecycle,
1139 },
1140 );
1141
1142 let mut mem_gotcha = BTreeMap::new();
1143 mem_gotcha.insert(
1144 "max_gotchas_per_project".into(),
1145 key(
1146 "usize",
1147 serde_json::json!(mem.gotcha.max_gotchas_per_project),
1148 "Maximum gotchas stored per project",
1149 ),
1150 );
1151 mem_gotcha.insert(
1152 "retrieval_budget_per_room".into(),
1153 key(
1154 "usize",
1155 serde_json::json!(mem.gotcha.retrieval_budget_per_room),
1156 "Maximum gotchas retrieved per room per query",
1157 ),
1158 );
1159 mem_gotcha.insert(
1160 "default_decay_rate".into(),
1161 key(
1162 "f32",
1163 clean_f32(mem.gotcha.default_decay_rate),
1164 "Default decay rate for gotcha importance",
1165 ),
1166 );
1167 sections.insert(
1168 "memory.gotcha".into(),
1169 SectionSchema {
1170 description: "Gotcha memory settings (project-specific warnings and pitfalls)"
1171 .into(),
1172 keys: mem_gotcha,
1173 },
1174 );
1175
1176 let mut mem_embeddings = BTreeMap::new();
1177 mem_embeddings.insert(
1178 "max_facts".into(),
1179 key(
1180 "usize",
1181 serde_json::json!(mem.embeddings.max_facts),
1182 "Maximum number of embedding facts stored",
1183 ),
1184 );
1185 sections.insert(
1186 "memory.embeddings".into(),
1187 SectionSchema {
1188 description: "Embeddings memory settings for semantic search".into(),
1189 keys: mem_embeddings,
1190 },
1191 );
1192
1193 let mut aliases = BTreeMap::new();
1194 aliases.insert(
1195 "command".into(),
1196 key(
1197 "string",
1198 serde_json::json!(""),
1199 "The command pattern to match (e.g. 'deploy')",
1200 ),
1201 );
1202 aliases.insert(
1203 "alias".into(),
1204 key(
1205 "string",
1206 serde_json::json!(""),
1207 "The alias definition to execute",
1208 ),
1209 );
1210 sections.insert("custom_aliases".into(), SectionSchema {
1211 description: "Custom command aliases (array of {command, alias} entries). Note: field names are 'command' and 'alias' (not 'name')".into(),
1212 keys: aliases,
1213 });
1214
1215 if let Some(root_section) = sections.get_mut("root") {
1216 root_section.keys.insert(
1217 "custom_aliases".into(),
1218 key(
1219 "array",
1220 serde_json::json!([]),
1221 "Custom command aliases (array of {command, alias} entries)",
1222 ),
1223 );
1224 }
1225
1226 ConfigSchema {
1227 version: 1,
1228 sections,
1229 }
1230 }
1231
1232 pub fn known_keys(&self) -> Vec<String> {
1234 let mut keys = Vec::new();
1235 for (section, schema) in &self.sections {
1236 if section == "root" {
1237 for key_name in schema.keys.keys() {
1238 keys.push(key_name.clone());
1239 }
1240 } else {
1241 if schema.keys.is_empty() {
1242 keys.push(section.clone());
1243 }
1244 for key_name in schema.keys.keys() {
1245 keys.push(format!("{section}.{key_name}"));
1246 }
1247 }
1248 }
1249 keys
1250 }
1251}