github-copilot-sdk 1.0.13

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
Documentation
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};

use serde_json::{Value, json};

fn main() {
    let session_id = std::env::var("SESSION_ID").expect("SESSION_ID");
    record_state(json!({
        "kind": "start",
        "pid": std::process::id(),
        "sessionId": session_id,
        "sdkPath": std::env::var("COPILOT_SDK_PATH").ok(),
        "parentPid": std::env::var("COPILOT_EXTENSION_PARENT_PID").ok(),
        "extensionPath": std::env::var("EXTENSION_PATH").ok(),
        "autoUpdate": std::env::var("COPILOT_AUTO_UPDATE").ok(),
        "cliDistDir": std::env::var("COPILOT_CLI_DIST_DIR").ok(),
    }));

    let stdin = std::io::stdin();
    let mut reader = BufReader::new(stdin.lock());
    let stdout = std::io::stdout();
    let mut writer = stdout.lock();
    write_message(
        &mut writer,
        &json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "session.resume",
            "params": {
                "sessionId": session_id,
                "tools": [{
                    "name": "fixture_echo",
                    "description": "Returns a deterministic fixture result",
                    "parameters": {
                        "type": "object",
                        "properties": { "text": { "type": "string" } },
                        "required": ["text"]
                    }
                }],
                "requestPermission": false,
                "enableConfigDiscovery": false,
                "disableResume": true,
                "streaming": true
            }
        }),
    );

    let mut next_id = 2_u64;
    while let Some(message) = read_message(&mut reader) {
        if message.get("id") == Some(&json!(1)) {
            assert!(
                message.get("error").is_none(),
                "extension resume failed: {message}"
            );
            continue;
        }
        if message.pointer("/params/event/type") != Some(&json!("external_tool.requested")) {
            continue;
        }
        let Some(request_id) = message
            .pointer("/params/event/data/requestId")
            .and_then(Value::as_str)
        else {
            continue;
        };
        record_state(json!({
            "kind": "invoke",
            "pid": std::process::id(),
            "requestId": request_id,
        }));
        write_message(
            &mut writer,
            &json!({
                "jsonrpc": "2.0",
                "id": next_id,
                "method": "session.tools.handlePendingToolCall",
                "params": {
                    "sessionId": session_id,
                    "requestId": request_id,
                    "result": "echoed"
                }
            }),
        );
        next_id += 1;
    }
}

fn record_state(value: Value) {
    let path = std::env::var("FIXTURE_STATE_PATH").expect("FIXTURE_STATE_PATH");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .expect("open fixture state");
    writeln!(file, "{value}").expect("write fixture state");
}

fn write_message(writer: &mut impl Write, value: &Value) {
    let body = serde_json::to_vec(value).expect("serialize fixture message");
    write!(writer, "Content-Length: {}\r\n\r\n", body.len()).expect("write fixture header");
    writer.write_all(&body).expect("write fixture body");
    writer.flush().expect("flush fixture message");
}

fn read_message(reader: &mut impl BufRead) -> Option<Value> {
    let mut content_length = None;
    loop {
        let mut line = String::new();
        if reader.read_line(&mut line).expect("read fixture header") == 0 {
            return None;
        }
        if line == "\r\n" {
            break;
        }
        if let Some(value) = line.strip_prefix("Content-Length:") {
            content_length = Some(
                value
                    .trim()
                    .parse::<usize>()
                    .expect("parse fixture content length"),
            );
        }
    }
    let mut body = vec![0; content_length.expect("fixture content length")];
    reader.read_exact(&mut body).expect("read fixture body");
    Some(serde_json::from_slice(&body).expect("parse fixture message"))
}