Skip to main content

plugmem_host/
settings_help.rs

1//! The single source of truth for config.toml help.
2//!
3//! The parser lives in [`super::settings`], while CLI/MCP/NAPI are separate
4//! surfaces. Keeping the public setting catalogue here lets those surfaces
5//! render their own help without copying descriptions or defaults.
6
7use std::fmt::Write as _;
8
9const PLATFORM_DEFAULT_SOURCE: &str = "platform default config path";
10
11/// Which runtime surface owns a setting.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum SettingScope {
14    /// Parsed by `plugmem-host` and shared by every wrapper.
15    Shared,
16    /// Read by `plugmem-cli` in addition to the shared settings.
17    Cli,
18    /// Read by `plugmem-mcp` in addition to the shared settings.
19    Mcp,
20}
21
22impl SettingScope {
23    /// The stable, user-facing scope label.
24    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/// Documentation for one supported config.toml key.
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct SettingDoc {
36    /// TOML section, without brackets.
37    pub section: &'static str,
38    /// TOML key inside [`Self::section`].
39    pub key: &'static str,
40    /// Human-readable value type.
41    pub value_type: &'static str,
42    /// Default as displayed to users.
43    pub default: &'static str,
44    /// What the setting controls.
45    pub description: &'static str,
46    /// The wrapper(s) that consume the setting.
47    pub scope: SettingScope,
48}
49
50/// Runtime access to the complete config.toml help catalogue.
51#[derive(Clone, Copy, Debug)]
52pub struct SettingsHelp {
53    docs: &'static [SettingDoc],
54    config_path_precedence: &'static [&'static str],
55}
56
57impl SettingsHelp {
58    /// Every documented config.toml key.
59    pub const fn docs(self) -> &'static [SettingDoc] {
60        self.docs
61    }
62
63    /// Config-file discovery order, from highest to lowest precedence.
64    pub const fn config_path_precedence(self) -> &'static [&'static str] {
65        self.config_path_precedence
66    }
67
68    /// Render the catalogue for a terminal or a human-facing tool response.
69    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
263/// Returns the shared settings catalogue used by host, CLI, MCP and NAPI.
264pub 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}