# Copilot Supercharged SDK for Elixir

Elixir SDK for the [GitHub Copilot CLI](https://github.com/jeremiahjordanisaacson/copilot-sdk-supercharged). Communicates with the Copilot CLI server via JSON-RPC 2.0 over stdio.

## Requirements

- Elixir 1.15+
- The `copilot` CLI installed and on your PATH (or specify the path via `:cli_path`)
- Authenticated via `copilot auth login`

## Installation

Add to your `mix.exs` dependencies:

```elixir
def deps do
  [
    {:copilot_sdk_supercharged, path: "../elixir"}  # or from Hex when published
  ]
end
```

Then fetch dependencies:

```bash
mix deps.get
```

## Quick Start

```elixir
alias Copilot.Client
alias Copilot.Session
alias Copilot.DefineTool
alias Copilot.Types.{CopilotClientOptions, SessionConfig, MessageOptions}

# Start the client (spawns the CLI process automatically)
{:ok, client} = Client.start_link(%CopilotClientOptions{log_level: "info"})

# Create a session
{:ok, session} = Client.create_session(client, %SessionConfig{})

# Subscribe to events
Session.on(session, fn event ->
  if event["type"] == "assistant.message" do
    IO.puts("Assistant: " <> event["data"]["content"])
  end
end)

# Send a message and wait for the response
{:ok, response} = Session.send_and_wait(session, %MessageOptions{prompt: "What is 2+2?"})
IO.puts("Response: #{response["data"]["content"]}")

# Clean up
Session.destroy(session)
Client.stop(client)
```

## Architecture

The SDK consists of the following modules:

| Module | Description |
|---|---|
| `Copilot.Client` | Main client GenServer. Spawns the CLI process, manages the JSON-RPC connection, and provides session lifecycle operations (create, resume, delete, list). |
| `Copilot.Session` | Session GenServer. Sends messages, subscribes to events, handles tool calls, permissions, user input, and hooks. |
| `Copilot.JsonRpcClient` | Low-level JSON-RPC 2.0 client using Erlang `Port` for stdio communication with Content-Length header framing. |
| `Copilot.Types` | All type definitions as Elixir structs with typespecs. |
| `Copilot.DefineTool` | Helper for defining tools to expose to the CLI. |
| `Copilot.SdkProtocolVersion` | Protocol version constant (must match the server). |

## Defining Tools

Tools allow the assistant to call custom functions you define. Use `Copilot.DefineTool.define/2`:

```elixir
tool = Copilot.DefineTool.define("get_weather",
  description: "Get the current weather for a city.",
  parameters: %{
    "type" => "object",
    "properties" => %{
      "city" => %{"type" => "string", "description" => "City name"}
    },
    "required" => ["city"]
  },
  handler: fn %{"city" => city}, _invocation ->
    "The weather in #{city} is sunny, 72F."
  end
)

{:ok, session} = Client.create_session(client, %SessionConfig{tools: [tool]})
```

The handler function receives two arguments:

1. The parsed arguments (a map matching your JSON schema)
2. A `Copilot.Types.ToolInvocation` struct with session context

It can return:
- A plain string (wrapped as a success result)
- A `Copilot.Types.ToolResult` struct (for full control)
- Any other term (JSON-encoded as a success result)

## Image Generation

Request image responses using `response_format` and `image_options`:

```elixir
response = Copilot.Session.send_and_wait(session, %Copilot.Types.MessageOptions{
  prompt: "Generate a sunset over mountains",
  response_format: :image,
  image_options: %{size: "1024x1024", quality: "hd", style: "natural"}
})
```

## Event Subscriptions

Subscribe to all events or specific event types:

```elixir
# All events
ref = Session.on(session, fn event -> IO.inspect(event) end)

# Specific event type
ref = Session.on(session, "assistant.message", fn event ->
  IO.puts(event["data"]["content"])
end)

# Unsubscribe
Session.off(session, ref)
```

### Common Event Types

| Event Type | Description |
|---|---|
| `session.start` | Session started |
| `session.idle` | Session finished processing |
| `session.error` | An error occurred |
| `user.message` | User message recorded |
| `assistant.message` | Final assistant response |
| `assistant.message_delta` | Streaming response chunk (when streaming enabled) |
| `tool.execution_start` | Tool execution started |
| `tool.execution_complete` | Tool execution finished |

## Permissions

Handle permission requests from the server:

```elixir
alias Copilot.Types.{PermissionRequest, PermissionRequestResult}

config = %SessionConfig{
  on_permission_request: fn %PermissionRequest{kind: kind}, _ctx ->
    IO.puts("Permission requested: #{kind}")
    %PermissionRequestResult{kind: :approved}
  end
}
```

## User Input (ask_user)

Handle user input requests from the agent:

```elixir
alias Copilot.Types.{UserInputRequest, UserInputResponse}

config = %SessionConfig{
  on_user_input_request: fn %UserInputRequest{question: q}, _ctx ->
    answer = IO.gets("#{q} > ") |> String.trim()
    %UserInputResponse{answer: answer, was_freeform: true}
  end
}
```

## Hooks

Intercept session lifecycle events with hooks:

```elixir
alias Copilot.Types.SessionHooks

config = %SessionConfig{
  hooks: %SessionHooks{
    on_pre_tool_use: fn input, _ctx ->
      IO.puts("About to use tool: #{input["toolName"]}")
      %{"permissionDecision" => "allow"}
    end,
    on_post_tool_use: fn input, _ctx ->
      IO.puts("Tool completed: #{input["toolName"]}")
      nil
    end
  }
}
```

## Session Management

```elixir
# List all sessions
{:ok, sessions} = Client.list_sessions(client)

# Resume a previous session
{:ok, session} = Client.resume_session(client, session_id)

# Delete a session
:ok = Client.delete_session(client, session_id)

# Get the last session ID
{:ok, last_id} = Client.get_last_session_id(client)

# List available models
{:ok, models} = Client.list_models(client)
```

### Session Idle Timeout

Configure automatic session cleanup after a period of inactivity:

```elixir
{:ok, client} = CopilotClient.start_link(session_idle_timeout_seconds: 300)
```

### SessionFs (Persistent Session Filesystem)

SessionFs provides a virtual filesystem scoped to each session, enabling persistent state across compaction boundaries and session resumes.

```elixir
{:ok, client} = CopilotClient.start_link(
  session_fs: %{
    initial_cwd: "/repo",
    session_state_path: "/tmp/state",
    conventions: "posix"
  }
)
```

### Session Metadata

Retrieve metadata about a session (model, creation time, status):

```elixir
{:ok, meta} = CopilotClient.get_session_metadata(client, "session-123")
```

### Skills and Sub-Agent Orchestration

Register skill directories and control sub-agent behavior:

```elixir
{:ok, session} = CopilotClient.create_session(client,
  skill_directories: ["./skills"],
  disabled_skills: ["test-skill"],
  include_sub_agent_streaming_events: true
)
```

- `skill_directories` - directories to scan for skill definitions
- `disabled_skills` - skills to exclude from the session
- `include_sub_agent_streaming_events` - receive streaming events from sub-agents

## Custom Providers (BYOK)

Use your own API endpoint:

```elixir
alias Copilot.Types.ProviderConfig

config = %SessionConfig{
  provider: %ProviderConfig{
    type: "openai",
    base_url: "http://localhost:11434/v1",
    api_key: "ollama"
  },
  model: "llama3"
}
```

## Recent Features (v2.4–v2.5)

Fields added in the v2.4 and v2.5 upstream syncs. These are `%SessionConfig{}`,
`%CopilotClientOptions{}`, or `%MessageOptions{}` fields (all under
`Copilot.Types`).

**v2.5.0**

- Reasoning effort — `reasoning_effort`
- Tool search — `tool_search`
- Session rewind — `rewind_enabled`
- Additional directories — `additional_directories`
- Disabled MCP servers — `disabled_mcp_servers`
- GitHub MCP tool config — `github_mcp_tool_config`
- Canvas provider — `canvas_provider`
- Custom agents local-only — `custom_agents_local_only`
- Experimental mode — `experimental_mode`
- Content exclusion — `content_exclusion`
- User-prompt-transformed hook — `SessionHooks.on_user_prompt_transformed`
- Permission decision context — `PermissionRequestResult.decision_context`
- Agent-factory args schema — `CustomAgentConfig.args_schema`
- Built-in plugin directories — `CopilotClientOptions.builtin_plugin_directories`
- In-process FFI transport — `CopilotClientOptions.in_process`

**v2.4.0**

- BYOK bearer-token provider — `CopilotClientOptions.bearer_token_provider`
- MCP OAuth token handler — `on_mcp_auth_request`
- HTTP request handler — `CopilotClientOptions.request_handler`
- Session citations — `enable_citations`
- Excluded built-in agents — `excluded_builtin_agents`
- Session spending limits — `Copilot.Types.SessionLimitsConfig` (`max_ai_credits`)
- Session memory — `Copilot.Types.MemoryConfiguration`
- OTLP protocol — `otlp_protocol`
- WebSocket responses — `enable_web_socket_responses`
- Experiment assignments — `exp_assignments`
- Tool defer loading — `Copilot.Types.ToolDefer` (`auto/0`, `never/0`)
- System-message sections — `SystemMessageSection.preamble/0`, `SystemMessageSection.preserve/0`
- Post-tool-use / pre-MCP hooks — `on_post_tool_use`, `on_pre_mcp_tool_call`
- Message agent mode / display prompt — `MessageOptions.agent_mode`, `MessageOptions.display_prompt`
- GitHub attachment variants — `GitHubAttachment.github_commit/0`, `GitHubAttachment.github_repository/0`

Reasoning effort, content exclusion, extra directories, and rewind:

```elixir
{:ok, session} = Client.create_session(client, %SessionConfig{
  reasoning_effort: "high",
  content_exclusion: true,
  additional_directories: ["../shared", "../docs"],
  rewind_enabled: true,
  disabled_mcp_servers: ["playwright"]
})
```

Tool search and experimental mode:

```elixir
config = %SessionConfig{
  tool_search: %{"enabled" => true},
  experimental_mode: true,
  custom_agents_local_only: true
}
```

Session spending limits, memory, and citations:

```elixir
alias Copilot.Types.{SessionConfig, SessionLimitsConfig, MemoryConfiguration}

config = %SessionConfig{
  session_limits: %SessionLimitsConfig{max_ai_credits: 5.0},
  memory: %MemoryConfiguration{enabled: true},
  enable_citations: true,
  otlp_protocol: "http/protobuf"
}
```

BYOK bearer-token provider and in-process transport (client options):

```elixir
{:ok, client} = Client.start_link(%CopilotClientOptions{
  bearer_token_provider: fn _args -> "ghs_ephemeral_token" end,
  builtin_plugin_directories: ["./plugins"],
  in_process: false
})
```

## Protocol Version

The SDK protocol version must match the CLI server's version. The current version is **2**. Version mismatches will produce a clear error message on connection.

## Running the Example

```bash
cd elixir
mix deps.get
mix run examples/basic_example.exs
```

## License

MIT - See [LICENSE](../LICENSE) for details.
