···11+# ClaudeIO - OCaml Eio Library for Claude Code CLI
22+33+An OCaml library that provides high-quality Eio-style bindings for the Claude Code CLI, enabling programmatic interaction with Claude through JSON streaming.
44+55+## Overview
66+77+ClaudeIO wraps Claude Code CLI invocations in an idiomatic OCaml Eio interface, leveraging:
88+- JSON input/output streaming modes of the CLI
99+- Ezjsonm for JSON message handling
1010+- Eio abstractions including `Buf_read` and `Seq` for efficient streaming
1111+1212+## Features
1313+1414+- **Streaming JSON Interface**: Communicate with Claude using structured JSON messages
1515+- **Eio Integration**: Built on modern OCaml concurrency primitives
1616+- **Type-safe API**: Strongly typed OCaml interface for Claude interactions
1717+- **Efficient Buffering**: Uses Eio's buffer management for optimal performance
1818+1919+## Installation
2020+2121+```bash
2222+opam install claudeio
2323+```
2424+2525+## Usage
2626+2727+```ocaml
2828+open Eio
2929+open Claudeio
3030+3131+let main ~env =
3232+ let claude = Claude.create ~env in
3333+ Claude.query claude ~prompt:"Your question here"
3434+ |> Seq.iter (fun response ->
3535+ Format.printf "Claude: %s\n" (Claude.Response.to_string response))
3636+```
3737+3838+## Known Issues
3939+4040+⚠️ **Permissions Support**: The permissions functionality is temporarily broken and awaiting a fix from Anthropic. This feature will be restored in a future update.
4141+4242+## Requirements
4343+4444+- OCaml >= 5.0
4545+- Eio >= 1.0
4646+- Ezjsonm >= 1.3
4747+- Claude Code CLI installed and configured
4848+4949+## License
5050+5151+See LICENSE file for details.
+31
claude.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "OCaml client library for Claude Code"
44+description:
55+ "An Eio-based OCaml library for interacting with the Claude CLI using JSON streaming"
66+depends: [
77+ "dune" {>= "3.18"}
88+ "ocaml"
99+ "eio"
1010+ "fmt"
1111+ "logs"
1212+ "jsont" {>= "0.2.0"}
1313+ "jsont_bytesrw" {>= "0.2.0"}
1414+ "alcotest" {with-test}
1515+ "odoc" {with-doc}
1616+]
1717+build: [
1818+ ["dune" "subst"] {dev}
1919+ [
2020+ "dune"
2121+ "build"
2222+ "-p"
2323+ name
2424+ "-j"
2525+ jobs
2626+ "@install"
2727+ "@runtest" {with-test}
2828+ "@doc" {with-doc}
2929+ ]
3030+]
3131+x-maintenance-intent: ["(latest)"]
+16
dune-project
···11+(lang dune 3.18)
22+(name claude)
33+(generate_opam_files true)
44+55+(package
66+ (name claude)
77+ (synopsis "OCaml client library for Claude Code")
88+ (description "An Eio-based OCaml library for interacting with the Claude CLI using JSON streaming")
99+ (depends
1010+ ocaml
1111+ eio
1212+ fmt
1313+ logs
1414+ (jsont (>= 0.2.0))
1515+ (jsont_bytesrw (>= 0.2.0))
1616+ (alcotest :with-test)))
+12
lib/claude.ml
···11+module Model = Model
22+module Content_block = Content_block
33+module Message = Message
44+module Control = Control
55+module Permissions = Permissions
66+module Hooks = Hooks
77+module Sdk_control = Sdk_control
88+module Incoming = Incoming
99+module Structured_output = Structured_output
1010+module Options = Options
1111+module Transport = Transport
1212+module Client = Client
+184
lib/claude.mli
···11+(** OCaml Eio library for Claude Code CLI.
22+33+ This library provides an interface to the Claude Code command-line interface
44+ using OCaml's Eio concurrency library. It wraps Claude CLI invocations with
55+ JSON streaming for asynchronous communication.
66+77+ {1 Overview}
88+99+ The Claude library enables you to:
1010+ - Send messages to Claude and receive streaming responses
1111+ - Control tool permissions and execution
1212+ - Configure system prompts and model parameters
1313+ - Handle content blocks including text, tool use, and thinking blocks
1414+ - Manage sessions with proper resource cleanup
1515+1616+ {1 Architecture}
1717+1818+ The library is structured into several focused modules:
1919+2020+ - {!Content_block}: Defines content blocks (text, tool use, tool results, thinking)
2121+ - {!Message}: Messages exchanged with Claude (user, assistant, system, result)
2222+ - {!Control}: Control flow messages for session management
2323+ - {!Permissions}: Fine-grained permission system for tool usage
2424+ - {!Options}: Configuration options for Claude sessions
2525+ - {!Transport}: Low-level transport layer for CLI communication
2626+ - {!Client}: High-level client interface for interacting with Claude
2727+2828+ {1 Basic Usage}
2929+3030+ {[
3131+ open Claude
3232+3333+ (* Create a simple query *)
3434+ let query_claude ~sw env prompt =
3535+ let options = Options.default in
3636+ Client.query ~sw env ~options prompt
3737+3838+ (* Process streaming responses *)
3939+ let process_response messages =
4040+ Seq.iter (function
4141+ | Message.Assistant msg ->
4242+ List.iter (function
4343+ | Content_block.Text t ->
4444+ print_endline (Content_block.Text.text t)
4545+ | _ -> ()
4646+ ) (Message.Assistant.content msg)
4747+ | _ -> ()
4848+ ) messages
4949+ ]}
5050+5151+ {1 Advanced Features}
5252+5353+ {2 Tool Permissions}
5454+5555+ Control which tools Claude can use and how:
5656+5757+ {[
5858+ let options =
5959+ Options.default
6060+ |> Options.with_allowed_tools ["Read"; "Write"; "Bash"]
6161+ |> Options.with_permission_mode Permissions.Mode.Accept_edits
6262+ ]}
6363+6464+ {2 Custom Permission Callbacks}
6565+6666+ Implement custom logic for tool approval:
6767+6868+ {[
6969+ let my_callback ~tool_name ~input ~context =
7070+ if tool_name = "Bash" then
7171+ Permissions.Result.deny ~message:"Bash not allowed" ~interrupt:false
7272+ else
7373+ Permissions.Result.allow ()
7474+7575+ let options = Options.default |> Options.with_permission_callback my_callback
7676+ ]}
7777+7878+ {2 System Prompts}
7979+8080+ Customize Claude's behavior with system prompts:
8181+8282+ {[
8383+ let options =
8484+ Options.default
8585+ |> Options.with_system_prompt "You are a helpful OCaml programming assistant."
8686+ |> Options.with_append_system_prompt "Always use Jane Street style."
8787+ ]}
8888+8989+ {1 Logging}
9090+9191+ The library uses the Logs library for structured logging. Each module has its
9292+ own log source (e.g., "claude.message", "claude.transport") allowing fine-grained
9393+ control over logging verbosity:
9494+9595+ {[
9696+ (* Enable debug logging for message handling *)
9797+ Logs.Src.set_level Message.src (Some Logs.Debug);
9898+9999+ (* Enable info logging for transport layer *)
100100+ Logs.Src.set_level Transport.src (Some Logs.Info);
101101+ ]}
102102+103103+ {1 Error Handling}
104104+105105+ The library uses exceptions for error handling. Common exceptions include:
106106+ - [Invalid_argument]: For malformed JSON or invalid parameters
107107+ - [Transport.Transport_error]: For CLI communication failures
108108+ - [Message.Message_parse_error]: For message parsing failures
109109+110110+ {1 Example: Complete Session}
111111+112112+ {[
113113+ let run_claude_session ~sw env =
114114+ let options =
115115+ Options.create
116116+ ~allowed_tools:["Read"; "Write"]
117117+ ~permission_mode:Permissions.Mode.Accept_edits
118118+ ~system_prompt:"You are an OCaml expert."
119119+ ~max_thinking_tokens:10000
120120+ ()
121121+ in
122122+123123+ let prompt = "Write a function to calculate fibonacci numbers" in
124124+ let messages = Client.query ~sw env ~options prompt in
125125+126126+ Seq.iter (fun msg ->
127127+ Message.log_received msg;
128128+ match msg with
129129+ | Message.Assistant assistant ->
130130+ Printf.printf "Claude: %s\n"
131131+ (Message.Assistant.model assistant);
132132+ List.iter (function
133133+ | Content_block.Text t ->
134134+ print_endline (Content_block.Text.text t)
135135+ | Content_block.Tool_use t ->
136136+ Printf.printf "Using tool: %s\n"
137137+ (Content_block.Tool_use.name t)
138138+ | _ -> ()
139139+ ) (Message.Assistant.content assistant)
140140+ | Message.Result result ->
141141+ Printf.printf "Session complete. Duration: %dms\n"
142142+ (Message.Result.duration_ms result)
143143+ | _ -> ()
144144+ ) messages
145145+ ]}
146146+*)
147147+148148+(** {1 Modules} *)
149149+150150+module Client = Client
151151+(** High-level client interface for Claude interactions. *)
152152+153153+module Options = Options
154154+(** Configuration options for Claude sessions. *)
155155+156156+module Model = Model
157157+(** Claude AI model identifiers with type-safe variants. *)
158158+159159+module Content_block = Content_block
160160+(** Content blocks for messages (text, tool use, tool results, thinking). *)
161161+162162+module Message = Message
163163+(** Messages exchanged with Claude (user, assistant, system, result). *)
164164+165165+module Control = Control
166166+(** Control messages for session management. *)
167167+168168+module Permissions = Permissions
169169+(** Permission system for tool invocations. *)
170170+171171+module Hooks = Hooks
172172+(** Hooks system for event interception. *)
173173+174174+module Sdk_control = Sdk_control
175175+(** SDK control protocol for dynamic configuration. *)
176176+177177+module Incoming = Incoming
178178+(** Discriminated union of all incoming message types from Claude CLI. *)
179179+180180+module Structured_output = Structured_output
181181+(** Structured output support using JSON Schema. *)
182182+183183+module Transport = Transport
184184+(** Low-level transport layer for CLI communication. *)
+396
lib/client.ml
···11+let src = Logs.Src.create "claude.client" ~doc:"Claude client"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(** Control response builders using jsont *)
55+module Control_response = struct
66+ let success ~request_id ~response =
77+ Jsont.Json.object' [
88+ Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
99+ Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
1010+ Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "success");
1111+ Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
1212+ Jsont.Json.mem (Jsont.Json.name "response") response;
1313+ ]);
1414+ ]
1515+1616+ let error ~request_id ~message =
1717+ Jsont.Json.object' [
1818+ Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
1919+ Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
2020+ Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "error");
2121+ Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
2222+ Jsont.Json.mem (Jsont.Json.name "error") (Jsont.Json.string message);
2323+ ]);
2424+ ]
2525+end
2626+2727+(* Helper functions for JSON manipulation using jsont *)
2828+let json_to_string json =
2929+ match Jsont_bytesrw.encode_string' Jsont.json json with
3030+ | Ok s -> s
3131+ | Error err -> failwith (Jsont.Error.to_string err)
3232+3333+(* JSON construction helpers using jsont *)
3434+let json_string s = Jsont.Json.string s
3535+let json_null () = Jsont.Json.null ()
3636+3737+let json_object pairs =
3838+ Jsont.Json.object' (List.map (fun (k, v) -> Jsont.Json.mem (Jsont.Json.name k) v) pairs)
3939+4040+type t = {
4141+ transport : Transport.t;
4242+ permission_callback : Permissions.callback option;
4343+ permission_log : Permissions.Rule.t list ref option;
4444+ hook_callbacks : (string, Hooks.callback) Hashtbl.t;
4545+ mutable next_callback_id : int;
4646+ mutable session_id : string option;
4747+ control_responses : (string, Jsont.json) Hashtbl.t;
4848+ control_mutex : Eio.Mutex.t;
4949+ control_condition : Eio.Condition.t;
5050+}
5151+5252+let handle_control_request t (ctrl_req : Incoming.Control_request.t) =
5353+ let request_id = Incoming.Control_request.request_id ctrl_req in
5454+ Log.info (fun m -> m "Handling control request: %s" (Incoming.Control_request.subtype ctrl_req));
5555+5656+ match Incoming.Control_request.request ctrl_req with
5757+ | Incoming.Control_request.Can_use_tool req ->
5858+ let tool_name = Incoming.Control_request.Can_use_tool.tool_name req in
5959+ let input = Incoming.Control_request.Can_use_tool.input req in
6060+ Log.info (fun m -> m "Permission request for tool '%s' with input: %s"
6161+ tool_name (json_to_string input));
6262+ (* TODO: Parse permission_suggestions properly *)
6363+ let context = Permissions.Context.create ~suggestions:[] () in
6464+6565+ Log.info (fun m -> m "Invoking permission callback for tool: %s" tool_name);
6666+ let result = match t.permission_callback with
6767+ | Some callback ->
6868+ Log.info (fun m -> m "Using custom permission callback");
6969+ callback ~tool_name ~input ~context
7070+ | None ->
7171+ Log.info (fun m -> m "Using default allow callback");
7272+ Permissions.default_allow_callback ~tool_name ~input ~context
7373+ in
7474+ Log.info (fun m -> m "Permission callback returned: %s"
7575+ (match result with
7676+ | Permissions.Result.Allow _ -> "ALLOW"
7777+ | Permissions.Result.Deny _ -> "DENY"));
7878+7979+ (* Convert permission result to CLI format *)
8080+ let response_data = match result with
8181+ | Permissions.Result.Allow { updated_input; updated_permissions = _; unknown = _ } ->
8282+ let updated_input = Option.value updated_input ~default:input in
8383+ json_object [
8484+ ("behavior", json_string "allow");
8585+ ("updatedInput", updated_input);
8686+ ]
8787+ | Permissions.Result.Deny { message; interrupt = _; unknown = _ } ->
8888+ json_object [
8989+ ("behavior", json_string "deny");
9090+ ("message", json_string message);
9191+ ]
9292+ in
9393+ let response = Control_response.success ~request_id ~response:response_data in
9494+ Log.info (fun m -> m "Sending control response: %s" (json_to_string response));
9595+ Transport.send t.transport response
9696+9797+ | Incoming.Control_request.Hook_callback req ->
9898+ let callback_id = Incoming.Control_request.Hook_callback.callback_id req in
9999+ let input = Incoming.Control_request.Hook_callback.input req in
100100+ let tool_use_id = Incoming.Control_request.Hook_callback.tool_use_id req in
101101+ Log.info (fun m -> m "Hook callback request for callback_id: %s" callback_id);
102102+103103+ (try
104104+ let callback = Hashtbl.find t.hook_callbacks callback_id in
105105+ let context = Hooks.Context.create () in
106106+ let result = callback ~input ~tool_use_id ~context in
107107+108108+ let result_json = match Jsont.Json.encode Hooks.result_jsont result with
109109+ | Ok j -> j
110110+ | Error msg -> failwith ("Failed to encode hook result: " ^ msg)
111111+ in
112112+ let response = Control_response.success ~request_id ~response:result_json in
113113+ Log.info (fun m -> m "Hook callback succeeded, sending response");
114114+ Transport.send t.transport response
115115+ with
116116+ | Not_found ->
117117+ let error_msg = Printf.sprintf "Hook callback not found: %s" callback_id in
118118+ Log.err (fun m -> m "%s" error_msg);
119119+ Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
120120+ | exn ->
121121+ let error_msg = Printf.sprintf "Hook callback error: %s" (Printexc.to_string exn) in
122122+ Log.err (fun m -> m "%s" error_msg);
123123+ Transport.send t.transport (Control_response.error ~request_id ~message:error_msg))
124124+125125+ | Incoming.Control_request.Unknown (subtype, _) ->
126126+ let error_msg = Printf.sprintf "Unsupported control request: %s" subtype in
127127+ Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
128128+129129+let handle_control_response t control_resp =
130130+ let request_id = match control_resp.Sdk_control.response with
131131+ | Sdk_control.Response.Success s -> s.request_id
132132+ | Sdk_control.Response.Error e -> e.request_id
133133+ in
134134+ Log.debug (fun m -> m "Received control response for request_id: %s" request_id);
135135+136136+ (* Store the response as JSON and signal waiting threads *)
137137+ let json = match Jsont.Json.encode Sdk_control.control_response_jsont control_resp with
138138+ | Ok j -> j
139139+ | Error err -> failwith ("Failed to encode control response: " ^ err)
140140+ in
141141+ Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
142142+ Hashtbl.replace t.control_responses request_id json;
143143+ Eio.Condition.broadcast t.control_condition
144144+ )
145145+146146+let handle_messages t =
147147+ let rec loop () =
148148+ match Transport.receive_line t.transport with
149149+ | None ->
150150+ (* EOF *)
151151+ Log.debug (fun m -> m "Handle messages: EOF received");
152152+ Seq.Nil
153153+ | Some line ->
154154+ (* Use unified Incoming codec for all message types *)
155155+ match Jsont_bytesrw.decode_string' Incoming.jsont line with
156156+ | Ok (Incoming.Message msg) ->
157157+ Log.info (fun m -> m "← %a" Message.pp msg);
158158+159159+ (* Extract session ID from system messages *)
160160+ (match msg with
161161+ | Message.System sys ->
162162+ (match Message.System.session_id sys with
163163+ | Some session_id ->
164164+ t.session_id <- Some session_id;
165165+ Log.debug (fun m -> m "Stored session ID: %s" session_id)
166166+ | None -> ())
167167+ | _ -> ());
168168+169169+ Seq.Cons (msg, loop)
170170+171171+ | Ok (Incoming.Control_response resp) ->
172172+ handle_control_response t resp;
173173+ loop ()
174174+175175+ | Ok (Incoming.Control_request ctrl_req) ->
176176+ Log.info (fun m -> m "Received control request: %s (request_id: %s)"
177177+ (Incoming.Control_request.subtype ctrl_req)
178178+ (Incoming.Control_request.request_id ctrl_req));
179179+ handle_control_request t ctrl_req;
180180+ loop ()
181181+182182+ | Error err ->
183183+ Log.err (fun m -> m "Failed to decode incoming message: %s\nLine: %s"
184184+ (Jsont.Error.to_string err) line);
185185+ loop ()
186186+ in
187187+ Log.debug (fun m -> m "Starting message handler");
188188+ loop
189189+190190+let create ?(options = Options.default) ~sw ~process_mgr () =
191191+ (* Automatically enable permission prompt tool when callback is configured
192192+ (matching Python SDK behavior in client.py:104-121) *)
193193+ let options =
194194+ match Options.permission_callback options with
195195+ | Some _ when Options.permission_prompt_tool_name options = None ->
196196+ (* Set permission_prompt_tool_name to "stdio" to enable control protocol *)
197197+ Options.with_permission_prompt_tool_name "stdio" options
198198+ | _ -> options
199199+ in
200200+ let transport = Transport.create ~sw ~process_mgr ~options () in
201201+202202+ (* Setup hook callbacks *)
203203+ let hook_callbacks = Hashtbl.create 16 in
204204+ let next_callback_id = ref 0 in
205205+206206+ let t = {
207207+ transport;
208208+ permission_callback = Options.permission_callback options;
209209+ permission_log = None;
210210+ hook_callbacks;
211211+ next_callback_id = 0;
212212+ session_id = None;
213213+ control_responses = Hashtbl.create 16;
214214+ control_mutex = Eio.Mutex.create ();
215215+ control_condition = Eio.Condition.create ();
216216+ } in
217217+218218+ (* Register hooks and send initialize if hooks are configured *)
219219+ (match Options.hooks options with
220220+ | Some hooks_config ->
221221+ Log.info (fun m -> m "Registering hooks...");
222222+223223+ (* Build hooks configuration with callback IDs *)
224224+ let hooks_json = List.fold_left (fun acc (event, matchers) ->
225225+ let event_name = Hooks.event_to_string event in
226226+ let matchers_json = List.map (fun matcher ->
227227+ let callback_ids = List.map (fun callback ->
228228+ let callback_id = Printf.sprintf "hook_%d" !next_callback_id in
229229+ incr next_callback_id;
230230+ Hashtbl.add hook_callbacks callback_id callback;
231231+ Log.debug (fun m -> m "Registered callback: %s for event: %s" callback_id event_name);
232232+ callback_id
233233+ ) matcher.Hooks.callbacks in
234234+ json_object [
235235+ "matcher", (match matcher.Hooks.matcher with
236236+ | Some p -> json_string p
237237+ | None -> json_null ());
238238+ "hookCallbackIds", Jsont.Json.list (List.map (fun id -> json_string id) callback_ids);
239239+ ]
240240+ ) matchers in
241241+ (event_name, Jsont.Json.list matchers_json) :: acc
242242+ ) [] hooks_config in
243243+244244+ (* Send initialize control request *)
245245+ let initialize_msg = json_object [
246246+ "type", json_string "control_request";
247247+ "request_id", json_string "init_hooks";
248248+ "request", json_object [
249249+ "subtype", json_string "initialize";
250250+ "hooks", json_object hooks_json;
251251+ ]
252252+ ] in
253253+ Log.info (fun m -> m "Sending hooks initialize request");
254254+ Transport.send t.transport initialize_msg;
255255+ t.next_callback_id <- !next_callback_id
256256+ | None -> ());
257257+258258+ t
259259+260260+let query t prompt =
261261+ let msg = Message.user_string prompt in
262262+ Log.info (fun m -> m "→ %a" Message.pp msg);
263263+ let json = Message.to_json msg in
264264+ Transport.send t.transport json
265265+266266+let send_message t msg =
267267+ Log.info (fun m -> m "→ %a" Message.pp msg);
268268+ let json = Message.to_json msg in
269269+ Transport.send t.transport json
270270+271271+let send_user_message t user_msg =
272272+ let msg = Message.User user_msg in
273273+ Log.info (fun m -> m "→ %a" Message.pp msg);
274274+ let json = Message.User.to_json user_msg in
275275+ Transport.send t.transport json
276276+277277+let receive t =
278278+ handle_messages t
279279+280280+let receive_all t =
281281+ let rec collect acc seq =
282282+ match seq () with
283283+ | Seq.Nil ->
284284+ Log.debug (fun m -> m "End of message sequence (%d messages)" (List.length acc));
285285+ List.rev acc
286286+ | Seq.Cons (Message.Result _ as msg, _) ->
287287+ Log.debug (fun m -> m "Received final Result message");
288288+ List.rev (msg :: acc)
289289+ | Seq.Cons (msg, rest) ->
290290+ collect (msg :: acc) rest
291291+ in
292292+ collect [] (handle_messages t)
293293+294294+let interrupt t =
295295+ Transport.interrupt t.transport
296296+297297+let discover_permissions t =
298298+ let log = ref [] in
299299+ let callback = Permissions.discovery_callback log in
300300+ { t with
301301+ permission_callback = Some callback;
302302+ permission_log = Some log
303303+ }
304304+305305+let get_discovered_permissions t =
306306+ match t.permission_log with
307307+ | Some log -> !log
308308+ | None -> []
309309+310310+let with_permission_callback t callback =
311311+ { t with permission_callback = Some callback }
312312+313313+(* Helper to send a control request and wait for response *)
314314+let send_control_request t ~request_id request =
315315+ (* Send the control request *)
316316+ let control_msg = Sdk_control.create_request ~request_id ~request () in
317317+ let json = match Jsont.Json.encode Sdk_control.jsont control_msg with
318318+ | Ok j -> j
319319+ | Error msg -> failwith ("Failed to encode control request: " ^ msg)
320320+ in
321321+ Log.info (fun m -> m "Sending control request: %s" (json_to_string json));
322322+ Transport.send t.transport json;
323323+324324+ (* Wait for the response with timeout *)
325325+ let max_wait = 10.0 in (* 10 seconds timeout *)
326326+ let start_time = Unix.gettimeofday () in
327327+328328+ let rec wait_for_response () =
329329+ Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
330330+ match Hashtbl.find_opt t.control_responses request_id with
331331+ | Some response_json ->
332332+ (* Remove it from the table *)
333333+ Hashtbl.remove t.control_responses request_id;
334334+ response_json
335335+ | None ->
336336+ let elapsed = Unix.gettimeofday () -. start_time in
337337+ if elapsed > max_wait then
338338+ raise (Failure (Printf.sprintf "Timeout waiting for control response: %s" request_id))
339339+ else (
340340+ (* Release mutex and wait for signal *)
341341+ Eio.Condition.await_no_mutex t.control_condition;
342342+ wait_for_response ()
343343+ )
344344+ )
345345+ in
346346+347347+ let response_json = wait_for_response () in
348348+ Log.debug (fun m -> m "Received control response: %s" (json_to_string response_json));
349349+350350+ (* Parse the response - extract the "response" field using jsont codec *)
351351+ let response_field_codec = Jsont.Object.map ~kind:"ResponseField" Fun.id
352352+ |> Jsont.Object.mem "response" Jsont.json ~enc:Fun.id
353353+ |> Jsont.Object.finish
354354+ in
355355+ let response_data = match Jsont.Json.decode response_field_codec response_json with
356356+ | Ok r -> r
357357+ | Error msg -> raise (Invalid_argument ("Failed to extract response field: " ^ msg))
358358+ in
359359+ let response = match Jsont.Json.decode Sdk_control.Response.jsont response_data with
360360+ | Ok r -> r
361361+ | Error msg -> raise (Invalid_argument ("Failed to decode response: " ^ msg))
362362+ in
363363+ match response with
364364+ | Sdk_control.Response.Success s -> s.response
365365+ | Sdk_control.Response.Error e ->
366366+ raise (Failure (Printf.sprintf "Control request failed: %s" e.error))
367367+368368+let set_permission_mode t mode =
369369+ let request_id = Printf.sprintf "set_perm_mode_%f" (Unix.gettimeofday ()) in
370370+ let request = Sdk_control.Request.set_permission_mode ~mode () in
371371+ let _response = send_control_request t ~request_id request in
372372+ Log.info (fun m -> m "Permission mode set to: %a" Permissions.Mode.pp mode)
373373+374374+let set_model t model =
375375+ let model_str = Model.to_string model in
376376+ let request_id = Printf.sprintf "set_model_%f" (Unix.gettimeofday ()) in
377377+ let request = Sdk_control.Request.set_model ~model:model_str () in
378378+ let _response = send_control_request t ~request_id request in
379379+ Log.info (fun m -> m "Model set to: %a" Model.pp model)
380380+381381+let set_model_string t model_str =
382382+ set_model t (Model.of_string model_str)
383383+384384+let get_server_info t =
385385+ let request_id = Printf.sprintf "get_server_info_%f" (Unix.gettimeofday ()) in
386386+ let request = Sdk_control.Request.get_server_info () in
387387+ match send_control_request t ~request_id request with
388388+ | Some response_data ->
389389+ let server_info = match Jsont.Json.decode Sdk_control.Server_info.jsont response_data with
390390+ | Ok si -> si
391391+ | Error msg -> raise (Invalid_argument ("Failed to decode server info: " ^ msg))
392392+ in
393393+ Log.info (fun m -> m "Retrieved server info: %a" Sdk_control.Server_info.pp server_info);
394394+ server_info
395395+ | None ->
396396+ raise (Failure "No response data from get_server_info request")
+213
lib/client.mli
···11+(** Client interface for interacting with Claude.
22+33+ This module provides the high-level client API for sending messages to
44+ Claude and receiving responses. It handles the bidirectional streaming
55+ protocol, permission callbacks, and hooks.
66+77+ {2 Basic Usage}
88+99+ {[
1010+ Eio.Switch.run @@ fun sw ->
1111+ let client = Client.create ~sw ~process_mgr () in
1212+ Client.query client "What is 2+2?";
1313+1414+ let messages = Client.receive_all client in
1515+ List.iter (function
1616+ | Message.Assistant msg ->
1717+ Printf.printf "Claude: %s\n" (Message.Assistant.text msg)
1818+ | _ -> ()
1919+ ) messages
2020+ ]}
2121+2222+ {2 Features}
2323+2424+ - {b Message Streaming}: Messages are streamed lazily via {!Seq.t}
2525+ - {b Permission Control}: Custom permission callbacks for tool usage
2626+ - {b Hooks}: Intercept and modify tool execution
2727+ - {b Dynamic Control}: Change settings mid-conversation
2828+ - {b Resource Management}: Automatic cleanup via Eio switches
2929+3030+ {2 Message Flow}
3131+3232+ 1. Create a client with {!create}
3333+ 2. Send messages with {!query} or {!send_message}
3434+ 3. Receive responses with {!receive} or {!receive_all}
3535+ 4. Continue multi-turn conversations by sending more messages
3636+ 5. Client automatically cleans up when the switch exits
3737+3838+ {2 Advanced Features}
3939+4040+ - Permission discovery mode for understanding required permissions
4141+ - Mid-conversation model switching and permission mode changes
4242+ - Server capability introspection *)
4343+4444+(** The log source for client operations *)
4545+val src : Logs.Src.t
4646+4747+type t
4848+(** The type of Claude clients. *)
4949+5050+val create :
5151+ ?options:Options.t ->
5252+ sw:Eio.Switch.t ->
5353+ process_mgr:_ Eio.Process.mgr ->
5454+ unit -> t
5555+(** [create ?options ~sw ~process_mgr ()] creates a new Claude client.
5656+5757+ @param options Configuration options (defaults to {!Options.default})
5858+ @param sw Eio switch for resource management
5959+ @param process_mgr Eio process manager for spawning the Claude CLI *)
6060+6161+val query : t -> string -> unit
6262+(** [query t prompt] sends a text message to Claude.
6363+6464+ This is a convenience function for simple string messages. For more
6565+ complex messages with tool results or multiple content blocks, use
6666+ {!send_message} instead. *)
6767+6868+val send_message : t -> Message.t -> unit
6969+(** [send_message t msg] sends a message to Claude.
7070+7171+ Supports all message types including user messages with tool results. *)
7272+7373+val send_user_message : t -> Message.User.t -> unit
7474+(** [send_user_message t msg] sends a user message to Claude. *)
7575+7676+val receive : t -> Message.t Seq.t
7777+(** [receive t] returns a lazy sequence of messages from Claude.
7878+7979+ The sequence yields messages as they arrive from Claude, including:
8080+ - {!constructor:Message.Assistant} - Claude's responses
8181+ - {!constructor:Message.System} - System notifications
8282+ - {!constructor:Message.Result} - Final result with usage statistics
8383+8484+ Control messages (permission requests, hook callbacks) are handled
8585+ internally and not yielded to the sequence. *)
8686+8787+val receive_all : t -> Message.t list
8888+(** [receive_all t] collects all messages into a list.
8989+9090+ This is a convenience function that consumes the {!receive} sequence.
9191+ Use this when you want to process all messages at once rather than
9292+ streaming them. *)
9393+9494+val interrupt : t -> unit
9595+(** [interrupt t] sends an interrupt signal to stop Claude's execution. *)
9696+9797+val discover_permissions : t -> t
9898+(** [discover_permissions t] enables permission discovery mode.
9999+100100+ In discovery mode, all tool usage is logged but allowed. Use
101101+ {!get_discovered_permissions} to retrieve the list of permissions
102102+ that were requested during execution.
103103+104104+ This is useful for understanding what permissions your prompt requires. *)
105105+106106+val get_discovered_permissions : t -> Permissions.Rule.t list
107107+(** [get_discovered_permissions t] returns permissions discovered during execution.
108108+109109+ Only useful after enabling {!discover_permissions}. *)
110110+111111+val with_permission_callback : t -> Permissions.callback -> t
112112+(** [with_permission_callback t callback] updates the permission callback.
113113+114114+ Allows dynamically changing the permission callback without recreating
115115+ the client. *)
116116+117117+(** {1 Dynamic Control Methods}
118118+119119+ These methods allow you to change Claude's behavior mid-conversation
120120+ without recreating the client. This is useful for:
121121+122122+ - Adjusting permission strictness based on user feedback
123123+ - Switching to faster/cheaper models for simple tasks
124124+ - Adapting to changing requirements during long conversations
125125+ - Introspecting server capabilities
126126+127127+ {2 Example: Adaptive Permission Control}
128128+129129+ {[
130130+ (* Start with strict permissions *)
131131+ let client = Client.create ~sw ~process_mgr
132132+ ~options:(Options.default
133133+ |> Options.with_permission_mode Permissions.Mode.Default) ()
134134+ in
135135+136136+ Client.query client "Analyze this code";
137137+ let _ = Client.receive_all client in
138138+139139+ (* User approves, switch to auto-accept edits *)
140140+ Client.set_permission_mode client Permissions.Mode.Accept_edits;
141141+142142+ Client.query client "Now refactor it";
143143+ let _ = Client.receive_all client in
144144+ ]}
145145+146146+ {2 Example: Model Switching for Efficiency}
147147+148148+ {[
149149+ (* Use powerful model for complex analysis *)
150150+ let client = Client.create ~sw ~process_mgr
151151+ ~options:(Options.default |> Options.with_model "claude-sonnet-4-5") ()
152152+ in
153153+154154+ Client.query client "Design a new architecture for this system";
155155+ let _ = Client.receive_all client in
156156+157157+ (* Switch to faster model for simple tasks *)
158158+ Client.set_model client "claude-haiku-4";
159159+160160+ Client.query client "Now write a README";
161161+ let _ = Client.receive_all client in
162162+ ]}
163163+164164+ {2 Example: Server Introspection}
165165+166166+ {[
167167+ let info = Client.get_server_info client in
168168+ Printf.printf "Claude CLI version: %s\n"
169169+ (Sdk_control.Server_info.version info);
170170+ Printf.printf "Capabilities: %s\n"
171171+ (String.concat ", " (Sdk_control.Server_info.capabilities info));
172172+ ]} *)
173173+174174+val set_permission_mode : t -> Permissions.Mode.t -> unit
175175+(** [set_permission_mode t mode] changes the permission mode mid-conversation.
176176+177177+ This allows switching between permission modes without recreating the client:
178178+ - {!Permissions.Mode.Default} - Prompt for all permissions
179179+ - {!Permissions.Mode.Accept_edits} - Auto-accept file edits
180180+ - {!Permissions.Mode.Plan} - Planning mode with restricted execution
181181+ - {!Permissions.Mode.Bypass_permissions} - Skip all permission checks
182182+183183+ @raise Failure if the server returns an error *)
184184+185185+val set_model : t -> Model.t -> unit
186186+(** [set_model t model] switches to a different AI model mid-conversation.
187187+188188+ Common models:
189189+ - [`Sonnet_4_5] - Most capable, balanced performance
190190+ - [`Opus_4] - Maximum capability for complex tasks
191191+ - [`Haiku_4] - Fast and cost-effective
192192+193193+ @raise Failure if the model is invalid or unavailable *)
194194+195195+val set_model_string : t -> string -> unit
196196+(** [set_model_string t model] switches to a different AI model using a string.
197197+198198+ This is a convenience function that parses the string using {!Model.of_string}.
199199+200200+ @raise Failure if the model is invalid or unavailable *)
201201+202202+val get_server_info : t -> Sdk_control.Server_info.t
203203+(** [get_server_info t] retrieves server capabilities and metadata.
204204+205205+ Returns information about:
206206+ - Server version string
207207+ - Available capabilities
208208+ - Supported commands
209209+ - Available output styles
210210+211211+ Useful for feature detection and debugging.
212212+213213+ @raise Failure if the server returns an error *)
+276
lib/content_block.ml
···11+let src = Logs.Src.create "claude.content_block" ~doc:"Claude content blocks"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+55+module Text = struct
66+ type t = {
77+ text : string;
88+ unknown : Unknown.t;
99+ }
1010+1111+ let create text = { text; unknown = Unknown.empty }
1212+1313+ let make text unknown = { text; unknown }
1414+ let text t = t.text
1515+ let unknown t = t.unknown
1616+1717+ let jsont : t Jsont.t =
1818+ Jsont.Object.map ~kind:"Text" make
1919+ |> Jsont.Object.mem "text" Jsont.string ~enc:text
2020+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
2121+ |> Jsont.Object.finish
2222+2323+ let to_json t =
2424+ match Jsont.Json.encode jsont t with
2525+ | Ok json -> json
2626+ | Error msg -> failwith ("Text.to_json: " ^ msg)
2727+2828+ let of_json json =
2929+ match Jsont.Json.decode jsont json with
3030+ | Ok v -> v
3131+ | Error msg -> raise (Invalid_argument ("Text.of_json: " ^ msg))
3232+3333+ let pp fmt t =
3434+ if String.length t.text > 60 then
3535+ let truncated = String.sub t.text 0 57 in
3636+ Fmt.pf fmt "Text[%s...]" truncated
3737+ else
3838+ Fmt.pf fmt "Text[%S]" t.text
3939+end
4040+4141+module Tool_use = struct
4242+ module Input = struct
4343+ (* Dynamic JSON data for tool inputs with typed accessors using jsont decoders *)
4444+ type t = Jsont.json
4545+4646+ let jsont = Jsont.json
4747+4848+ let of_string_pairs pairs =
4949+ Jsont.Json.object' (List.map (fun (k, v) ->
5050+ Jsont.Json.mem (Jsont.Json.name k) (Jsont.Json.string v)
5151+ ) pairs)
5252+5353+ let of_assoc (assoc : (string * Jsont.json) list) : t =
5454+ Jsont.Json.object' (List.map (fun (k, v) -> Jsont.Json.mem (Jsont.Json.name k) v) assoc)
5555+5656+ (* Helper to decode an optional field with a given codec *)
5757+ let get_opt (type a) (codec : a Jsont.t) t key : a option =
5858+ let field_codec = Jsont.Object.map ~kind:"field" (fun v -> v)
5959+ |> Jsont.Object.opt_mem key codec ~enc:Fun.id
6060+ |> Jsont.Object.finish
6161+ in
6262+ match Jsont.Json.decode field_codec t with
6363+ | Ok v -> v
6464+ | Error _ -> None
6565+6666+ let get_string t key = get_opt Jsont.string t key
6767+ let get_int t key = get_opt Jsont.int t key
6868+ let get_bool t key = get_opt Jsont.bool t key
6969+ let get_float t key = get_opt Jsont.number t key
7070+7171+ let keys t =
7272+ (* Decode as object with all members captured as unknown *)
7373+ match t with
7474+ | Jsont.Object (members, _) -> List.map (fun ((name, _), _) -> name) members
7575+ | _ -> []
7676+7777+ let to_json t = t
7878+ let of_json json = json
7979+ end
8080+8181+ type t = {
8282+ id : string;
8383+ name : string;
8484+ input : Input.t;
8585+ unknown : Unknown.t;
8686+ }
8787+8888+ let create ~id ~name ~input = { id; name; input; unknown = Unknown.empty }
8989+9090+ let make id name input unknown = { id; name; input; unknown }
9191+ let id t = t.id
9292+ let name t = t.name
9393+ let input t = t.input
9494+ let unknown t = t.unknown
9595+9696+ let jsont : t Jsont.t =
9797+ Jsont.Object.map ~kind:"Tool_use" make
9898+ |> Jsont.Object.mem "id" Jsont.string ~enc:id
9999+ |> Jsont.Object.mem "name" Jsont.string ~enc:name
100100+ |> Jsont.Object.mem "input" Input.jsont ~enc:input
101101+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
102102+ |> Jsont.Object.finish
103103+104104+ let to_json t =
105105+ match Jsont.Json.encode jsont t with
106106+ | Ok json -> json
107107+ | Error msg -> failwith ("Tool_use.to_json: " ^ msg)
108108+109109+ let of_json json =
110110+ match Jsont.Json.decode jsont json with
111111+ | Ok v -> v
112112+ | Error msg -> raise (Invalid_argument ("Tool_use.of_json: " ^ msg))
113113+114114+ let pp fmt t =
115115+ let keys = Input.keys t.input in
116116+ let key_info = match keys with
117117+ | [] -> ""
118118+ | [k] -> Printf.sprintf "(%s)" k
119119+ | ks -> Printf.sprintf "(%d params)" (List.length ks)
120120+ in
121121+ Fmt.pf fmt "Tool[%s%s]" t.name key_info
122122+end
123123+124124+module Tool_result = struct
125125+ type t = {
126126+ tool_use_id : string;
127127+ content : string option;
128128+ is_error : bool option;
129129+ unknown : Unknown.t;
130130+ }
131131+132132+ let create ~tool_use_id ?content ?is_error () =
133133+ { tool_use_id; content; is_error; unknown = Unknown.empty }
134134+135135+ let make tool_use_id content is_error unknown =
136136+ { tool_use_id; content; is_error; unknown }
137137+ let tool_use_id t = t.tool_use_id
138138+ let content t = t.content
139139+ let is_error t = t.is_error
140140+ let unknown t = t.unknown
141141+142142+ let jsont : t Jsont.t =
143143+ Jsont.Object.map ~kind:"Tool_result" make
144144+ |> Jsont.Object.mem "tool_use_id" Jsont.string ~enc:tool_use_id
145145+ |> Jsont.Object.opt_mem "content" Jsont.string ~enc:content
146146+ |> Jsont.Object.opt_mem "is_error" Jsont.bool ~enc:is_error
147147+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
148148+ |> Jsont.Object.finish
149149+150150+ let to_json t =
151151+ match Jsont.Json.encode jsont t with
152152+ | Ok json -> json
153153+ | Error msg -> failwith ("Tool_result.to_json: " ^ msg)
154154+155155+ let of_json json =
156156+ match Jsont.Json.decode jsont json with
157157+ | Ok v -> v
158158+ | Error msg -> raise (Invalid_argument ("Tool_result.of_json: " ^ msg))
159159+160160+ let pp fmt t =
161161+ match t.is_error, t.content with
162162+ | Some true, Some c ->
163163+ if String.length c > 40 then
164164+ let truncated = String.sub c 0 37 in
165165+ Fmt.pf fmt "ToolResult[error: %s...]" truncated
166166+ else
167167+ Fmt.pf fmt "ToolResult[error: %s]" c
168168+ | _, Some c ->
169169+ if String.length c > 40 then
170170+ let truncated = String.sub c 0 37 in
171171+ Fmt.pf fmt "ToolResult[%s...]" truncated
172172+ else
173173+ Fmt.pf fmt "ToolResult[%s]" c
174174+ | _, None -> Fmt.pf fmt "ToolResult[empty]"
175175+end
176176+177177+module Thinking = struct
178178+ type t = {
179179+ thinking : string;
180180+ signature : string;
181181+ unknown : Unknown.t;
182182+ }
183183+184184+ let create ~thinking ~signature = { thinking; signature; unknown = Unknown.empty }
185185+186186+ let make thinking signature unknown = { thinking; signature; unknown }
187187+ let thinking t = t.thinking
188188+ let signature t = t.signature
189189+ let unknown t = t.unknown
190190+191191+ let jsont : t Jsont.t =
192192+ Jsont.Object.map ~kind:"Thinking" make
193193+ |> Jsont.Object.mem "thinking" Jsont.string ~enc:thinking
194194+ |> Jsont.Object.mem "signature" Jsont.string ~enc:signature
195195+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
196196+ |> Jsont.Object.finish
197197+198198+ let to_json t =
199199+ match Jsont.Json.encode jsont t with
200200+ | Ok json -> json
201201+ | Error msg -> failwith ("Thinking.to_json: " ^ msg)
202202+203203+ let of_json json =
204204+ match Jsont.Json.decode jsont json with
205205+ | Ok v -> v
206206+ | Error msg -> raise (Invalid_argument ("Thinking.of_json: " ^ msg))
207207+208208+ let pp fmt t =
209209+ if String.length t.thinking > 50 then
210210+ let truncated = String.sub t.thinking 0 47 in
211211+ Fmt.pf fmt "Thinking[%s...]" truncated
212212+ else
213213+ Fmt.pf fmt "Thinking[%s]" t.thinking
214214+end
215215+216216+type t =
217217+ | Text of Text.t
218218+ | Tool_use of Tool_use.t
219219+ | Tool_result of Tool_result.t
220220+ | Thinking of Thinking.t
221221+222222+let text s = Text (Text.create s)
223223+let tool_use ~id ~name ~input = Tool_use (Tool_use.create ~id ~name ~input)
224224+let tool_result ~tool_use_id ?content ?is_error () =
225225+ Tool_result (Tool_result.create ~tool_use_id ?content ?is_error ())
226226+let thinking ~thinking ~signature =
227227+ Thinking (Thinking.create ~thinking ~signature)
228228+229229+let jsont : t Jsont.t =
230230+ let case_map kind obj dec = Jsont.Object.Case.map kind obj ~dec in
231231+232232+ let case_text = case_map "text" Text.jsont (fun v -> Text v) in
233233+ let case_tool_use = case_map "tool_use" Tool_use.jsont (fun v -> Tool_use v) in
234234+ let case_tool_result = case_map "tool_result" Tool_result.jsont (fun v -> Tool_result v) in
235235+ let case_thinking = case_map "thinking" Thinking.jsont (fun v -> Thinking v) in
236236+237237+ let enc_case = function
238238+ | Text v -> Jsont.Object.Case.value case_text v
239239+ | Tool_use v -> Jsont.Object.Case.value case_tool_use v
240240+ | Tool_result v -> Jsont.Object.Case.value case_tool_result v
241241+ | Thinking v -> Jsont.Object.Case.value case_thinking v
242242+ in
243243+244244+ let cases = Jsont.Object.Case.[
245245+ make case_text;
246246+ make case_tool_use;
247247+ make case_tool_result;
248248+ make case_thinking
249249+ ] in
250250+251251+ Jsont.Object.map ~kind:"Content_block" Fun.id
252252+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
253253+ ~tag_to_string:Fun.id ~tag_compare:String.compare
254254+ |> Jsont.Object.finish
255255+256256+let to_json t =
257257+ match Jsont.Json.encode jsont t with
258258+ | Ok json -> json
259259+ | Error msg -> failwith ("Content_block.to_json: " ^ msg)
260260+261261+let of_json json =
262262+ match Jsont.Json.decode jsont json with
263263+ | Ok v -> v
264264+ | Error msg -> raise (Invalid_argument ("Content_block.of_json: " ^ msg))
265265+266266+let pp fmt = function
267267+ | Text t -> Text.pp fmt t
268268+ | Tool_use t -> Tool_use.pp fmt t
269269+ | Tool_result t -> Tool_result.pp fmt t
270270+ | Thinking t -> Thinking.pp fmt t
271271+272272+let log_received t =
273273+ Log.debug (fun m -> m "Received content block: %a" pp t)
274274+275275+let log_sending t =
276276+ Log.debug (fun m -> m "Sending content block: %a" pp t)
+233
lib/content_block.mli
···11+(** Content blocks for Claude messages.
22+33+ This module defines the various types of content blocks that can appear
44+ in Claude messages, including text, tool use, tool results, and thinking blocks. *)
55+66+(** The log source for content block operations *)
77+val src : Logs.Src.t
88+99+(** {1 Text Blocks} *)
1010+1111+module Text : sig
1212+ (** Plain text content blocks. *)
1313+1414+ type t
1515+ (** The type of text blocks. *)
1616+1717+ val create : string -> t
1818+ (** [create text] creates a new text block with the given text content. *)
1919+2020+ val text : t -> string
2121+ (** [text t] returns the text content of the block. *)
2222+2323+ val unknown : t -> Unknown.t
2424+ (** [unknown t] returns any unknown fields from JSON parsing. *)
2525+2626+ val jsont : t Jsont.t
2727+ (** [jsont] is the Jsont codec for text blocks. *)
2828+2929+ val to_json : t -> Jsont.json
3030+ (** [to_json t] converts the text block to its JSON representation. *)
3131+3232+ val of_json : Jsont.json -> t
3333+ (** [of_json json] parses a text block from JSON.
3434+ @raise Invalid_argument if the JSON is not a valid text block. *)
3535+3636+ val pp : Format.formatter -> t -> unit
3737+ (** [pp fmt t] pretty-prints the text block. *)
3838+end
3939+4040+(** {1 Tool Use Blocks} *)
4141+4242+module Tool_use : sig
4343+ (** Tool invocation requests from the assistant. *)
4444+4545+ module Input : sig
4646+ (** Tool input parameters. *)
4747+4848+ type t
4949+ (** Abstract type for tool inputs (opaque JSON). *)
5050+5151+ val jsont : t Jsont.t
5252+ (** [jsont] is the Jsont codec for tool inputs. *)
5353+5454+ val of_string_pairs : (string * string) list -> t
5555+ (** [of_string_pairs pairs] creates tool input from string key-value pairs. *)
5656+5757+ val of_assoc : (string * Jsont.json) list -> t
5858+ (** [of_assoc assoc] creates tool input from an association list. *)
5959+6060+ val get_string : t -> string -> string option
6161+ (** [get_string t key] returns the string value for [key], if present. *)
6262+6363+ val get_int : t -> string -> int option
6464+ (** [get_int t key] returns the integer value for [key], if present. *)
6565+6666+ val get_bool : t -> string -> bool option
6767+ (** [get_bool t key] returns the boolean value for [key], if present. *)
6868+6969+ val get_float : t -> string -> float option
7070+ (** [get_float t key] returns the float value for [key], if present. *)
7171+7272+ val keys : t -> string list
7373+ (** [keys t] returns all keys in the input. *)
7474+7575+ val to_json : t -> Jsont.json
7676+ (** [to_json t] converts to JSON representation. Internal use only. *)
7777+7878+ val of_json : Jsont.json -> t
7979+ (** [of_json json] parses from JSON. Internal use only. *)
8080+ end
8181+8282+ type t
8383+ (** The type of tool use blocks. *)
8484+8585+ val create : id:string -> name:string -> input:Input.t -> t
8686+ (** [create ~id ~name ~input] creates a new tool use block.
8787+ @param id Unique identifier for this tool invocation
8888+ @param name Name of the tool to invoke
8989+ @param input Parameters for the tool *)
9090+9191+ val id : t -> string
9292+ (** [id t] returns the unique identifier of the tool use. *)
9393+9494+ val name : t -> string
9595+ (** [name t] returns the name of the tool being invoked. *)
9696+9797+ val input : t -> Input.t
9898+ (** [input t] returns the input parameters for the tool. *)
9999+100100+ val unknown : t -> Unknown.t
101101+ (** [unknown t] returns any unknown fields from JSON parsing. *)
102102+103103+ val jsont : t Jsont.t
104104+ (** [jsont] is the Jsont codec for tool use blocks. *)
105105+106106+ val to_json : t -> Jsont.json
107107+ (** [to_json t] converts the tool use block to its JSON representation. *)
108108+109109+ val of_json : Jsont.json -> t
110110+ (** [of_json json] parses a tool use block from JSON.
111111+ @raise Invalid_argument if the JSON is not a valid tool use block. *)
112112+113113+ val pp : Format.formatter -> t -> unit
114114+ (** [pp fmt t] pretty-prints the tool use block. *)
115115+end
116116+117117+(** {1 Tool Result Blocks} *)
118118+119119+module Tool_result : sig
120120+ (** Results from tool invocations. *)
121121+122122+ type t
123123+ (** The type of tool result blocks. *)
124124+125125+ val create : tool_use_id:string -> ?content:string -> ?is_error:bool -> unit -> t
126126+ (** [create ~tool_use_id ?content ?is_error ()] creates a new tool result block.
127127+ @param tool_use_id The ID of the corresponding tool use block
128128+ @param content Optional result content
129129+ @param is_error Whether the tool execution resulted in an error *)
130130+131131+ val tool_use_id : t -> string
132132+ (** [tool_use_id t] returns the ID of the corresponding tool use. *)
133133+134134+ val content : t -> string option
135135+ (** [content t] returns the optional result content. *)
136136+137137+ val is_error : t -> bool option
138138+ (** [is_error t] returns whether this result represents an error. *)
139139+140140+ val unknown : t -> Unknown.t
141141+ (** [unknown t] returns any unknown fields from JSON parsing. *)
142142+143143+ val jsont : t Jsont.t
144144+ (** [jsont] is the Jsont codec for tool result blocks. *)
145145+146146+ val to_json : t -> Jsont.json
147147+ (** [to_json t] converts the tool result block to its JSON representation. *)
148148+149149+ val of_json : Jsont.json -> t
150150+ (** [of_json json] parses a tool result block from JSON.
151151+ @raise Invalid_argument if the JSON is not a valid tool result block. *)
152152+153153+ val pp : Format.formatter -> t -> unit
154154+ (** [pp fmt t] pretty-prints the tool result block. *)
155155+end
156156+157157+(** {1 Thinking Blocks} *)
158158+159159+module Thinking : sig
160160+ (** Assistant's internal reasoning blocks. *)
161161+162162+ type t
163163+ (** The type of thinking blocks. *)
164164+165165+ val create : thinking:string -> signature:string -> t
166166+ (** [create ~thinking ~signature] creates a new thinking block.
167167+ @param thinking The assistant's internal reasoning
168168+ @param signature Cryptographic signature for verification *)
169169+170170+ val thinking : t -> string
171171+ (** [thinking t] returns the thinking content. *)
172172+173173+ val signature : t -> string
174174+ (** [signature t] returns the cryptographic signature. *)
175175+176176+ val unknown : t -> Unknown.t
177177+ (** [unknown t] returns any unknown fields from JSON parsing. *)
178178+179179+ val jsont : t Jsont.t
180180+ (** [jsont] is the Jsont codec for thinking blocks. *)
181181+182182+ val to_json : t -> Jsont.json
183183+ (** [to_json t] converts the thinking block to its JSON representation. *)
184184+185185+ val of_json : Jsont.json -> t
186186+ (** [of_json json] parses a thinking block from JSON.
187187+ @raise Invalid_argument if the JSON is not a valid thinking block. *)
188188+189189+ val pp : Format.formatter -> t -> unit
190190+ (** [pp fmt t] pretty-prints the thinking block. *)
191191+end
192192+193193+(** {1 Content Block Union Type} *)
194194+195195+type t =
196196+ | Text of Text.t
197197+ | Tool_use of Tool_use.t
198198+ | Tool_result of Tool_result.t
199199+ | Thinking of Thinking.t
200200+(** The type of content blocks, which can be text, tool use, tool result, or thinking. *)
201201+202202+val text : string -> t
203203+(** [text s] creates a text content block. *)
204204+205205+val tool_use : id:string -> name:string -> input:Tool_use.Input.t -> t
206206+(** [tool_use ~id ~name ~input] creates a tool use content block. *)
207207+208208+val tool_result : tool_use_id:string -> ?content:string -> ?is_error:bool -> unit -> t
209209+(** [tool_result ~tool_use_id ?content ?is_error ()] creates a tool result content block. *)
210210+211211+val thinking : thinking:string -> signature:string -> t
212212+(** [thinking ~thinking ~signature] creates a thinking content block. *)
213213+214214+val jsont : t Jsont.t
215215+(** [jsont] is the Jsont codec for content blocks. *)
216216+217217+val to_json : t -> Jsont.json
218218+(** [to_json t] converts any content block to its JSON representation. *)
219219+220220+val of_json : Jsont.json -> t
221221+(** [of_json json] parses a content block from JSON.
222222+ @raise Invalid_argument if the JSON is not a valid content block. *)
223223+224224+val pp : Format.formatter -> t -> unit
225225+(** [pp fmt t] pretty-prints any content block. *)
226226+227227+(** {1 Logging} *)
228228+229229+val log_received : t -> unit
230230+(** [log_received t] logs that a content block was received. *)
231231+232232+val log_sending : t -> unit
233233+(** [log_sending t] logs that a content block is being sent. *)
+59
lib/control.ml
···11+let src = Logs.Src.create "claude.control" ~doc:"Claude control messages"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(* Helper for pretty-printing JSON *)
55+let pp_json fmt json =
66+ let s = match Jsont_bytesrw.encode_string' Jsont.json json with
77+ | Ok s -> s
88+ | Error err -> Jsont.Error.to_string err
99+ in
1010+ Fmt.string fmt s
1111+1212+type t = {
1313+ request_id : string;
1414+ subtype : string;
1515+ data : Jsont.json;
1616+ unknown : Unknown.t;
1717+}
1818+1919+let jsont =
2020+ Jsont.Object.map ~kind:"Control"
2121+ (fun request_id subtype data unknown -> {request_id; subtype; data; unknown})
2222+ |> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun t -> t.request_id)
2323+ |> Jsont.Object.mem "subtype" Jsont.string ~enc:(fun t -> t.subtype)
2424+ |> Jsont.Object.mem "data" Jsont.json ~enc:(fun t -> t.data)
2525+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun t -> t.unknown)
2626+ |> Jsont.Object.finish
2727+2828+let create ~request_id ~subtype ~data =
2929+ { request_id; subtype; data; unknown = Unknown.empty }
3030+3131+let request_id t = t.request_id
3232+let subtype t = t.subtype
3333+let data t = t.data
3434+3535+let to_json t =
3636+ match Jsont_bytesrw.encode_string ~format:Jsont.Minify jsont t with
3737+ | Ok s ->
3838+ (match Jsont_bytesrw.decode_string' Jsont.json s with
3939+ | Ok json -> json
4040+ | Error e -> failwith (Jsont.Error.to_string e))
4141+ | Error e -> failwith e
4242+4343+let of_json json =
4444+ match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with
4545+ | Ok s ->
4646+ (match Jsont_bytesrw.decode_string jsont s with
4747+ | Ok t -> t
4848+ | Error e -> raise (Invalid_argument ("Control.of_json: " ^ e)))
4949+ | Error e -> raise (Invalid_argument ("Control.of_json: " ^ e))
5050+5151+let pp fmt t =
5252+ Fmt.pf fmt "@[<2>Control@ { request_id = %S;@ subtype = %S;@ data = %a }@]"
5353+ t.request_id t.subtype pp_json t.data
5454+5555+let log_received t =
5656+ Log.debug (fun m -> m "Received control message: %a" pp t)
5757+5858+let log_sending t =
5959+ Log.debug (fun m -> m "Sending control message: %a" pp t)
+47
lib/control.mli
···11+(** Control messages for Claude session management.
22+33+ Control messages are used to manage the interaction flow with Claude,
44+ including session control, cancellation requests, and other operational
55+ commands. *)
66+77+(** The log source for control message operations *)
88+val src : Logs.Src.t
99+1010+type t
1111+(** The type of control messages. *)
1212+1313+val jsont : t Jsont.t
1414+(** [jsont] is the jsont codec for control messages. *)
1515+1616+val create : request_id:string -> subtype:string -> data:Jsont.json -> t
1717+(** [create ~request_id ~subtype ~data] creates a new control message.
1818+ @param request_id Unique identifier for this control request
1919+ @param subtype The specific type of control message
2020+ @param data Additional JSON data for the control message *)
2121+2222+val request_id : t -> string
2323+(** [request_id t] returns the unique request identifier. *)
2424+2525+val subtype : t -> string
2626+(** [subtype t] returns the control message subtype. *)
2727+2828+val data : t -> Jsont.json
2929+(** [data t] returns the additional data associated with the control message. *)
3030+3131+val to_json : t -> Jsont.json
3232+(** [to_json t] converts the control message to its JSON representation. *)
3333+3434+val of_json : Jsont.json -> t
3535+(** [of_json json] parses a control message from JSON.
3636+ @raise Invalid_argument if the JSON is not a valid control message. *)
3737+3838+val pp : Format.formatter -> t -> unit
3939+(** [pp fmt t] pretty-prints the control message. *)
4040+4141+(** {1 Logging} *)
4242+4343+val log_received : t -> unit
4444+(** [log_received t] logs that a control message was received. *)
4545+4646+val log_sending : t -> unit
4747+(** [log_sending t] logs that a control message is being sent. *)
···11+let src = Logs.Src.create "claude.hooks" ~doc:"Claude hooks system"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(** Hook events that can be intercepted *)
55+type event =
66+ | Pre_tool_use
77+ | Post_tool_use
88+ | User_prompt_submit
99+ | Stop
1010+ | Subagent_stop
1111+ | Pre_compact
1212+1313+let event_to_string = function
1414+ | Pre_tool_use -> "PreToolUse"
1515+ | Post_tool_use -> "PostToolUse"
1616+ | User_prompt_submit -> "UserPromptSubmit"
1717+ | Stop -> "Stop"
1818+ | Subagent_stop -> "SubagentStop"
1919+ | Pre_compact -> "PreCompact"
2020+2121+let event_of_string = function
2222+ | "PreToolUse" -> Pre_tool_use
2323+ | "PostToolUse" -> Post_tool_use
2424+ | "UserPromptSubmit" -> User_prompt_submit
2525+ | "Stop" -> Stop
2626+ | "SubagentStop" -> Subagent_stop
2727+ | "PreCompact" -> Pre_compact
2828+ | s -> raise (Invalid_argument (Printf.sprintf "Unknown hook event: %s" s))
2929+3030+let event_jsont : event Jsont.t =
3131+ Jsont.enum [
3232+ "PreToolUse", Pre_tool_use;
3333+ "PostToolUse", Post_tool_use;
3434+ "UserPromptSubmit", User_prompt_submit;
3535+ "Stop", Stop;
3636+ "SubagentStop", Subagent_stop;
3737+ "PreCompact", Pre_compact;
3838+ ]
3939+4040+(** Context provided to hook callbacks *)
4141+module Context = struct
4242+ type t = {
4343+ signal: unit option; (* Future: abort signal support *)
4444+ unknown : Unknown.t;
4545+ }
4646+4747+ let create ?(signal = None) ?(unknown = Unknown.empty) () = { signal; unknown }
4848+4949+ let signal t = t.signal
5050+ let unknown t = t.unknown
5151+5252+ let jsont : t Jsont.t =
5353+ let make unknown = { signal = None; unknown } in
5454+ Jsont.Object.map ~kind:"Context" make
5555+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
5656+ |> Jsont.Object.finish
5757+end
5858+5959+(** Hook decision control *)
6060+type decision =
6161+ | Continue
6262+ | Block
6363+6464+let decision_jsont : decision Jsont.t =
6565+ Jsont.enum [
6666+ "continue", Continue;
6767+ "block", Block;
6868+ ]
6969+7070+(** Generic hook result *)
7171+type result = {
7272+ decision: decision option;
7373+ system_message: string option;
7474+ hook_specific_output: Jsont.json option;
7575+ unknown : Unknown.t;
7676+}
7777+7878+let result_jsont : result Jsont.t =
7979+ let make decision system_message hook_specific_output unknown =
8080+ { decision; system_message; hook_specific_output; unknown }
8181+ in
8282+ Jsont.Object.map ~kind:"Result" make
8383+ |> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun r -> r.decision)
8484+ |> Jsont.Object.opt_mem "systemMessage" Jsont.string ~enc:(fun r -> r.system_message)
8585+ |> Jsont.Object.opt_mem "hookSpecificOutput" Jsont.json ~enc:(fun r -> r.hook_specific_output)
8686+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun r -> r.unknown)
8787+ |> Jsont.Object.finish
8888+8989+(** {1 PreToolUse Hook} *)
9090+module PreToolUse = struct
9191+ type input = {
9292+ session_id: string;
9393+ transcript_path: string;
9494+ tool_name: string;
9595+ tool_input: Jsont.json;
9696+ unknown : Unknown.t;
9797+ }
9898+9999+ type t = input
100100+101101+ let session_id t = t.session_id
102102+ let transcript_path t = t.transcript_path
103103+ let tool_name t = t.tool_name
104104+ let tool_input t = t.tool_input
105105+ let unknown t = t.unknown
106106+107107+ let input_jsont : input Jsont.t =
108108+ let make session_id transcript_path tool_name tool_input unknown =
109109+ { session_id; transcript_path; tool_name; tool_input; unknown }
110110+ in
111111+ Jsont.Object.map ~kind:"PreToolUseInput" make
112112+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
113113+ |> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
114114+ |> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
115115+ |> Jsont.Object.mem "tool_input" Jsont.json ~enc:tool_input
116116+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
117117+ |> Jsont.Object.finish
118118+119119+ let of_json json =
120120+ match Jsont.Json.decode input_jsont json with
121121+ | Ok v -> v
122122+ | Error msg -> raise (Invalid_argument ("PreToolUse: " ^ msg))
123123+124124+ type permission_decision = [ `Allow | `Deny | `Ask ]
125125+126126+ let permission_decision_jsont : permission_decision Jsont.t =
127127+ Jsont.enum [
128128+ "allow", `Allow;
129129+ "deny", `Deny;
130130+ "ask", `Ask;
131131+ ]
132132+133133+ type output = {
134134+ permission_decision: permission_decision option;
135135+ permission_decision_reason: string option;
136136+ updated_input: Jsont.json option;
137137+ unknown : Unknown.t;
138138+ }
139139+140140+ let output_jsont : output Jsont.t =
141141+ let make permission_decision permission_decision_reason updated_input unknown =
142142+ { permission_decision; permission_decision_reason; updated_input; unknown }
143143+ in
144144+ Jsont.Object.map ~kind:"PreToolUseOutput" make
145145+ |> Jsont.Object.opt_mem "permissionDecision" permission_decision_jsont ~enc:(fun o -> o.permission_decision)
146146+ |> Jsont.Object.opt_mem "permissionDecisionReason" Jsont.string ~enc:(fun o -> o.permission_decision_reason)
147147+ |> Jsont.Object.opt_mem "updatedInput" Jsont.json ~enc:(fun o -> o.updated_input)
148148+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
149149+ |> Jsont.Object.finish
150150+151151+ let output_to_json output =
152152+ match Jsont.Json.encode output_jsont output with
153153+ | Ok json -> json
154154+ | Error msg -> failwith ("PreToolUse.output_to_json: " ^ msg)
155155+156156+ let allow ?reason ?updated_input ?(unknown = Unknown.empty) () =
157157+ { permission_decision = Some `Allow; permission_decision_reason = reason;
158158+ updated_input; unknown }
159159+160160+ let deny ?reason ?(unknown = Unknown.empty) () =
161161+ { permission_decision = Some `Deny; permission_decision_reason = reason;
162162+ updated_input = None; unknown }
163163+164164+ let ask ?reason ?(unknown = Unknown.empty) () =
165165+ { permission_decision = Some `Ask; permission_decision_reason = reason;
166166+ updated_input = None; unknown }
167167+168168+ let continue ?(unknown = Unknown.empty) () =
169169+ { permission_decision = None; permission_decision_reason = None;
170170+ updated_input = None; unknown }
171171+end
172172+173173+(** {1 PostToolUse Hook} *)
174174+module PostToolUse = struct
175175+ type input = {
176176+ session_id: string;
177177+ transcript_path: string;
178178+ tool_name: string;
179179+ tool_input: Jsont.json;
180180+ tool_response: Jsont.json;
181181+ unknown : Unknown.t;
182182+ }
183183+184184+ type t = input
185185+186186+ let session_id t = t.session_id
187187+ let transcript_path t = t.transcript_path
188188+ let tool_name t = t.tool_name
189189+ let tool_input t = t.tool_input
190190+ let tool_response t = t.tool_response
191191+ let unknown t = t.unknown
192192+193193+ let input_jsont : input Jsont.t =
194194+ let make session_id transcript_path tool_name tool_input tool_response unknown =
195195+ { session_id; transcript_path; tool_name; tool_input; tool_response; unknown }
196196+ in
197197+ Jsont.Object.map ~kind:"PostToolUseInput" make
198198+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
199199+ |> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
200200+ |> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
201201+ |> Jsont.Object.mem "tool_input" Jsont.json ~enc:tool_input
202202+ |> Jsont.Object.mem "tool_response" Jsont.json ~enc:tool_response
203203+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
204204+ |> Jsont.Object.finish
205205+206206+ let of_json json =
207207+ match Jsont.Json.decode input_jsont json with
208208+ | Ok v -> v
209209+ | Error msg -> raise (Invalid_argument ("PostToolUse: " ^ msg))
210210+211211+ type output = {
212212+ decision: decision option;
213213+ reason: string option;
214214+ additional_context: string option;
215215+ unknown : Unknown.t;
216216+ }
217217+218218+ let output_jsont : output Jsont.t =
219219+ let make decision reason additional_context unknown =
220220+ { decision; reason; additional_context; unknown }
221221+ in
222222+ Jsont.Object.map ~kind:"PostToolUseOutput" make
223223+ |> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
224224+ |> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
225225+ |> Jsont.Object.opt_mem "additionalContext" Jsont.string ~enc:(fun o -> o.additional_context)
226226+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
227227+ |> Jsont.Object.finish
228228+229229+ let output_to_json output =
230230+ match Jsont.Json.encode output_jsont output with
231231+ | Ok json -> json
232232+ | Error msg -> failwith ("PostToolUse.output_to_json: " ^ msg)
233233+234234+ let continue ?additional_context ?(unknown = Unknown.empty) () =
235235+ { decision = None; reason = None; additional_context; unknown }
236236+237237+ let block ?reason ?additional_context ?(unknown = Unknown.empty) () =
238238+ { decision = Some Block; reason; additional_context; unknown }
239239+end
240240+241241+(** {1 UserPromptSubmit Hook} *)
242242+module UserPromptSubmit = struct
243243+ type input = {
244244+ session_id: string;
245245+ transcript_path: string;
246246+ prompt: string;
247247+ unknown : Unknown.t;
248248+ }
249249+250250+ type t = input
251251+252252+ let session_id t = t.session_id
253253+ let transcript_path t = t.transcript_path
254254+ let prompt t = t.prompt
255255+ let unknown t = t.unknown
256256+257257+ let input_jsont : input Jsont.t =
258258+ let make session_id transcript_path prompt unknown =
259259+ { session_id; transcript_path; prompt; unknown }
260260+ in
261261+ Jsont.Object.map ~kind:"UserPromptSubmitInput" make
262262+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
263263+ |> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
264264+ |> Jsont.Object.mem "prompt" Jsont.string ~enc:prompt
265265+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
266266+ |> Jsont.Object.finish
267267+268268+ let of_json json =
269269+ match Jsont.Json.decode input_jsont json with
270270+ | Ok v -> v
271271+ | Error msg -> raise (Invalid_argument ("UserPromptSubmit: " ^ msg))
272272+273273+ type output = {
274274+ decision: decision option;
275275+ reason: string option;
276276+ additional_context: string option;
277277+ unknown : Unknown.t;
278278+ }
279279+280280+ let output_jsont : output Jsont.t =
281281+ let make decision reason additional_context unknown =
282282+ { decision; reason; additional_context; unknown }
283283+ in
284284+ Jsont.Object.map ~kind:"UserPromptSubmitOutput" make
285285+ |> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
286286+ |> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
287287+ |> Jsont.Object.opt_mem "additionalContext" Jsont.string ~enc:(fun o -> o.additional_context)
288288+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
289289+ |> Jsont.Object.finish
290290+291291+ let output_to_json output =
292292+ match Jsont.Json.encode output_jsont output with
293293+ | Ok json -> json
294294+ | Error msg -> failwith ("UserPromptSubmit.output_to_json: " ^ msg)
295295+296296+ let continue ?additional_context ?(unknown = Unknown.empty) () =
297297+ { decision = None; reason = None; additional_context; unknown }
298298+299299+ let block ?reason ?(unknown = Unknown.empty) () =
300300+ { decision = Some Block; reason; additional_context = None; unknown }
301301+end
302302+303303+(** {1 Stop Hook} *)
304304+module Stop = struct
305305+ type input = {
306306+ session_id: string;
307307+ transcript_path: string;
308308+ stop_hook_active: bool;
309309+ unknown : Unknown.t;
310310+ }
311311+312312+ type t = input
313313+314314+ let session_id t = t.session_id
315315+ let transcript_path t = t.transcript_path
316316+ let stop_hook_active t = t.stop_hook_active
317317+ let unknown t = t.unknown
318318+319319+ let input_jsont : input Jsont.t =
320320+ let make session_id transcript_path stop_hook_active unknown =
321321+ { session_id; transcript_path; stop_hook_active; unknown }
322322+ in
323323+ Jsont.Object.map ~kind:"StopInput" make
324324+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
325325+ |> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
326326+ |> Jsont.Object.mem "stop_hook_active" Jsont.bool ~enc:stop_hook_active
327327+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
328328+ |> Jsont.Object.finish
329329+330330+ let of_json json =
331331+ match Jsont.Json.decode input_jsont json with
332332+ | Ok v -> v
333333+ | Error msg -> raise (Invalid_argument ("Stop: " ^ msg))
334334+335335+ type output = {
336336+ decision: decision option;
337337+ reason: string option;
338338+ unknown : Unknown.t;
339339+ }
340340+341341+ let output_jsont : output Jsont.t =
342342+ let make decision reason unknown =
343343+ { decision; reason; unknown }
344344+ in
345345+ Jsont.Object.map ~kind:"StopOutput" make
346346+ |> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
347347+ |> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
348348+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
349349+ |> Jsont.Object.finish
350350+351351+ let output_to_json output =
352352+ match Jsont.Json.encode output_jsont output with
353353+ | Ok json -> json
354354+ | Error msg -> failwith ("Stop.output_to_json: " ^ msg)
355355+356356+ let continue ?(unknown = Unknown.empty) () = { decision = None; reason = None; unknown }
357357+ let block ?reason ?(unknown = Unknown.empty) () = { decision = Some Block; reason; unknown }
358358+end
359359+360360+(** {1 SubagentStop Hook} - Same structure as Stop *)
361361+module SubagentStop = struct
362362+ include Stop
363363+end
364364+365365+(** {1 PreCompact Hook} *)
366366+module PreCompact = struct
367367+ type input = {
368368+ session_id: string;
369369+ transcript_path: string;
370370+ unknown : Unknown.t;
371371+ }
372372+373373+ type t = input
374374+375375+ let session_id t = t.session_id
376376+ let transcript_path t = t.transcript_path
377377+ let unknown t = t.unknown
378378+379379+ let input_jsont : input Jsont.t =
380380+ let make session_id transcript_path unknown =
381381+ { session_id; transcript_path; unknown }
382382+ in
383383+ Jsont.Object.map ~kind:"PreCompactInput" make
384384+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
385385+ |> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
386386+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
387387+ |> Jsont.Object.finish
388388+389389+ let of_json json =
390390+ match Jsont.Json.decode input_jsont json with
391391+ | Ok v -> v
392392+ | Error msg -> raise (Invalid_argument ("PreCompact: " ^ msg))
393393+394394+ type output = unit (* No specific output for PreCompact *)
395395+396396+ let output_to_json () = Jsont.Object ([], Jsont.Meta.none)
397397+398398+ let continue () = ()
399399+end
400400+401401+(** {1 Generic Callback Type} *)
402402+type callback =
403403+ input:Jsont.json ->
404404+ tool_use_id:string option ->
405405+ context:Context.t ->
406406+ result
407407+408408+(** {1 Matcher Configuration} *)
409409+type matcher = {
410410+ matcher: string option;
411411+ callbacks: callback list;
412412+}
413413+414414+type config = (event * matcher list) list
415415+416416+(** {1 Result Builders} *)
417417+let continue ?system_message ?hook_specific_output ?(unknown = Unknown.empty) () =
418418+ { decision = None; system_message; hook_specific_output; unknown }
419419+420420+let block ?system_message ?hook_specific_output ?(unknown = Unknown.empty) () =
421421+ { decision = Some Block; system_message; hook_specific_output; unknown }
422422+423423+(** {1 Matcher Builders} *)
424424+let matcher ?pattern callbacks = { matcher = pattern; callbacks }
425425+426426+(** {1 Config Builders} *)
427427+let empty = []
428428+429429+let add event matchers config =
430430+ (event, matchers) :: config
431431+432432+(** {1 JSON Conversion} *)
433433+let result_to_json result =
434434+ match Jsont.Json.encode result_jsont result with
435435+ | Ok json -> json
436436+ | Error msg -> failwith ("result_to_json: " ^ msg)
437437+438438+let config_to_protocol_format config =
439439+ let hooks_dict = List.map (fun (event, matchers) ->
440440+ let event_name = event_to_string event in
441441+ let matchers_json = List.map (fun m ->
442442+ (* matcher and hookCallbackIds will be filled in by client *)
443443+ let mems = [
444444+ Jsont.Json.mem (Jsont.Json.name "matcher") (match m.matcher with
445445+ | Some p -> Jsont.Json.string p
446446+ | None -> Jsont.Json.null ());
447447+ Jsont.Json.mem (Jsont.Json.name "callbacks") (Jsont.Json.list []); (* Placeholder, filled by client *)
448448+ ] in
449449+ Jsont.Json.object' mems
450450+ ) matchers in
451451+ Jsont.Json.mem (Jsont.Json.name event_name) (Jsont.Json.list matchers_json)
452452+ ) config in
453453+ Jsont.Json.object' hooks_dict
+344
lib/hooks.mli
···11+(** Claude Code Hooks System
22+33+ Hooks allow you to intercept and control events in Claude Code sessions,
44+ such as tool usage, prompt submission, and session stops.
55+66+ {1 Overview}
77+88+ Hooks are organized by event type, with each event having:
99+ - A typed input structure (accessible via submodules)
1010+ - A typed output structure for responses
1111+ - Helper functions for common responses
1212+1313+ {1 Example Usage}
1414+1515+ {[
1616+ open Eio.Std
1717+1818+ (* Block dangerous bash commands *)
1919+ let get_string json key =
2020+ match json with
2121+ | Jsont.Object (members, _) ->
2222+ List.find_map (fun ((name, _), value) ->
2323+ if name = key then
2424+ match value with
2525+ | Jsont.String (s, _) -> Some s
2626+ | _ -> None
2727+ else None
2828+ ) members
2929+ | _ -> None
3030+ in
3131+ let block_rm_rf ~input ~tool_use_id:_ ~context:_ =
3232+ let hook = Hooks.PreToolUse.of_json input in
3333+ if Hooks.PreToolUse.tool_name hook = "Bash" then
3434+ let tool_input = Hooks.PreToolUse.tool_input hook in
3535+ match get_string tool_input "command" with
3636+ | Some cmd when String.contains cmd "rm -rf" ->
3737+ let output = Hooks.PreToolUse.deny ~reason:"Dangerous command" () in
3838+ Hooks.continue
3939+ ~hook_specific_output:(Hooks.PreToolUse.output_to_json output)
4040+ ()
4141+ | _ -> Hooks.continue ()
4242+ else Hooks.continue ()
4343+4444+ let hooks =
4545+ Hooks.empty
4646+ |> Hooks.add Hooks.Pre_tool_use [
4747+ Hooks.matcher ~pattern:"Bash" [block_rm_rf]
4848+ ]
4949+5050+ let options = Claude.Options.create ~hooks:(Some hooks) () in
5151+ let client = Claude.Client.create ~options ~sw ~process_mgr () in
5252+ ]}
5353+*)
5454+5555+(** The log source for hooks *)
5656+val src : Logs.Src.t
5757+5858+(** {1 Hook Events} *)
5959+6060+(** Hook event types *)
6161+type event =
6262+ | Pre_tool_use (** Fires before a tool is executed *)
6363+ | Post_tool_use (** Fires after a tool completes *)
6464+ | User_prompt_submit (** Fires when user submits a prompt *)
6565+ | Stop (** Fires when conversation stops *)
6666+ | Subagent_stop (** Fires when a subagent stops *)
6767+ | Pre_compact (** Fires before message compaction *)
6868+6969+val event_to_string : event -> string
7070+val event_of_string : string -> event
7171+val event_jsont : event Jsont.t
7272+7373+(** {1 Context} *)
7474+7575+module Context : sig
7676+ type t = {
7777+ signal: unit option;
7878+ unknown : Unknown.t;
7979+ }
8080+8181+ val create : ?signal:unit option -> ?unknown:Unknown.t -> unit -> t
8282+ val signal : t -> unit option
8383+ val unknown : t -> Unknown.t
8484+ val jsont : t Jsont.t
8585+end
8686+8787+(** {1 Decisions} *)
8888+8989+type decision =
9090+ | Continue (** Allow the action to proceed *)
9191+ | Block (** Block the action *)
9292+9393+val decision_jsont : decision Jsont.t
9494+9595+(** {1 Generic Hook Result} *)
9696+9797+(** Generic result structure for hooks *)
9898+type result = {
9999+ decision: decision option;
100100+ system_message: string option;
101101+ hook_specific_output: Jsont.json option;
102102+ unknown: Unknown.t;
103103+}
104104+105105+val result_jsont : result Jsont.t
106106+107107+(** {1 Typed Hook Modules} *)
108108+109109+(** PreToolUse hook - fires before tool execution *)
110110+module PreToolUse : sig
111111+ (** Typed input for PreToolUse hooks *)
112112+ type input = {
113113+ session_id: string;
114114+ transcript_path: string;
115115+ tool_name: string;
116116+ tool_input: Jsont.json;
117117+ unknown: Unknown.t;
118118+ }
119119+120120+ type t = input
121121+122122+ (** Parse hook input from JSON *)
123123+ val of_json : Jsont.json -> t
124124+125125+ (** {2 Accessors} *)
126126+ val session_id : t -> string
127127+ val transcript_path : t -> string
128128+ val tool_name : t -> string
129129+ val tool_input : t -> Jsont.json
130130+ val unknown : t -> Unknown.t
131131+132132+ val input_jsont : input Jsont.t
133133+134134+ (** Permission decision for tool usage *)
135135+ type permission_decision = [ `Allow | `Deny | `Ask ]
136136+137137+ val permission_decision_jsont : permission_decision Jsont.t
138138+139139+ (** Typed output for PreToolUse hooks *)
140140+ type output = {
141141+ permission_decision: permission_decision option;
142142+ permission_decision_reason: string option;
143143+ updated_input: Jsont.json option;
144144+ unknown: Unknown.t;
145145+ }
146146+147147+ val output_jsont : output Jsont.t
148148+149149+ (** {2 Response Builders} *)
150150+ val allow : ?reason:string -> ?updated_input:Jsont.json -> ?unknown:Unknown.t -> unit -> output
151151+ val deny : ?reason:string -> ?unknown:Unknown.t -> unit -> output
152152+ val ask : ?reason:string -> ?unknown:Unknown.t -> unit -> output
153153+ val continue : ?unknown:Unknown.t -> unit -> output
154154+155155+ (** Convert output to JSON for hook_specific_output *)
156156+ val output_to_json : output -> Jsont.json
157157+end
158158+159159+(** PostToolUse hook - fires after tool execution *)
160160+module PostToolUse : sig
161161+ type input = {
162162+ session_id: string;
163163+ transcript_path: string;
164164+ tool_name: string;
165165+ tool_input: Jsont.json;
166166+ tool_response: Jsont.json;
167167+ unknown: Unknown.t;
168168+ }
169169+170170+ type t = input
171171+172172+ val of_json : Jsont.json -> t
173173+174174+ val session_id : t -> string
175175+ val transcript_path : t -> string
176176+ val tool_name : t -> string
177177+ val tool_input : t -> Jsont.json
178178+ val tool_response : t -> Jsont.json
179179+ val unknown : t -> Unknown.t
180180+181181+ val input_jsont : input Jsont.t
182182+183183+ type output = {
184184+ decision: decision option;
185185+ reason: string option;
186186+ additional_context: string option;
187187+ unknown: Unknown.t;
188188+ }
189189+190190+ val output_jsont : output Jsont.t
191191+192192+ val continue : ?additional_context:string -> ?unknown:Unknown.t -> unit -> output
193193+ val block : ?reason:string -> ?additional_context:string -> ?unknown:Unknown.t -> unit -> output
194194+ val output_to_json : output -> Jsont.json
195195+end
196196+197197+(** UserPromptSubmit hook - fires when user submits a prompt *)
198198+module UserPromptSubmit : sig
199199+ type input = {
200200+ session_id: string;
201201+ transcript_path: string;
202202+ prompt: string;
203203+ unknown: Unknown.t;
204204+ }
205205+206206+ type t = input
207207+208208+ val of_json : Jsont.json -> t
209209+210210+ val session_id : t -> string
211211+ val transcript_path : t -> string
212212+ val prompt : t -> string
213213+ val unknown : t -> Unknown.t
214214+215215+ val input_jsont : input Jsont.t
216216+217217+ type output = {
218218+ decision: decision option;
219219+ reason: string option;
220220+ additional_context: string option;
221221+ unknown: Unknown.t;
222222+ }
223223+224224+ val output_jsont : output Jsont.t
225225+226226+ val continue : ?additional_context:string -> ?unknown:Unknown.t -> unit -> output
227227+ val block : ?reason:string -> ?unknown:Unknown.t -> unit -> output
228228+ val output_to_json : output -> Jsont.json
229229+end
230230+231231+(** Stop hook - fires when conversation stops *)
232232+module Stop : sig
233233+ type input = {
234234+ session_id: string;
235235+ transcript_path: string;
236236+ stop_hook_active: bool;
237237+ unknown: Unknown.t;
238238+ }
239239+240240+ type t = input
241241+242242+ val of_json : Jsont.json -> t
243243+244244+ val session_id : t -> string
245245+ val transcript_path : t -> string
246246+ val stop_hook_active : t -> bool
247247+ val unknown : t -> Unknown.t
248248+249249+ val input_jsont : input Jsont.t
250250+251251+ type output = {
252252+ decision: decision option;
253253+ reason: string option;
254254+ unknown: Unknown.t;
255255+ }
256256+257257+ val output_jsont : output Jsont.t
258258+259259+ val continue : ?unknown:Unknown.t -> unit -> output
260260+ val block : ?reason:string -> ?unknown:Unknown.t -> unit -> output
261261+ val output_to_json : output -> Jsont.json
262262+end
263263+264264+(** SubagentStop hook - fires when a subagent stops *)
265265+module SubagentStop : sig
266266+ include module type of Stop
267267+ val of_json : Jsont.json -> t
268268+end
269269+270270+(** PreCompact hook - fires before message compaction *)
271271+module PreCompact : sig
272272+ type input = {
273273+ session_id: string;
274274+ transcript_path: string;
275275+ unknown: Unknown.t;
276276+ }
277277+278278+ type t = input
279279+280280+ type output = unit
281281+282282+ val of_json : Jsont.json -> t
283283+284284+ val session_id : t -> string
285285+ val transcript_path : t -> string
286286+ val unknown : t -> Unknown.t
287287+288288+ val input_jsont : input Jsont.t
289289+290290+ val continue : unit -> output
291291+ val output_to_json : output -> Jsont.json
292292+end
293293+294294+(** {1 Callbacks} *)
295295+296296+(** Generic callback function type.
297297+298298+ Callbacks receive:
299299+ - [input]: Raw JSON input (parse with [PreToolUse.of_json], etc.)
300300+ - [tool_use_id]: Optional tool use ID
301301+ - [context]: Hook context
302302+303303+ And return a generic [result] with optional hook-specific output.
304304+*)
305305+type callback =
306306+ input:Jsont.json ->
307307+ tool_use_id:string option ->
308308+ context:Context.t ->
309309+ result
310310+311311+(** {1 Matchers} *)
312312+313313+(** A matcher configuration *)
314314+type matcher = {
315315+ matcher: string option; (** Pattern to match (e.g., "Bash" or "Write|Edit") *)
316316+ callbacks: callback list; (** Callbacks to invoke on match *)
317317+}
318318+319319+(** Hook configuration: map from events to matchers *)
320320+type config = (event * matcher list) list
321321+322322+(** {1 Generic Result Builders} *)
323323+324324+(** [continue ?system_message ?hook_specific_output ?unknown ()] creates a continue result *)
325325+val continue : ?system_message:string -> ?hook_specific_output:Jsont.json -> ?unknown:Unknown.t -> unit -> result
326326+327327+(** [block ?system_message ?hook_specific_output ?unknown ()] creates a block result *)
328328+val block : ?system_message:string -> ?hook_specific_output:Jsont.json -> ?unknown:Unknown.t -> unit -> result
329329+330330+(** {1 Configuration Builders} *)
331331+332332+(** [matcher ?pattern callbacks] creates a matcher *)
333333+val matcher : ?pattern:string -> callback list -> matcher
334334+335335+(** Empty hooks configuration *)
336336+val empty : config
337337+338338+(** [add event matchers config] adds matchers for an event *)
339339+val add : event -> matcher list -> config -> config
340340+341341+(** {1 JSON Serialization} *)
342342+343343+val result_to_json : result -> Jsont.json
344344+val config_to_protocol_format : config -> Jsont.json
+188
lib/incoming.ml
···11+let src = Logs.Src.create "claude.incoming" ~doc:"Incoming messages from Claude CLI"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(** Control request types for incoming control_request messages *)
55+module Control_request = struct
66+ (** Can use tool permission request *)
77+ module Can_use_tool = struct
88+ type t = {
99+ tool_name : string;
1010+ input : Jsont.json;
1111+ permission_suggestions : Jsont.json list;
1212+ }
1313+1414+ let tool_name t = t.tool_name
1515+ let input t = t.input
1616+ let permission_suggestions t = t.permission_suggestions
1717+1818+ let jsont : t Jsont.t =
1919+ let make tool_name input permission_suggestions =
2020+ { tool_name; input; permission_suggestions = Option.value permission_suggestions ~default:[] }
2121+ in
2222+ Jsont.Object.map ~kind:"CanUseTool" make
2323+ |> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
2424+ |> Jsont.Object.mem "input" Jsont.json ~enc:input
2525+ |> Jsont.Object.opt_mem "permission_suggestions" (Jsont.list Jsont.json)
2626+ ~enc:(fun t -> if t.permission_suggestions = [] then None else Some t.permission_suggestions)
2727+ |> Jsont.Object.finish
2828+ end
2929+3030+ (** Hook callback request *)
3131+ module Hook_callback = struct
3232+ type t = {
3333+ callback_id : string;
3434+ input : Jsont.json;
3535+ tool_use_id : string option;
3636+ }
3737+3838+ let callback_id t = t.callback_id
3939+ let input t = t.input
4040+ let tool_use_id t = t.tool_use_id
4141+4242+ let jsont : t Jsont.t =
4343+ let make callback_id input tool_use_id = { callback_id; input; tool_use_id } in
4444+ Jsont.Object.map ~kind:"HookCallback" make
4545+ |> Jsont.Object.mem "callback_id" Jsont.string ~enc:callback_id
4646+ |> Jsont.Object.mem "input" Jsont.json ~enc:input
4747+ |> Jsont.Object.opt_mem "tool_use_id" Jsont.string ~enc:tool_use_id
4848+ |> Jsont.Object.finish
4949+ end
5050+5151+ (** Request payload - discriminated by subtype *)
5252+ type request =
5353+ | Can_use_tool of Can_use_tool.t
5454+ | Hook_callback of Hook_callback.t
5555+ | Unknown of string * Jsont.json
5656+5757+ let request_of_json json =
5858+ let subtype_codec = Jsont.Object.map ~kind:"Subtype" Fun.id
5959+ |> Jsont.Object.mem "subtype" Jsont.string ~enc:Fun.id
6060+ |> Jsont.Object.finish
6161+ in
6262+ match Jsont.Json.decode subtype_codec json with
6363+ | Error _ -> Unknown ("unknown", json)
6464+ | Ok subtype ->
6565+ match subtype with
6666+ | "can_use_tool" ->
6767+ (match Jsont.Json.decode Can_use_tool.jsont json with
6868+ | Ok r -> Can_use_tool r
6969+ | Error _ -> Unknown (subtype, json))
7070+ | "hook_callback" ->
7171+ (match Jsont.Json.decode Hook_callback.jsont json with
7272+ | Ok r -> Hook_callback r
7373+ | Error _ -> Unknown (subtype, json))
7474+ | _ -> Unknown (subtype, json)
7575+7676+ (** Full control request message *)
7777+ type t = {
7878+ request_id : string;
7979+ request : request;
8080+ }
8181+8282+ let request_id t = t.request_id
8383+ let request t = t.request
8484+8585+ let subtype t =
8686+ match t.request with
8787+ | Can_use_tool _ -> "can_use_tool"
8888+ | Hook_callback _ -> "hook_callback"
8989+ | Unknown (s, _) -> s
9090+9191+ let jsont : t Jsont.t =
9292+ let dec json =
9393+ let envelope_codec =
9494+ Jsont.Object.map ~kind:"ControlRequestEnvelope" (fun request_id request_json -> (request_id, request_json))
9595+ |> Jsont.Object.mem "request_id" Jsont.string ~enc:fst
9696+ |> Jsont.Object.mem "request" Jsont.json ~enc:snd
9797+ |> Jsont.Object.finish
9898+ in
9999+ match Jsont.Json.decode envelope_codec json with
100100+ | Error err -> failwith ("Failed to decode control_request envelope: " ^ err)
101101+ | Ok (request_id, request_json) ->
102102+ { request_id; request = request_of_json request_json }
103103+ in
104104+ let enc t =
105105+ let request_json = match t.request with
106106+ | Can_use_tool r ->
107107+ (match Jsont.Json.encode Can_use_tool.jsont r with
108108+ | Ok j -> j
109109+ | Error err -> failwith ("Failed to encode Can_use_tool: " ^ err))
110110+ | Hook_callback r ->
111111+ (match Jsont.Json.encode Hook_callback.jsont r with
112112+ | Ok j -> j
113113+ | Error err -> failwith ("Failed to encode Hook_callback: " ^ err))
114114+ | Unknown (_, j) -> j
115115+ in
116116+ Jsont.Json.object' [
117117+ Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_request");
118118+ Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string t.request_id);
119119+ Jsont.Json.mem (Jsont.Json.name "request") request_json;
120120+ ]
121121+ in
122122+ Jsont.map ~kind:"ControlRequest" ~dec ~enc Jsont.json
123123+end
124124+125125+type t =
126126+ | Message of Message.t
127127+ | Control_response of Sdk_control.control_response
128128+ | Control_request of Control_request.t
129129+130130+let jsont : t Jsont.t =
131131+ (* Custom decoder that checks the type field and dispatches to the appropriate codec.
132132+133133+ The challenge is that Message can have multiple type values ("user", "assistant",
134134+ "system", "result"), while control_response and control_request have single type values.
135135+ Jsont's case_mem discriminator doesn't support multiple tags per case, so we implement
136136+ a custom decoder/encoder. *)
137137+138138+ let type_field_codec = Jsont.Object.map ~kind:"type_field" Fun.id
139139+ |> Jsont.Object.opt_mem "type" Jsont.string ~enc:Fun.id
140140+ |> Jsont.Object.finish
141141+ in
142142+143143+ let dec json =
144144+ match Jsont.Json.decode type_field_codec json with
145145+ | Error _ | Ok None ->
146146+ (* No type field, try as message *)
147147+ (match Jsont.Json.decode Message.jsont json with
148148+ | Ok msg -> Message msg
149149+ | Error err -> failwith ("Failed to decode message: " ^ err))
150150+ | Ok (Some typ) ->
151151+ match typ with
152152+ | "control_response" ->
153153+ (match Jsont.Json.decode Sdk_control.control_response_jsont json with
154154+ | Ok resp -> Control_response resp
155155+ | Error err -> failwith ("Failed to decode control_response: " ^ err))
156156+ | "control_request" ->
157157+ (match Jsont.Json.decode Control_request.jsont json with
158158+ | Ok req -> Control_request req
159159+ | Error err -> failwith ("Failed to decode control_request: " ^ err))
160160+ | "user" | "assistant" | "system" | "result" | _ ->
161161+ (* Message types *)
162162+ (match Jsont.Json.decode Message.jsont json with
163163+ | Ok msg -> Message msg
164164+ | Error err -> failwith ("Failed to decode message: " ^ err))
165165+ in
166166+167167+ let enc = function
168168+ | Message msg ->
169169+ (match Jsont.Json.encode Message.jsont msg with
170170+ | Ok json -> json
171171+ | Error err -> failwith ("Failed to encode message: " ^ err))
172172+ | Control_response resp ->
173173+ (match Jsont.Json.encode Sdk_control.control_response_jsont resp with
174174+ | Ok json -> json
175175+ | Error err -> failwith ("Failed to encode control response: " ^ err))
176176+ | Control_request req ->
177177+ (match Jsont.Json.encode Control_request.jsont req with
178178+ | Ok json -> json
179179+ | Error err -> failwith ("Failed to encode control request: " ^ err))
180180+ in
181181+182182+ Jsont.map ~kind:"Incoming" ~dec ~enc Jsont.json
183183+184184+let pp fmt = function
185185+ | Message msg -> Format.fprintf fmt "@[<2>Message@ %a@]" Message.pp msg
186186+ | Control_response resp -> Format.fprintf fmt "@[<2>ControlResponse@ %a@]" Sdk_control.pp (Sdk_control.Response resp)
187187+ | Control_request req -> Format.fprintf fmt "@[<2>ControlRequest@ { request_id=%S; subtype=%S }@]"
188188+ (Control_request.request_id req) (Control_request.subtype req)
+60
lib/incoming.mli
···11+(** Incoming messages from the Claude CLI.
22+33+ This module defines a discriminated union of all possible message types
44+ that can be received from the Claude CLI, with a single jsont codec.
55+66+ The codec uses the "type" field to discriminate between message types:
77+ - "user", "assistant", "system", "result" -> Message variant
88+ - "control_response" -> Control_response variant
99+ - "control_request" -> Control_request variant
1010+1111+ This provides a clean, type-safe way to decode incoming messages in a single
1212+ operation, avoiding the parse-then-switch-then-parse pattern. *)
1313+1414+(** Control request types for incoming control_request messages *)
1515+module Control_request : sig
1616+ (** Can use tool permission request *)
1717+ module Can_use_tool : sig
1818+ type t
1919+2020+ val tool_name : t -> string
2121+ val input : t -> Jsont.json
2222+ val permission_suggestions : t -> Jsont.json list
2323+ val jsont : t Jsont.t
2424+ end
2525+2626+ (** Hook callback request *)
2727+ module Hook_callback : sig
2828+ type t
2929+3030+ val callback_id : t -> string
3131+ val input : t -> Jsont.json
3232+ val tool_use_id : t -> string option
3333+ val jsont : t Jsont.t
3434+ end
3535+3636+ (** Request payload - discriminated by subtype *)
3737+ type request =
3838+ | Can_use_tool of Can_use_tool.t
3939+ | Hook_callback of Hook_callback.t
4040+ | Unknown of string * Jsont.json
4141+4242+ (** Full control request message *)
4343+ type t
4444+4545+ val request_id : t -> string
4646+ val request : t -> request
4747+ val subtype : t -> string
4848+ val jsont : t Jsont.t
4949+end
5050+5151+type t =
5252+ | Message of Message.t
5353+ | Control_response of Sdk_control.control_response
5454+ | Control_request of Control_request.t
5555+5656+val jsont : t Jsont.t
5757+(** Codec for incoming messages. Uses the "type" field to discriminate. *)
5858+5959+val pp : Format.formatter -> t -> unit
6060+(** [pp fmt t] pretty-prints the incoming message. *)
+673
lib/message.ml
···11+let src = Logs.Src.create "claude.message" ~doc:"Claude messages"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+55+module User = struct
66+ type content =
77+ | String of string
88+ | Blocks of Content_block.t list
99+1010+ type t = {
1111+ content : content;
1212+ unknown : Unknown.t;
1313+ }
1414+1515+ let create_string s = { content = String s; unknown = Unknown.empty }
1616+ let create_blocks blocks = { content = Blocks blocks; unknown = Unknown.empty }
1717+1818+ let create_with_tool_result ~tool_use_id ~content ?is_error () =
1919+ let tool_result = Content_block.tool_result ~tool_use_id ~content ?is_error () in
2020+ { content = Blocks [tool_result]; unknown = Unknown.empty }
2121+2222+ let create_mixed ~text ~tool_results =
2323+ let blocks =
2424+ let text_blocks = match text with
2525+ | Some t -> [Content_block.text t]
2626+ | None -> []
2727+ in
2828+ let tool_blocks = List.map (fun (tool_use_id, content, is_error) ->
2929+ Content_block.tool_result ~tool_use_id ~content ?is_error ()
3030+ ) tool_results in
3131+ text_blocks @ tool_blocks
3232+ in
3333+ { content = Blocks blocks; unknown = Unknown.empty }
3434+3535+ let make content unknown = { content; unknown }
3636+ let content t = t.content
3737+ let unknown t = t.unknown
3838+3939+ let as_text t = match t.content with
4040+ | String s -> Some s
4141+ | Blocks _ -> None
4242+4343+ let get_blocks t = match t.content with
4444+ | String s -> [Content_block.text s]
4545+ | Blocks blocks -> blocks
4646+4747+ (* Decode content from json value *)
4848+ let decode_content json = match json with
4949+ | Jsont.String (s, _) -> String s
5050+ | Jsont.Array (items, _) ->
5151+ let blocks = List.map (fun j ->
5252+ match Jsont.Json.decode Content_block.jsont j with
5353+ | Ok b -> b
5454+ | Error msg -> failwith ("Invalid content block: " ^ msg)
5555+ ) items in
5656+ Blocks blocks
5757+ | _ -> failwith "Content must be string or array"
5858+5959+ (* Encode content to json value *)
6060+ let encode_content = function
6161+ | String s -> Jsont.String (s, Jsont.Meta.none)
6262+ | Blocks blocks -> Jsont.Array (List.map Content_block.to_json blocks, Jsont.Meta.none)
6363+6464+ let jsont : t Jsont.t =
6565+ Jsont.Object.map ~kind:"User" (fun json_content unknown ->
6666+ let content = decode_content json_content in
6767+ make content unknown
6868+ )
6969+ |> Jsont.Object.mem "content" Jsont.json ~enc:(fun t -> encode_content (content t))
7070+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
7171+ |> Jsont.Object.finish
7272+7373+ let to_json t =
7474+ let content_json = match t.content with
7575+ | String s -> Jsont.String (s, Jsont.Meta.none)
7676+ | Blocks blocks ->
7777+ Jsont.Array (List.map Content_block.to_json blocks, Jsont.Meta.none)
7878+ in
7979+ Jsont.Object ([
8080+ (Jsont.Json.name "type", Jsont.String ("user", Jsont.Meta.none));
8181+ (Jsont.Json.name "message", Jsont.Object ([
8282+ (Jsont.Json.name "role", Jsont.String ("user", Jsont.Meta.none));
8383+ (Jsont.Json.name "content", content_json);
8484+ ], Jsont.Meta.none));
8585+ ], Jsont.Meta.none)
8686+8787+ (* Jsont codec for parsing incoming user messages from CLI *)
8888+ let incoming_jsont : t Jsont.t =
8989+ let message_jsont =
9090+ Jsont.Object.map ~kind:"UserMessage" (fun json_content ->
9191+ let content = decode_content json_content in
9292+ { content; unknown = Unknown.empty }
9393+ )
9494+ |> Jsont.Object.mem "content" Jsont.json ~enc:(fun t -> encode_content (content t))
9595+ |> Jsont.Object.finish
9696+ in
9797+ Jsont.Object.map ~kind:"UserEnvelope" Fun.id
9898+ |> Jsont.Object.mem "message" message_jsont ~enc:Fun.id
9999+ |> Jsont.Object.finish
100100+101101+ let of_json json =
102102+ match Jsont.Json.decode incoming_jsont json with
103103+ | Ok v -> v
104104+ | Error msg -> raise (Invalid_argument ("User.of_json: " ^ msg))
105105+106106+ let pp fmt t =
107107+ match t.content with
108108+ | String s ->
109109+ if String.length s > 60 then
110110+ let truncated = String.sub s 0 57 in
111111+ Fmt.pf fmt "@[<2>User:@ %s...@]" truncated
112112+ else
113113+ Fmt.pf fmt "@[<2>User:@ %S@]" s
114114+ | Blocks blocks ->
115115+ let text_count = List.length (List.filter (function
116116+ | Content_block.Text _ -> true | _ -> false) blocks) in
117117+ let tool_result_count = List.length (List.filter (function
118118+ | Content_block.Tool_result _ -> true | _ -> false) blocks) in
119119+ match text_count, tool_result_count with
120120+ | 1, 0 ->
121121+ let text = List.find_map (function
122122+ | Content_block.Text t -> Some (Content_block.Text.text t)
123123+ | _ -> None) blocks in
124124+ Fmt.pf fmt "@[<2>User:@ %a@]" Fmt.(option string) text
125125+ | 0, 1 ->
126126+ Fmt.pf fmt "@[<2>User:@ [tool result]@]"
127127+ | 0, n when n > 1 ->
128128+ Fmt.pf fmt "@[<2>User:@ [%d tool results]@]" n
129129+ | _ ->
130130+ Fmt.pf fmt "@[<2>User:@ [%d blocks]@]" (List.length blocks)
131131+end
132132+133133+module Assistant = struct
134134+ type error = [
135135+ | `Authentication_failed
136136+ | `Billing_error
137137+ | `Rate_limit
138138+ | `Invalid_request
139139+ | `Server_error
140140+ | `Unknown
141141+ ]
142142+143143+ let error_to_string = function
144144+ | `Authentication_failed -> "authentication_failed"
145145+ | `Billing_error -> "billing_error"
146146+ | `Rate_limit -> "rate_limit"
147147+ | `Invalid_request -> "invalid_request"
148148+ | `Server_error -> "server_error"
149149+ | `Unknown -> "unknown"
150150+151151+ let error_of_string = function
152152+ | "authentication_failed" -> `Authentication_failed
153153+ | "billing_error" -> `Billing_error
154154+ | "rate_limit" -> `Rate_limit
155155+ | "invalid_request" -> `Invalid_request
156156+ | "server_error" -> `Server_error
157157+ | "unknown" | _ -> `Unknown
158158+159159+ let error_jsont : error Jsont.t =
160160+ Jsont.enum [
161161+ ("authentication_failed", `Authentication_failed);
162162+ ("billing_error", `Billing_error);
163163+ ("rate_limit", `Rate_limit);
164164+ ("invalid_request", `Invalid_request);
165165+ ("server_error", `Server_error);
166166+ ("unknown", `Unknown);
167167+ ]
168168+169169+ type t = {
170170+ content : Content_block.t list;
171171+ model : string;
172172+ error : error option;
173173+ unknown : Unknown.t;
174174+ }
175175+176176+ let create ~content ~model ?error () = { content; model; error; unknown = Unknown.empty }
177177+ let make content model error unknown = { content; model; error; unknown }
178178+ let content t = t.content
179179+ let model t = t.model
180180+ let error t = t.error
181181+ let unknown t = t.unknown
182182+183183+ let get_text_blocks t =
184184+ List.filter_map (function
185185+ | Content_block.Text text -> Some (Content_block.Text.text text)
186186+ | _ -> None
187187+ ) t.content
188188+189189+ let get_tool_uses t =
190190+ List.filter_map (function
191191+ | Content_block.Tool_use tool -> Some tool
192192+ | _ -> None
193193+ ) t.content
194194+195195+ let get_thinking t =
196196+ List.filter_map (function
197197+ | Content_block.Thinking thinking -> Some thinking
198198+ | _ -> None
199199+ ) t.content
200200+201201+ let has_tool_use t =
202202+ List.exists (function
203203+ | Content_block.Tool_use _ -> true
204204+ | _ -> false
205205+ ) t.content
206206+207207+ let combined_text t =
208208+ String.concat "\n" (get_text_blocks t)
209209+210210+ let jsont : t Jsont.t =
211211+ Jsont.Object.map ~kind:"Assistant" make
212212+ |> Jsont.Object.mem "content" (Jsont.list Content_block.jsont) ~enc:content
213213+ |> Jsont.Object.mem "model" Jsont.string ~enc:model
214214+ |> Jsont.Object.opt_mem "error" error_jsont ~enc:error
215215+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
216216+ |> Jsont.Object.finish
217217+218218+ let to_json t =
219219+ let msg_fields = [
220220+ (Jsont.Json.name "content", Jsont.Array (List.map Content_block.to_json t.content, Jsont.Meta.none));
221221+ (Jsont.Json.name "model", Jsont.String (t.model, Jsont.Meta.none));
222222+ ] in
223223+ let msg_fields = match t.error with
224224+ | Some err -> (Jsont.Json.name "error", Jsont.String (error_to_string err, Jsont.Meta.none)) :: msg_fields
225225+ | None -> msg_fields
226226+ in
227227+ Jsont.Object ([
228228+ (Jsont.Json.name "type", Jsont.String ("assistant", Jsont.Meta.none));
229229+ (Jsont.Json.name "message", Jsont.Object (msg_fields, Jsont.Meta.none));
230230+ ], Jsont.Meta.none)
231231+232232+ (* Jsont codec for parsing incoming assistant messages from CLI *)
233233+ let incoming_jsont : t Jsont.t =
234234+ Jsont.Object.map ~kind:"AssistantEnvelope" Fun.id
235235+ |> Jsont.Object.mem "message" jsont ~enc:Fun.id
236236+ |> Jsont.Object.finish
237237+238238+ let of_json json =
239239+ match Jsont.Json.decode incoming_jsont json with
240240+ | Ok v -> v
241241+ | Error msg -> raise (Invalid_argument ("Assistant.of_json: " ^ msg))
242242+243243+ let pp fmt t =
244244+ let text_count = List.length (get_text_blocks t) in
245245+ let tool_count = List.length (get_tool_uses t) in
246246+ let thinking_count = List.length (get_thinking t) in
247247+ match text_count, tool_count, thinking_count with
248248+ | 1, 0, 0 ->
249249+ (* Simple text response *)
250250+ Fmt.pf fmt "@[<2>Assistant@ [%s]:@ %S@]"
251251+ t.model (combined_text t)
252252+ | _, 0, 0 when text_count > 0 ->
253253+ (* Multiple text blocks *)
254254+ Fmt.pf fmt "@[<2>Assistant@ [%s]:@ %d text blocks@]"
255255+ t.model text_count
256256+ | 0, _, 0 when tool_count > 0 ->
257257+ (* Only tool uses *)
258258+ let tools = get_tool_uses t in
259259+ let tool_names = List.map Content_block.Tool_use.name tools in
260260+ Fmt.pf fmt "@[<2>Assistant@ [%s]:@ tools(%a)@]"
261261+ t.model Fmt.(list ~sep:comma string) tool_names
262262+ | _ ->
263263+ (* Mixed content *)
264264+ let parts = [] in
265265+ let parts = if text_count > 0 then
266266+ Printf.sprintf "%d text" text_count :: parts else parts in
267267+ let parts = if tool_count > 0 then
268268+ Printf.sprintf "%d tools" tool_count :: parts else parts in
269269+ let parts = if thinking_count > 0 then
270270+ Printf.sprintf "%d thinking" thinking_count :: parts else parts in
271271+ Fmt.pf fmt "@[<2>Assistant@ [%s]:@ %s@]"
272272+ t.model (String.concat ", " (List.rev parts))
273273+end
274274+275275+module System = struct
276276+ (** System messages as a discriminated union on "subtype" field *)
277277+278278+ type init = {
279279+ session_id : string option;
280280+ model : string option;
281281+ cwd : string option;
282282+ unknown : Unknown.t;
283283+ }
284284+285285+ type error = {
286286+ error : string;
287287+ unknown : Unknown.t;
288288+ }
289289+290290+ type other = {
291291+ subtype : string;
292292+ unknown : Unknown.t;
293293+ }
294294+295295+ type t =
296296+ | Init of init
297297+ | Error of error
298298+ | Other of other
299299+300300+ (* Accessors *)
301301+ let session_id = function Init i -> i.session_id | _ -> None
302302+ let model = function Init i -> i.model | _ -> None
303303+ let cwd = function Init i -> i.cwd | _ -> None
304304+ let error_msg = function Error e -> Some e.error | _ -> None
305305+ let subtype = function Init _ -> "init" | Error _ -> "error" | Other o -> o.subtype
306306+ let unknown = function
307307+ | Init i -> i.unknown
308308+ | Error e -> e.unknown
309309+ | Other o -> o.unknown
310310+311311+ (* Constructors *)
312312+ let init ?session_id ?model ?cwd () =
313313+ Init { session_id; model; cwd; unknown = Unknown.empty }
314314+315315+ let error ~error =
316316+ Error { error; unknown = Unknown.empty }
317317+318318+ let other ~subtype =
319319+ Other { subtype; unknown = Unknown.empty }
320320+321321+ (* Individual record codecs *)
322322+ let init_jsont : init Jsont.t =
323323+ let make session_id model cwd unknown : init = { session_id; model; cwd; unknown } in
324324+ Jsont.Object.map ~kind:"SystemInit" make
325325+ |> Jsont.Object.opt_mem "session_id" Jsont.string ~enc:(fun (r : init) -> r.session_id)
326326+ |> Jsont.Object.opt_mem "model" Jsont.string ~enc:(fun (r : init) -> r.model)
327327+ |> Jsont.Object.opt_mem "cwd" Jsont.string ~enc:(fun (r : init) -> r.cwd)
328328+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : init) -> r.unknown)
329329+ |> Jsont.Object.finish
330330+331331+ let error_jsont : error Jsont.t =
332332+ let make err unknown : error = { error = err; unknown } in
333333+ Jsont.Object.map ~kind:"SystemError" make
334334+ |> Jsont.Object.mem "error" Jsont.string ~enc:(fun (r : error) -> r.error)
335335+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : error) -> r.unknown)
336336+ |> Jsont.Object.finish
337337+338338+ (* Main codec using case_mem for "subtype" discriminator *)
339339+ let jsont : t Jsont.t =
340340+ let case_init = Jsont.Object.Case.map "init" init_jsont ~dec:(fun v -> Init v) in
341341+ let case_error = Jsont.Object.Case.map "error" error_jsont ~dec:(fun v -> Error v) in
342342+ let case_other tag =
343343+ (* For unknown subtypes, create Other with the tag as subtype *)
344344+ let other_codec : other Jsont.t =
345345+ let make unknown : other = { subtype = tag; unknown } in
346346+ Jsont.Object.map ~kind:"SystemOther" make
347347+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : other) -> r.unknown)
348348+ |> Jsont.Object.finish
349349+ in
350350+ Jsont.Object.Case.map tag other_codec ~dec:(fun v -> Other v)
351351+ in
352352+ let enc_case = function
353353+ | Init v -> Jsont.Object.Case.value case_init v
354354+ | Error v -> Jsont.Object.Case.value case_error v
355355+ | Other v -> Jsont.Object.Case.value (case_other v.subtype) v
356356+ in
357357+ let cases = Jsont.Object.Case.[
358358+ make case_init;
359359+ make case_error;
360360+ ] in
361361+ Jsont.Object.map ~kind:"System" Fun.id
362362+ |> Jsont.Object.case_mem "subtype" Jsont.string ~enc:Fun.id ~enc_case cases
363363+ ~tag_to_string:Fun.id ~tag_compare:String.compare
364364+ |> Jsont.Object.finish
365365+366366+ let to_json t =
367367+ match Jsont.Json.encode jsont t with
368368+ | Ok json -> json
369369+ | Error msg -> failwith ("System.to_json: " ^ msg)
370370+371371+ let of_json json =
372372+ match Jsont.Json.decode jsont json with
373373+ | Ok v -> v
374374+ | Error msg -> raise (Invalid_argument ("System.of_json: " ^ msg))
375375+376376+ let pp fmt = function
377377+ | Init i ->
378378+ Fmt.pf fmt "@[<2>System.init@ { session_id = %a;@ model = %a;@ cwd = %a }@]"
379379+ Fmt.(option string) i.session_id
380380+ Fmt.(option string) i.model
381381+ Fmt.(option string) i.cwd
382382+ | Error e ->
383383+ Fmt.pf fmt "@[<2>System.error@ { error = %s }@]" e.error
384384+ | Other o ->
385385+ Fmt.pf fmt "@[<2>System.%s@ { ... }@]" o.subtype
386386+end
387387+388388+module Result = struct
389389+ module Usage = struct
390390+ type t = {
391391+ input_tokens : int option;
392392+ output_tokens : int option;
393393+ total_tokens : int option;
394394+ cache_creation_input_tokens : int option;
395395+ cache_read_input_tokens : int option;
396396+ unknown : Unknown.t;
397397+ }
398398+399399+ let make input_tokens output_tokens total_tokens
400400+ cache_creation_input_tokens cache_read_input_tokens unknown =
401401+ { input_tokens; output_tokens; total_tokens;
402402+ cache_creation_input_tokens; cache_read_input_tokens; unknown }
403403+404404+ let create ?input_tokens ?output_tokens ?total_tokens
405405+ ?cache_creation_input_tokens ?cache_read_input_tokens () =
406406+ { input_tokens; output_tokens; total_tokens;
407407+ cache_creation_input_tokens; cache_read_input_tokens;
408408+ unknown = Unknown.empty }
409409+410410+ let input_tokens t = t.input_tokens
411411+ let output_tokens t = t.output_tokens
412412+ let total_tokens t = t.total_tokens
413413+ let cache_creation_input_tokens t = t.cache_creation_input_tokens
414414+ let cache_read_input_tokens t = t.cache_read_input_tokens
415415+ let unknown t = t.unknown
416416+417417+ let jsont : t Jsont.t =
418418+ Jsont.Object.map ~kind:"Usage" make
419419+ |> Jsont.Object.opt_mem "input_tokens" Jsont.int ~enc:input_tokens
420420+ |> Jsont.Object.opt_mem "output_tokens" Jsont.int ~enc:output_tokens
421421+ |> Jsont.Object.opt_mem "total_tokens" Jsont.int ~enc:total_tokens
422422+ |> Jsont.Object.opt_mem "cache_creation_input_tokens" Jsont.int ~enc:cache_creation_input_tokens
423423+ |> Jsont.Object.opt_mem "cache_read_input_tokens" Jsont.int ~enc:cache_read_input_tokens
424424+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
425425+ |> Jsont.Object.finish
426426+427427+ let effective_input_tokens t =
428428+ match t.input_tokens with
429429+ | None -> 0
430430+ | Some input ->
431431+ let cached = Option.value t.cache_read_input_tokens ~default:0 in
432432+ max 0 (input - cached)
433433+434434+ let total_cost_estimate t ~input_price ~output_price =
435435+ match t.input_tokens, t.output_tokens with
436436+ | Some input, Some output ->
437437+ let input_cost = float_of_int input *. input_price /. 1_000_000. in
438438+ let output_cost = float_of_int output *. output_price /. 1_000_000. in
439439+ Some (input_cost +. output_cost)
440440+ | _ -> None
441441+442442+ let pp fmt t =
443443+ Fmt.pf fmt "@[<2>Usage@ { input = %a;@ output = %a;@ total = %a;@ \
444444+ cache_creation = %a;@ cache_read = %a }@]"
445445+ Fmt.(option int) t.input_tokens
446446+ Fmt.(option int) t.output_tokens
447447+ Fmt.(option int) t.total_tokens
448448+ Fmt.(option int) t.cache_creation_input_tokens
449449+ Fmt.(option int) t.cache_read_input_tokens
450450+451451+ let to_json t =
452452+ match Jsont.Json.encode jsont t with
453453+ | Ok json -> json
454454+ | Error msg -> failwith ("Usage.to_json: " ^ msg)
455455+456456+ let of_json json =
457457+ match Jsont.Json.decode jsont json with
458458+ | Ok v -> v
459459+ | Error msg -> raise (Invalid_argument ("Usage.of_json: " ^ msg))
460460+ end
461461+462462+ type t = {
463463+ subtype : string;
464464+ duration_ms : int;
465465+ duration_api_ms : int;
466466+ is_error : bool;
467467+ num_turns : int;
468468+ session_id : string;
469469+ total_cost_usd : float option;
470470+ usage : Usage.t option;
471471+ result : string option;
472472+ structured_output : Jsont.json option;
473473+ unknown : Unknown.t;
474474+ }
475475+476476+ let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
477477+ ~session_id ?total_cost_usd ?usage ?result ?structured_output () =
478478+ { subtype; duration_ms; duration_api_ms; is_error; num_turns;
479479+ session_id; total_cost_usd; usage; result; structured_output; unknown = Unknown.empty }
480480+481481+ let make subtype duration_ms duration_api_ms is_error num_turns
482482+ session_id total_cost_usd usage result structured_output unknown =
483483+ { subtype; duration_ms; duration_api_ms; is_error; num_turns;
484484+ session_id; total_cost_usd; usage; result; structured_output; unknown }
485485+486486+ let subtype t = t.subtype
487487+ let duration_ms t = t.duration_ms
488488+ let duration_api_ms t = t.duration_api_ms
489489+ let is_error t = t.is_error
490490+ let num_turns t = t.num_turns
491491+ let session_id t = t.session_id
492492+ let total_cost_usd t = t.total_cost_usd
493493+ let usage t = t.usage
494494+ let result t = t.result
495495+ let structured_output t = t.structured_output
496496+ let unknown t = t.unknown
497497+498498+ let jsont : t Jsont.t =
499499+ Jsont.Object.map ~kind:"Result" make
500500+ |> Jsont.Object.mem "subtype" Jsont.string ~enc:subtype
501501+ |> Jsont.Object.mem "duration_ms" Jsont.int ~enc:duration_ms
502502+ |> Jsont.Object.mem "duration_api_ms" Jsont.int ~enc:duration_api_ms
503503+ |> Jsont.Object.mem "is_error" Jsont.bool ~enc:is_error
504504+ |> Jsont.Object.mem "num_turns" Jsont.int ~enc:num_turns
505505+ |> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
506506+ |> Jsont.Object.opt_mem "total_cost_usd" Jsont.number ~enc:total_cost_usd
507507+ |> Jsont.Object.opt_mem "usage" Usage.jsont ~enc:usage
508508+ |> Jsont.Object.opt_mem "result" Jsont.string ~enc:result
509509+ |> Jsont.Object.opt_mem "structured_output" Jsont.json ~enc:structured_output
510510+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
511511+ |> Jsont.Object.finish
512512+513513+ let to_json t =
514514+ let fields = [
515515+ (Jsont.Json.name "type", Jsont.String ("result", Jsont.Meta.none));
516516+ (Jsont.Json.name "subtype", Jsont.String (t.subtype, Jsont.Meta.none));
517517+ (Jsont.Json.name "duration_ms", Jsont.Number (float_of_int t.duration_ms, Jsont.Meta.none));
518518+ (Jsont.Json.name "duration_api_ms", Jsont.Number (float_of_int t.duration_api_ms, Jsont.Meta.none));
519519+ (Jsont.Json.name "is_error", Jsont.Bool (t.is_error, Jsont.Meta.none));
520520+ (Jsont.Json.name "num_turns", Jsont.Number (float_of_int t.num_turns, Jsont.Meta.none));
521521+ (Jsont.Json.name "session_id", Jsont.String (t.session_id, Jsont.Meta.none));
522522+ ] in
523523+ let fields = match t.total_cost_usd with
524524+ | Some cost -> (Jsont.Json.name "total_cost_usd", Jsont.Number (cost, Jsont.Meta.none)) :: fields
525525+ | None -> fields
526526+ in
527527+ let fields = match t.usage with
528528+ | Some usage -> (Jsont.Json.name "usage", Usage.to_json usage) :: fields
529529+ | None -> fields
530530+ in
531531+ let fields = match t.result with
532532+ | Some result -> (Jsont.Json.name "result", Jsont.String (result, Jsont.Meta.none)) :: fields
533533+ | None -> fields
534534+ in
535535+ let fields = match t.structured_output with
536536+ | Some output -> (Jsont.Json.name "structured_output", output) :: fields
537537+ | None -> fields
538538+ in
539539+ Jsont.Object (fields, Jsont.Meta.none)
540540+541541+ let of_json json =
542542+ match Jsont.Json.decode jsont json with
543543+ | Ok v -> v
544544+ | Error msg -> raise (Invalid_argument ("Result.of_json: " ^ msg))
545545+546546+ let pp fmt t =
547547+ if t.is_error then
548548+ Fmt.pf fmt "@[<2>Result.error@ { session = %S;@ result = %a }@]"
549549+ t.session_id
550550+ Fmt.(option string) t.result
551551+ else
552552+ let tokens_info = match t.usage with
553553+ | Some u ->
554554+ let input = Usage.input_tokens u in
555555+ let output = Usage.output_tokens u in
556556+ let cached = Usage.cache_read_input_tokens u in
557557+ (match input, output, cached with
558558+ | Some i, Some o, Some c when c > 0 ->
559559+ Printf.sprintf " (tokens: %d+%d, cached: %d)" i o c
560560+ | Some i, Some o, _ ->
561561+ Printf.sprintf " (tokens: %d+%d)" i o
562562+ | _ -> "")
563563+ | None -> ""
564564+ in
565565+ Fmt.pf fmt "@[<2>Result.%s@ { duration = %dms;@ cost = $%.4f%s }@]"
566566+ t.subtype
567567+ t.duration_ms
568568+ (Option.value t.total_cost_usd ~default:0.0)
569569+ tokens_info
570570+end
571571+572572+type t =
573573+ | User of User.t
574574+ | Assistant of Assistant.t
575575+ | System of System.t
576576+ | Result of Result.t
577577+578578+let user_string s = User (User.create_string s)
579579+let user_blocks blocks = User (User.create_blocks blocks)
580580+let user_with_tool_result ~tool_use_id ~content ?is_error () =
581581+ User (User.create_with_tool_result ~tool_use_id ~content ?is_error ())
582582+583583+let assistant ~content ~model ?error () = Assistant (Assistant.create ~content ~model ?error ())
584584+let assistant_text ~text ~model ?error () =
585585+ Assistant (Assistant.create ~content:[Content_block.text text] ~model ?error ())
586586+587587+let system_init ~session_id =
588588+ System (System.init ~session_id ())
589589+let system_error ~error =
590590+ System (System.error ~error)
591591+592592+let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
593593+ ~session_id ?total_cost_usd ?usage ?result ?structured_output () =
594594+ Result (Result.create ~subtype ~duration_ms ~duration_api_ms ~is_error
595595+ ~num_turns ~session_id ?total_cost_usd ?usage ?result ?structured_output ())
596596+597597+let to_json = function
598598+ | User t -> User.to_json t
599599+ | Assistant t -> Assistant.to_json t
600600+ | System t -> System.to_json t
601601+ | Result t -> Result.to_json t
602602+603603+(* Jsont codec for the main Message variant type.
604604+ Uses case_mem for discriminated union based on "type" field. *)
605605+let jsont : t Jsont.t =
606606+ let case_map kind obj dec = Jsont.Object.Case.map kind obj ~dec in
607607+ let case_user = case_map "user" User.incoming_jsont (fun v -> User v) in
608608+ let case_assistant = case_map "assistant" Assistant.incoming_jsont (fun v -> Assistant v) in
609609+ let case_system = case_map "system" System.jsont (fun v -> System v) in
610610+ let case_result = case_map "result" Result.jsont (fun v -> Result v) in
611611+ let enc_case = function
612612+ | User v -> Jsont.Object.Case.value case_user v
613613+ | Assistant v -> Jsont.Object.Case.value case_assistant v
614614+ | System v -> Jsont.Object.Case.value case_system v
615615+ | Result v -> Jsont.Object.Case.value case_result v
616616+ in
617617+ let cases = Jsont.Object.Case.[
618618+ make case_user;
619619+ make case_assistant;
620620+ make case_system;
621621+ make case_result
622622+ ] in
623623+ Jsont.Object.map ~kind:"Message" Fun.id
624624+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
625625+ ~tag_to_string:Fun.id ~tag_compare:String.compare
626626+ |> Jsont.Object.finish
627627+628628+let of_json json =
629629+ match Jsont.Json.decode jsont json with
630630+ | Ok v -> v
631631+ | Error msg -> raise (Invalid_argument ("Message.of_json: " ^ msg))
632632+633633+let pp fmt = function
634634+ | User t -> User.pp fmt t
635635+ | Assistant t -> Assistant.pp fmt t
636636+ | System t -> System.pp fmt t
637637+ | Result t -> Result.pp fmt t
638638+639639+let is_user = function User _ -> true | _ -> false
640640+let is_assistant = function Assistant _ -> true | _ -> false
641641+let is_system = function System _ -> true | _ -> false
642642+let is_result = function Result _ -> true | _ -> false
643643+644644+let is_error = function
645645+ | Result r -> Result.is_error r
646646+ | System s -> System.subtype s = "error"
647647+ | _ -> false
648648+649649+let extract_text = function
650650+ | User u -> User.as_text u
651651+ | Assistant a ->
652652+ let text = Assistant.combined_text a in
653653+ if text = "" then None else Some text
654654+ | _ -> None
655655+656656+let extract_tool_uses = function
657657+ | Assistant a -> Assistant.get_tool_uses a
658658+ | _ -> []
659659+660660+let get_session_id = function
661661+ | System s -> System.session_id s
662662+ | Result r -> Some (Result.session_id r)
663663+ | _ -> None
664664+665665+let log_received t =
666666+ Log.info (fun m -> m "← %a" pp t)
667667+668668+let log_sending t =
669669+ Log.info (fun m -> m "→ %a" pp t)
670670+671671+let log_error msg t =
672672+ Log.err (fun m -> m "%s: %a" msg pp t)
673673+
+450
lib/message.mli
···11+(** Messages exchanged with Claude.
22+33+ This module defines the various types of messages that can be sent to and
44+ received from Claude, including user input, assistant responses, system
55+ messages, and result metadata. *)
66+77+(** The log source for message operations *)
88+val src : Logs.Src.t
99+1010+(** {1 User Messages} *)
1111+1212+module User : sig
1313+ (** Messages sent by the user. *)
1414+1515+ type content =
1616+ | String of string (** Simple text message *)
1717+ | Blocks of Content_block.t list (** Complex message with multiple content blocks *)
1818+ (** The content of a user message. *)
1919+2020+ type t
2121+ (** The type of user messages. *)
2222+2323+ val jsont : t Jsont.t
2424+ (** [jsont] is the Jsont codec for user messages. *)
2525+2626+ val create_string : string -> t
2727+ (** [create_string s] creates a user message with simple text content. *)
2828+2929+ val create_blocks : Content_block.t list -> t
3030+ (** [create_blocks blocks] creates a user message with content blocks. *)
3131+3232+ val create_with_tool_result :
3333+ tool_use_id:string ->
3434+ content:string ->
3535+ ?is_error:bool ->
3636+ unit -> t
3737+ (** [create_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user
3838+ message containing a tool result. *)
3939+4040+ val create_mixed : text:string option -> tool_results:(string * string * bool option) list -> t
4141+ (** [create_mixed ?text ~tool_results] creates a user message with optional text
4242+ and tool results. Each tool result is (tool_use_id, content, is_error). *)
4343+4444+ val content : t -> content
4545+ (** [content t] returns the content of the user message. *)
4646+4747+ val unknown : t -> Unknown.t
4848+ (** [unknown t] returns the unknown fields preserved from JSON. *)
4949+5050+ val as_text : t -> string option
5151+ (** [as_text t] returns the text content if the message is a simple string, None otherwise. *)
5252+5353+ val get_blocks : t -> Content_block.t list
5454+ (** [get_blocks t] returns the content blocks, or a single text block if it's a string message. *)
5555+5656+ val to_json : t -> Jsont.json
5757+ (** [to_json t] converts the user message to its JSON representation. *)
5858+5959+ val of_json : Jsont.json -> t
6060+ (** [of_json json] parses a user message from JSON.
6161+ @raise Invalid_argument if the JSON is not a valid user message. *)
6262+6363+ val pp : Format.formatter -> t -> unit
6464+ (** [pp fmt t] pretty-prints the user message. *)
6565+end
6666+6767+(** {1 Assistant Messages} *)
6868+6969+module Assistant : sig
7070+ (** Messages from Claude assistant. *)
7171+7272+ type error = [
7373+ | `Authentication_failed (** Authentication with Claude API failed *)
7474+ | `Billing_error (** Billing or account issue *)
7575+ | `Rate_limit (** Rate limit exceeded *)
7676+ | `Invalid_request (** Request was invalid *)
7777+ | `Server_error (** Internal server error *)
7878+ | `Unknown (** Unknown error type *)
7979+ ]
8080+ (** The type of assistant message errors based on Python SDK error types. *)
8181+8282+ val error_to_string : error -> string
8383+ (** [error_to_string err] converts an error to its string representation. *)
8484+8585+ val error_of_string : string -> error
8686+ (** [error_of_string s] parses an error string. Unknown strings become [`Unknown]. *)
8787+8888+ type t
8989+ (** The type of assistant messages. *)
9090+9191+ val jsont : t Jsont.t
9292+ (** [jsont] is the Jsont codec for assistant messages. *)
9393+9494+ val create : content:Content_block.t list -> model:string -> ?error:error -> unit -> t
9595+ (** [create ~content ~model ?error ()] creates an assistant message.
9696+ @param content List of content blocks in the response
9797+ @param model The model identifier used for the response
9898+ @param error Optional error that occurred during message generation *)
9999+100100+ val content : t -> Content_block.t list
101101+ (** [content t] returns the content blocks of the assistant message. *)
102102+103103+ val model : t -> string
104104+ (** [model t] returns the model identifier. *)
105105+106106+ val error : t -> error option
107107+ (** [error t] returns the optional error that occurred during message generation. *)
108108+109109+ val unknown : t -> Unknown.t
110110+ (** [unknown t] returns the unknown fields preserved from JSON. *)
111111+112112+ val get_text_blocks : t -> string list
113113+ (** [get_text_blocks t] extracts all text content from the message. *)
114114+115115+ val get_tool_uses : t -> Content_block.Tool_use.t list
116116+ (** [get_tool_uses t] extracts all tool use blocks from the message. *)
117117+118118+ val get_thinking : t -> Content_block.Thinking.t list
119119+ (** [get_thinking t] extracts all thinking blocks from the message. *)
120120+121121+ val has_tool_use : t -> bool
122122+ (** [has_tool_use t] returns true if the message contains any tool use blocks. *)
123123+124124+ val combined_text : t -> string
125125+ (** [combined_text t] concatenates all text blocks into a single string. *)
126126+127127+ val to_json : t -> Jsont.json
128128+ (** [to_json t] converts the assistant message to its JSON representation. *)
129129+130130+ val of_json : Jsont.json -> t
131131+ (** [of_json json] parses an assistant message from JSON.
132132+ @raise Invalid_argument if the JSON is not a valid assistant message. *)
133133+134134+ val pp : Format.formatter -> t -> unit
135135+ (** [pp fmt t] pretty-prints the assistant message. *)
136136+end
137137+138138+(** {1 System Messages} *)
139139+140140+module System : sig
141141+ (** System control and status messages.
142142+143143+ System messages use a discriminated union on the "subtype" field:
144144+ - "init": Session initialization with session_id, model, cwd
145145+ - "error": Error messages with error string
146146+ - Other subtypes are preserved as [Other] *)
147147+148148+ type init = {
149149+ session_id : string option;
150150+ model : string option;
151151+ cwd : string option;
152152+ unknown : Unknown.t;
153153+ }
154154+ (** Init message fields. *)
155155+156156+ type error = {
157157+ error : string;
158158+ unknown : Unknown.t;
159159+ }
160160+ (** Error message fields. *)
161161+162162+ type other = {
163163+ subtype : string;
164164+ unknown : Unknown.t;
165165+ }
166166+ (** Unknown subtype fields. *)
167167+168168+ type t =
169169+ | Init of init
170170+ | Error of error
171171+ | Other of other
172172+ (** The type of system messages. *)
173173+174174+ val jsont : t Jsont.t
175175+ (** [jsont] is the Jsont codec for system messages. *)
176176+177177+ (** {2 Constructors} *)
178178+179179+ val init : ?session_id:string -> ?model:string -> ?cwd:string -> unit -> t
180180+ (** [init ?session_id ?model ?cwd ()] creates an init message. *)
181181+182182+ val error : error:string -> t
183183+ (** [error ~error] creates an error message. *)
184184+185185+ val other : subtype:string -> t
186186+ (** [other ~subtype] creates a message with unknown subtype. *)
187187+188188+ (** {2 Accessors} *)
189189+190190+ val session_id : t -> string option
191191+ (** [session_id t] returns session_id from Init, None otherwise. *)
192192+193193+ val model : t -> string option
194194+ (** [model t] returns model from Init, None otherwise. *)
195195+196196+ val cwd : t -> string option
197197+ (** [cwd t] returns cwd from Init, None otherwise. *)
198198+199199+ val error_msg : t -> string option
200200+ (** [error_msg t] returns error from Error, None otherwise. *)
201201+202202+ val subtype : t -> string
203203+ (** [subtype t] returns the subtype string. *)
204204+205205+ val unknown : t -> Unknown.t
206206+ (** [unknown t] returns the unknown fields. *)
207207+208208+ (** {2 Conversion} *)
209209+210210+ val to_json : t -> Jsont.json
211211+ (** [to_json t] converts to JSON representation. *)
212212+213213+ val of_json : Jsont.json -> t
214214+ (** [of_json json] parses from JSON.
215215+ @raise Invalid_argument if invalid. *)
216216+217217+ val pp : Format.formatter -> t -> unit
218218+ (** [pp fmt t] pretty-prints the message. *)
219219+end
220220+221221+(** {1 Result Messages} *)
222222+223223+module Result : sig
224224+ (** Final result messages with metadata about the conversation. *)
225225+226226+ module Usage : sig
227227+ (** Usage statistics for API calls. *)
228228+229229+ type t
230230+ (** Type for usage statistics. *)
231231+232232+ val jsont : t Jsont.t
233233+ (** [jsont] is the Jsont codec for usage statistics. *)
234234+235235+ val create :
236236+ ?input_tokens:int ->
237237+ ?output_tokens:int ->
238238+ ?total_tokens:int ->
239239+ ?cache_creation_input_tokens:int ->
240240+ ?cache_read_input_tokens:int ->
241241+ unit -> t
242242+ (** [create ?input_tokens ?output_tokens ?total_tokens ?cache_creation_input_tokens
243243+ ?cache_read_input_tokens ()] creates usage statistics. *)
244244+245245+ val input_tokens : t -> int option
246246+ (** [input_tokens t] returns the number of input tokens used. *)
247247+248248+ val output_tokens : t -> int option
249249+ (** [output_tokens t] returns the number of output tokens generated. *)
250250+251251+ val total_tokens : t -> int option
252252+ (** [total_tokens t] returns the total number of tokens. *)
253253+254254+ val cache_creation_input_tokens : t -> int option
255255+ (** [cache_creation_input_tokens t] returns cache creation input tokens. *)
256256+257257+ val cache_read_input_tokens : t -> int option
258258+ (** [cache_read_input_tokens t] returns cache read input tokens. *)
259259+260260+ val unknown : t -> Unknown.t
261261+ (** [unknown t] returns the unknown fields preserved from JSON. *)
262262+263263+ val effective_input_tokens : t -> int
264264+ (** [effective_input_tokens t] returns input tokens minus cached tokens, or 0 if not available. *)
265265+266266+ val total_cost_estimate : t -> input_price:float -> output_price:float -> float option
267267+ (** [total_cost_estimate t ~input_price ~output_price] estimates the cost based on token
268268+ prices per million tokens. Returns None if token counts are not available. *)
269269+270270+ val pp : Format.formatter -> t -> unit
271271+ (** [pp fmt t] pretty-prints the usage statistics. *)
272272+273273+ val to_json : t -> Jsont.json
274274+ (** [to_json t] converts to JSON representation. Internal use only. *)
275275+276276+ val of_json : Jsont.json -> t
277277+ (** [of_json json] parses from JSON. Internal use only. *)
278278+ end
279279+280280+ type t
281281+ (** The type of result messages. *)
282282+283283+ val jsont : t Jsont.t
284284+ (** [jsont] is the Jsont codec for result messages. *)
285285+286286+ val create :
287287+ subtype:string ->
288288+ duration_ms:int ->
289289+ duration_api_ms:int ->
290290+ is_error:bool ->
291291+ num_turns:int ->
292292+ session_id:string ->
293293+ ?total_cost_usd:float ->
294294+ ?usage:Usage.t ->
295295+ ?result:string ->
296296+ ?structured_output:Jsont.json ->
297297+ unit -> t
298298+ (** [create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
299299+ ~session_id ?total_cost_usd ?usage ?result ()] creates a result message.
300300+ @param subtype The subtype of the result
301301+ @param duration_ms Total duration in milliseconds
302302+ @param duration_api_ms API duration in milliseconds
303303+ @param is_error Whether the result represents an error
304304+ @param num_turns Number of conversation turns
305305+ @param session_id Unique session identifier
306306+ @param total_cost_usd Optional total cost in USD
307307+ @param usage Optional usage statistics as JSON
308308+ @param result Optional result string
309309+ @param structured_output Optional structured JSON output from Claude *)
310310+311311+ val subtype : t -> string
312312+ (** [subtype t] returns the subtype of the result. *)
313313+314314+ val duration_ms : t -> int
315315+ (** [duration_ms t] returns the total duration in milliseconds. *)
316316+317317+ val duration_api_ms : t -> int
318318+ (** [duration_api_ms t] returns the API duration in milliseconds. *)
319319+320320+ val is_error : t -> bool
321321+ (** [is_error t] returns whether this result represents an error. *)
322322+323323+ val num_turns : t -> int
324324+ (** [num_turns t] returns the number of conversation turns. *)
325325+326326+ val session_id : t -> string
327327+ (** [session_id t] returns the session identifier. *)
328328+329329+ val total_cost_usd : t -> float option
330330+ (** [total_cost_usd t] returns the optional total cost in USD. *)
331331+332332+ val usage : t -> Usage.t option
333333+ (** [usage t] returns the optional usage statistics. *)
334334+335335+ val result : t -> string option
336336+ (** [result t] returns the optional result string. *)
337337+338338+ val structured_output : t -> Jsont.json option
339339+ (** [structured_output t] returns the optional structured JSON output. *)
340340+341341+ val unknown : t -> Unknown.t
342342+ (** [unknown t] returns the unknown fields preserved from JSON. *)
343343+344344+ val to_json : t -> Jsont.json
345345+ (** [to_json t] converts the result message to its JSON representation. *)
346346+347347+ val of_json : Jsont.json -> t
348348+ (** [of_json json] parses a result message from JSON.
349349+ @raise Invalid_argument if the JSON is not a valid result message. *)
350350+351351+ val pp : Format.formatter -> t -> unit
352352+ (** [pp fmt t] pretty-prints the result message. *)
353353+end
354354+355355+(** {1 Message Union Type} *)
356356+357357+type t =
358358+ | User of User.t
359359+ | Assistant of Assistant.t
360360+ | System of System.t
361361+ | Result of Result.t
362362+(** The type of messages, which can be user, assistant, system, or result. *)
363363+364364+val jsont : t Jsont.t
365365+(** [jsont] is the Jsont codec for messages. *)
366366+367367+val user_string : string -> t
368368+(** [user_string s] creates a user message with text content. *)
369369+370370+val user_blocks : Content_block.t list -> t
371371+(** [user_blocks blocks] creates a user message with content blocks. *)
372372+373373+val user_with_tool_result : tool_use_id:string -> content:string -> ?is_error:bool -> unit -> t
374374+(** [user_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user message
375375+ containing a tool result. *)
376376+377377+val assistant : content:Content_block.t list -> model:string -> ?error:Assistant.error -> unit -> t
378378+(** [assistant ~content ~model ?error ()] creates an assistant message. *)
379379+380380+val assistant_text : text:string -> model:string -> ?error:Assistant.error -> unit -> t
381381+(** [assistant_text ~text ~model ?error ()] creates an assistant message with only text content. *)
382382+383383+val system_init : session_id:string -> t
384384+(** [system_init ~session_id] creates a system init message. *)
385385+386386+val system_error : error:string -> t
387387+(** [system_error ~error] creates a system error message. *)
388388+389389+val result :
390390+ subtype:string ->
391391+ duration_ms:int ->
392392+ duration_api_ms:int ->
393393+ is_error:bool ->
394394+ num_turns:int ->
395395+ session_id:string ->
396396+ ?total_cost_usd:float ->
397397+ ?usage:Result.Usage.t ->
398398+ ?result:string ->
399399+ ?structured_output:Jsont.json ->
400400+ unit -> t
401401+(** [result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
402402+ ~session_id ?total_cost_usd ?usage ?result ()] creates a result message. *)
403403+404404+val to_json : t -> Jsont.json
405405+(** [to_json t] converts any message to its JSON representation. *)
406406+407407+val of_json : Jsont.json -> t
408408+(** [of_json json] parses a message from JSON.
409409+ @raise Invalid_argument if the JSON is not a valid message. *)
410410+411411+val pp : Format.formatter -> t -> unit
412412+(** [pp fmt t] pretty-prints any message. *)
413413+414414+(** {1 Message Analysis} *)
415415+416416+val is_user : t -> bool
417417+(** [is_user t] returns true if the message is from a user. *)
418418+419419+val is_assistant : t -> bool
420420+(** [is_assistant t] returns true if the message is from the assistant. *)
421421+422422+val is_system : t -> bool
423423+(** [is_system t] returns true if the message is a system message. *)
424424+425425+val is_result : t -> bool
426426+(** [is_result t] returns true if the message is a result message. *)
427427+428428+val is_error : t -> bool
429429+(** [is_error t] returns true if the message represents an error. *)
430430+431431+val extract_text : t -> string option
432432+(** [extract_text t] attempts to extract text content from any message type. *)
433433+434434+val extract_tool_uses : t -> Content_block.Tool_use.t list
435435+(** [extract_tool_uses t] extracts tool use blocks from assistant messages. *)
436436+437437+val get_session_id : t -> string option
438438+(** [get_session_id t] extracts the session ID from system or result messages. *)
439439+440440+(** {1 Logging} *)
441441+442442+val log_received : t -> unit
443443+(** [log_received t] logs that a message was received. *)
444444+445445+val log_sending : t -> unit
446446+(** [log_sending t] logs that a message is being sent. *)
447447+448448+val log_error : string -> t -> unit
449449+(** [log_error msg t] logs an error with the given message and context. *)
450450+
···11+(** Claude AI model identifiers.
22+33+ This module provides type-safe model identifiers based on the Python SDK's
44+ model strings. Use polymorphic variants for known models with a custom
55+ escape hatch for future or unknown models. *)
66+77+type t = [
88+ | `Sonnet_4_5 (** claude-sonnet-4-5 - Most recent Sonnet model *)
99+ | `Sonnet_4 (** claude-sonnet-4 - Sonnet 4 model *)
1010+ | `Sonnet_3_5 (** claude-sonnet-3-5 - Sonnet 3.5 model *)
1111+ | `Opus_4 (** claude-opus-4 - Opus 4 model for complex tasks *)
1212+ | `Haiku_4 (** claude-haiku-4 - Fast, cost-effective Haiku model *)
1313+ | `Custom of string (** Custom model string for future/unknown models *)
1414+]
1515+(** The type of Claude models. *)
1616+1717+val to_string : t -> string
1818+(** [to_string t] converts a model to its CLI string representation.
1919+2020+ Examples:
2121+ - [`Sonnet_4_5] becomes "claude-sonnet-4-5"
2222+ - [`Opus_4] becomes "claude-opus-4"
2323+ - [`Custom "my-model"] becomes "my-model" *)
2424+2525+val of_string : string -> t
2626+(** [of_string s] parses a model string into a typed model.
2727+2828+ Known model strings are converted to their typed variants.
2929+ Unknown strings become [`Custom s].
3030+3131+ Examples:
3232+ - "claude-sonnet-4-5" becomes [`Sonnet_4_5]
3333+ - "future-model" becomes [`Custom "future-model"] *)
3434+3535+val pp : Format.formatter -> t -> unit
3636+(** [pp fmt t] pretty-prints a model identifier. *)
+252
lib/options.ml
···11+let src = Logs.Src.create "claude.options" ~doc:"Claude configuration options"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+type setting_source = User | Project | Local
55+66+type t = {
77+ allowed_tools : string list;
88+ disallowed_tools : string list;
99+ max_thinking_tokens : int;
1010+ system_prompt : string option;
1111+ append_system_prompt : string option;
1212+ permission_mode : Permissions.Mode.t option;
1313+ permission_callback : Permissions.callback option;
1414+ model : Model.t option;
1515+ cwd : Eio.Fs.dir_ty Eio.Path.t option;
1616+ env : (string * string) list;
1717+ continue_conversation : bool;
1818+ resume : string option;
1919+ max_turns : int option;
2020+ permission_prompt_tool_name : string option;
2121+ settings : string option;
2222+ add_dirs : string list;
2323+ extra_args : (string * string option) list;
2424+ debug_stderr : Eio.Flow.sink_ty Eio.Flow.sink option;
2525+ hooks : Hooks.config option;
2626+ max_budget_usd : float option;
2727+ fallback_model : Model.t option;
2828+ setting_sources : setting_source list option;
2929+ max_buffer_size : int option;
3030+ user : string option;
3131+ output_format : Structured_output.t option;
3232+ unknown : Unknown.t;
3333+}
3434+3535+let default = {
3636+ allowed_tools = [];
3737+ disallowed_tools = [];
3838+ max_thinking_tokens = 8000;
3939+ system_prompt = None;
4040+ append_system_prompt = None;
4141+ permission_mode = None;
4242+ permission_callback = Some Permissions.default_allow_callback;
4343+ model = None;
4444+ cwd = None;
4545+ env = [];
4646+ continue_conversation = false;
4747+ resume = None;
4848+ max_turns = None;
4949+ permission_prompt_tool_name = None;
5050+ settings = None;
5151+ add_dirs = [];
5252+ extra_args = [];
5353+ debug_stderr = None;
5454+ hooks = None;
5555+ max_budget_usd = None;
5656+ fallback_model = None;
5757+ setting_sources = None;
5858+ max_buffer_size = None;
5959+ user = None;
6060+ output_format = None;
6161+ unknown = Unknown.empty;
6262+}
6363+6464+let create
6565+ ?(allowed_tools = [])
6666+ ?(disallowed_tools = [])
6767+ ?(max_thinking_tokens = 8000)
6868+ ?system_prompt
6969+ ?append_system_prompt
7070+ ?permission_mode
7171+ ?permission_callback
7272+ ?model
7373+ ?cwd
7474+ ?(env = [])
7575+ ?(continue_conversation = false)
7676+ ?resume
7777+ ?max_turns
7878+ ?permission_prompt_tool_name
7979+ ?settings
8080+ ?(add_dirs = [])
8181+ ?(extra_args = [])
8282+ ?debug_stderr
8383+ ?hooks
8484+ ?max_budget_usd
8585+ ?fallback_model
8686+ ?setting_sources
8787+ ?max_buffer_size
8888+ ?user
8989+ ?output_format
9090+ ?(unknown = Unknown.empty)
9191+ () =
9292+ { allowed_tools; disallowed_tools; max_thinking_tokens;
9393+ system_prompt; append_system_prompt; permission_mode;
9494+ permission_callback; model; cwd; env;
9595+ continue_conversation; resume; max_turns;
9696+ permission_prompt_tool_name; settings; add_dirs;
9797+ extra_args; debug_stderr; hooks;
9898+ max_budget_usd; fallback_model; setting_sources;
9999+ max_buffer_size; user; output_format; unknown }
100100+101101+let allowed_tools t = t.allowed_tools
102102+let disallowed_tools t = t.disallowed_tools
103103+let max_thinking_tokens t = t.max_thinking_tokens
104104+let system_prompt t = t.system_prompt
105105+let append_system_prompt t = t.append_system_prompt
106106+let permission_mode t = t.permission_mode
107107+let permission_callback t = t.permission_callback
108108+let model t = t.model
109109+let cwd t = t.cwd
110110+let env t = t.env
111111+let continue_conversation t = t.continue_conversation
112112+let resume t = t.resume
113113+let max_turns t = t.max_turns
114114+let permission_prompt_tool_name t = t.permission_prompt_tool_name
115115+let settings t = t.settings
116116+let add_dirs t = t.add_dirs
117117+let extra_args t = t.extra_args
118118+let debug_stderr t = t.debug_stderr
119119+let hooks t = t.hooks
120120+let max_budget_usd t = t.max_budget_usd
121121+let fallback_model t = t.fallback_model
122122+let setting_sources t = t.setting_sources
123123+let max_buffer_size t = t.max_buffer_size
124124+let user t = t.user
125125+let output_format t = t.output_format
126126+let unknown t = t.unknown
127127+128128+let with_allowed_tools tools t = { t with allowed_tools = tools }
129129+let with_disallowed_tools tools t = { t with disallowed_tools = tools }
130130+let with_max_thinking_tokens tokens t = { t with max_thinking_tokens = tokens }
131131+let with_system_prompt prompt t = { t with system_prompt = Some prompt }
132132+let with_append_system_prompt prompt t = { t with append_system_prompt = Some prompt }
133133+let with_permission_mode mode t = { t with permission_mode = Some mode }
134134+let with_permission_callback callback t = { t with permission_callback = Some callback }
135135+let with_model model t = { t with model = Some model }
136136+let with_model_string model t = { t with model = Some (Model.of_string model) }
137137+let with_cwd cwd t = { t with cwd = Some cwd }
138138+let with_env env t = { t with env }
139139+let with_continue_conversation continue t = { t with continue_conversation = continue }
140140+let with_resume session_id t = { t with resume = Some session_id }
141141+let with_max_turns turns t = { t with max_turns = Some turns }
142142+let with_permission_prompt_tool_name tool t = { t with permission_prompt_tool_name = Some tool }
143143+let with_settings path t = { t with settings = Some path }
144144+let with_add_dirs dirs t = { t with add_dirs = dirs }
145145+let with_extra_args args t = { t with extra_args = args }
146146+let with_debug_stderr sink t = { t with debug_stderr = Some sink }
147147+let with_hooks hooks t = { t with hooks = Some hooks }
148148+let with_max_budget_usd budget t = { t with max_budget_usd = Some budget }
149149+let with_fallback_model model t = { t with fallback_model = Some model }
150150+let with_fallback_model_string model t = { t with fallback_model = Some (Model.of_string model) }
151151+let with_setting_sources sources t = { t with setting_sources = Some sources }
152152+let with_no_settings t = { t with setting_sources = Some [] }
153153+let with_max_buffer_size size t = { t with max_buffer_size = Some size }
154154+let with_user user t = { t with user = Some user }
155155+let with_output_format format t = { t with output_format = Some format }
156156+157157+(* Helper codec for Model.t *)
158158+let model_jsont : Model.t Jsont.t =
159159+ Jsont.map ~kind:"Model"
160160+ ~dec:Model.of_string
161161+ ~enc:Model.to_string
162162+ Jsont.string
163163+164164+(* Helper codec for env - list of string pairs encoded as object.
165165+ Env is a dynamic object where all values should be strings.
166166+ Uses pattern matching to extract object members, then jsont for string decoding. *)
167167+let env_jsont : (string * string) list Jsont.t =
168168+ Jsont.map ~kind:"Env"
169169+ ~dec:(fun json ->
170170+ match json with
171171+ | Jsont.Object (members, _) ->
172172+ List.filter_map (fun ((name, _), value) ->
173173+ match Jsont.Json.decode Jsont.string value with
174174+ | Ok s -> Some (name, s)
175175+ | Error _ -> None
176176+ ) members
177177+ | _ -> [])
178178+ ~enc:(fun pairs ->
179179+ Jsont.Json.object' (List.map (fun (k, v) ->
180180+ Jsont.Json.mem (Jsont.Json.name k) (Jsont.Json.string v)
181181+ ) pairs))
182182+ Jsont.json
183183+184184+let jsont : t Jsont.t =
185185+ let make allowed_tools disallowed_tools max_thinking_tokens
186186+ system_prompt append_system_prompt permission_mode
187187+ model env unknown =
188188+ { allowed_tools; disallowed_tools; max_thinking_tokens;
189189+ system_prompt; append_system_prompt; permission_mode;
190190+ permission_callback = Some Permissions.default_allow_callback;
191191+ model; cwd = None; env;
192192+ continue_conversation = false;
193193+ resume = None;
194194+ max_turns = None;
195195+ permission_prompt_tool_name = None;
196196+ settings = None;
197197+ add_dirs = [];
198198+ extra_args = [];
199199+ debug_stderr = None;
200200+ hooks = None;
201201+ max_budget_usd = None;
202202+ fallback_model = None;
203203+ setting_sources = None;
204204+ max_buffer_size = None;
205205+ user = None;
206206+ output_format = None;
207207+ unknown }
208208+ in
209209+ Jsont.Object.map ~kind:"Options" make
210210+ |> Jsont.Object.mem "allowed_tools" (Jsont.list Jsont.string) ~enc:allowed_tools ~dec_absent:[]
211211+ |> Jsont.Object.mem "disallowed_tools" (Jsont.list Jsont.string) ~enc:disallowed_tools ~dec_absent:[]
212212+ |> Jsont.Object.mem "max_thinking_tokens" Jsont.int ~enc:max_thinking_tokens ~dec_absent:8000
213213+ |> Jsont.Object.opt_mem "system_prompt" Jsont.string ~enc:system_prompt
214214+ |> Jsont.Object.opt_mem "append_system_prompt" Jsont.string ~enc:append_system_prompt
215215+ |> Jsont.Object.opt_mem "permission_mode" Permissions.Mode.jsont ~enc:permission_mode
216216+ |> Jsont.Object.opt_mem "model" model_jsont ~enc:model
217217+ |> Jsont.Object.mem "env" env_jsont ~enc:env ~dec_absent:[]
218218+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
219219+ |> Jsont.Object.finish
220220+221221+let to_json t =
222222+ match Jsont.Json.encode jsont t with
223223+ | Ok json -> json
224224+ | Error msg -> failwith ("Options.to_json: " ^ msg)
225225+226226+let of_json json =
227227+ match Jsont.Json.decode jsont json with
228228+ | Ok t -> t
229229+ | Error msg -> raise (Invalid_argument ("Options.of_json: " ^ msg))
230230+231231+let pp fmt t =
232232+ Fmt.pf fmt "@[<v>Options {@ \
233233+ allowed_tools = %a;@ \
234234+ disallowed_tools = %a;@ \
235235+ max_thinking_tokens = %d;@ \
236236+ system_prompt = %a;@ \
237237+ append_system_prompt = %a;@ \
238238+ permission_mode = %a;@ \
239239+ model = %a;@ \
240240+ env = %a@ \
241241+ }@]"
242242+ Fmt.(list string) t.allowed_tools
243243+ Fmt.(list string) t.disallowed_tools
244244+ t.max_thinking_tokens
245245+ Fmt.(option string) t.system_prompt
246246+ Fmt.(option string) t.append_system_prompt
247247+ Fmt.(option Permissions.Mode.pp) t.permission_mode
248248+ Fmt.(option Model.pp) t.model
249249+ Fmt.(list (pair string string)) t.env
250250+251251+let log_options t =
252252+ Log.debug (fun m -> m "Claude options: %a" pp t)
+372
lib/options.mli
···11+(** Configuration options for Claude sessions.
22+33+ This module provides comprehensive configuration options for controlling
44+ Claude's behavior, including tool permissions, system prompts, models,
55+ execution environment, cost controls, and structured outputs.
66+77+ {2 Overview}
88+99+ Options control all aspects of Claude's behavior:
1010+ - {b Permissions}: Which tools Claude can use and how permission is granted
1111+ - {b Models}: Which AI model to use and fallback options
1212+ - {b Environment}: Working directory, environment variables, settings
1313+ - {b Cost Control}: Budget limits to prevent runaway spending
1414+ - {b Hooks}: Intercept and modify tool execution
1515+ - {b Structured Output}: JSON schema validation for responses
1616+ - {b Session Management}: Continue or resume conversations
1717+1818+ {2 Builder Pattern}
1919+2020+ Options use a functional builder pattern - each [with_*] function returns
2121+ a new options value with the specified field updated:
2222+2323+ {[
2424+ let options = Options.default
2525+ |> Options.with_model "claude-sonnet-4-5"
2626+ |> Options.with_max_budget_usd 1.0
2727+ |> Options.with_permission_mode Permissions.Mode.Accept_edits
2828+ ]}
2929+3030+ {2 Common Configuration Scenarios}
3131+3232+ {3 CI/CD: Isolated, Reproducible Builds}
3333+3434+ {[
3535+ let ci_config = Options.default
3636+ |> Options.with_no_settings (* Ignore user config *)
3737+ |> Options.with_max_budget_usd 0.50 (* 50 cent limit *)
3838+ |> Options.with_permission_mode
3939+ Permissions.Mode.Bypass_permissions
4040+ |> Options.with_model "claude-haiku-4"
4141+ ]}
4242+4343+ {3 Production: Cost Control with Fallback}
4444+4545+ {[
4646+ let prod_config = Options.default
4747+ |> Options.with_model "claude-sonnet-4-5"
4848+ |> Options.with_fallback_model "claude-haiku-4"
4949+ |> Options.with_max_budget_usd 10.0 (* $10 daily limit *)
5050+ |> Options.with_max_buffer_size 5_000_000
5151+ ]}
5252+5353+ {3 Development: User Settings with Overrides}
5454+5555+ {[
5656+ let dev_config = Options.default
5757+ |> Options.with_setting_sources [User; Project]
5858+ |> Options.with_max_budget_usd 1.0
5959+ |> Options.with_permission_mode Permissions.Mode.Default
6060+ ]}
6161+6262+ {3 Structured Output: Type-Safe Responses}
6363+6464+ {[
6565+ let schema = Jsont.json_of_json (`O [
6666+ ("type", `String "object");
6767+ ("properties", `O [
6868+ ("count", `O [("type", `String "integer")]);
6969+ ("has_tests", `O [("type", `String "boolean")]);
7070+ ]);
7171+ ])
7272+ let format = Structured_output.of_json_schema schema
7373+7474+ let analysis_config = Options.default
7575+ |> Options.with_output_format format
7676+ |> Options.with_allowed_tools ["Read"; "Glob"; "Grep"]
7777+ ]}
7878+7979+ {2 Advanced Options}
8080+8181+ {3 Budget Control}
8282+8383+ Use {!with_max_budget_usd} to set hard spending limits. Claude will
8484+ terminate the session if the budget is exceeded, preventing runaway costs.
8585+8686+ {3 Settings Isolation}
8787+8888+ Use {!with_setting_sources} or {!with_no_settings} to control which
8989+ configuration files are loaded:
9090+ - [User] - ~/.claude/config
9191+ - [Project] - .claude/ in project root
9292+ - [Local] - Current directory settings
9393+ - [Some \[\]] (via {!with_no_settings}) - No settings, fully isolated
9494+9595+ This is critical for reproducible builds in CI/CD environments.
9696+9797+ {3 Model Fallback}
9898+9999+ Use {!with_fallback_model} to specify an alternative model when the
100100+ primary model is unavailable or overloaded. This improves reliability. *)
101101+102102+(** The log source for options operations *)
103103+val src : Logs.Src.t
104104+105105+(** {1 Types} *)
106106+107107+type setting_source = User | Project | Local
108108+(** Setting source determines which configuration files to load.
109109+ - [User]: Load user-level settings from ~/.claude/config
110110+ - [Project]: Load project-level settings from .claude/ in project root
111111+ - [Local]: Load local settings from current directory *)
112112+113113+type t
114114+(** The type of configuration options. *)
115115+116116+val default : t
117117+(** [default] returns the default configuration with sensible defaults:
118118+ - No tool restrictions
119119+ - 8000 max thinking tokens
120120+ - Default allow permission callback
121121+ - No custom prompts or model override *)
122122+123123+val create :
124124+ ?allowed_tools:string list ->
125125+ ?disallowed_tools:string list ->
126126+ ?max_thinking_tokens:int ->
127127+ ?system_prompt:string ->
128128+ ?append_system_prompt:string ->
129129+ ?permission_mode:Permissions.Mode.t ->
130130+ ?permission_callback:Permissions.callback ->
131131+ ?model:Model.t ->
132132+ ?cwd:Eio.Fs.dir_ty Eio.Path.t ->
133133+ ?env:(string * string) list ->
134134+ ?continue_conversation:bool ->
135135+ ?resume:string ->
136136+ ?max_turns:int ->
137137+ ?permission_prompt_tool_name:string ->
138138+ ?settings:string ->
139139+ ?add_dirs:string list ->
140140+ ?extra_args:(string * string option) list ->
141141+ ?debug_stderr:Eio.Flow.sink_ty Eio.Flow.sink ->
142142+ ?hooks:Hooks.config ->
143143+ ?max_budget_usd:float ->
144144+ ?fallback_model:Model.t ->
145145+ ?setting_sources:setting_source list ->
146146+ ?max_buffer_size:int ->
147147+ ?user:string ->
148148+ ?output_format:Structured_output.t ->
149149+ ?unknown:Jsont.json ->
150150+ unit -> t
151151+(** [create ?allowed_tools ?disallowed_tools ?max_thinking_tokens ?system_prompt
152152+ ?append_system_prompt ?permission_mode ?permission_callback ?model ?cwd ?env
153153+ ?continue_conversation ?resume ?max_turns ?permission_prompt_tool_name ?settings
154154+ ?add_dirs ?extra_args ?debug_stderr ?hooks ?max_budget_usd ?fallback_model
155155+ ?setting_sources ?max_buffer_size ?user ()]
156156+ creates a new configuration.
157157+ @param allowed_tools List of explicitly allowed tool names
158158+ @param disallowed_tools List of explicitly disallowed tool names
159159+ @param max_thinking_tokens Maximum tokens for thinking blocks (default: 8000)
160160+ @param system_prompt Replace the default system prompt
161161+ @param append_system_prompt Append to the default system prompt
162162+ @param permission_mode Permission mode to use
163163+ @param permission_callback Custom permission callback
164164+ @param model Override the default model
165165+ @param cwd Working directory for file operations
166166+ @param env Environment variables to set
167167+ @param continue_conversation Continue an existing conversation
168168+ @param resume Resume from a specific session ID
169169+ @param max_turns Maximum number of conversation turns
170170+ @param permission_prompt_tool_name Tool name for permission prompts
171171+ @param settings Path to settings file
172172+ @param add_dirs Additional directories to allow access to
173173+ @param extra_args Additional CLI flags to pass through
174174+ @param debug_stderr Sink for debug output when debug-to-stderr is set
175175+ @param hooks Hooks configuration for event interception
176176+ @param max_budget_usd Hard spending limit in USD (terminates on exceed)
177177+ @param fallback_model Automatic fallback on primary model unavailability
178178+ @param setting_sources Control which settings load (user/project/local)
179179+ @param max_buffer_size Control for stdout buffer size in bytes
180180+ @param user Unix user for subprocess execution
181181+ @param output_format Optional structured output format specification *)
182182+183183+(** {1 Accessors} *)
184184+185185+val allowed_tools : t -> string list
186186+(** [allowed_tools t] returns the list of allowed tools. *)
187187+188188+val disallowed_tools : t -> string list
189189+(** [disallowed_tools t] returns the list of disallowed tools. *)
190190+191191+val max_thinking_tokens : t -> int
192192+(** [max_thinking_tokens t] returns the maximum thinking tokens. *)
193193+194194+val system_prompt : t -> string option
195195+(** [system_prompt t] returns the optional system prompt override. *)
196196+197197+val append_system_prompt : t -> string option
198198+(** [append_system_prompt t] returns the optional system prompt append. *)
199199+200200+val permission_mode : t -> Permissions.Mode.t option
201201+(** [permission_mode t] returns the optional permission mode. *)
202202+203203+val permission_callback : t -> Permissions.callback option
204204+(** [permission_callback t] returns the optional permission callback. *)
205205+206206+val model : t -> Model.t option
207207+(** [model t] returns the optional model override. *)
208208+209209+val cwd : t -> Eio.Fs.dir_ty Eio.Path.t option
210210+(** [cwd t] returns the optional working directory. *)
211211+212212+val env : t -> (string * string) list
213213+(** [env t] returns the environment variables. *)
214214+215215+val continue_conversation : t -> bool
216216+(** [continue_conversation t] returns whether to continue an existing conversation. *)
217217+218218+val resume : t -> string option
219219+(** [resume t] returns the optional session ID to resume. *)
220220+221221+val max_turns : t -> int option
222222+(** [max_turns t] returns the optional maximum number of turns. *)
223223+224224+val permission_prompt_tool_name : t -> string option
225225+(** [permission_prompt_tool_name t] returns the optional tool name for permission prompts. *)
226226+227227+val settings : t -> string option
228228+(** [settings t] returns the optional path to settings file. *)
229229+230230+val add_dirs : t -> string list
231231+(** [add_dirs t] returns the list of additional allowed directories. *)
232232+233233+val extra_args : t -> (string * string option) list
234234+(** [extra_args t] returns the additional CLI flags. *)
235235+236236+val debug_stderr : t -> Eio.Flow.sink_ty Eio.Flow.sink option
237237+(** [debug_stderr t] returns the optional debug output sink. *)
238238+239239+val hooks : t -> Hooks.config option
240240+(** [hooks t] returns the optional hooks configuration. *)
241241+242242+val max_budget_usd : t -> float option
243243+(** [max_budget_usd t] returns the optional spending limit in USD. *)
244244+245245+val fallback_model : t -> Model.t option
246246+(** [fallback_model t] returns the optional fallback model. *)
247247+248248+val setting_sources : t -> setting_source list option
249249+(** [setting_sources t] returns the optional list of setting sources to load. *)
250250+251251+val max_buffer_size : t -> int option
252252+(** [max_buffer_size t] returns the optional stdout buffer size in bytes. *)
253253+254254+val user : t -> string option
255255+(** [user t] returns the optional Unix user for subprocess execution. *)
256256+257257+val output_format : t -> Structured_output.t option
258258+(** [output_format t] returns the optional structured output format. *)
259259+260260+val unknown : t -> Jsont.json
261261+(** [unknown t] returns any unknown JSON fields that were preserved during decoding. *)
262262+263263+(** {1 Builders} *)
264264+265265+val with_allowed_tools : string list -> t -> t
266266+(** [with_allowed_tools tools t] sets the allowed tools. *)
267267+268268+val with_disallowed_tools : string list -> t -> t
269269+(** [with_disallowed_tools tools t] sets the disallowed tools. *)
270270+271271+val with_max_thinking_tokens : int -> t -> t
272272+(** [with_max_thinking_tokens tokens t] sets the maximum thinking tokens. *)
273273+274274+val with_system_prompt : string -> t -> t
275275+(** [with_system_prompt prompt t] sets the system prompt override. *)
276276+277277+val with_append_system_prompt : string -> t -> t
278278+(** [with_append_system_prompt prompt t] sets the system prompt append. *)
279279+280280+val with_permission_mode : Permissions.Mode.t -> t -> t
281281+(** [with_permission_mode mode t] sets the permission mode. *)
282282+283283+val with_permission_callback : Permissions.callback -> t -> t
284284+(** [with_permission_callback callback t] sets the permission callback. *)
285285+286286+val with_model : Model.t -> t -> t
287287+(** [with_model model t] sets the model override using a typed Model.t. *)
288288+289289+val with_model_string : string -> t -> t
290290+(** [with_model_string model t] sets the model override from a string.
291291+ The string is parsed using {!Model.of_string}. *)
292292+293293+val with_cwd : Eio.Fs.dir_ty Eio.Path.t -> t -> t
294294+(** [with_cwd cwd t] sets the working directory. *)
295295+296296+val with_env : (string * string) list -> t -> t
297297+(** [with_env env t] sets the environment variables. *)
298298+299299+val with_continue_conversation : bool -> t -> t
300300+(** [with_continue_conversation continue t] sets whether to continue conversation. *)
301301+302302+val with_resume : string -> t -> t
303303+(** [with_resume session_id t] sets the session ID to resume. *)
304304+305305+val with_max_turns : int -> t -> t
306306+(** [with_max_turns turns t] sets the maximum number of turns. *)
307307+308308+val with_permission_prompt_tool_name : string -> t -> t
309309+(** [with_permission_prompt_tool_name tool t] sets the permission prompt tool name. *)
310310+311311+val with_settings : string -> t -> t
312312+(** [with_settings path t] sets the path to settings file. *)
313313+314314+val with_add_dirs : string list -> t -> t
315315+(** [with_add_dirs dirs t] sets the additional allowed directories. *)
316316+317317+val with_extra_args : (string * string option) list -> t -> t
318318+(** [with_extra_args args t] sets the additional CLI flags. *)
319319+320320+val with_debug_stderr : Eio.Flow.sink_ty Eio.Flow.sink -> t -> t
321321+(** [with_debug_stderr sink t] sets the debug output sink. *)
322322+323323+val with_hooks : Hooks.config -> t -> t
324324+(** [with_hooks hooks t] sets the hooks configuration. *)
325325+326326+val with_max_budget_usd : float -> t -> t
327327+(** [with_max_budget_usd budget t] sets the maximum spending limit in USD.
328328+ The session will terminate if this limit is exceeded. *)
329329+330330+val with_fallback_model : Model.t -> t -> t
331331+(** [with_fallback_model model t] sets the fallback model using a typed Model.t. *)
332332+333333+val with_fallback_model_string : string -> t -> t
334334+(** [with_fallback_model_string model t] sets the fallback model from a string.
335335+ The string is parsed using {!Model.of_string}. *)
336336+337337+val with_setting_sources : setting_source list -> t -> t
338338+(** [with_setting_sources sources t] sets which configuration sources to load.
339339+ Use empty list for isolated environments (e.g., CI/CD). *)
340340+341341+val with_no_settings : t -> t
342342+(** [with_no_settings t] disables all settings loading (user, project, local).
343343+ Useful for CI/CD environments where you want isolated, reproducible behavior. *)
344344+345345+val with_max_buffer_size : int -> t -> t
346346+(** [with_max_buffer_size size t] sets the maximum stdout buffer size in bytes. *)
347347+348348+val with_user : string -> t -> t
349349+(** [with_user user t] sets the Unix user for subprocess execution. *)
350350+351351+val with_output_format : Structured_output.t -> t -> t
352352+(** [with_output_format format t] sets the structured output format. *)
353353+354354+(** {1 Serialization} *)
355355+356356+val jsont : t Jsont.t
357357+(** [jsont] is the Jsont codec for Options.t *)
358358+359359+val to_json : t -> Jsont.json
360360+(** [to_json t] converts options to JSON representation. *)
361361+362362+val of_json : Jsont.json -> t
363363+(** [of_json json] parses options from JSON.
364364+ @raise Invalid_argument if the JSON is not valid options. *)
365365+366366+val pp : Format.formatter -> t -> unit
367367+(** [pp fmt t] pretty-prints the options. *)
368368+369369+(** {1 Logging} *)
370370+371371+val log_options : t -> unit
372372+(** [log_options t] logs the current options configuration. *)
+337
lib/permissions.ml
···11+let src = Logs.Src.create "claude.permission" ~doc:"Claude permission system"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(* Helper for pretty-printing JSON *)
55+let pp_json fmt json =
66+ let s = match Jsont_bytesrw.encode_string' Jsont.json json with
77+ | Ok s -> s
88+ | Error err -> Jsont.Error.to_string err
99+ in
1010+ Fmt.string fmt s
1111+1212+(** Permission modes *)
1313+module Mode = struct
1414+ type t =
1515+ | Default
1616+ | Accept_edits
1717+ | Plan
1818+ | Bypass_permissions
1919+2020+ let to_string = function
2121+ | Default -> "default"
2222+ | Accept_edits -> "acceptEdits"
2323+ | Plan -> "plan"
2424+ | Bypass_permissions -> "bypassPermissions"
2525+2626+ let of_string = function
2727+ | "default" -> Default
2828+ | "acceptEdits" -> Accept_edits
2929+ | "plan" -> Plan
3030+ | "bypassPermissions" -> Bypass_permissions
3131+ | s -> raise (Invalid_argument (Printf.sprintf "Mode.of_string: unknown mode %s" s))
3232+3333+ let pp fmt t = Fmt.string fmt (to_string t)
3434+3535+ let jsont : t Jsont.t =
3636+ Jsont.enum [
3737+ "default", Default;
3838+ "acceptEdits", Accept_edits;
3939+ "plan", Plan;
4040+ "bypassPermissions", Bypass_permissions;
4141+ ]
4242+end
4343+4444+(** Permission behaviors *)
4545+module Behavior = struct
4646+ type t = Allow | Deny | Ask
4747+4848+ let to_string = function
4949+ | Allow -> "allow"
5050+ | Deny -> "deny"
5151+ | Ask -> "ask"
5252+5353+ let of_string = function
5454+ | "allow" -> Allow
5555+ | "deny" -> Deny
5656+ | "ask" -> Ask
5757+ | s -> raise (Invalid_argument (Printf.sprintf "Behavior.of_string: unknown behavior %s" s))
5858+5959+ let pp fmt t = Fmt.string fmt (to_string t)
6060+6161+ let jsont : t Jsont.t =
6262+ Jsont.enum [
6363+ "allow", Allow;
6464+ "deny", Deny;
6565+ "ask", Ask;
6666+ ]
6767+end
6868+6969+(** Permission rules *)
7070+module Rule = struct
7171+ type t = {
7272+ tool_name : string;
7373+ rule_content : string option;
7474+ unknown : Unknown.t;
7575+ }
7676+7777+ let create ~tool_name ?rule_content ?(unknown = Unknown.empty) () =
7878+ { tool_name; rule_content; unknown }
7979+ let tool_name t = t.tool_name
8080+ let rule_content t = t.rule_content
8181+ let unknown t = t.unknown
8282+8383+ let jsont : t Jsont.t =
8484+ let make tool_name rule_content unknown = { tool_name; rule_content; unknown } in
8585+ Jsont.Object.map ~kind:"Rule" make
8686+ |> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
8787+ |> Jsont.Object.opt_mem "rule_content" Jsont.string ~enc:rule_content
8888+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
8989+ |> Jsont.Object.finish
9090+9191+ let pp fmt t =
9292+ Fmt.pf fmt "@[<2>Rule@ { tool_name = %S;@ rule_content = %a }@]"
9393+ t.tool_name Fmt.(option string) t.rule_content
9494+end
9595+9696+(** Permission updates *)
9797+module Update = struct
9898+ type destination =
9999+ | User_settings
100100+ | Project_settings
101101+ | Local_settings
102102+ | Session
103103+104104+ let destination_to_string = function
105105+ | User_settings -> "userSettings"
106106+ | Project_settings -> "projectSettings"
107107+ | Local_settings -> "localSettings"
108108+ | Session -> "session"
109109+110110+ let _destination_of_string = function
111111+ | "userSettings" -> User_settings
112112+ | "projectSettings" -> Project_settings
113113+ | "localSettings" -> Local_settings
114114+ | "session" -> Session
115115+ | s -> raise (Invalid_argument (Printf.sprintf "destination_of_string: unknown %s" s))
116116+117117+ let destination_jsont : destination Jsont.t =
118118+ Jsont.enum [
119119+ "userSettings", User_settings;
120120+ "projectSettings", Project_settings;
121121+ "localSettings", Local_settings;
122122+ "session", Session;
123123+ ]
124124+125125+ type update_type =
126126+ | Add_rules
127127+ | Replace_rules
128128+ | Remove_rules
129129+ | Set_mode
130130+ | Add_directories
131131+ | Remove_directories
132132+133133+ let update_type_to_string = function
134134+ | Add_rules -> "addRules"
135135+ | Replace_rules -> "replaceRules"
136136+ | Remove_rules -> "removeRules"
137137+ | Set_mode -> "setMode"
138138+ | Add_directories -> "addDirectories"
139139+ | Remove_directories -> "removeDirectories"
140140+141141+ let _update_type_of_string = function
142142+ | "addRules" -> Add_rules
143143+ | "replaceRules" -> Replace_rules
144144+ | "removeRules" -> Remove_rules
145145+ | "setMode" -> Set_mode
146146+ | "addDirectories" -> Add_directories
147147+ | "removeDirectories" -> Remove_directories
148148+ | s -> raise (Invalid_argument (Printf.sprintf "update_type_of_string: unknown %s" s))
149149+150150+ let update_type_jsont : update_type Jsont.t =
151151+ Jsont.enum [
152152+ "addRules", Add_rules;
153153+ "replaceRules", Replace_rules;
154154+ "removeRules", Remove_rules;
155155+ "setMode", Set_mode;
156156+ "addDirectories", Add_directories;
157157+ "removeDirectories", Remove_directories;
158158+ ]
159159+160160+ type t = {
161161+ update_type : update_type;
162162+ rules : Rule.t list option;
163163+ behavior : Behavior.t option;
164164+ mode : Mode.t option;
165165+ directories : string list option;
166166+ destination : destination option;
167167+ unknown : Unknown.t;
168168+ }
169169+170170+ let create ~update_type ?rules ?behavior ?mode ?directories ?destination ?(unknown = Unknown.empty) () =
171171+ { update_type; rules; behavior; mode; directories; destination; unknown }
172172+173173+ let update_type t = t.update_type
174174+ let rules t = t.rules
175175+ let behavior t = t.behavior
176176+ let mode t = t.mode
177177+ let directories t = t.directories
178178+ let destination t = t.destination
179179+ let unknown t = t.unknown
180180+181181+ let jsont : t Jsont.t =
182182+ let make update_type rules behavior mode directories destination unknown =
183183+ { update_type; rules; behavior; mode; directories; destination; unknown }
184184+ in
185185+ Jsont.Object.map ~kind:"Update" make
186186+ |> Jsont.Object.mem "type" update_type_jsont ~enc:update_type
187187+ |> Jsont.Object.opt_mem "rules" (Jsont.list Rule.jsont) ~enc:rules
188188+ |> Jsont.Object.opt_mem "behavior" Behavior.jsont ~enc:behavior
189189+ |> Jsont.Object.opt_mem "mode" Mode.jsont ~enc:mode
190190+ |> Jsont.Object.opt_mem "directories" (Jsont.list Jsont.string) ~enc:directories
191191+ |> Jsont.Object.opt_mem "destination" destination_jsont ~enc:destination
192192+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
193193+ |> Jsont.Object.finish
194194+195195+ let pp fmt t =
196196+ Fmt.pf fmt "@[<2>Update@ { type = %s;@ rules = %a;@ behavior = %a;@ \
197197+ mode = %a;@ directories = %a;@ destination = %a }@]"
198198+ (update_type_to_string t.update_type)
199199+ Fmt.(option (list Rule.pp)) t.rules
200200+ Fmt.(option Behavior.pp) t.behavior
201201+ Fmt.(option Mode.pp) t.mode
202202+ Fmt.(option (list string)) t.directories
203203+ Fmt.(option (fun fmt d -> Fmt.string fmt (destination_to_string d))) t.destination
204204+end
205205+206206+(** Permission context for callbacks *)
207207+module Context = struct
208208+ type t = {
209209+ suggestions : Update.t list;
210210+ unknown : Unknown.t;
211211+ }
212212+213213+ let create ?(suggestions = []) ?(unknown = Unknown.empty) () = { suggestions; unknown }
214214+ let suggestions t = t.suggestions
215215+ let unknown t = t.unknown
216216+217217+ let jsont : t Jsont.t =
218218+ let make suggestions unknown = { suggestions; unknown } in
219219+ Jsont.Object.map ~kind:"Context" make
220220+ |> Jsont.Object.mem "suggestions" (Jsont.list Update.jsont) ~enc:suggestions ~dec_absent:[]
221221+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
222222+ |> Jsont.Object.finish
223223+224224+ let pp fmt t =
225225+ Fmt.pf fmt "@[<2>Context@ { suggestions = @[<v>%a@] }@]"
226226+ Fmt.(list ~sep:(any "@,") Update.pp) t.suggestions
227227+end
228228+229229+(** Permission results *)
230230+module Result = struct
231231+ type t =
232232+ | Allow of {
233233+ updated_input : Jsont.json option;
234234+ updated_permissions : Update.t list option;
235235+ unknown : Unknown.t;
236236+ }
237237+ | Deny of {
238238+ message : string;
239239+ interrupt : bool;
240240+ unknown : Unknown.t;
241241+ }
242242+243243+ let allow ?updated_input ?updated_permissions ?(unknown = Unknown.empty) () =
244244+ Allow { updated_input; updated_permissions; unknown }
245245+246246+ let deny ~message ~interrupt ?(unknown = Unknown.empty) () =
247247+ Deny { message; interrupt; unknown }
248248+249249+ let jsont : t Jsont.t =
250250+ let allow_record =
251251+ let make updated_input updated_permissions unknown =
252252+ Allow { updated_input; updated_permissions; unknown }
253253+ in
254254+ Jsont.Object.map ~kind:"AllowRecord" make
255255+ |> Jsont.Object.opt_mem "updated_input" Jsont.json ~enc:(function
256256+ | Allow { updated_input; _ } -> updated_input
257257+ | _ -> None)
258258+ |> Jsont.Object.opt_mem "updated_permissions" (Jsont.list Update.jsont) ~enc:(function
259259+ | Allow { updated_permissions; _ } -> updated_permissions
260260+ | _ -> None)
261261+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(function
262262+ | Allow { unknown; _ } -> unknown
263263+ | _ -> Unknown.empty)
264264+ |> Jsont.Object.finish
265265+ in
266266+ let deny_record =
267267+ let make message interrupt unknown =
268268+ Deny { message; interrupt; unknown }
269269+ in
270270+ Jsont.Object.map ~kind:"DenyRecord" make
271271+ |> Jsont.Object.mem "message" Jsont.string ~enc:(function
272272+ | Deny { message; _ } -> message
273273+ | _ -> "")
274274+ |> Jsont.Object.mem "interrupt" Jsont.bool ~enc:(function
275275+ | Deny { interrupt; _ } -> interrupt
276276+ | _ -> false)
277277+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(function
278278+ | Deny { unknown; _ } -> unknown
279279+ | _ -> Unknown.empty)
280280+ |> Jsont.Object.finish
281281+ in
282282+ let case_allow = Jsont.Object.Case.map "allow" allow_record ~dec:(fun v -> v) in
283283+ let case_deny = Jsont.Object.Case.map "deny" deny_record ~dec:(fun v -> v) in
284284+285285+ let enc_case = function
286286+ | Allow _ as v -> Jsont.Object.Case.value case_allow v
287287+ | Deny _ as v -> Jsont.Object.Case.value case_deny v
288288+ in
289289+290290+ let cases = Jsont.Object.Case.[
291291+ make case_allow;
292292+ make case_deny
293293+ ] in
294294+295295+ Jsont.Object.map ~kind:"Result" Fun.id
296296+ |> Jsont.Object.case_mem "behavior" Jsont.string ~enc:Fun.id ~enc_case cases
297297+ ~tag_to_string:Fun.id ~tag_compare:String.compare
298298+ |> Jsont.Object.finish
299299+300300+ let pp fmt = function
301301+ | Allow { updated_input; updated_permissions; _ } ->
302302+ Fmt.pf fmt "@[<2>Allow@ { updated_input = %a;@ updated_permissions = %a }@]"
303303+ Fmt.(option pp_json) updated_input
304304+ Fmt.(option (list Update.pp)) updated_permissions
305305+ | Deny { message; interrupt; _ } ->
306306+ Fmt.pf fmt "@[<2>Deny@ { message = %S;@ interrupt = %b }@]" message interrupt
307307+end
308308+309309+(** Permission callback type *)
310310+type callback =
311311+ tool_name:string ->
312312+ input:Jsont.json ->
313313+ context:Context.t ->
314314+ Result.t
315315+316316+(** Default callbacks *)
317317+let default_allow_callback ~tool_name:_ ~input:_ ~context:_ =
318318+ Result.allow ()
319319+320320+let discovery_callback log ~tool_name:_ ~input:_ ~context =
321321+ List.iter (fun update ->
322322+ match Update.rules update with
323323+ | Some rules ->
324324+ List.iter (fun rule ->
325325+ log := rule :: !log
326326+ ) rules
327327+ | None -> ()
328328+ ) (Context.suggestions context);
329329+ Result.allow ()
330330+331331+(** Logging *)
332332+let log_permission_check ~tool_name ~result =
333333+ match result with
334334+ | Result.Allow _ ->
335335+ Log.info (fun m -> m "Permission granted for tool: %s" tool_name)
336336+ | Result.Deny { message; _ } ->
337337+ Log.warn (fun m -> m "Permission denied for tool %s: %s" tool_name message)
+253
lib/permissions.mli
···11+(** Permission system for Claude tool invocations.
22+33+ This module provides a permission system for controlling
44+ which tools Claude can invoke and how they can be used. It includes
55+ support for permission modes, rules, updates, and callbacks. *)
66+77+(** The log source for permission operations *)
88+val src : Logs.Src.t
99+1010+(** {1 Permission Modes} *)
1111+1212+module Mode : sig
1313+ (** Permission modes control the overall behavior of the permission system. *)
1414+1515+ type t =
1616+ | Default (** Standard permission mode with normal checks *)
1717+ | Accept_edits (** Automatically accept file edits *)
1818+ | Plan (** Planning mode with restricted execution *)
1919+ | Bypass_permissions (** Bypass all permission checks *)
2020+ (** The type of permission modes. *)
2121+2222+ val to_string : t -> string
2323+ (** [to_string t] converts a mode to its string representation. *)
2424+2525+ val of_string : string -> t
2626+ (** [of_string s] parses a mode from its string representation.
2727+ @raise Invalid_argument if the string is not a valid mode. *)
2828+2929+ val pp : Format.formatter -> t -> unit
3030+ (** [pp fmt t] pretty-prints the mode. *)
3131+3232+ val jsont : t Jsont.t
3333+ (** [jsont] is the Jsont codec for permission modes. *)
3434+end
3535+3636+(** {1 Permission Behaviors} *)
3737+3838+module Behavior : sig
3939+ (** Behaviors determine how permission requests are handled. *)
4040+4141+ type t =
4242+ | Allow (** Allow the operation *)
4343+ | Deny (** Deny the operation *)
4444+ | Ask (** Ask the user for permission *)
4545+ (** The type of permission behaviors. *)
4646+4747+ val to_string : t -> string
4848+ (** [to_string t] converts a behavior to its string representation. *)
4949+5050+ val of_string : string -> t
5151+ (** [of_string s] parses a behavior from its string representation.
5252+ @raise Invalid_argument if the string is not a valid behavior. *)
5353+5454+ val pp : Format.formatter -> t -> unit
5555+ (** [pp fmt t] pretty-prints the behavior. *)
5656+5757+ val jsont : t Jsont.t
5858+ (** [jsont] is the Jsont codec for permission behaviors. *)
5959+end
6060+6161+(** {1 Permission Rules} *)
6262+6363+module Rule : sig
6464+ (** Rules define specific permissions for tools. *)
6565+6666+ type t = {
6767+ tool_name : string; (** Name of the tool *)
6868+ rule_content : string option; (** Optional rule specification *)
6969+ unknown : Unknown.t; (** Unknown fields *)
7070+ }
7171+ (** The type of permission rules. *)
7272+7373+ val create : tool_name:string -> ?rule_content:string -> ?unknown:Unknown.t -> unit -> t
7474+ (** [create ~tool_name ?rule_content ?unknown ()] creates a new rule.
7575+ @param tool_name The name of the tool this rule applies to
7676+ @param rule_content Optional rule specification or pattern
7777+ @param unknown Optional unknown fields to preserve *)
7878+7979+ val tool_name : t -> string
8080+ (** [tool_name t] returns the tool name. *)
8181+8282+ val rule_content : t -> string option
8383+ (** [rule_content t] returns the optional rule content. *)
8484+8585+ val unknown : t -> Unknown.t
8686+ (** [unknown t] returns the unknown fields. *)
8787+8888+ val pp : Format.formatter -> t -> unit
8989+ (** [pp fmt t] pretty-prints the rule. *)
9090+9191+ val jsont : t Jsont.t
9292+ (** [jsont] is the Jsont codec for permission rules. *)
9393+end
9494+9595+(** {1 Permission Updates} *)
9696+9797+module Update : sig
9898+ (** Updates modify permission settings. *)
9999+100100+ type destination =
101101+ | User_settings (** Apply to user settings *)
102102+ | Project_settings (** Apply to project settings *)
103103+ | Local_settings (** Apply to local settings *)
104104+ | Session (** Apply to current session only *)
105105+ (** The destination for permission updates. *)
106106+107107+ type update_type =
108108+ | Add_rules (** Add new rules *)
109109+ | Replace_rules (** Replace existing rules *)
110110+ | Remove_rules (** Remove rules *)
111111+ | Set_mode (** Set permission mode *)
112112+ | Add_directories (** Add allowed directories *)
113113+ | Remove_directories (** Remove allowed directories *)
114114+ (** The type of permission update. *)
115115+116116+ type t
117117+ (** The type of permission updates. *)
118118+119119+ val create :
120120+ update_type:update_type ->
121121+ ?rules:Rule.t list ->
122122+ ?behavior:Behavior.t ->
123123+ ?mode:Mode.t ->
124124+ ?directories:string list ->
125125+ ?destination:destination ->
126126+ ?unknown:Unknown.t ->
127127+ unit -> t
128128+ (** [create ~update_type ?rules ?behavior ?mode ?directories ?destination ?unknown ()]
129129+ creates a new permission update.
130130+ @param update_type The type of update to perform
131131+ @param rules Optional list of rules to add/remove/replace
132132+ @param behavior Optional behavior to set
133133+ @param mode Optional permission mode to set
134134+ @param directories Optional directories to add/remove
135135+ @param destination Optional destination for the update
136136+ @param unknown Optional unknown fields to preserve *)
137137+138138+ val update_type : t -> update_type
139139+ (** [update_type t] returns the update type. *)
140140+141141+ val rules : t -> Rule.t list option
142142+ (** [rules t] returns the optional list of rules. *)
143143+144144+ val behavior : t -> Behavior.t option
145145+ (** [behavior t] returns the optional behavior. *)
146146+147147+ val mode : t -> Mode.t option
148148+ (** [mode t] returns the optional mode. *)
149149+150150+ val directories : t -> string list option
151151+ (** [directories t] returns the optional list of directories. *)
152152+153153+ val destination : t -> destination option
154154+ (** [destination t] returns the optional destination. *)
155155+156156+ val unknown : t -> Unknown.t
157157+ (** [unknown t] returns the unknown fields. *)
158158+159159+ val pp : Format.formatter -> t -> unit
160160+ (** [pp fmt t] pretty-prints the update. *)
161161+162162+ val jsont : t Jsont.t
163163+ (** [jsont] is the Jsont codec for permission updates. *)
164164+end
165165+166166+(** {1 Permission Context} *)
167167+168168+module Context : sig
169169+ (** Context provided to permission callbacks. *)
170170+171171+ type t = {
172172+ suggestions : Update.t list; (** Suggested permission updates *)
173173+ unknown : Unknown.t; (** Unknown fields *)
174174+ }
175175+ (** The type of permission context. *)
176176+177177+ val create : ?suggestions:Update.t list -> ?unknown:Unknown.t -> unit -> t
178178+ (** [create ?suggestions ?unknown ()] creates a new context.
179179+ @param suggestions Optional list of suggested permission updates
180180+ @param unknown Optional unknown fields to preserve *)
181181+182182+ val suggestions : t -> Update.t list
183183+ (** [suggestions t] returns the list of suggested updates. *)
184184+185185+ val unknown : t -> Unknown.t
186186+ (** [unknown t] returns the unknown fields. *)
187187+188188+ val pp : Format.formatter -> t -> unit
189189+ (** [pp fmt t] pretty-prints the context. *)
190190+191191+ val jsont : t Jsont.t
192192+ (** [jsont] is the Jsont codec for permission context. *)
193193+end
194194+195195+(** {1 Permission Results} *)
196196+197197+module Result : sig
198198+ (** Results of permission checks. *)
199199+200200+ type t =
201201+ | Allow of {
202202+ updated_input : Jsont.json option; (** Modified tool input *)
203203+ updated_permissions : Update.t list option; (** Permission updates to apply *)
204204+ unknown : Unknown.t; (** Unknown fields *)
205205+ }
206206+ | Deny of {
207207+ message : string; (** Reason for denial *)
208208+ interrupt : bool; (** Whether to interrupt execution *)
209209+ unknown : Unknown.t; (** Unknown fields *)
210210+ }
211211+ (** The type of permission results. *)
212212+213213+ val allow : ?updated_input:Jsont.json -> ?updated_permissions:Update.t list -> ?unknown:Unknown.t -> unit -> t
214214+ (** [allow ?updated_input ?updated_permissions ?unknown ()] creates an allow result.
215215+ @param updated_input Optional modified tool input
216216+ @param updated_permissions Optional permission updates to apply
217217+ @param unknown Optional unknown fields to preserve *)
218218+219219+ val deny : message:string -> interrupt:bool -> ?unknown:Unknown.t -> unit -> t
220220+ (** [deny ~message ~interrupt ?unknown ()] creates a deny result.
221221+ @param message The reason for denying permission
222222+ @param interrupt Whether to interrupt further execution
223223+ @param unknown Optional unknown fields to preserve *)
224224+225225+ val pp : Format.formatter -> t -> unit
226226+ (** [pp fmt t] pretty-prints the result. *)
227227+228228+ val jsont : t Jsont.t
229229+ (** [jsont] is the Jsont codec for permission results. *)
230230+end
231231+232232+(** {1 Permission Callbacks} *)
233233+234234+type callback =
235235+ tool_name:string ->
236236+ input:Jsont.json ->
237237+ context:Context.t ->
238238+ Result.t
239239+(** The type of permission callbacks. Callbacks are invoked when Claude
240240+ attempts to use a tool, allowing custom permission logic. *)
241241+242242+val default_allow_callback : callback
243243+(** [default_allow_callback] always allows tool invocations. *)
244244+245245+val discovery_callback : Rule.t list ref -> callback
246246+(** [discovery_callback log] creates a callback that collects suggested
247247+ rules into the provided reference. Useful for discovering what
248248+ permissions an operation requires. *)
249249+250250+(** {1 Logging} *)
251251+252252+val log_permission_check : tool_name:string -> result:Result.t -> unit
253253+(** [log_permission_check ~tool_name ~result] logs a permission check result. *)
+454
lib/sdk_control.ml
···11+let src = Logs.Src.create "claude.sdk_control" ~doc:"Claude SDK control protocol"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+module Request = struct
55+ type interrupt = {
66+ subtype : [`Interrupt];
77+ unknown : Unknown.t;
88+ }
99+1010+ type permission = {
1111+ subtype : [`Can_use_tool];
1212+ tool_name : string;
1313+ input : Jsont.json;
1414+ permission_suggestions : Permissions.Update.t list option;
1515+ blocked_path : string option;
1616+ unknown : Unknown.t;
1717+ }
1818+1919+ type initialize = {
2020+ subtype : [`Initialize];
2121+ hooks : (string * Jsont.json) list option;
2222+ unknown : Unknown.t;
2323+ }
2424+2525+ type set_permission_mode = {
2626+ subtype : [`Set_permission_mode];
2727+ mode : Permissions.Mode.t;
2828+ unknown : Unknown.t;
2929+ }
3030+3131+ type hook_callback = {
3232+ subtype : [`Hook_callback];
3333+ callback_id : string;
3434+ input : Jsont.json;
3535+ tool_use_id : string option;
3636+ unknown : Unknown.t;
3737+ }
3838+3939+ type mcp_message = {
4040+ subtype : [`Mcp_message];
4141+ server_name : string;
4242+ message : Jsont.json;
4343+ unknown : Unknown.t;
4444+ }
4545+4646+ type set_model = {
4747+ subtype : [`Set_model];
4848+ model : string;
4949+ unknown : Unknown.t;
5050+ }
5151+5252+ type get_server_info = {
5353+ subtype : [`Get_server_info];
5454+ unknown : Unknown.t;
5555+ }
5656+5757+ type t =
5858+ | Interrupt of interrupt
5959+ | Permission of permission
6060+ | Initialize of initialize
6161+ | Set_permission_mode of set_permission_mode
6262+ | Hook_callback of hook_callback
6363+ | Mcp_message of mcp_message
6464+ | Set_model of set_model
6565+ | Get_server_info of get_server_info
6666+6767+ let interrupt ?(unknown = Unknown.empty) () =
6868+ Interrupt { subtype = `Interrupt; unknown }
6969+7070+ let permission ~tool_name ~input ?permission_suggestions ?blocked_path ?(unknown = Unknown.empty) () =
7171+ Permission {
7272+ subtype = `Can_use_tool;
7373+ tool_name;
7474+ input;
7575+ permission_suggestions;
7676+ blocked_path;
7777+ unknown;
7878+ }
7979+8080+ let initialize ?hooks ?(unknown = Unknown.empty) () =
8181+ Initialize { subtype = `Initialize; hooks; unknown }
8282+8383+ let set_permission_mode ~mode ?(unknown = Unknown.empty) () =
8484+ Set_permission_mode { subtype = `Set_permission_mode; mode; unknown }
8585+8686+ let hook_callback ~callback_id ~input ?tool_use_id ?(unknown = Unknown.empty) () =
8787+ Hook_callback {
8888+ subtype = `Hook_callback;
8989+ callback_id;
9090+ input;
9191+ tool_use_id;
9292+ unknown;
9393+ }
9494+9595+ let mcp_message ~server_name ~message ?(unknown = Unknown.empty) () =
9696+ Mcp_message {
9797+ subtype = `Mcp_message;
9898+ server_name;
9999+ message;
100100+ unknown;
101101+ }
102102+103103+ let set_model ~model ?(unknown = Unknown.empty) () =
104104+ Set_model { subtype = `Set_model; model; unknown }
105105+106106+ let get_server_info ?(unknown = Unknown.empty) () =
107107+ Get_server_info { subtype = `Get_server_info; unknown }
108108+109109+ (* Individual record codecs *)
110110+ let interrupt_jsont : interrupt Jsont.t =
111111+ let make (unknown : Unknown.t) : interrupt = { subtype = `Interrupt; unknown } in
112112+ Jsont.Object.map ~kind:"Interrupt" make
113113+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : interrupt) -> r.unknown)
114114+ |> Jsont.Object.finish
115115+116116+ let permission_jsont : permission Jsont.t =
117117+ let make tool_name input permission_suggestions blocked_path (unknown : Unknown.t) : permission =
118118+ { subtype = `Can_use_tool; tool_name; input; permission_suggestions; blocked_path; unknown }
119119+ in
120120+ Jsont.Object.map ~kind:"Permission" make
121121+ |> Jsont.Object.mem "tool_name" Jsont.string ~enc:(fun (r : permission) -> r.tool_name)
122122+ |> Jsont.Object.mem "input" Jsont.json ~enc:(fun (r : permission) -> r.input)
123123+ |> Jsont.Object.opt_mem "permission_suggestions" (Jsont.list Permissions.Update.jsont) ~enc:(fun (r : permission) -> r.permission_suggestions)
124124+ |> Jsont.Object.opt_mem "blocked_path" Jsont.string ~enc:(fun (r : permission) -> r.blocked_path)
125125+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : permission) -> r.unknown)
126126+ |> Jsont.Object.finish
127127+128128+ let initialize_jsont : initialize Jsont.t =
129129+ (* The hooks field is an object with string keys and json values *)
130130+ let hooks_map_jsont = Jsont.Object.as_string_map Jsont.json in
131131+ let module StringMap = Map.Make(String) in
132132+ let hooks_jsont = Jsont.map
133133+ ~dec:(fun m -> StringMap.bindings m)
134134+ ~enc:(fun l -> StringMap.of_seq (List.to_seq l))
135135+ hooks_map_jsont
136136+ in
137137+ let make hooks (unknown : Unknown.t) : initialize = { subtype = `Initialize; hooks; unknown } in
138138+ Jsont.Object.map ~kind:"Initialize" make
139139+ |> Jsont.Object.opt_mem "hooks" hooks_jsont ~enc:(fun (r : initialize) -> r.hooks)
140140+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : initialize) -> r.unknown)
141141+ |> Jsont.Object.finish
142142+143143+ let set_permission_mode_jsont : set_permission_mode Jsont.t =
144144+ let make mode (unknown : Unknown.t) : set_permission_mode = { subtype = `Set_permission_mode; mode; unknown } in
145145+ Jsont.Object.map ~kind:"SetPermissionMode" make
146146+ |> Jsont.Object.mem "mode" Permissions.Mode.jsont ~enc:(fun (r : set_permission_mode) -> r.mode)
147147+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : set_permission_mode) -> r.unknown)
148148+ |> Jsont.Object.finish
149149+150150+ let hook_callback_jsont : hook_callback Jsont.t =
151151+ let make callback_id input tool_use_id (unknown : Unknown.t) : hook_callback =
152152+ { subtype = `Hook_callback; callback_id; input; tool_use_id; unknown }
153153+ in
154154+ Jsont.Object.map ~kind:"HookCallback" make
155155+ |> Jsont.Object.mem "callback_id" Jsont.string ~enc:(fun (r : hook_callback) -> r.callback_id)
156156+ |> Jsont.Object.mem "input" Jsont.json ~enc:(fun (r : hook_callback) -> r.input)
157157+ |> Jsont.Object.opt_mem "tool_use_id" Jsont.string ~enc:(fun (r : hook_callback) -> r.tool_use_id)
158158+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : hook_callback) -> r.unknown)
159159+ |> Jsont.Object.finish
160160+161161+ let mcp_message_jsont : mcp_message Jsont.t =
162162+ let make server_name message (unknown : Unknown.t) : mcp_message =
163163+ { subtype = `Mcp_message; server_name; message; unknown }
164164+ in
165165+ Jsont.Object.map ~kind:"McpMessage" make
166166+ |> Jsont.Object.mem "server_name" Jsont.string ~enc:(fun (r : mcp_message) -> r.server_name)
167167+ |> Jsont.Object.mem "message" Jsont.json ~enc:(fun (r : mcp_message) -> r.message)
168168+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : mcp_message) -> r.unknown)
169169+ |> Jsont.Object.finish
170170+171171+ let set_model_jsont : set_model Jsont.t =
172172+ let make model (unknown : Unknown.t) : set_model = { subtype = `Set_model; model; unknown } in
173173+ Jsont.Object.map ~kind:"SetModel" make
174174+ |> Jsont.Object.mem "model" Jsont.string ~enc:(fun (r : set_model) -> r.model)
175175+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : set_model) -> r.unknown)
176176+ |> Jsont.Object.finish
177177+178178+ let get_server_info_jsont : get_server_info Jsont.t =
179179+ let make (unknown : Unknown.t) : get_server_info = { subtype = `Get_server_info; unknown } in
180180+ Jsont.Object.map ~kind:"GetServerInfo" make
181181+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : get_server_info) -> r.unknown)
182182+ |> Jsont.Object.finish
183183+184184+ (* Main variant codec using subtype discriminator *)
185185+ let jsont : t Jsont.t =
186186+ let case_interrupt = Jsont.Object.Case.map "interrupt" interrupt_jsont ~dec:(fun v -> Interrupt v) in
187187+ let case_permission = Jsont.Object.Case.map "can_use_tool" permission_jsont ~dec:(fun v -> Permission v) in
188188+ let case_initialize = Jsont.Object.Case.map "initialize" initialize_jsont ~dec:(fun v -> Initialize v) in
189189+ let case_set_permission_mode = Jsont.Object.Case.map "set_permission_mode" set_permission_mode_jsont ~dec:(fun v -> Set_permission_mode v) in
190190+ let case_hook_callback = Jsont.Object.Case.map "hook_callback" hook_callback_jsont ~dec:(fun v -> Hook_callback v) in
191191+ let case_mcp_message = Jsont.Object.Case.map "mcp_message" mcp_message_jsont ~dec:(fun v -> Mcp_message v) in
192192+ let case_set_model = Jsont.Object.Case.map "set_model" set_model_jsont ~dec:(fun v -> Set_model v) in
193193+ let case_get_server_info = Jsont.Object.Case.map "get_server_info" get_server_info_jsont ~dec:(fun v -> Get_server_info v) in
194194+195195+ let enc_case = function
196196+ | Interrupt v -> Jsont.Object.Case.value case_interrupt v
197197+ | Permission v -> Jsont.Object.Case.value case_permission v
198198+ | Initialize v -> Jsont.Object.Case.value case_initialize v
199199+ | Set_permission_mode v -> Jsont.Object.Case.value case_set_permission_mode v
200200+ | Hook_callback v -> Jsont.Object.Case.value case_hook_callback v
201201+ | Mcp_message v -> Jsont.Object.Case.value case_mcp_message v
202202+ | Set_model v -> Jsont.Object.Case.value case_set_model v
203203+ | Get_server_info v -> Jsont.Object.Case.value case_get_server_info v
204204+ in
205205+206206+ let cases = Jsont.Object.Case.[
207207+ make case_interrupt;
208208+ make case_permission;
209209+ make case_initialize;
210210+ make case_set_permission_mode;
211211+ make case_hook_callback;
212212+ make case_mcp_message;
213213+ make case_set_model;
214214+ make case_get_server_info;
215215+ ] in
216216+217217+ Jsont.Object.map ~kind:"Request" Fun.id
218218+ |> Jsont.Object.case_mem "subtype" Jsont.string ~enc:Fun.id ~enc_case cases
219219+ ~tag_to_string:Fun.id ~tag_compare:String.compare
220220+ |> Jsont.Object.finish
221221+222222+ let pp fmt = function
223223+ | Interrupt _ ->
224224+ Fmt.pf fmt "@[<2>Interrupt@]"
225225+ | Permission p ->
226226+ Fmt.pf fmt "@[<2>Permission@ { tool = %S;@ blocked_path = %a }@]"
227227+ p.tool_name Fmt.(option string) p.blocked_path
228228+ | Initialize i ->
229229+ Fmt.pf fmt "@[<2>Initialize@ { hooks = %s }@]"
230230+ (if Option.is_some i.hooks then "present" else "none")
231231+ | Set_permission_mode s ->
232232+ Fmt.pf fmt "@[<2>SetPermissionMode@ { mode = %a }@]"
233233+ Permissions.Mode.pp s.mode
234234+ | Hook_callback h ->
235235+ Fmt.pf fmt "@[<2>HookCallback@ { id = %S;@ tool_use_id = %a }@]"
236236+ h.callback_id Fmt.(option string) h.tool_use_id
237237+ | Mcp_message m ->
238238+ Fmt.pf fmt "@[<2>McpMessage@ { server = %S }@]"
239239+ m.server_name
240240+ | Set_model s ->
241241+ Fmt.pf fmt "@[<2>SetModel@ { model = %S }@]" s.model
242242+ | Get_server_info _ ->
243243+ Fmt.pf fmt "@[<2>GetServerInfo@]"
244244+end
245245+246246+module Response = struct
247247+ type success = {
248248+ subtype : [`Success];
249249+ request_id : string;
250250+ response : Jsont.json option;
251251+ unknown : Unknown.t;
252252+ }
253253+254254+ type error = {
255255+ subtype : [`Error];
256256+ request_id : string;
257257+ error : string;
258258+ unknown : Unknown.t;
259259+ }
260260+261261+ type t =
262262+ | Success of success
263263+ | Error of error
264264+265265+ let success ~request_id ?response ?(unknown = Unknown.empty) () =
266266+ Success {
267267+ subtype = `Success;
268268+ request_id;
269269+ response;
270270+ unknown;
271271+ }
272272+273273+ let error ~request_id ~error ?(unknown = Unknown.empty) () =
274274+ Error {
275275+ subtype = `Error;
276276+ request_id;
277277+ error;
278278+ unknown;
279279+ }
280280+281281+ (* Individual record codecs *)
282282+ let success_jsont : success Jsont.t =
283283+ let make request_id response (unknown : Unknown.t) : success =
284284+ { subtype = `Success; request_id; response; unknown }
285285+ in
286286+ Jsont.Object.map ~kind:"Success" make
287287+ |> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : success) -> r.request_id)
288288+ |> Jsont.Object.opt_mem "response" Jsont.json ~enc:(fun (r : success) -> r.response)
289289+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : success) -> r.unknown)
290290+ |> Jsont.Object.finish
291291+292292+ let error_jsont : error Jsont.t =
293293+ let make request_id error (unknown : Unknown.t) : error =
294294+ { subtype = `Error; request_id; error; unknown }
295295+ in
296296+ Jsont.Object.map ~kind:"Error" make
297297+ |> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : error) -> r.request_id)
298298+ |> Jsont.Object.mem "error" Jsont.string ~enc:(fun (r : error) -> r.error)
299299+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : error) -> r.unknown)
300300+ |> Jsont.Object.finish
301301+302302+ (* Main variant codec using subtype discriminator *)
303303+ let jsont : t Jsont.t =
304304+ let case_success = Jsont.Object.Case.map "success" success_jsont ~dec:(fun v -> Success v) in
305305+ let case_error = Jsont.Object.Case.map "error" error_jsont ~dec:(fun v -> Error v) in
306306+307307+ let enc_case = function
308308+ | Success v -> Jsont.Object.Case.value case_success v
309309+ | Error v -> Jsont.Object.Case.value case_error v
310310+ in
311311+312312+ let cases = Jsont.Object.Case.[
313313+ make case_success;
314314+ make case_error;
315315+ ] in
316316+317317+ Jsont.Object.map ~kind:"Response" Fun.id
318318+ |> Jsont.Object.case_mem "subtype" Jsont.string ~enc:Fun.id ~enc_case cases
319319+ ~tag_to_string:Fun.id ~tag_compare:String.compare
320320+ |> Jsont.Object.finish
321321+322322+ let pp fmt = function
323323+ | Success s ->
324324+ Fmt.pf fmt "@[<2>Success@ { request_id = %S;@ response = %s }@]"
325325+ s.request_id (if Option.is_some s.response then "present" else "none")
326326+ | Error e ->
327327+ Fmt.pf fmt "@[<2>Error@ { request_id = %S;@ error = %S }@]"
328328+ e.request_id e.error
329329+end
330330+331331+type control_request = {
332332+ type_ : [`Control_request];
333333+ request_id : string;
334334+ request : Request.t;
335335+ unknown : Unknown.t;
336336+}
337337+338338+type control_response = {
339339+ type_ : [`Control_response];
340340+ response : Response.t;
341341+ unknown : Unknown.t;
342342+}
343343+344344+type t =
345345+ | Request of control_request
346346+ | Response of control_response
347347+348348+let create_request ~request_id ~request ?(unknown = Unknown.empty) () =
349349+ Request {
350350+ type_ = `Control_request;
351351+ request_id;
352352+ request;
353353+ unknown;
354354+ }
355355+356356+let create_response ~response ?(unknown = Unknown.empty) () =
357357+ Response {
358358+ type_ = `Control_response;
359359+ response;
360360+ unknown;
361361+ }
362362+363363+(* Individual record codecs *)
364364+let control_request_jsont : control_request Jsont.t =
365365+ let make request_id request (unknown : Unknown.t) : control_request =
366366+ { type_ = `Control_request; request_id; request; unknown }
367367+ in
368368+ Jsont.Object.map ~kind:"ControlRequest" make
369369+ |> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : control_request) -> r.request_id)
370370+ |> Jsont.Object.mem "request" Request.jsont ~enc:(fun (r : control_request) -> r.request)
371371+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : control_request) -> r.unknown)
372372+ |> Jsont.Object.finish
373373+374374+let control_response_jsont : control_response Jsont.t =
375375+ let make response (unknown : Unknown.t) : control_response =
376376+ { type_ = `Control_response; response; unknown }
377377+ in
378378+ Jsont.Object.map ~kind:"ControlResponse" make
379379+ |> Jsont.Object.mem "response" Response.jsont ~enc:(fun (r : control_response) -> r.response)
380380+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : control_response) -> r.unknown)
381381+ |> Jsont.Object.finish
382382+383383+(* Main variant codec using type discriminator *)
384384+let jsont : t Jsont.t =
385385+ let case_request = Jsont.Object.Case.map "control_request" control_request_jsont ~dec:(fun v -> Request v) in
386386+ let case_response = Jsont.Object.Case.map "control_response" control_response_jsont ~dec:(fun v -> Response v) in
387387+388388+ let enc_case = function
389389+ | Request v -> Jsont.Object.Case.value case_request v
390390+ | Response v -> Jsont.Object.Case.value case_response v
391391+ in
392392+393393+ let cases = Jsont.Object.Case.[
394394+ make case_request;
395395+ make case_response;
396396+ ] in
397397+398398+ Jsont.Object.map ~kind:"Control" Fun.id
399399+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
400400+ ~tag_to_string:Fun.id ~tag_compare:String.compare
401401+ |> Jsont.Object.finish
402402+403403+let pp fmt = function
404404+ | Request r ->
405405+ Fmt.pf fmt "@[<2>ControlRequest@ { id = %S;@ request = %a }@]"
406406+ r.request_id Request.pp r.request
407407+ | Response r ->
408408+ Fmt.pf fmt "@[<2>ControlResponse@ { %a }@]"
409409+ Response.pp r.response
410410+411411+let log_request req =
412412+ Log.debug (fun m -> m "SDK control request: %a" Request.pp req)
413413+414414+let log_response resp =
415415+ Log.debug (fun m -> m "SDK control response: %a" Response.pp resp)
416416+417417+(** Server information *)
418418+module Server_info = struct
419419+ type t = {
420420+ version : string;
421421+ capabilities : string list;
422422+ commands : string list;
423423+ output_styles : string list;
424424+ unknown : Unknown.t;
425425+ }
426426+427427+ let create ~version ~capabilities ~commands ~output_styles ?(unknown = Unknown.empty) () =
428428+ { version; capabilities; commands; output_styles; unknown }
429429+430430+ let version t = t.version
431431+ let capabilities t = t.capabilities
432432+ let commands t = t.commands
433433+ let output_styles t = t.output_styles
434434+ let unknown t = t.unknown
435435+436436+ let jsont : t Jsont.t =
437437+ let make version capabilities commands output_styles (unknown : Unknown.t) : t =
438438+ { version; capabilities; commands; output_styles; unknown }
439439+ in
440440+ Jsont.Object.map ~kind:"ServerInfo" make
441441+ |> Jsont.Object.mem "version" Jsont.string ~enc:(fun (r : t) -> r.version)
442442+ |> Jsont.Object.mem "capabilities" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.capabilities) ~dec_absent:[]
443443+ |> Jsont.Object.mem "commands" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.commands) ~dec_absent:[]
444444+ |> Jsont.Object.mem "outputStyles" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.output_styles) ~dec_absent:[]
445445+ |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : t) -> r.unknown)
446446+ |> Jsont.Object.finish
447447+448448+ let pp fmt t =
449449+ Fmt.pf fmt "@[<2>ServerInfo@ { version = %S;@ capabilities = [%a];@ commands = [%a];@ output_styles = [%a] }@]"
450450+ t.version
451451+ Fmt.(list ~sep:(any ", ") (quote string)) t.capabilities
452452+ Fmt.(list ~sep:(any ", ") (quote string)) t.commands
453453+ Fmt.(list ~sep:(any ", ") (quote string)) t.output_styles
454454+end
+338
lib/sdk_control.mli
···11+(** SDK Control Protocol for Claude.
22+33+ This module defines the typed SDK control protocol for bidirectional
44+ communication between the SDK and the Claude CLI. It handles:
55+66+ - Permission requests (tool usage authorization)
77+ - Hook callbacks (intercepting and modifying tool execution)
88+ - Dynamic control (changing settings mid-conversation)
99+ - Server introspection (querying capabilities)
1010+1111+ {2 Protocol Overview}
1212+1313+ The SDK control protocol is a JSON-based request/response protocol that
1414+ runs alongside the main message stream. It enables:
1515+1616+ 1. {b Callbacks}: Claude asks the SDK for permission or hook execution
1717+ 2. {b Control}: SDK changes Claude's behavior dynamically
1818+ 3. {b Introspection}: SDK queries server metadata
1919+2020+ {2 Request/Response Flow}
2121+2222+ {v
2323+ SDK Claude CLI
2424+ | |
2525+ |-- Initialize (with hooks) --> |
2626+ |<-- Permission Request --------| (for tool usage)
2727+ |-- Allow/Deny Response ------> |
2828+ | |
2929+ |<-- Hook Callback -------------| (pre/post tool)
3030+ |-- Hook Result -------------> |
3131+ | |
3232+ |-- Set Model ---------------> | (dynamic control)
3333+ |<-- Success Response ----------|
3434+ | |
3535+ |-- Get Server Info ----------> |
3636+ |<-- Server Info Response ------|
3737+ v}
3838+3939+ {2 Usage}
4040+4141+ Most users won't interact with this module directly. The {!Client} module
4242+ handles the protocol automatically. However, this module is exposed for:
4343+4444+ - Understanding the control protocol
4545+ - Implementing custom control logic
4646+ - Debugging control message flow
4747+ - Advanced SDK extensions
4848+4949+ {2 Dynamic Control Examples}
5050+5151+ See {!Client.set_permission_mode}, {!Client.set_model}, and
5252+ {!Client.get_server_info} for high-level APIs that use this protocol. *)
5353+5454+(** The log source for SDK control operations *)
5555+val src : Logs.Src.t
5656+5757+(** {1 Request Types} *)
5858+5959+module Request : sig
6060+ (** SDK control request types. *)
6161+6262+ type interrupt = {
6363+ subtype : [`Interrupt];
6464+ unknown : Unknown.t;
6565+ }
6666+ (** Interrupt request to stop execution. *)
6767+6868+ type permission = {
6969+ subtype : [`Can_use_tool];
7070+ tool_name : string;
7171+ input : Jsont.json;
7272+ permission_suggestions : Permissions.Update.t list option;
7373+ blocked_path : string option;
7474+ unknown : Unknown.t;
7575+ }
7676+ (** Permission request for tool usage. *)
7777+7878+ type initialize = {
7979+ subtype : [`Initialize];
8080+ hooks : (string * Jsont.json) list option; (* Hook event to configuration *)
8181+ unknown : Unknown.t;
8282+ }
8383+ (** Initialize request with optional hook configuration. *)
8484+8585+ type set_permission_mode = {
8686+ subtype : [`Set_permission_mode];
8787+ mode : Permissions.Mode.t;
8888+ unknown : Unknown.t;
8989+ }
9090+ (** Request to change permission mode. *)
9191+9292+ type hook_callback = {
9393+ subtype : [`Hook_callback];
9494+ callback_id : string;
9595+ input : Jsont.json;
9696+ tool_use_id : string option;
9797+ unknown : Unknown.t;
9898+ }
9999+ (** Hook callback request. *)
100100+101101+ type mcp_message = {
102102+ subtype : [`Mcp_message];
103103+ server_name : string;
104104+ message : Jsont.json;
105105+ unknown : Unknown.t;
106106+ }
107107+ (** MCP server message request. *)
108108+109109+ type set_model = {
110110+ subtype : [`Set_model];
111111+ model : string;
112112+ unknown : Unknown.t;
113113+ }
114114+ (** Request to change the AI model. *)
115115+116116+ type get_server_info = {
117117+ subtype : [`Get_server_info];
118118+ unknown : Unknown.t;
119119+ }
120120+ (** Request to get server information. *)
121121+122122+ type t =
123123+ | Interrupt of interrupt
124124+ | Permission of permission
125125+ | Initialize of initialize
126126+ | Set_permission_mode of set_permission_mode
127127+ | Hook_callback of hook_callback
128128+ | Mcp_message of mcp_message
129129+ | Set_model of set_model
130130+ | Get_server_info of get_server_info
131131+ (** The type of SDK control requests. *)
132132+133133+ val interrupt : ?unknown:Unknown.t -> unit -> t
134134+ (** [interrupt ?unknown ()] creates an interrupt request. *)
135135+136136+ val permission :
137137+ tool_name:string ->
138138+ input:Jsont.json ->
139139+ ?permission_suggestions:Permissions.Update.t list ->
140140+ ?blocked_path:string ->
141141+ ?unknown:Unknown.t ->
142142+ unit -> t
143143+ (** [permission ~tool_name ~input ?permission_suggestions ?blocked_path ?unknown ()]
144144+ creates a permission request. *)
145145+146146+ val initialize : ?hooks:(string * Jsont.json) list -> ?unknown:Unknown.t -> unit -> t
147147+ (** [initialize ?hooks ?unknown ()] creates an initialize request. *)
148148+149149+ val set_permission_mode : mode:Permissions.Mode.t -> ?unknown:Unknown.t -> unit -> t
150150+ (** [set_permission_mode ~mode ?unknown] creates a permission mode change request. *)
151151+152152+ val hook_callback :
153153+ callback_id:string ->
154154+ input:Jsont.json ->
155155+ ?tool_use_id:string ->
156156+ ?unknown:Unknown.t ->
157157+ unit -> t
158158+ (** [hook_callback ~callback_id ~input ?tool_use_id ?unknown ()] creates a hook callback request. *)
159159+160160+ val mcp_message : server_name:string -> message:Jsont.json -> ?unknown:Unknown.t -> unit -> t
161161+ (** [mcp_message ~server_name ~message ?unknown] creates an MCP message request. *)
162162+163163+ val set_model : model:string -> ?unknown:Unknown.t -> unit -> t
164164+ (** [set_model ~model ?unknown] creates a model change request. *)
165165+166166+ val get_server_info : ?unknown:Unknown.t -> unit -> t
167167+ (** [get_server_info ?unknown ()] creates a server info request. *)
168168+169169+ val jsont : t Jsont.t
170170+ (** [jsont] is the jsont codec for requests. *)
171171+172172+ val pp : Format.formatter -> t -> unit
173173+ (** [pp fmt t] pretty-prints the request. *)
174174+end
175175+176176+(** {1 Response Types} *)
177177+178178+module Response : sig
179179+ (** SDK control response types. *)
180180+181181+ type success = {
182182+ subtype : [`Success];
183183+ request_id : string;
184184+ response : Jsont.json option;
185185+ unknown : Unknown.t;
186186+ }
187187+ (** Successful response. *)
188188+189189+ type error = {
190190+ subtype : [`Error];
191191+ request_id : string;
192192+ error : string;
193193+ unknown : Unknown.t;
194194+ }
195195+ (** Error response. *)
196196+197197+ type t =
198198+ | Success of success
199199+ | Error of error
200200+ (** The type of SDK control responses. *)
201201+202202+ val success : request_id:string -> ?response:Jsont.json -> ?unknown:Unknown.t -> unit -> t
203203+ (** [success ~request_id ?response ?unknown ()] creates a success response. *)
204204+205205+ val error : request_id:string -> error:string -> ?unknown:Unknown.t -> unit -> t
206206+ (** [error ~request_id ~error ?unknown] creates an error response. *)
207207+208208+ val jsont : t Jsont.t
209209+ (** [jsont] is the jsont codec for responses. *)
210210+211211+ val pp : Format.formatter -> t -> unit
212212+ (** [pp fmt t] pretty-prints the response. *)
213213+end
214214+215215+(** {1 Control Messages} *)
216216+217217+type control_request = {
218218+ type_ : [`Control_request];
219219+ request_id : string;
220220+ request : Request.t;
221221+ unknown : Unknown.t;
222222+}
223223+(** Control request message. *)
224224+225225+type control_response = {
226226+ type_ : [`Control_response];
227227+ response : Response.t;
228228+ unknown : Unknown.t;
229229+}
230230+(** Control response message. *)
231231+232232+val control_response_jsont : control_response Jsont.t
233233+(** [control_response_jsont] is the jsont codec for control response messages. *)
234234+235235+type t =
236236+ | Request of control_request
237237+ | Response of control_response
238238+(** The type of SDK control messages. *)
239239+240240+val create_request : request_id:string -> request:Request.t -> ?unknown:Unknown.t -> unit -> t
241241+(** [create_request ~request_id ~request ?unknown ()] creates a control request message. *)
242242+243243+val create_response : response:Response.t -> ?unknown:Unknown.t -> unit -> t
244244+(** [create_response ~response ?unknown ()] creates a control response message. *)
245245+246246+val jsont : t Jsont.t
247247+(** [jsont] is the jsont codec for control messages. *)
248248+249249+val pp : Format.formatter -> t -> unit
250250+(** [pp fmt t] pretty-prints the control message. *)
251251+252252+(** {1 Logging} *)
253253+254254+val log_request : Request.t -> unit
255255+(** [log_request req] logs an SDK control request. *)
256256+257257+val log_response : Response.t -> unit
258258+(** [log_response resp] logs an SDK control response. *)
259259+260260+(** {1 Server Information}
261261+262262+ Server information provides metadata about the Claude CLI server,
263263+ including version, capabilities, available commands, and output styles.
264264+265265+ {2 Use Cases}
266266+267267+ - Feature detection: Check if specific capabilities are available
268268+ - Version compatibility: Ensure minimum version requirements
269269+ - Debugging: Log server information for troubleshooting
270270+ - Dynamic adaptation: Adjust SDK behavior based on capabilities
271271+272272+ {2 Example}
273273+274274+ {[
275275+ let info = Client.get_server_info client in
276276+ Printf.printf "Claude CLI version: %s\n"
277277+ (Server_info.version info);
278278+279279+ if List.mem "structured-output" (Server_info.capabilities info) then
280280+ Printf.printf "Structured output is supported\n"
281281+ else
282282+ Printf.printf "Structured output not available\n";
283283+ ]} *)
284284+285285+module Server_info : sig
286286+ (** Server information and capabilities. *)
287287+288288+ type t = {
289289+ version : string;
290290+ (** Server version string (e.g., "2.0.0") *)
291291+292292+ capabilities : string list;
293293+ (** Available server capabilities (e.g., "hooks", "structured-output") *)
294294+295295+ commands : string list;
296296+ (** Available CLI commands *)
297297+298298+ output_styles : string list;
299299+ (** Supported output formats (e.g., "json", "stream-json") *)
300300+301301+ unknown : Unknown.t;
302302+ (** Unknown fields for forward compatibility *)
303303+ }
304304+ (** Server metadata and capabilities.
305305+306306+ This information is useful for feature detection and debugging. *)
307307+308308+ val create :
309309+ version:string ->
310310+ capabilities:string list ->
311311+ commands:string list ->
312312+ output_styles:string list ->
313313+ ?unknown:Unknown.t ->
314314+ unit ->
315315+ t
316316+ (** [create ~version ~capabilities ~commands ~output_styles ?unknown ()] creates server info. *)
317317+318318+ val version : t -> string
319319+ (** [version t] returns the server version. *)
320320+321321+ val capabilities : t -> string list
322322+ (** [capabilities t] returns the server capabilities. *)
323323+324324+ val commands : t -> string list
325325+ (** [commands t] returns available commands. *)
326326+327327+ val output_styles : t -> string list
328328+ (** [output_styles t] returns available output styles. *)
329329+330330+ val unknown : t -> Unknown.t
331331+ (** [unknown t] returns the unknown fields. *)
332332+333333+ val jsont : t Jsont.t
334334+ (** [jsont] is the jsont codec for server info. *)
335335+336336+ val pp : Format.formatter -> t -> unit
337337+ (** [pp fmt t] pretty-prints the server info. *)
338338+end
+49
lib/structured_output.ml
···11+let src = Logs.Src.create "claude.structured_output" ~doc:"Structured output"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+type t = {
55+ json_schema : Jsont.json;
66+}
77+88+let json_to_string json =
99+ match Jsont_bytesrw.encode_string' Jsont.json json with
1010+ | Ok str -> str
1111+ | Error err -> failwith (Jsont.Error.to_string err)
1212+1313+let of_json_schema schema =
1414+ Log.debug (fun m -> m "Created output format from JSON schema: %s"
1515+ (json_to_string schema));
1616+ { json_schema = schema }
1717+1818+let json_schema t = t.json_schema
1919+2020+(* Codec for serializing structured output format *)
2121+let jsont : t Jsont.t =
2222+ Jsont.Object.map ~kind:"StructuredOutput"
2323+ (fun json_schema -> {json_schema})
2424+ |> Jsont.Object.mem "jsonSchema" Jsont.json ~enc:(fun t -> t.json_schema)
2525+ |> Jsont.Object.finish
2626+2727+let to_json t =
2828+ match Jsont.Json.encode jsont t with
2929+ | Ok json -> json
3030+ | Error msg -> failwith ("Structured_output.to_json: " ^ msg)
3131+3232+let of_json json =
3333+ match Jsont.Json.decode jsont json with
3434+ | Ok t -> t
3535+ | Error msg -> raise (Invalid_argument ("Structured_output.of_json: " ^ msg))
3636+3737+let pp fmt t =
3838+ let schema_str =
3939+ match Jsont_bytesrw.encode_string' ~format:Jsont.Minify Jsont.json t.json_schema with
4040+ | Ok s -> s
4141+ | Error err -> Jsont.Error.to_string err
4242+ in
4343+ let truncated =
4444+ if String.length schema_str > 100 then
4545+ String.sub schema_str 0 97 ^ "..."
4646+ else
4747+ schema_str
4848+ in
4949+ Fmt.pf fmt "@[<2>StructuredOutput { schema = %s }@]" truncated
+171
lib/structured_output.mli
···11+(** Structured output configuration using JSON Schema.
22+33+ This module provides structured output support for Claude, allowing you to
44+ specify the expected output format using JSON schemas. When a structured
55+ output format is configured, Claude will return its response in the
66+ specified JSON format, validated against your schema.
77+88+ {2 Overview}
99+1010+ Structured outputs ensure that Claude's responses conform to a specific
1111+ JSON schema, making it easier to parse and use the results programmatically.
1212+ This is particularly useful for:
1313+1414+ - Extracting structured data from unstructured text
1515+ - Building APIs that require consistent JSON responses
1616+ - Integrating Claude into data pipelines
1717+ - Ensuring type-safe parsing of Claude's outputs
1818+1919+ {2 Creating Output Formats}
2020+2121+ Use {!of_json_schema} to specify a JSON Schema as a {!Jsont.json} value:
2222+ {[
2323+ let meta = Jsont.Meta.none in
2424+ let schema = Jsont.Object ([
2525+ (("type", meta), Jsont.String ("object", meta));
2626+ (("properties", meta), Jsont.Object ([
2727+ (("name", meta), Jsont.Object ([
2828+ (("type", meta), Jsont.String ("string", meta))
2929+ ], meta));
3030+ (("age", meta), Jsont.Object ([
3131+ (("type", meta), Jsont.String ("integer", meta))
3232+ ], meta));
3333+ ], meta));
3434+ (("required", meta), Jsont.Array ([
3535+ Jsont.String ("name", meta);
3636+ Jsont.String ("age", meta)
3737+ ], meta));
3838+ ], meta) in
3939+4040+ let format = Structured_output.of_json_schema schema
4141+ ]}
4242+4343+ {3 Helper Functions for Building Schemas}
4444+4545+ For complex schemas, you can use helper functions to make construction easier:
4646+ {[
4747+ let json_object fields =
4848+ Jsont.Object (fields, Jsont.Meta.none)
4949+5050+ let json_string s =
5151+ Jsont.String (s, Jsont.Meta.none)
5252+5353+ let json_array items =
5454+ Jsont.Array (items, Jsont.Meta.none)
5555+5656+ let json_field name value =
5757+ ((name, Jsont.Meta.none), value)
5858+5959+ let person_schema =
6060+ json_object [
6161+ json_field "type" (json_string "object");
6262+ json_field "properties" (json_object [
6363+ json_field "name" (json_object [
6464+ json_field "type" (json_string "string")
6565+ ]);
6666+ json_field "age" (json_object [
6767+ json_field "type" (json_string "integer")
6868+ ]);
6969+ ]);
7070+ json_field "required" (json_array [
7171+ json_string "name";
7272+ json_string "age"
7373+ ])
7474+ ]
7575+7676+ let format = Structured_output.of_json_schema person_schema
7777+ ]}
7878+7979+ {2 Usage with Claude Client}
8080+8181+ {[
8282+ let options = Options.default
8383+ |> Options.with_output_format format
8484+8585+ let client = Client.create ~sw ~process_mgr ~options () in
8686+ Client.query client "Extract person info from: John is 30 years old";
8787+8888+ let messages = Client.receive_all client in
8989+ List.iter (function
9090+ | Message.Result result ->
9191+ (match Message.Result.structured_output result with
9292+ | Some json -> (* Process validated JSON *)
9393+ let json_str = match Jsont_bytesrw.encode_string' Jsont.json json with
9494+ | Ok s -> s
9595+ | Error err -> Jsont.Error.to_string err
9696+ in
9797+ Printf.printf "Structured output: %s\n" json_str
9898+ | None -> ())
9999+ | _ -> ()
100100+ ) messages
101101+ ]}
102102+103103+ {2 JSON Schema Support}
104104+105105+ The module supports standard JSON Schema Draft 7, including:
106106+ - Primitive types (string, integer, number, boolean, null)
107107+ - Objects with properties and required fields
108108+ - Arrays with item schemas
109109+ - Enumerations
110110+ - Nested objects and arrays
111111+ - Complex validation rules
112112+113113+ @see <https://json-schema.org/> JSON Schema specification
114114+ @see <https://erratique.ch/software/jsont> jsont documentation *)
115115+116116+(** The log source for structured output operations *)
117117+val src : Logs.Src.t
118118+119119+(** {1 Output Format Configuration} *)
120120+121121+type t
122122+(** The type of structured output format configurations. *)
123123+124124+val of_json_schema : Jsont.json -> t
125125+(** [of_json_schema schema] creates an output format from a JSON Schema.
126126+127127+ The schema should be a valid JSON Schema Draft 7 as a {!Jsont.json} value.
128128+129129+ Example:
130130+ {[
131131+ let meta = Jsont.Meta.none in
132132+ let schema = Jsont.Object ([
133133+ (("type", meta), Jsont.String ("object", meta));
134134+ (("properties", meta), Jsont.Object ([
135135+ (("name", meta), Jsont.Object ([
136136+ (("type", meta), Jsont.String ("string", meta))
137137+ ], meta));
138138+ (("age", meta), Jsont.Object ([
139139+ (("type", meta), Jsont.String ("integer", meta))
140140+ ], meta));
141141+ ], meta));
142142+ (("required", meta), Jsont.Array ([
143143+ Jsont.String ("name", meta);
144144+ Jsont.String ("age", meta)
145145+ ], meta));
146146+ ], meta) in
147147+148148+ let format = Structured_output.of_json_schema schema
149149+ ]} *)
150150+151151+val json_schema : t -> Jsont.json
152152+(** [json_schema t] returns the JSON Schema. *)
153153+154154+val jsont : t Jsont.t
155155+(** Codec for structured output format. *)
156156+157157+(** {1 Serialization}
158158+159159+ Internal use for encoding/decoding with the CLI. *)
160160+161161+val to_json : t -> Jsont.json
162162+(** [to_json t] converts the output format to its JSON representation.
163163+ Internal use only. *)
164164+165165+val of_json : Jsont.json -> t
166166+(** [of_json json] parses an output format from JSON.
167167+ Internal use only.
168168+ @raise Invalid_argument if the JSON is not a valid output format. *)
169169+170170+val pp : Format.formatter -> t -> unit
171171+(** [pp fmt t] pretty-prints the output format. *)
+212
lib/transport.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "claude.transport" ~doc:"Claude transport layer"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+exception CLI_not_found of string
77+exception Process_error of string
88+exception Connection_error of string
99+1010+type process = P : _ Eio.Process.t -> process
1111+1212+type t = {
1313+ process : process;
1414+ stdin : Eio.Flow.sink_ty r;
1515+ stdin_close : [`Close | `Flow] r;
1616+ stdout : Eio.Buf_read.t;
1717+ sw : Switch.t;
1818+}
1919+2020+let setting_source_to_string = function
2121+ | Options.User -> "user"
2222+ | Options.Project -> "project"
2323+ | Options.Local -> "local"
2424+2525+let build_command ~claude_path ~options =
2626+ let cmd = [claude_path; "--output-format"; "stream-json"; "--verbose"] in
2727+2828+ let cmd = match Options.system_prompt options with
2929+ | Some prompt -> cmd @ ["--system-prompt"; prompt]
3030+ | None -> cmd
3131+ in
3232+3333+ let cmd = match Options.append_system_prompt options with
3434+ | Some prompt -> cmd @ ["--append-system-prompt"; prompt]
3535+ | None -> cmd
3636+ in
3737+3838+ let cmd = match Options.allowed_tools options with
3939+ | [] -> cmd
4040+ | tools -> cmd @ ["--allowedTools"; String.concat "," tools]
4141+ in
4242+4343+ let cmd = match Options.disallowed_tools options with
4444+ | [] -> cmd
4545+ | tools -> cmd @ ["--disallowedTools"; String.concat "," tools]
4646+ in
4747+4848+ let cmd = match Options.model options with
4949+ | Some model -> cmd @ ["--model"; Model.to_string model]
5050+ | None -> cmd
5151+ in
5252+5353+ let cmd = match Options.permission_mode options with
5454+ | Some mode ->
5555+ let mode_str = Permissions.Mode.to_string mode in
5656+ cmd @ ["--permission-mode"; mode_str]
5757+ | None -> cmd
5858+ in
5959+6060+ let cmd = match Options.permission_prompt_tool_name options with
6161+ | Some tool_name -> cmd @ ["--permission-prompt-tool"; tool_name]
6262+ | None -> cmd
6363+ in
6464+6565+ (* Advanced configuration options *)
6666+ let cmd = match Options.max_budget_usd options with
6767+ | Some budget -> cmd @ ["--max-budget-usd"; Float.to_string budget]
6868+ | None -> cmd
6969+ in
7070+7171+ let cmd = match Options.fallback_model options with
7272+ | Some model -> cmd @ ["--fallback-model"; Model.to_string model]
7373+ | None -> cmd
7474+ in
7575+7676+ let cmd = match Options.setting_sources options with
7777+ | Some sources ->
7878+ let sources_str = String.concat "," (List.map setting_source_to_string sources) in
7979+ cmd @ ["--setting-sources"; sources_str]
8080+ | None -> cmd
8181+ in
8282+8383+ (* Add JSON Schema if specified *)
8484+ let cmd = match Options.output_format options with
8585+ | Some format ->
8686+ let schema = Structured_output.json_schema format in
8787+ let schema_str = match Jsont_bytesrw.encode_string' Jsont.json schema with
8888+ | Ok s -> s
8989+ | Error err -> failwith (Jsont.Error.to_string err)
9090+ in
9191+ cmd @ ["--json-schema"; schema_str]
9292+ | None -> cmd
9393+ in
9494+9595+ (* Use streaming input mode *)
9696+ cmd @ ["--input-format"; "stream-json"]
9797+9898+let create ~sw ~process_mgr ~options () =
9999+ let claude_path = "claude" in
100100+ let cmd = build_command ~claude_path ~options in
101101+102102+ (* Build environment - preserve essential vars for Claude config/auth access *)
103103+ let home = try Unix.getenv "HOME" with Not_found -> "/tmp" in
104104+ let path = try Unix.getenv "PATH" with Not_found -> "/usr/bin:/bin" in
105105+106106+ (* Preserve other potentially important environment variables *)
107107+ let preserve_vars = [
108108+ "USER"; "LOGNAME"; "SHELL"; "TERM";
109109+ "XDG_CONFIG_HOME"; "XDG_DATA_HOME"; "XDG_CACHE_HOME";
110110+ "ANTHROPIC_API_KEY"; "CLAUDE_API_KEY" (* In case API key is set via env *)
111111+ ] in
112112+113113+ let preserved = List.filter_map (fun var ->
114114+ try Some (Printf.sprintf "%s=%s" var (Unix.getenv var))
115115+ with Not_found -> None
116116+ ) preserve_vars in
117117+118118+ let base_env = [
119119+ Printf.sprintf "HOME=%s" home;
120120+ Printf.sprintf "PATH=%s" path;
121121+ "CLAUDE_CODE_ENTRYPOINT=sdk-ocaml";
122122+ ] @ preserved in
123123+124124+ let custom_env = List.map (fun (k, v) -> Printf.sprintf "%s=%s" k v) (Options.env options) in
125125+ let env = Array.of_list (base_env @ custom_env) in
126126+ Log.debug (fun m -> m "Environment: HOME=%s, PATH=%s" home path);
127127+ Log.info (fun m -> m "Full environment variables: %s" (String.concat ", " (Array.to_list env)));
128128+129129+ let stdin_r, stdin_w = Eio.Process.pipe ~sw process_mgr in
130130+ let stdout_r, stdout_w = Eio.Process.pipe ~sw process_mgr in
131131+ let stderr_r, stderr_w = Eio.Process.pipe ~sw process_mgr in
132132+ (* Close stderr pipes - we don't need them *)
133133+ Eio.Flow.close stderr_r;
134134+ Eio.Flow.close stderr_w;
135135+136136+ let process =
137137+ try
138138+ Log.info (fun m -> m "Spawning claude with command: %s" (String.concat " " cmd));
139139+ Log.info (fun m -> m "Command arguments breakdown:");
140140+ List.iteri (fun i arg ->
141141+ Log.info (fun m -> m " [%d]: %s" i arg)
142142+ ) cmd;
143143+ Eio.Process.spawn ~sw process_mgr
144144+ ~env
145145+ ~stdin:(stdin_r :> Eio.Flow.source_ty r)
146146+ ~stdout:(stdout_w :> Eio.Flow.sink_ty r)
147147+ ?cwd:(Options.cwd options)
148148+ cmd
149149+ with
150150+ | exn ->
151151+ Log.err (fun m -> m "Failed to spawn claude CLI: %s" (Printexc.to_string exn));
152152+ Log.err (fun m -> m "Make sure 'claude' is installed and authenticated");
153153+ Log.err (fun m -> m "You may need to run 'claude login' first");
154154+ raise (CLI_not_found (Printf.sprintf "Failed to spawn claude CLI: %s" (Printexc.to_string exn)))
155155+ in
156156+157157+ let stdin = (stdin_w :> Eio.Flow.sink_ty r) in
158158+ let stdin_close = (stdin_w :> [`Close | `Flow] r) in
159159+ let max_size = match Options.max_buffer_size options with
160160+ | Some size -> size
161161+ | None -> 1_000_000 (* Default 1MB *)
162162+ in
163163+ let stdout = Eio.Buf_read.of_flow ~max_size (stdout_r :> Eio.Flow.source_ty r) in
164164+165165+ { process = P process; stdin; stdin_close; stdout; sw }
166166+167167+let send t json =
168168+ let data = match Jsont_bytesrw.encode_string' Jsont.json json with
169169+ | Ok s -> s
170170+ | Error err -> failwith (Jsont.Error.to_string err)
171171+ in
172172+ Log.debug (fun m -> m "Sending: %s" data);
173173+ try
174174+ Eio.Flow.write t.stdin [Cstruct.of_string (data ^ "\n")]
175175+ with
176176+ | exn ->
177177+ Log.err (fun m -> m "Failed to send message: %s" (Printexc.to_string exn));
178178+ raise (Connection_error (Printf.sprintf "Failed to send message: %s" (Printexc.to_string exn)))
179179+180180+let receive_line t =
181181+ try
182182+ match Eio.Buf_read.line t.stdout with
183183+ | line ->
184184+ Log.debug (fun m -> m "Raw JSON: %s" line);
185185+ Some line
186186+ | exception End_of_file ->
187187+ Log.debug (fun m -> m "Received EOF");
188188+ None
189189+ with
190190+ | exn ->
191191+ Log.err (fun m -> m "Failed to receive message: %s" (Printexc.to_string exn));
192192+ raise (Connection_error (Printf.sprintf "Failed to receive message: %s" (Printexc.to_string exn)))
193193+194194+let interrupt t =
195195+ Log.info (fun m -> m "Sending interrupt signal");
196196+ let interrupt_msg =
197197+ Jsont.Json.object' [
198198+ Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
199199+ Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
200200+ Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "interrupt");
201201+ Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string "");
202202+ ])
203203+ ]
204204+ in
205205+ send t interrupt_msg
206206+207207+let close t =
208208+ try
209209+ Eio.Flow.close t.stdin_close;
210210+ let (P process) = t.process in
211211+ Eio.Process.await_exn process
212212+ with _ -> ()
+19
lib/transport.mli
···11+(** The log source for transport operations *)
22+val src : Logs.Src.t
33+44+exception CLI_not_found of string
55+exception Process_error of string
66+exception Connection_error of string
77+88+type t
99+1010+val create :
1111+ sw:Eio.Switch.t ->
1212+ process_mgr:_ Eio.Process.mgr ->
1313+ options:Options.t ->
1414+ unit -> t
1515+1616+val send : t -> Jsont.json -> unit
1717+val receive_line : t -> string option
1818+val interrupt : t -> unit
1919+val close : t -> unit
+20
lib/unknown.ml
···11+(** Unknown fields for capturing extra JSON object members.
22+33+ This module provides a type and utilities for preserving unknown/extra
44+ fields when parsing JSON objects with jsont. Use with
55+ [Jsont.Object.keep_unknown] to capture fields not explicitly defined
66+ in your codec. *)
77+88+type t = Jsont.json
99+(** The type of unknown fields - stored as raw JSON. *)
1010+1111+let empty = Jsont.Object ([], Jsont.Meta.none)
1212+(** An empty unknown fields value (empty JSON object). *)
1313+1414+let is_empty = function
1515+ | Jsont.Object ([], _) -> true
1616+ | _ -> false
1717+(** [is_empty t] returns [true] if there are no unknown fields. *)
1818+1919+let jsont = Jsont.json
2020+(** Codec for unknown fields. *)
+18
lib/unknown.mli
···11+(** Unknown fields for capturing extra JSON object members.
22+33+ This module provides a type and utilities for preserving unknown/extra
44+ fields when parsing JSON objects with jsont. Use with
55+ [Jsont.Object.keep_unknown] to capture fields not explicitly defined
66+ in your codec. *)
77+88+type t = Jsont.json
99+(** The type of unknown fields - stored as raw JSON. *)
1010+1111+val empty : t
1212+(** An empty unknown fields value (empty JSON object). *)
1313+1414+val is_empty : t -> bool
1515+(** [is_empty t] returns [true] if there are no unknown fields. *)
1616+1717+val jsont : t Jsont.t
1818+(** Codec for unknown fields. *)
+35
test/README.md
···11+# Claude IO Test Suite
22+33+This directory contains test programs for the Claude IO OCaml library.
44+55+## Available Tests
66+77+### camel_jokes
88+A fun demonstration that runs three concurrent Claude instances to generate camel jokes.
99+Tests concurrent client handling and basic message processing.
1010+1111+### permission_demo
1212+An interactive demonstration of Claude's permission system.
1313+Shows how to implement custom permission callbacks and grant/deny access to tools dynamically.
1414+1515+## Running Tests
1616+1717+```bash
1818+# Run the camel joke competition
1919+dune exec camel_jokes
2020+2121+# Run the permission demo (interactive)
2222+dune exec permission_demo
2323+2424+# With verbose output to see message flow
2525+dune exec permission_demo -- -v
2626+```
2727+2828+## Features Tested
2929+3030+- Concurrent Claude client instances
3131+- Message handling and processing
3232+- Permission callbacks
3333+- Tool access control
3434+- Typed message API
3535+- Pretty printing of messages
+112
test/TEST.md
···11+# Claude Library Architecture Summary
22+33+This document summarizes the architecture of the OCaml Eio Claude library located in `../lib`.
44+55+## Overview
66+77+The Claude library is a high-quality OCaml Eio wrapper around the Claude Code CLI that provides structured JSON streaming communication with Claude. It follows a clean layered architecture with strong typing and comprehensive error handling.
88+99+## Core Architecture
1010+1111+The library is organized into several focused modules that work together to provide a complete Claude integration:
1212+1313+### 1. Transport Layer (`Transport`)
1414+- **Purpose**: Low-level CLI process management and communication
1515+- **Key Functions**:
1616+ - Spawns and manages the `claude` CLI process using Eio's process manager
1717+ - Handles bidirectional JSON streaming via stdin/stdout
1818+ - Provides `send`/`receive_line` primitives with proper resource cleanup
1919+- **Integration**: Forms the foundation for all Claude communication
2020+2121+### 2. Message Protocol Layer
2222+2323+#### Content Blocks (`Content_block`)
2424+- **Purpose**: Defines the building blocks of Claude messages
2525+- **Types**: Text, Tool_use, Tool_result, Thinking blocks
2626+- **Key Features**: Each block type has specialized accessors and JSON serialization
2727+- **Integration**: Used by messages to represent diverse content types
2828+2929+#### Messages (`Message`)
3030+- **Purpose**: Structured message types for Claude communication
3131+- **Types**: User, Assistant, System, Result messages
3232+- **Key Features**:
3333+ - User messages support both simple strings and complex content blocks
3434+ - Assistant messages include model info and mixed content
3535+ - System messages handle session control
3636+ - Result messages provide conversation metadata and usage stats
3737+- **Integration**: Primary data structures exchanged between client and Claude
3838+3939+#### Control Messages (`Control`)
4040+- **Purpose**: Session management and control flow
4141+- **Key Features**: Request IDs, subtypes, and arbitrary JSON data payload
4242+- **Integration**: Used for session initialization, cancellation, and other operational commands
4343+4444+### 3. Permission System (`Permissions`)
4545+- **Purpose**: Fine-grained control over Claude's tool usage
4646+- **Components**:
4747+ - **Modes**: Default, Accept_edits, Plan, Bypass_permissions
4848+ - **Rules**: Tool-specific permission specifications
4949+ - **Callbacks**: Custom permission logic with context and suggestions
5050+ - **Results**: Allow/Deny decisions with optional modifications
5151+- **Integration**: Consulted by client before allowing tool invocations
5252+5353+### 4. Configuration (`Options`)
5454+- **Purpose**: Session configuration and behavior control
5555+- **Features**:
5656+ - Tool allow/disallow lists
5757+ - System prompt customization (replace or append)
5858+ - Model selection and thinking token limits
5959+ - Working directory and environment variables
6060+- **Integration**: Passed to transport layer and used throughout the session
6161+- **Pattern**: Builder pattern with `with_*` functions for immutable updates
6262+6363+### 5. Client Interface (`Client`)
6464+- **Purpose**: High-level API for Claude interactions
6565+- **Key Functions**:
6666+ - Session creation and management
6767+ - Message sending (`query`, `send_message`, `send_user_message`)
6868+ - Response streaming (`receive`, `receive_all`)
6969+ - Permission discovery and callback management
7070+- **Integration**: Orchestrates all other modules to provide the main user API
7171+7272+### 6. Main Module (`Claude`)
7373+- **Purpose**: Public API facade with comprehensive documentation
7474+- **Features**:
7575+ - Re-exports all sub-modules
7676+ - Extensive usage examples and architectural documentation
7777+ - Logging configuration guidance
7878+- **Integration**: Single entry point for library users
7979+8080+## Data Flow
8181+8282+1. **Configuration**: Options are created with desired settings
8383+2. **Transport**: Client creates transport layer with CLI process
8484+3. **Message Exchange**:
8585+ - User messages are sent via JSON streaming
8686+ - Claude responses are received as streaming JSON
8787+ - Messages are parsed into strongly-typed structures
8888+4. **Permission Checking**: Tool usage is filtered through permission system
8989+5. **Content Processing**: Response content blocks are extracted and processed
9090+6. **Session Management**: Control messages handle session lifecycle
9191+9292+## Key Design Principles
9393+9494+- **Eio Integration**: Native use of Eio's concurrency primitives (Switch, Process.mgr)
9595+- **Type Safety**: Comprehensive typing with specific error exceptions
9696+- **Streaming**: Efficient processing via `Message.t Seq.t` sequences
9797+- **Modularity**: Clear separation of concerns with minimal inter-dependencies
9898+- **Documentation**: Extensive interface documentation with usage examples
9999+- **Error Handling**: Specific exception types for different failure modes
100100+- **Logging**: Structured logging with per-module sources using the Logs library
101101+102102+## Usage Patterns
103103+104104+The library supports both simple text queries and complex multi-turn conversations:
105105+106106+- **Simple Queries**: `Client.query` with text input
107107+- **Tool Control**: Permission callbacks and allow/disallow lists
108108+- **Streaming**: Process responses as they arrive via sequences
109109+- **Session Management**: Full control over Claude's execution environment
110110+- **Custom Prompts**: System prompt replacement and augmentation
111111+112112+The architecture enables fine-grained control over Claude's capabilities while maintaining ease of use for common scenarios.
+165
test/advanced_config_demo.ml
···11+(* Advanced Configuration Demo
22+33+ This example demonstrates the advanced configuration options available
44+ in the OCaml Claude SDK, including:
55+ - Budget limits for cost control
66+ - Fallback models for reliability
77+ - Settings isolation for CI/CD environments
88+ - Custom buffer sizes for large outputs
99+*)
1010+1111+open Eio.Std
1212+open Claude
1313+1414+let log_setup () =
1515+ Logs.set_reporter (Logs_fmt.reporter ());
1616+ Logs.set_level (Some Logs.Info)
1717+1818+(* Example 1: CI/CD Configuration
1919+2020+ In CI/CD environments, you want isolated, reproducible behavior
2121+ without any user/project/local settings interfering.
2222+*)
2323+let ci_cd_config () =
2424+ Options.default
2525+ |> Options.with_no_settings (* Disable all settings loading *)
2626+ |> Options.with_max_budget_usd 0.50 (* 50 cent limit per run *)
2727+ |> Options.with_fallback_model_string "claude-haiku-4" (* Fast fallback *)
2828+ |> Options.with_model_string "claude-sonnet-4-5"
2929+ |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
3030+3131+(* Example 2: Production Configuration with Fallback
3232+3333+ Production usage with cost controls and automatic fallback
3434+ to ensure availability.
3535+*)
3636+let production_config () =
3737+ Options.default
3838+ |> Options.with_model_string "claude-sonnet-4-5"
3939+ |> Options.with_fallback_model_string "claude-sonnet-3-5"
4040+ |> Options.with_max_budget_usd 10.0 (* $10 limit *)
4141+ |> Options.with_max_buffer_size 5_000_000 (* 5MB buffer for large outputs *)
4242+4343+(* Example 3: Development Configuration
4444+4545+ Development with user settings enabled but with cost controls.
4646+*)
4747+let dev_config () =
4848+ Options.default
4949+ |> Options.with_setting_sources [Options.User; Options.Project]
5050+ |> Options.with_max_budget_usd 1.0 (* $1 limit for dev testing *)
5151+ |> Options.with_fallback_model_string "claude-haiku-4"
5252+5353+(* Example 4: Isolated Test Configuration
5454+5555+ For automated testing with no external settings and strict limits.
5656+*)
5757+let test_config () =
5858+ Options.default
5959+ |> Options.with_no_settings
6060+ |> Options.with_max_budget_usd 0.10 (* 10 cent limit per test *)
6161+ |> Options.with_model_string "claude-haiku-4" (* Fast, cheap model *)
6262+ |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
6363+ |> Options.with_max_buffer_size 1_000_000 (* 1MB buffer *)
6464+6565+(* Example 5: Custom Buffer Size Demo
6666+6767+ For applications that need to handle very large outputs.
6868+*)
6969+let _large_output_config () =
7070+ Options.default
7171+ |> Options.with_max_buffer_size 10_000_000 (* 10MB buffer *)
7272+ |> Options.with_model_string "claude-sonnet-4-5"
7373+7474+(* Helper to run a query with a specific configuration *)
7575+let run_query ~sw process_mgr config prompt =
7676+ print_endline "\n=== Configuration ===";
7777+ (match Options.max_budget_usd config with
7878+ | Some budget -> Printf.printf "Budget limit: $%.2f\n" budget
7979+ | None -> print_endline "Budget limit: None");
8080+ (match Options.fallback_model config with
8181+ | Some model -> Printf.printf "Fallback model: %s\n" (Claude.Model.to_string model)
8282+ | None -> print_endline "Fallback model: None");
8383+ (match Options.setting_sources config with
8484+ | Some [] -> print_endline "Settings: Isolated (no settings loaded)"
8585+ | Some sources ->
8686+ let source_str = String.concat ", " (List.map (function
8787+ | Options.User -> "user"
8888+ | Options.Project -> "project"
8989+ | Options.Local -> "local"
9090+ ) sources) in
9191+ Printf.printf "Settings: %s\n" source_str
9292+ | None -> print_endline "Settings: Default");
9393+ (match Options.max_buffer_size config with
9494+ | Some size -> Printf.printf "Buffer size: %d bytes\n" size
9595+ | None -> print_endline "Buffer size: Default (1MB)");
9696+9797+ print_endline "\n=== Running Query ===";
9898+ let client = Client.create ~options:config ~sw ~process_mgr () in
9999+ Client.query client prompt;
100100+ let messages = Client.receive client in
101101+102102+ Seq.iter (function
103103+ | Message.Assistant msg ->
104104+ List.iter (function
105105+ | Content_block.Text t ->
106106+ Printf.printf "Response: %s\n" (Content_block.Text.text t)
107107+ | _ -> ()
108108+ ) (Message.Assistant.content msg)
109109+ | Message.Result result ->
110110+ Printf.printf "\n=== Session Complete ===\n";
111111+ Printf.printf "Duration: %dms\n" (Message.Result.duration_ms result);
112112+ (match Message.Result.total_cost_usd result with
113113+ | Some cost -> Printf.printf "Cost: $%.4f\n" cost
114114+ | None -> ());
115115+ Printf.printf "Turns: %d\n" (Message.Result.num_turns result)
116116+ | _ -> ()
117117+ ) messages
118118+119119+let main () =
120120+ log_setup ();
121121+122122+ Eio_main.run @@ fun env ->
123123+ Switch.run @@ fun sw ->
124124+ let process_mgr = Eio.Stdenv.process_mgr env in
125125+126126+ print_endline "==============================================";
127127+ print_endline "Claude SDK - Advanced Configuration Examples";
128128+ print_endline "==============================================";
129129+130130+ (* Example: CI/CD isolated environment *)
131131+ print_endline "\n\n### Example 1: CI/CD Configuration ###";
132132+ print_endline "Purpose: Isolated, reproducible environment for CI/CD";
133133+ let config = ci_cd_config () in
134134+ run_query ~sw process_mgr config "What is 2+2? Answer in one sentence.";
135135+136136+ (* Example: Production with fallback *)
137137+ print_endline "\n\n### Example 2: Production Configuration ###";
138138+ print_endline "Purpose: Production with cost controls and fallback";
139139+ let config = production_config () in
140140+ run_query ~sw process_mgr config "Explain OCaml in one sentence.";
141141+142142+ (* Example: Development with settings *)
143143+ print_endline "\n\n### Example 3: Development Configuration ###";
144144+ print_endline "Purpose: Development with user/project settings";
145145+ let config = dev_config () in
146146+ run_query ~sw process_mgr config "What is functional programming? One sentence.";
147147+148148+ (* Example: Test configuration *)
149149+ print_endline "\n\n### Example 4: Test Configuration ###";
150150+ print_endline "Purpose: Automated testing with strict limits";
151151+ let config = test_config () in
152152+ run_query ~sw process_mgr config "Say 'test passed' in one word.";
153153+154154+ print_endline "\n\n==============================================";
155155+ print_endline "All examples completed successfully!";
156156+ print_endline "=============================================="
157157+158158+let () =
159159+ try
160160+ main ()
161161+ with
162162+ | e ->
163163+ Printf.eprintf "Error: %s\n" (Printexc.to_string e);
164164+ Printexc.print_backtrace stderr;
165165+ exit 1
+126
test/camel_jokes.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "camel_jokes" ~doc:"Camel joke competition"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+let process_claude_response client name =
77+ Log.info (fun m -> m "=== %s's Response ===" name);
88+ let messages = Claude.Client.receive_all client in
99+ List.iter (fun msg ->
1010+ match msg with
1111+ | Claude.Message.Assistant msg ->
1212+ List.iter (function
1313+ | Claude.Content_block.Text t ->
1414+ let text = Claude.Content_block.Text.text t in
1515+ Log.app (fun m -> m "%s: %s" name text)
1616+ | Claude.Content_block.Tool_use t ->
1717+ Log.debug (fun m -> m "%s using tool: %s" name
1818+ (Claude.Content_block.Tool_use.name t))
1919+ | Claude.Content_block.Thinking t ->
2020+ Log.debug (fun m -> m "%s thinking: %s" name
2121+ (Claude.Content_block.Thinking.thinking t))
2222+ | _ -> ()
2323+ ) (Claude.Message.Assistant.content msg);
2424+ Log.debug (fun m -> m "%s using model: %s" name
2525+ (Claude.Message.Assistant.model msg))
2626+ | Claude.Message.Result msg ->
2727+ if Claude.Message.Result.is_error msg then
2828+ Log.err (fun m -> m "Error from %s!" name)
2929+ else
3030+ (match Claude.Message.Result.total_cost_usd msg with
3131+ | Some cost ->
3232+ Log.info (fun m -> m "%s's joke cost: $%.6f" name cost)
3333+ | None -> ());
3434+ Log.debug (fun m -> m "%s session: %s, duration: %dms"
3535+ name
3636+ (Claude.Message.Result.session_id msg)
3737+ (Claude.Message.Result.duration_ms msg))
3838+ | Claude.Message.System _ ->
3939+ (* System messages are already logged by the library *)
4040+ ()
4141+ | Claude.Message.User _ ->
4242+ (* User messages are already logged by the library *)
4343+ ()
4444+ ) messages
4545+4646+let run_claude ~sw ~env name prompt =
4747+ Log.info (fun m -> m "🐪 Starting %s..." name);
4848+ let options = Claude.Options.create ~model:(Claude.Model.of_string "sonnet") ~allowed_tools:[] () in
4949+5050+ let client = Claude.Client.create ~options ~sw
5151+ ~process_mgr:env#process_mgr
5252+ () in
5353+5454+ Claude.Client.query client prompt;
5555+ process_claude_response client name
5656+5757+let main ~env =
5858+ Switch.run @@ fun sw ->
5959+6060+ Log.app (fun m -> m "🐪 Starting the Great Camel Joke Competition! 🐪");
6161+ Log.app (fun m -> m "================================================\n");
6262+6363+ let prompts = [
6464+ "Claude 1", "Tell me a short, funny joke about camels! Make it original and clever.";
6565+ "Claude 2", "Give me your best camel joke - something witty and unexpected!";
6666+ "Claude 3", "Share a hilarious camel joke that will make everyone laugh!";
6767+ ] in
6868+6969+ (* Run all three Claudes concurrently *)
7070+ Fiber.all (
7171+ List.map (fun (name, prompt) ->
7272+ fun () -> run_claude ~sw ~env name prompt
7373+ ) prompts
7474+ );
7575+7676+ Log.app (fun m -> m "\n================================================");
7777+ Log.app (fun m -> m "🎉 The Camel Joke Competition is complete! 🎉")
7878+7979+(* Command-line interface *)
8080+open Cmdliner
8181+8282+let main_term env =
8383+ let setup_log style_renderer level =
8484+ Fmt_tty.setup_std_outputs ?style_renderer ();
8585+ Logs.set_level level;
8686+ Logs.set_reporter (Logs_fmt.reporter ());
8787+ (* Set default to App level if not specified *)
8888+ if level = None then Logs.set_level (Some Logs.App);
8989+ (* Enable debug for Client module if in debug mode *)
9090+ if level = Some Logs.Debug then
9191+ Logs.Src.set_level Claude.Client.src (Some Logs.Debug)
9292+ in
9393+ let run style level =
9494+ setup_log style level;
9595+ main ~env
9696+ in
9797+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
9898+9999+let cmd env =
100100+ let doc = "Run the Great Camel Joke Competition using Claude" in
101101+ let man = [
102102+ `S Manpage.s_description;
103103+ `P "This program runs three concurrent Claude instances to generate camel jokes.";
104104+ `P "Use $(b,-v) or $(b,--verbosity=info) to see RPC message traffic.";
105105+ `P "Use $(b,-vv) or $(b,--verbosity=debug) to see all internal operations.";
106106+ `S Manpage.s_examples;
107107+ `P "Run with normal output:";
108108+ `Pre " $(mname)";
109109+ `P "Run with info-level logging (RPC traffic):";
110110+ `Pre " $(mname) -v";
111111+ `Pre " $(mname) --verbosity=info";
112112+ `P "Run with debug logging (all operations):";
113113+ `Pre " $(mname) -vv";
114114+ `Pre " $(mname) --verbosity=debug";
115115+ `P "Enable debug for specific modules:";
116116+ `Pre " LOGS='claude.transport=debug' $(mname)";
117117+ `Pre " LOGS='claude.message=info,camel_jokes=debug' $(mname)";
118118+ `S Manpage.s_bugs;
119119+ `P "Report bugs at https://github.com/your-repo/issues";
120120+ ] in
121121+ let info = Cmd.info "camel_jokes" ~version:"1.0" ~doc ~man in
122122+ Cmd.v info (main_term env)
123123+124124+let () =
125125+ Eio_main.run @@ fun env ->
126126+ exit (Cmd.eval (cmd env))
+96
test/discovery_demo.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "discovery_demo" ~doc:"Permission discovery demonstration"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+let process_response client =
77+ let messages = Claude.Client.receive_all client in
88+ List.iter (fun msg ->
99+ match msg with
1010+ | Claude.Message.Assistant msg ->
1111+ List.iter (function
1212+ | Claude.Content_block.Text t ->
1313+ let text = Claude.Content_block.Text.text t in
1414+ Log.app (fun m -> m "Claude: %s"
1515+ (if String.length text > 100 then
1616+ String.sub text 0 100 ^ "..."
1717+ else text))
1818+ | Claude.Content_block.Tool_use t ->
1919+ Log.info (fun m -> m "Tool use: %s"
2020+ (Claude.Content_block.Tool_use.name t))
2121+ | _ -> ()
2222+ ) (Claude.Message.Assistant.content msg)
2323+ | Claude.Message.Result msg ->
2424+ if Claude.Message.Result.is_error msg then
2525+ Log.err (fun m -> m "Error occurred!")
2626+ else
2727+ (match Claude.Message.Result.total_cost_usd msg with
2828+ | Some cost ->
2929+ Log.info (fun m -> m "Cost: $%.6f" cost)
3030+ | None -> ())
3131+ | _ -> ()
3232+ ) messages
3333+3434+let run_discovery ~sw ~env =
3535+ Log.app (fun m -> m "🔍 Permission Discovery Demo");
3636+ Log.app (fun m -> m "=============================");
3737+ Log.app (fun m -> m "This will discover what permissions Claude needs.\n");
3838+3939+ (* Create client with discovery mode *)
4040+ let options = Claude.Options.create ~model:(Claude.Model.of_string "sonnet") () in
4141+ let client = Claude.Client.discover_permissions
4242+ (Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()) in
4343+4444+ (* Send a prompt that will need permissions *)
4545+ Log.app (fun m -> m "Asking Claude to read a secret file...");
4646+ Claude.Client.query client
4747+ "Please read the file test/secret_data.txt and tell me what the secret code is.";
4848+ process_response client;
4949+5050+ (* Check what permissions were requested *)
5151+ let permissions = Claude.Client.get_discovered_permissions client in
5252+ if permissions = [] then
5353+ Log.app (fun m -> m "\n📋 No permissions were requested (Claude may have used its knowledge).")
5454+ else begin
5555+ Log.app (fun m -> m "\n📋 Permissions that were requested:");
5656+ List.iter (fun rule ->
5757+ Log.app (fun m -> m " - Tool: %s%s"
5858+ (Claude.Permissions.Rule.tool_name rule)
5959+ (match Claude.Permissions.Rule.rule_content rule with
6060+ | Some content -> Printf.sprintf " (rule: %s)" content
6161+ | None -> ""))
6262+ ) permissions
6363+ end
6464+6565+let main ~env =
6666+ Switch.run @@ fun sw ->
6767+ run_discovery ~sw ~env
6868+6969+(* Command-line interface *)
7070+open Cmdliner
7171+7272+let main_term env =
7373+ let setup_log style_renderer level =
7474+ Fmt_tty.setup_std_outputs ?style_renderer ();
7575+ Logs.set_level level;
7676+ Logs.set_reporter (Logs_fmt.reporter ());
7777+ if level = None then Logs.set_level (Some Logs.App)
7878+ in
7979+ let run style level =
8080+ setup_log style level;
8181+ main ~env
8282+ in
8383+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
8484+8585+let cmd env =
8686+ let doc = "Discover what permissions Claude needs" in
8787+ let man = [
8888+ `S Manpage.s_description;
8989+ `P "This program runs Claude in discovery mode to see what permissions it requests.";
9090+ ] in
9191+ let info = Cmd.info "discovery_demo" ~version:"1.0" ~doc ~man in
9292+ Cmd.v info (main_term env)
9393+9494+let () =
9595+ Eio_main.run @@ fun env ->
9696+ exit (Cmd.eval (cmd env))
···11+open Claude
22+open Eio.Std
33+44+let () = Logs.set_reporter (Logs_fmt.reporter ())
55+let () = Logs.set_level (Some Logs.Info)
66+77+let run env =
88+ Switch.run @@ fun sw ->
99+ let process_mgr = Eio.Stdenv.process_mgr env in
1010+1111+ (* Create client with default options *)
1212+ let options = Options.default in
1313+ let client = Client.create ~options ~sw ~process_mgr () in
1414+1515+ traceln "=== Dynamic Control Demo ===\n";
1616+1717+ (* First query with default model *)
1818+ traceln "1. Initial query with default model";
1919+ Client.query client "What model are you?";
2020+2121+ (* Consume initial messages *)
2222+ let messages = Client.receive_all client in
2323+ List.iter (function
2424+ | Message.Assistant msg ->
2525+ List.iter (function
2626+ | Content_block.Text t ->
2727+ traceln "Assistant: %s" (Content_block.Text.text t)
2828+ | _ -> ()
2929+ ) (Message.Assistant.content msg)
3030+ | _ -> ()
3131+ ) messages;
3232+3333+ traceln "\n2. Getting server info...";
3434+ (try
3535+ let info = Client.get_server_info client in
3636+ traceln "Server version: %s" (Sdk_control.Server_info.version info);
3737+ traceln "Capabilities: [%s]"
3838+ (String.concat ", " (Sdk_control.Server_info.capabilities info));
3939+ traceln "Commands: [%s]"
4040+ (String.concat ", " (Sdk_control.Server_info.commands info));
4141+ traceln "Output styles: [%s]"
4242+ (String.concat ", " (Sdk_control.Server_info.output_styles info));
4343+ with
4444+ | Failure msg -> traceln "Failed to get server info: %s" msg
4545+ | exn -> traceln "Error getting server info: %s" (Printexc.to_string exn));
4646+4747+ traceln "\n3. Switching to a different model (if available)...";
4848+ (try
4949+ Client.set_model_string client "claude-sonnet-4";
5050+ traceln "Model switched successfully";
5151+5252+ (* Query with new model *)
5353+ Client.query client "Confirm your model again please.";
5454+ let messages = Client.receive_all client in
5555+ List.iter (function
5656+ | Message.Assistant msg ->
5757+ List.iter (function
5858+ | Content_block.Text t ->
5959+ traceln "Assistant (new model): %s" (Content_block.Text.text t)
6060+ | _ -> ()
6161+ ) (Message.Assistant.content msg)
6262+ | _ -> ()
6363+ ) messages;
6464+ with
6565+ | Failure msg -> traceln "Failed to switch model: %s" msg
6666+ | exn -> traceln "Error switching model: %s" (Printexc.to_string exn));
6767+6868+ traceln "\n4. Changing permission mode...";
6969+ (try
7070+ Client.set_permission_mode client Permissions.Mode.Accept_edits;
7171+ traceln "Permission mode changed to Accept_edits";
7272+ with
7373+ | Failure msg -> traceln "Failed to change permission mode: %s" msg
7474+ | exn -> traceln "Error changing permission mode: %s" (Printexc.to_string exn));
7575+7676+ traceln "\n=== Demo Complete ===";
7777+ ()
7878+7979+let () =
8080+ Eio_main.run @@ fun env ->
8181+ try
8282+ run env
8383+ with
8484+ | Transport.CLI_not_found msg ->
8585+ traceln "Error: %s" msg;
8686+ traceln "Make sure the 'claude' CLI is installed and authenticated.";
8787+ exit 1
8888+ | exn ->
8989+ traceln "Unexpected error: %s" (Printexc.to_string exn);
9090+ Printexc.print_backtrace stderr;
9191+ exit 1
+140
test/hooks_example.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "hooks_example" ~doc:"Hooks example"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+(* Example 1: Block dangerous bash commands *)
77+let block_dangerous_bash ~input ~tool_use_id:_ ~context:_ =
88+ let hook = Claude.Hooks.PreToolUse.of_json input in
99+ let tool_name = Claude.Hooks.PreToolUse.tool_name hook in
1010+1111+ if tool_name = "Bash" then
1212+ let tool_input = Claude.Hooks.PreToolUse.tool_input hook in
1313+ match Test_json_utils.get_string tool_input "command" with
1414+ | Some command ->
1515+ if String.length command >= 6 && String.sub command 0 6 = "rm -rf" then begin
1616+ Log.app (fun m -> m "🚫 Blocked dangerous command: %s" command);
1717+ let output = Claude.Hooks.PreToolUse.deny
1818+ ~reason:"Command contains dangerous 'rm -rf' pattern" () in
1919+ Claude.Hooks.continue
2020+ ~system_message:"Blocked dangerous rm -rf command"
2121+ ~hook_specific_output:(Claude.Hooks.PreToolUse.output_to_json output)
2222+ ()
2323+ end else
2424+ Claude.Hooks.continue ()
2525+ | _ ->
2626+ Claude.Hooks.continue ()
2727+ else
2828+ Claude.Hooks.continue ()
2929+3030+(* Example 2: Log all tool usage *)
3131+let log_tool_usage ~input ~tool_use_id ~context:_ =
3232+ let hook = Claude.Hooks.PreToolUse.of_json input in
3333+ let tool_name = Claude.Hooks.PreToolUse.tool_name hook in
3434+ let tool_use_id_str = Option.value tool_use_id ~default:"<none>" in
3535+ Log.app (fun m -> m "📝 Tool %s called (ID: %s)" tool_name tool_use_id_str);
3636+ Claude.Hooks.continue ()
3737+3838+let run_example ~sw ~env =
3939+ Log.app (fun m -> m "🔧 Hooks System Example");
4040+ Log.app (fun m -> m "====================\n");
4141+4242+ (* Configure hooks *)
4343+ let hooks =
4444+ Claude.Hooks.empty
4545+ |> Claude.Hooks.add Claude.Hooks.Pre_tool_use [
4646+ (* Log all tool usage *)
4747+ Claude.Hooks.matcher [log_tool_usage];
4848+ (* Block dangerous bash commands *)
4949+ Claude.Hooks.matcher ~pattern:"Bash" [block_dangerous_bash];
5050+ ]
5151+ in
5252+5353+ let options = Claude.Options.create
5454+ ~model:(Claude.Model.of_string "sonnet")
5555+ ~hooks
5656+ () in
5757+5858+ let client = Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr () in
5959+6060+ (* Test 1: Safe command (should work) *)
6161+ Log.app (fun m -> m "Test 1: Safe bash command");
6262+ Claude.Client.query client "Run the bash command: echo 'Hello from hooks!'";
6363+6464+ let messages = Claude.Client.receive_all client in
6565+ List.iter (fun msg ->
6666+ match msg with
6767+ | Claude.Message.Assistant msg ->
6868+ List.iter (function
6969+ | Claude.Content_block.Text t ->
7070+ let text = Claude.Content_block.Text.text t in
7171+ if String.length text > 0 then
7272+ Log.app (fun m -> m "Claude: %s" text)
7373+ | _ -> ()
7474+ ) (Claude.Message.Assistant.content msg)
7575+ | Claude.Message.Result msg ->
7676+ if Claude.Message.Result.is_error msg then
7777+ Log.err (fun m -> m "❌ Error!")
7878+ else
7979+ Log.app (fun m -> m "✅ Test 1 complete\n")
8080+ | _ -> ()
8181+ ) messages;
8282+8383+ (* Test 2: Dangerous command (should be blocked) *)
8484+ Log.app (fun m -> m "Test 2: Dangerous bash command (should be blocked)");
8585+ Claude.Client.query client "Run the bash command: rm -rf /tmp/test";
8686+8787+ let messages = Claude.Client.receive_all client in
8888+ List.iter (fun msg ->
8989+ match msg with
9090+ | Claude.Message.Assistant msg ->
9191+ List.iter (function
9292+ | Claude.Content_block.Text t ->
9393+ let text = Claude.Content_block.Text.text t in
9494+ if String.length text > 0 then
9595+ Log.app (fun m -> m "Claude: %s" text)
9696+ | _ -> ()
9797+ ) (Claude.Message.Assistant.content msg)
9898+ | Claude.Message.Result msg ->
9999+ if Claude.Message.Result.is_error msg then
100100+ Log.err (fun m -> m "❌ Error!")
101101+ else
102102+ Log.app (fun m -> m "✅ Test 2 complete")
103103+ | _ -> ()
104104+ ) messages;
105105+106106+ Log.app (fun m -> m "\n====================");
107107+ Log.app (fun m -> m "✨ Example complete!")
108108+109109+let main ~env =
110110+ Switch.run @@ fun sw ->
111111+ run_example ~sw ~env
112112+113113+(* Command-line interface *)
114114+open Cmdliner
115115+116116+let main_term env =
117117+ let setup_log style_renderer level =
118118+ Fmt_tty.setup_std_outputs ?style_renderer ();
119119+ Logs.set_level level;
120120+ Logs.set_reporter (Logs_fmt.reporter ());
121121+ if level = None then Logs.set_level (Some Logs.App);
122122+ match level with
123123+ | Some Logs.Info | Some Logs.Debug ->
124124+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
125125+ | _ -> ()
126126+ in
127127+ let run style level =
128128+ setup_log style level;
129129+ main ~env
130130+ in
131131+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
132132+133133+let cmd env =
134134+ let doc = "Demonstrate Claude's hooks system" in
135135+ let info = Cmd.info "hooks_example" ~version:"1.0" ~doc in
136136+ Cmd.v info (main_term env)
137137+138138+let () =
139139+ Eio_main.run @@ fun env ->
140140+ exit (Cmd.eval (cmd env))
+220
test/permission_demo.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "permission_demo" ~doc:"Permission callback demonstration"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+(* Mutable state to track what permissions have been granted *)
77+module Granted = struct
88+ module StringSet = Set.Make(String)
99+ let tools = ref StringSet.empty
1010+1111+ let grant tool_name =
1212+ tools := StringSet.add tool_name !tools;
1313+ Log.app (fun m -> m "✅ Permission granted for: %s" tool_name)
1414+1515+ let deny tool_name =
1616+ Log.app (fun m -> m "❌ Permission denied for: %s" tool_name)
1717+1818+ let is_granted tool_name =
1919+ StringSet.mem tool_name !tools
2020+2121+ let list () =
2222+ if StringSet.is_empty !tools then
2323+ Log.app (fun m -> m "No permissions granted yet")
2424+ else
2525+ Log.app (fun m -> m "Currently granted permissions: %s"
2626+ (StringSet.elements !tools |> String.concat ", "))
2727+end
2828+2929+(* Interactive permission callback *)
3030+let interactive_permission_callback ~tool_name ~input ~context:_ =
3131+ Log.info (fun m -> m "🔔 Permission callback invoked for tool: %s" tool_name);
3232+ Log.app (fun m -> m "\n🔐 PERMISSION REQUEST 🔐");
3333+ Log.app (fun m -> m "Tool: %s" tool_name);
3434+3535+ (* Log the full input for debugging *)
3636+ Log.info (fun m -> m "Full input JSON: %s" (Test_json_utils.to_string input));
3737+3838+ (* Show input details *)
3939+ (* Try to extract key information from the input *)
4040+ (try
4141+ match tool_name with
4242+ | "Read" ->
4343+ (match Test_json_utils.get_string input "file_path" with
4444+ | Some file_path -> Log.app (fun m -> m "File: %s" file_path)
4545+ | None -> ())
4646+ | "Bash" ->
4747+ (match Test_json_utils.get_string input "command" with
4848+ | Some command -> Log.app (fun m -> m "Command: %s" command)
4949+ | None -> ())
5050+ | "Write" | "Edit" ->
5151+ (match Test_json_utils.get_string input "file_path" with
5252+ | Some file_path -> Log.app (fun m -> m "File: %s" file_path)
5353+ | None -> ())
5454+ | "Glob" ->
5555+ (match Test_json_utils.get_string input "pattern" with
5656+ | Some pattern ->
5757+ Log.app (fun m -> m "Pattern: %s" pattern);
5858+ (match Test_json_utils.get_string input "path" with
5959+ | Some path -> Log.app (fun m -> m "Path: %s" path)
6060+ | None -> Log.app (fun m -> m "Path: (current directory)"))
6161+ | None -> ())
6262+ | "Grep" ->
6363+ (match Test_json_utils.get_string input "pattern" with
6464+ | Some pattern ->
6565+ Log.app (fun m -> m "Pattern: %s" pattern);
6666+ (match Test_json_utils.get_string input "path" with
6767+ | Some path -> Log.app (fun m -> m "Path: %s" path)
6868+ | None -> Log.app (fun m -> m "Path: (current directory)"))
6969+ | None -> ())
7070+ | _ ->
7171+ Log.app (fun m -> m "Input: %s" (Test_json_utils.to_string input))
7272+ with exn ->
7373+ Log.info (fun m -> m "Failed to parse input details: %s" (Printexc.to_string exn)));
7474+7575+ (* Check if already granted *)
7676+ if Granted.is_granted tool_name then begin
7777+ Log.app (fun m -> m "→ Auto-approved (previously granted)");
7878+ Log.info (fun m -> m "Returning allow result for %s" tool_name);
7979+ Claude.Permissions.Result.allow ()
8080+ end else begin
8181+ (* Ask user - read from /dev/tty since stdin is connected to Claude process *)
8282+ Printf.printf "Allow? [y/N/always]: %!";
8383+ let tty = open_in "/dev/tty" in
8484+ let response = input_line tty |> String.lowercase_ascii in
8585+ close_in tty;
8686+ match response with
8787+ | "y" | "yes" ->
8888+ Log.app (fun m -> m "→ Allowed (this time only)");
8989+ Log.info (fun m -> m "User approved %s for this request only" tool_name);
9090+ Claude.Permissions.Result.allow ()
9191+ | "a" | "always" ->
9292+ Granted.grant tool_name;
9393+ Log.info (fun m -> m "User granted permanent permission for %s" tool_name);
9494+ Claude.Permissions.Result.allow ()
9595+ | _ ->
9696+ Granted.deny tool_name;
9797+ Log.info (fun m -> m "User denied permission for %s" tool_name);
9898+ Claude.Permissions.Result.deny ~message:(Printf.sprintf "User denied access to %s" tool_name) ~interrupt:false ()
9999+ end
100100+101101+let process_response client =
102102+ let messages = Claude.Client.receive_all client in
103103+ List.iter (fun msg ->
104104+ match msg with
105105+ | Claude.Message.Assistant msg ->
106106+ List.iter (function
107107+ | Claude.Content_block.Text t ->
108108+ let text = Claude.Content_block.Text.text t in
109109+ Log.app (fun m -> m "\n📝 Claude says:\n%s" text)
110110+ | Claude.Content_block.Tool_use t ->
111111+ Log.info (fun m -> m "🔧 Tool use: %s (id: %s)"
112112+ (Claude.Content_block.Tool_use.name t)
113113+ (Claude.Content_block.Tool_use.id t))
114114+ | _ -> ()
115115+ ) (Claude.Message.Assistant.content msg)
116116+ | Claude.Message.Result msg ->
117117+ if Claude.Message.Result.is_error msg then
118118+ Log.err (fun m -> m "❌ Error occurred!")
119119+ else
120120+ (match Claude.Message.Result.total_cost_usd msg with
121121+ | Some cost ->
122122+ Log.info (fun m -> m "💰 Cost: $%.6f" cost)
123123+ | None -> ());
124124+ Log.info (fun m -> m "⏱️ Duration: %dms"
125125+ (Claude.Message.Result.duration_ms msg))
126126+ | _ -> ()
127127+ ) messages
128128+129129+let run_demo ~sw ~env =
130130+ Log.app (fun m -> m "🚀 Starting Permission Demo");
131131+ Log.app (fun m -> m "==================================");
132132+ Log.app (fun m -> m "This demo starts with NO permissions.");
133133+ Log.app (fun m -> m "Claude will request permissions as needed.\n");
134134+135135+ (* Create options with custom permission callback *)
136136+ (* DON'T specify allowed_tools - let the permission callback handle everything.
137137+ The Default permission mode with a callback should send requests for all tools. *)
138138+ let options = Claude.Options.create
139139+ ~model:(Claude.Model.of_string "sonnet")
140140+ ~permission_mode:Claude.Permissions.Mode.Default
141141+ ~permission_callback:interactive_permission_callback
142142+ () in
143143+144144+ let client = Claude.Client.create ~options ~sw
145145+ ~process_mgr:env#process_mgr
146146+ () in
147147+148148+ (* First prompt - Claude will need to request Read permission for ../lib *)
149149+ Log.app (fun m -> m "\n📤 Sending first prompt (reading from ../lib)...");
150150+ Claude.Client.query client
151151+ "Please read and analyze the source files in the ../lib directory. Focus on the main OCaml modules and their purpose. What is the overall architecture of this Claude library?";
152152+ process_response client;
153153+154154+ (* Show current permissions *)
155155+ Log.app (fun m -> m "\n📋 Current permission status:");
156156+ Granted.list ();
157157+158158+ (* Second prompt - will need Write permission *)
159159+ Log.app (fun m -> m "\n📤 Sending second prompt (writing TEST.md)...");
160160+ Claude.Client.query client
161161+ "Now write a summary of what you learned about the Claude library architecture to a file called TEST.md in the current directory. Include the main modules, their purposes, and how they work together.";
162162+ process_response client;
163163+164164+ (* Show final permissions *)
165165+ Log.app (fun m -> m "\n📋 Final permission status:");
166166+ Granted.list ();
167167+168168+ Log.app (fun m -> m "\n==================================");
169169+ Log.app (fun m -> m "✨ Demo complete!")
170170+171171+let main ~env =
172172+ Switch.run @@ fun sw ->
173173+ run_demo ~sw ~env
174174+175175+(* Command-line interface *)
176176+open Cmdliner
177177+178178+let main_term env =
179179+ let setup_log style_renderer level =
180180+ Fmt_tty.setup_std_outputs ?style_renderer ();
181181+ Logs.set_level level;
182182+ Logs.set_reporter (Logs_fmt.reporter ());
183183+ (* Set default to App level if not specified *)
184184+ if level = None then Logs.set_level (Some Logs.App);
185185+ (* Enable info level for Client module if in info mode or above *)
186186+ match level with
187187+ | Some Logs.Info | Some Logs.Debug ->
188188+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
189189+ | _ -> ()
190190+ in
191191+ let run style level =
192192+ setup_log style level;
193193+ main ~env
194194+ in
195195+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
196196+197197+let cmd env =
198198+ let doc = "Demonstrate Claude's dynamic permission system" in
199199+ let man = [
200200+ `S Manpage.s_description;
201201+ `P "This program demonstrates how to use permission callbacks with Claude.";
202202+ `P "It starts with no permissions and asks for them interactively.";
203203+ `P "You can grant permissions for:";
204204+ `P "- Individual requests (y/yes)";
205205+ `P "- All future requests of that type (a/always)";
206206+ `P "- Or deny the request (n/no or just press Enter)";
207207+ `S Manpage.s_examples;
208208+ `P "Run the demo:";
209209+ `Pre " $(mname)";
210210+ `P "Run with verbose output to see message flow:";
211211+ `Pre " $(mname) -v";
212212+ `S Manpage.s_bugs;
213213+ `P "Report bugs at https://github.com/your-repo/issues";
214214+ ] in
215215+ let info = Cmd.info "permission_demo" ~version:"1.0" ~doc ~man in
216216+ Cmd.v info (main_term env)
217217+218218+let () =
219219+ Eio_main.run @@ fun env ->
220220+ exit (Cmd.eval (cmd env))
+185
test/permission_demo.py
···11+#!/usr/bin/env python3
22+# /// script
33+# requires-python = ">=3.9"
44+# dependencies = [
55+# "claude-code-sdk",
66+# ]
77+# ///
88+"""
99+Permission demo for Claude Code SDK Python.
1010+Demonstrates how the permission callback system works.
1111+"""
1212+1313+import asyncio
1414+import sys
1515+import logging
1616+from typing import Any, Dict
1717+1818+from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions
1919+from claude_code_sdk.types import (
2020+ PermissionResultAllow,
2121+ PermissionResultDeny,
2222+ ToolPermissionContext,
2323+)
2424+2525+# Set up logging
2626+logging.basicConfig(
2727+ level=logging.INFO,
2828+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
2929+)
3030+logger = logging.getLogger(__name__)
3131+3232+# Track granted permissions
3333+granted_permissions = set()
3434+3535+3636+async def interactive_permission_callback(
3737+ tool_name: str,
3838+ tool_input: Dict[str, Any],
3939+ context: ToolPermissionContext
4040+) -> PermissionResultAllow | PermissionResultDeny:
4141+ """Interactive permission callback that asks user for permission."""
4242+4343+ logger.info(f"🔔 Permission callback invoked for tool: {tool_name}")
4444+ print(f"\n🔐 PERMISSION REQUEST 🔐")
4545+ print(f"Tool: {tool_name}")
4646+4747+ # Log the full input for debugging
4848+ logger.info(f"Full input: {tool_input}")
4949+5050+ # Show input details
5151+ try:
5252+ if tool_name == "Read":
5353+ file_path = tool_input.get("file_path", "")
5454+ print(f"File: {file_path}")
5555+ elif tool_name == "Bash":
5656+ command = tool_input.get("command", "")
5757+ print(f"Command: {command}")
5858+ elif tool_name in ["Write", "Edit"]:
5959+ file_path = tool_input.get("file_path", "")
6060+ print(f"File: {file_path}")
6161+ elif tool_name == "Glob":
6262+ pattern = tool_input.get("pattern", "")
6363+ path = tool_input.get("path", "(current directory)")
6464+ print(f"Pattern: {pattern}")
6565+ print(f"Path: {path}")
6666+ elif tool_name == "Grep":
6767+ pattern = tool_input.get("pattern", "")
6868+ path = tool_input.get("path", "(current directory)")
6969+ print(f"Pattern: {pattern}")
7070+ print(f"Path: {path}")
7171+ else:
7272+ print(f"Input: {tool_input}")
7373+ except Exception as e:
7474+ logger.info(f"Failed to parse input details: {e}")
7575+7676+ # Check if already granted
7777+ if tool_name in granted_permissions:
7878+ print("→ Auto-approved (previously granted)")
7979+ logger.info(f"Returning allow result for {tool_name}")
8080+ return PermissionResultAllow()
8181+8282+ # Ask user
8383+ response = input("Allow? [y/N/always]: ").lower().strip()
8484+8585+ if response in ["y", "yes"]:
8686+ print("→ Allowed (this time only)")
8787+ logger.info(f"User approved {tool_name} for this request only")
8888+ return PermissionResultAllow()
8989+ elif response in ["a", "always"]:
9090+ granted_permissions.add(tool_name)
9191+ print(f"✅ Permission granted for: {tool_name}")
9292+ logger.info(f"User granted permanent permission for {tool_name}")
9393+ return PermissionResultAllow()
9494+ else:
9595+ print(f"❌ Permission denied for: {tool_name}")
9696+ logger.info(f"User denied permission for {tool_name}")
9797+ return PermissionResultDeny(
9898+ message=f"User denied access to {tool_name}",
9999+ interrupt=False
100100+ )
101101+102102+103103+async def run_demo():
104104+ """Run the permission demo."""
105105+ print("🚀 Starting Permission Demo")
106106+ print("==================================")
107107+ print("This demo starts with NO permissions.")
108108+ print("Claude will request permissions as needed.\n")
109109+110110+ # Create options with custom permission callback
111111+ # Test WITHOUT allowed_tools to see if permission requests come through
112112+ options = ClaudeCodeOptions(
113113+ model="sonnet",
114114+ # allowed_tools=["Read", "Write", "Bash", "Edit", "Glob", "Grep"],
115115+ can_use_tool=interactive_permission_callback,
116116+ )
117117+118118+ async with ClaudeSDKClient(options=options) as client:
119119+ # First prompt - Claude will need to request Read permission
120120+ print("\n📤 Sending first prompt (reading from ../lib)...")
121121+ messages = []
122122+ await client.query(
123123+ "Please read and analyze the source files in the ../lib directory. "
124124+ "Focus on the main OCaml modules and their purpose. "
125125+ "What is the overall architecture of this Claude library?"
126126+ )
127127+128128+ async for msg in client.receive_response():
129129+ messages.append(msg)
130130+ if hasattr(msg, 'content'):
131131+ if isinstance(msg.content, str):
132132+ print(f"\n📝 Claude says:\n{msg.content}")
133133+ elif isinstance(msg.content, list):
134134+ for block in msg.content:
135135+ if hasattr(block, 'text'):
136136+ print(f"\n📝 Claude says:\n{block.text}")
137137+138138+ # Show current permissions
139139+ print("\n📋 Current permission status:")
140140+ if granted_permissions:
141141+ print(f"Currently granted permissions: {', '.join(granted_permissions)}")
142142+ else:
143143+ print("No permissions granted yet")
144144+145145+ # Second prompt - will need Write permission
146146+ print("\n📤 Sending second prompt (writing TEST.md)...")
147147+ await client.query(
148148+ "Now write a summary of what you learned about the Claude library "
149149+ "architecture to a file called TEST.md in the current directory. "
150150+ "Include the main modules, their purposes, and how they work together."
151151+ )
152152+153153+ async for msg in client.receive_response():
154154+ if hasattr(msg, 'content'):
155155+ if isinstance(msg.content, str):
156156+ print(f"\n📝 Claude says:\n{msg.content}")
157157+ elif isinstance(msg.content, list):
158158+ for block in msg.content:
159159+ if hasattr(block, 'text'):
160160+ print(f"\n📝 Claude says:\n{block.text}")
161161+162162+ # Show final permissions
163163+ print("\n📋 Final permission status:")
164164+ if granted_permissions:
165165+ print(f"Currently granted permissions: {', '.join(granted_permissions)}")
166166+ else:
167167+ print("No permissions granted yet")
168168+169169+ print("\n==================================")
170170+ print("✨ Demo complete!")
171171+172172+173173+async def main():
174174+ """Main entry point."""
175175+ try:
176176+ await run_demo()
177177+ except KeyboardInterrupt:
178178+ print("\n\nDemo interrupted by user.")
179179+ except Exception as e:
180180+ logger.error(f"Error in demo: {e}", exc_info=True)
181181+ sys.exit(1)
182182+183183+184184+if __name__ == "__main__":
185185+ asyncio.run(main())
+3
test/secret_data.txt
···11+The secret code is: OCAML-2024-ROCKS
22+This file was created specifically for the permission demo.
33+Claude should not know about this content without reading the file.
+131
test/simple_permission_test.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "simple_permission_test" ~doc:"Simple permission test"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+(* Auto-allow callback that logs what it sees *)
77+let auto_allow_callback ~tool_name ~input ~context:_ =
88+ Log.app (fun m -> m "\n🔐 Permission callback invoked!");
99+ Log.app (fun m -> m " Tool: %s" tool_name);
1010+ Log.app (fun m -> m " Input: %s" (Test_json_utils.to_string input));
1111+ Log.app (fun m -> m " ✅ Auto-allowing");
1212+ Claude.Permissions.Result.allow ()
1313+1414+let run_test ~sw ~env =
1515+ Log.app (fun m -> m "🧪 Testing Permission Callbacks (Auto-Allow Mode)");
1616+ Log.app (fun m -> m "====================================================");
1717+1818+ (* Create options with permission callback *)
1919+ let options = Claude.Options.create
2020+ ~model:(Claude.Model.of_string "sonnet")
2121+ ~permission_callback:auto_allow_callback
2222+ () in
2323+2424+ Log.app (fun m -> m "Creating client with permission callback...");
2525+ let client = Claude.Client.create ~options ~sw
2626+ ~process_mgr:env#process_mgr
2727+ () in
2828+2929+ (* Query that should trigger Write tool *)
3030+ Log.app (fun m -> m "\n📤 Asking Claude to write a file...");
3131+ Claude.Client.query client
3232+ "Write a simple hello world message to /tmp/test_permission.txt";
3333+3434+ (* Process response *)
3535+ let messages = Claude.Client.receive_all client in
3636+ Log.app (fun m -> m "\n📨 Received %d messages" (List.length messages));
3737+3838+ let tool_count = ref 0 in
3939+ let write_used = ref false in
4040+4141+ List.iter (fun msg ->
4242+ match msg with
4343+ | Claude.Message.Assistant msg ->
4444+ List.iter (function
4545+ | Claude.Content_block.Text t ->
4646+ let text = Claude.Content_block.Text.text t in
4747+ if String.length text > 0 then
4848+ Log.app (fun m -> m "\n💬 Claude: %s" text)
4949+ | Claude.Content_block.Tool_use t ->
5050+ incr tool_count;
5151+ let tool_name = Claude.Content_block.Tool_use.name t in
5252+ if tool_name = "Write" then write_used := true;
5353+ Log.app (fun m -> m "🔧 Tool use #%d: %s" !tool_count tool_name)
5454+ | _ -> ()
5555+ ) (Claude.Message.Assistant.content msg)
5656+ | Claude.Message.User msg ->
5757+ (* Check for tool results which might have errors *)
5858+ (match Claude.Message.User.content msg with
5959+ | Claude.Message.User.Blocks blocks ->
6060+ List.iter (function
6161+ | Claude.Content_block.Tool_result r ->
6262+ let tool_use_id = Claude.Content_block.Tool_result.tool_use_id r in
6363+ let is_error = Claude.Content_block.Tool_result.is_error r |> Option.value ~default:false in
6464+ if is_error then begin
6565+ Log.app (fun m -> m "\n⚠️ Tool result error for %s:" tool_use_id);
6666+ (match Claude.Content_block.Tool_result.content r with
6767+ | Some s -> Log.app (fun m -> m " %s" s)
6868+ | None -> ())
6969+ end
7070+ | _ -> ()
7171+ ) blocks
7272+ | _ -> ())
7373+ | Claude.Message.Result msg ->
7474+ if Claude.Message.Result.is_error msg then
7575+ Log.err (fun m -> m "\n❌ Error occurred!")
7676+ else
7777+ Log.app (fun m -> m "\n✅ Success!");
7878+ (match Claude.Message.Result.total_cost_usd msg with
7979+ | Some cost -> Log.app (fun m -> m "💰 Cost: $%.6f" cost)
8080+ | None -> ());
8181+ Log.app (fun m -> m "⏱️ Duration: %dms"
8282+ (Claude.Message.Result.duration_ms msg))
8383+ | _ -> ()
8484+ ) messages;
8585+8686+ Log.app (fun m -> m "\n====================================================");
8787+ Log.app (fun m -> m "📊 Test Results:");
8888+ Log.app (fun m -> m " Total tools used: %d" !tool_count);
8989+ Log.app (fun m -> m " Write tool used: %b" !write_used);
9090+9191+ if !write_used then
9292+ Log.app (fun m -> m " ✅ Permission callback successfully intercepted Write tool!")
9393+ else
9494+ Log.app (fun m -> m " ⚠️ Write tool was not used (unexpected)");
9595+9696+ Log.app (fun m -> m "====================================================");
9797+ Log.app (fun m -> m "✨ Test complete!")
9898+9999+let main ~env =
100100+ Switch.run @@ fun sw ->
101101+ run_test ~sw ~env
102102+103103+(* Command-line interface *)
104104+open Cmdliner
105105+106106+let main_term env =
107107+ let setup_log style_renderer level =
108108+ Fmt_tty.setup_std_outputs ?style_renderer ();
109109+ Logs.set_level level;
110110+ Logs.set_reporter (Logs_fmt.reporter ());
111111+ if level = None then Logs.set_level (Some Logs.App);
112112+ match level with
113113+ | Some Logs.Info | Some Logs.Debug ->
114114+ Logs.Src.set_level Claude.Client.src (Some Logs.Info);
115115+ Logs.Src.set_level Claude.Transport.src (Some Logs.Info)
116116+ | _ -> ()
117117+ in
118118+ let run style level =
119119+ setup_log style level;
120120+ main ~env
121121+ in
122122+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
123123+124124+let cmd env =
125125+ let doc = "Test permission callback with auto-allow" in
126126+ let info = Cmd.info "simple_permission_test" ~version:"1.0" ~doc in
127127+ Cmd.v info (main_term env)
128128+129129+let () =
130130+ Eio_main.run @@ fun env ->
131131+ exit (Cmd.eval (cmd env))
+190
test/simulated_permissions.ml
···11+let src = Logs.Src.create "simulated_permissions" ~doc:"Simulated permission demonstration"
22+module Log = (val Logs.src_log src : Logs.LOG)
33+44+(* Track granted permissions *)
55+module PermissionState = struct
66+ module StringSet = Set.Make(String)
77+ let granted = ref StringSet.empty
88+ let denied = ref StringSet.empty
99+1010+ let grant tool =
1111+ granted := StringSet.add tool !granted;
1212+ denied := StringSet.remove tool !denied
1313+1414+ let deny tool =
1515+ denied := StringSet.add tool !denied;
1616+ granted := StringSet.remove tool !granted
1717+1818+ let is_granted tool = StringSet.mem tool !granted
1919+ let is_denied tool = StringSet.mem tool !denied
2020+2121+ let _reset () =
2222+ granted := StringSet.empty;
2323+ denied := StringSet.empty
2424+2525+ let show () =
2626+ Log.app (fun m -> m "\n📊 Permission Status:");
2727+ if StringSet.is_empty !granted && StringSet.is_empty !denied then
2828+ Log.app (fun m -> m " No permissions configured")
2929+ else begin
3030+ if not (StringSet.is_empty !granted) then
3131+ Log.app (fun m -> m " ✅ Granted: %s"
3232+ (StringSet.elements !granted |> String.concat ", "));
3333+ if not (StringSet.is_empty !denied) then
3434+ Log.app (fun m -> m " ❌ Denied: %s"
3535+ (StringSet.elements !denied |> String.concat ", "))
3636+ end
3737+end
3838+3939+(* Example permission callback *)
4040+let example_permission_callback ~tool_name ~input:_ ~context:_ =
4141+ Log.app (fun m -> m "\n🔐 Permission Request for: %s" tool_name);
4242+4343+ (* Check current state *)
4444+ if PermissionState.is_granted tool_name then begin
4545+ Log.app (fun m -> m " → Auto-approved (previously granted)");
4646+ Claude.Permissions.Result.allow ()
4747+ end else if PermissionState.is_denied tool_name then begin
4848+ Log.app (fun m -> m " → Auto-denied (previously denied)");
4949+ Claude.Permissions.Result.deny
5050+ ~message:(Printf.sprintf "Tool %s is blocked by policy" tool_name)
5151+ ~interrupt:false ()
5252+ end else begin
5353+ (* Ask user *)
5454+ Printf.printf " Allow %s? [y/n/always/never]: %!" tool_name;
5555+ match read_line () |> String.lowercase_ascii with
5656+ | "y" | "yes" ->
5757+ Log.app (fun m -> m " → Allowed (one time)");
5858+ Claude.Permissions.Result.allow ()
5959+ | "n" | "no" ->
6060+ Log.app (fun m -> m " → Denied (one time)");
6161+ Claude.Permissions.Result.deny
6262+ ~message:(Printf.sprintf "User denied %s" tool_name)
6363+ ~interrupt:false ()
6464+ | "a" | "always" ->
6565+ PermissionState.grant tool_name;
6666+ Log.app (fun m -> m " → Allowed (always)");
6767+ Claude.Permissions.Result.allow ()
6868+ | "never" ->
6969+ PermissionState.deny tool_name;
7070+ Log.app (fun m -> m " → Denied (always)");
7171+ Claude.Permissions.Result.deny
7272+ ~message:(Printf.sprintf "Tool %s permanently blocked" tool_name)
7373+ ~interrupt:false ()
7474+ | _ ->
7575+ Log.app (fun m -> m " → Denied (invalid response)");
7676+ Claude.Permissions.Result.deny
7777+ ~message:"Invalid permission response"
7878+ ~interrupt:false ()
7979+ end
8080+8181+(* Demonstrate the permission system *)
8282+let demo_permissions () =
8383+ Log.app (fun m -> m "🎭 Permission System Demonstration");
8484+ Log.app (fun m -> m "==================================\n");
8585+8686+ (* Simulate permission requests *)
8787+ let tools = ["Read"; "Write"; "Bash"; "Edit"] in
8888+ let context = Claude.Permissions.Context.create () in
8989+9090+ Log.app (fun m -> m "This demo simulates permission requests.");
9191+ Log.app (fun m -> m "You can respond with: y/n/always/never\n");
9292+9393+ (* Test each tool *)
9494+ List.iter (fun tool ->
9595+ let input =
9696+ let open Jsont in
9797+ Object ([
9898+ (("file_path", Meta.none), String ("/example/path.txt", Meta.none))
9999+ ], Meta.none)
100100+ in
101101+ let result = example_permission_callback
102102+ ~tool_name:tool ~input ~context in
103103+104104+ (* Show result *)
105105+ (match result with
106106+ | Claude.Permissions.Result.Allow _ ->
107107+ Log.info (fun m -> m "Result: Permission granted for %s" tool)
108108+ | Claude.Permissions.Result.Deny { message; _ } ->
109109+ Log.info (fun m -> m "Result: Permission denied for %s - %s" tool message))
110110+ ) tools;
111111+112112+ (* Show final state *)
113113+ PermissionState.show ()
114114+115115+(* Also demonstrate discovery callback *)
116116+let demo_discovery () =
117117+ Log.app (fun m -> m "\n\n🔍 Discovery Callback Demonstration");
118118+ Log.app (fun m -> m "====================================\n");
119119+120120+ let discovered = ref [] in
121121+ let callback = Claude.Permissions.discovery_callback discovered in
122122+123123+ (* Simulate some tool requests *)
124124+ let requests =
125125+ let open Jsont in
126126+ [
127127+ ("Read", Object ([
128128+ (("file_path", Meta.none), String ("test.ml", Meta.none))
129129+ ], Meta.none));
130130+ ("Bash", Object ([
131131+ (("command", Meta.none), String ("ls -la", Meta.none))
132132+ ], Meta.none));
133133+ ("Write", Object ([
134134+ (("file_path", Meta.none), String ("output.txt", Meta.none))
135135+ ], Meta.none));
136136+ ]
137137+ in
138138+139139+ Log.app (fun m -> m "Simulating tool requests with discovery callback...\n");
140140+141141+ List.iter (fun (tool, input) ->
142142+ Log.app (fun m -> m " Request: %s" tool);
143143+ let context = Claude.Permissions.Context.create () in
144144+ let _ = callback ~tool_name:tool ~input ~context in
145145+ ()
146146+ ) requests;
147147+148148+ Log.app (fun m -> m "\n📋 Discovered permissions:");
149149+ if !discovered = [] then
150150+ Log.app (fun m -> m " None")
151151+ else
152152+ List.iter (fun rule ->
153153+ Log.app (fun m -> m " - %s%s"
154154+ (Claude.Permissions.Rule.tool_name rule)
155155+ (match Claude.Permissions.Rule.rule_content rule with
156156+ | Some content -> Printf.sprintf " (content: %s)" content
157157+ | None -> ""))
158158+ ) !discovered
159159+160160+let main () =
161161+ demo_permissions ();
162162+ demo_discovery ()
163163+164164+(* Command-line interface *)
165165+open Cmdliner
166166+167167+let main_term =
168168+ let setup_log style_renderer level =
169169+ Fmt_tty.setup_std_outputs ?style_renderer ();
170170+ Logs.set_level level;
171171+ Logs.set_reporter (Logs_fmt.reporter ());
172172+ if level = None then Logs.set_level (Some Logs.App)
173173+ in
174174+ let run style level =
175175+ setup_log style level;
176176+ main ()
177177+ in
178178+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
179179+180180+let cmd =
181181+ let doc = "Demonstrate permission callbacks and discovery" in
182182+ let man = [
183183+ `S Manpage.s_description;
184184+ `P "This program demonstrates how permission callbacks work in the Claude OCaml library.";
185185+ `P "It simulates permission requests and shows how to implement custom callbacks.";
186186+ ] in
187187+ let info = Cmd.info "simulated_permissions" ~version:"1.0" ~doc ~man in
188188+ Cmd.v info main_term
189189+190190+let () = exit (Cmd.eval cmd)
+172
test/structured_output_demo.ml
···11+(* Example demonstrating structured output with JSON Schema *)
22+33+module C = Claude
44+55+let () =
66+ (* Configure logging to see what's happening *)
77+ Logs.set_reporter (Logs_fmt.reporter ());
88+ Logs.set_level (Some Logs.Info);
99+ Logs.Src.set_level C.Message.src (Some Logs.Debug)
1010+1111+let run_codebase_analysis env =
1212+ Printf.printf "\n=== Codebase Analysis with Structured Output ===\n\n";
1313+1414+ (* Define the JSON Schema for our expected output structure *)
1515+ let analysis_schema =
1616+ let open Jsont in
1717+ Object ([
1818+ (("type", Meta.none), String ("object", Meta.none));
1919+ (("properties", Meta.none), Object ([
2020+ (("file_count", Meta.none), Object ([
2121+ (("type", Meta.none), String ("integer", Meta.none));
2222+ (("description", Meta.none), String ("Total number of files analyzed", Meta.none))
2323+ ], Meta.none));
2424+ (("has_tests", Meta.none), Object ([
2525+ (("type", Meta.none), String ("boolean", Meta.none));
2626+ (("description", Meta.none), String ("Whether the codebase has test files", Meta.none))
2727+ ], Meta.none));
2828+ (("primary_language", Meta.none), Object ([
2929+ (("type", Meta.none), String ("string", Meta.none));
3030+ (("description", Meta.none), String ("The primary programming language used", Meta.none))
3131+ ], Meta.none));
3232+ (("complexity_rating", Meta.none), Object ([
3333+ (("type", Meta.none), String ("string", Meta.none));
3434+ (("enum", Meta.none), Array ([
3535+ String ("low", Meta.none);
3636+ String ("medium", Meta.none);
3737+ String ("high", Meta.none)
3838+ ], Meta.none));
3939+ (("description", Meta.none), String ("Overall complexity rating", Meta.none))
4040+ ], Meta.none));
4141+ (("key_findings", Meta.none), Object ([
4242+ (("type", Meta.none), String ("array", Meta.none));
4343+ (("items", Meta.none), Object ([
4444+ (("type", Meta.none), String ("string", Meta.none))
4545+ ], Meta.none));
4646+ (("description", Meta.none), String ("List of key findings from the analysis", Meta.none))
4747+ ], Meta.none));
4848+ ], Meta.none));
4949+ (("required", Meta.none), Array ([
5050+ String ("file_count", Meta.none);
5151+ String ("has_tests", Meta.none);
5252+ String ("primary_language", Meta.none);
5353+ String ("complexity_rating", Meta.none);
5454+ String ("key_findings", Meta.none)
5555+ ], Meta.none));
5656+ (("additionalProperties", Meta.none), Bool (false, Meta.none))
5757+ ], Meta.none)
5858+ in
5959+6060+ (* Create structured output format from the schema *)
6161+ let output_format = C.Structured_output.of_json_schema analysis_schema in
6262+6363+ (* Configure Claude with structured output *)
6464+ let options = C.Options.default
6565+ |> C.Options.with_output_format output_format
6666+ |> C.Options.with_allowed_tools ["Read"; "Glob"; "Grep"]
6767+ |> C.Options.with_system_prompt
6868+ "You are a code analysis assistant. Analyze codebases and provide \
6969+ structured output matching the given JSON Schema."
7070+ in
7171+7272+ Printf.printf "Structured output format configured\n";
7373+ Printf.printf "Schema: %s\n\n"
7474+ (Test_json_utils.to_string ~minify:false analysis_schema);
7575+7676+ (* Create Claude client and query *)
7777+ Eio.Switch.run @@ fun sw ->
7878+ let process_mgr = Eio.Stdenv.process_mgr env in
7979+ let client = C.Client.create ~sw ~process_mgr ~options () in
8080+8181+ let prompt =
8282+ "Please analyze the current codebase structure. Look at the files, \
8383+ identify the primary language, count files, check for tests, assess \
8484+ complexity, and provide key findings. Return your analysis in the \
8585+ structured JSON format I specified."
8686+ in
8787+8888+ Printf.printf "Sending query: %s\n\n" prompt;
8989+ C.Client.query client prompt;
9090+9191+ (* Process responses *)
9292+ let messages = C.Client.receive client in
9393+ Seq.iter (function
9494+ | C.Message.Assistant msg ->
9595+ Printf.printf "\nAssistant response:\n";
9696+ List.iter (function
9797+ | C.Content_block.Text text ->
9898+ Printf.printf " Text: %s\n" (C.Content_block.Text.text text)
9999+ | C.Content_block.Tool_use tool ->
100100+ Printf.printf " Using tool: %s\n" (C.Content_block.Tool_use.name tool)
101101+ | _ -> ()
102102+ ) (C.Message.Assistant.content msg)
103103+104104+ | C.Message.Result result ->
105105+ Printf.printf "\n=== Result ===\n";
106106+ Printf.printf "Duration: %dms\n" (C.Message.Result.duration_ms result);
107107+ Printf.printf "Cost: $%.4f\n"
108108+ (Option.value (C.Message.Result.total_cost_usd result) ~default:0.0);
109109+110110+ (* Extract and display structured output *)
111111+ (match C.Message.Result.structured_output result with
112112+ | Some output ->
113113+ Printf.printf "\n=== Structured Output ===\n";
114114+ Printf.printf "%s\n\n" (Test_json_utils.to_string ~minify:false output);
115115+116116+ (* Parse the structured output *)
117117+ let file_count = Test_json_utils.get_int output "file_count" |> Option.value ~default:0 in
118118+ let has_tests = Test_json_utils.get_bool output "has_tests" |> Option.value ~default:false in
119119+ let language = Test_json_utils.get_string output "primary_language" |> Option.value ~default:"unknown" in
120120+ let complexity = Test_json_utils.get_string output "complexity_rating" |> Option.value ~default:"unknown" in
121121+ let findings =
122122+ match Test_json_utils.get_array output "key_findings" with
123123+ | Some items ->
124124+ List.filter_map (fun json ->
125125+ Test_json_utils.as_string json
126126+ ) items
127127+ | None -> []
128128+ in
129129+130130+ Printf.printf "=== Parsed Analysis ===\n";
131131+ Printf.printf "File Count: %d\n" file_count;
132132+ Printf.printf "Has Tests: %b\n" has_tests;
133133+ Printf.printf "Primary Language: %s\n" language;
134134+ Printf.printf "Complexity: %s\n" complexity;
135135+ Printf.printf "Key Findings:\n";
136136+ List.iter (fun finding ->
137137+ Printf.printf " - %s\n" finding
138138+ ) findings
139139+140140+ | None ->
141141+ Printf.printf "No structured output received\n";
142142+ (match C.Message.Result.result result with
143143+ | Some text -> Printf.printf "Text result: %s\n" text
144144+ | None -> ()))
145145+146146+ | C.Message.System sys ->
147147+ (match C.Message.System.subtype sys with
148148+ | "init" ->
149149+ Printf.printf "Session initialized\n"
150150+ | _ -> ())
151151+152152+ | _ -> ()
153153+ ) messages;
154154+155155+ Printf.printf "\nDone!\n"
156156+157157+let () =
158158+ Eio_main.run @@ fun env ->
159159+ try
160160+ run_codebase_analysis env
161161+ with
162162+ | C.Transport.CLI_not_found msg ->
163163+ Printf.eprintf "Error: Claude CLI not found\n%s\n" msg;
164164+ Printf.eprintf "Make sure 'claude' is installed and in your PATH\n";
165165+ exit 1
166166+ | C.Transport.Connection_error msg ->
167167+ Printf.eprintf "Connection error: %s\n" msg;
168168+ exit 1
169169+ | exn ->
170170+ Printf.eprintf "Unexpected error: %s\n" (Printexc.to_string exn);
171171+ Printexc.print_backtrace stderr;
172172+ exit 1
+72
test/structured_output_simple.ml
···11+(* Simple example showing structured output with explicit JSON Schema *)
22+33+module C = Claude
44+55+let () =
66+ Logs.set_reporter (Logs_fmt.reporter ());
77+ Logs.set_level (Some Logs.Info)
88+99+let simple_example env =
1010+ Printf.printf "\n=== Simple Structured Output Example ===\n\n";
1111+1212+ (* Define a simple schema for a person's info *)
1313+ let person_schema =
1414+ let open Jsont in
1515+ Object ([
1616+ (("type", Meta.none), String ("object", Meta.none));
1717+ (("properties", Meta.none), Object ([
1818+ (("name", Meta.none), Object ([
1919+ (("type", Meta.none), String ("string", Meta.none))
2020+ ], Meta.none));
2121+ (("age", Meta.none), Object ([
2222+ (("type", Meta.none), String ("integer", Meta.none))
2323+ ], Meta.none));
2424+ (("occupation", Meta.none), Object ([
2525+ (("type", Meta.none), String ("string", Meta.none))
2626+ ], Meta.none));
2727+ ], Meta.none));
2828+ (("required", Meta.none), Array ([
2929+ String ("name", Meta.none);
3030+ String ("age", Meta.none);
3131+ String ("occupation", Meta.none)
3232+ ], Meta.none))
3333+ ], Meta.none)
3434+ in
3535+3636+ let output_format = C.Structured_output.of_json_schema person_schema in
3737+3838+ let options = C.Options.default
3939+ |> C.Options.with_output_format output_format
4040+ |> C.Options.with_max_turns 1
4141+ in
4242+4343+ Printf.printf "Asking Claude to provide structured data...\n\n";
4444+4545+ Eio.Switch.run @@ fun sw ->
4646+ let process_mgr = Eio.Stdenv.process_mgr env in
4747+ let client = C.Client.create ~sw ~process_mgr ~options () in
4848+4949+ C.Client.query client
5050+ "Tell me about a famous computer scientist. Provide their name, age, \
5151+ and occupation in the exact JSON structure I specified.";
5252+5353+ let messages = C.Client.receive_all client in
5454+ List.iter (function
5555+ | C.Message.Result result ->
5656+ Printf.printf "Response received!\n";
5757+ (match C.Message.Result.structured_output result with
5858+ | Some json ->
5959+ Printf.printf "\nStructured Output:\n%s\n"
6060+ (Test_json_utils.to_string ~minify:false json)
6161+ | None ->
6262+ Printf.printf "No structured output\n")
6363+ | _ -> ()
6464+ ) messages
6565+6666+let () =
6767+ Eio_main.run @@ fun env ->
6868+ try
6969+ simple_example env
7070+ with exn ->
7171+ Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
7272+ exit 1
+78
test/test_incoming.ml
···11+(** Test the Incoming message codec *)
22+33+open Claude
44+55+let test_decode_user_message () =
66+ let json_str = {|{"type":"user","content":"Hello"}|} in
77+ match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
88+ | Ok (Incoming.Message (Message.User _)) ->
99+ print_endline "✓ Decoded user message successfully"
1010+ | Ok _ ->
1111+ print_endline "✗ Wrong message type decoded"
1212+ | Error err ->
1313+ Printf.printf "✗ Failed to decode user message: %s\n" (Jsont.Error.to_string err)
1414+1515+let test_decode_assistant_message () =
1616+ let json_str = {|{"type":"assistant","model":"claude-sonnet-4","content":[{"type":"text","text":"Hi"}]}|} in
1717+ match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
1818+ | Ok (Incoming.Message (Message.Assistant _)) ->
1919+ print_endline "✓ Decoded assistant message successfully"
2020+ | Ok _ ->
2121+ print_endline "✗ Wrong message type decoded"
2222+ | Error err ->
2323+ Printf.printf "✗ Failed to decode assistant message: %s\n" (Jsont.Error.to_string err)
2424+2525+let test_decode_system_message () =
2626+ let json_str = {|{"type":"system","subtype":"init","data":{"session_id":"test-123"}}|} in
2727+ match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
2828+ | Ok (Incoming.Message (Message.System _)) ->
2929+ print_endline "✓ Decoded system message successfully"
3030+ | Ok _ ->
3131+ print_endline "✗ Wrong message type decoded"
3232+ | Error err ->
3333+ Printf.printf "✗ Failed to decode system message: %s\n" (Jsont.Error.to_string err)
3434+3535+let test_decode_control_response () =
3636+ let json_str = {|{"type":"control_response","response":{"subtype":"success","request_id":"test-req-1"}}|} in
3737+ match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
3838+ | Ok (Incoming.Control_response resp) ->
3939+ (match resp.response with
4040+ | Sdk_control.Response.Success s ->
4141+ if s.request_id = "test-req-1" then
4242+ print_endline "✓ Decoded control response successfully"
4343+ else
4444+ Printf.printf "✗ Wrong request_id: %s\n" s.request_id
4545+ | Sdk_control.Response.Error _ ->
4646+ print_endline "✗ Got error response instead of success")
4747+ | Ok _ ->
4848+ print_endline "✗ Wrong message type decoded"
4949+ | Error err ->
5050+ Printf.printf "✗ Failed to decode control response: %s\n" (Jsont.Error.to_string err)
5151+5252+let test_decode_control_response_error () =
5353+ let json_str = {|{"type":"control_response","response":{"subtype":"error","request_id":"test-req-2","error":"Something went wrong"}}|} in
5454+ match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
5555+ | Ok (Incoming.Control_response resp) ->
5656+ (match resp.response with
5757+ | Sdk_control.Response.Error e ->
5858+ if e.request_id = "test-req-2" && e.error = "Something went wrong" then
5959+ print_endline "✓ Decoded control error response successfully"
6060+ else
6161+ Printf.printf "✗ Wrong error content\n"
6262+ | Sdk_control.Response.Success _ ->
6363+ print_endline "✗ Got success response instead of error")
6464+ | Ok _ ->
6565+ print_endline "✗ Wrong message type decoded"
6666+ | Error err ->
6767+ Printf.printf "✗ Failed to decode control error response: %s\n" (Jsont.Error.to_string err)
6868+6969+let () =
7070+ print_endline "Testing Incoming message codec...";
7171+ print_endline "";
7272+ test_decode_user_message ();
7373+ test_decode_assistant_message ();
7474+ test_decode_system_message ();
7575+ test_decode_control_response ();
7676+ test_decode_control_response_error ();
7777+ print_endline "";
7878+ print_endline "All tests completed!"
+29
test/test_json_utils.ml
···11+(* Helper functions for JSON operations in tests using jsont codecs *)
22+33+let to_string ?(minify=false) json =
44+ let format = if minify then Jsont.Minify else Jsont.Indent in
55+ match Jsont_bytesrw.encode_string' ~format Jsont.json json with
66+ | Ok s -> s
77+ | Error err -> Jsont.Error.to_string err
88+99+(* Helper to decode an optional field with a given codec *)
1010+let get_opt (type a) (codec : a Jsont.t) json key : a option =
1111+ let field_codec = Jsont.Object.map ~kind:"field" (fun v -> v)
1212+ |> Jsont.Object.opt_mem key codec ~enc:Fun.id
1313+ |> Jsont.Object.finish
1414+ in
1515+ match Jsont.Json.decode field_codec json with
1616+ | Ok v -> v
1717+ | Error _ -> None
1818+1919+let get_string json key = get_opt Jsont.string json key
2020+let get_int json key = get_opt Jsont.int json key
2121+let get_bool json key = get_opt Jsont.bool json key
2222+2323+let get_array json key =
2424+ get_opt (Jsont.list Jsont.json) json key
2525+2626+let as_string json =
2727+ match Jsont.Json.decode Jsont.string json with
2828+ | Ok s -> Some s
2929+ | Error _ -> None
+91
test/test_permissions.ml
···11+open Eio.Std
22+33+let src = Logs.Src.create "test_permissions" ~doc:"Permission callback test"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+(* Simple auto-allow permission callback *)
77+let auto_allow_callback ~tool_name ~input:_ ~context:_ =
88+ Log.app (fun m -> m "✅ Auto-allowing tool: %s" tool_name);
99+ Claude.Permissions.Result.allow ()
1010+1111+let run_test ~sw ~env =
1212+ Log.app (fun m -> m "🧪 Testing Permission Callbacks");
1313+ Log.app (fun m -> m "================================");
1414+1515+ (* Create options with custom permission callback *)
1616+ let options = Claude.Options.create
1717+ ~model:(Claude.Model.of_string "sonnet")
1818+ ~permission_callback:auto_allow_callback
1919+ () in
2020+2121+ Log.app (fun m -> m "Creating client with permission callback...");
2222+ let client = Claude.Client.create ~options ~sw
2323+ ~process_mgr:env#process_mgr
2424+ () in
2525+2626+ (* Simple query that will trigger tool use *)
2727+ Log.app (fun m -> m "\n📤 Sending test query...");
2828+ Claude.Client.query client
2929+ "What is 2 + 2? Just give me the number.";
3030+3131+ (* Process response *)
3232+ let messages = Claude.Client.receive_all client in
3333+ Log.app (fun m -> m "\n📨 Received %d messages" (List.length messages));
3434+3535+ List.iter (fun msg ->
3636+ match msg with
3737+ | Claude.Message.Assistant msg ->
3838+ List.iter (function
3939+ | Claude.Content_block.Text t ->
4040+ let text = Claude.Content_block.Text.text t in
4141+ Log.app (fun m -> m "Claude: %s" text)
4242+ | Claude.Content_block.Tool_use t ->
4343+ Log.app (fun m -> m "🔧 Tool use: %s"
4444+ (Claude.Content_block.Tool_use.name t))
4545+ | _ -> ()
4646+ ) (Claude.Message.Assistant.content msg)
4747+ | Claude.Message.Result msg ->
4848+ if Claude.Message.Result.is_error msg then
4949+ Log.err (fun m -> m "❌ Error occurred!")
5050+ else
5151+ Log.app (fun m -> m "✅ Success!");
5252+ Log.app (fun m -> m "Duration: %dms"
5353+ (Claude.Message.Result.duration_ms msg))
5454+ | _ -> ()
5555+ ) messages;
5656+5757+ Log.app (fun m -> m "\n================================");
5858+ Log.app (fun m -> m "✨ Test complete!")
5959+6060+let main ~env =
6161+ Switch.run @@ fun sw ->
6262+ run_test ~sw ~env
6363+6464+(* Command-line interface *)
6565+open Cmdliner
6666+6767+let main_term env =
6868+ let setup_log style_renderer level =
6969+ Fmt_tty.setup_std_outputs ?style_renderer ();
7070+ Logs.set_level level;
7171+ Logs.set_reporter (Logs_fmt.reporter ());
7272+ if level = None then Logs.set_level (Some Logs.App);
7373+ match level with
7474+ | Some Logs.Info | Some Logs.Debug ->
7575+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
7676+ | _ -> ()
7777+ in
7878+ let run style level =
7979+ setup_log style level;
8080+ main ~env
8181+ in
8282+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
8383+8484+let cmd env =
8585+ let doc = "Test permission callback functionality" in
8686+ let info = Cmd.info "test_permissions" ~version:"1.0" ~doc in
8787+ Cmd.v info (main_term env)
8888+8989+let () =
9090+ Eio_main.run @@ fun env ->
9191+ exit (Cmd.eval (cmd env))