My agentic slop goes here. Not intended for anyone else!

moe

+2
claudeio/claude.opam
···
"ocaml"
"dune" {>= "3.0"}
"eio"
+
"fmt"
+
"logs"
"ezjsonm"
"alcotest" {with-test}
"odoc" {with-doc}
+3 -1
claudeio/dune-project
···
ocaml
dune
eio
+
fmt
+
logs
ezjsonm
-
(alcotest :with-test)))
+
(alcotest :with-test)))
+2 -1
claudeio/lib/claude.ml
···
-
module Types = Types
+
module Content_block = Content_block
module Message = Message
+
module Control = Control
module Permissions = Permissions
module Options = Options
module Transport = Transport
+165 -2
claudeio/lib/claude.mli
···
-
module Types = Types
+
(** OCaml Eio library for Claude Code CLI.
+
+
This library provides an interface to the Claude Code command-line interface
+
using OCaml's Eio concurrency library. It wraps Claude CLI invocations with
+
JSON streaming for asynchronous communication.
+
+
{1 Overview}
+
+
The Claude library enables you to:
+
- Send messages to Claude and receive streaming responses
+
- Control tool permissions and execution
+
- Configure system prompts and model parameters
+
- Handle content blocks including text, tool use, and thinking blocks
+
- Manage sessions with proper resource cleanup
+
+
{1 Architecture}
+
+
The library is structured into several focused modules:
+
+
- {!Content_block}: Defines content blocks (text, tool use, tool results, thinking)
+
- {!Message}: Messages exchanged with Claude (user, assistant, system, result)
+
- {!Control}: Control flow messages for session management
+
- {!Permissions}: Fine-grained permission system for tool usage
+
- {!Options}: Configuration options for Claude sessions
+
- {!Transport}: Low-level transport layer for CLI communication
+
- {!Client}: High-level client interface for interacting with Claude
+
+
{1 Basic Usage}
+
+
{[
+
open Claude
+
+
(* Create a simple query *)
+
let query_claude ~sw env prompt =
+
let options = Options.default in
+
Client.query ~sw env ~options prompt
+
+
(* Process streaming responses *)
+
let process_response messages =
+
Seq.iter (function
+
| Message.Assistant msg ->
+
List.iter (function
+
| Content_block.Text t ->
+
print_endline (Content_block.Text.text t)
+
| _ -> ()
+
) (Message.Assistant.content msg)
+
| _ -> ()
+
) messages
+
]}
+
+
{1 Advanced Features}
+
+
{2 Tool Permissions}
+
+
Control which tools Claude can use and how:
+
+
{[
+
let options =
+
Options.default
+
|> Options.with_allowed_tools ["Read"; "Write"; "Bash"]
+
|> Options.with_permission_mode Permissions.Mode.Accept_edits
+
]}
+
+
{2 Custom Permission Callbacks}
+
+
Implement custom logic for tool approval:
+
+
{[
+
let my_callback ~tool_name ~input ~context =
+
if tool_name = "Bash" then
+
Permissions.Result.deny ~message:"Bash not allowed" ~interrupt:false
+
else
+
Permissions.Result.allow ()
+
+
let options = Options.default |> Options.with_permission_callback my_callback
+
]}
+
+
{2 System Prompts}
+
+
Customize Claude's behavior with system prompts:
+
+
{[
+
let options =
+
Options.default
+
|> Options.with_system_prompt "You are a helpful OCaml programming assistant."
+
|> Options.with_append_system_prompt "Always use Jane Street style."
+
]}
+
+
{1 Logging}
+
+
The library uses the Logs library for structured logging. Each module has its
+
own log source (e.g., "claude.message", "claude.transport") allowing fine-grained
+
control over logging verbosity:
+
+
{[
+
(* Enable debug logging for message handling *)
+
Logs.Src.set_level Message.src (Some Logs.Debug);
+
+
(* Enable info logging for transport layer *)
+
Logs.Src.set_level Transport.src (Some Logs.Info);
+
]}
+
+
{1 Error Handling}
+
+
The library uses exceptions for error handling. Common exceptions include:
+
- [Invalid_argument]: For malformed JSON or invalid parameters
+
- [Transport.Transport_error]: For CLI communication failures
+
- [Message.Message_parse_error]: For message parsing failures
+
+
{1 Example: Complete Session}
+
+
{[
+
let run_claude_session ~sw env =
+
let options =
+
Options.create
+
~allowed_tools:["Read"; "Write"]
+
~permission_mode:Permissions.Mode.Accept_edits
+
~system_prompt:"You are an OCaml expert."
+
~max_thinking_tokens:10000
+
()
+
in
+
+
let prompt = "Write a function to calculate fibonacci numbers" in
+
let messages = Client.query ~sw env ~options prompt in
+
+
Seq.iter (fun msg ->
+
Message.log_received msg;
+
match msg with
+
| Message.Assistant assistant ->
+
Printf.printf "Claude: %s\n"
+
(Message.Assistant.model assistant);
+
List.iter (function
+
| Content_block.Text t ->
+
print_endline (Content_block.Text.text t)
+
| Content_block.Tool_use t ->
+
Printf.printf "Using tool: %s\n"
+
(Content_block.Tool_use.name t)
+
| _ -> ()
+
) (Message.Assistant.content assistant)
+
| Message.Result result ->
+
Printf.printf "Session complete. Duration: %dms\n"
+
(Message.Result.duration_ms result)
+
| _ -> ()
+
) messages
+
]}
+
*)
+
+
(** {1 Modules} *)
+
+
module Content_block = Content_block
+
(** Content blocks for messages (text, tool use, tool results, thinking). *)
+
module Message = Message
+
(** Messages exchanged with Claude (user, assistant, system, result). *)
+
+
module Control = Control
+
(** Control messages for session management. *)
+
module Permissions = Permissions
+
(** Permission system for tool invocations. *)
+
module Options = Options
+
(** Configuration options for Claude sessions. *)
+
module Transport = Transport
-
module Client = Client
+
(** Low-level transport layer for CLI communication. *)
+
+
module Client = Client
+
(** High-level client interface for Claude interactions. *)
+58 -60
claudeio/lib/client.ml
···
-
open Eio.Std
+
let src = Logs.Src.create "claude.client" ~doc:"Claude client"
+
module Log = (val Logs.src_log src : Logs.LOG)
type t = {
transport : Transport.t;
-
receive_stream : Types.message Eio.Stream.t;
-
receive_fiber : unit Eio.Promise.or_exn;
-
control_stream : Types.control_message Eio.Stream.t;
permission_callback : Permissions.callback option;
-
permission_log : Permissions.rule list ref option;
+
permission_log : Permissions.Rule.t list ref option;
mutable session_id : string option;
}
let handle_control_request t control_msg =
let open Ezjsonm in
-
match find control_msg.Types.data ["request"; "subtype"] |> get_string with
+
let data = Control.data control_msg in
+
match find data ["request"; "subtype"] |> get_string with
| "can_use_tool" ->
-
let tool_name = find control_msg.data ["request"; "tool_name"] |> get_string in
-
let input = find control_msg.data ["request"; "input"] in
+
let tool_name = find data ["request"; "tool_name"] |> get_string in
+
let input = find data ["request"; "input"] in
let suggestions =
try
-
let sugg_json = find control_msg.data ["request"; "permission_suggestions"] in
+
let sugg_json = find data ["request"; "permission_suggestions"] in
match sugg_json with
| `A _ ->
(* TODO: Parse permission suggestions *)
···
| _ -> []
with Not_found -> []
in
-
let context = Permissions.{ suggestions } in
+
let context = Permissions.Context.create ~suggestions () in
let result = match t.permission_callback with
| Some callback -> callback ~tool_name ~input ~context
···
"type", string "control_response";
"response", dict [
"subtype", string "success";
-
"request_id", string control_msg.request_id;
-
"response", Permissions.serialize_result result
+
"request_id", string (Control.request_id control_msg);
+
"response", Permissions.Result.to_json result
]
] in
Transport.send t.transport response
···
"type", string "control_response";
"response", dict [
"subtype", string "error";
-
"request_id", string control_msg.request_id;
+
"request_id", string (Control.request_id control_msg);
"error", string (Printf.sprintf "Unsupported control request: %s" subtype)
]
] in
Transport.send t.transport response
-
let receiver_loop t =
+
let handle_messages t =
let rec loop () =
match Transport.receive_line t.transport with
-
| None -> () (* EOF *)
+
| None ->
+
(* EOF *)
+
Log.debug (fun m -> m "Handle messages: EOF received");
+
Seq.Nil
| Some line ->
try
let json = Ezjsonm.value_from_string line in
···
(* Check if it's a control request *)
match Ezjsonm.find json ["type"] |> Ezjsonm.get_string with
| "control_request" ->
-
let control_msg = Types.{
-
request_id = Ezjsonm.find json ["request_id"] |> Ezjsonm.get_string;
-
subtype = Ezjsonm.find json ["request"; "subtype"] |> Ezjsonm.get_string;
-
data = json;
-
} in
-
Eio.Stream.add t.control_stream control_msg;
-
handle_control_request t control_msg
+
let control_msg = Control.create
+
~request_id:(Ezjsonm.find json ["request_id"] |> Ezjsonm.get_string)
+
~subtype:(Ezjsonm.find json ["request"; "subtype"] |> Ezjsonm.get_string)
+
~data:json in
+
Log.debug (fun m -> m "Received control request: %s" (Control.subtype control_msg));
+
handle_control_request t control_msg;
+
loop ()
| _ ->
(* Regular message *)
-
let msg = Message.parse json in
+
let msg = Message.of_json json in
+
Log.debug (fun m -> m "Received message: %a" Message.pp msg);
(* Extract session ID from system messages *)
(match msg with
-
| Types.System { subtype = "init"; data } ->
+
| Message.System sys when Message.System.subtype sys = "init" ->
(try
-
t.session_id <- Some (Ezjsonm.find data ["session_id"] |> Ezjsonm.get_string)
+
let data = Message.System.data sys in
+
let session_id = Ezjsonm.find data ["session_id"] |> Ezjsonm.get_string in
+
t.session_id <- Some session_id;
+
Log.info (fun m -> m "Session ID: %s" session_id)
with Not_found -> ())
| _ -> ());
-
Eio.Stream.add t.receive_stream msg
+
Seq.Cons (msg, loop)
with
| exn ->
-
traceln "Failed to parse message: %s\nLine: %s"
-
(Printexc.to_string exn) line;
-
loop ()
+
Log.err (fun m -> m "Failed to parse message: %s\nLine: %s"
+
(Printexc.to_string exn) line);
+
loop ()
in
-
try
-
loop ()
-
with
-
| exn ->
-
traceln "Receiver loop error: %s" (Printexc.to_string exn);
-
raise exn
+
Log.debug (fun m -> m "Starting message handler");
+
loop
-
let create ?(options = Options.default) ~sw ~process_mgr ~fs () =
-
let transport = Transport.create ~sw ~process_mgr ~fs ~options () in
-
let receive_stream = Eio.Stream.create 100 in
-
let control_stream = Eio.Stream.create 10 in
-
-
let t = {
+
let create ?(options = Options.default) ~sw ~process_mgr () =
+
let transport = Transport.create ~sw ~process_mgr ~options () in
+
{
transport;
-
receive_stream;
-
receive_fiber = Eio.Promise.create_resolved (Ok ()); (* Placeholder *)
-
control_stream;
-
permission_callback = options.permission_callback;
+
permission_callback = Options.permission_callback options;
permission_log = None;
session_id = None;
-
} in
-
-
(* Start receiver fiber *)
-
let receive_fiber = Fiber.fork_promise ~sw (fun () -> receiver_loop t) in
-
-
{ t with receive_fiber }
+
}
let query t prompt =
-
let msg = Message.serialize_user_message (`String prompt) in
-
Transport.send t.transport msg
+
let msg = Message.user_string prompt in
+
Transport.send t.transport (Message.to_json msg)
let send_message t json =
Transport.send t.transport json
let receive t =
-
t.receive_stream
+
handle_messages t
let receive_all t =
-
let rec collect acc =
-
match Eio.Stream.take t.receive_stream with
-
| Types.Result _ as msg -> List.rev (msg :: acc)
-
| msg -> collect (msg :: acc)
+
let rec collect acc seq =
+
match seq () with
+
| Seq.Nil ->
+
Log.debug (fun m -> m "receive_all: end of sequence, returning %d messages" (List.length acc));
+
List.rev acc
+
| Seq.Cons (Message.Result _ as msg, _) ->
+
Log.debug (fun m -> m "receive_all: got Result message, returning %d messages" (List.length acc + 1));
+
List.rev (msg :: acc)
+
| Seq.Cons (msg, rest) ->
+
Log.debug (fun m -> m "receive_all: got %a, continuing" Message.pp msg);
+
collect (msg :: acc) rest
in
-
collect []
+
collect [] (handle_messages t)
let interrupt t =
Transport.interrupt t.transport
···
}
let with_permission_callback t callback =
-
{ t with permission_callback = Some callback }
+
{ t with permission_callback = Some callback }
+6 -4
claudeio/lib/client.mli
···
+
(** The log source for client operations *)
+
val src : Logs.Src.t
+
type t
val create :
?options:Options.t ->
sw:Eio.Switch.t ->
process_mgr:_ Eio.Process.mgr ->
-
fs:Eio.Fs.dir_ty Eio.Path.t ->
unit -> t
val query : t -> string -> unit
val send_message : t -> Ezjsonm.value -> unit
-
val receive : t -> Types.message Eio.Stream.t
-
val receive_all : t -> Types.message list
+
val receive : t -> Message.t Seq.t
+
val receive_all : t -> Message.t list
val interrupt : t -> unit
val discover_permissions : t -> t
-
val with_permission_callback : t -> Permissions.callback -> t
+
val with_permission_callback : t -> Permissions.callback -> t
+182
claudeio/lib/content_block.ml
···
+
open Ezjsonm
+
+
let src = Logs.Src.create "claude.content_block" ~doc:"Claude content blocks"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
+
(* Helper for pretty-printing JSON *)
+
let pp_json fmt json =
+
Fmt.string fmt (value_to_string json)
+
+
module Text = struct
+
type t = { text : string }
+
+
let create text = { text }
+
let text t = t.text
+
+
let to_json t =
+
`O [("type", `String "text"); ("text", `String t.text)]
+
+
let of_json = function
+
| `O fields ->
+
let text = get_string (List.assoc "text" fields) in
+
{ text }
+
| _ -> raise (Invalid_argument "Text.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Text@ { text = %S }@]" t.text
+
end
+
+
module Tool_use = struct
+
type t = {
+
id : string;
+
name : string;
+
input : value;
+
}
+
+
let create ~id ~name ~input = { id; name; input }
+
let id t = t.id
+
let name t = t.name
+
let input t = t.input
+
+
let to_json t =
+
`O [
+
("type", `String "tool_use");
+
("id", `String t.id);
+
("name", `String t.name);
+
("input", t.input);
+
]
+
+
let of_json = function
+
| `O fields ->
+
let id = get_string (List.assoc "id" fields) in
+
let name = get_string (List.assoc "name" fields) in
+
let input = List.assoc "input" fields in
+
{ id; name; input }
+
| _ -> raise (Invalid_argument "Tool_use.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Tool_use@ { id = %S;@ name = %S;@ input = %a }@]"
+
t.id t.name pp_json t.input
+
end
+
+
module Tool_result = struct
+
type t = {
+
tool_use_id : string;
+
content : string option;
+
is_error : bool option;
+
}
+
+
let create ~tool_use_id ?content ?is_error () =
+
{ tool_use_id; content; is_error }
+
+
let tool_use_id t = t.tool_use_id
+
let content t = t.content
+
let is_error t = t.is_error
+
+
let to_json t =
+
let fields = [
+
("type", `String "tool_result");
+
("tool_use_id", `String t.tool_use_id);
+
] in
+
let fields = match t.content with
+
| Some c -> ("content", `String c) :: fields
+
| None -> fields
+
in
+
let fields = match t.is_error with
+
| Some e -> ("is_error", `Bool e) :: fields
+
| None -> fields
+
in
+
`O fields
+
+
let of_json = function
+
| `O fields ->
+
let tool_use_id = get_string (List.assoc "tool_use_id" fields) in
+
let content =
+
try Some (get_string (List.assoc "content" fields))
+
with Not_found -> None
+
in
+
let is_error =
+
try Some (get_bool (List.assoc "is_error" fields))
+
with Not_found -> None
+
in
+
{ tool_use_id; content; is_error }
+
| _ -> raise (Invalid_argument "Tool_result.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Tool_result@ { tool_use_id = %S;@ content = %a;@ is_error = %a }@]"
+
t.tool_use_id
+
Fmt.(option string) t.content
+
Fmt.(option bool) t.is_error
+
end
+
+
module Thinking = struct
+
type t = {
+
thinking : string;
+
signature : string;
+
}
+
+
let create ~thinking ~signature = { thinking; signature }
+
let thinking t = t.thinking
+
let signature t = t.signature
+
+
let to_json t =
+
`O [
+
("type", `String "thinking");
+
("thinking", `String t.thinking);
+
("signature", `String t.signature);
+
]
+
+
let of_json = function
+
| `O fields ->
+
let thinking = get_string (List.assoc "thinking" fields) in
+
let signature = get_string (List.assoc "signature" fields) in
+
{ thinking; signature }
+
| _ -> raise (Invalid_argument "Thinking.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Thinking@ { thinking = %S;@ signature = %S }@]"
+
t.thinking t.signature
+
end
+
+
type t =
+
| Text of Text.t
+
| Tool_use of Tool_use.t
+
| Tool_result of Tool_result.t
+
| Thinking of Thinking.t
+
+
let text s = Text (Text.create s)
+
let tool_use ~id ~name ~input = Tool_use (Tool_use.create ~id ~name ~input)
+
let tool_result ~tool_use_id ?content ?is_error () =
+
Tool_result (Tool_result.create ~tool_use_id ?content ?is_error ())
+
let thinking ~thinking ~signature =
+
Thinking (Thinking.create ~thinking ~signature)
+
+
let to_json = function
+
| Text t -> Text.to_json t
+
| Tool_use t -> Tool_use.to_json t
+
| Tool_result t -> Tool_result.to_json t
+
| Thinking t -> Thinking.to_json t
+
+
let of_json json =
+
match json with
+
| `O fields -> (
+
match List.assoc_opt "type" fields with
+
| Some (`String "text") -> Text (Text.of_json json)
+
| Some (`String "tool_use") -> Tool_use (Tool_use.of_json json)
+
| Some (`String "tool_result") -> Tool_result (Tool_result.of_json json)
+
| Some (`String "thinking") -> Thinking (Thinking.of_json json)
+
| _ -> raise (Invalid_argument "Content_block.of_json: unknown type")
+
)
+
| _ -> raise (Invalid_argument "Content_block.of_json: expected object")
+
+
let pp fmt = function
+
| Text t -> Text.pp fmt t
+
| Tool_use t -> Tool_use.pp fmt t
+
| Tool_result t -> Tool_result.pp fmt t
+
| Thinking t -> Thinking.pp fmt t
+
+
let log_received t =
+
Log.debug (fun m -> m "Received content block: %a" pp t)
+
+
let log_sending t =
+
Log.debug (fun m -> m "Sending content block: %a" pp t)
+171
claudeio/lib/content_block.mli
···
+
(** Content blocks for Claude messages.
+
+
This module defines the various types of content blocks that can appear
+
in Claude messages, including text, tool use, tool results, and thinking blocks. *)
+
+
open Ezjsonm
+
+
(** The log source for content block operations *)
+
val src : Logs.Src.t
+
+
(** {1 Text Blocks} *)
+
+
module Text : sig
+
(** Plain text content blocks. *)
+
+
type t
+
(** The type of text blocks. *)
+
+
val create : string -> t
+
(** [create text] creates a new text block with the given text content. *)
+
+
val text : t -> string
+
(** [text t] returns the text content of the block. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the text block to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a text block from JSON.
+
@raise Invalid_argument if the JSON is not a valid text block. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the text block. *)
+
end
+
+
(** {1 Tool Use Blocks} *)
+
+
module Tool_use : sig
+
(** Tool invocation requests from the assistant. *)
+
+
type t
+
(** The type of tool use blocks. *)
+
+
val create : id:string -> name:string -> input:value -> t
+
(** [create ~id ~name ~input] creates a new tool use block.
+
@param id Unique identifier for this tool invocation
+
@param name Name of the tool to invoke
+
@param input JSON parameters for the tool *)
+
+
val id : t -> string
+
(** [id t] returns the unique identifier of the tool use. *)
+
+
val name : t -> string
+
(** [name t] returns the name of the tool being invoked. *)
+
+
val input : t -> value
+
(** [input t] returns the JSON input parameters for the tool. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the tool use block to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a tool use block from JSON.
+
@raise Invalid_argument if the JSON is not a valid tool use block. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the tool use block. *)
+
end
+
+
(** {1 Tool Result Blocks} *)
+
+
module Tool_result : sig
+
(** Results from tool invocations. *)
+
+
type t
+
(** The type of tool result blocks. *)
+
+
val create : tool_use_id:string -> ?content:string -> ?is_error:bool -> unit -> t
+
(** [create ~tool_use_id ?content ?is_error ()] creates a new tool result block.
+
@param tool_use_id The ID of the corresponding tool use block
+
@param content Optional result content
+
@param is_error Whether the tool execution resulted in an error *)
+
+
val tool_use_id : t -> string
+
(** [tool_use_id t] returns the ID of the corresponding tool use. *)
+
+
val content : t -> string option
+
(** [content t] returns the optional result content. *)
+
+
val is_error : t -> bool option
+
(** [is_error t] returns whether this result represents an error. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the tool result block to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a tool result block from JSON.
+
@raise Invalid_argument if the JSON is not a valid tool result block. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the tool result block. *)
+
end
+
+
(** {1 Thinking Blocks} *)
+
+
module Thinking : sig
+
(** Assistant's internal reasoning blocks. *)
+
+
type t
+
(** The type of thinking blocks. *)
+
+
val create : thinking:string -> signature:string -> t
+
(** [create ~thinking ~signature] creates a new thinking block.
+
@param thinking The assistant's internal reasoning
+
@param signature Cryptographic signature for verification *)
+
+
val thinking : t -> string
+
(** [thinking t] returns the thinking content. *)
+
+
val signature : t -> string
+
(** [signature t] returns the cryptographic signature. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the thinking block to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a thinking block from JSON.
+
@raise Invalid_argument if the JSON is not a valid thinking block. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the thinking block. *)
+
end
+
+
(** {1 Content Block Union Type} *)
+
+
type t =
+
| Text of Text.t
+
| Tool_use of Tool_use.t
+
| Tool_result of Tool_result.t
+
| Thinking of Thinking.t
+
(** The type of content blocks, which can be text, tool use, tool result, or thinking. *)
+
+
val text : string -> t
+
(** [text s] creates a text content block. *)
+
+
val tool_use : id:string -> name:string -> input:value -> t
+
(** [tool_use ~id ~name ~input] creates a tool use content block. *)
+
+
val tool_result : tool_use_id:string -> ?content:string -> ?is_error:bool -> unit -> t
+
(** [tool_result ~tool_use_id ?content ?is_error ()] creates a tool result content block. *)
+
+
val thinking : thinking:string -> signature:string -> t
+
(** [thinking ~thinking ~signature] creates a thinking content block. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts any content block to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a content block from JSON.
+
@raise Invalid_argument if the JSON is not a valid content block. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints any content block. *)
+
+
(** {1 Logging} *)
+
+
val log_received : t -> unit
+
(** [log_received t] logs that a content block was received. *)
+
+
val log_sending : t -> unit
+
(** [log_sending t] logs that a content block is being sent. *)
+46
claudeio/lib/control.ml
···
+
open Ezjsonm
+
+
let src = Logs.Src.create "claude.control" ~doc:"Claude control messages"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
+
(* Helper for pretty-printing JSON *)
+
let pp_json fmt json =
+
Fmt.string fmt (value_to_string json)
+
+
type t = {
+
request_id : string;
+
subtype : string;
+
data : value;
+
}
+
+
let create ~request_id ~subtype ~data = { request_id; subtype; data }
+
+
let request_id t = t.request_id
+
let subtype t = t.subtype
+
let data t = t.data
+
+
let to_json t =
+
`O [
+
("type", `String "control");
+
("request_id", `String t.request_id);
+
("subtype", `String t.subtype);
+
("data", t.data);
+
]
+
+
let of_json = function
+
| `O fields ->
+
let request_id = get_string (List.assoc "request_id" fields) in
+
let subtype = get_string (List.assoc "subtype" fields) in
+
let data = List.assoc "data" fields in
+
{ request_id; subtype; data }
+
| _ -> raise (Invalid_argument "Control.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Control@ { request_id = %S;@ subtype = %S;@ data = %a }@]"
+
t.request_id t.subtype pp_json t.data
+
+
let log_received t =
+
Log.debug (fun m -> m "Received control message: %a" pp t)
+
+
let log_sending t =
+
Log.debug (fun m -> m "Sending control message: %a" pp t)
+46
claudeio/lib/control.mli
···
+
(** Control messages for Claude session management.
+
+
Control messages are used to manage the interaction flow with Claude,
+
including session control, cancellation requests, and other operational
+
commands. *)
+
+
open Ezjsonm
+
+
(** The log source for control message operations *)
+
val src : Logs.Src.t
+
+
type t
+
(** The type of control messages. *)
+
+
val create : request_id:string -> subtype:string -> data:value -> t
+
(** [create ~request_id ~subtype ~data] creates a new control message.
+
@param request_id Unique identifier for this control request
+
@param subtype The specific type of control message
+
@param data Additional JSON data for the control message *)
+
+
val request_id : t -> string
+
(** [request_id t] returns the unique request identifier. *)
+
+
val subtype : t -> string
+
(** [subtype t] returns the control message subtype. *)
+
+
val data : t -> value
+
(** [data t] returns the additional data associated with the control message. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the control message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a control message from JSON.
+
@raise Invalid_argument if the JSON is not a valid control message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the control message. *)
+
+
(** {1 Logging} *)
+
+
val log_received : t -> unit
+
(** [log_received t] logs that a control message was received. *)
+
+
val log_sending : t -> unit
+
(** [log_sending t] logs that a control message is being sent. *)
+1 -1
claudeio/lib/dune
···
(library
(public_name claude)
(name claude)
-
(libraries eio eio.unix ezjsonm))
+
(libraries eio eio.unix ezjsonm fmt logs))
+252 -101
claudeio/lib/message.ml
···
-
open Types
+
open Ezjsonm
-
exception Message_parse_error of string * Ezjsonm.value
+
let src = Logs.Src.create "claude.message" ~doc:"Claude messages"
+
module Log = (val Logs.src_log src : Logs.LOG)
-
let parse_content_block json =
-
let open Ezjsonm in
-
match find json ["type"] |> get_string with
-
| "text" ->
-
Text { text = find json ["text"] |> get_string }
-
| "tool_use" ->
-
ToolUse {
-
id = find json ["id"] |> get_string;
-
name = find json ["name"] |> get_string;
-
input = find json ["input"];
-
}
-
| "tool_result" ->
-
ToolResult {
-
tool_use_id = find json ["tool_use_id"] |> get_string;
-
content = (try Some (find json ["content"] |> get_string) with Not_found -> None);
-
is_error = (try Some (find json ["is_error"] |> get_bool) with Not_found -> None);
-
}
-
| "thinking" ->
-
Thinking {
-
thinking = find json ["thinking"] |> get_string;
-
signature = find json ["signature"] |> get_string;
-
}
-
| t -> raise (Message_parse_error (Printf.sprintf "Unknown content block type: %s" t, json))
+
(* Helper for pretty-printing JSON *)
+
let pp_json fmt json =
+
Fmt.string fmt (value_to_string json)
-
let parse json =
-
let open Ezjsonm in
-
try
-
match find json ["type"] |> get_string with
-
| "user" ->
-
let message = find json ["message"] in
-
let content_json = find message ["content"] in
-
let content =
-
match content_json with
-
| `String s -> `String s
-
| `A blocks ->
-
let parsed_blocks = List.map parse_content_block blocks in
-
`Blocks parsed_blocks
-
| _ -> `String (value_to_string content_json)
+
module User = struct
+
type content =
+
| String of string
+
| Blocks of Content_block.t list
+
+
type t = { content : content }
+
+
let create_string s = { content = String s }
+
let create_blocks blocks = { content = Blocks blocks }
+
let content t = t.content
+
+
let to_json t =
+
let content_json = match t.content with
+
| String s -> `String s
+
| Blocks blocks ->
+
`A (List.map Content_block.to_json blocks)
+
in
+
`O [
+
("type", `String "user");
+
("message", `O [
+
("role", `String "user");
+
("content", content_json);
+
]);
+
]
+
+
let of_json = function
+
| `O fields ->
+
let message = List.assoc "message" fields in
+
let content = match message with
+
| `O msg_fields ->
+
(match List.assoc "content" msg_fields with
+
| `String s -> String s
+
| `A blocks -> Blocks (List.map Content_block.of_json blocks)
+
| _ -> raise (Invalid_argument "User.of_json: invalid content"))
+
| _ -> raise (Invalid_argument "User.of_json: invalid message")
in
-
User { content }
-
-
| "assistant" ->
-
let message = find json ["message"] in
-
let content_blocks = find message ["content"] |> get_list parse_content_block in
-
let model = find message ["model"] |> get_string in
-
Assistant { content = content_blocks; model }
-
-
| "system" ->
-
let subtype = find json ["subtype"] |> get_string in
-
System { subtype; data = json }
-
-
| "result" ->
-
Result {
-
subtype = find json ["subtype"] |> get_string;
-
duration_ms = find json ["duration_ms"] |> get_int;
-
duration_api_ms = find json ["duration_api_ms"] |> get_int;
-
is_error = find json ["is_error"] |> get_bool;
-
num_turns = find json ["num_turns"] |> get_int;
-
session_id = find json ["session_id"] |> get_string;
-
total_cost_usd = (try Some (find json ["total_cost_usd"] |> get_float) with Not_found -> None);
-
usage = (try Some (find json ["usage"]) with Not_found -> None);
-
result = (try Some (find json ["result"] |> get_string) with Not_found -> None);
-
}
-
-
| msg_type ->
-
raise (Message_parse_error (Printf.sprintf "Unknown message type: %s" msg_type, json))
-
with
-
| Message_parse_error _ as e -> raise e
-
| e ->
-
raise (Message_parse_error (Printf.sprintf "Failed to parse message: %s" (Printexc.to_string e), json))
+
{ content }
+
| _ -> raise (Invalid_argument "User.of_json: expected object")
+
+
let pp fmt t =
+
match t.content with
+
| String s -> Fmt.pf fmt "@[<2>User@ { content = %S }@]" s
+
| Blocks blocks ->
+
Fmt.pf fmt "@[<2>User@ { content = @[<v>%a@] }@]"
+
Fmt.(list ~sep:(any "@,") Content_block.pp) blocks
+
end
-
let serialize_user_message content =
-
let open Ezjsonm in
-
let content_json =
-
match content with
-
| `String s -> string s
-
| `Blocks blocks ->
-
let serialize_block = function
-
| Text { text } ->
-
dict [ "type", string "text"; "text", string text ]
-
| ToolUse { id; name; input } ->
-
dict [ "type", string "tool_use"; "id", string id; "name", string name; "input", input ]
-
| ToolResult { tool_use_id; content; is_error } ->
-
let fields = [ "type", string "tool_result"; "tool_use_id", string tool_use_id ] in
-
let fields = match content with
-
| Some c -> ("content", string c) :: fields
-
| None -> fields
-
in
-
let fields = match is_error with
-
| Some e -> ("is_error", bool e) :: fields
-
| None -> fields
+
module Assistant = struct
+
type t = {
+
content : Content_block.t list;
+
model : string;
+
}
+
+
let create ~content ~model = { content; model }
+
let content t = t.content
+
let model t = t.model
+
+
let to_json t =
+
`O [
+
("type", `String "assistant");
+
("message", `O [
+
("content", `A (List.map Content_block.to_json t.content));
+
("model", `String t.model);
+
]);
+
]
+
+
let of_json = function
+
| `O fields ->
+
let message = List.assoc "message" fields in
+
let content, model = match message with
+
| `O msg_fields ->
+
let content =
+
match List.assoc "content" msg_fields with
+
| `A blocks -> List.map Content_block.of_json blocks
+
| _ -> raise (Invalid_argument "Assistant.of_json: invalid content")
in
-
dict fields
-
| Thinking { thinking; signature } ->
-
dict [ "type", string "thinking"; "thinking", string thinking; "signature", string signature ]
+
let model = get_string (List.assoc "model" msg_fields) in
+
content, model
+
| _ -> raise (Invalid_argument "Assistant.of_json: invalid message")
in
-
list serialize_block blocks
-
in
-
dict [
-
"type", string "user";
-
"message", dict [
-
"role", string "user";
-
"content", content_json
+
{ content; model }
+
| _ -> raise (Invalid_argument "Assistant.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Assistant@ { model = %S;@ content = @[<v>%a@] }@]"
+
t.model
+
Fmt.(list ~sep:(any "@,") Content_block.pp) t.content
+
end
+
+
module System = struct
+
type t = {
+
subtype : string;
+
data : value;
+
}
+
+
let create ~subtype ~data = { subtype; data }
+
let subtype t = t.subtype
+
let data t = t.data
+
+
let to_json t =
+
`O [
+
("type", `String "system");
+
("subtype", `String t.subtype);
+
("data", t.data);
]
-
]
+
+
let of_json = function
+
| `O fields ->
+
let subtype = get_string (List.assoc "subtype" fields) in
+
let data = try List.assoc "data" fields with Not_found -> `O fields in
+
{ subtype; data }
+
| _ -> raise (Invalid_argument "System.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>System@ { subtype = %S;@ data = %a }@]"
+
t.subtype
+
pp_json t.data
+
end
+
+
module Result = struct
+
type t = {
+
subtype : string;
+
duration_ms : int;
+
duration_api_ms : int;
+
is_error : bool;
+
num_turns : int;
+
session_id : string;
+
total_cost_usd : float option;
+
usage : value option;
+
result : string option;
+
}
+
+
let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result () =
+
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
+
session_id; total_cost_usd; usage; result }
+
+
let subtype t = t.subtype
+
let duration_ms t = t.duration_ms
+
let duration_api_ms t = t.duration_api_ms
+
let is_error t = t.is_error
+
let num_turns t = t.num_turns
+
let session_id t = t.session_id
+
let total_cost_usd t = t.total_cost_usd
+
let usage t = t.usage
+
let result t = t.result
+
+
let to_json t =
+
let fields = [
+
("type", `String "result");
+
("subtype", `String t.subtype);
+
("duration_ms", `Float (float_of_int t.duration_ms));
+
("duration_api_ms", `Float (float_of_int t.duration_api_ms));
+
("is_error", `Bool t.is_error);
+
("num_turns", `Float (float_of_int t.num_turns));
+
("session_id", `String t.session_id);
+
] in
+
let fields = match t.total_cost_usd with
+
| Some cost -> ("total_cost_usd", `Float cost) :: fields
+
| None -> fields
+
in
+
let fields = match t.usage with
+
| Some usage -> ("usage", usage) :: fields
+
| None -> fields
+
in
+
let fields = match t.result with
+
| Some result -> ("result", `String result) :: fields
+
| None -> fields
+
in
+
`O fields
+
+
let of_json = function
+
| `O fields ->
+
let subtype = get_string (List.assoc "subtype" fields) in
+
let duration_ms = int_of_float (get_float (List.assoc "duration_ms" fields)) in
+
let duration_api_ms = int_of_float (get_float (List.assoc "duration_api_ms" fields)) in
+
let is_error = get_bool (List.assoc "is_error" fields) in
+
let num_turns = int_of_float (get_float (List.assoc "num_turns" fields)) in
+
let session_id = get_string (List.assoc "session_id" fields) in
+
let total_cost_usd =
+
try Some (get_float (List.assoc "total_cost_usd" fields))
+
with Not_found -> None
+
in
+
let usage = List.assoc_opt "usage" fields in
+
let result =
+
try Some (get_string (List.assoc "result" fields))
+
with Not_found -> None
+
in
+
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
+
session_id; total_cost_usd; usage; result }
+
| _ -> raise (Invalid_argument "Result.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Result@ { subtype = %S;@ duration_ms = %d;@ \
+
duration_api_ms = %d;@ is_error = %b;@ num_turns = %d;@ \
+
session_id = %S;@ total_cost_usd = %a;@ usage = %a;@ result = %a }@]"
+
t.subtype t.duration_ms t.duration_api_ms t.is_error t.num_turns
+
t.session_id
+
Fmt.(option float) t.total_cost_usd
+
Fmt.(option pp_json) t.usage
+
Fmt.(option string) t.result
+
end
+
+
type t =
+
| User of User.t
+
| Assistant of Assistant.t
+
| System of System.t
+
| Result of Result.t
+
+
let user_string s = User (User.create_string s)
+
let user_blocks blocks = User (User.create_blocks blocks)
+
let assistant ~content ~model = Assistant (Assistant.create ~content ~model)
+
let system ~subtype ~data = System (System.create ~subtype ~data)
+
let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result () =
+
Result (Result.create ~subtype ~duration_ms ~duration_api_ms ~is_error
+
~num_turns ~session_id ?total_cost_usd ?usage ?result ())
+
+
let to_json = function
+
| User t -> User.to_json t
+
| Assistant t -> Assistant.to_json t
+
| System t -> System.to_json t
+
| Result t -> Result.to_json t
+
+
let of_json json =
+
match json with
+
| `O fields -> (
+
match List.assoc_opt "type" fields with
+
| Some (`String "user") -> User (User.of_json json)
+
| Some (`String "assistant") -> Assistant (Assistant.of_json json)
+
| Some (`String "system") -> System (System.of_json json)
+
| Some (`String "result") -> Result (Result.of_json json)
+
| _ -> raise (Invalid_argument "Message.of_json: unknown type")
+
)
+
| _ -> raise (Invalid_argument "Message.of_json: expected object")
+
+
let pp fmt = function
+
| User t -> User.pp fmt t
+
| Assistant t -> Assistant.pp fmt t
+
| System t -> System.pp fmt t
+
| Result t -> Result.pp fmt t
+
+
let log_received t =
+
Log.info (fun m -> m "Received message: %a" pp t)
+
+
let log_sending t =
+
Log.info (fun m -> m "Sending message: %a" pp t)
+
+
let log_error msg t =
+
Log.err (fun m -> m "%s: %a" msg pp t)
+
+227 -5
claudeio/lib/message.mli
···
-
exception Message_parse_error of string * Ezjsonm.value
+
(** Messages exchanged with Claude.
+
+
This module defines the various types of messages that can be sent to and
+
received from Claude, including user input, assistant responses, system
+
messages, and result metadata. *)
+
+
open Ezjsonm
+
+
(** The log source for message operations *)
+
val src : Logs.Src.t
-
val parse : Ezjsonm.value -> Types.message
+
(** {1 User Messages} *)
-
val serialize_user_message :
-
[ `String of string | `Blocks of Types.content_block list ] ->
-
Ezjsonm.value
+
module User : sig
+
(** Messages sent by the user. *)
+
+
type content =
+
| String of string (** Simple text message *)
+
| Blocks of Content_block.t list (** Complex message with multiple content blocks *)
+
(** The content of a user message. *)
+
+
type t
+
(** The type of user messages. *)
+
+
val create_string : string -> t
+
(** [create_string s] creates a user message with simple text content. *)
+
+
val create_blocks : Content_block.t list -> t
+
(** [create_blocks blocks] creates a user message with content blocks. *)
+
+
val content : t -> content
+
(** [content t] returns the content of the user message. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the user message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a user message from JSON.
+
@raise Invalid_argument if the JSON is not a valid user message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the user message. *)
+
end
+
+
(** {1 Assistant Messages} *)
+
+
module Assistant : sig
+
(** Messages from Claude assistant. *)
+
+
type t
+
(** The type of assistant messages. *)
+
+
val create : content:Content_block.t list -> model:string -> t
+
(** [create ~content ~model] creates an assistant message.
+
@param content List of content blocks in the response
+
@param model The model identifier used for the response *)
+
+
val content : t -> Content_block.t list
+
(** [content t] returns the content blocks of the assistant message. *)
+
+
val model : t -> string
+
(** [model t] returns the model identifier. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the assistant message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses an assistant message from JSON.
+
@raise Invalid_argument if the JSON is not a valid assistant message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the assistant message. *)
+
end
+
+
(** {1 System Messages} *)
+
+
module System : sig
+
(** System control and status messages. *)
+
+
type t
+
(** The type of system messages. *)
+
+
val create : subtype:string -> data:value -> t
+
(** [create ~subtype ~data] creates a system message.
+
@param subtype The subtype of the system message
+
@param data Additional JSON data for the message *)
+
+
val subtype : t -> string
+
(** [subtype t] returns the subtype of the system message. *)
+
+
val data : t -> value
+
(** [data t] returns the additional data of the system message. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the system message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a system message from JSON.
+
@raise Invalid_argument if the JSON is not a valid system message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the system message. *)
+
end
+
+
(** {1 Result Messages} *)
+
+
module Result : sig
+
(** Final result messages with metadata about the conversation. *)
+
+
type t
+
(** The type of result messages. *)
+
+
val create :
+
subtype:string ->
+
duration_ms:int ->
+
duration_api_ms:int ->
+
is_error:bool ->
+
num_turns:int ->
+
session_id:string ->
+
?total_cost_usd:float ->
+
?usage:value ->
+
?result:string ->
+
unit -> t
+
(** [create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result ()] creates a result message.
+
@param subtype The subtype of the result
+
@param duration_ms Total duration in milliseconds
+
@param duration_api_ms API duration in milliseconds
+
@param is_error Whether the result represents an error
+
@param num_turns Number of conversation turns
+
@param session_id Unique session identifier
+
@param total_cost_usd Optional total cost in USD
+
@param usage Optional usage statistics as JSON
+
@param result Optional result string *)
+
+
val subtype : t -> string
+
(** [subtype t] returns the subtype of the result. *)
+
+
val duration_ms : t -> int
+
(** [duration_ms t] returns the total duration in milliseconds. *)
+
+
val duration_api_ms : t -> int
+
(** [duration_api_ms t] returns the API duration in milliseconds. *)
+
+
val is_error : t -> bool
+
(** [is_error t] returns whether this result represents an error. *)
+
+
val num_turns : t -> int
+
(** [num_turns t] returns the number of conversation turns. *)
+
+
val session_id : t -> string
+
(** [session_id t] returns the session identifier. *)
+
+
val total_cost_usd : t -> float option
+
(** [total_cost_usd t] returns the optional total cost in USD. *)
+
+
val usage : t -> value option
+
(** [usage t] returns the optional usage statistics. *)
+
+
val result : t -> string option
+
(** [result t] returns the optional result string. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts the result message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a result message from JSON.
+
@raise Invalid_argument if the JSON is not a valid result message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the result message. *)
+
end
+
+
(** {1 Message Union Type} *)
+
+
type t =
+
| User of User.t
+
| Assistant of Assistant.t
+
| System of System.t
+
| Result of Result.t
+
(** The type of messages, which can be user, assistant, system, or result. *)
+
+
val user_string : string -> t
+
(** [user_string s] creates a user message with text content. *)
+
+
val user_blocks : Content_block.t list -> t
+
(** [user_blocks blocks] creates a user message with content blocks. *)
+
+
val assistant : content:Content_block.t list -> model:string -> t
+
(** [assistant ~content ~model] creates an assistant message. *)
+
+
val system : subtype:string -> data:value -> t
+
(** [system ~subtype ~data] creates a system message. *)
+
+
val result :
+
subtype:string ->
+
duration_ms:int ->
+
duration_api_ms:int ->
+
is_error:bool ->
+
num_turns:int ->
+
session_id:string ->
+
?total_cost_usd:float ->
+
?usage:value ->
+
?result:string ->
+
unit -> t
+
(** [result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result ()] creates a result message. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts any message to its JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a message from JSON.
+
@raise Invalid_argument if the JSON is not a valid message. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints any message. *)
+
+
(** {1 Logging} *)
+
+
val log_received : t -> unit
+
(** [log_received t] logs that a message was received. *)
+
+
val log_sending : t -> unit
+
(** [log_sending t] logs that a message is being sent. *)
+
+
val log_error : string -> t -> unit
+
(** [log_error msg t] logs an error with the given message and context. *)
+
+152 -2
claudeio/lib/options.ml
···
+
open Ezjsonm
+
+
let src = Logs.Src.create "claude.options" ~doc:"Claude configuration options"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
type t = {
allowed_tools : string list;
disallowed_tools : string list;
max_thinking_tokens : int;
system_prompt : string option;
append_system_prompt : string option;
-
permission_mode : [ `Default | `AcceptEdits | `Plan | `BypassPermissions ] option;
+
permission_mode : Permissions.Mode.t option;
permission_callback : Permissions.callback option;
model : string option;
cwd : Eio.Fs.dir_ty Eio.Path.t option;
···
model = None;
cwd = None;
env = [];
-
}
+
}
+
+
let create
+
?(allowed_tools = [])
+
?(disallowed_tools = [])
+
?(max_thinking_tokens = 8000)
+
?system_prompt
+
?append_system_prompt
+
?permission_mode
+
?permission_callback
+
?model
+
?cwd
+
?(env = [])
+
() =
+
{ allowed_tools; disallowed_tools; max_thinking_tokens;
+
system_prompt; append_system_prompt; permission_mode;
+
permission_callback; model; cwd; env }
+
+
let allowed_tools t = t.allowed_tools
+
let disallowed_tools t = t.disallowed_tools
+
let max_thinking_tokens t = t.max_thinking_tokens
+
let system_prompt t = t.system_prompt
+
let append_system_prompt t = t.append_system_prompt
+
let permission_mode t = t.permission_mode
+
let permission_callback t = t.permission_callback
+
let model t = t.model
+
let cwd t = t.cwd
+
let env t = t.env
+
+
let with_allowed_tools tools t = { t with allowed_tools = tools }
+
let with_disallowed_tools tools t = { t with disallowed_tools = tools }
+
let with_max_thinking_tokens tokens t = { t with max_thinking_tokens = tokens }
+
let with_system_prompt prompt t = { t with system_prompt = Some prompt }
+
let with_append_system_prompt prompt t = { t with append_system_prompt = Some prompt }
+
let with_permission_mode mode t = { t with permission_mode = Some mode }
+
let with_permission_callback callback t = { t with permission_callback = Some callback }
+
let with_model model t = { t with model = Some model }
+
let with_cwd cwd t = { t with cwd = Some cwd }
+
let with_env env t = { t with env }
+
+
let to_json t =
+
let fields = [] in
+
let fields =
+
if t.allowed_tools <> [] then
+
("allowed_tools", `A (List.map (fun s -> `String s) t.allowed_tools)) :: fields
+
else fields
+
in
+
let fields =
+
if t.disallowed_tools <> [] then
+
("disallowed_tools", `A (List.map (fun s -> `String s) t.disallowed_tools)) :: fields
+
else fields
+
in
+
let fields =
+
if t.max_thinking_tokens <> 8000 then
+
("max_thinking_tokens", `Float (float_of_int t.max_thinking_tokens)) :: fields
+
else fields
+
in
+
let fields = match t.system_prompt with
+
| Some p -> ("system_prompt", `String p) :: fields
+
| None -> fields
+
in
+
let fields = match t.append_system_prompt with
+
| Some p -> ("append_system_prompt", `String p) :: fields
+
| None -> fields
+
in
+
let fields = match t.permission_mode with
+
| Some m -> ("permission_mode", Permissions.Mode.to_json m) :: fields
+
| None -> fields
+
in
+
let fields = match t.model with
+
| Some m -> ("model", `String m) :: fields
+
| None -> fields
+
in
+
let fields =
+
if t.env <> [] then
+
let env_obj = `O (List.map (fun (k, v) -> (k, `String v)) t.env) in
+
("env", env_obj) :: fields
+
else fields
+
in
+
`O fields
+
+
let of_json = function
+
| `O fields ->
+
let allowed_tools =
+
try get_list get_string (List.assoc "allowed_tools" fields)
+
with Not_found -> []
+
in
+
let disallowed_tools =
+
try get_list get_string (List.assoc "disallowed_tools" fields)
+
with Not_found -> []
+
in
+
let max_thinking_tokens =
+
try int_of_float (get_float (List.assoc "max_thinking_tokens" fields))
+
with Not_found -> 8000
+
in
+
let system_prompt =
+
try Some (get_string (List.assoc "system_prompt" fields))
+
with Not_found -> None
+
in
+
let append_system_prompt =
+
try Some (get_string (List.assoc "append_system_prompt" fields))
+
with Not_found -> None
+
in
+
let permission_mode =
+
try Some (Permissions.Mode.of_json (List.assoc "permission_mode" fields))
+
with Not_found -> None
+
in
+
let model =
+
try Some (get_string (List.assoc "model" fields))
+
with Not_found -> None
+
in
+
let env =
+
try
+
match List.assoc "env" fields with
+
| `O pairs -> List.map (fun (k, v) -> (k, get_string v)) pairs
+
| _ -> []
+
with Not_found -> []
+
in
+
{ allowed_tools; disallowed_tools; max_thinking_tokens;
+
system_prompt; append_system_prompt; permission_mode;
+
permission_callback = Some Permissions.default_allow_callback;
+
model; cwd = None; env }
+
| _ -> raise (Invalid_argument "Options.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<v>Options {@ \
+
allowed_tools = %a;@ \
+
disallowed_tools = %a;@ \
+
max_thinking_tokens = %d;@ \
+
system_prompt = %a;@ \
+
append_system_prompt = %a;@ \
+
permission_mode = %a;@ \
+
model = %a;@ \
+
env = %a@ \
+
}@]"
+
Fmt.(list string) t.allowed_tools
+
Fmt.(list string) t.disallowed_tools
+
t.max_thinking_tokens
+
Fmt.(option string) t.system_prompt
+
Fmt.(option string) t.append_system_prompt
+
Fmt.(option Permissions.Mode.pp) t.permission_mode
+
Fmt.(option string) t.model
+
Fmt.(list (pair string string)) t.env
+
+
let log_options t =
+
Log.debug (fun m -> m "Claude options: %a" pp t)
+126 -13
claudeio/lib/options.mli
···
-
type t = {
-
allowed_tools : string list;
-
disallowed_tools : string list;
-
max_thinking_tokens : int;
-
system_prompt : string option;
-
append_system_prompt : string option;
-
permission_mode : [ `Default | `AcceptEdits | `Plan | `BypassPermissions ] option;
-
permission_callback : Permissions.callback option;
-
model : string option;
-
cwd : Eio.Fs.dir_ty Eio.Path.t option;
-
env : (string * string) list;
-
}
+
(** Configuration options for Claude sessions.
+
+
This module provides configuration options for controlling Claude's
+
behavior, including tool permissions, system prompts, models, and
+
execution environment. *)
+
+
open Ezjsonm
+
+
(** The log source for options operations *)
+
val src : Logs.Src.t
+
+
type t
+
(** The type of configuration options. *)
+
+
val default : t
+
(** [default] returns the default configuration with sensible defaults:
+
- No tool restrictions
+
- 8000 max thinking tokens
+
- Default allow permission callback
+
- No custom prompts or model override *)
+
+
val create :
+
?allowed_tools:string list ->
+
?disallowed_tools:string list ->
+
?max_thinking_tokens:int ->
+
?system_prompt:string ->
+
?append_system_prompt:string ->
+
?permission_mode:Permissions.Mode.t ->
+
?permission_callback:Permissions.callback ->
+
?model:string ->
+
?cwd:Eio.Fs.dir_ty Eio.Path.t ->
+
?env:(string * string) list ->
+
unit -> t
+
(** [create ?allowed_tools ?disallowed_tools ?max_thinking_tokens ?system_prompt
+
?append_system_prompt ?permission_mode ?permission_callback ?model ?cwd ?env ()]
+
creates a new configuration.
+
@param allowed_tools List of explicitly allowed tool names
+
@param disallowed_tools List of explicitly disallowed tool names
+
@param max_thinking_tokens Maximum tokens for thinking blocks (default: 8000)
+
@param system_prompt Replace the default system prompt
+
@param append_system_prompt Append to the default system prompt
+
@param permission_mode Permission mode to use
+
@param permission_callback Custom permission callback
+
@param model Override the default model
+
@param cwd Working directory for file operations
+
@param env Environment variables to set *)
+
+
(** {1 Accessors} *)
+
+
val allowed_tools : t -> string list
+
(** [allowed_tools t] returns the list of allowed tools. *)
+
+
val disallowed_tools : t -> string list
+
(** [disallowed_tools t] returns the list of disallowed tools. *)
+
+
val max_thinking_tokens : t -> int
+
(** [max_thinking_tokens t] returns the maximum thinking tokens. *)
+
+
val system_prompt : t -> string option
+
(** [system_prompt t] returns the optional system prompt override. *)
+
+
val append_system_prompt : t -> string option
+
(** [append_system_prompt t] returns the optional system prompt append. *)
+
+
val permission_mode : t -> Permissions.Mode.t option
+
(** [permission_mode t] returns the optional permission mode. *)
+
+
val permission_callback : t -> Permissions.callback option
+
(** [permission_callback t] returns the optional permission callback. *)
+
+
val model : t -> string option
+
(** [model t] returns the optional model override. *)
+
+
val cwd : t -> Eio.Fs.dir_ty Eio.Path.t option
+
(** [cwd t] returns the optional working directory. *)
+
+
val env : t -> (string * string) list
+
(** [env t] returns the environment variables. *)
+
+
(** {1 Builders} *)
+
+
val with_allowed_tools : string list -> t -> t
+
(** [with_allowed_tools tools t] sets the allowed tools. *)
+
+
val with_disallowed_tools : string list -> t -> t
+
(** [with_disallowed_tools tools t] sets the disallowed tools. *)
+
+
val with_max_thinking_tokens : int -> t -> t
+
(** [with_max_thinking_tokens tokens t] sets the maximum thinking tokens. *)
+
+
val with_system_prompt : string -> t -> t
+
(** [with_system_prompt prompt t] sets the system prompt override. *)
+
+
val with_append_system_prompt : string -> t -> t
+
(** [with_append_system_prompt prompt t] sets the system prompt append. *)
+
+
val with_permission_mode : Permissions.Mode.t -> t -> t
+
(** [with_permission_mode mode t] sets the permission mode. *)
+
+
val with_permission_callback : Permissions.callback -> t -> t
+
(** [with_permission_callback callback t] sets the permission callback. *)
+
+
val with_model : string -> t -> t
+
(** [with_model model t] sets the model override. *)
+
+
val with_cwd : Eio.Fs.dir_ty Eio.Path.t -> t -> t
+
(** [with_cwd cwd t] sets the working directory. *)
-
val default : t
+
val with_env : (string * string) list -> t -> t
+
(** [with_env env t] sets the environment variables. *)
+
+
(** {1 Serialization} *)
+
+
val to_json : t -> value
+
(** [to_json t] converts options to JSON representation. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses options from JSON.
+
@raise Invalid_argument if the JSON is not valid options. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the options. *)
+
+
(** {1 Logging} *)
+
+
val log_options : t -> unit
+
(** [log_options t] logs the current options configuration. *)
+329 -47
claudeio/lib/permissions.ml
···
-
type behavior = Allow | Deny | Ask
+
open Ezjsonm
-
type rule = {
-
tool_name : string;
-
rule_content : string option;
-
}
+
let src = Logs.Src.create "claude.permission" ~doc:"Claude permission system"
+
module Log = (val Logs.src_log src : Logs.LOG)
-
type update = {
-
behavior : behavior;
-
rules : rule list;
-
}
+
(* Helper for pretty-printing JSON *)
+
let pp_json fmt json =
+
Fmt.string fmt (value_to_string json)
-
type context = {
-
suggestions : update list;
-
}
+
(** Permission modes *)
+
module Mode = struct
+
type t =
+
| Default
+
| Accept_edits
+
| Plan
+
| Bypass_permissions
+
+
let to_string = function
+
| Default -> "default"
+
| Accept_edits -> "acceptEdits"
+
| Plan -> "plan"
+
| Bypass_permissions -> "bypassPermissions"
+
+
let of_string = function
+
| "default" -> Default
+
| "acceptEdits" -> Accept_edits
+
| "plan" -> Plan
+
| "bypassPermissions" -> Bypass_permissions
+
| s -> raise (Invalid_argument (Printf.sprintf "Mode.of_string: unknown mode %s" s))
+
+
let to_json t = `String (to_string t)
+
+
let of_json = function
+
| `String s -> of_string s
+
| _ -> raise (Invalid_argument "Mode.of_json: expected string")
+
+
let pp fmt t = Fmt.string fmt (to_string t)
+
end
-
type result =
-
| Allow of {
-
updated_input : Ezjsonm.value option;
-
updated_permissions : update list option;
-
}
-
| Deny of {
-
message : string;
-
interrupt : bool;
-
}
+
(** Permission behaviors *)
+
module Behavior = struct
+
type t = Allow | Deny | Ask
+
+
let to_string = function
+
| Allow -> "allow"
+
| Deny -> "deny"
+
| Ask -> "ask"
+
+
let of_string = function
+
| "allow" -> Allow
+
| "deny" -> Deny
+
| "ask" -> Ask
+
| s -> raise (Invalid_argument (Printf.sprintf "Behavior.of_string: unknown behavior %s" s))
+
+
let to_json t = `String (to_string t)
+
+
let of_json = function
+
| `String s -> of_string s
+
| _ -> raise (Invalid_argument "Behavior.of_json: expected string")
+
+
let pp fmt t = Fmt.string fmt (to_string t)
+
end
+
(** Permission rules *)
+
module Rule = struct
+
type t = {
+
tool_name : string;
+
rule_content : string option;
+
}
+
+
let create ~tool_name ?rule_content () = { tool_name; rule_content }
+
let tool_name t = t.tool_name
+
let rule_content t = t.rule_content
+
+
let to_json t =
+
let fields = [("tool_name", `String t.tool_name)] in
+
let fields = match t.rule_content with
+
| Some c -> ("rule_content", `String c) :: fields
+
| None -> fields
+
in
+
`O fields
+
+
let of_json = function
+
| `O fields ->
+
let tool_name = get_string (List.assoc "tool_name" fields) in
+
let rule_content =
+
try Some (get_string (List.assoc "rule_content" fields))
+
with Not_found -> None
+
in
+
{ tool_name; rule_content }
+
| _ -> raise (Invalid_argument "Rule.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Rule@ { tool_name = %S;@ rule_content = %a }@]"
+
t.tool_name Fmt.(option string) t.rule_content
+
end
+
+
(** Permission updates *)
+
module Update = struct
+
type destination =
+
| User_settings
+
| Project_settings
+
| Local_settings
+
| Session
+
+
let destination_to_string = function
+
| User_settings -> "userSettings"
+
| Project_settings -> "projectSettings"
+
| Local_settings -> "localSettings"
+
| Session -> "session"
+
+
let destination_of_string = function
+
| "userSettings" -> User_settings
+
| "projectSettings" -> Project_settings
+
| "localSettings" -> Local_settings
+
| "session" -> Session
+
| s -> raise (Invalid_argument (Printf.sprintf "destination_of_string: unknown %s" s))
+
+
type update_type =
+
| Add_rules
+
| Replace_rules
+
| Remove_rules
+
| Set_mode
+
| Add_directories
+
| Remove_directories
+
+
let update_type_to_string = function
+
| Add_rules -> "addRules"
+
| Replace_rules -> "replaceRules"
+
| Remove_rules -> "removeRules"
+
| Set_mode -> "setMode"
+
| Add_directories -> "addDirectories"
+
| Remove_directories -> "removeDirectories"
+
+
let update_type_of_string = function
+
| "addRules" -> Add_rules
+
| "replaceRules" -> Replace_rules
+
| "removeRules" -> Remove_rules
+
| "setMode" -> Set_mode
+
| "addDirectories" -> Add_directories
+
| "removeDirectories" -> Remove_directories
+
| s -> raise (Invalid_argument (Printf.sprintf "update_type_of_string: unknown %s" s))
+
+
type t = {
+
update_type : update_type;
+
rules : Rule.t list option;
+
behavior : Behavior.t option;
+
mode : Mode.t option;
+
directories : string list option;
+
destination : destination option;
+
}
+
+
let create ~update_type ?rules ?behavior ?mode ?directories ?destination () =
+
{ update_type; rules; behavior; mode; directories; destination }
+
+
let update_type t = t.update_type
+
let rules t = t.rules
+
let behavior t = t.behavior
+
let mode t = t.mode
+
let directories t = t.directories
+
let destination t = t.destination
+
+
let to_json t =
+
let fields = [("type", `String (update_type_to_string t.update_type))] in
+
let fields = match t.rules with
+
| Some rules -> ("rules", `A (List.map Rule.to_json rules)) :: fields
+
| None -> fields
+
in
+
let fields = match t.behavior with
+
| Some b -> ("behavior", Behavior.to_json b) :: fields
+
| None -> fields
+
in
+
let fields = match t.mode with
+
| Some m -> ("mode", Mode.to_json m) :: fields
+
| None -> fields
+
in
+
let fields = match t.directories with
+
| Some dirs -> ("directories", `A (List.map (fun s -> `String s) dirs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.destination with
+
| Some d -> ("destination", `String (destination_to_string d)) :: fields
+
| None -> fields
+
in
+
`O fields
+
+
let of_json = function
+
| `O fields ->
+
let update_type = update_type_of_string (get_string (List.assoc "type" fields)) in
+
let rules =
+
try Some (get_list Rule.of_json (List.assoc "rules" fields))
+
with Not_found -> None
+
in
+
let behavior =
+
try Some (Behavior.of_json (List.assoc "behavior" fields))
+
with Not_found -> None
+
in
+
let mode =
+
try Some (Mode.of_json (List.assoc "mode" fields))
+
with Not_found -> None
+
in
+
let directories =
+
try Some (get_list get_string (List.assoc "directories" fields))
+
with Not_found -> None
+
in
+
let destination =
+
try Some (destination_of_string (get_string (List.assoc "destination" fields)))
+
with Not_found -> None
+
in
+
{ update_type; rules; behavior; mode; directories; destination }
+
| _ -> raise (Invalid_argument "Update.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Update@ { type = %s;@ rules = %a;@ behavior = %a;@ \
+
mode = %a;@ directories = %a;@ destination = %a }@]"
+
(update_type_to_string t.update_type)
+
Fmt.(option (list Rule.pp)) t.rules
+
Fmt.(option Behavior.pp) t.behavior
+
Fmt.(option Mode.pp) t.mode
+
Fmt.(option (list string)) t.directories
+
Fmt.(option (fun fmt d -> Fmt.string fmt (destination_to_string d))) t.destination
+
end
+
+
(** Permission context for callbacks *)
+
module Context = struct
+
type t = {
+
suggestions : Update.t list;
+
}
+
+
let create ?(suggestions = []) () = { suggestions }
+
let suggestions t = t.suggestions
+
+
let to_json t =
+
`O [("suggestions", `A (List.map Update.to_json t.suggestions))]
+
+
let of_json = function
+
| `O fields ->
+
let suggestions =
+
try get_list Update.of_json (List.assoc "suggestions" fields)
+
with Not_found -> []
+
in
+
{ suggestions }
+
| _ -> raise (Invalid_argument "Context.of_json: expected object")
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>Context@ { suggestions = @[<v>%a@] }@]"
+
Fmt.(list ~sep:(any "@,") Update.pp) t.suggestions
+
end
+
+
(** Permission results *)
+
module Result = struct
+
type t =
+
| Allow of {
+
updated_input : value option;
+
updated_permissions : Update.t list option;
+
}
+
| Deny of {
+
message : string;
+
interrupt : bool;
+
}
+
+
let allow ?updated_input ?updated_permissions () =
+
Allow { updated_input; updated_permissions }
+
+
let deny ~message ~interrupt = Deny { message; interrupt }
+
+
let to_json = function
+
| Allow { updated_input; updated_permissions } ->
+
let fields = [("behavior", `String "allow")] in
+
let fields = match updated_input with
+
| Some input -> ("updated_input", input) :: fields
+
| None -> fields
+
in
+
let fields = match updated_permissions with
+
| Some perms -> ("updated_permissions", `A (List.map Update.to_json perms)) :: fields
+
| None -> fields
+
in
+
`O fields
+
| Deny { message; interrupt } ->
+
`O [
+
("behavior", `String "deny");
+
("message", `String message);
+
("interrupt", `Bool interrupt);
+
]
+
+
let of_json = function
+
| `O fields -> (
+
match List.assoc "behavior" fields with
+
| `String "allow" ->
+
let updated_input = List.assoc_opt "updated_input" fields in
+
let updated_permissions =
+
try Some (get_list Update.of_json (List.assoc "updated_permissions" fields))
+
with Not_found -> None
+
in
+
Allow { updated_input; updated_permissions }
+
| `String "deny" ->
+
let message = get_string (List.assoc "message" fields) in
+
let interrupt = get_bool (List.assoc "interrupt" fields) in
+
Deny { message; interrupt }
+
| _ -> raise (Invalid_argument "Result.of_json: unknown behavior")
+
)
+
| _ -> raise (Invalid_argument "Result.of_json: expected object")
+
+
let pp fmt = function
+
| Allow { updated_input; updated_permissions } ->
+
Fmt.pf fmt "@[<2>Allow@ { updated_input = %a;@ updated_permissions = %a }@]"
+
Fmt.(option pp_json) updated_input
+
Fmt.(option (list Update.pp)) updated_permissions
+
| Deny { message; interrupt } ->
+
Fmt.pf fmt "@[<2>Deny@ { message = %S;@ interrupt = %b }@]" message interrupt
+
end
+
+
(** Permission callback type *)
type callback =
tool_name:string ->
-
input:Ezjsonm.value ->
-
context:context ->
-
result
-
-
let serialize_result = function
-
| Allow { updated_input; updated_permissions = _ } ->
-
let open Ezjsonm in
-
let fields = [ "behavior", string "allow" ] in
-
let fields = match updated_input with
-
| Some input -> ("updated_input", input) :: fields
-
| None -> fields
-
in
-
dict fields
-
| Deny { message; interrupt } ->
-
let open Ezjsonm in
-
dict [
-
"behavior", string "deny";
-
"message", string message;
-
"interrupt", bool interrupt
-
]
+
input:value ->
+
context:Context.t ->
+
Result.t
+
(** Default callbacks *)
let default_allow_callback ~tool_name:_ ~input:_ ~context:_ =
-
Allow { updated_input = None; updated_permissions = None }
+
Result.allow ()
let discovery_callback log ~tool_name:_ ~input:_ ~context =
List.iter (fun update ->
-
List.iter (fun rule ->
-
log := rule :: !log
-
) update.rules
-
) context.suggestions;
-
Allow { updated_input = None; updated_permissions = None }
+
match Update.rules update with
+
| Some rules ->
+
List.iter (fun rule ->
+
log := rule :: !log
+
) rules
+
| None -> ()
+
) (Context.suggestions context);
+
Result.allow ()
+
+
(** Logging *)
+
let log_permission_check ~tool_name ~result =
+
match result with
+
| Result.Allow _ ->
+
Log.info (fun m -> m "Permission granted for tool: %s" tool_name)
+
| Result.Deny { message; _ } ->
+
Log.warn (fun m -> m "Permission denied for tool %s: %s" tool_name message)
+250 -27
claudeio/lib/permissions.mli
···
-
type behavior = Allow | Deny | Ask
+
(** Permission system for Claude tool invocations.
+
+
This module provides a permission system for controlling
+
which tools Claude can invoke and how they can be used. It includes
+
support for permission modes, rules, updates, and callbacks. *)
+
+
open Ezjsonm
+
+
(** The log source for permission operations *)
+
val src : Logs.Src.t
+
+
(** {1 Permission Modes} *)
+
+
module Mode : sig
+
(** Permission modes control the overall behavior of the permission system. *)
+
+
type t =
+
| Default (** Standard permission mode with normal checks *)
+
| Accept_edits (** Automatically accept file edits *)
+
| Plan (** Planning mode with restricted execution *)
+
| Bypass_permissions (** Bypass all permission checks *)
+
(** The type of permission modes. *)
+
+
val to_string : t -> string
+
(** [to_string t] converts a mode to its string representation. *)
+
+
val of_string : string -> t
+
(** [of_string s] parses a mode from its string representation.
+
@raise Invalid_argument if the string is not a valid mode. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts a mode to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a mode from JSON.
+
@raise Invalid_argument if the JSON is not a valid mode. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the mode. *)
+
end
+
+
(** {1 Permission Behaviors} *)
+
+
module Behavior : sig
+
(** Behaviors determine how permission requests are handled. *)
+
+
type t =
+
| Allow (** Allow the operation *)
+
| Deny (** Deny the operation *)
+
| Ask (** Ask the user for permission *)
+
(** The type of permission behaviors. *)
+
+
val to_string : t -> string
+
(** [to_string t] converts a behavior to its string representation. *)
+
+
val of_string : string -> t
+
(** [of_string s] parses a behavior from its string representation.
+
@raise Invalid_argument if the string is not a valid behavior. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts a behavior to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a behavior from JSON.
+
@raise Invalid_argument if the JSON is not a valid behavior. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the behavior. *)
+
end
+
+
(** {1 Permission Rules} *)
+
+
module Rule : sig
+
(** Rules define specific permissions for tools. *)
+
+
type t = {
+
tool_name : string; (** Name of the tool *)
+
rule_content : string option; (** Optional rule specification *)
+
}
+
(** The type of permission rules. *)
+
+
val create : tool_name:string -> ?rule_content:string -> unit -> t
+
(** [create ~tool_name ?rule_content ()] creates a new rule.
+
@param tool_name The name of the tool this rule applies to
+
@param rule_content Optional rule specification or pattern *)
+
+
val tool_name : t -> string
+
(** [tool_name t] returns the tool name. *)
+
+
val rule_content : t -> string option
+
(** [rule_content t] returns the optional rule content. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts a rule to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a rule from JSON.
+
@raise Invalid_argument if the JSON is not a valid rule. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the rule. *)
+
end
+
+
(** {1 Permission Updates} *)
+
+
module Update : sig
+
(** Updates modify permission settings. *)
+
+
type destination =
+
| User_settings (** Apply to user settings *)
+
| Project_settings (** Apply to project settings *)
+
| Local_settings (** Apply to local settings *)
+
| Session (** Apply to current session only *)
+
(** The destination for permission updates. *)
+
+
type update_type =
+
| Add_rules (** Add new rules *)
+
| Replace_rules (** Replace existing rules *)
+
| Remove_rules (** Remove rules *)
+
| Set_mode (** Set permission mode *)
+
| Add_directories (** Add allowed directories *)
+
| Remove_directories (** Remove allowed directories *)
+
(** The type of permission update. *)
+
+
type t
+
(** The type of permission updates. *)
+
+
val create :
+
update_type:update_type ->
+
?rules:Rule.t list ->
+
?behavior:Behavior.t ->
+
?mode:Mode.t ->
+
?directories:string list ->
+
?destination:destination ->
+
unit -> t
+
(** [create ~update_type ?rules ?behavior ?mode ?directories ?destination ()]
+
creates a new permission update.
+
@param update_type The type of update to perform
+
@param rules Optional list of rules to add/remove/replace
+
@param behavior Optional behavior to set
+
@param mode Optional permission mode to set
+
@param directories Optional directories to add/remove
+
@param destination Optional destination for the update *)
+
+
val update_type : t -> update_type
+
(** [update_type t] returns the update type. *)
+
+
val rules : t -> Rule.t list option
+
(** [rules t] returns the optional list of rules. *)
+
+
val behavior : t -> Behavior.t option
+
(** [behavior t] returns the optional behavior. *)
+
+
val mode : t -> Mode.t option
+
(** [mode t] returns the optional mode. *)
+
+
val directories : t -> string list option
+
(** [directories t] returns the optional list of directories. *)
+
+
val destination : t -> destination option
+
(** [destination t] returns the optional destination. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts an update to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses an update from JSON.
+
@raise Invalid_argument if the JSON is not a valid update. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the update. *)
+
end
-
type rule = {
-
tool_name : string;
-
rule_content : string option;
-
}
+
(** {1 Permission Context} *)
-
type update = {
-
behavior : behavior;
-
rules : rule list;
-
}
+
module Context : sig
+
(** Context provided to permission callbacks. *)
+
+
type t = {
+
suggestions : Update.t list; (** Suggested permission updates *)
+
}
+
(** The type of permission context. *)
+
+
val create : ?suggestions:Update.t list -> unit -> t
+
(** [create ?suggestions ()] creates a new context.
+
@param suggestions Optional list of suggested permission updates *)
+
+
val suggestions : t -> Update.t list
+
(** [suggestions t] returns the list of suggested updates. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts a context to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a context from JSON.
+
@raise Invalid_argument if the JSON is not a valid context. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the context. *)
+
end
-
type context = {
-
suggestions : update list;
-
}
+
(** {1 Permission Results} *)
+
+
module Result : sig
+
(** Results of permission checks. *)
+
+
type t =
+
| Allow of {
+
updated_input : value option; (** Modified tool input *)
+
updated_permissions : Update.t list option; (** Permission updates to apply *)
+
}
+
| Deny of {
+
message : string; (** Reason for denial *)
+
interrupt : bool; (** Whether to interrupt execution *)
+
}
+
(** The type of permission results. *)
+
+
val allow : ?updated_input:value -> ?updated_permissions:Update.t list -> unit -> t
+
(** [allow ?updated_input ?updated_permissions ()] creates an allow result.
+
@param updated_input Optional modified tool input
+
@param updated_permissions Optional permission updates to apply *)
+
+
val deny : message:string -> interrupt:bool -> t
+
(** [deny ~message ~interrupt] creates a deny result.
+
@param message The reason for denying permission
+
@param interrupt Whether to interrupt further execution *)
+
+
val to_json : t -> value
+
(** [to_json t] converts a result to JSON. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses a result from JSON.
+
@raise Invalid_argument if the JSON is not a valid result. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the result. *)
+
end
-
type result =
-
| Allow of {
-
updated_input : Ezjsonm.value option;
-
updated_permissions : update list option;
-
}
-
| Deny of {
-
message : string;
-
interrupt : bool;
-
}
+
(** {1 Permission Callbacks} *)
type callback =
tool_name:string ->
-
input:Ezjsonm.value ->
-
context:context ->
-
result
-
-
val serialize_result : result -> Ezjsonm.value
+
input:value ->
+
context:Context.t ->
+
Result.t
+
(** The type of permission callbacks. Callbacks are invoked when Claude
+
attempts to use a tool, allowing custom permission logic. *)
val default_allow_callback : callback
+
(** [default_allow_callback] always allows tool invocations. *)
-
val discovery_callback : rule list ref -> callback
+
val discovery_callback : Rule.t list ref -> callback
+
(** [discovery_callback log] creates a callback that collects suggested
+
rules into the provided reference. Useful for discovering what
+
permissions an operation requires. *)
+
+
(** {1 Logging} *)
+
+
val log_permission_check : tool_name:string -> result:Result.t -> unit
+
(** [log_permission_check ~tool_name ~result] logs a permission check result. *)
+55 -49
claudeio/lib/transport.ml
···
open Eio.Std
+
let src = Logs.Src.create "claude.transport" ~doc:"Claude transport layer"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
exception CLI_not_found of string
exception Process_error of string
exception Connection_error of string
···
sw : Switch.t;
}
-
let find_claude_cli ~fs =
-
let ( / ) = Eio.Path.( / ) in
-
let paths = [
-
fs / "usr" / "local" / "bin" / "claude";
-
fs / "opt" / "homebrew" / "bin" / "claude";
-
] in
-
-
let rec check_paths = function
-
| [] ->
-
(* Try using 'which' *)
-
None
-
| path :: rest ->
-
if Eio.Path.is_file path then
-
Some (Eio.Path.native_exn path)
-
else
-
check_paths rest
-
in
-
-
match check_paths paths with
-
| Some path -> path
-
| None ->
-
(* Try to find using shell *)
-
"claude"
-
let build_command ~claude_path ~options =
-
let open Options in
let cmd = [claude_path; "--output-format"; "stream-json"; "--verbose"] in
-
let cmd = match options.system_prompt with
+
let cmd = match Options.system_prompt options with
| Some prompt -> cmd @ ["--system-prompt"; prompt]
| None -> cmd
in
-
let cmd = match options.append_system_prompt with
+
let cmd = match Options.append_system_prompt options with
| Some prompt -> cmd @ ["--append-system-prompt"; prompt]
| None -> cmd
in
-
let cmd = match options.allowed_tools with
+
let cmd = match Options.allowed_tools options with
| [] -> cmd
| tools -> cmd @ ["--allowedTools"; String.concat "," tools]
in
-
let cmd = match options.disallowed_tools with
+
let cmd = match Options.disallowed_tools options with
| [] -> cmd
| tools -> cmd @ ["--disallowedTools"; String.concat "," tools]
in
-
let cmd = match options.model with
+
let cmd = match Options.model options with
| Some model -> cmd @ ["--model"; model]
| None -> cmd
in
-
let cmd = match options.permission_mode with
-
| Some `Default -> cmd @ ["--permission-mode"; "default"]
-
| Some `AcceptEdits -> cmd @ ["--permission-mode"; "acceptEdits"]
-
| Some `Plan -> cmd @ ["--permission-mode"; "plan"]
-
| Some `BypassPermissions -> cmd @ ["--permission-mode"; "bypassPermissions"]
+
let cmd = match Options.permission_mode options with
+
| Some mode ->
+
let mode_str = Permissions.Mode.to_string mode in
+
cmd @ ["--permission-mode"; mode_str]
| None -> cmd
in
(* Use streaming input mode *)
cmd @ ["--input-format"; "stream-json"]
-
let create ~sw ~process_mgr ~fs ~options () =
-
let claude_path = find_claude_cli ~fs in
+
let create ~sw ~process_mgr ~options () =
+
let claude_path = "claude" in
let cmd = build_command ~claude_path ~options in
-
let env =
-
Array.of_list (
-
List.map (fun (k, v) -> Printf.sprintf "%s=%s" k v) (
-
("CLAUDE_CODE_ENTRYPOINT", "sdk-ocaml") ::
-
options.Options.env
-
)
-
)
-
in
+
(* Build environment - preserve essential vars for Claude config/auth access *)
+
let home = try Unix.getenv "HOME" with Not_found -> "/tmp" in
+
let path = try Unix.getenv "PATH" with Not_found -> "/usr/bin:/bin" in
+
+
(* Preserve other potentially important environment variables *)
+
let preserve_vars = [
+
"USER"; "LOGNAME"; "SHELL"; "TERM";
+
"XDG_CONFIG_HOME"; "XDG_DATA_HOME"; "XDG_CACHE_HOME";
+
"ANTHROPIC_API_KEY"; "CLAUDE_API_KEY" (* In case API key is set via env *)
+
] in
+
+
let preserved = List.filter_map (fun var ->
+
try Some (Printf.sprintf "%s=%s" var (Unix.getenv var))
+
with Not_found -> None
+
) preserve_vars in
+
+
let base_env = [
+
Printf.sprintf "HOME=%s" home;
+
Printf.sprintf "PATH=%s" path;
+
"CLAUDE_CODE_ENTRYPOINT=sdk-ocaml";
+
] @ preserved in
+
+
let custom_env = List.map (fun (k, v) -> Printf.sprintf "%s=%s" k v) (Options.env options) in
+
let env = Array.of_list (base_env @ custom_env) in
+
Log.debug (fun m -> m "Environment: HOME=%s, PATH=%s" home path);
let stdin_r, stdin_w = Eio.Process.pipe ~sw process_mgr in
let stdout_r, stdout_w = Eio.Process.pipe ~sw process_mgr in
···
let process =
try
+
Log.info (fun m -> m "Spawning claude with command: %s" (String.concat " " cmd));
Eio.Process.spawn ~sw process_mgr
~env
~stdin:(stdin_r :> Eio.Flow.source_ty r)
~stdout:(stdout_w :> Eio.Flow.sink_ty r)
-
?cwd:options.Options.cwd
+
?cwd:(Options.cwd options)
cmd
with
| exn ->
+
Log.err (fun m -> m "Failed to spawn claude CLI: %s" (Printexc.to_string exn));
+
Log.err (fun m -> m "Make sure 'claude' is installed and authenticated");
+
Log.err (fun m -> m "You may need to run 'claude login' first");
raise (CLI_not_found (Printf.sprintf "Failed to spawn claude CLI: %s" (Printexc.to_string exn)))
in
···
let send t json =
let data = Ezjsonm.value_to_string json in
+
Log.debug (fun m -> m "Sending: %s" data);
try
Eio.Flow.write t.stdin [Cstruct.of_string (data ^ "\n")]
with
| exn ->
+
Log.err (fun m -> m "Failed to send message: %s" (Printexc.to_string exn));
raise (Connection_error (Printf.sprintf "Failed to send message: %s" (Printexc.to_string exn)))
let receive_line t =
try
match Eio.Buf_read.line t.stdout with
-
| line -> Some line
-
| exception End_of_file -> None
+
| line ->
+
Log.debug (fun m -> m "Received: %s" line);
+
Some line
+
| exception End_of_file ->
+
Log.debug (fun m -> m "Received EOF");
+
None
with
| exn ->
+
Log.err (fun m -> m "Failed to receive message: %s" (Printexc.to_string exn));
raise (Connection_error (Printf.sprintf "Failed to receive message: %s" (Printexc.to_string exn)))
let interrupt t =
+
Log.info (fun m -> m "Sending interrupt signal");
let interrupt_msg =
Ezjsonm.dict [
"type", Ezjsonm.string "control_response";
···
Eio.Flow.close t.stdin_close;
let (P process) = t.process in
Eio.Process.await_exn process
-
with _ -> ()
+
with _ -> ()
+4 -2
claudeio/lib/transport.mli
···
+
(** The log source for transport operations *)
+
val src : Logs.Src.t
+
exception CLI_not_found of string
exception Process_error of string
exception Connection_error of string
···
val create :
sw:Eio.Switch.t ->
process_mgr:_ Eio.Process.mgr ->
-
fs:Eio.Fs.dir_ty Eio.Path.t ->
options:Options.t ->
unit -> t
val send : t -> Ezjsonm.value -> unit
val receive_line : t -> string option
val interrupt : t -> unit
-
val close : t -> unit
+
val close : t -> unit
-62
claudeio/lib/types.ml
···
-
type text_block = { text : string }
-
-
type tool_use_block = {
-
id : string;
-
name : string;
-
input : Ezjsonm.value
-
}
-
-
type tool_result_block = {
-
tool_use_id : string;
-
content : string option;
-
is_error : bool option
-
}
-
-
type thinking_block = {
-
thinking : string;
-
signature : string;
-
}
-
-
type content_block =
-
| Text of text_block
-
| ToolUse of tool_use_block
-
| ToolResult of tool_result_block
-
| Thinking of thinking_block
-
-
type user_message = {
-
content : [ `String of string | `Blocks of content_block list ]
-
}
-
-
type assistant_message = {
-
content : content_block list;
-
model : string
-
}
-
-
type system_message = {
-
subtype : string;
-
data : Ezjsonm.value
-
}
-
-
type result_message = {
-
subtype : string;
-
duration_ms : int;
-
duration_api_ms : int;
-
is_error : bool;
-
num_turns : int;
-
session_id : string;
-
total_cost_usd : float option;
-
usage : Ezjsonm.value option;
-
result : string option;
-
}
-
-
type message =
-
| User of user_message
-
| Assistant of assistant_message
-
| System of system_message
-
| Result of result_message
-
-
type control_message = {
-
request_id : string;
-
subtype : string;
-
data : Ezjsonm.value;
-
}
-62
claudeio/lib/types.mli
···
-
type text_block = { text : string }
-
-
type tool_use_block = {
-
id : string;
-
name : string;
-
input : Ezjsonm.value
-
}
-
-
type tool_result_block = {
-
tool_use_id : string;
-
content : string option;
-
is_error : bool option
-
}
-
-
type thinking_block = {
-
thinking : string;
-
signature : string;
-
}
-
-
type content_block =
-
| Text of text_block
-
| ToolUse of tool_use_block
-
| ToolResult of tool_result_block
-
| Thinking of thinking_block
-
-
type user_message = {
-
content : [ `String of string | `Blocks of content_block list ]
-
}
-
-
type assistant_message = {
-
content : content_block list;
-
model : string
-
}
-
-
type system_message = {
-
subtype : string;
-
data : Ezjsonm.value
-
}
-
-
type result_message = {
-
subtype : string;
-
duration_ms : int;
-
duration_api_ms : int;
-
is_error : bool;
-
num_turns : int;
-
session_id : string;
-
total_cost_usd : float option;
-
usage : Ezjsonm.value option;
-
result : string option;
-
}
-
-
type message =
-
| User of user_message
-
| Assistant of assistant_message
-
| System of system_message
-
| Result of result_message
-
-
type control_message = {
-
request_id : string;
-
subtype : string;
-
data : Ezjsonm.value;
-
}
+90 -25
claudeio/test/camel_jokes.ml
···
open Eio.Std
+
let src = Logs.Src.create "camel_jokes" ~doc:"Camel joke competition"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
let process_claude_response client name =
-
traceln "\n=== %s's Response ===" name;
+
Log.info (fun m -> m "=== %s's Response ===" name);
let messages = Claude.Client.receive_all client in
List.iter (fun msg ->
match msg with
-
| Claude.Types.Assistant msg ->
+
| Claude.Message.Assistant msg ->
List.iter (function
-
| Claude.Types.Text { text } ->
-
traceln "%s: %s" name text
+
| Claude.Content_block.Text t ->
+
let text = Claude.Content_block.Text.text t in
+
Log.app (fun m -> m "%s: %s" name text)
+
| Claude.Content_block.Tool_use t ->
+
Log.debug (fun m -> m "%s using tool: %s" name
+
(Claude.Content_block.Tool_use.name t))
+
| Claude.Content_block.Thinking t ->
+
Log.debug (fun m -> m "%s thinking: %s" name
+
(Claude.Content_block.Thinking.thinking t))
| _ -> ()
-
) msg.content
-
| Claude.Types.Result msg ->
-
if msg.is_error then
-
traceln "Error from %s!" name
+
) (Claude.Message.Assistant.content msg);
+
Log.debug (fun m -> m "%s using model: %s" name
+
(Claude.Message.Assistant.model msg))
+
| Claude.Message.Result msg ->
+
if Claude.Message.Result.is_error msg then
+
Log.err (fun m -> m "Error from %s!" name)
else
-
(match msg.total_cost_usd with
-
| Some cost -> traceln "%s's joke cost: $%.6f" name cost
-
| None -> ())
-
| _ -> ()
+
(match Claude.Message.Result.total_cost_usd msg with
+
| Some cost ->
+
Log.info (fun m -> m "%s's joke cost: $%.6f" name cost)
+
| None -> ());
+
Log.debug (fun m -> m "%s session: %s, duration: %dms"
+
name
+
(Claude.Message.Result.session_id msg)
+
(Claude.Message.Result.duration_ms msg))
+
| Claude.Message.System msg ->
+
Log.debug (fun m -> m "System message (%s): %a"
+
(Claude.Message.System.subtype msg)
+
Claude.Message.System.pp msg)
+
| Claude.Message.User msg ->
+
Log.debug (fun m -> m "User message: %a"
+
Claude.Message.User.pp msg)
) messages
let run_claude ~sw ~env name prompt =
-
traceln "Starting %s..." name;
-
let options = Claude.Options.{
-
default with
-
model = Some "claude-3-5-sonnet-20241022";
-
(* Allow Claude to be creative *)
-
allowed_tools = [];
-
} in
+
Log.info (fun m -> m "Starting %s..." name);
+
let options = Claude.Options.create ~model:"sonnet" ~allowed_tools:[] () in
+
+
Claude.Options.log_options options;
let client = Claude.Client.create ~options ~sw
~process_mgr:env#process_mgr
-
~fs:env#fs
() in
+
Log.debug (fun m -> m "Sending prompt to %s: %s" name prompt);
Claude.Client.query client prompt;
process_claude_response client name
let main ~env =
Switch.run @@ fun sw ->
-
traceln "🐪 Starting the Great Camel Joke Competition! 🐪";
-
traceln "================================================\n";
+
Log.app (fun m -> m "🐪 Starting the Great Camel Joke Competition! 🐪");
+
Log.app (fun m -> m "================================================\n");
let prompts = [
"Claude 1", "Tell me a short, funny joke about camels! Make it original and clever.";
···
) prompts
);
-
traceln "\n================================================";
-
traceln "🎉 The Camel Joke Competition is complete! 🎉"
+
Log.app (fun m -> m "\n================================================");
+
Log.app (fun m -> m "🎉 The Camel Joke Competition is complete! 🎉")
+
+
(* Command-line interface *)
+
open Cmdliner
+
+
let main_term env =
+
let setup_log style_renderer level =
+
Fmt_tty.setup_std_outputs ?style_renderer ();
+
Logs.set_level level;
+
Logs.set_reporter (Logs_fmt.reporter ());
+
(* Set default to App level if not specified *)
+
if level = None then Logs.set_level (Some Logs.App);
+
(* Enable debug for Client module if in debug mode *)
+
if level = Some Logs.Debug then
+
Logs.Src.set_level Claude.Client.src (Some Logs.Debug)
+
in
+
let run style level =
+
setup_log style level;
+
main ~env
+
in
+
Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
+
+
let cmd env =
+
let doc = "Run the Great Camel Joke Competition using Claude" in
+
let man = [
+
`S Manpage.s_description;
+
`P "This program runs three concurrent Claude instances to generate camel jokes.";
+
`P "Use $(b,-v) or $(b,--verbosity=info) to see RPC message traffic.";
+
`P "Use $(b,-vv) or $(b,--verbosity=debug) to see all internal operations.";
+
`S Manpage.s_examples;
+
`P "Run with normal output:";
+
`Pre " $(mname)";
+
`P "Run with info-level logging (RPC traffic):";
+
`Pre " $(mname) -v";
+
`Pre " $(mname) --verbosity=info";
+
`P "Run with debug logging (all operations):";
+
`Pre " $(mname) -vv";
+
`Pre " $(mname) --verbosity=debug";
+
`P "Enable debug for specific modules:";
+
`Pre " LOGS='claude.transport=debug' $(mname)";
+
`Pre " LOGS='claude.message=info,camel_jokes=debug' $(mname)";
+
`S Manpage.s_bugs;
+
`P "Report bugs at https://github.com/your-repo/issues";
+
] in
+
let info = Cmd.info "camel_jokes" ~version:"1.0" ~doc ~man in
+
Cmd.v info (main_term env)
let () =
Eio_main.run @@ fun env ->
-
main ~env
+
exit (Cmd.eval (cmd env))
+1 -1
claudeio/test/dune
···
(executable
(public_name camel_jokes)
(name camel_jokes)
-
(libraries claude eio_main))
+
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))