haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
use super::tools;

/// The repository plugin manifest; bundle installation may bind its command locally.
pub const MANIFEST: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../plugins/haematite/.claude-plugin/plugin.json"
));
/// The repository Codex plugin manifest, shared by installed bundles.
pub const CODEX_MANIFEST: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../plugins/haematite/.codex-plugin/plugin.json"
));
/// The generated skill page shipped with the plugin.
pub const PAGE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../plugins/haematite/skills/haematite/SKILL.md"
));
const PREAMBLE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../plugins/haematite/skills/haematite/preamble.md"
));

#[derive(Debug)]
pub struct RenderError(&'static str);

impl std::fmt::Display for RenderError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "skill_generation_failed: {}", self.0)
    }
}

impl std::error::Error for RenderError {}

/// Render the documented tool inventory and pagination from the served schema.
///
/// # Errors
/// Refuses a missing generation marker or missing or ambiguous query cursor shape.
pub fn render() -> Result<String, RenderError> {
    let registry = tools();
    let query = registry
        .iter()
        .find(|tool| tool.name == "run_query")
        .ok_or(RenderError("skill_query_tool_missing"))?;
    let continuation = continuation_text(
        query.input_schema.as_value(),
        query.output_schema.as_value(),
    )?;
    if PREAMBLE.matches("{{query_continuation}}").count() != 1 {
        return Err(RenderError("skill_query_marker_invalid"));
    }
    let mut page = PREAMBLE
        .trim_end()
        .replace("{{query_continuation}}", &continuation);
    page.push_str("\n\n| Tool | Purpose |\n| --- | --- |\n");
    for tool in registry {
        page.push_str("| `");
        page.push_str(&cell(tool.name));
        page.push_str("` | ");
        page.push_str(&cell(tool.description));
        page.push_str(" |\n");
    }
    Ok(page)
}

fn continuation_text(
    input: &serde_json::Value,
    output: &serde_json::Value,
) -> Result<String, RenderError> {
    Ok(format!(
        "For `run_query`, pass output `{}` back as input `{}` until a bounded read is complete.",
        cell(&page_cursor_field(output)?),
        cell(cursor_field(input)?)
    ))
}

fn page_cursor_field(schema: &serde_json::Value) -> Result<String, RenderError> {
    let properties = schema
        .get("properties")
        .and_then(serde_json::Value::as_object)
        .ok_or(RenderError("skill_query_cursor_invalid"))?;
    let mut cursor = None;
    for (name, shape) in properties {
        let page = if let Some(reference) = shape.get("$ref") {
            let pointer = reference
                .as_str()
                .and_then(|reference| reference.strip_prefix('#'))
                .ok_or(RenderError("skill_query_cursor_invalid"))?;
            schema
                .pointer(pointer)
                .ok_or(RenderError("skill_query_cursor_invalid"))?
        } else {
            shape
        };
        let Some(fields) = page
            .get("properties")
            .and_then(serde_json::Value::as_object)
        else {
            continue;
        };
        let Some(entries) = fields.get("entries") else {
            continue;
        };
        if entries.get("type").and_then(serde_json::Value::as_str) != Some("array") {
            return Err(RenderError("skill_query_cursor_invalid"));
        }
        if cursor.is_some() {
            return Err(RenderError("skill_query_cursor_ambiguous"));
        }
        cursor = Some(format!("{name}.{}", cursor_field(page)?));
    }
    cursor.ok_or(RenderError("skill_query_cursor_invalid"))
}

fn cursor_field(schema: &serde_json::Value) -> Result<&str, RenderError> {
    let properties = schema
        .get("properties")
        .and_then(serde_json::Value::as_object)
        .ok_or(RenderError("skill_query_cursor_invalid"))?;
    let mut fields = properties
        .iter()
        .filter(|(_, shape)| nullable_string(shape));
    let (name, _) = fields
        .next()
        .ok_or(RenderError("skill_query_cursor_invalid"))?;
    if fields.next().is_some() {
        return Err(RenderError("skill_query_cursor_ambiguous"));
    }
    Ok(name)
}

fn nullable_string(shape: &serde_json::Value) -> bool {
    if let Some(types) = shape.get("type").and_then(serde_json::Value::as_array) {
        return types.len() == 2
            && types.iter().any(|kind| kind == "string")
            && types.iter().any(|kind| kind == "null");
    }
    shape
        .get("anyOf")
        .and_then(serde_json::Value::as_array)
        .is_some_and(|choices| {
            choices.len() == 2
                && choices
                    .iter()
                    .any(|choice| choice.get("type").is_some_and(|kind| kind == "string"))
                && choices
                    .iter()
                    .any(|choice| choice.get("type").is_some_and(|kind| kind == "null"))
        })
}

fn cell(text: &str) -> String {
    text.replace('\\', "\\\\")
        .replace('`', "\\`")
        .replace('|', "\\|")
        .replace(['\n', '\r', '\t'], " ")
}

