Skip to main content

mempal_runtime/
doctor.rs

1use std::env;
2use std::path::{Path, PathBuf};
3
4use rusqlite::{Connection, OpenFlags};
5use serde::Serialize;
6
7use crate::core::db::CURRENT_SCHEMA_VERSION;
8
9pub const REQUIRED_MCP_TOOLS: &[&str] = &[
10    "mempal_context",
11    "mempal_brief",
12    "mempal_phase3",
13    "mempal_session_peek",
14    "mempal_cowork_bus",
15];
16
17pub const PHASE3_ACTIONS: &[&str] = &[
18    "guidance",
19    "instrumentation_policy",
20    "prepare_record",
21    "capture",
22    "evaluator_advise",
23    "default_proposal",
24    "rollback_control",
25    "check_record",
26    "record_checked",
27    "review",
28    "readiness",
29    "analytics",
30    "record",
31    "list",
32    "stats",
33    "gate",
34    "research_validate_plan",
35    "research_ingest_plan",
36];
37
38pub const COWORK_BUS_ACTIONS: &[&str] = &[
39    "register",
40    "list",
41    "send",
42    "broadcast",
43    "drain",
44    "events",
45    "deliveries",
46    "ack",
47    "heartbeat",
48    "channel_set",
49    "channel_list",
50    "channel_send",
51    "tmux_peek",
52    "doctor",
53    "session_create",
54    "session_list",
55    "session_status",
56    "session_close",
57    "handoff",
58    "capture",
59];
60
61#[derive(Debug, Clone, Serialize)]
62pub struct DoctorReport {
63    pub current_version: String,
64    pub supported_schema_version: u32,
65    pub db: DoctorDbReport,
66    pub install: DoctorInstallReport,
67    pub warnings: Vec<String>,
68    pub recommendations: Vec<String>,
69}
70
71#[derive(Debug, Clone, Serialize)]
72pub struct DoctorDbReport {
73    pub path: String,
74    pub exists: bool,
75    pub schema_version: Option<u32>,
76    pub compatible: bool,
77    pub error: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize)]
81pub struct DoctorInstallReport {
82    pub current_exe: Option<String>,
83    pub path_mempal: Option<String>,
84    pub path_matches_current_exe: Option<bool>,
85}
86
87pub fn build_doctor_report(db_path: &Path) -> DoctorReport {
88    let db = inspect_db(db_path);
89    let install = inspect_install();
90    let mut warnings = Vec::new();
91    let mut recommendations = vec![
92        "Run `mempal doctor --format json` after installing or upgrading mempal.".to_string(),
93        "If PATH points at an older binary, restart the MCP client after updating PATH."
94            .to_string(),
95    ];
96
97    if db.exists && !db.compatible {
98        warnings.push(format!(
99            "database schema is not compatible with this binary: found {:?}, supported {}",
100            db.schema_version, CURRENT_SCHEMA_VERSION
101        ));
102        recommendations
103            .push("Install a mempal binary that supports this palace.db schema.".to_string());
104    }
105    if let Some(error) = db.error.as_deref() {
106        warnings.push(format!("database schema could not be inspected: {error}"));
107    }
108    if install.path_matches_current_exe == Some(false) {
109        warnings
110            .push("PATH resolves mempal to a different executable than this process".to_string());
111        recommendations
112            .push("Check `which mempal` and restart long-lived MCP clients.".to_string());
113    }
114    if install.path_mempal.is_none() {
115        warnings.push("PATH does not contain a mempal executable".to_string());
116    }
117
118    DoctorReport {
119        current_version: env!("CARGO_PKG_VERSION").to_string(),
120        supported_schema_version: CURRENT_SCHEMA_VERSION,
121        db,
122        install,
123        warnings,
124        recommendations,
125    }
126}
127
128fn inspect_db(db_path: &Path) -> DoctorDbReport {
129    if !db_path.exists() {
130        return DoctorDbReport {
131            path: db_path.display().to_string(),
132            exists: false,
133            schema_version: None,
134            compatible: true,
135            error: None,
136        };
137    }
138
139    match read_schema_version_read_only(db_path) {
140        Ok(schema_version) => DoctorDbReport {
141            path: db_path.display().to_string(),
142            exists: true,
143            schema_version: Some(schema_version),
144            compatible: schema_version <= CURRENT_SCHEMA_VERSION,
145            error: None,
146        },
147        Err(error) => DoctorDbReport {
148            path: db_path.display().to_string(),
149            exists: true,
150            schema_version: None,
151            compatible: false,
152            error: Some(error),
153        },
154    }
155}
156
157fn read_schema_version_read_only(db_path: &Path) -> Result<u32, String> {
158    let conn = Connection::open_with_flags(db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)
159        .map_err(|error| error.to_string())?;
160    conn.pragma_query_value(None, "user_version", |row| row.get::<_, u32>(0))
161        .map_err(|error| error.to_string())
162}
163
164fn inspect_install() -> DoctorInstallReport {
165    let current_exe = env::current_exe().ok();
166    let path_mempal = find_path_executable("mempal");
167    let path_matches_current_exe = match (current_exe.as_ref(), path_mempal.as_ref()) {
168        (Some(current), Some(path_mempal)) => Some(paths_match(current, path_mempal)),
169        (_, Some(_)) => None,
170        _ => None,
171    };
172
173    DoctorInstallReport {
174        current_exe: current_exe.map(|path| path.display().to_string()),
175        path_mempal: path_mempal.map(|path| path.display().to_string()),
176        path_matches_current_exe,
177    }
178}
179
180fn find_path_executable(name: &str) -> Option<PathBuf> {
181    let path = env::var_os("PATH")?;
182    env::split_paths(&path)
183        .map(|dir| dir.join(name))
184        .find(|candidate| candidate.is_file())
185}
186
187fn paths_match(left: &Path, right: &Path) -> bool {
188    let left = left.canonicalize().unwrap_or_else(|_| left.to_path_buf());
189    let right = right.canonicalize().unwrap_or_else(|_| right.to_path_buf());
190    left == right
191}