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