defmodule ClaudeWrapper do @moduledoc """ Elixir wrapper for the Claude Code CLI. Two ways to drive `claude` from Elixir: 1. `ClaudeWrapper.DuplexSession` -- a `GenServer` that holds **one** `claude` subprocess open for the lifetime of a conversation. Streams partial tokens as they arrive, supports clean mid-turn cancellation, and routes tool-permission prompts back to the host. The right fit for chat UIs, agent runtimes, Phoenix servers, and any long-running OTP host. 2. The convenience `ClaudeWrapper.query/2` and `ClaudeWrapper.stream/2` functions in this module -- one subprocess per turn, simple request/response. The right fit for `mix` tasks, escripts, batch jobs, and anything else that runs and exits. Both modes are first-class. Pick whichever matches the lifecycle of the host using `claude_wrapper`. ## Long-lived chat-style sessions (DuplexSession) config = ClaudeWrapper.Config.new(working_dir: ".") {:ok, pid} = ClaudeWrapper.DuplexSession.start_link(config: config) ClaudeWrapper.DuplexSession.subscribe(pid) {:ok, result} = ClaudeWrapper.DuplexSession.send(pid, "Explain this codebase.") # Subscriber inbox now holds streaming events: # {:claude, {:assistant, msg}} # {:claude, {:stream_event, msg}} -- partial token deltas # {:claude, {:result, %ClaudeWrapper.Result{}}} ClaudeWrapper.DuplexSession.close(pid) See `ClaudeWrapper.DuplexSession` for the permission callback, interrupt API, and full event vocabulary. `ClaudeWrapper.DuplexIEx` wraps it with REPL helpers that stream tokens to stdout live. ## One-shot queries # One-shot query (convenience -- uses default config) {:ok, result} = ClaudeWrapper.query("Explain this error: ...") # With options {:ok, result} = ClaudeWrapper.query("Fix the bug in lib/foo.ex", model: "sonnet", working_dir: "/path/to/project", max_turns: 5, permission_mode: :bypass_permissions ) # Streaming ClaudeWrapper.stream("Implement the feature described in issue #42", working_dir: "/path/to/project" ) |> Stream.each(fn event -> IO.inspect(event.type) end) |> Stream.run() # Full control via Query builder config = ClaudeWrapper.Config.new(working_dir: "/path/to/project") ClaudeWrapper.Query.new("Fix the tests") |> ClaudeWrapper.Query.model("sonnet") |> ClaudeWrapper.Query.dangerously_skip_permissions() |> ClaudeWrapper.Query.max_turns(10) |> ClaudeWrapper.Query.execute(config) ## Multi-turn continuity for one-shot mode (Session) `ClaudeWrapper.Session` threads `--resume ` across one-shot calls so you can have a multi-turn conversation without holding a subprocess open. Use it when you want a struct-passing API rather than a process API, or when turns are far enough apart that cold-start cost doesn't matter. ## Other modules * `ClaudeWrapper.Query` -- the query builder * `ClaudeWrapper.SessionServer` -- supervised wrapper for `Session` * `ClaudeWrapper.McpConfig` -- programmatic `.mcp.json` builder * `ClaudeWrapper.Retry` -- exponential backoff * `ClaudeWrapper.Telemetry` -- `:telemetry` spans for exec/stream/session * `ClaudeWrapper.Commands.{Auth, Mcp, Plugin, Marketplace, Doctor, Version}` -- CLI subcommand wrappers ## Binary discovery The `claude` binary is found via (in order): 1. `:binary` option passed directly 2. `CLAUDE_CLI` environment variable 3. System PATH lookup """ alias ClaudeWrapper.{Commands, Config, Query, Result} @doc """ Run an arbitrary CLI command that isn't wrapped by a dedicated module. This is the escape hatch for new or experimental CLI subcommands. ## Examples ClaudeWrapper.raw(["config", "list"]) ClaudeWrapper.raw(["plugin", "install", "my-plugin"], working_dir: "/tmp") """ @spec raw([String.t()], keyword()) :: {:ok, String.t()} | {:error, term()} def raw(args, opts \\ []) when is_list(args) do config = Config.new(opts) all_args = Config.base_args(config) ++ args cmd_opts = Config.cmd_opts(config) case System.cmd(config.binary, all_args, cmd_opts) do {output, 0} -> {:ok, String.trim(output)} {output, code} -> {:error, {:exit, code, output}} end rescue e in ErlangError -> {:error, {:system_cmd, e}} end @doc """ Execute a one-shot query and return the result. Convenience wrapper that builds a `Config` and `Query` from keyword options. Returns `{:ok, %Result{}}` on success or `{:error, reason}` on failure. ## Options Config options (passed to `ClaudeWrapper.Config.new/1`): * `:binary` - Path to claude binary * `:working_dir` - Working directory * `:env` - Environment variables * `:timeout` - Timeout in ms * `:verbose` - Enable verbose output * `:debug` - Enable debug output Query options (passed to `Query` builder): * `:model` - Model name * `:system_prompt` - System prompt override * `:max_turns` - Max turns * `:max_budget_usd` - Budget limit * `:permission_mode` - Permission mode atom * `:dangerously_skip_permissions` - Bypass permissions (boolean) * `:session_id` - Session ID * `:continue_session` - Continue recent session (boolean) * `:resume` - Resume session ID """ @spec query(String.t(), keyword()) :: {:ok, Result.t()} | {:error, term()} def query(prompt, opts \\ []) do {config_opts, query_opts} = split_opts(opts) config = Config.new(config_opts) query = build_query(prompt, query_opts) Query.execute(query, config) end @doc """ Execute a query and return a lazy stream of `%StreamEvent{}` structs. The subprocess starts when the stream is consumed. Accepts the same options as `query/2`. """ @spec stream(String.t(), keyword()) :: Enumerable.t() def stream(prompt, opts \\ []) do {config_opts, query_opts} = split_opts(opts) config = Config.new(config_opts) query = build_query(prompt, query_opts) Query.stream(query, config) end @doc """ Get the CLI version. """ @spec version(keyword()) :: {:ok, map()} | {:error, term()} def version(opts \\ []) do config = Config.new(opts) Commands.Version.execute(config) end @doc """ Check authentication status. """ @spec auth_status(keyword()) :: {:ok, map()} | {:error, term()} def auth_status(opts \\ []) do config = Config.new(opts) Commands.Auth.status(config) end @doc """ Run `claude doctor`. """ @spec doctor(keyword()) :: {:ok, String.t()} | {:error, term()} def doctor(opts \\ []) do config = Config.new(opts) Commands.Doctor.execute(config) end @doc """ List configured agents. Returns a list of agent maps with `:name` and `:model` keys. ## Options * `:setting_sources` - comma-separated setting sources (e.g. `"user,project"`) """ @spec agents(keyword()) :: {:ok, [map()]} | {:error, term()} def agents(opts \\ []) do {config_opts, agent_opts} = split_opts(opts) config = Config.new(config_opts) Commands.Agents.list(config, agent_opts) end # --- Private --- @config_keys [:binary, :working_dir, :env, :timeout, :verbose, :debug] defp split_opts(opts) do Enum.split_with(opts, fn {k, _v} -> k in @config_keys end) end defp build_query(prompt, opts) do prompt |> Query.new() |> Query.apply_opts(opts) end end