defmodule ClaudeWrapper do @moduledoc """ Elixir wrapper for the Claude Code CLI. Provides a typed interface for executing queries against the `claude` CLI, with support for one-shot execution and streaming NDJSON output. ## Quick start # 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) ## Commands * `ClaudeWrapper.Query` -- the main query/prompt interface * `ClaudeWrapper.Commands.Auth` -- login, logout, status, token * `ClaudeWrapper.Commands.Mcp` -- MCP server management * `ClaudeWrapper.Commands.Doctor` -- CLI health check * `ClaudeWrapper.Commands.Version` -- CLI version ## 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