recall_echo/
config_cli.rs1use std::path::Path;
8
9use crate::cli_provider::CliSpec;
10use crate::config::{self, Provider};
11use crate::error::RecallError;
12
13const BOLD: &str = "\x1b[1m";
14const DIM: &str = "\x1b[2m";
15const GREEN: &str = "\x1b[32m";
16const RESET: &str = "\x1b[0m";
17
18pub fn show(memory_dir: &Path) -> Result<(), RecallError> {
20 let cfg = config::load(memory_dir);
21 let path = config::config_path(memory_dir);
22 let exists = path.exists();
23
24 eprintln!("{BOLD}recall-echo config{RESET}");
25 if exists {
26 eprintln!("{DIM}{}{RESET}\n", path.display());
27 } else {
28 eprintln!("{DIM}(no config file — using defaults){RESET}\n");
29 }
30
31 eprintln!("{BOLD}[ephemeral]{RESET}");
33 eprintln!(" max_entries = {}", cfg.ephemeral.max_entries);
34
35 eprintln!("\n{BOLD}[llm]{RESET}");
37 let provider_label = match &cfg.llm.provider {
38 Provider::Openai => "openai (ollama)".to_string(),
39 other => other.to_string(),
40 };
41 eprintln!(" provider = {provider_label}");
42 eprintln!(
43 " model = {} {DIM}({}){RESET}",
44 cfg.llm.resolved_model(),
45 if cfg.llm.model.is_empty() {
46 "default"
47 } else {
48 "custom"
49 }
50 );
51 if cfg.llm.provider.is_cli() {
52 show_cli_section(&cfg.llm);
53 } else {
54 eprintln!(
55 " api_base = {} {DIM}({}){RESET}",
56 cfg.llm.resolved_api_base(),
57 if cfg.llm.api_base.is_empty() {
58 "default"
59 } else {
60 "custom"
61 }
62 );
63 }
64
65 show_capture_section(&cfg.capture);
66 show_extraction_section(&cfg.extraction);
67 show_serve_section(&cfg.serve);
68
69 if let Some(ref pipeline) = cfg.pipeline {
71 eprintln!("\n{BOLD}[pipeline]{RESET}");
72 eprintln!(
73 " docs_dir = {}",
74 pipeline
75 .docs_dir
76 .as_deref()
77 .unwrap_or("{DIM}(not set){RESET}")
78 );
79 eprintln!(" auto_sync = {}", pipeline.auto_sync.unwrap_or(false));
80 }
81
82 Ok(())
83}
84
85fn show_capture_section(capture: &config::CaptureSection) {
91 eprintln!("\n{BOLD}[capture]{RESET}");
92 eprintln!(" enabled = {}", capture.enabled);
93 let sources = match &capture.sources {
94 Some(sources) if !sources.is_empty() => sources
95 .iter()
96 .map(ToString::to_string)
97 .collect::<Vec<_>>()
98 .join(", "),
99 _ => "(every CLI with sessions on this machine)".to_string(),
100 };
101 eprintln!(" sources = {sources}");
102 eprintln!(
103 " settle_secs = {} {DIM}(a transcript must be this quiet to count as finished){RESET}",
104 capture.settle_secs
105 );
106}
107
108fn show_extraction_section(extraction: &config::ExtractionSection) {
110 eprintln!("\n{BOLD}[extraction]{RESET}");
111 eprintln!(" background_enabled = {}", extraction.background_enabled);
112 eprintln!(
113 " idle_after_secs = {} {DIM}(quiet period before a batch starts){RESET}",
114 extraction.idle_after_secs
115 );
116 eprintln!(
117 " batch_size = {} {DIM}(archives per batch){RESET}",
118 extraction.batch_size
119 );
120}
121
122fn show_serve_section(serve: &config::ServeSection) {
124 eprintln!("\n{BOLD}[serve]{RESET}");
125 let socket = serve
126 .socket_path
127 .as_deref()
128 .filter(|path| !path.trim().is_empty());
129 match socket {
130 Some(path) => eprintln!(" socket_path = {path}"),
131 None => eprintln!(" socket_path = {DIM}(derived from the memory directory){RESET}"),
132 }
133 let idle = match serve.idle_timeout_secs {
134 0 => "0 (never idle-shuts-down)".to_string(),
135 secs => format!("{secs}"),
136 };
137 eprintln!(" idle_timeout_secs = {idle}");
138}
139
140fn show_cli_section(llm: &config::LlmSection) {
143 eprintln!("\n{BOLD}[llm.cli]{RESET}");
144 match CliSpec::resolve(&llm.provider, &llm.cli) {
145 Ok(spec) => {
146 let preset = llm
147 .cli
148 .preset
149 .or_else(|| llm.provider.default_cli_preset())
150 .map_or_else(|| "custom".to_string(), |p| p.to_string());
151 eprintln!(" preset = {preset}");
152 eprintln!(" command = {}", spec.resolve_command());
153 eprintln!(
154 " timeout = {}",
155 spec.timeout
156 .map_or_else(|| "none".to_string(), |t| format!("{}s", t.as_secs()))
157 );
158 eprintln!(" output = {}", spec.output_mode);
159 let result = if spec.result_json_paths.is_empty() {
160 "raw stdout".to_string()
161 } else {
162 spec.result_json_paths.to_string()
163 };
164 eprintln!(" result = {result}");
165 if !spec.ndjson_match.is_empty() {
166 eprintln!(" match = {}", spec.ndjson_match);
167 }
168 let usage = if spec.usage_input_paths.is_empty() && spec.usage_output_paths.is_empty() {
171 format!("{DIM}estimated (this CLI reports no token counts){RESET}")
172 } else {
173 format!("{} / {}", spec.usage_input_paths, spec.usage_output_paths)
174 };
175 eprintln!(" usage = {usage}");
176 eprintln!(
177 " {DIM}{}{RESET}",
178 spec.argv_preview(&spec.resolve_model(&llm.model))
179 );
180 }
181 Err(err) => eprintln!(" {err}"),
182 }
183}
184
185pub fn set(memory_dir: &Path, key: &str, value: &str) -> Result<(), RecallError> {
187 let mut cfg = config::load(memory_dir);
188 cfg.set_key(key, value)?;
189 config::save(memory_dir, &cfg)?;
190
191 eprintln!("{GREEN}✓{RESET} Set {BOLD}{key}{RESET} = {BOLD}{value}{RESET}");
192
193 if key == "llm.provider" || key == "provider" {
196 if cfg.llm.provider.is_cli() {
197 match CliSpec::resolve(&cfg.llm.provider, &cfg.llm.cli) {
198 Ok(spec) => eprintln!(
199 " command → {}",
200 spec.argv_preview(&spec.resolve_model(&cfg.llm.model))
201 ),
202 Err(err) => eprintln!(" {err}"),
203 }
204 } else {
205 eprintln!(" model → {}", cfg.llm.resolved_model());
206 eprintln!(" api_base → {}", cfg.llm.resolved_api_base());
207 }
208 }
209
210 Ok(())
211}