car-ffi-common 0.17.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! FFI wrappers for `car-automation` (AppleScript + Shortcuts).
//!
//! Each function accepts JSON in / returns JSON out — the shape that
//! both the NAPI and PyO3 binding layers can pass through unchanged
//! and the WebSocket JSON-RPC dispatcher can hand back to the wire.
//!
//! Errors come back as `Err(String)` on the Result, which each binding
//! lifts into its native error type (napi::Error, PyErr, JSON-RPC
//! error envelope).

use serde::{Deserialize, Serialize};

use car_automation::{applescript, shortcuts, AutomationOutput};

#[derive(Debug, Deserialize)]
pub struct RunAppleScriptArgs {
    pub script: String,
    /// "applescript" (default) or "javascript".
    #[serde(default)]
    pub language: Option<String>,
    /// Optional positional args passed to the script's `on run argv` /
    /// `function run(argv)` handler.
    #[serde(default)]
    pub args: Vec<String>,
    /// Soft timeout in milliseconds. `None` = no timeout (use sparingly).
    #[serde(default)]
    pub timeout_ms: Option<u64>,
}

#[derive(Debug, Serialize)]
pub struct AutomationOutputJson {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: Option<i32>,
}

impl From<AutomationOutput> for AutomationOutputJson {
    fn from(o: AutomationOutput) -> Self {
        Self {
            stdout: o.stdout,
            stderr: o.stderr,
            exit_code: o.exit_code,
        }
    }
}

/// Run AppleScript or JXA. Returns JSON `{stdout, stderr, exit_code}`.
pub async fn run_applescript(args_json: &str) -> Result<String, String> {
    let args: RunAppleScriptArgs =
        serde_json::from_str(args_json).map_err(|e| format!("invalid args: {e}"))?;
    let lang = match args.language.as_deref() {
        Some("javascript") | Some("jxa") => applescript::Language::JavaScript,
        _ => applescript::Language::AppleScript,
    };
    let timeout = args.timeout_ms.map(std::time::Duration::from_millis);
    let arg_refs: Vec<&str> = args.args.iter().map(String::as_str).collect();
    let out = applescript::run_with_args(&args.script, lang, &arg_refs, timeout)
        .await
        .map_err(|e| format!("{e}"))?;
    Ok(serde_json::to_string(&AutomationOutputJson::from(out))
        .map_err(|e| format!("serialize: {e}"))?)
}

#[derive(Debug, Deserialize)]
pub struct ListShortcutsArgs {
    /// Folder name or "none" for the unsorted bucket. `None` = no filter.
    #[serde(default)]
    pub folder: Option<String>,
    /// Include UUIDs alongside names.
    #[serde(default)]
    pub with_identifiers: bool,
}

#[derive(Debug, Serialize)]
pub struct ShortcutJson {
    pub name: String,
    pub identifier: Option<String>,
    pub tool_slug: String,
    pub tool_description: String,
    pub parameters_schema: serde_json::Value,
}

impl From<shortcuts::Shortcut> for ShortcutJson {
    fn from(s: shortcuts::Shortcut) -> Self {
        let tool_slug = s.tool_slug();
        let tool_description = s.tool_description();
        let parameters_schema = s.parameters_schema();
        Self {
            name: s.name,
            identifier: s.identifier,
            tool_slug,
            tool_description,
            parameters_schema,
        }
    }
}

/// Enumerate available shortcuts (both user-authored and AppShortcuts).
pub async fn list_shortcuts(args_json: &str) -> Result<String, String> {
    let args: ListShortcutsArgs =
        serde_json::from_str(args_json).map_err(|e| format!("invalid args: {e}"))?;
    let list = shortcuts::list(args.folder.as_deref(), args.with_identifiers)
        .await
        .map_err(|e| format!("{e}"))?;
    let json: Vec<ShortcutJson> = list.into_iter().map(Into::into).collect();
    serde_json::to_string(&json).map_err(|e| format!("serialize: {e}"))
}

#[derive(Debug, Deserialize)]
pub struct RunShortcutArgs {
    /// Name or UUID of the shortcut to invoke. Prefer UUID.
    pub name_or_id: String,
    /// Optional input passed to the shortcut as Shortcut Input.
    #[serde(default)]
    pub input: Option<String>,
    /// Output Uniform Type Identifier — e.g. `"public.plain-text"`.
    #[serde(default)]
    pub output_type: Option<String>,
    /// Soft timeout in milliseconds.
    #[serde(default)]
    pub timeout_ms: Option<u64>,
}

/// Invoke a shortcut. Returns JSON `{stdout, stderr, exit_code}`.
pub async fn run_shortcut(args_json: &str) -> Result<String, String> {
    let args: RunShortcutArgs =
        serde_json::from_str(args_json).map_err(|e| format!("invalid args: {e}"))?;
    let timeout = args.timeout_ms.map(std::time::Duration::from_millis);
    let input_bytes = args.input.as_ref().map(|s| s.as_bytes());
    let out = shortcuts::run(
        &args.name_or_id,
        input_bytes,
        args.output_type.as_deref(),
        timeout,
    )
    .await
    .map_err(|e| format!("{e}"))?;
    serde_json::to_string(&AutomationOutputJson::from(out)).map_err(|e| format!("serialize: {e}"))
}