agent-first-psql 0.8.2

A PostgreSQL interface for AI agents: reliable, structured, explicit, and read-only by default.
Documentation
use crate::cli::{
    SkillAdminAction, SkillAdminOptions, SkillAdminRequest, SkillAgentSelection, SkillScope,
};
use agent_first_data::skill::{
    self, SkillAction, SkillAgentSelection as AfSelection, SkillOptions, SkillScope as AfScope,
    SkillSpec,
};
use serde_json::Value;

const SPEC: SkillSpec = SkillSpec {
    name: "agent-first-psql",
    source: include_str!("../skills/agent-first-psql/SKILL.md"),
    title: "Agent-First PSQL",
    marker_slug: "afpsql",
    assets: &[],
};

pub fn run(req: SkillAdminRequest) -> i32 {
    let (action, options) = split_action(req.action);
    let mut emitter =
        agent_first_data::CliEmitter::from_output_to(crate::emit::output_to(), req.output)
            .with_strict_protocol();
    match skill::run_skill_admin(&SPEC, action, &options) {
        Ok(report) => match serde_json::to_value(&report) {
            Ok(value) => match emitter.emit_result(value) {
                Ok(()) => 0,
                Err(_) => 4,
            },
            Err(error) => match emitter.emit_error(
                "serialization_failed",
                &format!("failed to serialize skill report: {error}"),
            ) {
                Ok(()) => 1,
                Err(_) => 4,
            },
        },
        Err(err) => match agent_first_data::json_error("cli_error", &err.message)
            .hint_if_some(err.hint.as_deref())
            .field(
                "partial_report",
                err.partial_report
                    .and_then(|report| serde_json::to_value(report).ok())
                    .unwrap_or(Value::Null),
            )
            .build()
            .map_err(agent_first_data::CliEmitterError::Build)
        {
            Ok(event) => match emitter.emit(event) {
                Ok(()) => 1,
                Err(_) => 4,
            },
            Err(_) => 4,
        },
    }
}

fn split_action(action: SkillAdminAction) -> (SkillAction, SkillOptions) {
    match action {
        SkillAdminAction::Status(options) => (SkillAction::Status, convert_options(options)),
        SkillAdminAction::Install(options) => (SkillAction::Install, convert_options(options)),
        SkillAdminAction::Uninstall(options) => (SkillAction::Uninstall, convert_options(options)),
    }
}

fn convert_options(options: SkillAdminOptions) -> SkillOptions {
    SkillOptions {
        agent: convert_agent(options.agent),
        scope: convert_scope(options.scope),
        skills_dir: options.skills_dir,
        force: options.force,
    }
}

fn convert_agent(agent: SkillAgentSelection) -> AfSelection {
    match agent {
        SkillAgentSelection::All => AfSelection::All,
        SkillAgentSelection::Codex => AfSelection::Codex,
        SkillAgentSelection::ClaudeCode => AfSelection::ClaudeCode,
        SkillAgentSelection::Opencode => AfSelection::Opencode,
        SkillAgentSelection::Hermes => AfSelection::Hermes,
    }
}

fn convert_scope(scope: SkillScope) -> AfScope {
    match scope {
        SkillScope::Personal => AfScope::Personal,
        SkillScope::Workspace => AfScope::Workspace,
    }
}

#[cfg(test)]
mod tests {
    use super::SPEC;

    #[test]
    fn shell_guidance_protects_placeholder_dollars() {
        assert!(
            SPEC.source
                .contains("quote SQL containing `$1..$N` placeholders with single")
        );
        assert!(SPEC.source.contains("shells expand `$1` and `$2`"));
        assert!(SPEC.source.contains("`--sql-file` / pipe mode JSON"));
    }
}