1use std::fmt::Write as _;
8
9const PLATFORM_DEFAULT_SOURCE: &str = "platform default config path";
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum SettingScope {
14 Shared,
16 Cli,
18 Mcp,
20}
21
22impl SettingScope {
23 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Shared => "shared",
27 Self::Cli => "CLI",
28 Self::Mcp => "MCP",
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct SettingDoc {
36 pub section: &'static str,
38 pub key: &'static str,
40 pub value_type: &'static str,
42 pub default: &'static str,
44 pub description: &'static str,
46 pub scope: SettingScope,
48}
49
50#[derive(Clone, Copy, Debug)]
52pub struct SettingsHelp {
53 docs: &'static [SettingDoc],
54 config_path_precedence: &'static [&'static str],
55}
56
57impl SettingsHelp {
58 pub const fn docs(self) -> &'static [SettingDoc] {
60 self.docs
61 }
62
63 pub const fn config_path_precedence(self) -> &'static [&'static str] {
65 self.config_path_precedence
66 }
67
68 pub fn render_human(self) -> String {
70 let mut output = String::from("plugmem settings\n\n");
71 output.push_str("Config file precedence:\n");
72 for (index, source) in self.config_path_precedence.iter().enumerate() {
73 if *source == PLATFORM_DEFAULT_SOURCE {
74 match crate::default_config_path() {
75 Some(path) => {
76 let _ = writeln!(output, " {}. {}", index + 1, path.display());
77 }
78 None => {
79 let _ = writeln!(output, " {}. {source} (unavailable)", index + 1);
80 }
81 }
82 } else {
83 let _ = writeln!(output, " {}. {source}", index + 1);
84 }
85 }
86 output.push('\n');
87
88 let mut section = None;
89 for doc in self.docs {
90 if section != Some(doc.section) {
91 if section.is_some() {
92 output.push('\n');
93 }
94 let _ = writeln!(output, "[{}]", doc.section);
95 section = Some(doc.section);
96 }
97 let _ = writeln!(
98 output,
99 " {} ({}, default: {}) — {} [{}]",
100 doc.key,
101 doc.value_type,
102 doc.default,
103 doc.description,
104 doc.scope.as_str()
105 );
106 }
107
108 output
109 }
110}
111
112const CONFIG_PATH_PRECEDENCE: &[&str] = &[
113 "--config PATH",
114 "$PLUGMEM_CONFIG",
115 "platform default config path",
116 "built-in defaults",
117];
118
119const DOCS: &[SettingDoc] = &[
120 SettingDoc {
121 section: "database",
122 key: "path",
123 value_type: "path string",
124 default: "platform data directory/memory.plugmem",
125 description: "Persistent database file; explicit --db/constructor path and PLUGMEM_DB override it",
126 scope: SettingScope::Shared,
127 },
128 SettingDoc {
129 section: "workspace",
130 key: "dir",
131 value_type: "path string",
132 default: "unset (one database, no workspace)",
133 description: "Directory of named databases; unset means the single-database default",
134 scope: SettingScope::Shared,
135 },
136 SettingDoc {
137 section: "workspace",
138 key: "max_open",
139 value_type: "positive integer",
140 default: "16",
141 description: "Workspace databases kept open at once; the least recently used is closed",
142 scope: SettingScope::Shared,
143 },
144 SettingDoc {
145 section: "workspace",
146 key: "idle_timeout_ms",
147 value_type: "non-negative integer",
148 default: "60000",
149 description: "Close a workspace database unused this long, releasing its lock; 0 never closes",
150 scope: SettingScope::Shared,
151 },
152 SettingDoc {
153 section: "engine",
154 key: "dim",
155 value_type: "non-negative integer",
156 default: "0",
157 description: "Embedding dimension; 0 disables vector storage",
158 scope: SettingScope::Shared,
159 },
160 SettingDoc {
161 section: "engine",
162 key: "max_bytes",
163 value_type: "non-negative integer",
164 default: "2147483648",
165 description: "Ceiling applied to each byte pool separately, not to their sum",
166 scope: SettingScope::Shared,
167 },
168 SettingDoc {
169 section: "engine",
170 key: "max_text",
171 value_type: "non-negative integer",
172 default: "4096",
173 description: "Maximum fact text length in bytes",
174 scope: SettingScope::Shared,
175 },
176 SettingDoc {
177 section: "engine",
178 key: "max_blob",
179 value_type: "non-negative integer",
180 default: "65536",
181 description: "Maximum single blob length in bytes",
182 scope: SettingScope::Shared,
183 },
184 SettingDoc {
185 section: "embedder",
186 key: "kind",
187 value_type: "string",
188 default: "none",
189 description: "Embedding provider: none, ollama, openai, lmstudio, vllm or llamacpp",
190 scope: SettingScope::Shared,
191 },
192 SettingDoc {
193 section: "embedder",
194 key: "url",
195 value_type: "string",
196 default: "unset",
197 description: "OpenAI-compatible /v1/embeddings endpoint",
198 scope: SettingScope::Shared,
199 },
200 SettingDoc {
201 section: "embedder",
202 key: "model",
203 value_type: "string",
204 default: "unset",
205 description: "Embedding model name",
206 scope: SettingScope::Shared,
207 },
208 SettingDoc {
209 section: "embedder",
210 key: "api_key_env",
211 value_type: "string",
212 default: "unset",
213 description: "Environment variable containing the bearer token",
214 scope: SettingScope::Shared,
215 },
216 SettingDoc {
217 section: "maintenance",
218 key: "snapshot_every_ops",
219 value_type: "non-negative integer",
220 default: "1024",
221 description: "Snapshot after this many mutations",
222 scope: SettingScope::Shared,
223 },
224 SettingDoc {
225 section: "maintenance",
226 key: "snapshot_journal_bytes",
227 value_type: "non-negative integer",
228 default: "4194304",
229 description: "Snapshot when the journal reaches this size",
230 scope: SettingScope::Shared,
231 },
232 SettingDoc {
233 section: "maintenance",
234 key: "maintain_every_forgets",
235 value_type: "non-negative integer",
236 default: "off",
237 description: "Run policy maintenance after this many forgets",
238 scope: SettingScope::Shared,
239 },
240 SettingDoc {
241 section: "maintenance",
242 key: "batch_size",
243 value_type: "positive integer",
244 default: "128",
245 description: "CLI import facts per embedding request and journal fsync",
246 scope: SettingScope::Cli,
247 },
248 SettingDoc {
249 section: "server",
250 key: "workers",
251 value_type: "positive integer",
252 default: "half of available cores",
253 description: "MCP worker threads",
254 scope: SettingScope::Mcp,
255 },
256];
257
258static SETTINGS_HELP: SettingsHelp = SettingsHelp {
259 docs: DOCS,
260 config_path_precedence: CONFIG_PATH_PRECEDENCE,
261};
262
263pub const fn settings_help() -> &'static SettingsHelp {
265 &SETTINGS_HELP
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn every_documented_setting_has_a_complete_description() {
274 assert!(!DOCS.is_empty());
275 for doc in DOCS {
276 assert!(!doc.section.is_empty());
277 assert!(!doc.key.is_empty());
278 assert!(!doc.value_type.is_empty());
279 assert!(!doc.default.is_empty());
280 assert!(!doc.description.is_empty());
281 }
282 }
283
284 #[test]
285 fn human_help_contains_every_documented_key() {
286 let rendered = settings_help().render_human();
287 for doc in DOCS {
288 assert!(
289 rendered.contains(doc.key),
290 "missing {}.{}",
291 doc.section,
292 doc.key
293 );
294 }
295 }
296}