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