#[cfg(test)]
mod tests {
    use std::error::Error;

    use serde_json::Value;

    use super::{MANIFEST, PAGE, render};
    use crate::mcp::schema::tools;

    type TestResult = Result<(), Box<dyn Error>>;

    #[test]
    fn checked_in_page_equals_the_live_schema_registry() -> TestResult {
        assert_eq!(render()?, PAGE);
        assert_eq!(
            PAGE.lines().filter(|line| line.starts_with("| `")).count(),
            tools().len()
        );
        Ok(())
    }

    #[test]
    fn rendering_is_deterministic() -> TestResult {
        assert_eq!(render()?, render()?);
        Ok(())
    }

    #[test]
    fn plugin_registers_the_launcher_and_optional_lys_proof() -> TestResult {
        let manifest: Value = serde_json::from_str(MANIFEST)?;
        let server = &manifest["mcpServers"]["haematite"];
        assert_eq!(server["command"], "${CLAUDE_PLUGIN_ROOT}/scripts/mcp.sh");
        assert_eq!(server["type"], "stdio");
        assert_eq!(
            server["env"],
            serde_json::json!({
                "HAEMATITE_CONFIG": "${user_config.config_file}",
                "HAEMATITE_LYS_ISSUER": "${user_config.lys_issuer}",
                "HAEMATITE_LYS_AGENT": "${user_config.lys_agent}",
                "HAEMATITE_KEY_FILE": "${user_config.key_file}"
            })
        );
        assert_eq!(manifest["userConfig"]["config_file"]["required"], true);
        for (name, kind) in [
            ("lys_issuer", "string"),
            ("lys_agent", "string"),
            ("key_file", "file"),
        ] {
            let option = &manifest["userConfig"][name];
            assert_eq!(option["type"], kind);
            assert_eq!(option["required"], false);
            assert_eq!(option["default"], "");
        }
        Ok(())
    }

    #[test]
    fn every_registered_tool_is_taught_exactly_once() -> TestResult {
        let page = render()?;
        for tool in tools() {
            assert_eq!(page.matches(&format!("| `{}` |", tool.name)).count(), 1);
            assert!(page.contains(&super::cell(tool.description)));
        }
        Ok(())
    }

    #[test]
    fn pagination_names_come_from_the_schema_instead_of_prose() -> TestResult {
        let input = serde_json::json!({"properties":{"resumption":{"type":["string","null"]}}});
        let page = serde_json::json!({
            "properties": {
                "entries": {"type":"array","items":{}},
                "following": {"anyOf": [{"type":"string"}, {"type":"null"}]}
            }
        });
        for output in [
            serde_json::json!({"properties":{"results":page}}),
            serde_json::json!({
                "properties":{"results":{"$ref":"#/$defs/ResultPage"}},
                "$defs":{"ResultPage":page}
            }),
        ] {
            let text = super::continuation_text(&input, &output)?;
            assert!(text.contains("output `results.following` back as input `resumption`"));
            assert!(!text.contains("`next`"));
            assert!(!text.contains("`continuation`"));
        }
        Ok(())
    }

    #[test]
    fn missing_or_ambiguous_cursor_shapes_refuse_by_name() -> TestResult {
        for (shape, code) in [
            (serde_json::json!({}), "skill_query_cursor_invalid"),
            (
                serde_json::json!({
                    "properties": {
                        "a": {"type": ["string", "null"]},
                        "b": {"type": ["string", "null"]}
                    }
                }),
                "skill_query_cursor_ambiguous",
            ),
        ] {
            let error = super::cursor_field(&shape)
                .err()
                .ok_or("invalid cursor accepted")?;
            assert!(error.to_string().contains(code));
        }
        Ok(())
    }

    #[test]
    fn the_bundled_page_explains_its_local_config_and_snapshot() {
        assert!(PAGE.contains("bundled `serve.json`"));
        assert!(PAGE.contains("no `userConfig` prompt"));
        assert!(PAGE.contains("`socket` or `data_dir`"));
        assert!(PAGE.contains("new bundle directory"));
        assert!(PAGE.contains("`bundle_conflict`"));
        assert!(PAGE.contains("pass output `rows.next_after` back as input `after`"));
    }

    #[test]
    fn the_page_names_pass_refusals_and_connection_close_bounds() {
        let page = PAGE.split_whitespace().collect::<Vec<_>>().join(" ");
        assert!(page.contains("401 `mcp_sign_in_required`"));
        assert!(page.contains("401 `mcp_pass_refused`"));
        assert!(page.contains("503 `mcp_store_unavailable`"));
        assert!(page.contains("for up to 1 s before closing"));
        assert!(page.contains("without a linger read"));
        for removed in [
            "lys_userinfo",
            "permitted_subjects",
            "mcp_lys_answer_malformed",
        ] {
            assert!(!page.contains(removed), "the page still names {removed}");
        }
    }
}