Skip to main content

atman_runtime/help/
mod.rs

1//! Built-in help system — docstring-driven, version-synced.
2//!
3//! Dynamic topics (`tools`, `cli`, `config`, `mcp`) are generated at runtime
4//! from `ToolRegistry`, `META_COMMANDS`, and `DocumentedFields::FIELD_DOCS`,
5//! so they can never drift from the code. Static topics (`dsl`, `features`)
6//! are embedded markdown, test-guarded for key terms.
7
8use crate::mcp::{McpServerConfig, TransportKind};
9use crate::meta_commands::META_COMMANDS;
10use crate::safety::SafetyConfig;
11use crate::storage::StorageConfig;
12use crate::tool::{ToolCtx, ToolRegistry};
13use crate::trust::TrustConfig;
14use documented::{DocumentedFields, DocumentedVariants};
15
16pub struct HelpTopic {
17    pub id: &'static str,
18    pub title: &'static str,
19}
20
21pub const TOPICS: &[HelpTopic] = &[
22    HelpTopic {
23        id: "index",
24        title: "Topic Index",
25    },
26    HelpTopic {
27        id: "tools",
28        title: "Built-in Tools",
29    },
30    HelpTopic {
31        id: "cli",
32        title: "CLI Meta Commands",
33    },
34    HelpTopic {
35        id: "config",
36        title: "Configuration Reference",
37    },
38    HelpTopic {
39        id: "mcp",
40        title: "MCP Setup",
41    },
42    HelpTopic {
43        id: "dsl",
44        title: "Atman DSL Syntax",
45    },
46    HelpTopic {
47        id: "features",
48        title: "Feature Overview",
49    },
50];
51
52pub fn version() -> &'static str {
53    env!("CARGO_PKG_VERSION")
54}
55
56/// Render help content for a topic. Returns `None` if the topic is unknown.
57pub fn topic_content(topic: &str, ctx: &ToolCtx) -> Option<String> {
58    match topic {
59        "index" => Some(render_index()),
60        "tools" => ctx
61            .registry
62            .as_ref()
63            .map(|r| render_tools(r))
64            .or_else(|| Some(render_tools_offline())),
65        "cli" => Some(render_cli()),
66        "config" => Some(render_config()),
67        "mcp" => Some(render_mcp()),
68        "dsl" => Some(include_str!("content/dsl.md").to_string()),
69        "features" => Some(include_str!("content/features.md").to_string()),
70        _ => None,
71    }
72}
73
74fn render_index() -> String {
75    let mut out = String::new();
76    out.push_str(&format!("# Atman {} — Help Index\n\n", version()));
77    out.push_str("Call `help.show(topic: \"...\")` with one of:\n\n");
78    for t in TOPICS {
79        out.push_str(&format!("- **{}** — {}\n", t.id, t.title));
80    }
81    out
82}
83
84fn render_tools(registry: &ToolRegistry) -> String {
85    let mut tools: Vec<_> = registry.iter();
86    tools.sort_by(|a, b| a.0.cmp(&b.0));
87    let mut out = String::from("# Built-in Tools\n\n");
88    out.push_str("| Tier | Name | Description |\n|------|------|-------------|\n");
89    for (name, tool) in &tools {
90        let tier = format!("{:?}", tool.tier());
91        let desc = tool
92            .description()
93            .unwrap_or("")
94            .lines()
95            .next()
96            .unwrap_or("");
97        out.push_str(&format!("| {} | `{}` | {} |\n", tier, name, desc));
98    }
99    out.push_str(&format!("\n_{} tools registered._\n", tools.len()));
100    out
101}
102
103fn render_tools_offline() -> String {
104    String::from("# Built-in Tools\n\nTool registry not available in this context.\n")
105}
106
107fn render_cli() -> String {
108    let mut out = String::from("# CLI Meta Commands (REPL `:`commands)\n\n");
109    out.push_str("| Command | Aliases | Description |\n|---------|---------|-------------|\n");
110    for cmd in META_COMMANDS {
111        let aliases = if cmd.aliases.is_empty() {
112            "—".to_string()
113        } else {
114            cmd.aliases
115                .iter()
116                .map(|a| format!("`:{a}`"))
117                .collect::<Vec<_>>()
118                .join(", ")
119        };
120        out.push_str(&format!(
121            "| `:{}` | {} | {} |\n",
122            cmd.name, aliases, cmd.desc
123        ));
124    }
125    out.push_str("\n```\n");
126    for cmd in META_COMMANDS {
127        out.push_str(&format!("{}\n", cmd.usage));
128    }
129    out.push_str("```\n");
130    out
131}
132
133fn render_config() -> String {
134    let mut out = String::from("# Configuration Reference\n\n");
135    out.push_str("Config file: `~/.config/atman/config.toml`\n\n");
136
137    out.push_str("## [safety]\n\n");
138    out.push_str("| Field | Type | Description |\n|-------|------|-------------|\n");
139    for (field, ty) in [
140        ("enabled", "bool"),
141        ("mode", "SafetyMode"),
142        ("auto_rewrite", "bool"),
143    ] {
144        let doc = SafetyConfig::get_field_docs(field).unwrap_or("");
145        out.push_str(&format!("| `{}` | {} | {} |\n", field, ty, doc));
146    }
147
148    out.push_str("\n## [storage]\n\n");
149    out.push_str("| Field | Type | Description |\n|-------|------|-------------|\n");
150    let doc = StorageConfig::get_field_docs("scope").unwrap_or("");
151    out.push_str(&format!("| `scope` | Option<StorageScope> | {} |\n", doc));
152
153    out.push_str("\n## [trust]\n\n");
154    out.push_str("| Field | Type | Description |\n|-------|------|-------------|\n");
155    for (field, ty) in [
156        ("mode", "TrustMode"),
157        ("theme", "Theme"),
158        ("outside", "OutsideBehavior"),
159    ] {
160        let doc = TrustConfig::get_field_docs(field).unwrap_or("");
161        out.push_str(&format!("| `{}` | {} | {} |\n", field, ty, doc));
162    }
163
164    out.push_str("\n## [compaction]\n\n");
165    out.push_str(
166        "| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n",
167    );
168    out.push_str("| `review` | string | `\"manual-only\"` | When to show compaction review modal: `always` / `manual-only` / `never` |\n");
169
170    out.push_str("\n## [registry]\n\n");
171    out.push_str(
172        "| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n",
173    );
174    out.push_str("| `auto_snapshot` | bool | `false` | Auto flow snapshot on every run |\n");
175
176    out.push_str("\n## [suggest]\n\n");
177    out.push_str(
178        "| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n",
179    );
180    out.push_str("| `model` | string | `\"gpt-4o-mini\"` | Model for flow suggestions |\n");
181
182    out.push_str("\n## [interjection]\n\n");
183    out.push_str(
184        "| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n",
185    );
186    out.push_str("| `classifier` | string | — | Model for interjection classification |\n");
187
188    out
189}
190
191fn render_mcp() -> String {
192    let mut out = String::from("# MCP Setup\n\n");
193    out.push_str("Config file: `~/.config/atman/mcp_servers.json`\n\n");
194    out.push_str("## McpServerConfig Fields\n\n");
195    out.push_str("| Field | Type | Description |\n|-------|------|-------------|\n");
196    let fields = [
197        ("name", "String"),
198        ("transport", "TransportKind"),
199        ("command", "String"),
200        ("args", "Vec<String>"),
201        ("env", "Vec<(String, String)>"),
202        ("url", "Option<String>"),
203        ("auth_token", "Option<String>"),
204        ("headers", "Vec<(String, String)>"),
205        ("tier", "Tier"),
206        ("timeout_ms", "u64"),
207        ("disabled", "bool"),
208    ];
209    for (field, ty) in &fields {
210        let doc = McpServerConfig::get_field_docs(field).unwrap_or("");
211        out.push_str(&format!("| `{}` | {} | {} |\n", field, ty, doc));
212    }
213
214    out.push_str("\n## TransportKind\n\n");
215    out.push_str("| Variant | Description |\n|---------|-------------|\n");
216    for v in [
217        TransportKind::Stdio,
218        TransportKind::Http,
219        TransportKind::Sse,
220    ] {
221        let doc = v.get_variant_docs();
222        out.push_str(&format!("| `{:?}` | {} |\n", v, doc));
223    }
224
225    out.push_str("\n## Tier\n\n");
226    out.push_str(
227        "| Tier | Approval Level | Description |\n|------|---------------|-------------|\n",
228    );
229    for (tier, level, desc) in [
230        ("Zero", "Auto", "Read-only, safe — no approval needed"),
231        ("One", "Approve", "Requires user approval"),
232        ("Two", "Approve", "File writes and mutations"),
233        ("Three", "Dangerous", "Network operations, push"),
234        ("Four", "Dangerous", "Shell, terminal — most dangerous"),
235    ] {
236        out.push_str(&format!("| `{}` | {} | {} |\n", tier, level, desc));
237    }
238
239    out.push_str("\n## Example\n\n");
240    out.push_str("```json\n");
241    out.push_str("{\n  \"mcpServers\": {\n    \"filesystem\": {\n");
242    out.push_str("      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/tmp\"],\n");
243    out.push_str("      \"tier\": \"Two\",\n      \"timeout_ms\": 30000\n    }\n  }\n}\n");
244    out.push_str("```\n");
245
246    out.push_str("\nIn a flow, include MCP tools with `\"mcp.*\"` in the `tools` list:\n\n");
247    out.push_str("```\nllm {\n    tools: [fs.read, \"mcp.*\"]\n}\n```\n");
248    out
249}