Current section

Files

Jump to
claude_code lib claude_code cli parser.ex
Raw

lib/claude_code/cli/parser.ex

defmodule ClaudeCode.CLI.Parser do
@moduledoc """
Parses CLI JSON output into message and content structs.
This module is the CLI protocol layer responsible for converting
newline-delimited JSON from `--output-format stream-json` into
the adapter-agnostic struct types defined in `ClaudeCode.Message.*`
and `ClaudeCode.Content.*`.
A future native API adapter would produce the same structs but from
a different wire format. The struct definitions and type-checking
functions remain in `ClaudeCode.Message` and `ClaudeCode.Content`.
"""
alias ClaudeCode.Content.TextBlock
alias ClaudeCode.Content.ThinkingBlock
alias ClaudeCode.Content.ToolResultBlock
alias ClaudeCode.Content.ToolUseBlock
alias ClaudeCode.Message.AssistantMessage
alias ClaudeCode.Message.CompactBoundaryMessage
alias ClaudeCode.Message.PartialAssistantMessage
alias ClaudeCode.Message.ResultMessage
alias ClaudeCode.Message.SystemMessage
alias ClaudeCode.Message.UserMessage
# -- Message parsing --------------------------------------------------------
@doc """
Parses a decoded JSON map into a message struct.
Dispatches on `"type"` to the appropriate message module's `new/1`
constructor.
## Examples
iex> ClaudeCode.CLI.Parser.parse_message(%{"type" => "system", "subtype" => "init", ...})
{:ok, %ClaudeCode.Message.SystemMessage{...}}
iex> ClaudeCode.CLI.Parser.parse_message(%{"type" => "unknown"})
{:error, {:unknown_message_type, "unknown"}}
"""
@spec parse_message(map()) :: {:ok, ClaudeCode.Message.t()} | {:error, term()}
def parse_message(%{"type" => type} = data) do
case type do
"system" -> parse_system(data)
"assistant" -> AssistantMessage.new(data)
"user" -> UserMessage.new(data)
"result" -> ResultMessage.new(data)
"stream_event" -> PartialAssistantMessage.new(data)
other -> {:error, {:unknown_message_type, other}}
end
end
def parse_message(_), do: {:error, :missing_type}
@doc """
Parses a list of decoded JSON maps into message structs.
Returns `{:ok, messages}` if all messages parse successfully,
or `{:error, {:parse_error, index, error}}` for the first failure.
"""
@spec parse_all_messages(list(map())) :: {:ok, [ClaudeCode.Message.t()]} | {:error, term()}
def parse_all_messages(messages) when is_list(messages) do
messages
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {message, index}, {:ok, acc} ->
case parse_message(message) do
{:ok, parsed} -> {:cont, {:ok, [parsed | acc]}}
{:error, error} -> {:halt, {:error, {:parse_error, index, error}}}
end
end)
|> case do
{:ok, parsed} -> {:ok, Enum.reverse(parsed)}
error -> error
end
end
@doc """
Parses a newline-delimited JSON stream from the CLI.
This is the format output by the CLI with `--output-format stream-json`.
Each line is a complete JSON object representing a single message.
"""
@spec parse_stream(String.t()) :: {:ok, [ClaudeCode.Message.t()]} | {:error, term()}
def parse_stream(stream) when is_binary(stream) do
stream
|> String.split("\n", trim: true)
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {line, index}, {:ok, acc} ->
case Jason.decode(line) do
{:ok, json} ->
case parse_message(json) do
{:ok, message} -> {:cont, {:ok, [message | acc]}}
{:error, error} -> {:halt, {:error, {:parse_error, index, error}}}
end
{:error, error} ->
{:halt, {:error, {:json_decode_error, index, error}}}
end
end)
|> case do
{:ok, messages} -> {:ok, Enum.reverse(messages)}
error -> error
end
end
# -- Content parsing --------------------------------------------------------
@doc """
Parses a decoded JSON map into a content block struct.
Dispatches on `"type"` to the appropriate content module's `new/1`
constructor.
## Examples
iex> ClaudeCode.CLI.Parser.parse_content(%{"type" => "text", "text" => "Hello"})
{:ok, %ClaudeCode.Content.TextBlock{type: :text, text: "Hello"}}
iex> ClaudeCode.CLI.Parser.parse_content(%{"type" => "unknown"})
{:error, {:unknown_content_type, "unknown"}}
"""
@spec parse_content(map()) :: {:ok, ClaudeCode.Content.t()} | {:error, term()}
def parse_content(%{"type" => type} = data) do
case type do
"text" -> TextBlock.new(data)
"thinking" -> ThinkingBlock.new(data)
"tool_use" -> ToolUseBlock.new(data)
"tool_result" -> ToolResultBlock.new(data)
other -> {:error, {:unknown_content_type, other}}
end
end
def parse_content(_), do: {:error, :missing_type}
@doc """
Parses a list of decoded JSON maps into content block structs.
Returns `{:ok, contents}` if all blocks parse successfully,
or `{:error, {:parse_error, index, error}}` for the first failure.
"""
@spec parse_all_contents(list(map())) :: {:ok, [ClaudeCode.Content.t()]} | {:error, term()}
def parse_all_contents(blocks) when is_list(blocks) do
blocks
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {block, index}, {:ok, acc} ->
case parse_content(block) do
{:ok, content} -> {:cont, {:ok, [content | acc]}}
{:error, error} -> {:halt, {:error, {:parse_error, index, error}}}
end
end)
|> case do
{:ok, contents} -> {:ok, Enum.reverse(contents)}
error -> error
end
end
# -- Private: system message dispatch ---------------------------------------
defp parse_system(%{"subtype" => "compact_boundary"} = data) do
CompactBoundaryMessage.new(data)
end
defp parse_system(%{"subtype" => _} = data) do
SystemMessage.new(data)
end
defp parse_system(_), do: {:error, :invalid_system_subtype}
end