···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+<!-- $MDX non-deterministic=command -->
1818+```sh
1919+$ # Run the camel joke competition
2020+$ dune exec camel_jokes
2121+2222+$ # Run the permission demo (interactive)
2323+$ dune exec permission_demo
2424+2525+$ # With verbose output to see message flow
2626+$ dune exec permission_demo -- -v
2727+```
2828+2929+## Features Tested
3030+3131+- Concurrent Claude client instances
3232+- Message handling and processing
3333+- Permission callbacks
3434+- Tool access control
3535+- Typed message API
3636+- Pretty printing of messages
+112
examples/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.
+159
examples/advanced_config_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Advanced Configuration Demo
77+88+ This example demonstrates the advanced configuration options available
99+ in the OCaml Claude SDK, including:
1010+ - Budget limits for cost control
1111+ - Fallback models for reliability
1212+ - Settings isolation for CI/CD environments
1313+ - Custom buffer sizes for large outputs
1414+*)
1515+1616+open Eio.Std
1717+open Claude
1818+1919+let log_setup () =
2020+ Logs.set_reporter (Logs_fmt.reporter ());
2121+ Logs.set_level (Some Logs.Info)
2222+2323+(* Example 1: CI/CD Configuration
2424+2525+ In CI/CD environments, you want isolated, reproducible behavior
2626+ without any user/project/local settings interfering.
2727+*)
2828+let ci_cd_config () =
2929+ Options.default |> Options.with_no_settings (* Disable all settings loading *)
3030+ |> Options.with_max_budget_usd 0.50 (* 50 cent limit per run *)
3131+ |> Options.with_fallback_model (Claude.Model.of_string "claude-haiku-4")
3232+ (* Fast fallback *)
3333+ |> Options.with_model (Claude.Model.of_string "claude-sonnet-4-5")
3434+ |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
3535+3636+(* Example 2: Production Configuration with Fallback
3737+3838+ Production usage with cost controls and automatic fallback
3939+ to ensure availability.
4040+*)
4141+let production_config () =
4242+ Options.default
4343+ |> Options.with_model (Claude.Model.of_string "claude-sonnet-4-5")
4444+ |> Options.with_fallback_model (Claude.Model.of_string "claude-sonnet-3-5")
4545+ |> Options.with_max_budget_usd 10.0 (* $10 limit *)
4646+ |> Options.with_max_buffer_size 5_000_000 (* 5MB buffer for large outputs *)
4747+4848+(* Example 3: Development Configuration
4949+5050+ Development with user settings enabled but with cost controls.
5151+*)
5252+let dev_config () =
5353+ Options.default
5454+ (* Note: Settings are loaded by default from user/project/local files *)
5555+ |> Options.with_max_budget_usd 1.0 (* $1 limit for dev testing *)
5656+ |> Options.with_fallback_model (Claude.Model.of_string "claude-haiku-4")
5757+5858+(* Example 4: Isolated Test Configuration
5959+6060+ For automated testing with no external settings and strict limits.
6161+*)
6262+let test_config () =
6363+ Options.default |> Options.with_no_settings
6464+ |> Options.with_max_budget_usd 0.10 (* 10 cent limit per test *)
6565+ |> Options.with_model (Claude.Model.of_string "claude-haiku-4")
6666+ (* Fast, cheap model *)
6767+ |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
6868+ |> Options.with_max_buffer_size 1_000_000 (* 1MB buffer *)
6969+7070+(* Example 5: Custom Buffer Size Demo
7171+7272+ For applications that need to handle very large outputs.
7373+*)
7474+let _large_output_config () =
7575+ Options.default
7676+ |> Options.with_max_buffer_size 10_000_000 (* 10MB buffer *)
7777+ |> Options.with_model (Claude.Model.of_string "claude-sonnet-4-5")
7878+7979+(* Helper to run a query with a specific configuration *)
8080+let run_query ~sw process_mgr clock config prompt =
8181+ print_endline "\n=== Configuration ===";
8282+ (match Options.max_budget_usd config with
8383+ | Some budget -> Fmt.pr "Budget limit: $%.2f\n" budget
8484+ | None -> print_endline "Budget limit: None");
8585+ (match Options.fallback_model config with
8686+ | Some model -> Fmt.pr "Fallback model: %s\n" (Claude.Model.to_string model)
8787+ | None -> print_endline "Fallback model: None");
8888+ (* Settings configuration display removed - API doesn't expose setting_sources *)
8989+ print_endline "Settings: Default (user/project/local files)";
9090+ (match Options.max_buffer_size config with
9191+ | Some size -> Fmt.pr "Buffer size: %d bytes\n" size
9292+ | None -> print_endline "Buffer size: Default (1MB)");
9393+9494+ print_endline "\n=== Running Query ===";
9595+ let client = Client.v ~options:config ~sw ~process_mgr ~clock () in
9696+ Client.query client prompt;
9797+ let responses = Client.receive client in
9898+9999+ Seq.iter
100100+ (function
101101+ | Response.Text text ->
102102+ Fmt.pr "Response: %s\n" (Response.Text.content text)
103103+ | Response.Complete result ->
104104+ Fmt.pr "\n=== Session Complete ===\n";
105105+ Fmt.pr "Duration: %dms\n" (Response.Complete.duration_ms result);
106106+ (match Response.Complete.total_cost_usd result with
107107+ | Some cost -> Fmt.pr "Cost: $%.4f\n" cost
108108+ | None -> ());
109109+ Fmt.pr "Turns: %d\n" (Response.Complete.num_turns result)
110110+ | _ -> ())
111111+ responses
112112+113113+let main () =
114114+ log_setup ();
115115+116116+ Eio_main.run @@ fun env ->
117117+ Switch.run @@ fun sw ->
118118+ let process_mgr = Eio.Stdenv.process_mgr env in
119119+ let clock = Eio.Stdenv.clock env in
120120+121121+ print_endline "==============================================";
122122+ print_endline "Claude SDK - Advanced Configuration Examples";
123123+ print_endline "==============================================";
124124+125125+ (* Example: CI/CD isolated environment *)
126126+ print_endline "\n\n### Example 1: CI/CD Configuration ###";
127127+ print_endline "Purpose: Isolated, reproducible environment for CI/CD";
128128+ let config = ci_cd_config () in
129129+ run_query ~sw process_mgr clock config "What is 2+2? Answer in one sentence.";
130130+131131+ (* Example: Production with fallback *)
132132+ print_endline "\n\n### Example 2: Production Configuration ###";
133133+ print_endline "Purpose: Production with cost controls and fallback";
134134+ let config = production_config () in
135135+ run_query ~sw process_mgr clock config "Explain OCaml in one sentence.";
136136+137137+ (* Example: Development with settings *)
138138+ print_endline "\n\n### Example 3: Development Configuration ###";
139139+ print_endline "Purpose: Development with user/project settings";
140140+ let config = dev_config () in
141141+ run_query ~sw process_mgr clock config
142142+ "What is functional programming? One sentence.";
143143+144144+ (* Example: Test configuration *)
145145+ print_endline "\n\n### Example 4: Test Configuration ###";
146146+ print_endline "Purpose: Automated testing with strict limits";
147147+ let config = test_config () in
148148+ run_query ~sw process_mgr clock config "Say 'test passed' in one word.";
149149+150150+ print_endline "\n\n==============================================";
151151+ print_endline "All examples completed successfully!";
152152+ print_endline "=============================================="
153153+154154+let () =
155155+ try main ()
156156+ with e ->
157157+ Fmt.epr "Error: %s\n" (Printexc.to_string e);
158158+ Printexc.print_backtrace stderr;
159159+ exit 1
+139
examples/camel_jokes.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src = Logs.Src.create "camel_jokes" ~doc:"Camel joke competition"
99+1010+module Log = (val Logs.src_log src : Logs.LOG)
1111+1212+let process_claude_response client name =
1313+ Log.info (fun m -> m "=== %s's Response ===" name);
1414+ let responses = Claude.Client.receive_all client in
1515+ List.iter
1616+ (fun resp ->
1717+ match resp with
1818+ | Claude.Response.Text t ->
1919+ let text = Claude.Response.Text.content t in
2020+ Log.app (fun m -> m "%s: %s" name text)
2121+ | Claude.Response.Tool_use t ->
2222+ Log.debug (fun m ->
2323+ m "%s using tool: %s" name (Claude.Response.Tool_use.name t))
2424+ | Claude.Response.Thinking t ->
2525+ Log.debug (fun m ->
2626+ m "%s thinking: %s" name (Claude.Response.Thinking.content t))
2727+ | Claude.Response.Complete c ->
2828+ (if Claude.Response.Complete.total_cost_usd c <> None then
2929+ let cost =
3030+ Option.get (Claude.Response.Complete.total_cost_usd c)
3131+ in
3232+ Log.info (fun m -> m "%s's joke cost: $%.6f" name cost));
3333+ Log.debug (fun m ->
3434+ m "%s session: %s, duration: %dms" name
3535+ (Claude.Response.Complete.session_id c)
3636+ (Claude.Response.Complete.duration_ms c))
3737+ | Claude.Response.Error e ->
3838+ Log.err (fun m ->
3939+ m "Error from %s: %s" name (Claude.Response.Error.message e))
4040+ | Claude.Response.Init _ ->
4141+ (* Init messages are already logged by the library *)
4242+ ()
4343+ | Claude.Response.Tool_result _ ->
4444+ (* Tool results are user messages, skip *)
4545+ ())
4646+ responses
4747+4848+let run_claude ~sw ~env name prompt =
4949+ Log.info (fun m -> m "🐪 Starting %s..." name);
5050+ let options =
5151+ Claude.Options.default
5252+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
5353+ |> Claude.Options.with_allowed_tools []
5454+ in
5555+5656+ let client =
5757+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
5858+ ()
5959+ in
6060+6161+ Claude.Client.query client prompt;
6262+ process_claude_response client name
6363+6464+let main ~env =
6565+ Switch.run @@ fun sw ->
6666+ Log.app (fun m -> m "🐪 Starting the Great Camel Joke Competition! 🐪");
6767+ Log.app (fun m -> m "================================================\n");
6868+6969+ let prompts =
7070+ [
7171+ ( "Claude 1",
7272+ "Tell me a short, funny joke about camels! Make it original and clever."
7373+ );
7474+ ( "Claude 2",
7575+ "Give me your best camel joke - something witty and unexpected!" );
7676+ ("Claude 3", "Share a hilarious camel joke that will make everyone laugh!");
7777+ ]
7878+ in
7979+8080+ (* Run all three Claudes concurrently *)
8181+ Fiber.all
8282+ (List.map
8383+ (fun (name, prompt) -> fun () -> run_claude ~sw ~env name prompt)
8484+ prompts);
8585+8686+ Log.app (fun m -> m "\n================================================");
8787+ Log.app (fun m -> m "🎉 The Camel Joke Competition is complete! 🎉")
8888+8989+(* Command-line interface *)
9090+open Cmdliner
9191+9292+let main_term env =
9393+ let setup_log style_renderer level =
9494+ Fmt_tty.setup_std_outputs ?style_renderer ();
9595+ Logs.set_level level;
9696+ Logs.set_reporter (Logs_fmt.reporter ());
9797+ (* Set default to App level if not specified *)
9898+ if level = None then Logs.set_level (Some Logs.App);
9999+ (* Enable debug for Client module if in debug mode *)
100100+ if level = Some Logs.Debug then
101101+ Logs.Src.set_level Claude.Client.src (Some Logs.Debug)
102102+ in
103103+ let run style level =
104104+ setup_log style level;
105105+ main ~env
106106+ in
107107+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
108108+109109+let cmd env =
110110+ let doc = "Run the Great Camel Joke Competition using Claude" in
111111+ let man =
112112+ [
113113+ `S Manpage.s_description;
114114+ `P
115115+ "This program runs three concurrent Claude instances to generate camel \
116116+ jokes.";
117117+ `P "Use $(b,-v) or $(b,--verbosity=info) to see RPC message traffic.";
118118+ `P
119119+ "Use $(b,-vv) or $(b,--verbosity=debug) to see all internal operations.";
120120+ `S Manpage.s_examples;
121121+ `P "Run with normal output:";
122122+ `Pre " $(mname)";
123123+ `P "Run with info-level logging (RPC traffic):";
124124+ `Pre " $(mname) -v";
125125+ `Pre " $(mname) --verbosity=info";
126126+ `P "Run with debug logging (all operations):";
127127+ `Pre " $(mname) -vv";
128128+ `Pre " $(mname) --verbosity=debug";
129129+ `P "Enable debug for specific modules:";
130130+ `Pre " LOGS='claude.transport=debug' $(mname)";
131131+ `Pre " LOGS='claude.message=info,camel_jokes=debug' $(mname)";
132132+ `S Manpage.s_bugs;
133133+ `P "Report bugs at https://github.com/your-repo/issues";
134134+ ]
135135+ in
136136+ let info = Cmd.info "camel_jokes" ~version:"1.0" ~doc ~man in
137137+ Cmd.v info (main_term env)
138138+139139+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
+111
examples/discovery_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src =
99+ Logs.Src.create "discovery_demo" ~doc:"Permission discovery demonstration"
1010+1111+module Log = (val Logs.src_log src : Logs.LOG)
1212+1313+let process_response client =
1414+ let responses = Claude.Client.receive_all client in
1515+ List.iter
1616+ (fun resp ->
1717+ match resp with
1818+ | Claude.Response.Text text ->
1919+ let content = Claude.Response.Text.content text in
2020+ Log.app (fun m ->
2121+ m "Claude: %s"
2222+ (if String.length content > 100 then
2323+ String.sub content 0 100 ^ "..."
2424+ else content))
2525+ | Claude.Response.Tool_use t ->
2626+ Log.info (fun m -> m "Tool use: %s" (Claude.Response.Tool_use.name t))
2727+ | Claude.Response.Error err ->
2828+ Log.err (fun m -> m "Error: %s" (Claude.Response.Error.message err))
2929+ | Claude.Response.Complete result -> (
3030+ match Claude.Response.Complete.total_cost_usd result with
3131+ | Some cost -> Log.info (fun m -> m "Cost: $%.6f" cost)
3232+ | None -> ())
3333+ | _ -> ())
3434+ responses
3535+3636+let run_discovery ~sw ~env =
3737+ Log.app (fun m -> m "🔍 Permission Discovery Demo");
3838+ Log.app (fun m -> m "=============================");
3939+ Log.app (fun m -> m "This will discover what permissions Claude needs.\n");
4040+4141+ (* Create client with discovery mode *)
4242+ let options =
4343+ Claude.Options.default
4444+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
4545+ in
4646+ let client =
4747+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
4848+ ()
4949+ in
5050+ Claude.Client.enable_permission_discovery client;
5151+5252+ (* Send a prompt that will need permissions *)
5353+ Log.app (fun m -> m "Asking Claude to read a secret file...");
5454+ Claude.Client.query client
5555+ "Please read the file test/secret_data.txt and tell me what the secret \
5656+ code is.";
5757+ process_response client;
5858+5959+ (* Check what permissions were requested *)
6060+ let permissions = Claude.Client.discovered_permissions client in
6161+ if permissions = [] then
6262+ Log.app (fun m ->
6363+ m
6464+ "\n\
6565+ 📋 No permissions were requested (Claude may have used its \
6666+ knowledge).")
6767+ else begin
6868+ Log.app (fun m -> m "\n📋 Permissions that were requested:");
6969+ List.iter
7070+ (fun rule ->
7171+ Log.app (fun m ->
7272+ m " - Tool: %s%s"
7373+ (Claude.Permissions.Rule.tool_name rule)
7474+ (match Claude.Permissions.Rule.rule_content rule with
7575+ | Some content -> Fmt.str " (rule: %s)" content
7676+ | None -> "")))
7777+ permissions
7878+ end
7979+8080+let main ~env = Switch.run @@ fun sw -> run_discovery ~sw ~env
8181+8282+(* Command-line interface *)
8383+open Cmdliner
8484+8585+let main_term env =
8686+ let setup_log style_renderer level =
8787+ Fmt_tty.setup_std_outputs ?style_renderer ();
8888+ Logs.set_level level;
8989+ Logs.set_reporter (Logs_fmt.reporter ());
9090+ if level = None then Logs.set_level (Some Logs.App)
9191+ in
9292+ let run style level =
9393+ setup_log style level;
9494+ main ~env
9595+ in
9696+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
9797+9898+let cmd env =
9999+ let doc = "Discover what permissions Claude needs" in
100100+ let man =
101101+ [
102102+ `S Manpage.s_description;
103103+ `P
104104+ "This program runs Claude in discovery mode to see what permissions it \
105105+ requests.";
106106+ ]
107107+ in
108108+ let info = Cmd.info "discovery_demo" ~version:"1.0" ~doc ~man in
109109+ Cmd.v info (main_term env)
110110+111111+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Claude
77+open Eio.Std
88+99+let () = Logs.set_reporter (Logs_fmt.reporter ())
1010+let () = Logs.set_level (Some Logs.Info)
1111+1212+let print_server_info client =
1313+ try
1414+ let info = Client.server_info client in
1515+ traceln "Server version: %s" (Claude.Server_info.version info);
1616+ traceln "Capabilities: [%s]"
1717+ (String.concat ", " (Claude.Server_info.capabilities info));
1818+ traceln "Commands: [%s]"
1919+ (String.concat ", " (Claude.Server_info.commands info));
2020+ traceln "Output styles: [%s]"
2121+ (String.concat ", " (Claude.Server_info.output_styles info))
2222+ with
2323+ | Failure msg -> traceln "Failed to get server info: %s" msg
2424+ | exn -> traceln "Error getting server info: %s" (Printexc.to_string exn)
2525+2626+let run env =
2727+ Switch.run @@ fun sw ->
2828+ let process_mgr = Eio.Stdenv.process_mgr env in
2929+ let clock = Eio.Stdenv.clock env in
3030+3131+ (* Create client with default options *)
3232+ let options = Options.default in
3333+ let client = Client.v ~options ~sw ~process_mgr ~clock () in
3434+3535+ traceln "=== Dynamic Control Demo ===\n";
3636+3737+ (* First query with default model *)
3838+ traceln "1. Initial query with default model";
3939+ Client.query client "What model are you?";
4040+4141+ (* Consume initial responses *)
4242+ let responses = Client.receive_all client in
4343+ List.iter
4444+ (function
4545+ | Response.Text text ->
4646+ traceln "Assistant: %s" (Response.Text.content text)
4747+ | _ -> ())
4848+ responses;
4949+5050+ traceln "\n2. Getting server info...";
5151+ print_server_info client;
5252+5353+ traceln "\n3. Switching to a different model (if available)...";
5454+ (try
5555+ Client.set_model client (Model.of_string "claude-sonnet-4");
5656+ traceln "Model switched successfully";
5757+5858+ (* Query with new model *)
5959+ Client.query client "Confirm your model again please.";
6060+ let responses = Client.receive_all client in
6161+ List.iter
6262+ (function
6363+ | Response.Text text ->
6464+ traceln "Assistant (new model): %s" (Response.Text.content text)
6565+ | _ -> ())
6666+ responses
6767+ with
6868+ | Failure msg -> traceln "Failed to switch model: %s" msg
6969+ | exn -> traceln "Error switching model: %s" (Printexc.to_string exn));
7070+7171+ traceln "\n4. Changing permission mode...";
7272+ (try
7373+ Client.set_permission_mode client Permissions.Mode.Accept_edits;
7474+ traceln "Permission mode changed to Accept_edits"
7575+ with
7676+ | Failure msg -> traceln "Failed to change permission mode: %s" msg
7777+ | exn -> traceln "Error changing permission mode: %s" (Printexc.to_string exn));
7878+7979+ traceln "\n=== Demo Complete ===";
8080+ ()
8181+8282+let () =
8383+ Eio_main.run @@ fun env ->
8484+ try run env with
8585+ | Transport.CLI_not_found msg ->
8686+ traceln "Error: %s" msg;
8787+ traceln "Make sure the 'claude' CLI is installed and authenticated.";
8888+ exit 1
8989+ | exn ->
9090+ traceln "Unexpected error: %s" (Printexc.to_string exn);
9191+ Printexc.print_backtrace stderr;
9292+ exit 1
+123
examples/hooks_example.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src = Logs.Src.create "hooks_example" ~doc:"Hooks example"
99+1010+module Log = (val Logs.src_log src : Logs.LOG)
1111+1212+(* Example 1: Block dangerous bash commands *)
1313+let block_dangerous_bash input =
1414+ if input.Claude.Hooks.Pre_tool_use.tool_name = "Bash" then
1515+ match
1616+ Claude.Tool_input.string input.Claude.Hooks.Pre_tool_use.tool_input
1717+ "command"
1818+ with
1919+ | Some command ->
2020+ if String.length command >= 6 && String.sub command 0 6 = "rm -rf" then begin
2121+ Log.app (fun m -> m "🚫 Blocked dangerous command: %s" command);
2222+ Claude.Hooks.Pre_tool_use.deny
2323+ ~reason:"Command contains dangerous 'rm -rf' pattern" ()
2424+ end
2525+ else Claude.Hooks.Pre_tool_use.continue ()
2626+ | _ -> Claude.Hooks.Pre_tool_use.continue ()
2727+ else Claude.Hooks.Pre_tool_use.continue ()
2828+2929+(* Example 2: Log all tool usage *)
3030+let log_tool_usage input =
3131+ Log.app (fun m ->
3232+ m "📝 Tool %s called" input.Claude.Hooks.Pre_tool_use.tool_name);
3333+ Claude.Hooks.Pre_tool_use.continue ()
3434+3535+let run_example ~sw ~env =
3636+ Log.app (fun m -> m "🔧 Hooks System Example");
3737+ Log.app (fun m -> m "====================\n");
3838+3939+ (* Configure hooks *)
4040+ let hooks =
4141+ Claude.Hooks.empty
4242+ |> Claude.Hooks.on_pre_tool_use log_tool_usage
4343+ |> Claude.Hooks.on_pre_tool_use ~pattern:"Bash" block_dangerous_bash
4444+ in
4545+4646+ let options =
4747+ Claude.Options.default
4848+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
4949+ |> Claude.Options.with_hooks hooks
5050+ in
5151+5252+ let client =
5353+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
5454+ ()
5555+ in
5656+5757+ (* Test 1: Safe command (should work) *)
5858+ Log.app (fun m -> m "Test 1: Safe bash command");
5959+ Claude.Client.query client "Run the bash command: echo 'Hello from hooks!'";
6060+6161+ let messages = Claude.Client.receive_all client in
6262+ List.iter
6363+ (fun resp ->
6464+ match resp with
6565+ | Claude.Response.Text text ->
6666+ let content = Claude.Response.Text.content text in
6767+ if String.length content > 0 then
6868+ Log.app (fun m -> m "Claude: %s" content)
6969+ | Claude.Response.Complete _ -> Log.app (fun m -> m "✅ Test 1 complete\n")
7070+ | Claude.Response.Error err ->
7171+ Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message err))
7272+ | _ -> ())
7373+ messages;
7474+7575+ (* Test 2: Dangerous command (should be blocked) *)
7676+ Log.app (fun m -> m "Test 2: Dangerous bash command (should be blocked)");
7777+ Claude.Client.query client "Run the bash command: rm -rf /tmp/test";
7878+7979+ let messages = Claude.Client.receive_all client in
8080+ List.iter
8181+ (fun resp ->
8282+ match resp with
8383+ | Claude.Response.Text text ->
8484+ let content = Claude.Response.Text.content text in
8585+ if String.length content > 0 then
8686+ Log.app (fun m -> m "Claude: %s" content)
8787+ | Claude.Response.Complete _ -> Log.app (fun m -> m "✅ Test 2 complete")
8888+ | Claude.Response.Error err ->
8989+ Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message err))
9090+ | _ -> ())
9191+ messages;
9292+9393+ Log.app (fun m -> m "\n====================");
9494+ Log.app (fun m -> m "✨ Example complete!")
9595+9696+let main ~env = Switch.run @@ fun sw -> run_example ~sw ~env
9797+9898+(* Command-line interface *)
9999+open Cmdliner
100100+101101+let main_term env =
102102+ let setup_log style_renderer level =
103103+ Fmt_tty.setup_std_outputs ?style_renderer ();
104104+ Logs.set_level level;
105105+ Logs.set_reporter (Logs_fmt.reporter ());
106106+ if level = None then Logs.set_level (Some Logs.App);
107107+ match level with
108108+ | Some Logs.Info | Some Logs.Debug ->
109109+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
110110+ | _ -> ()
111111+ in
112112+ let run style level =
113113+ setup_log style level;
114114+ main ~env
115115+ in
116116+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
117117+118118+let cmd env =
119119+ let doc = "Demonstrate Claude's hooks system" in
120120+ let info = Cmd.info "hooks_example" ~version:"1.0" ~doc in
121121+ Cmd.v info (main_term env)
122122+123123+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
+89
examples/incoming_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Test the Incoming message codec *)
77+88+let test_decode_user_message () =
99+ let json_str = {|{"type":"user","content":"Hello"}|} in
1010+ match Json.of_string Claude.Incoming.json json_str with
1111+ | Ok (Claude.Incoming.Message (Claude.Message.User _)) ->
1212+ print_endline "✓ Decoded user message successfully"
1313+ | Ok _ -> print_endline "✗ Wrong message type decoded"
1414+ | Error err ->
1515+ Fmt.pr "✗ Failed to decode user message: %s\n" (Json.Error.to_string err)
1616+1717+let test_decode_assistant_message () =
1818+ let json_str =
1919+ {|{"type":"assistant","model":"claude-sonnet-4","content":[{"type":"text","text":"Hi"}]}|}
2020+ in
2121+ match Json.of_string Claude.Incoming.json json_str with
2222+ | Ok (Claude.Incoming.Message (Claude.Message.Assistant _)) ->
2323+ print_endline "✓ Decoded assistant message successfully"
2424+ | Ok _ -> print_endline "✗ Wrong message type decoded"
2525+ | Error err ->
2626+ Fmt.pr "✗ Failed to decode assistant message: %s\n"
2727+ (Json.Error.to_string err)
2828+2929+let test_decode_system_message () =
3030+ let json_str =
3131+ {|{"type":"system","subtype":"init","data":{"session_id":"test-123"}}|}
3232+ in
3333+ match Json.of_string Claude.Incoming.json json_str with
3434+ | Ok (Claude.Incoming.Message (Claude.Message.System _)) ->
3535+ print_endline "✓ Decoded system message successfully"
3636+ | Ok _ -> print_endline "✗ Wrong message type decoded"
3737+ | Error err ->
3838+ Fmt.pr "✗ Failed to decode system message: %s\n"
3939+ (Json.Error.to_string err)
4040+4141+let test_decode_control_response () =
4242+ let json_str =
4343+ {|{"type":"control_response","response":{"subtype":"success","request_id":"test-req-1"}}|}
4444+ in
4545+ match Json.of_string Claude.Incoming.json json_str with
4646+ | Ok (Claude.Incoming.Control_response resp) -> (
4747+ match resp.response with
4848+ | Claude.Control.Response.Success s ->
4949+ if s.request_id = "test-req-1" then
5050+ print_endline "✓ Decoded control response successfully"
5151+ else Fmt.pr "✗ Wrong request_id: %s\n" s.request_id
5252+ | Claude.Control.Response.Error _ ->
5353+ print_endline "✗ Got error response instead of success")
5454+ | Ok _ -> print_endline "✗ Wrong message type decoded"
5555+ | Error err ->
5656+ Fmt.pr "✗ Failed to decode control response: %s\n"
5757+ (Json.Error.to_string err)
5858+5959+let test_decode_control_response_error () =
6060+ let json_str =
6161+ {|{"type":"control_response","response":{"subtype":"error","request_id":"test-req-2","error":{"code":-32603,"message":"Something went wrong"}}}|}
6262+ in
6363+ match Json.of_string Claude.Incoming.json json_str with
6464+ | Ok (Claude.Incoming.Control_response resp) -> (
6565+ match resp.response with
6666+ | Claude.Control.Response.Error e ->
6767+ if
6868+ e.request_id = "test-req-2"
6969+ && e.error.code = -32603
7070+ && e.error.message = "Something went wrong"
7171+ then print_endline "✓ Decoded control error response successfully"
7272+ else Fmt.pr "✗ Wrong error content\n"
7373+ | Claude.Control.Response.Success _ ->
7474+ print_endline "✗ Got success response instead of error")
7575+ | Ok _ -> print_endline "✗ Wrong message type decoded"
7676+ | Error err ->
7777+ Fmt.pr "✗ Failed to decode control error response: %s\n"
7878+ (Json.Error.to_string err)
7979+8080+let () =
8181+ print_endline "Testing Incoming message codec...";
8282+ print_endline "";
8383+ test_decode_user_message ();
8484+ test_decode_assistant_message ();
8585+ test_decode_system_message ();
8686+ test_decode_control_response ();
8787+ test_decode_control_response_error ();
8888+ print_endline "";
8989+ print_endline "All tests completed!"
+27
examples/json_utils.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+let to_string ?(minify = false) json =
77+ let indent = if minify then None else Some 2 in
88+ Json.Value.to_string ?indent json
99+1010+let find (type a) (codec : a Json.codec) json key : a option =
1111+ let field_codec =
1212+ let open Json.Codec in
1313+ Object.map ~kind:"field" (fun v -> v)
1414+ |> Object.opt_member key codec ~enc:Fun.id
1515+ |> Object.seal
1616+ in
1717+ match Json.decode field_codec json with Ok v -> v | Error _ -> None
1818+1919+let string json key = find Json.Codec.string json key
2020+let int json key = find Json.Codec.int json key
2121+let bool json key = find Json.Codec.bool json key
2222+let array json key = find (Json.Codec.list Json.Codec.Value.t) json key
2323+2424+let as_string json =
2525+ match Json.decode Json.Codec.string json with
2626+ | Ok s -> Some s
2727+ | Error _ -> None
+24
examples/json_utils.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Helper functions for JSON operations in examples using json codecs. *)
77+88+val to_string : ?minify:bool -> Json.t -> string
99+(** Encode JSON to string. *)
1010+1111+val string : Json.t -> string -> string option
1212+(** [string json key] extracts a string field. *)
1313+1414+val int : Json.t -> string -> int option
1515+(** [int json key] extracts an integer field. *)
1616+1717+val bool : Json.t -> string -> bool option
1818+(** [bool json key] extracts a boolean field. *)
1919+2020+val array : Json.t -> string -> Json.t list option
2121+(** [array json key] extracts an array field. *)
2222+2323+val as_string : Json.t -> string option
2424+(** [as_string json] decodes JSON as a string value. *)
+243
examples/permission_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src =
99+ Logs.Src.create "permission_demo" ~doc:"Permission callback demonstration"
1010+1111+module Log = (val Logs.src_log src : Logs.LOG)
1212+1313+(* Mutable state to track what permissions have been granted *)
1414+module Granted = struct
1515+ module String_set = Set.Make (String)
1616+1717+ let tools = ref String_set.empty
1818+1919+ let grant tool_name =
2020+ tools := String_set.add tool_name !tools;
2121+ Log.app (fun m -> m "✅ Permission granted for: %s" tool_name)
2222+2323+ let deny tool_name =
2424+ Log.app (fun m -> m "❌ Permission denied for: %s" tool_name)
2525+2626+ let is_granted tool_name = String_set.mem tool_name !tools
2727+2828+ let list () =
2929+ if String_set.is_empty !tools then
3030+ Log.app (fun m -> m "No permissions granted yet")
3131+ else
3232+ Log.app (fun m ->
3333+ m "Currently granted permissions: %s"
3434+ (String_set.elements !tools |> String.concat ", "))
3535+end
3636+3737+(* Interactive permission callback *)
3838+let log_tool_input tool_name input_json =
3939+ try
4040+ match tool_name with
4141+ | "Read" -> (
4242+ match Json_utils.string input_json "file_path" with
4343+ | Some file_path -> Log.app (fun m -> m "File: %s" file_path)
4444+ | None -> ())
4545+ | "Bash" -> (
4646+ match Json_utils.string input_json "command" with
4747+ | Some command -> Log.app (fun m -> m "Command: %s" command)
4848+ | None -> ())
4949+ | "Write" | "Edit" -> (
5050+ match Json_utils.string input_json "file_path" with
5151+ | Some file_path -> Log.app (fun m -> m "File: %s" file_path)
5252+ | None -> ())
5353+ | "Glob" -> (
5454+ match Json_utils.string input_json "pattern" with
5555+ | Some pattern -> (
5656+ Log.app (fun m -> m "Pattern: %s" pattern);
5757+ match Json_utils.string input_json "path" with
5858+ | Some path -> Log.app (fun m -> m "Path: %s" path)
5959+ | None -> Log.app (fun m -> m "Path: (current directory)"))
6060+ | None -> ())
6161+ | "Grep" -> (
6262+ match Json_utils.string input_json "pattern" with
6363+ | Some pattern -> (
6464+ Log.app (fun m -> m "Pattern: %s" pattern);
6565+ match Json_utils.string input_json "path" with
6666+ | Some path -> Log.app (fun m -> m "Path: %s" path)
6767+ | None -> Log.app (fun m -> m "Path: (current directory)"))
6868+ | None -> ())
6969+ | _ -> Log.app (fun m -> m "Input: %s" (Json_utils.to_string input_json))
7070+ with exn ->
7171+ Log.info (fun m ->
7272+ m "Failed to parse input details: %s" (Printexc.to_string exn))
7373+7474+let prompt_user tool_name =
7575+ let open Claude.Permissions in
7676+ Fmt.pr "Allow? [y/N/always]: %!";
7777+ let tty = open_in "/dev/tty" in
7878+ let response = input_line tty |> String.lowercase_ascii in
7979+ close_in tty;
8080+ match response with
8181+ | "y" | "yes" ->
8282+ Log.app (fun m -> m "→ Allowed (this time only)");
8383+ Log.info (fun m -> m "User approved %s for this request only" tool_name);
8484+ Decision.allow ()
8585+ | "a" | "always" ->
8686+ Granted.grant tool_name;
8787+ Log.info (fun m -> m "User granted permanent permission for %s" tool_name);
8888+ Decision.allow ()
8989+ | _ ->
9090+ Granted.deny tool_name;
9191+ Log.info (fun m -> m "User denied permission for %s" tool_name);
9292+ Decision.deny
9393+ ~message:(Fmt.str "User denied access to %s" tool_name)
9494+ ~interrupt:false
9595+9696+let interactive_permission_callback ctx =
9797+ let open Claude.Permissions in
9898+ let tool_name = ctx.tool_name in
9999+ let input = ctx.input in
100100+101101+ Log.info (fun m -> m "🔔 Permission callback invoked for tool: %s" tool_name);
102102+ Log.app (fun m -> m "\n🔐 PERMISSION REQUEST 🔐");
103103+ Log.app (fun m -> m "Tool: %s" tool_name);
104104+105105+ (* Log the full input for debugging *)
106106+ let input_json = Claude.Tool_input.to_json input in
107107+ Log.info (fun m -> m "Full input JSON: %s" (Json_utils.to_string input_json));
108108+109109+ (* Show input details *)
110110+ log_tool_input tool_name input_json;
111111+112112+ (* Check if already granted *)
113113+ if Granted.is_granted tool_name then begin
114114+ Log.app (fun m -> m "→ Auto-approved (previously granted)");
115115+ Log.info (fun m -> m "Returning allow result for %s" tool_name);
116116+ Decision.allow ()
117117+ end
118118+ else prompt_user tool_name
119119+120120+let process_response client =
121121+ let responses = Claude.Client.receive_all client in
122122+ List.iter
123123+ (fun response ->
124124+ match response with
125125+ | Claude.Response.Text t ->
126126+ let text = Claude.Response.Text.content t in
127127+ Log.app (fun m -> m "\n📝 Claude says:\n%s" text)
128128+ | Claude.Response.Tool_use t ->
129129+ Log.info (fun m ->
130130+ m "🔧 Tool use: %s (id: %s)"
131131+ (Claude.Response.Tool_use.name t)
132132+ (Claude.Response.Tool_use.id t))
133133+ | Claude.Response.Complete c ->
134134+ (if Claude.Response.Complete.result_text c = None then
135135+ Log.err (fun m -> m "❌ Error occurred!")
136136+ else
137137+ match Claude.Response.Complete.total_cost_usd c with
138138+ | Some cost -> Log.info (fun m -> m "💰 Cost: $%.6f" cost)
139139+ | None -> ());
140140+ Log.info (fun m ->
141141+ m "⏱️ Duration: %dms" (Claude.Response.Complete.duration_ms c))
142142+ | Claude.Response.Error e ->
143143+ Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message e))
144144+ | _ -> ())
145145+ responses
146146+147147+let run_demo ~sw ~env =
148148+ Log.app (fun m -> m "🚀 Starting Permission Demo");
149149+ Log.app (fun m -> m "==================================");
150150+ Log.app (fun m -> m "This demo starts with NO permissions.");
151151+ Log.app (fun m -> m "Claude will request permissions as needed.\n");
152152+153153+ (* Create options with custom permission callback *)
154154+ (* DON'T specify allowed_tools - let the permission callback handle everything.
155155+ The Default permission mode with a callback should send requests for all tools. *)
156156+ let options =
157157+ Claude.Options.default
158158+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
159159+ |> Claude.Options.with_permission_mode Claude.Permissions.Mode.Default
160160+ |> Claude.Options.with_permission_callback interactive_permission_callback
161161+ in
162162+163163+ let client =
164164+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
165165+ ()
166166+ in
167167+168168+ (* First prompt - Claude will need to request Read permission for ../lib *)
169169+ Log.app (fun m -> m "\n📤 Sending first prompt (reading from ../lib)...");
170170+ Claude.Client.query client
171171+ "Please read and analyze the source files in the ../lib directory. Focus \
172172+ on the main OCaml modules and their purpose. What is the overall \
173173+ architecture of this Claude library?";
174174+ process_response client;
175175+176176+ (* Show current permissions *)
177177+ Log.app (fun m -> m "\n📋 Current permission status:");
178178+ Granted.list ();
179179+180180+ (* Second prompt - will need Write permission *)
181181+ Log.app (fun m -> m "\n📤 Sending second prompt (writing TEST.md)...");
182182+ Claude.Client.query client
183183+ "Now write a summary of what you learned about the Claude library \
184184+ architecture to a file called TEST.md in the current directory. Include \
185185+ the main modules, their purposes, and how they work together.";
186186+ process_response client;
187187+188188+ (* Show final permissions *)
189189+ Log.app (fun m -> m "\n📋 Final permission status:");
190190+ Granted.list ();
191191+192192+ Log.app (fun m -> m "\n==================================");
193193+ Log.app (fun m -> m "✨ Demo complete!")
194194+195195+let main ~env = Switch.run @@ fun sw -> run_demo ~sw ~env
196196+197197+(* Command-line interface *)
198198+open Cmdliner
199199+200200+let main_term env =
201201+ let setup_log style_renderer level =
202202+ Fmt_tty.setup_std_outputs ?style_renderer ();
203203+ Logs.set_level level;
204204+ Logs.set_reporter (Logs_fmt.reporter ());
205205+ (* Set default to App level if not specified *)
206206+ if level = None then Logs.set_level (Some Logs.App);
207207+ (* Enable info level for Client module if in info mode or above *)
208208+ match level with
209209+ | Some Logs.Info | Some Logs.Debug ->
210210+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
211211+ | _ -> ()
212212+ in
213213+ let run style level =
214214+ setup_log style level;
215215+ main ~env
216216+ in
217217+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
218218+219219+let cmd env =
220220+ let doc = "Demonstrate Claude's dynamic permission system" in
221221+ let man =
222222+ [
223223+ `S Manpage.s_description;
224224+ `P
225225+ "This program demonstrates how to use permission callbacks with Claude.";
226226+ `P "It starts with no permissions and asks for them interactively.";
227227+ `P "You can grant permissions for:";
228228+ `P "- Individual requests (y/yes)";
229229+ `P "- All future requests of that type (a/always)";
230230+ `P "- Or deny the request (n/no or just press Enter)";
231231+ `S Manpage.s_examples;
232232+ `P "Run the demo:";
233233+ `Pre " $(mname)";
234234+ `P "Run with verbose output to see message flow:";
235235+ `Pre " $(mname) -v";
236236+ `S Manpage.s_bugs;
237237+ `P "Report bugs at https://github.com/your-repo/issues";
238238+ ]
239239+ in
240240+ let info = Cmd.info "permission_demo" ~version:"1.0" ~doc ~man in
241241+ Cmd.v info (main_term env)
242242+243243+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
+185
examples/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())
+90
examples/permissions_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src = Logs.Src.create "test_permissions" ~doc:"Permission callback test"
99+1010+module Log = (val Logs.src_log src : Logs.LOG)
1111+1212+(* Simple auto-allow permission callback *)
1313+let auto_allow_callback ctx =
1414+ Log.app (fun m ->
1515+ m "✅ Auto-allowing tool: %s" ctx.Claude.Permissions.tool_name);
1616+ Claude.Permissions.Decision.allow ()
1717+1818+let run_test ~sw ~env =
1919+ Log.app (fun m -> m "🧪 Testing Permission Callbacks");
2020+ Log.app (fun m -> m "================================");
2121+2222+ (* Create options with custom permission callback *)
2323+ let options =
2424+ Claude.Options.default
2525+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
2626+ |> Claude.Options.with_permission_callback auto_allow_callback
2727+ in
2828+2929+ Log.app (fun m -> m "Creating client with permission callback...");
3030+ let client =
3131+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
3232+ ()
3333+ in
3434+3535+ (* Simple query that will trigger tool use *)
3636+ Log.app (fun m -> m "\n📤 Sending test query...");
3737+ Claude.Client.query client "What is 2 + 2? Just give me the number.";
3838+3939+ (* Process response *)
4040+ let messages = Claude.Client.receive_all client in
4141+ Log.app (fun m -> m "\n📨 Received %d messages" (List.length messages));
4242+4343+ List.iter
4444+ (fun resp ->
4545+ match resp with
4646+ | Claude.Response.Text text ->
4747+ Log.app (fun m -> m "Claude: %s" (Claude.Response.Text.content text))
4848+ | Claude.Response.Tool_use t ->
4949+ Log.app (fun m ->
5050+ m "🔧 Tool use: %s" (Claude.Response.Tool_use.name t))
5151+ | Claude.Response.Complete result ->
5252+ Log.app (fun m -> m "✅ Success!");
5353+ Log.app (fun m ->
5454+ m "Duration: %dms" (Claude.Response.Complete.duration_ms result))
5555+ | Claude.Response.Error err ->
5656+ Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message err))
5757+ | _ -> ())
5858+ messages;
5959+6060+ Log.app (fun m -> m "\n================================");
6161+ Log.app (fun m -> m "✨ Test complete!")
6262+6363+let main ~env = Switch.run @@ fun sw -> run_test ~sw ~env
6464+6565+(* Command-line interface *)
6666+open Cmdliner
6767+6868+let main_term env =
6969+ let setup_log style_renderer level =
7070+ Fmt_tty.setup_std_outputs ?style_renderer ();
7171+ Logs.set_level level;
7272+ Logs.set_reporter (Logs_fmt.reporter ());
7373+ if level = None then Logs.set_level (Some Logs.App);
7474+ match level with
7575+ | Some Logs.Info | Some Logs.Debug ->
7676+ Logs.Src.set_level Claude.Client.src (Some Logs.Info)
7777+ | _ -> ()
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 = "Test permission callback functionality" in
8787+ let info = Cmd.info "test_permissions" ~version:"1.0" ~doc in
8888+ Cmd.v info (main_term env)
8989+9090+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
+3
examples/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.
+137
examples/simple_permission_test.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Eio.Std
77+88+let src = Logs.Src.create "simple_permission_test" ~doc:"Simple permission test"
99+1010+module Log = (val Logs.src_log src : Logs.LOG)
1111+1212+(* Auto-allow callback that logs what it sees *)
1313+let auto_allow_callback ctx =
1414+ Log.app (fun m -> m "\n🔐 Permission callback invoked!");
1515+ Log.app (fun m -> m " Tool: %s" ctx.Claude.Permissions.tool_name);
1616+ Log.app (fun m ->
1717+ m " Input: %s"
1818+ (Json_utils.to_string
1919+ (Claude.Tool_input.to_json ctx.Claude.Permissions.input)));
2020+ Log.app (fun m -> m " ✅ Auto-allowing");
2121+ Claude.Permissions.Decision.allow ()
2222+2323+let process_test_responses messages =
2424+ let tool_count = ref 0 in
2525+ let write_used = ref false in
2626+2727+ List.iter
2828+ (fun resp ->
2929+ match resp with
3030+ | Claude.Response.Text text ->
3131+ let content = Claude.Response.Text.content text in
3232+ if String.length content > 0 then
3333+ Log.app (fun m -> m "\n💬 Claude: %s" content)
3434+ | Claude.Response.Tool_use t ->
3535+ incr tool_count;
3636+ let tool_name = Claude.Response.Tool_use.name t in
3737+ if tool_name = "Write" then write_used := true;
3838+ Log.app (fun m -> m "🔧 Tool use #%d: %s" !tool_count tool_name)
3939+ | Claude.Response.Tool_result r ->
4040+ let tool_use_id = Claude.Content_block.Tool_result.tool_use_id r in
4141+ let is_error =
4242+ Claude.Content_block.Tool_result.is_error r
4343+ |> Option.value ~default:false
4444+ in
4545+ if is_error then begin
4646+ Log.app (fun m -> m "\n⚠️ Tool result error for %s:" tool_use_id);
4747+ match Claude.Content_block.Tool_result.content r with
4848+ | Some json ->
4949+ Log.app (fun m -> m " %s" (Json.Value.to_string json))
5050+ | None -> ()
5151+ end
5252+ | Claude.Response.Complete result ->
5353+ Log.app (fun m -> m "\n✅ Success!");
5454+ (match Claude.Response.Complete.total_cost_usd result with
5555+ | Some cost -> Log.app (fun m -> m "💰 Cost: $%.6f" cost)
5656+ | None -> ());
5757+ Log.app (fun m ->
5858+ m "⏱️ Duration: %dms"
5959+ (Claude.Response.Complete.duration_ms result))
6060+ | Claude.Response.Error err ->
6161+ Log.err (fun m ->
6262+ m "\n❌ Error: %s" (Claude.Response.Error.message err))
6363+ | _ -> ())
6464+ messages;
6565+6666+ (!tool_count, !write_used)
6767+6868+let run_test ~sw ~env =
6969+ Log.app (fun m -> m "🧪 Testing Permission Callbacks (Auto-Allow Mode)");
7070+ Log.app (fun m -> m "====================================================");
7171+7272+ (* Create options with permission callback *)
7373+ let options =
7474+ Claude.Options.default
7575+ |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
7676+ |> Claude.Options.with_permission_callback auto_allow_callback
7777+ in
7878+7979+ Log.app (fun m -> m "Creating client with permission callback...");
8080+ let client =
8181+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
8282+ ()
8383+ in
8484+8585+ (* Query that should trigger Write tool *)
8686+ Log.app (fun m -> m "\n📤 Asking Claude to write a file...");
8787+ Claude.Client.query client
8888+ "Write a simple hello world message to /tmp/test_permission.txt";
8989+9090+ (* Process response *)
9191+ let messages = Claude.Client.receive_all client in
9292+ Log.app (fun m -> m "\n📨 Received %d messages" (List.length messages));
9393+9494+ let tool_count, write_used = process_test_responses messages in
9595+9696+ Log.app (fun m -> m "\n====================================================");
9797+ Log.app (fun m -> m "📊 Test Results:");
9898+ Log.app (fun m -> m " Total tools used: %d" tool_count);
9999+ Log.app (fun m -> m " Write tool used: %b" write_used);
100100+101101+ if write_used then
102102+ Log.app (fun m ->
103103+ m " ✅ Permission callback successfully intercepted Write tool!")
104104+ else Log.app (fun m -> m " ⚠️ Write tool was not used (unexpected)");
105105+106106+ Log.app (fun m -> m "====================================================");
107107+ Log.app (fun m -> m "✨ Test complete!")
108108+109109+let main ~env = Switch.run @@ fun sw -> run_test ~sw ~env
110110+111111+(* Command-line interface *)
112112+open Cmdliner
113113+114114+let main_term env =
115115+ let setup_log style_renderer level =
116116+ Fmt_tty.setup_std_outputs ?style_renderer ();
117117+ Logs.set_level level;
118118+ Logs.set_reporter (Logs_fmt.reporter ());
119119+ if level = None then Logs.set_level (Some Logs.App);
120120+ match level with
121121+ | Some Logs.Info | Some Logs.Debug ->
122122+ Logs.Src.set_level Claude.Client.src (Some Logs.Info);
123123+ Logs.Src.set_level Claude.Transport.src (Some Logs.Info)
124124+ | _ -> ()
125125+ in
126126+ let run style level =
127127+ setup_log style level;
128128+ main ~env
129129+ in
130130+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
131131+132132+let cmd env =
133133+ let doc = "Test permission callback with auto-allow" in
134134+ let info = Cmd.info "simple_permission_test" ~version:"1.0" ~doc in
135135+ Cmd.v info (main_term env)
136136+137137+let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))
+223
examples/simulated_permissions.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+let src =
77+ Logs.Src.create "simulated_permissions"
88+ ~doc:"Simulated permission demonstration"
99+1010+module Log = (val Logs.src_log src : Logs.LOG)
1111+1212+(* Track granted permissions *)
1313+module Permission_state = struct
1414+ module String_set = Set.Make (String)
1515+1616+ let granted = ref String_set.empty
1717+ let denied = ref String_set.empty
1818+1919+ let grant tool =
2020+ granted := String_set.add tool !granted;
2121+ denied := String_set.remove tool !denied
2222+2323+ let deny tool =
2424+ denied := String_set.add tool !denied;
2525+ granted := String_set.remove tool !granted
2626+2727+ let is_granted tool = String_set.mem tool !granted
2828+ let is_denied tool = String_set.mem tool !denied
2929+3030+ let _reset () =
3131+ granted := String_set.empty;
3232+ denied := String_set.empty
3333+3434+ let show () =
3535+ Log.app (fun m -> m "\n📊 Permission Status:");
3636+ if String_set.is_empty !granted && String_set.is_empty !denied then
3737+ Log.app (fun m -> m " No permissions configured")
3838+ else begin
3939+ if not (String_set.is_empty !granted) then
4040+ Log.app (fun m ->
4141+ m " ✅ Granted: %s"
4242+ (String_set.elements !granted |> String.concat ", "));
4343+ if not (String_set.is_empty !denied) then
4444+ Log.app (fun m ->
4545+ m " ❌ Denied: %s"
4646+ (String_set.elements !denied |> String.concat ", "))
4747+ end
4848+end
4949+5050+(* Example permission callback *)
5151+let example_permission_callback ctx =
5252+ let open Claude.Permissions in
5353+ let tool_name = ctx.tool_name in
5454+5555+ Log.app (fun m -> m "\n🔐 Permission Request for: %s" tool_name);
5656+5757+ (* Check current state *)
5858+ if Permission_state.is_granted tool_name then begin
5959+ Log.app (fun m -> m " → Auto-approved (previously granted)");
6060+ Decision.allow ()
6161+ end
6262+ else if Permission_state.is_denied tool_name then begin
6363+ Log.app (fun m -> m " → Auto-denied (previously denied)");
6464+ Decision.deny
6565+ ~message:(Fmt.str "Tool %s is blocked by policy" tool_name)
6666+ ~interrupt:false
6767+ end
6868+ else begin
6969+ (* Ask user *)
7070+ Fmt.pr " Allow %s? [y/n/always/never]: %!" tool_name;
7171+ match read_line () |> String.lowercase_ascii with
7272+ | "y" | "yes" ->
7373+ Log.app (fun m -> m " → Allowed (one time)");
7474+ Decision.allow ()
7575+ | "n" | "no" ->
7676+ Log.app (fun m -> m " → Denied (one time)");
7777+ Decision.deny
7878+ ~message:(Fmt.str "User denied %s" tool_name)
7979+ ~interrupt:false
8080+ | "a" | "always" ->
8181+ Permission_state.grant tool_name;
8282+ Log.app (fun m -> m " → Allowed (always)");
8383+ Decision.allow ()
8484+ | "never" ->
8585+ Permission_state.deny tool_name;
8686+ Log.app (fun m -> m " → Denied (always)");
8787+ Decision.deny
8888+ ~message:(Fmt.str "Tool %s permanently blocked" tool_name)
8989+ ~interrupt:false
9090+ | _ ->
9191+ Log.app (fun m -> m " → Denied (invalid response)");
9292+ Decision.deny ~message:"Invalid permission response" ~interrupt:false
9393+ end
9494+9595+(* Demonstrate the permission system *)
9696+let demo_permissions () =
9797+ Log.app (fun m -> m "🎭 Permission System Demonstration");
9898+ Log.app (fun m -> m "==================================\n");
9999+100100+ (* Simulate permission requests *)
101101+ let tools = [ "Read"; "Write"; "Bash"; "Edit" ] in
102102+103103+ Log.app (fun m -> m "This demo simulates permission requests.");
104104+ Log.app (fun m -> m "You can respond with: y/n/always/never\n");
105105+106106+ (* Test each tool *)
107107+ List.iter
108108+ (fun tool_name ->
109109+ let input =
110110+ Json.Value.object'
111111+ [
112112+ Json.Value.member
113113+ (Json.Value.name "file_path")
114114+ (Json.Value.string "/example/path.txt");
115115+ ]
116116+ in
117117+ let tool_input = Claude.Tool_input.of_json input in
118118+ let ctx =
119119+ Claude.Permissions.
120120+ { tool_name; input = tool_input; suggested_rules = [] }
121121+ in
122122+ let decision = example_permission_callback ctx in
123123+124124+ (* Show result *)
125125+ if Claude.Permissions.Decision.is_allow decision then
126126+ Log.info (fun m -> m "Result: Permission granted for %s" tool_name)
127127+ else
128128+ match Claude.Permissions.Decision.deny_message decision with
129129+ | Some message ->
130130+ Log.info (fun m ->
131131+ m "Result: Permission denied for %s - %s" tool_name message)
132132+ | None ->
133133+ Log.info (fun m -> m "Result: Permission denied for %s" tool_name))
134134+ tools;
135135+136136+ (* Show final state *)
137137+ Permission_state.show ()
138138+139139+(* Also demonstrate discovery callback *)
140140+let demo_discovery () =
141141+ Log.app (fun m -> m "\n\n🔍 Discovery Callback Demonstration");
142142+ Log.app (fun m -> m "====================================\n");
143143+144144+ let discovered = ref [] in
145145+ let callback = Claude.Permissions.discovery discovered in
146146+147147+ (* Simulate some tool requests *)
148148+ let obj k v =
149149+ Json.Value.object'
150150+ [ Json.Value.member (Json.Value.name k) (Json.Value.string v) ]
151151+ in
152152+ let requests =
153153+ [
154154+ ("Read", obj "file_path" "test.ml");
155155+ ("Bash", obj "command" "ls -la");
156156+ ("Write", obj "file_path" "output.txt");
157157+ ]
158158+ in
159159+160160+ Log.app (fun m -> m "Simulating tool requests with discovery callback...\n");
161161+162162+ List.iter
163163+ (fun (tool_name, input) ->
164164+ Log.app (fun m -> m " Request: %s" tool_name);
165165+ let tool_input = Claude.Tool_input.of_json input in
166166+ let ctx =
167167+ Claude.Permissions.
168168+ { tool_name; input = tool_input; suggested_rules = [] }
169169+ in
170170+ let _ = callback ctx in
171171+ ())
172172+ requests;
173173+174174+ Log.app (fun m -> m "\n📋 Discovered permissions:");
175175+ if !discovered = [] then Log.app (fun m -> m " None")
176176+ else
177177+ List.iter
178178+ (fun rule ->
179179+ Log.app (fun m ->
180180+ m " - %s%s"
181181+ (Claude.Permissions.Rule.tool_name rule)
182182+ (match Claude.Permissions.Rule.rule_content rule with
183183+ | Some content -> Fmt.str " (content: %s)" content
184184+ | None -> "")))
185185+ !discovered
186186+187187+let main () =
188188+ demo_permissions ();
189189+ demo_discovery ()
190190+191191+(* Command-line interface *)
192192+open Cmdliner
193193+194194+let main_term =
195195+ let setup_log style_renderer level =
196196+ Fmt_tty.setup_std_outputs ?style_renderer ();
197197+ Logs.set_level level;
198198+ Logs.set_reporter (Logs_fmt.reporter ());
199199+ if level = None then Logs.set_level (Some Logs.App)
200200+ in
201201+ let run style level =
202202+ setup_log style level;
203203+ main ()
204204+ in
205205+ Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
206206+207207+let cmd =
208208+ let doc = "Demonstrate permission callbacks and discovery" in
209209+ let man =
210210+ [
211211+ `S Manpage.s_description;
212212+ `P
213213+ "This program demonstrates how permission callbacks work in the Claude \
214214+ OCaml library.";
215215+ `P
216216+ "It simulates permission requests and shows how to implement custom \
217217+ callbacks.";
218218+ ]
219219+ in
220220+ let info = Cmd.info "simulated_permissions" ~version:"1.0" ~doc ~man in
221221+ Cmd.v info main_term
222222+223223+let () = exit (Cmd.eval cmd)
+265
examples/structured_error_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+---------------------------------------------------------------------------*)
55+66+(** Test structured errors by provoking a JSON-RPC error from Claude *)
77+88+open Eio.Std
99+1010+let test_create_error_detail () =
1111+ print_endline "\nTesting structured error creation...";
1212+1313+ (* Create a simple error *)
1414+ let error1 =
1515+ Claude.Control.Response.error_detail ~code:`Method_not_found
1616+ ~message:"Method not found" ()
1717+ in
1818+ Fmt.pr "✓ Created error: [%d] %s\n" error1.code error1.message;
1919+2020+ (* Create an error without additional data for simplicity *)
2121+ let error2 =
2222+ Claude.Control.Response.error_detail ~code:`Invalid_params
2323+ ~message:"Invalid parameters" ()
2424+ in
2525+ Fmt.pr "✓ Created error: [%d] %s\n" error2.code error2.message;
2626+2727+ (* Encode and decode an error response *)
2828+ let error_resp =
2929+ Claude.Control.Response.error ~request_id:"test-123" ~error:error2 ()
3030+ in
3131+3232+ let json = Json.encode Claude.Control.Response.json error_resp in
3333+ let json_str = Json.Value.to_string json in
3434+ Fmt.pr "✓ Encoded error response: %s\n" json_str;
3535+3636+ (* Decode it back *)
3737+ match Json.decode Claude.Control.Response.json json with
3838+ | Ok (Claude.Control.Response.Error decoded) ->
3939+ Fmt.pr "✓ Decoded error: [%d] %s\n" decoded.error.code
4040+ decoded.error.message
4141+ | Ok _ -> print_endline "✗ Wrong response type"
4242+ | Error e -> Fmt.pr "✗ Decode failed: %s\n" (Json.Error.to_string e)
4343+4444+let test_error_code_conventions () =
4545+ print_endline "\nTesting JSON-RPC error code conventions...";
4646+4747+ (* Standard JSON-RPC errors using the typed API with polymorphic variants *)
4848+ let errors =
4949+ [
5050+ (`Parse_error, "Parse error");
5151+ (`Invalid_request, "Invalid request");
5252+ (`Method_not_found, "Method not found");
5353+ (`Invalid_params, "Invalid params");
5454+ (`Internal_error, "Internal error");
5555+ (`Custom 1, "Application error");
5656+ ]
5757+ in
5858+5959+ List.iter
6060+ (fun (code, msg) ->
6161+ let err = Claude.Control.Response.error_detail ~code ~message:msg () in
6262+ Fmt.pr "✓ Error [%d]: %s (typed)\n" err.code err.message)
6363+ errors
6464+6565+let process_error_responses messages =
6666+ let error_found = ref false in
6767+ let text_error_found = ref false in
6868+ List.iter
6969+ (fun resp ->
7070+ match resp with
7171+ | Claude.Response.Error err ->
7272+ error_found := true;
7373+ Fmt.pr "✓ Received structured error response: %s\n"
7474+ (Claude.Response.Error.message err);
7575+ Fmt.pr " Is system error: %b\n"
7676+ (Claude.Response.Error.is_system_error err);
7777+ Fmt.pr " Is assistant error: %b\n"
7878+ (Claude.Response.Error.is_assistant_error err)
7979+ | Claude.Response.Text text ->
8080+ let content = Claude.Response.Text.content text in
8181+ if
8282+ String.length content > 0
8383+ && (String.contains content '4' || String.contains content 'e')
8484+ then begin
8585+ text_error_found := true;
8686+ Fmt.pr "✓ Received error as text: %s\n" content
8787+ end
8888+ | Claude.Response.Complete result ->
8989+ Fmt.pr " Complete (duration: %dms)\n"
9090+ (Claude.Response.Complete.duration_ms result)
9191+ | _ -> ())
9292+ messages;
9393+9494+ if !error_found then
9595+ Fmt.pr "✓ Successfully caught structured error response\n"
9696+ else if !text_error_found then
9797+ Fmt.pr "✓ Successfully caught error (returned as text)\n"
9898+ else Fmt.pr "✗ No error was returned (unexpected)\n"
9999+100100+let test_provoke_api_error ~sw ~env =
101101+ print_endline "\nTesting API error from Claude...";
102102+103103+ (* Configure client with an invalid model to provoke an API error *)
104104+ let options =
105105+ Claude.Options.default
106106+ |> Claude.Options.with_model
107107+ (Claude.Model.of_string "invalid-model-that-does-not-exist")
108108+ in
109109+110110+ Fmt.pr "Creating client with invalid model...\n";
111111+112112+ try
113113+ let client =
114114+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
115115+ ()
116116+ in
117117+118118+ Fmt.pr "Sending query to provoke API error...\n";
119119+ Claude.Client.query client
120120+ "Hello, this should fail with an invalid model error";
121121+122122+ (* Process responses to see if we get an error *)
123123+ let messages = Claude.Client.receive_all client in
124124+ process_error_responses messages
125125+ with
126126+ | Claude.Transport.Connection_error msg ->
127127+ Fmt.pr "✓ Connection error as expected: %s\n" msg
128128+ | exn ->
129129+ Fmt.pr "✗ Unexpected exception: %s\n" (Printexc.to_string exn);
130130+ Printexc.print_backtrace stdout
131131+132132+let test_control_protocol_error () =
133133+ print_endline "\nTesting control protocol error encoding/decoding...";
134134+135135+ (* Test that we can create and encode a control protocol error using polymorphic variant codes *)
136136+ let error_detail =
137137+ Claude.Control.Response.error_detail ~code:`Invalid_params
138138+ ~message:"Invalid params for permission request"
139139+ ~data:
140140+ (Json.Value.object'
141141+ [
142142+ Json.Value.member
143143+ (Json.Value.name "tool_name")
144144+ (Json.Value.string "Write");
145145+ Json.Value.member (Json.Value.name "reason")
146146+ (Json.Value.string "Missing required file_path parameter");
147147+ ])
148148+ ()
149149+ in
150150+151151+ let error_response =
152152+ Claude.Control.Response.error ~request_id:"test-req-456" ~error:error_detail
153153+ ()
154154+ in
155155+156156+ let json = Json.encode Claude.Control.Response.json error_response in
157157+ let json_str = Json.Value.to_string json in
158158+ Fmt.pr "✓ Encoded control error with data:\n %s\n" json_str;
159159+160160+ (* Verify we can decode it back *)
161161+ match Json.decode Claude.Control.Response.json json with
162162+ | Ok (Claude.Control.Response.Error decoded) -> (
163163+ Fmt.pr "✓ Decoded control error:\n";
164164+ Fmt.pr " Code: %d\n" decoded.error.code;
165165+ Fmt.pr " Message: %s\n" decoded.error.message;
166166+ Fmt.pr " Has data: %b\n" (Option.is_some decoded.error.data);
167167+ match decoded.error.data with
168168+ | Some data -> Fmt.pr " Data: %s\n" (Json.Value.to_string data)
169169+ | None -> ())
170170+ | Ok _ -> print_endline "✗ Wrong response type"
171171+ | Error e -> Fmt.pr "✗ Decode failed: %s\n" (Json.Error.to_string e)
172172+173173+let process_hook_responses messages =
174174+ let hook_called = ref false in
175175+ let error_found = ref false in
176176+ List.iter
177177+ (fun resp ->
178178+ match resp with
179179+ | Claude.Response.Tool_use tool ->
180180+ let tool_name = Claude.Response.Tool_use.name tool in
181181+ if tool_name = "Write" then begin
182182+ hook_called := true;
183183+ Fmt.pr "✓ Write tool was called (hook intercepted it)\n"
184184+ end
185185+ | Claude.Response.Error err ->
186186+ error_found := true;
187187+ Fmt.pr " Error response: %s\n" (Claude.Response.Error.message err)
188188+ | Claude.Response.Complete _ -> Fmt.pr " Query completed\n"
189189+ | _ -> ())
190190+ messages;
191191+192192+ if !hook_called then Fmt.pr "✓ Hook was triggered, exception caught by SDK\n"
193193+ else
194194+ Fmt.pr
195195+ " Note: Hook may not have been called if query didn't use Write tool\n";
196196+197197+ Fmt.pr "✓ Test completed (SDK sent -32603 Internal Error to CLI)\n"
198198+199199+let test_hook_error ~sw ~env =
200200+ print_endline "\nTesting hook callback errors trigger JSON-RPC error codes...";
201201+202202+ (* Create a hook that will throw an exception *)
203203+ let failing_hook input =
204204+ Fmt.pr "✓ Hook called for tool: %s\n"
205205+ input.Claude.Hooks.Pre_tool_use.tool_name;
206206+ failwith "Intentional hook failure to test error handling"
207207+ in
208208+209209+ (* Register the failing hook *)
210210+ let hooks =
211211+ Claude.Hooks.empty
212212+ |> Claude.Hooks.on_pre_tool_use ~pattern:"Write" failing_hook
213213+ in
214214+215215+ let options =
216216+ Claude.Options.default
217217+ |> Claude.Options.with_hooks hooks
218218+ |> Claude.Options.with_model (Claude.Model.of_string "haiku")
219219+ in
220220+221221+ Fmt.pr "Creating client with failing hook...\n";
222222+223223+ try
224224+ let client =
225225+ Claude.Client.v ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock
226226+ ()
227227+ in
228228+229229+ Fmt.pr "Asking Claude to write a file (should trigger failing hook)...\n";
230230+ Claude.Client.query client "Write 'test' to /tmp/test_hook_error.txt";
231231+232232+ (* Process responses *)
233233+ let messages = Claude.Client.receive_all client in
234234+ process_hook_responses messages
235235+ with exn ->
236236+ Fmt.pr "Exception during test: %s\n" (Printexc.to_string exn);
237237+ Printexc.print_backtrace stdout
238238+239239+let run_all_tests env =
240240+ print_endline "=== Structured Error Tests ===";
241241+ test_create_error_detail ();
242242+ test_error_code_conventions ();
243243+ test_control_protocol_error ();
244244+245245+ (* Test with actual Claude invocation *)
246246+ Switch.run @@ fun sw ->
247247+ test_provoke_api_error ~sw ~env;
248248+249249+ (* Test hook errors that trigger JSON-RPC error codes *)
250250+ Switch.run @@ fun sw ->
251251+ test_hook_error ~sw ~env;
252252+253253+ print_endline "\n=== All Structured Error Tests Completed ==="
254254+255255+let () =
256256+ Eio_main.run @@ fun env ->
257257+ try run_all_tests env with
258258+ | Claude.Transport.CLI_not_found msg ->
259259+ Fmt.epr "Error: Claude CLI not found\n%s\n" msg;
260260+ Fmt.epr "Make sure 'claude' is installed and in your PATH\n";
261261+ exit 1
262262+ | exn ->
263263+ Fmt.epr "Fatal error: %s\n" (Printexc.to_string exn);
264264+ Printexc.print_backtrace stderr;
265265+ exit 1
+210
examples/structured_output_demo.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Example demonstrating structured output with JSON Schema *)
77+88+module C = Claude
99+1010+let () =
1111+ (* Configure logging to see what's happening *)
1212+ Logs.set_reporter (Logs_fmt.reporter ());
1313+ Logs.set_level (Some Logs.Info);
1414+ Logs.Src.set_level C.Message.src (Some Logs.Debug)
1515+1616+let typed_prop typ desc =
1717+ Json.Value.object'
1818+ [
1919+ Json.Value.member (Json.Value.name "type") (Json.Value.string typ);
2020+ Json.Value.member (Json.Value.name "description") (Json.Value.string desc);
2121+ ]
2222+2323+let complexity_rating_prop =
2424+ Json.Value.object'
2525+ [
2626+ Json.Value.member (Json.Value.name "type") (Json.Value.string "string");
2727+ Json.Value.member (Json.Value.name "enum")
2828+ (Json.Value.list
2929+ [
3030+ Json.Value.string "low";
3131+ Json.Value.string "medium";
3232+ Json.Value.string "high";
3333+ ]);
3434+ Json.Value.member
3535+ (Json.Value.name "description")
3636+ (Json.Value.string "Overall complexity rating");
3737+ ]
3838+3939+let key_findings_prop =
4040+ Json.Value.object'
4141+ [
4242+ Json.Value.member (Json.Value.name "type") (Json.Value.string "array");
4343+ Json.Value.member (Json.Value.name "items")
4444+ (Json.Value.object'
4545+ [
4646+ Json.Value.member (Json.Value.name "type")
4747+ (Json.Value.string "string");
4848+ ]);
4949+ Json.Value.member
5050+ (Json.Value.name "description")
5151+ (Json.Value.string "List of key findings from the analysis");
5252+ ]
5353+5454+let analysis_properties =
5555+ Json.Value.object'
5656+ [
5757+ Json.Value.member
5858+ (Json.Value.name "file_count")
5959+ (typed_prop "integer" "Total number of files analyzed");
6060+ Json.Value.member
6161+ (Json.Value.name "has_tests")
6262+ (typed_prop "boolean" "Whether the codebase has test files");
6363+ Json.Value.member
6464+ (Json.Value.name "primary_language")
6565+ (typed_prop "string" "The primary programming language used");
6666+ Json.Value.member
6767+ (Json.Value.name "complexity_rating")
6868+ complexity_rating_prop;
6969+ Json.Value.member (Json.Value.name "key_findings") key_findings_prop;
7070+ ]
7171+7272+let analysis_schema =
7373+ Json.Value.object'
7474+ [
7575+ Json.Value.member (Json.Value.name "type") (Json.Value.string "object");
7676+ Json.Value.member (Json.Value.name "properties") analysis_properties;
7777+ Json.Value.member
7878+ (Json.Value.name "required")
7979+ (Json.Value.list
8080+ [
8181+ Json.Value.string "file_count";
8282+ Json.Value.string "has_tests";
8383+ Json.Value.string "primary_language";
8484+ Json.Value.string "complexity_rating";
8585+ Json.Value.string "key_findings";
8686+ ]);
8787+ Json.Value.member
8888+ (Json.Value.name "additionalProperties")
8989+ (Json.Value.bool false);
9090+ ]
9191+9292+let display_parsed_analysis output =
9393+ Fmt.pr "\n=== Structured Output ===\n";
9494+ Fmt.pr "%s\n\n" (Json_utils.to_string ~minify:false output);
9595+9696+ (* Parse the structured output *)
9797+ let file_count =
9898+ Json_utils.int output "file_count" |> Option.value ~default:0
9999+ in
100100+ let has_tests =
101101+ Json_utils.bool output "has_tests" |> Option.value ~default:false
102102+ in
103103+ let language =
104104+ Json_utils.string output "primary_language"
105105+ |> Option.value ~default:"unknown"
106106+ in
107107+ let complexity =
108108+ Json_utils.string output "complexity_rating"
109109+ |> Option.value ~default:"unknown"
110110+ in
111111+ let findings =
112112+ match Json_utils.array output "key_findings" with
113113+ | Some items ->
114114+ List.filter_map (fun json -> Json_utils.as_string json) items
115115+ | None -> []
116116+ in
117117+118118+ Fmt.pr "=== Parsed Analysis ===\n";
119119+ Fmt.pr "File Count: %d\n" file_count;
120120+ Fmt.pr "Has Tests: %b\n" has_tests;
121121+ Fmt.pr "Primary Language: %s\n" language;
122122+ Fmt.pr "Complexity: %s\n" complexity;
123123+ Fmt.pr "Key Findings:\n";
124124+ List.iter (fun finding -> Fmt.pr " - %s\n" finding) findings
125125+126126+let process_analysis_responses responses =
127127+ Seq.iter
128128+ (function
129129+ | C.Response.Text text ->
130130+ Fmt.pr "\nAssistant text:\n";
131131+ Fmt.pr " %s\n" (C.Response.Text.content text)
132132+ | C.Response.Tool_use tool ->
133133+ Fmt.pr " Using tool: %s\n" (C.Response.Tool_use.name tool)
134134+ | C.Response.Complete result -> (
135135+ Fmt.pr "\n=== Result ===\n";
136136+ Fmt.pr "Duration: %dms\n" (C.Response.Complete.duration_ms result);
137137+ Fmt.pr "Cost: $%.4f\n"
138138+ (Option.value
139139+ (C.Response.Complete.total_cost_usd result)
140140+ ~default:0.0);
141141+142142+ (* Extract and display structured output *)
143143+ match C.Response.Complete.structured_output result with
144144+ | Some output -> display_parsed_analysis output
145145+ | None -> (
146146+ Fmt.pr "No structured output received\n";
147147+ match C.Response.Complete.result_text result with
148148+ | Some text -> Fmt.pr "Text result: %s\n" text
149149+ | None -> ()))
150150+ | C.Response.Init _ -> Fmt.pr "Session initialized\n"
151151+ | C.Response.Error err ->
152152+ Fmt.pr "Error: %s\n" (C.Response.Error.message err)
153153+ | _ -> ())
154154+ responses
155155+156156+let run_codebase_analysis env =
157157+ Fmt.pr "\n=== Codebase Analysis with Structured Output ===\n\n";
158158+159159+ (* Create structured output format from the schema *)
160160+ let output_format = Claude.Structured_output.of_json_schema analysis_schema in
161161+162162+ (* Configure Claude with structured output *)
163163+ let options =
164164+ C.Options.default
165165+ |> C.Options.with_output_format output_format
166166+ |> C.Options.with_allowed_tools [ "Read"; "Glob"; "Grep" ]
167167+ |> C.Options.with_system_prompt
168168+ "You are a code analysis assistant. Analyze codebases and provide \
169169+ structured output matching the given JSON Schema."
170170+ in
171171+172172+ Fmt.pr "Structured output format configured\n";
173173+ Fmt.pr "Schema: %s\n\n" (Json_utils.to_string ~minify:false analysis_schema);
174174+175175+ (* Create Claude client and query *)
176176+ Eio.Switch.run @@ fun sw ->
177177+ let process_mgr = Eio.Stdenv.process_mgr env in
178178+ let clock = Eio.Stdenv.clock env in
179179+ let client = C.Client.v ~sw ~process_mgr ~clock ~options () in
180180+181181+ let prompt =
182182+ "Please analyze the current codebase structure. Look at the files, \
183183+ identify the primary language, count files, check for tests, assess \
184184+ complexity, and provide key findings. Return your analysis in the \
185185+ structured JSON format I specified."
186186+ in
187187+188188+ Fmt.pr "Sending query: %s\n\n" prompt;
189189+ C.Client.query client prompt;
190190+191191+ (* Process responses *)
192192+ let responses = C.Client.receive client in
193193+ process_analysis_responses responses;
194194+195195+ Fmt.pr "\nDone!\n"
196196+197197+let () =
198198+ Eio_main.run @@ fun env ->
199199+ try run_codebase_analysis env with
200200+ | C.Transport.CLI_not_found msg ->
201201+ Fmt.epr "Error: Claude CLI not found\n%s\n" msg;
202202+ Fmt.epr "Make sure 'claude' is installed and in your PATH\n";
203203+ exit 1
204204+ | C.Transport.Connection_error msg ->
205205+ Fmt.epr "Connection error: %s\n" msg;
206206+ exit 1
207207+ | exn ->
208208+ Fmt.epr "Unexpected error: %s\n" (Printexc.to_string exn);
209209+ Printexc.print_backtrace stderr;
210210+ exit 1
+82
examples/structured_output_simple.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Simple example showing structured output with explicit JSON Schema *)
77+88+module C = Claude
99+1010+let () =
1111+ Logs.set_reporter (Logs_fmt.reporter ());
1212+ Logs.set_level (Some Logs.Info)
1313+1414+let person_schema =
1515+ let typ t =
1616+ Json.Value.object'
1717+ [ Json.Value.member (Json.Value.name "type") (Json.Value.string t) ]
1818+ in
1919+ Json.Value.object'
2020+ [
2121+ Json.Value.member (Json.Value.name "type") (Json.Value.string "object");
2222+ Json.Value.member
2323+ (Json.Value.name "properties")
2424+ (Json.Value.object'
2525+ [
2626+ Json.Value.member (Json.Value.name "name") (typ "string");
2727+ Json.Value.member (Json.Value.name "age") (typ "integer");
2828+ Json.Value.member (Json.Value.name "occupation") (typ "string");
2929+ ]);
3030+ Json.Value.member
3131+ (Json.Value.name "required")
3232+ (Json.Value.list
3333+ [
3434+ Json.Value.string "name";
3535+ Json.Value.string "age";
3636+ Json.Value.string "occupation";
3737+ ]);
3838+ ]
3939+4040+let simple_example env =
4141+ Fmt.pr "\n=== Simple Structured Output Example ===\n\n";
4242+4343+ let output_format = Claude.Structured_output.of_json_schema person_schema in
4444+4545+ let options =
4646+ C.Options.default
4747+ |> C.Options.with_output_format output_format
4848+ |> C.Options.with_max_turns 1
4949+ in
5050+5151+ Fmt.pr "Asking Claude to provide structured data...\n\n";
5252+5353+ Eio.Switch.run @@ fun sw ->
5454+ let process_mgr = Eio.Stdenv.process_mgr env in
5555+ let clock = Eio.Stdenv.clock env in
5656+ let client = C.Client.v ~sw ~process_mgr ~clock ~options () in
5757+5858+ C.Client.query client
5959+ "Tell me about a famous computer scientist. Provide their name, age, and \
6060+ occupation in the exact JSON structure I specified.";
6161+6262+ let responses = C.Client.receive_all client in
6363+ List.iter
6464+ (function
6565+ | C.Response.Complete result -> (
6666+ Fmt.pr "Response received!\n";
6767+ match C.Response.Complete.structured_output result with
6868+ | Some json ->
6969+ Fmt.pr "\nStructured Output:\n%s\n"
7070+ (Json_utils.to_string ~minify:false json)
7171+ | None -> Fmt.pr "No structured output\n")
7272+ | C.Response.Error err ->
7373+ Fmt.pr "Error: %s\n" (C.Response.Error.message err)
7474+ | _ -> ())
7575+ responses
7676+7777+let () =
7878+ Eio_main.run @@ fun env ->
7979+ try simple_example env
8080+ with exn ->
8181+ Fmt.epr "Error: %s\n" (Printexc.to_string exn);
8282+ exit 1
+5-2
lib/claude.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-module Err = Err
66+module Error = Error
77module Client = Client
88module Options = Options
99module Response = Response
1010module Handler = Handler
1111module Tool_input = Tool_input
1212module Content_block = Content_block
1313+module Incoming = Incoming
1314module Message = Message
1415module Permissions = Permissions
1516module Hooks = Hooks
1617module Server_info = Server_info
1718module Transport = Transport
1819module Model = Model
1919-module Proto = Proto
2020module Structured_output = Structured_output
2121+module Control = Control
2222+module Outgoing = Outgoing
2323+module Unknown = Unknown
21242225(* New MCP-based custom tool support *)
2326module Tool = Tool
+97-99
lib/claude.mli
···3737 - {!Tool_input}: Opaque tool input with typed accessors
3838 - {!Server_info}: Server capabilities and metadata
39394040- {2 Wire Format (Advanced)}
4141- - {!Proto}: Direct access to wire-format types and JSON codecs
4242-4340 {1 Quick Start}
44414542 {[
4646- open Eio.Std
4343+ open Eio.Std
47444848- let () =
4949- Eio_main.run @@ fun env ->
5050- Switch.run @@ fun sw ->
5151- let client =
5252- Claude.Client.create ~sw ~process_mgr:(Eio.Stdenv.process_mgr env) ()
5353- in
4545+ let () =
4646+ Eio_main.run @@ fun env ->
4747+ Switch.run @@ fun sw ->
4848+ let client =
4949+ Claude.Client.v ~sw ~process_mgr:(Eio.Stdenv.process_mgr env) ()
5050+ in
54515555- Claude.Client.query client "What is 2+2?";
5252+ Claude.Client.query client "What is 2+2?";
56535757- let handler =
5858- object
5959- inherit Claude.Handler.default
6060- method! on_text t = print_endline (Claude.Response.Text.content t)
6161- end
6262- in
5454+ let handler =
5555+ object
5656+ inherit Claude.Handler.default
5757+ method! on_text t = print_endline (Claude.Response.Text.content t)
5858+ end
5959+ in
63606464- Claude.Client.run client ~handler
6161+ Claude.Client.run client ~handler
6562 ]}
66636764 {1 Response Handling}
···7370 Subclass {!Handler.default} and override only the methods you need:
74717572 {[
7676- let my_handler =
7777- object
7878- inherit Claude.Handler.default
7979- method! on_text t = print_endline (Claude.Response.Text.content t)
7373+ let my_handler =
7474+ object
7575+ inherit Claude.Handler.default
7676+ method! on_text t = print_endline (Claude.Response.Text.content t)
80778181- method! on_tool_use t =
8282- Printf.printf "Tool: %s\n" (Claude.Response.Tool_use.name t)
7878+ method! on_tool_use t =
7979+ Printf.printf "Tool: %s\n" (Claude.Response.Tool_use.name t)
83808484- method! on_complete c =
8585- Printf.printf "Done! Cost: $%.4f\n"
8686- (Option.value ~default:0.0
8787- (Claude.Response.Complete.total_cost_usd c))
8888- end
8989- in
8181+ method! on_complete c =
8282+ Printf.printf "Done! Cost: $%.4f\n"
8383+ (Option.value ~default:0.0
8484+ (Claude.Response.Complete.total_cost_usd c))
8585+ end
8686+ in
90879191- Claude.Client.run client ~handler:my_handler
8888+ Claude.Client.run client ~handler:my_handler
9289 ]}
93909491 {2 Functional Sequence}
···9693 For more control, use {!Client.receive} to get a lazy sequence:
97949895 {[
9999- Claude.Client.receive client
100100- |> Seq.iter (function
101101- | Claude.Response.Text t ->
102102- print_endline (Claude.Response.Text.content t)
103103- | Claude.Response.Complete c -> Printf.printf "Done!\n"
104104- | _ -> ())
9696+ Claude.Client.receive client
9797+ |> Seq.iter (function
9898+ | Claude.Response.Text t -> print_endline (Claude.Response.Text.content t)
9999+ | Claude.Response.Complete c -> Printf.printf "Done!\n"
100100+ | _ -> ())
105101 ]}
106102107103 {1 Tool Permissions}
···109105 Control which tools Claude can use:
110106111107 {[
112112- let options =
113113- Claude.Options.default
114114- |> Claude.Options.with_allowed_tools [ "Read"; "Write"; "Bash" ]
115115- |> Claude.Options.with_permission_mode
116116- Claude.Permissions.Mode.Accept_edits
108108+ let options =
109109+ Claude.Options.default
110110+ |> Claude.Options.with_allowed_tools [ "Read"; "Write"; "Bash" ]
111111+ |> Claude.Options.with_permission_mode
112112+ Claude.Permissions.Mode.Accept_edits
117113 ]}
118114119115 {2 Custom Permission Callbacks}
···121117 Implement custom logic for tool approval:
122118123119 {[
124124- let my_callback ctx =
125125- if ctx.Claude.Permissions.tool_name = "Bash" then
126126- Claude.Permissions.Decision.deny ~message:"Bash not allowed"
127127- ~interrupt:false
128128- else Claude.Permissions.Decision.allow ()
120120+ let my_callback ctx =
121121+ if ctx.Claude.Permissions.tool_name = "Bash" then
122122+ Claude.Permissions.Decision.deny ~message:"Bash not allowed"
123123+ ~interrupt:false
124124+ else Claude.Permissions.Decision.allow ()
129125130130- let options =
131131- Claude.Options.default
132132- |> Claude.Options.with_permission_callback my_callback
126126+ let options =
127127+ Claude.Options.default
128128+ |> Claude.Options.with_permission_callback my_callback
133129 ]}
134130135131 {1 Typed Hooks}
···137133 Intercept and control tool execution with fully typed callbacks:
138134139135 {[
140140- let hooks =
141141- Claude.Hooks.empty
142142- |> Claude.Hooks.on_pre_tool_use ~pattern:"Bash" (fun input ->
143143- if
144144- String.is_prefix ~prefix:"rm"
145145- (input.tool_input
146146- |> Claude.Tool_input.get_string "command"
147147- |> Option.value ~default:"")
148148- then Claude.Hooks.PreToolUse.deny ~reason:"Dangerous command" ()
149149- else Claude.Hooks.PreToolUse.continue ())
136136+ let hooks =
137137+ Claude.Hooks.empty
138138+ |> Claude.Hooks.on_pre_tool_use ~pattern:"Bash" (fun input ->
139139+ if
140140+ String.is_prefix ~prefix:"rm"
141141+ (input.tool_input
142142+ |> Claude.Tool_input.string "command"
143143+ |> Option.value ~default:"")
144144+ then Claude.Hooks.Pre_tool_use.deny ~reason:"Dangerous command" ()
145145+ else Claude.Hooks.Pre_tool_use.continue ())
150146151151- let options = Claude.Options.default |> Claude.Options.with_hooks hooks
147147+ let options = Claude.Options.default |> Claude.Options.with_hooks hooks
152148 ]}
153149154150 {1 Error Handling}
155151156156- The library uses a structured exception type {!Err.E} for all errors:
152152+ The library uses a structured exception type {!Error.E} for all errors:
157153158154 {[
159159- try Claude.Client.query client "Hello"
160160- with Claude.Err.E err ->
161161- Printf.eprintf "Error: %s\n" (Claude.Err.to_string err)
155155+ try Claude.Client.query client "Hello"
156156+ with Claude.Error.E err ->
157157+ Printf.eprintf "Error: %s\n" (Claude.Error.to_string err)
162158 ]}
163159164160 Error types include:
165165- - {!Err.Cli_not_found}: Claude CLI not found
166166- - {!Err.Process_error}: Process execution failure
167167- - {!Err.Protocol_error}: JSON/protocol parsing error
168168- - {!Err.Timeout}: Operation timed out
169169- - {!Err.Permission_denied}: Tool permission denied
170170- - {!Err.Hook_error}: Hook callback error
161161+ - {!Error.Cli_not_found}: Claude CLI not found
162162+ - {!Error.Process_error}: Process execution failure
163163+ - {!Error.Protocol_error}: JSON/protocol parsing error
164164+ - {!Error.Timeout}: Operation timed out
165165+ - {!Error.Permission_denied}: Tool permission denied
166166+ - {!Error.Hook_error}: Hook callback error
171167172168 {1 Logging}
173169···175171 its own log source allowing fine-grained control:
176172177173 {[
178178- Logs.Src.set_level Claude.Client.src (Some Logs.Debug);
179179- Logs.Src.set_level Claude.Transport.src (Some Logs.Info)
174174+ Logs.Src.set_level Claude.Client.src (Some Logs.Debug);
175175+ Logs.Src.set_level Claude.Transport.src (Some Logs.Info)
180176 ]} *)
181177182178(** {1 Core Modules} *)
183179184184-module Err = Err
180180+module Error = Error
185181(** Error handling with structured exception type. *)
186182187183module Client = Client
···203199204200module Content_block = Content_block
205201(** Content blocks for messages (text, tool use, tool results, thinking). *)
202202+203203+module Incoming = Incoming
204204+(** Incoming messages from the Claude CLI (messages, control responses). *)
206205207206module Message = Message
208207(** Messages exchanged with Claude (user, assistant, system, result). *)
···222221module Structured_output = Structured_output
223222(** Structured output configuration using JSON Schema. *)
224223224224+module Control = Control
225225+(** Control protocol envelopes. *)
226226+227227+module Outgoing = Outgoing
228228+(** Outgoing message envelopes for the CLI. *)
229229+230230+module Unknown = Unknown
231231+(** Unknown JSON fields preserved during round-trip. *)
232232+225233(** {1 Custom Tools (MCP)}
226234227235 These modules enable custom tool definitions that run in-process via MCP
···231239 {2 Example}
232240233241 {[
234234- let greet =
235235- Claude.Tool.create ~name:"greet" ~description:"Greet a user"
236236- ~input_schema:
237237- (Claude.Tool.schema_object
238238- [ ("name", Claude.Tool.schema_string) ]
239239- ~required:[ "name" ])
240240- ~handler:(fun args ->
241241- match Claude.Tool_input.get_string args "name" with
242242- | Some name -> Ok (Claude.Tool.text_result ("Hello, " ^ name ^ "!"))
243243- | None -> Error "Missing name")
242242+ let greet =
243243+ Claude.Tool.v ~name:"greet" ~description:"Greet a user"
244244+ ~input_schema:
245245+ (Claude.Tool.schema_object
246246+ [ ("name", Claude.Tool.schema_string) ]
247247+ ~required:[ "name" ])
248248+ ~handler:(fun args ->
249249+ match Claude.Tool_input.string args "name" with
250250+ | Some name -> Ok (Claude.Tool.text_result ("Hello, " ^ name ^ "!"))
251251+ | None -> Error "Missing name")
244252245245- let server = Claude.Mcp_server.create ~name:"my-tools" ~tools:[ greet ] ()
253253+ let server = Claude.Mcp_server.v ~name:"my-tools" ~tools:[ greet ] ()
246254247247- let options =
248248- Claude.Options.default
249249- |> Claude.Options.with_mcp_server ~name:"tools" server
250250- |> Claude.Options.with_allowed_tools [ "mcp__tools__greet" ]
255255+ let options =
256256+ Claude.Options.default
257257+ |> Claude.Options.with_mcp_server ~name:"tools" server
258258+ |> Claude.Options.with_allowed_tools [ "mcp__tools__greet" ]
251259 ]} *)
252260253261module Tool = Tool
···260268261269module Transport = Transport
262270(** Low-level transport layer for CLI communication. *)
263263-264264-(** {1 Wire Format (Advanced)}
265265-266266- The {!Proto} module provides direct access to wire-format types and JSON
267267- codecs. Use this for advanced scenarios like custom transports or debugging.
268268-269269- Most users should use the high-level types above instead. *)
270270-271271-module Proto = Proto
272272-(** Wire-format types and JSON codecs. *)
+222-259
lib/client.ml
···7788module Log = (val Logs.src_log src : Logs.LOG)
991010-(** Control response builders using Sdk_control codecs *)
1010+let encode_or_raise ~msg:_ codec v = Json.encode codec v
1111+1212+(** Control response builders using Control codecs *)
1113module Control_response = struct
1214 let success ~request_id ~response =
1313- let resp = Sdk_control.Response.success ~request_id ?response () in
1414- let ctrl = Sdk_control.create_response ~response:resp () in
1515- Jsont.Json.encode Sdk_control.jsont ctrl
1616- |> Err.get_ok ~msg:"Control_response.success: "
1515+ let resp = Control.Response.success ~request_id ?response () in
1616+ let ctrl = Control.response ~response:resp () in
1717+ encode_or_raise ~msg:"Control_response.success: " Control.json ctrl
17181819 let error ~request_id ~code ~message ?data () =
1919- let error_detail =
2020- Sdk_control.Response.error_detail ~code ~message ?data ()
2121- in
2222- let resp = Sdk_control.Response.error ~request_id ~error:error_detail () in
2323- let ctrl = Sdk_control.create_response ~response:resp () in
2424- Jsont.Json.encode Sdk_control.jsont ctrl
2525- |> Err.get_ok ~msg:"Control_response.error: "
2020+ let error_detail = Control.Response.error_detail ~code ~message ?data () in
2121+ let resp = Control.Response.error ~request_id ~error:error_detail () in
2222+ let ctrl = Control.response ~response:resp () in
2323+ encode_or_raise ~msg:"Control_response.error: " Control.json ctrl
2624end
27252828-(* Helper functions for JSON manipulation using jsont *)
2929-let json_to_string json =
3030- Jsont_bytesrw.encode_string' Jsont.json json
3131- |> Result.map_error Jsont.Error.to_string
3232- |> Err.get_ok ~msg:""
2626+let json_to_string json = Json.Value.to_string json
33273428(** Wire-level codec for hook matcher configuration sent to CLI. *)
3529module Hook_matcher_wire = struct
3630 type t = { matcher : string option; hook_callback_ids : string list }
37313838- let jsont : t Jsont.t =
3232+ let json : t Json.codec =
3333+ let open Json.Codec in
3934 let make matcher hook_callback_ids = { matcher; hook_callback_ids } in
4040- Jsont.Object.map ~kind:"HookMatcherWire" make
4141- |> Jsont.Object.opt_mem "matcher" Jsont.string ~enc:(fun r -> r.matcher)
4242- |> Jsont.Object.mem "hookCallbackIds" (Jsont.list Jsont.string)
4343- ~enc:(fun r -> r.hook_callback_ids)
4444- |> Jsont.Object.finish
3535+ Object.map ~kind:"HookMatcherWire" make
3636+ |> Object.opt_member "matcher" string ~enc:(fun r -> r.matcher)
3737+ |> Object.member "hookCallbackIds" (list string) ~enc:(fun r ->
3838+ r.hook_callback_ids)
3939+ |> Object.seal
45404641 let encode matchers =
4747- List.map
4848- (fun m ->
4949- Jsont.Json.encode jsont m
5050- |> Err.get_ok ~msg:"Hook_matcher_wire.encode: ")
5151- matchers
5252- |> Jsont.Json.list
4242+ List.map (fun m -> Json.encode json m) matchers |> Json.Value.list
5343end
54445545type t = {
5646 transport : Transport.t;
5747 mutable permission_callback : Permissions.callback option;
5848 mutable permission_log : Permissions.Rule.t list ref option;
5959- hook_callbacks : (string, Jsont.json -> Proto.Hooks.result) Hashtbl.t;
4949+ hook_callbacks : (string, Json.t -> Hooks.result) Hashtbl.t;
6050 mutable session_id : string option;
6161- control_responses : (string, Jsont.json) Hashtbl.t;
5151+ control_responses : (string, Json.t) Hashtbl.t;
6252 control_mutex : Eio.Mutex.t;
6353 control_condition : Eio.Condition.t;
6454 clock : float Eio.Time.clock_ty Eio.Resource.t;
···70607161let session_id t = t.session_id
72627373-let handle_control_request t (ctrl_req : Sdk_control.control_request) =
7474- let request_id = ctrl_req.request_id in
7575- Log.info (fun m -> m "Handling control request: %s" request_id);
6363+let handle_permission_request t ~request_id (req : Control.Request.permission) =
6464+ let tool_name = req.tool_name in
6565+ let input_json = req.input in
6666+ Log.info (fun m ->
6767+ m "Permission request for tool '%s' with input: %s" tool_name
6868+ (json_to_string input_json));
6969+ let suggestions = Option.value req.permission_suggestions ~default:[] in
7070+ let suggested_rules =
7171+ Permissions.extract_rules_from_proto_updates suggestions
7272+ in
7373+7474+ (* Convert input to Tool_input.t *)
7575+ let input = Tool_input.of_json input_json in
7676+7777+ (* Create context *)
7878+ let context : Permissions.context = { tool_name; input; suggested_rules } in
76797777- match ctrl_req.request with
7878- | Sdk_control.Request.Permission req ->
7979- let tool_name = req.tool_name in
8080- let input_json = req.input in
8181- Log.info (fun m ->
8282- m "Permission request for tool '%s' with input: %s" tool_name
8383- (json_to_string input_json));
8484- (* Convert permission_suggestions to suggested rules *)
8585- let suggestions = Option.value req.permission_suggestions ~default:[] in
8686- let suggested_rules =
8787- Permissions.extract_rules_from_proto_updates suggestions
8080+ Log.info (fun m -> m "Invoking permission callback for tool: %s" tool_name);
8181+ let callback =
8282+ Option.value t.permission_callback ~default:Permissions.default_allow
8383+ in
8484+ let decision = callback context in
8585+ Log.info (fun m ->
8686+ m "Permission callback returned: %s"
8787+ (if Permissions.Decision.is_allow decision then "ALLOW" else "DENY"));
8888+8989+ let lib_result =
9090+ Permissions.Decision.to_proto_result ~original_input:input decision
9191+ in
9292+ let response_data = Json.encode Permissions.Result.json lib_result in
9393+ let response =
9494+ Control_response.success ~request_id ~response:(Some response_data)
9595+ in
9696+ Log.info (fun m -> m "Sending control response: %s" (json_to_string response));
9797+ Transport.send t.transport response
9898+9999+let handle_hook_callback t ~request_id (req : Control.Request.hook_callback) =
100100+ let callback_id = req.callback_id in
101101+ let input = req.input in
102102+ let _tool_use_id = req.tool_use_id in
103103+ Log.info (fun m -> m "Hook callback request for callback_id: %s" callback_id);
104104+105105+ try
106106+ let callback = Hashtbl.find t.hook_callbacks callback_id in
107107+ let result = callback input in
108108+109109+ let result_json =
110110+ encode_or_raise ~msg:"Failed to encode hook result: " Hooks.result_jsont
111111+ result
112112+ in
113113+ Log.debug (fun m -> m "Hook result JSON: %s" (json_to_string result_json));
114114+ let response =
115115+ Control_response.success ~request_id ~response:(Some result_json)
116116+ in
117117+ Log.info (fun m -> m "Hook callback succeeded, sending response");
118118+ Transport.send t.transport response
119119+ with
120120+ | Not_found ->
121121+ let error_msg = Fmt.str "Hook callback not found: %s" callback_id in
122122+ Log.err (fun m -> m "%s" error_msg);
123123+ Transport.send t.transport
124124+ (Control_response.error ~request_id ~code:`Method_not_found
125125+ ~message:error_msg ())
126126+ | exn ->
127127+ let error_msg =
128128+ Fmt.str "Hook callback error: %s" (Printexc.to_string exn)
88129 in
130130+ Log.err (fun m -> m "%s" error_msg);
131131+ Transport.send t.transport
132132+ (Control_response.error ~request_id ~code:`Internal_error
133133+ ~message:error_msg ())
891349090- (* Convert input to Tool_input.t *)
9191- let input = Tool_input.of_json input_json in
135135+let handle_mcp_message t ~request_id (req : Control.Request.mcp_message) =
136136+ let module J = Json.Value in
137137+ let server_name = req.server_name in
138138+ let message = req.message in
139139+ Log.info (fun m -> m "MCP request for server '%s'" server_name);
921409393- (* Create context *)
9494- let context : Permissions.context =
9595- { tool_name; input; suggested_rules }
141141+ match Hashtbl.find_opt t.mcp_servers server_name with
142142+ | None ->
143143+ let error_msg = Fmt.str "MCP server '%s' not found" server_name in
144144+ Log.err (fun m -> m "%s" error_msg);
145145+ (* Return JSONRPC error in mcp_response format *)
146146+ let mcp_error =
147147+ J.object'
148148+ [
149149+ J.member (J.name "jsonrpc") (J.string "2.0");
150150+ J.member (J.name "id") (J.null ());
151151+ J.member (J.name "error")
152152+ (J.object'
153153+ [
154154+ J.member (J.name "code") (J.number (-32601.0));
155155+ J.member (J.name "message") (J.string error_msg);
156156+ ]);
157157+ ]
96158 in
9797-9898- Log.info (fun m ->
9999- m "Invoking permission callback for tool: %s" tool_name);
100100- let callback =
101101- Option.value t.permission_callback ~default:Permissions.default_allow
159159+ let response_data =
160160+ J.object' [ J.member (J.name "mcp_response") mcp_error ]
102161 in
103103- let decision = callback context in
104104- Log.info (fun m ->
105105- m "Permission callback returned: %s"
106106- (if Permissions.Decision.is_allow decision then "ALLOW" else "DENY"));
107107-108108- (* Convert permission decision to proto result *)
109109- let proto_result =
110110- Permissions.Decision.to_proto_result ~original_input:input decision
162162+ let response =
163163+ Control_response.success ~request_id ~response:(Some response_data)
111164 in
112112-113113- (* Encode to JSON *)
165165+ Transport.send t.transport response
166166+ | Some server ->
167167+ let mcp_response = Mcp_server.handle_json_message server message in
168168+ Log.debug (fun m -> m "MCP response: %s" (json_to_string mcp_response));
114169 let response_data =
115115- match Jsont.Json.encode Proto.Permissions.Result.jsont proto_result with
116116- | Ok json -> json
117117- | Error err ->
118118- Log.err (fun m -> m "Failed to encode permission result: %s" err);
119119- failwith "Permission result encoding failed"
170170+ J.object' [ J.member (J.name "mcp_response") mcp_response ]
120171 in
121172 let response =
122173 Control_response.success ~request_id ~response:(Some response_data)
123174 in
124124- Log.info (fun m ->
125125- m "Sending control response: %s" (json_to_string response));
126175 Transport.send t.transport response
127127- | Sdk_control.Request.Hook_callback req -> (
128128- let callback_id = req.callback_id in
129129- let input = req.input in
130130- let _tool_use_id = req.tool_use_id in
131131- Log.info (fun m ->
132132- m "Hook callback request for callback_id: %s" callback_id);
133176134134- try
135135- let callback = Hashtbl.find t.hook_callbacks callback_id in
136136- let result = callback input in
177177+let handle_control_request t (ctrl_req : Control.control_request) =
178178+ let request_id = ctrl_req.request_id in
179179+ Log.info (fun m -> m "Handling control request: %s" request_id);
137180138138- let result_json =
139139- Jsont.Json.encode Proto.Hooks.result_jsont result
140140- |> Err.get_ok ~msg:"Failed to encode hook result: "
141141- in
142142- Log.debug (fun m ->
143143- m "Hook result JSON: %s" (json_to_string result_json));
144144- let response =
145145- Control_response.success ~request_id ~response:(Some result_json)
146146- in
147147- Log.info (fun m -> m "Hook callback succeeded, sending response");
148148- Transport.send t.transport response
149149- with
150150- | Not_found ->
151151- let error_msg =
152152- Printf.sprintf "Hook callback not found: %s" callback_id
153153- in
154154- Log.err (fun m -> m "%s" error_msg);
155155- Transport.send t.transport
156156- (Control_response.error ~request_id ~code:`Method_not_found
157157- ~message:error_msg ())
158158- | exn ->
159159- let error_msg =
160160- Printf.sprintf "Hook callback error: %s" (Printexc.to_string exn)
161161- in
162162- Log.err (fun m -> m "%s" error_msg);
163163- Transport.send t.transport
164164- (Control_response.error ~request_id ~code:`Internal_error
165165- ~message:error_msg ()))
166166- | Sdk_control.Request.Mcp_message req -> (
167167- let module
168168- (* Handle MCP request for in-process SDK servers *)
169169- J =
170170- Jsont.Json
171171- in
172172- let server_name = req.server_name in
173173- let message = req.message in
174174- Log.info (fun m -> m "MCP request for server '%s'" server_name);
175175-176176- match Hashtbl.find_opt t.mcp_servers server_name with
177177- | None ->
178178- let error_msg =
179179- Printf.sprintf "MCP server '%s' not found" server_name
180180- in
181181- Log.err (fun m -> m "%s" error_msg);
182182- (* Return JSONRPC error in mcp_response format *)
183183- let mcp_error =
184184- J.object'
185185- [
186186- J.mem (J.name "jsonrpc") (J.string "2.0");
187187- J.mem (J.name "id") (J.null ());
188188- J.mem (J.name "error")
189189- (J.object'
190190- [
191191- J.mem (J.name "code") (J.number (-32601.0));
192192- J.mem (J.name "message") (J.string error_msg);
193193- ]);
194194- ]
195195- in
196196- let response_data =
197197- J.object' [ J.mem (J.name "mcp_response") mcp_error ]
198198- in
199199- let response =
200200- Control_response.success ~request_id ~response:(Some response_data)
201201- in
202202- Transport.send t.transport response
203203- | Some server ->
204204- let mcp_response = Mcp_server.handle_json_message server message in
205205- Log.debug (fun m ->
206206- m "MCP response: %s" (json_to_string mcp_response));
207207- let response_data =
208208- J.object' [ J.mem (J.name "mcp_response") mcp_response ]
209209- in
210210- let response =
211211- Control_response.success ~request_id ~response:(Some response_data)
212212- in
213213- Transport.send t.transport response)
181181+ match ctrl_req.request with
182182+ | Control.Request.Permission req ->
183183+ handle_permission_request t ~request_id req
184184+ | Control.Request.Hook_callback req -> handle_hook_callback t ~request_id req
185185+ | Control.Request.Mcp_message req -> handle_mcp_message t ~request_id req
214186 | _ ->
215187 (* Other request types not handled here *)
216188 let error_msg = "Unsupported control request type" in
···220192221193let handle_control_response t control_resp =
222194 let request_id =
223223- match control_resp.Sdk_control.response with
224224- | Sdk_control.Response.Success s -> s.request_id
225225- | Sdk_control.Response.Error e -> e.request_id
195195+ match control_resp.Control.response with
196196+ | Control.Response.Success s -> s.request_id
197197+ | Control.Response.Error e -> e.request_id
226198 in
227199 Log.debug (fun m ->
228200 m "Received control response for request_id: %s" request_id);
229201230202 (* Store the response as JSON and signal waiting threads *)
231203 let json =
232232- Jsont.Json.encode Sdk_control.control_response_jsont control_resp
233233- |> Err.get_ok ~msg:"Failed to encode control response: "
204204+ encode_or_raise ~msg:"Failed to encode control response: "
205205+ Control.control_response_jsont control_resp
234206 in
235207 Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
236208 Hashtbl.replace t.control_responses request_id json;
···244216 Log.debug (fun m -> m "Handle messages: EOF received");
245217 Seq.Nil
246218 | Some line -> (
247247- (* Use unified Incoming codec for all message types *)
248248- match Jsont_bytesrw.decode_string' Incoming.jsont line with
219219+ match Json.of_string Incoming.json line with
249220 | Ok incoming -> Seq.Cons (incoming, loop)
250221 | Error err ->
251222 Log.err (fun m ->
252223 m "Failed to decode incoming message: %s\nLine: %s"
253253- (Jsont.Error.to_string err)
254254- line);
224224+ (Json.Error.to_string err) line);
255225 loop ())
256226 in
257227 Log.debug (fun m -> m "Starting message handler");
···287257 m "Received control request (request_id: %s)"
288258 ctrl_req.request_id);
289259 handle_control_request t ctrl_req;
260260+ loop rest
261261+ | Incoming.Rate_limit_event ->
262262+ Log.debug (fun m -> m "Received rate_limit_event (ignored)");
290263 loop rest)
291264 and emit_responses responses rest =
292265 match responses with
···295268 in
296269 loop raw_seq
297270298298-let create ?(options = Options.default) ~sw ~process_mgr ~clock () =
271271+let register_hooks t ~options ~hook_callbacks ~next_callback_id =
272272+ let register_matcher event_name (pattern, callback) =
273273+ let callback_id = Fmt.str "hook_%d" !next_callback_id in
274274+ incr next_callback_id;
275275+ Hashtbl.add hook_callbacks callback_id callback;
276276+ Log.debug (fun m ->
277277+ m "Registered callback: %s for event: %s" callback_id event_name);
278278+ Hook_matcher_wire.{ matcher = pattern; hook_callback_ids = [ callback_id ] }
279279+ in
280280+ Options.hooks options
281281+ |> Option.iter (fun hooks_config ->
282282+ Log.info (fun m -> m "Registering hooks...");
283283+ let callbacks_by_event = Hooks.callbacks hooks_config in
284284+ let hooks_list =
285285+ List.map
286286+ (fun (event, matchers) ->
287287+ let event_name = Hooks.event_to_string event in
288288+ let matcher_wires =
289289+ List.map (register_matcher event_name) matchers
290290+ in
291291+ (event_name, Hook_matcher_wire.encode matcher_wires))
292292+ callbacks_by_event
293293+ in
294294+ let request = Control.Request.initialize ~hooks:hooks_list () in
295295+ let ctrl_req = Control.request ~request_id:"init_hooks" ~request () in
296296+ let initialize_msg =
297297+ encode_or_raise ~msg:"Failed to encode initialize request: "
298298+ Control.json ctrl_req
299299+ in
300300+ Log.info (fun m -> m "Sending hooks initialize request");
301301+ Transport.send t.transport initialize_msg)
302302+303303+let v ?(options = Options.default) ~sw ~process_mgr ~clock () =
299304 (* Automatically enable permission prompt tool when callback is configured
300305 (matching Python SDK behavior in client.py:104-121) *)
301306 let options =
···305310 Options.with_permission_prompt_tool_name "stdio" options
306311 | _ -> options
307312 in
308308- let transport = Transport.create ~sw ~process_mgr ~options () in
313313+ let transport = Transport.v ~sw ~process_mgr ~options () in
309314310315 (* Setup hook callbacks *)
311316 let hook_callbacks = Hashtbl.create 16 in
···335340 }
336341 in
337342338338- (* Register hooks and send initialize if hooks are configured *)
339339- Options.hooks options
340340- |> Option.iter (fun hooks_config ->
341341- Log.info (fun m -> m "Registering hooks...");
342342-343343- (* Get callbacks in wire format from the new Hooks API *)
344344- let callbacks_by_event = Hooks.get_callbacks hooks_config in
345345-346346- (* Build hooks configuration with callback IDs as (string * Jsont.json) list *)
347347- let hooks_list =
348348- List.map
349349- (fun (event, matchers) ->
350350- let event_name = Proto.Hooks.event_to_string event in
351351- let matcher_wires =
352352- List.map
353353- (fun (pattern, callback) ->
354354- let callback_id =
355355- Printf.sprintf "hook_%d" !next_callback_id
356356- in
357357- incr next_callback_id;
358358- Hashtbl.add hook_callbacks callback_id callback;
359359- Log.debug (fun m ->
360360- m "Registered callback: %s for event: %s" callback_id
361361- event_name);
362362- Hook_matcher_wire.
363363- { matcher = pattern; hook_callback_ids = [ callback_id ] })
364364- matchers
365365- in
366366- (event_name, Hook_matcher_wire.encode matcher_wires))
367367- callbacks_by_event
368368- in
369369-370370- (* Create initialize request using Sdk_control codec *)
371371- let request = Sdk_control.Request.initialize ~hooks:hooks_list () in
372372- let ctrl_req =
373373- Sdk_control.create_request ~request_id:"init_hooks" ~request ()
374374- in
375375- let initialize_msg =
376376- Jsont.Json.encode Sdk_control.jsont ctrl_req
377377- |> Err.get_ok ~msg:"Failed to encode initialize request: "
378378- in
379379- Log.info (fun m -> m "Sending hooks initialize request");
380380- Transport.send t.transport initialize_msg);
381381-343343+ register_hooks t ~options ~hook_callbacks ~next_callback_id;
382344 t
383345384384-(* Helper to send a message with proper "type" wrapper via Proto.Outgoing *)
385346let send_message t msg =
386386- Log.info (fun m -> m "→ %a" Message.pp msg);
387387- let proto_msg = Message.to_proto msg in
388388- let outgoing = Proto.Outgoing.Message proto_msg in
389389- let json = Proto.Outgoing.to_json outgoing in
347347+ Log.info (fun m -> m "-> %a" Message.pp msg);
348348+ let outgoing = Outgoing.Message msg in
349349+ let json = Outgoing.to_json outgoing in
390350 Transport.send t.transport json
391351392352let query t prompt =
···480440let discovered_permissions t =
481441 t.permission_log |> Option.map ( ! ) |> Option.value ~default:[]
482442443443+let decode_or_raise ~msg codec v =
444444+ Json.decode codec v |> Result.map_error Json.Error.to_string |> Error.ok' ~msg
445445+446446+let decode_control_response response_json =
447447+ let response_field_codec =
448448+ let open Json.Codec in
449449+ Object.map ~kind:"ResponseField" Fun.id
450450+ |> Object.member "response" Value.t ~enc:Fun.id
451451+ |> Object.seal
452452+ in
453453+ let response_data =
454454+ decode_or_raise ~msg:"Failed to extract response field: "
455455+ response_field_codec response_json
456456+ in
457457+ let response =
458458+ decode_or_raise ~msg:"Failed to decode response: " Control.Response.json
459459+ response_data
460460+ in
461461+ match response with
462462+ | Control.Response.Success s -> s.response
463463+ | Control.Response.Error e ->
464464+ raise
465465+ (Failure
466466+ (Fmt.str "Control request failed: [%d] %s" e.error.code
467467+ e.error.message))
468468+483469(* Helper to send a control request and wait for response *)
484470let send_control_request t ~request_id request =
485485- (* Send the control request *)
486486- let control_msg = Sdk_control.create_request ~request_id ~request () in
471471+ let control_msg = Control.request ~request_id ~request () in
487472 let json =
488488- Jsont.Json.encode Sdk_control.jsont control_msg
489489- |> Err.get_ok ~msg:"Failed to encode control request: "
473473+ encode_or_raise ~msg:"Failed to encode control request: " Control.json
474474+ control_msg
490475 in
491476 Log.info (fun m -> m "Sending control request: %s" (json_to_string json));
492477 Transport.send t.transport json;
···508493 if elapsed > max_wait then
509494 raise
510495 (Failure
511511- (Printf.sprintf "Timeout waiting for control response: %s"
496496+ (Fmt.str "Timeout waiting for control response: %s"
512497 request_id))
513498 else (
514499 (* Release mutex and wait for signal *)
···519504 let response_json = wait_for_response () in
520505 Log.debug (fun m ->
521506 m "Received control response: %s" (json_to_string response_json));
522522-523523- (* Parse the response - extract the "response" field using jsont codec *)
524524- let response_field_codec =
525525- Jsont.Object.map ~kind:"ResponseField" Fun.id
526526- |> Jsont.Object.mem "response" Jsont.json ~enc:Fun.id
527527- |> Jsont.Object.finish
528528- in
529529- let response_data =
530530- Jsont.Json.decode response_field_codec response_json
531531- |> Err.get_ok' ~msg:"Failed to extract response field: "
532532- in
533533- let response =
534534- Jsont.Json.decode Sdk_control.Response.jsont response_data
535535- |> Err.get_ok' ~msg:"Failed to decode response: "
536536- in
537537- match response with
538538- | Sdk_control.Response.Success s -> s.response
539539- | Sdk_control.Response.Error e ->
540540- raise
541541- (Failure
542542- (Printf.sprintf "Control request failed: [%d] %s" e.error.code
543543- e.error.message))
507507+ decode_control_response response_json
544508545509let set_permission_mode t mode =
546546- let request_id = Printf.sprintf "set_perm_mode_%f" (Eio.Time.now t.clock) in
547547- let proto_mode = Permissions.Mode.to_proto mode in
548548- let request = Sdk_control.Request.set_permission_mode ~mode:proto_mode () in
510510+ let request_id = Fmt.str "set_perm_mode_%f" (Eio.Time.now t.clock) in
511511+ let request = Control.Request.set_permission_mode ~mode () in
549512 let _response = send_control_request t ~request_id request in
550513 Log.info (fun m ->
551514 m "Permission mode set to: %s" (Permissions.Mode.to_string mode))
552515553516let set_model t model =
554517 let model_str = Model.to_string model in
555555- let request_id = Printf.sprintf "set_model_%f" (Eio.Time.now t.clock) in
556556- let request = Sdk_control.Request.set_model ~model:model_str () in
518518+ let request_id = Fmt.str "set_model_%f" (Eio.Time.now t.clock) in
519519+ let request = Control.Request.set_model ~model:model_str () in
557520 let _response = send_control_request t ~request_id request in
558521 Log.info (fun m -> m "Model set to: %s" model_str)
559522560560-let get_server_info t =
561561- let request_id = Printf.sprintf "get_server_info_%f" (Eio.Time.now t.clock) in
562562- let request = Sdk_control.Request.get_server_info () in
523523+let server_info t =
524524+ let request_id = Fmt.str "get_server_info_%f" (Eio.Time.now t.clock) in
525525+ let request = Control.Request.get_server_info () in
563526 let response_data =
564527 send_control_request t ~request_id request
565528 |> Option.to_result ~none:"No response data from get_server_info request"
566566- |> Err.get_ok ~msg:""
529529+ |> Error.ok ~msg:""
567530 in
568531 let server_info =
569569- Jsont.Json.decode Sdk_control.Server_info.jsont response_data
570570- |> Err.get_ok' ~msg:"Failed to decode server info: "
532532+ decode_or_raise ~msg:"Failed to decode server info: "
533533+ Control.Server_info.json response_data
571534 in
572535 Log.info (fun m ->
573536 m "Retrieved server info: %a"
574574- (Jsont.pp_value Sdk_control.Server_info.jsont ())
537537+ (Json.pp_value Control.Server_info.json)
575538 server_info);
576576- Server_info.of_sdk_control server_info
539539+ Server_info.of_control server_info
577540578541module Advanced = struct
579542 let send_message t msg = send_message t msg
···584547585548 let send_raw t control =
586549 let json =
587587- Jsont.Json.encode Sdk_control.jsont control
588588- |> Err.get_ok ~msg:"Failed to encode control message: "
550550+ encode_or_raise ~msg:"Failed to encode control message: " Control.json
551551+ control
589552 in
590553 Log.info (fun m -> m "→ Raw control: %s" (json_to_string json));
591554 Transport.send t.transport json
+53-59
lib/client.mli
···1212 {2 Basic Usage}
13131414 {[
1515- Eio.Switch.run @@ fun sw ->
1616- let client = Client.create ~sw ~process_mgr ~clock () in
1717- Client.query client "What is 2+2?";
1515+ Eio.Switch.run @@ fun sw ->
1616+ let client = Client.v ~sw ~process_mgr ~clock () in
1717+ Client.query client "What is 2+2?";
18181919- let messages = Client.receive_all client in
2020- List.iter
2121- (function
2222- | Message.Assistant msg ->
2323- Printf.printf "Claude: %s\n" (Message.Assistant.text msg)
2424- | _ -> ())
2525- messages
1919+ let messages = Client.receive_all client in
2020+ List.iter
2121+ (function
2222+ | Message.Assistant msg ->
2323+ Printf.printf "Claude: %s\n" (Message.Assistant.text msg)
2424+ | _ -> ())
2525+ messages
2626 ]}
27272828 {2 Features}
···35353636 {2 Message Flow}
37373838- 1. Create a client with {!create} 2. Send messages with {!query} or
3838+ 1. Create a client with {!v} 2. Send messages with {!query} or
3939 {!Advanced.send_message} 3. Receive responses with {!receive} or
4040 {!receive_all} 4. Continue multi-turn conversations by sending more messages
4141 5. Client automatically cleans up when the switch exits
···4747 - Server capability introspection *)
48484949val src : Logs.Src.t
5050-(** The log source for client operations *)
5050+(** The log source for client operations. *)
51515252type t
5353(** The type of Claude clients. *)
···5757 The session ID is provided in system init messages and uniquely identifies
5858 the current conversation session. *)
59596060-val create :
6060+val v :
6161 ?options:Options.t ->
6262 sw:Eio.Switch.t ->
6363 process_mgr:_ Eio.Process.mgr ->
6464 clock:float Eio.Time.clock_ty Eio.Resource.t ->
6565 unit ->
6666 t
6767-(** [create ?options ~sw ~process_mgr ~clock ()] creates a new Claude client.
6767+(** [v ?options ~sw ~process_mgr ~clock ()] creates a new Claude client.
68686969 @param options Configuration options (defaults to {!Options.default})
7070 @param sw Eio switch for resource management
7171 @param process_mgr Eio process manager for spawning the Claude CLI
7272- @param clock Eio clock for time operations *)
7272+ @param clock Eio clock for time operations. *)
73737474(** {1 Simple Query Interface} *)
7575···8181 {!Advanced.send_message} instead. *)
82828383val respond_to_tool :
8484- t ->
8585- tool_use_id:string ->
8686- content:Jsont.json ->
8787- ?is_error:bool ->
8888- unit ->
8989- unit
8484+ t -> tool_use_id:string -> content:Json.t -> ?is_error:bool -> unit -> unit
9085(** [respond_to_tool t ~tool_use_id ~content ?is_error ()] responds to a tool
9186 use request.
9287···9792 @param tool_use_id The ID from the {!Response.Tool_use.t} event
9893 @param content
9994 The result content (can be a string or array of content blocks)
100100- @param is_error Whether this is an error response (default: false) *)
9595+ @param is_error Whether this is an error response (default: false). *)
10196102102-val respond_to_tools : t -> (string * Jsont.json * bool option) list -> unit
9797+val respond_to_tools : t -> (string * Json.t * bool option) list -> unit
10398(** [respond_to_tools t responses] responds to multiple tool use requests at
10499 once.
105100···111106112107 Example:
113108 {[
114114- Client.respond_to_tools client
115115- [
116116- ("tool_use_123", Jsont.string "Success", None);
117117- ("tool_use_456", Jsont.string "Error occurred", Some true);
118118- ]
109109+ Client.respond_to_tools client
110110+ [
111111+ ("tool_use_123", Json.Value.string "Success", None);
112112+ ("tool_use_456", Json.Value.string "Error occurred", Some true);
113113+ ]
119114 ]} *)
120115121116val clear_tool_response_tracking : t -> unit
···136131137132 Example:
138133 {[
139139- let my_handler =
140140- object
141141- inherit Claude.Handler.default
142142- method! on_text t = print_endline (Response.Text.content t)
134134+ let my_handler =
135135+ object
136136+ inherit Claude.Handler.default
137137+ method! on_text t = print_endline (Response.Text.content t)
143138144144- method! on_complete c =
145145- Printf.printf "Cost: $%.4f\n"
146146- (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
147147- end
148148- in
149149- Client.query client "Hello";
150150- Client.run client ~handler:my_handler
139139+ method! on_complete c =
140140+ Printf.printf "Cost: $%.4f\n"
141141+ (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
142142+ end
143143+ in
144144+ Client.query client "Hello";
145145+ Client.run client ~handler:my_handler
151146 ]} *)
152147153148val receive : t -> Response.t Seq.t
···192187193188 {[
194189 (* Start with strict permissions *)
195195- let client = Client.create ~sw ~process_mgr ~clock
190190+ let client = Client.v ~sw ~process_mgr ~clock
196191 ~options:(Options.default
197192 |> Options.with_permission_mode Permissions.Mode.Default) ()
198193 in
···211206212207 {[
213208 (* Use powerful model for complex analysis *)
214214- let client = Client.create ~sw ~process_mgr ~clock
209209+ let client = Client.v ~sw ~process_mgr ~clock
215210 ~options:(Options.default |> Options.with_model "claude-sonnet-4-5") ()
216211 in
217212···228223 {2 Example: Server Introspection}
229224230225 {[
231231- let info = Client.get_server_info client in
232232- Printf.printf "Claude CLI version: %s\n"
233233- (Sdk_control.Server_info.version info);
234234- Printf.printf "Capabilities: %s\n"
235235- (String.concat ", " (Sdk_control.Server_info.capabilities info))
226226+ let info = Client.server_info client in
227227+ Printf.printf "Claude CLI version: %s\n" (Control.Server_info.version info);
228228+ Printf.printf "Capabilities: %s\n"
229229+ (String.concat ", " (Control.Server_info.capabilities info))
236230 ]} *)
237231238232val set_permission_mode : t -> Permissions.Mode.t -> unit
···243237 - {!Permissions.Mode.Default} - Prompt for all permissions
244238 - {!Permissions.Mode.Accept_edits} - Auto-accept file edits
245239 - {!Permissions.Mode.Plan} - Planning mode with restricted execution
246246- - {!Permissions.Mode.Bypass_permissions} - Skip all permission checks
240240+ - {!Permissions.Mode.Bypass_permissions} - Skip all permission checks.
247241248248- @raise Failure if the server returns an error *)
242242+ @raise Failure if the server returns an error. *)
249243250244val set_model : t -> Model.t -> unit
251245(** [set_model t model] switches to a different AI model mid-conversation.
···255249 - [`Opus_4] - Maximum capability for complex tasks
256250 - [`Haiku_4] - Fast and cost-effective
257251258258- @raise Failure if the model is invalid or unavailable *)
252252+ @raise Failure if the model is invalid or unavailable. *)
259253260260-val get_server_info : t -> Server_info.t
261261-(** [get_server_info t] retrieves server capabilities and metadata.
254254+val server_info : t -> Server_info.t
255255+(** [server_info t] retrieves server capabilities and metadata.
262256263257 Returns information about:
264258 - Server version string
···268262269263 Useful for feature detection and debugging.
270264271271- @raise Failure if the server returns an error *)
265265+ @raise Failure if the server returns an error. *)
272266273267(** {1 Permission Discovery} *)
274268···299293 val send_user_message : t -> Message.User.t -> unit
300294 (** [send_user_message t msg] sends a user message to Claude. *)
301295302302- val send_raw : t -> Sdk_control.t -> unit
296296+ val send_raw : t -> Control.t -> unit
303297 (** [send_raw t control] sends a raw SDK control message.
304298305299 This is for advanced use cases that need direct control protocol access.
306300 *)
307301308308- val send_json : t -> Jsont.json -> unit
302302+ val send_json : t -> Json.t -> unit
309303 (** [send_json t json] sends raw JSON to Claude.
310304311305 This is the lowest-level send operation. Use with caution. *)
···314308 (** [receive_raw t] returns a lazy sequence of raw incoming messages.
315309316310 This includes all message types before Response conversion:
317317- - {!Proto.Incoming.t.constructor-Message} - Regular messages
318318- - {!Proto.Incoming.t.constructor-Control_response} - Control responses
319319- (normally handled internally)
320320- - {!Proto.Incoming.t.constructor-Control_request} - Control requests
321321- (normally handled internally)
311311+ - {!Incoming.t.constructor-Message} - Regular messages
312312+ - {!Incoming.t.constructor-Control_response} - Control responses (normally
313313+ handled internally)
314314+ - {!Incoming.t.constructor-Control_request} - Control requests (normally
315315+ handled internally)
322316323317 Most users should use {!receive} or {!run} instead. *)
324318end
+104-76
lib/content_block.ml
···88module Log = (val Logs.src_log src : Logs.LOG)
991010module Text = struct
1111- type t = Proto.Content_block.Text.t
1111+ type t = { text : string; unknown : Unknown.t }
12121313- let text = Proto.Content_block.Text.text
1414- let of_proto proto = proto
1515- let to_proto t = t
1313+ let create text = { text; unknown = Unknown.empty }
1414+ let make text unknown = { text; unknown }
1515+ let text t = t.text
1616+ let unknown t = t.unknown
1717+1818+ let json : t Json.codec =
1919+ let open Json.Codec in
2020+ Object.map ~kind:"Text" make
2121+ |> Object.member "text" string ~enc:text
2222+ |> Object.keep_unknown Unknown.mems ~enc:unknown
2323+ |> Object.seal
1624end
17251826module Tool_use = struct
1919- type t = Proto.Content_block.Tool_use.t
2727+ type t = { id : string; name : string; input : Json.t; unknown : Unknown.t }
2828+2929+ let create ~id ~name ~input = { id; name; input; unknown = Unknown.empty }
3030+ let make id name input unknown = { id; name; input; unknown }
3131+ let id t = t.id
3232+ let name t = t.name
3333+ let input t = Tool_input.of_json t.input
3434+ let unknown t = t.unknown
20352121- let id = Proto.Content_block.Tool_use.id
2222- let name = Proto.Content_block.Tool_use.name
2323- let input t = Proto.Content_block.Tool_use.input t |> Tool_input.of_json
2424- let of_proto proto = proto
2525- let to_proto t = t
3636+ let json : t Json.codec =
3737+ let open Json.Codec in
3838+ Object.map ~kind:"Tool_use" make
3939+ |> Object.member "id" string ~enc:id
4040+ |> Object.member "name" string ~enc:name
4141+ |> Object.member "input" Value.t ~enc:(fun t -> t.input)
4242+ |> Object.keep_unknown Unknown.mems ~enc:unknown
4343+ |> Object.seal
2644end
27452846module Tool_result = struct
2929- type t = Proto.Content_block.Tool_result.t
4747+ type t = {
4848+ tool_use_id : string;
4949+ content : Json.t option;
5050+ is_error : bool option;
5151+ unknown : Unknown.t;
5252+ }
30533131- let tool_use_id = Proto.Content_block.Tool_result.tool_use_id
3232- let content = Proto.Content_block.Tool_result.content
3333- let is_error = Proto.Content_block.Tool_result.is_error
3434- let of_proto proto = proto
3535- let to_proto t = t
5454+ let create ~tool_use_id ?content ?is_error () =
5555+ { tool_use_id; content; is_error; unknown = Unknown.empty }
5656+5757+ let make tool_use_id content is_error unknown =
5858+ { tool_use_id; content; is_error; unknown }
5959+6060+ let tool_use_id t = t.tool_use_id
6161+ let content t = t.content
6262+ let is_error t = t.is_error
6363+ let unknown t = t.unknown
6464+6565+ let json : t Json.codec =
6666+ let open Json.Codec in
6767+ Object.map ~kind:"Tool_result" make
6868+ |> Object.member "tool_use_id" string ~enc:tool_use_id
6969+ |> Object.opt_member "content" Value.t ~enc:content
7070+ |> Object.opt_member "is_error" bool ~enc:is_error
7171+ |> Object.keep_unknown Unknown.mems ~enc:unknown
7272+ |> Object.seal
3673end
37743875module Thinking = struct
3939- type t = Proto.Content_block.Thinking.t
7676+ type t = { thinking : string; signature : string; unknown : Unknown.t }
7777+7878+ let create ~thinking ~signature =
7979+ { thinking; signature; unknown = Unknown.empty }
40804141- let thinking = Proto.Content_block.Thinking.thinking
4242- let signature = Proto.Content_block.Thinking.signature
4343- let of_proto proto = proto
4444- let to_proto t = t
8181+ let make thinking signature unknown = { thinking; signature; unknown }
8282+ let thinking t = t.thinking
8383+ let signature t = t.signature
8484+ let unknown t = t.unknown
8585+8686+ let json : t Json.codec =
8787+ let open Json.Codec in
8888+ Object.map ~kind:"Thinking" make
8989+ |> Object.member "thinking" string ~enc:thinking
9090+ |> Object.member "signature" string ~enc:signature
9191+ |> Object.keep_unknown Unknown.mems ~enc:unknown
9292+ |> Object.seal
4593end
46944795type t =
···5098 | Tool_result of Tool_result.t
5199 | Thinking of Thinking.t
521005353-let text s =
5454- let proto = Proto.Content_block.text s in
5555- match proto with
5656- | Proto.Content_block.Text proto_text -> Text (Text.of_proto proto_text)
5757- | _ -> failwith "Internal error: Proto.Content_block.text returned non-Text"
101101+let text s = Text (Text.create s)
5810259103let tool_use ~id ~name ~input =
6060- let json_input = Tool_input.to_json input in
6161- let proto = Proto.Content_block.tool_use ~id ~name ~input:json_input in
6262- match proto with
6363- | Proto.Content_block.Tool_use proto_tool_use ->
6464- Tool_use (Tool_use.of_proto proto_tool_use)
6565- | _ ->
6666- failwith
6767- "Internal error: Proto.Content_block.tool_use returned non-Tool_use"
104104+ Tool_use (Tool_use.create ~id ~name ~input:(Tool_input.to_json input))
6810569106let tool_result ~tool_use_id ?content ?is_error () =
7070- let proto =
7171- Proto.Content_block.tool_result ~tool_use_id ?content ?is_error ()
7272- in
7373- match proto with
7474- | Proto.Content_block.Tool_result proto_tool_result ->
7575- Tool_result (Tool_result.of_proto proto_tool_result)
7676- | _ ->
7777- failwith
7878- "Internal error: Proto.Content_block.tool_result returned \
7979- non-Tool_result"
107107+ Tool_result (Tool_result.create ~tool_use_id ?content ?is_error ())
8010881109let thinking ~thinking ~signature =
8282- let proto = Proto.Content_block.thinking ~thinking ~signature in
8383- match proto with
8484- | Proto.Content_block.Thinking proto_thinking ->
8585- Thinking (Thinking.of_proto proto_thinking)
8686- | _ ->
8787- failwith
8888- "Internal error: Proto.Content_block.thinking returned non-Thinking"
8989-9090-let of_proto proto =
9191- match proto with
9292- | Proto.Content_block.Text t -> Text (Text.of_proto t)
9393- | Proto.Content_block.Tool_use t -> Tool_use (Tool_use.of_proto t)
9494- | Proto.Content_block.Tool_result t -> Tool_result (Tool_result.of_proto t)
9595- | Proto.Content_block.Thinking t -> Thinking (Thinking.of_proto t)
9696-9797-let to_proto = function
9898- | Text t -> Proto.Content_block.Text (Text.to_proto t)
9999- | Tool_use t -> Proto.Content_block.Tool_use (Tool_use.to_proto t)
100100- | Tool_result t -> Proto.Content_block.Tool_result (Tool_result.to_proto t)
101101- | Thinking t -> Proto.Content_block.Thinking (Thinking.to_proto t)
110110+ Thinking (Thinking.create ~thinking ~signature)
102111103103-let log_received t =
104104- let proto = to_proto t in
105105- Log.debug (fun m ->
106106- m "Received content block: %a"
107107- (Jsont.pp_value Proto.Content_block.jsont ())
108108- proto)
112112+let json : t Json.codec =
113113+ let open Json.Codec in
114114+ let case_map kind obj dec = Object.Case.map kind obj ~dec in
115115+ let case_text = case_map "text" Text.json (fun v -> Text v) in
116116+ let case_tool_use = case_map "tool_use" Tool_use.json (fun v -> Tool_use v) in
117117+ let case_tool_result =
118118+ case_map "tool_result" Tool_result.json (fun v -> Tool_result v)
119119+ in
120120+ let case_thinking = case_map "thinking" Thinking.json (fun v -> Thinking v) in
121121+ let enc_case = function
122122+ | Text v -> Object.Case.value case_text v
123123+ | Tool_use v -> Object.Case.value case_tool_use v
124124+ | Tool_result v -> Object.Case.value case_tool_result v
125125+ | Thinking v -> Object.Case.value case_thinking v
126126+ in
127127+ let cases =
128128+ Object.Case.
129129+ [
130130+ make case_text;
131131+ make case_tool_use;
132132+ make case_tool_result;
133133+ make case_thinking;
134134+ ]
135135+ in
136136+ Object.map ~kind:"Content_block" Fun.id
137137+ |> Object.case_member "type" string ~enc:Fun.id ~enc_case cases
138138+ ~tag_to_string:Fun.id ~tag_compare:String.compare
139139+ |> Object.seal
109140110110-let log_sending t =
111111- let proto = to_proto t in
112112- Log.debug (fun m ->
113113- m "Sending content block: %a"
114114- (Jsont.pp_value Proto.Content_block.jsont ())
115115- proto)
141141+let pp ppf t = Json.pp_value json ppf t
142142+let log_received t = Log.debug (fun m -> m "Received content block: %a" pp t)
143143+let log_sending t = Log.debug (fun m -> m "Sending content block: %a" pp t)
+21-81
lib/content_block.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Content blocks in messages. Opaque types without wire concerns.
77-88- This module provides opaque wrapper types around the proto content block
99- types, hiding unknown fields and wire format details from the public API. *)
66+(** Content blocks in messages. *)
107118val src : Logs.Src.t
129(** Log source for content block operations. *)
···1411(** {1 Text Blocks} *)
15121613module Text : sig
1717- (** Plain text content blocks. *)
1818-1914 type t
2020- (** The type of text blocks (opaque). *)
21151616+ val create : string -> t
2217 val text : t -> string
2323- (** [text t] returns the text content of the block. *)
2424-2525- (** {1 Internal - for lib use only} *)
2626-2727- val of_proto : Proto.Content_block.Text.t -> t
2828- (** [of_proto proto] wraps a proto text block. *)
2929-3030- val to_proto : t -> Proto.Content_block.Text.t
3131- (** [to_proto t] extracts the proto text block. *)
1818+ val unknown : t -> Unknown.t
1919+ val json : t Json.codec
3220end
33213422(** {1 Tool Use Blocks} *)
35233624module Tool_use : sig
3737- (** Tool invocation requests from the assistant. *)
3838-3925 type t
4040- (** The type of tool use blocks (opaque). *)
41262727+ val create : id:string -> name:string -> input:Json.t -> t
4228 val id : t -> string
4343- (** [id t] returns the unique identifier of the tool use. *)
4444-4529 val name : t -> string
4646- (** [name t] returns the name of the tool being invoked. *)
47304831 val input : t -> Tool_input.t
4949- (** [input t] returns the input parameters for the tool. *)
3232+ (** [input t] returns the tool input as a typed {!Tool_input.t}. *)
50335151- (** {1 Internal - for lib use only} *)
5252-5353- val of_proto : Proto.Content_block.Tool_use.t -> t
5454- (** [of_proto proto] wraps a proto tool use block. *)
5555-5656- val to_proto : t -> Proto.Content_block.Tool_use.t
5757- (** [to_proto t] extracts the proto tool use block. *)
3434+ val unknown : t -> Unknown.t
3535+ val json : t Json.codec
5836end
59376038(** {1 Tool Result Blocks} *)
61396240module Tool_result : sig
6363- (** Results from tool invocations. *)
4141+ type t
64426565- type t
6666- (** The type of tool result blocks (opaque). *)
4343+ val create :
4444+ tool_use_id:string -> ?content:Json.t -> ?is_error:bool -> unit -> t
67456846 val tool_use_id : t -> string
6969- (** [tool_use_id t] returns the ID of the corresponding tool use. *)
7070-7171- val content : t -> Jsont.json option
7272- (** [content t] returns the optional result content as raw JSON. *)
7373-4747+ val content : t -> Json.t option
7448 val is_error : t -> bool option
7575- (** [is_error t] returns whether this result represents an error. *)
7676-7777- (** {1 Internal - for lib use only} *)
7878-7979- val of_proto : Proto.Content_block.Tool_result.t -> t
8080- (** [of_proto proto] wraps a proto tool result block. *)
8181-8282- val to_proto : t -> Proto.Content_block.Tool_result.t
8383- (** [to_proto t] extracts the proto tool result block. *)
4949+ val unknown : t -> Unknown.t
5050+ val json : t Json.codec
8451end
85528653(** {1 Thinking Blocks} *)
87548855module Thinking : sig
8989- (** Assistant's internal reasoning blocks. *)
9090-9156 type t
9292- (** The type of thinking blocks (opaque). *)
93575858+ val create : thinking:string -> signature:string -> t
9459 val thinking : t -> string
9595- (** [thinking t] returns the thinking content. *)
9696-9760 val signature : t -> string
9898- (** [signature t] returns the cryptographic signature. *)
9999-100100- (** {1 Internal - for lib use only} *)
101101-102102- val of_proto : Proto.Content_block.Thinking.t -> t
103103- (** [of_proto proto] wraps a proto thinking block. *)
104104-105105- val to_proto : t -> Proto.Content_block.Thinking.t
106106- (** [to_proto t] extracts the proto thinking block. *)
6161+ val unknown : t -> Unknown.t
6262+ val json : t Json.codec
10763end
1086410965(** {1 Content Block Union Type} *)
···11369 | Tool_use of Tool_use.t
11470 | Tool_result of Tool_result.t
11571 | Thinking of Thinking.t
116116- (** The type of content blocks, which can be text, tool use, tool result,
117117- or thinking. *)
7272+7373+val pp : Format.formatter -> t -> unit
1187411975(** {1 Constructors} *)
1207612177val text : string -> t
122122-(** [text s] creates a text content block. *)
123123-12478val tool_use : id:string -> name:string -> input:Tool_input.t -> t
125125-(** [tool_use ~id ~name ~input] creates a tool use content block. *)
1267912780val tool_result :
128128- tool_use_id:string -> ?content:Jsont.json -> ?is_error:bool -> unit -> t
129129-(** [tool_result ~tool_use_id ?content ?is_error ()] creates a tool result
130130- content block. Content can be a string or array. *)
8181+ tool_use_id:string -> ?content:Json.t -> ?is_error:bool -> unit -> t
1318213283val thinking : thinking:string -> signature:string -> t
133133-(** [thinking ~thinking ~signature] creates a thinking content block. *)
134134-135135-(** {1 Conversion} *)
136136-137137-val of_proto : Proto.Content_block.t -> t
138138-(** [of_proto proto] converts a proto content block to a lib content block. *)
139139-140140-val to_proto : t -> Proto.Content_block.t
141141-(** [to_proto t] converts a lib content block to a proto content block. *)
8484+val json : t Json.codec
1428514386(** {1 Logging} *)
1448714588val log_received : t -> unit
146146-(** [log_received t] logs that a content block was received. *)
147147-14889val log_sending : t -> unit
149149-(** [log_sending t] logs that a content block is being sent. *)
+481-33
lib/control.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-let src = Logs.Src.create "claude.control" ~doc:"Claude control messages"
66+open Json.Codec
77+88+let src = Logs.Src.create "claude.control" ~doc:"Claude control protocol"
79810module Log = (val Logs.src_log src : Logs.LOG)
9111010-type t = {
1212+module Request = struct
1313+ type interrupt = { subtype : [ `Interrupt ]; unknown : Unknown.t }
1414+1515+ type permission = {
1616+ subtype : [ `Can_use_tool ];
1717+ tool_name : string;
1818+ input : Json.t;
1919+ permission_suggestions : Permissions.Update.t list option;
2020+ blocked_path : string option;
2121+ unknown : Unknown.t;
2222+ }
2323+2424+ type initialize = {
2525+ subtype : [ `Initialize ];
2626+ hooks : (string * Json.t) list option;
2727+ unknown : Unknown.t;
2828+ }
2929+3030+ type set_permission_mode = {
3131+ subtype : [ `Set_permission_mode ];
3232+ mode : Permissions.Mode.t;
3333+ unknown : Unknown.t;
3434+ }
3535+3636+ type hook_callback = {
3737+ subtype : [ `Hook_callback ];
3838+ callback_id : string;
3939+ input : Json.t;
4040+ tool_use_id : string option;
4141+ unknown : Unknown.t;
4242+ }
4343+4444+ type mcp_message = {
4545+ subtype : [ `Mcp_message ];
4646+ server_name : string;
4747+ message : Json.t;
4848+ unknown : Unknown.t;
4949+ }
5050+5151+ type set_model = {
5252+ subtype : [ `Set_model ];
5353+ model : string;
5454+ unknown : Unknown.t;
5555+ }
5656+5757+ type get_server_info = { subtype : [ `Get_server_info ]; unknown : Unknown.t }
5858+5959+ type t =
6060+ | Interrupt of interrupt
6161+ | Permission of permission
6262+ | Initialize of initialize
6363+ | Set_permission_mode of set_permission_mode
6464+ | Hook_callback of hook_callback
6565+ | Mcp_message of mcp_message
6666+ | Set_model of set_model
6767+ | Get_server_info of get_server_info
6868+6969+ let interrupt ?(unknown = Unknown.empty) () =
7070+ Interrupt { subtype = `Interrupt; unknown }
7171+7272+ let permission ~tool_name ~input ?permission_suggestions ?blocked_path
7373+ ?(unknown = Unknown.empty) () =
7474+ Permission
7575+ {
7676+ subtype = `Can_use_tool;
7777+ tool_name;
7878+ input;
7979+ permission_suggestions;
8080+ blocked_path;
8181+ unknown;
8282+ }
8383+8484+ let initialize ?hooks ?(unknown = Unknown.empty) () =
8585+ Initialize { subtype = `Initialize; hooks; unknown }
8686+8787+ let set_permission_mode ~mode ?(unknown = Unknown.empty) () =
8888+ Set_permission_mode { subtype = `Set_permission_mode; mode; unknown }
8989+9090+ let hook_callback ~callback_id ~input ?tool_use_id ?(unknown = Unknown.empty)
9191+ () =
9292+ Hook_callback
9393+ { subtype = `Hook_callback; callback_id; input; tool_use_id; unknown }
9494+9595+ let mcp_message ~server_name ~message ?(unknown = Unknown.empty) () =
9696+ Mcp_message { subtype = `Mcp_message; server_name; message; unknown }
9797+9898+ let set_model ~model ?(unknown = Unknown.empty) () =
9999+ Set_model { subtype = `Set_model; model; unknown }
100100+101101+ let get_server_info ?(unknown = Unknown.empty) () =
102102+ Get_server_info { subtype = `Get_server_info; unknown }
103103+104104+ (* Individual record codecs *)
105105+ let interrupt_jsont : interrupt Json.codec =
106106+ let make (unknown : Unknown.t) : interrupt =
107107+ { subtype = `Interrupt; unknown }
108108+ in
109109+ Object.map ~kind:"Interrupt" make
110110+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : interrupt) -> r.unknown)
111111+ |> Object.seal
112112+113113+ let permission_jsont : permission Json.codec =
114114+ let make tool_name input permission_suggestions blocked_path
115115+ (unknown : Unknown.t) : permission =
116116+ {
117117+ subtype = `Can_use_tool;
118118+ tool_name;
119119+ input;
120120+ permission_suggestions;
121121+ blocked_path;
122122+ unknown;
123123+ }
124124+ in
125125+ Object.map ~kind:"Permission" make
126126+ |> Object.member "tool_name" string ~enc:(fun (r : permission) ->
127127+ r.tool_name)
128128+ |> Object.member "input" Value.t ~enc:(fun (r : permission) -> r.input)
129129+ |> Object.opt_member "permission_suggestions" (list Permissions.Update.json)
130130+ ~enc:(fun (r : permission) -> r.permission_suggestions)
131131+ |> Object.opt_member "blocked_path" string ~enc:(fun (r : permission) ->
132132+ r.blocked_path)
133133+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : permission) -> r.unknown)
134134+ |> Object.seal
135135+136136+ let initialize_jsont : initialize Json.codec =
137137+ (* The hooks field is an object with string keys and json values *)
138138+ let hooks_map_jsont = Object.as_string_map Value.t in
139139+ let module StringMap = Map.Make (String) in
140140+ let hooks_jsont =
141141+ map
142142+ ~dec:(fun m -> StringMap.bindings m)
143143+ ~enc:(fun l -> StringMap.of_seq (List.to_seq l))
144144+ hooks_map_jsont
145145+ in
146146+ let make hooks (unknown : Unknown.t) : initialize =
147147+ { subtype = `Initialize; hooks; unknown }
148148+ in
149149+ Object.map ~kind:"Initialize" make
150150+ |> Object.opt_member "hooks" hooks_jsont ~enc:(fun (r : initialize) ->
151151+ r.hooks)
152152+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : initialize) -> r.unknown)
153153+ |> Object.seal
154154+155155+ let set_permission_mode_jsont : set_permission_mode Json.codec =
156156+ let make mode (unknown : Unknown.t) : set_permission_mode =
157157+ { subtype = `Set_permission_mode; mode; unknown }
158158+ in
159159+ Object.map ~kind:"SetPermissionMode" make
160160+ |> Object.member "mode" Permissions.Mode.json
161161+ ~enc:(fun (r : set_permission_mode) -> r.mode)
162162+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : set_permission_mode) ->
163163+ r.unknown)
164164+ |> Object.seal
165165+166166+ let hook_callback_jsont : hook_callback Json.codec =
167167+ let make callback_id input tool_use_id (unknown : Unknown.t) : hook_callback
168168+ =
169169+ { subtype = `Hook_callback; callback_id; input; tool_use_id; unknown }
170170+ in
171171+ Object.map ~kind:"HookCallback" make
172172+ |> Object.member "callback_id" string ~enc:(fun (r : hook_callback) ->
173173+ r.callback_id)
174174+ |> Object.member "input" Value.t ~enc:(fun (r : hook_callback) -> r.input)
175175+ |> Object.opt_member "tool_use_id" string ~enc:(fun (r : hook_callback) ->
176176+ r.tool_use_id)
177177+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : hook_callback) ->
178178+ r.unknown)
179179+ |> Object.seal
180180+181181+ let mcp_message_jsont : mcp_message Json.codec =
182182+ let make server_name message (unknown : Unknown.t) : mcp_message =
183183+ { subtype = `Mcp_message; server_name; message; unknown }
184184+ in
185185+ Object.map ~kind:"McpMessage" make
186186+ |> Object.member "server_name" string ~enc:(fun (r : mcp_message) ->
187187+ r.server_name)
188188+ |> Object.member "message" Value.t ~enc:(fun (r : mcp_message) -> r.message)
189189+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : mcp_message) ->
190190+ r.unknown)
191191+ |> Object.seal
192192+193193+ let set_model_jsont : set_model Json.codec =
194194+ let make model (unknown : Unknown.t) : set_model =
195195+ { subtype = `Set_model; model; unknown }
196196+ in
197197+ Object.map ~kind:"SetModel" make
198198+ |> Object.member "model" string ~enc:(fun (r : set_model) -> r.model)
199199+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : set_model) -> r.unknown)
200200+ |> Object.seal
201201+202202+ let get_server_info_jsont : get_server_info Json.codec =
203203+ let make (unknown : Unknown.t) : get_server_info =
204204+ { subtype = `Get_server_info; unknown }
205205+ in
206206+ Object.map ~kind:"GetServerInfo" make
207207+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : get_server_info) ->
208208+ r.unknown)
209209+ |> Object.seal
210210+211211+ (* Main variant codec using subtype discriminator *)
212212+ let json : t Json.codec =
213213+ let case_interrupt =
214214+ Object.Case.map "interrupt" interrupt_jsont ~dec:(fun v -> Interrupt v)
215215+ in
216216+ let case_permission =
217217+ Object.Case.map "can_use_tool" permission_jsont ~dec:(fun v ->
218218+ Permission v)
219219+ in
220220+ let case_initialize =
221221+ Object.Case.map "initialize" initialize_jsont ~dec:(fun v -> Initialize v)
222222+ in
223223+ let case_set_permission_mode =
224224+ Object.Case.map "set_permission_mode" set_permission_mode_jsont
225225+ ~dec:(fun v -> Set_permission_mode v)
226226+ in
227227+ let case_hook_callback =
228228+ Object.Case.map "hook_callback" hook_callback_jsont ~dec:(fun v ->
229229+ Hook_callback v)
230230+ in
231231+ let case_mcp_message =
232232+ Object.Case.map "mcp_message" mcp_message_jsont ~dec:(fun v ->
233233+ Mcp_message v)
234234+ in
235235+ let case_set_model =
236236+ Object.Case.map "set_model" set_model_jsont ~dec:(fun v -> Set_model v)
237237+ in
238238+ let case_get_server_info =
239239+ Object.Case.map "get_server_info" get_server_info_jsont ~dec:(fun v ->
240240+ Get_server_info v)
241241+ in
242242+243243+ let enc_case = function
244244+ | Interrupt v -> Object.Case.value case_interrupt v
245245+ | Permission v -> Object.Case.value case_permission v
246246+ | Initialize v -> Object.Case.value case_initialize v
247247+ | Set_permission_mode v -> Object.Case.value case_set_permission_mode v
248248+ | Hook_callback v -> Object.Case.value case_hook_callback v
249249+ | Mcp_message v -> Object.Case.value case_mcp_message v
250250+ | Set_model v -> Object.Case.value case_set_model v
251251+ | Get_server_info v -> Object.Case.value case_get_server_info v
252252+ in
253253+254254+ let cases =
255255+ Object.Case.
256256+ [
257257+ make case_interrupt;
258258+ make case_permission;
259259+ make case_initialize;
260260+ make case_set_permission_mode;
261261+ make case_hook_callback;
262262+ make case_mcp_message;
263263+ make case_set_model;
264264+ make case_get_server_info;
265265+ ]
266266+ in
267267+268268+ Object.map ~kind:"Request" Fun.id
269269+ |> Object.case_member "subtype" string ~enc:Fun.id ~enc_case cases
270270+ ~tag_to_string:Fun.id ~tag_compare:String.compare
271271+ |> Object.seal
272272+end
273273+274274+module Response = struct
275275+ module Error_code = struct
276276+ type t =
277277+ [ `Parse_error
278278+ | `Invalid_request
279279+ | `Method_not_found
280280+ | `Invalid_params
281281+ | `Internal_error
282282+ | `Custom of int ]
283283+284284+ let to_int : [< t ] -> int = function
285285+ | `Parse_error -> -32700
286286+ | `Invalid_request -> -32600
287287+ | `Method_not_found -> -32601
288288+ | `Invalid_params -> -32602
289289+ | `Internal_error -> -32603
290290+ | `Custom n -> n
291291+292292+ let of_int = function
293293+ | -32700 -> `Parse_error
294294+ | -32600 -> `Invalid_request
295295+ | -32601 -> `Method_not_found
296296+ | -32602 -> `Invalid_params
297297+ | -32603 -> `Internal_error
298298+ | n -> `Custom n
299299+300300+ let json : t Json.codec = map ~kind:"ErrorCode" ~dec:of_int ~enc:to_int int
301301+ end
302302+303303+ type error_detail = { code : int; message : string; data : Json.t option }
304304+305305+ let error_detail ~code ~message ?data () =
306306+ { code = Error_code.to_int code; message; data }
307307+308308+ let error_detail_jsont : error_detail Json.codec =
309309+ let make code message data = { code; message; data } in
310310+ Object.map ~kind:"ErrorDetail" make
311311+ |> Object.member "code" int ~enc:(fun e -> e.code)
312312+ |> Object.member "message" string ~enc:(fun e -> e.message)
313313+ |> Object.opt_member "data" Value.t ~enc:(fun e -> e.data)
314314+ |> Object.seal
315315+316316+ type success = {
317317+ subtype : [ `Success ];
318318+ request_id : string;
319319+ response : Json.t option;
320320+ unknown : Unknown.t;
321321+ }
322322+323323+ type error = {
324324+ subtype : [ `Error ];
325325+ request_id : string;
326326+ error : error_detail;
327327+ unknown : Unknown.t;
328328+ }
329329+330330+ type t = Success of success | Error of error
331331+332332+ let success ~request_id ?response ?(unknown = Unknown.empty) () =
333333+ Success { subtype = `Success; request_id; response; unknown }
334334+335335+ let error ~request_id ~error ?(unknown = Unknown.empty) () =
336336+ Error { subtype = `Error; request_id; error; unknown }
337337+338338+ (* Individual record codecs *)
339339+ let success_jsont : success Json.codec =
340340+ let make request_id response (unknown : Unknown.t) : success =
341341+ { subtype = `Success; request_id; response; unknown }
342342+ in
343343+ Object.map ~kind:"Success" make
344344+ |> Object.member "request_id" string ~enc:(fun (r : success) ->
345345+ r.request_id)
346346+ |> Object.opt_member "response" Value.t ~enc:(fun (r : success) ->
347347+ r.response)
348348+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : success) -> r.unknown)
349349+ |> Object.seal
350350+351351+ let error_jsont : error Json.codec =
352352+ let make request_id error (unknown : Unknown.t) : error =
353353+ { subtype = `Error; request_id; error; unknown }
354354+ in
355355+ Object.map ~kind:"Error" make
356356+ |> Object.member "request_id" string ~enc:(fun (r : error) -> r.request_id)
357357+ |> Object.member "error" error_detail_jsont ~enc:(fun (r : error) ->
358358+ r.error)
359359+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : error) -> r.unknown)
360360+ |> Object.seal
361361+362362+ (* Main variant codec using subtype discriminator *)
363363+ let json : t Json.codec =
364364+ let case_success =
365365+ Object.Case.map "success" success_jsont ~dec:(fun v -> Success v)
366366+ in
367367+ let case_error =
368368+ Object.Case.map "error" error_jsont ~dec:(fun v -> Error v)
369369+ in
370370+371371+ let enc_case = function
372372+ | Success v -> Object.Case.value case_success v
373373+ | Error v -> Object.Case.value case_error v
374374+ in
375375+376376+ let cases = Object.Case.[ make case_success; make case_error ] in
377377+378378+ Object.map ~kind:"Response" Fun.id
379379+ |> Object.case_member "subtype" string ~enc:Fun.id ~enc_case cases
380380+ ~tag_to_string:Fun.id ~tag_compare:String.compare
381381+ |> Object.seal
382382+end
383383+384384+type control_request = {
385385+ type_ : [ `Control_request ];
11386 request_id : string;
1212- subtype : string;
1313- data : Jsont.json;
387387+ request : Request.t;
388388+ unknown : Unknown.t;
389389+}
390390+391391+type control_response = {
392392+ type_ : [ `Control_response ];
393393+ response : Response.t;
14394 unknown : Unknown.t;
15395}
163961717-let jsont =
1818- Jsont.Object.map ~kind:"Control" (fun request_id subtype data unknown ->
1919- { request_id; subtype; data; unknown })
2020- |> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun t -> t.request_id)
2121- |> Jsont.Object.mem "subtype" Jsont.string ~enc:(fun t -> t.subtype)
2222- |> Jsont.Object.mem "data" Jsont.json ~enc:(fun t -> t.data)
2323- |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun t -> t.unknown)
2424- |> Jsont.Object.finish
397397+type t = Request of control_request | Response of control_response
253982626-let create ~request_id ~subtype ~data =
2727- { request_id; subtype; data; unknown = Unknown.empty }
399399+let request ~request_id ~request ?(unknown = Unknown.empty) () =
400400+ Request { type_ = `Control_request; request_id; request; unknown }
284012929-let request_id t = t.request_id
3030-let subtype t = t.subtype
3131-let data t = t.data
402402+let response ~response ?(unknown = Unknown.empty) () =
403403+ Response { type_ = `Control_response; response; unknown }
324043333-let to_json t =
3434- Jsont_bytesrw.encode_string ~format:Jsont.Minify jsont t
3535- |> Err.get_ok ~msg:"Control.to_json: "
3636- |> Jsont_bytesrw.decode_string' Jsont.json
3737- |> Result.map_error Jsont.Error.to_string
3838- |> Err.get_ok ~msg:"Control.to_json: "
405405+(* Individual record codecs *)
406406+let control_request_jsont : control_request Json.codec =
407407+ let make request_id request (unknown : Unknown.t) : control_request =
408408+ { type_ = `Control_request; request_id; request; unknown }
409409+ in
410410+ Object.map ~kind:"ControlRequest" make
411411+ |> Object.member "request_id" string ~enc:(fun (r : control_request) ->
412412+ r.request_id)
413413+ |> Object.member "request" Request.json ~enc:(fun (r : control_request) ->
414414+ r.request)
415415+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : control_request) ->
416416+ r.unknown)
417417+ |> Object.seal
394184040-let of_json json =
4141- Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json
4242- |> Err.get_ok' ~msg:"Control.of_json: "
4343- |> Jsont_bytesrw.decode_string jsont
4444- |> Err.get_ok' ~msg:"Control.of_json: "
419419+let control_response_jsont : control_response Json.codec =
420420+ let make response (unknown : Unknown.t) : control_response =
421421+ { type_ = `Control_response; response; unknown }
422422+ in
423423+ Object.map ~kind:"ControlResponse" make
424424+ |> Object.member "response" Response.json ~enc:(fun (r : control_response) ->
425425+ r.response)
426426+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : control_response) ->
427427+ r.unknown)
428428+ |> Object.seal
454294646-let log_received t =
4747- Log.debug (fun m ->
4848- m "Received control message: %a" (Jsont.pp_value jsont ()) t)
430430+(* Main variant codec using type discriminator *)
431431+let json : t Json.codec =
432432+ let case_request =
433433+ Object.Case.map "control_request" control_request_jsont ~dec:(fun v ->
434434+ Request v)
435435+ in
436436+ let case_response =
437437+ Object.Case.map "control_response" control_response_jsont ~dec:(fun v ->
438438+ Response v)
439439+ in
494405050-let log_sending t =
441441+ let enc_case = function
442442+ | Request v -> Object.Case.value case_request v
443443+ | Response v -> Object.Case.value case_response v
444444+ in
445445+446446+ let cases = Object.Case.[ make case_request; make case_response ] in
447447+448448+ Object.map ~kind:"Control" Fun.id
449449+ |> Object.case_member "type" string ~enc:Fun.id ~enc_case cases
450450+ ~tag_to_string:Fun.id ~tag_compare:String.compare
451451+ |> Object.seal
452452+453453+let pp ppf t = Json.pp_value json ppf t
454454+455455+let log_request req =
456456+ Log.debug (fun m -> m "control request: %a" (Json.pp_value Request.json) req)
457457+458458+let log_response resp =
51459 Log.debug (fun m ->
5252- m "Sending control message: %a" (Jsont.pp_value jsont ()) t)
460460+ m "control response: %a" (Json.pp_value Response.json) resp)
461461+462462+(** Server information *)
463463+module Server_info = struct
464464+ type t = {
465465+ version : string;
466466+ capabilities : string list;
467467+ commands : string list;
468468+ output_styles : string list;
469469+ unknown : Unknown.t;
470470+ }
471471+472472+ let create ~version ~capabilities ~commands ~output_styles
473473+ ?(unknown = Unknown.empty) () =
474474+ { version; capabilities; commands; output_styles; unknown }
475475+476476+ let version t = t.version
477477+ let capabilities t = t.capabilities
478478+ let commands t = t.commands
479479+ let output_styles t = t.output_styles
480480+ let unknown t = t.unknown
481481+482482+ let json : t Json.codec =
483483+ let make version capabilities commands output_styles (unknown : Unknown.t) :
484484+ t =
485485+ { version; capabilities; commands; output_styles; unknown }
486486+ in
487487+ Object.map ~kind:"ServerInfo" make
488488+ |> Object.member "version" string ~enc:(fun (r : t) -> r.version)
489489+ |> Object.member "capabilities" (list string)
490490+ ~enc:(fun (r : t) -> r.capabilities)
491491+ ~dec_absent:[]
492492+ |> Object.member "commands" (list string)
493493+ ~enc:(fun (r : t) -> r.commands)
494494+ ~dec_absent:[]
495495+ |> Object.member "outputStyles" (list string)
496496+ ~enc:(fun (r : t) -> r.output_styles)
497497+ ~dec_absent:[]
498498+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : t) -> r.unknown)
499499+ |> Object.seal
500500+end
+366-29
lib/control.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Control messages for Claude session management.
66+(** SDK Control Protocol for Claude.
77+88+ This module defines the typed SDK control protocol for bidirectional
99+ communication between the SDK and the Claude CLI. It handles:
1010+1111+ - Permission requests (tool usage authorization)
1212+ - Hook callbacks (intercepting and modifying tool execution)
1313+ - Dynamic control (changing settings mid-conversation)
1414+ - Server introspection (querying capabilities)
1515+1616+ {2 Protocol Overview}
1717+1818+ The SDK control protocol is a JSON-based request/response protocol that runs
1919+ alongside the main message stream. It enables:
2020+2121+ 1. {b Callbacks}: Claude asks the SDK for permission or hook execution 2.
2222+ {b Control}: SDK changes Claude's behavior dynamically 3. {b Introspection}:
2323+ SDK queries server metadata
2424+2525+ {2 Request/Response Flow}
2626+2727+ {v
2828+ SDK Claude CLI
2929+ | |
3030+ |-- Initialize (with hooks) --> |
3131+ |<-- Permission Request --------| (for tool usage)
3232+ |-- Allow/Deny Response ------> |
3333+ | |
3434+ |<-- Hook Callback -------------| (pre/post tool)
3535+ |-- Hook Result -------------> |
3636+ | |
3737+ |-- Set Model ---------------> | (dynamic control)
3838+ |<-- Success Response ----------|
3939+ | |
4040+ |-- Get Server Info ----------> |
4141+ |<-- Server Info Response ------|
4242+ v}
4343+4444+ {2 Usage}
4545+4646+ Most users won't interact with this module directly. The {!Client} module
4747+ handles the protocol automatically. However, this module is exposed for:
4848+4949+ - Understanding the control protocol
5050+ - Implementing custom control logic
5151+ - Debugging control message flow
5252+ - Advanced SDK extensions
5353+5454+ {2 Dynamic Control Examples}
75588- Control messages are used to manage the interaction flow with Claude,
99- including session control, cancellation requests, and other operational
1010- commands. *)
5656+ See {!Client.set_permission_mode}, {!Client.set_model}, and
5757+ {!Client.server_info} for high-level APIs that use this protocol. *)
11581259val src : Logs.Src.t
1313-(** The log source for control message operations *)
6060+(** The log source for SDK control operations. *)
6161+6262+(** {1 Request Types} *)
6363+6464+module Request : sig
6565+ (** SDK control request types. *)
6666+6767+ type interrupt = { subtype : [ `Interrupt ]; unknown : Unknown.t }
6868+ (** Interrupt request to stop execution. *)
6969+7070+ type permission = {
7171+ subtype : [ `Can_use_tool ];
7272+ tool_name : string;
7373+ input : Json.t;
7474+ permission_suggestions : Permissions.Update.t list option;
7575+ blocked_path : string option;
7676+ unknown : Unknown.t;
7777+ }
7878+ (** Permission request for tool usage. *)
7979+8080+ type initialize = {
8181+ subtype : [ `Initialize ];
8282+ hooks : (string * Json.t) list option; (* Hook event to configuration *)
8383+ unknown : Unknown.t;
8484+ }
8585+ (** Initialize request with optional hook configuration. *)
8686+8787+ type set_permission_mode = {
8888+ subtype : [ `Set_permission_mode ];
8989+ mode : Permissions.Mode.t;
9090+ unknown : Unknown.t;
9191+ }
9292+ (** Request to change permission mode. *)
9393+9494+ type hook_callback = {
9595+ subtype : [ `Hook_callback ];
9696+ callback_id : string;
9797+ input : Json.t;
9898+ tool_use_id : string option;
9999+ unknown : Unknown.t;
100100+ }
101101+ (** Hook callback request. *)
102102+103103+ type mcp_message = {
104104+ subtype : [ `Mcp_message ];
105105+ server_name : string;
106106+ message : Json.t;
107107+ unknown : Unknown.t;
108108+ }
109109+ (** MCP server message request. *)
110110+111111+ type set_model = {
112112+ subtype : [ `Set_model ];
113113+ model : string;
114114+ unknown : Unknown.t;
115115+ }
116116+ (** Request to change the AI model. *)
117117+118118+ type get_server_info = { subtype : [ `Get_server_info ]; unknown : Unknown.t }
119119+ (** Request to get server information. *)
120120+121121+ type t =
122122+ | Interrupt of interrupt
123123+ | Permission of permission
124124+ | Initialize of initialize
125125+ | Set_permission_mode of set_permission_mode
126126+ | Hook_callback of hook_callback
127127+ | Mcp_message of mcp_message
128128+ | Set_model of set_model
129129+ | Get_server_info of get_server_info
130130+ (** The type of SDK control requests. *)
131131+132132+ val interrupt : ?unknown:Unknown.t -> unit -> t
133133+ (** [interrupt ?unknown ()] creates an interrupt request. *)
134134+135135+ val permission :
136136+ tool_name:string ->
137137+ input:Json.t ->
138138+ ?permission_suggestions:Permissions.Update.t list ->
139139+ ?blocked_path:string ->
140140+ ?unknown:Unknown.t ->
141141+ unit ->
142142+ t
143143+ (** [permission ~tool_name ~input ?permission_suggestions ?blocked_path
144144+ ?unknown ()] creates a permission request. *)
145145+146146+ val initialize :
147147+ ?hooks:(string * Json.t) list -> ?unknown:Unknown.t -> unit -> t
148148+ (** [initialize ?hooks ?unknown ()] creates an initialize request. *)
149149+150150+ val set_permission_mode :
151151+ mode:Permissions.Mode.t -> ?unknown:Unknown.t -> unit -> t
152152+ (** [set_permission_mode ~mode ?unknown] creates a permission mode change
153153+ request. *)
154154+155155+ val hook_callback :
156156+ callback_id:string ->
157157+ input:Json.t ->
158158+ ?tool_use_id:string ->
159159+ ?unknown:Unknown.t ->
160160+ unit ->
161161+ t
162162+ (** [hook_callback ~callback_id ~input ?tool_use_id ?unknown ()] creates a
163163+ hook callback request. *)
164164+165165+ val mcp_message :
166166+ server_name:string -> message:Json.t -> ?unknown:Unknown.t -> unit -> t
167167+ (** [mcp_message ~server_name ~message ?unknown] creates an MCP message
168168+ request. *)
169169+170170+ val set_model : model:string -> ?unknown:Unknown.t -> unit -> t
171171+ (** [set_model ~model ?unknown] creates a model change request. *)
172172+173173+ val get_server_info : ?unknown:Unknown.t -> unit -> t
174174+ (** [get_server_info ?unknown ()] creates a server info request. *)
175175+176176+ val json : t Json.codec
177177+ (** [json] is the json codec for requests. Use [Json.pp_value json] for
178178+ pretty-printing. *)
179179+end
180180+181181+(** {1 Response Types} *)
182182+183183+module Response : sig
184184+ (** SDK control response types. *)
185185+186186+ module Error_code : sig
187187+ type t =
188188+ [ `Parse_error (** -32700: Invalid JSON received *)
189189+ | `Invalid_request (** -32600: The request object is invalid *)
190190+ | `Method_not_found (** -32601: The requested method does not exist *)
191191+ | `Invalid_params (** -32602: Invalid method parameters *)
192192+ | `Internal_error (** -32603: Internal server error *)
193193+ | `Custom of int (** Application-specific error codes *) ]
194194+195195+ val to_int : [< t ] -> int
196196+ (** [to_int t] converts an error code to its integer representation. *)
197197+198198+ val of_int : int -> t
199199+ (** [of_int n] converts an integer to a variant. Unknown codes become
200200+ [`Custom n]. *)
201201+202202+ val json : t Json.codec
203203+ (** [json] encodes an error code as a JSON integer. *)
204204+ end
205205+206206+ type error_detail = {
207207+ code : int; (** Error code for programmatic handling *)
208208+ message : string; (** Human-readable error message *)
209209+ data : Json.t option; (** Optional additional error data *)
210210+ }
211211+ (** Structured error detail similar to JSON-RPC.
212212+213213+ This allows programmatic error handling with numeric error codes and
214214+ optional structured data for additional context. *)
215215+216216+ val error_detail :
217217+ code:[< Error_code.t ] ->
218218+ message:string ->
219219+ ?data:Json.t ->
220220+ unit ->
221221+ error_detail
222222+ (** [error_detail ~code ~message ?data ()] creates a structured error detail
223223+ using typed error codes.
224224+225225+ Example:
226226+ {[
227227+ error_detail ~code:`Method_not_found ~message:"Hook callback not found" ()
228228+ ]} *)
229229+230230+ val error_detail_jsont : error_detail Json.codec
231231+ (** [error_detail_jsont] is the Jsont codec for error details. *)
232232+233233+ type success = {
234234+ subtype : [ `Success ];
235235+ request_id : string;
236236+ response : Json.t option;
237237+ unknown : Unknown.t;
238238+ }
239239+ (** Successful response. *)
240240+241241+ type error = {
242242+ subtype : [ `Error ];
243243+ request_id : string;
244244+ error : error_detail;
245245+ unknown : Unknown.t;
246246+ }
247247+ (** Error response with structured error detail. *)
248248+249249+ type t =
250250+ | Success of success
251251+ | Error of error (** The type of SDK control responses. *)
252252+253253+ val success :
254254+ request_id:string -> ?response:Json.t -> ?unknown:Unknown.t -> unit -> t
255255+ (** [success ~request_id ?response ?unknown ()] creates a success response. *)
256256+257257+ val error :
258258+ request_id:string -> error:error_detail -> ?unknown:Unknown.t -> unit -> t
259259+ (** [error ~request_id ~error ?unknown] creates an error response with
260260+ structured error detail. *)
261261+262262+ val json : t Json.codec
263263+ (** [json] is the json codec for responses. Use [Json.pp_value json] for
264264+ pretty-printing. *)
265265+end
266266+267267+(** {1 Control Messages} *)
142681515-type t
1616-(** The type of control messages. *)
269269+type control_request = {
270270+ type_ : [ `Control_request ];
271271+ request_id : string;
272272+ request : Request.t;
273273+ unknown : Unknown.t;
274274+}
275275+(** Control request message. *)
276276+277277+type control_response = {
278278+ type_ : [ `Control_response ];
279279+ response : Response.t;
280280+ unknown : Unknown.t;
281281+}
282282+(** Control response message. *)
172831818-val jsont : t Jsont.t
1919-(** [jsont] is the jsont codec for control messages. *)
284284+val control_request_jsont : control_request Json.codec
285285+(** [control_request_jsont] is the json codec for control request messages. *)
202862121-val create : request_id:string -> subtype:string -> data:Jsont.json -> t
2222-(** [create ~request_id ~subtype ~data] creates a new control message.
2323- @param request_id Unique identifier for this control request
2424- @param subtype The specific type of control message
2525- @param data Additional JSON data for the control message *)
287287+val control_response_jsont : control_response Json.codec
288288+(** [control_response_jsont] is the json codec for control response messages. *)
262892727-val request_id : t -> string
2828-(** [request_id t] returns the unique request identifier. *)
290290+type t =
291291+ | Request of control_request
292292+ | Response of control_response (** The type of SDK control messages. *)
292933030-val subtype : t -> string
3131-(** [subtype t] returns the control message subtype. *)
294294+val request :
295295+ request_id:string -> request:Request.t -> ?unknown:Unknown.t -> unit -> t
296296+(** [request ~request_id ~request ?unknown ()] creates a control request
297297+ message. *)
322983333-val data : t -> Jsont.json
3434-(** [data t] returns the additional data associated with the control message. *)
299299+val response : response:Response.t -> ?unknown:Unknown.t -> unit -> t
300300+(** [response ~response ?unknown ()] creates a control response message. *)
353013636-val to_json : t -> Jsont.json
3737-(** [to_json t] converts the control message to its JSON representation. *)
302302+val json : t Json.codec
303303+(** [json] is the json codec for control messages. Use [Json.pp_value json] for
304304+ pretty-printing. *)
383053939-val of_json : Jsont.json -> t
4040-(** [of_json json] parses a control message from JSON.
4141- @raise Invalid_argument if the JSON is not a valid control message. *)
306306+val pp : Format.formatter -> t -> unit
307307+(** [pp ppf t] pretty-prints the SDK control message. *)
4230843309(** {1 Logging} *)
443104545-val log_received : t -> unit
4646-(** [log_received t] logs that a control message was received. *)
311311+val log_request : Request.t -> unit
312312+(** [log_request req] logs an SDK control request. *)
313313+314314+val log_response : Response.t -> unit
315315+(** [log_response resp] logs an SDK control response. *)
316316+317317+(** {1 Server Information}
473184848-val log_sending : t -> unit
4949-(** [log_sending t] logs that a control message is being sent. *)
319319+ Server information provides metadata about the Claude CLI server, including
320320+ version, capabilities, available commands, and output styles.
321321+322322+ {2 Use Cases}
323323+324324+ - Feature detection: Check if specific capabilities are available
325325+ - Version compatibility: Ensure minimum version requirements
326326+ - Debugging: Log server information for troubleshooting
327327+ - Dynamic adaptation: Adjust SDK behavior based on capabilities
328328+329329+ {2 Example}
330330+331331+ {[
332332+ let info = Client.server_info client in
333333+ Printf.printf "Claude CLI version: %s\n" (Server_info.version info);
334334+335335+ if List.mem "structured-output" (Server_info.capabilities info) then
336336+ Printf.printf "Structured output is supported\n"
337337+ else Printf.printf "Structured output not available\n"
338338+ ]} *)
339339+340340+module Server_info : sig
341341+ (** Server information and capabilities. *)
342342+343343+ type t = {
344344+ version : string; (** Server version string (e.g., "2.0.0") *)
345345+ capabilities : string list;
346346+ (** Available server capabilities (e.g., "hooks", "structured-output")
347347+ *)
348348+ commands : string list; (** Available CLI commands *)
349349+ output_styles : string list;
350350+ (** Supported output formats (e.g., "json", "stream-json") *)
351351+ unknown : Unknown.t; (** Unknown fields for forward compatibility *)
352352+ }
353353+ (** Server metadata and capabilities.
354354+355355+ This information is useful for feature detection and debugging. *)
356356+357357+ val create :
358358+ version:string ->
359359+ capabilities:string list ->
360360+ commands:string list ->
361361+ output_styles:string list ->
362362+ ?unknown:Unknown.t ->
363363+ unit ->
364364+ t
365365+ (** [create ~version ~capabilities ~commands ~output_styles ?unknown ()]
366366+ creates server info. *)
367367+368368+ val version : t -> string
369369+ (** [version t] returns the server version. *)
370370+371371+ val capabilities : t -> string list
372372+ (** [capabilities t] returns the server capabilities. *)
373373+374374+ val commands : t -> string list
375375+ (** [commands t] returns available commands. *)
376376+377377+ val output_styles : t -> string list
378378+ (** [output_styles t] returns available output styles. *)
379379+380380+ val unknown : t -> Unknown.t
381381+ (** [unknown t] returns the unknown fields. *)
382382+383383+ val json : t Json.codec
384384+ (** [json] is the json codec for server info. Use [Json.pp_value json] for
385385+ pretty-printing. *)
386386+end
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Error handling for the claude library. *)
77+88+type t =
99+ | Cli_not_found of string
1010+ | Process_error of string
1111+ | Connection_error of string
1212+ | Protocol_error of string
1313+ | Timeout of string
1414+ | Permission_denied of { tool_name : string; message : string }
1515+ | Hook_error of { callback_id : string; message : string }
1616+ | Control_error of { request_id : string; message : string }
1717+1818+exception E of t
1919+2020+val pp : Format.formatter -> t -> unit
2121+(** Pretty-print an error. *)
2222+2323+val to_string : t -> string
2424+(** Convert error to string. *)
2525+2626+val raise : t -> 'a
2727+(** [raise err] raises [E err]. *)
2828+2929+(** {1 Convenience Raisers} *)
3030+3131+val cli_not_found : string -> 'a
3232+3333+val process_error : string -> 'a
3434+(** Raise a process error. *)
3535+3636+val connection_error : string -> 'a
3737+(** Raise a connection error. *)
3838+3939+val protocol_error : string -> 'a
4040+(** Raise a protocol error. *)
4141+4242+val timeout : string -> 'a
4343+(** Raise a timeout error. *)
4444+4545+val permission_denied : tool_name:string -> message:string -> 'a
4646+(** Raise a permission denied error. *)
4747+4848+val hook_error : callback_id:string -> message:string -> 'a
4949+(** Raise a hook error. *)
5050+5151+val control_error : request_id:string -> message:string -> 'a
5252+(** Raise a control error. *)
5353+5454+(** {1 Result Helpers} *)
5555+5656+val ok : msg:string -> ('a, string) result -> 'a
5757+(** [ok ~msg result] returns the Ok value or raises Protocol_error with msg
5858+ prefix. *)
5959+6060+val ok' : msg:string -> ('a, string) result -> 'a
6161+(** [ok' ~msg result] returns the Ok value or raises Protocol_error with string
6262+ error. *)
+22-24
lib/handler.mli
···1616 methods you care about:
17171818 {[
1919- let my_handler =
2020- object
2121- inherit Claude.Handler.default
2222- method! on_text t = print_endline (Response.Text.content t)
1919+ let my_handler =
2020+ object
2121+ inherit Claude.Handler.default
2222+ method! on_text t = print_endline (Response.Text.content t)
23232424- method! on_complete c =
2525- Printf.printf "Done! Cost: $%.4f\n"
2626- (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
2727- end
2424+ method! on_complete c =
2525+ Printf.printf "Done! Cost: $%.4f\n"
2626+ (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
2727+ end
2828 ]}
29293030 For compile-time guarantees that all events are handled, inherit from
···8787 methods you need:
88888989 {[
9090- let handler =
9191- object
9292- inherit Claude.Handler.default
9393-9494- method! on_text t =
9595- Printf.printf "Text: %s\n" (Response.Text.content t)
9696- end
9090+ let handler =
9191+ object
9292+ inherit Claude.Handler.default
9393+ method! on_text t = Printf.printf "Text: %s\n" (Response.Text.content t)
9494+ end
9795 ]}
98969997 Methods you don't override will simply be ignored, making this ideal for
···149147150148 Example:
151149 {[
152152- let handler =
153153- object
154154- inherit Claude.Handler.default
155155- method! on_text t = print_endline (Response.Text.content t)
156156- end
157157- in
158158- dispatch handler (Response.Text text_event)
150150+ let handler =
151151+ object
152152+ inherit Claude.Handler.default
153153+ method! on_text t = print_endline (Response.Text.content t)
154154+ end
155155+ in
156156+ dispatch handler (Response.Text text_event)
159157 ]} *)
160158161159val dispatch_all : #handler -> Response.t list -> unit
···166164 may be more convenient:
167165168166 {[
169169- let responses = Client.receive_all client in
170170- dispatch_all handler responses
167167+ let responses = Client.receive_all client in
168168+ dispatch_all handler responses
171169 ]} *)
+325-337
lib/hooks.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566+open Json.Codec
77+68let src = Logs.Src.create "claude.hooks" ~doc:"Claude hooks system"
79810module Log = (val Logs.src_log src : Logs.LOG)
9111010-(** {1 PreToolUse Hook} *)
1212+(** {1 Hook Events} *)
1313+1414+type event =
1515+ | Pre_tool_use
1616+ | Post_tool_use
1717+ | User_prompt_submit
1818+ | Stop
1919+ | Subagent_stop
2020+ | Pre_compact
2121+2222+let event_to_string = function
2323+ | Pre_tool_use -> "PreToolUse"
2424+ | Post_tool_use -> "PostToolUse"
2525+ | User_prompt_submit -> "UserPromptSubmit"
2626+ | Stop -> "Stop"
2727+ | Subagent_stop -> "SubagentStop"
2828+ | Pre_compact -> "PreCompact"
2929+3030+let event_of_string = function
3131+ | "PreToolUse" -> Pre_tool_use
3232+ | "PostToolUse" -> Post_tool_use
3333+ | "UserPromptSubmit" -> User_prompt_submit
3434+ | "Stop" -> Stop
3535+ | "SubagentStop" -> Subagent_stop
3636+ | "PreCompact" -> Pre_compact
3737+ | s -> raise (Invalid_argument (Fmt.str "Unknown hook event: %s" s))
3838+3939+let event_jsont : event Json.codec =
4040+ enum
4141+ [
4242+ ("PreToolUse", Pre_tool_use);
4343+ ("PostToolUse", Post_tool_use);
4444+ ("UserPromptSubmit", User_prompt_submit);
4545+ ("Stop", Stop);
4646+ ("SubagentStop", Subagent_stop);
4747+ ("PreCompact", Pre_compact);
4848+ ]
4949+5050+(** {1 Decision} *)
5151+5252+type decision = Continue | Block
5353+5454+let decision_jsont : decision Json.codec =
5555+ enum [ ("continue", Continue); ("block", Block) ]
5656+5757+(** {1 Pre_tool_use Hook} *)
11581212-module PreToolUse = struct
5959+module Pre_tool_use = struct
1360 type input = {
1461 session_id : string;
1562 transcript_path : string;
···1764 tool_input : Tool_input.t;
1865 }
19666767+ let input_jsont : input Json.codec =
6868+ let make session_id transcript_path tool_name tool_input _unknown =
6969+ {
7070+ session_id;
7171+ transcript_path;
7272+ tool_name;
7373+ tool_input = Tool_input.of_json tool_input;
7474+ }
7575+ in
7676+ Object.map ~kind:"PreToolUseInput" make
7777+ |> Object.member "session_id" string ~enc:(fun i -> i.session_id)
7878+ |> Object.member "transcript_path" string ~enc:(fun i -> i.transcript_path)
7979+ |> Object.member "tool_name" string ~enc:(fun i -> i.tool_name)
8080+ |> Object.member "tool_input" Value.t ~enc:(fun i ->
8181+ Tool_input.to_json i.tool_input)
8282+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
8383+ |> Object.seal
8484+2085 type decision = Allow | Deny | Ask
8686+8787+ let decision_jsont : decision Json.codec =
8888+ enum [ ("allow", Allow); ("deny", Deny); ("ask", Ask) ]
21892290 type output = {
2391 decision : decision option;
···32100 let ask ?reason () = { decision = Some Ask; reason; updated_input = None }
33101 let continue () = { decision = None; reason = None; updated_input = None }
341023535- type callback = input -> output
3636-3737- let input_of_proto proto =
3838- {
3939- session_id = Proto.Hooks.PreToolUse.Input.session_id proto;
4040- transcript_path = Proto.Hooks.PreToolUse.Input.transcript_path proto;
4141- tool_name = Proto.Hooks.PreToolUse.Input.tool_name proto;
4242- tool_input =
4343- Tool_input.of_json (Proto.Hooks.PreToolUse.Input.tool_input proto);
4444- }
103103+ let output_jsont : output Json.codec =
104104+ let make _hook_event_name decision reason updated_input _unknown =
105105+ {
106106+ decision;
107107+ reason;
108108+ updated_input = Option.map Tool_input.of_json updated_input;
109109+ }
110110+ in
111111+ Object.map ~kind:"PreToolUseOutput" make
112112+ |> Object.member "hookEventName" string ~enc:(fun _ -> "PreToolUse")
113113+ |> Object.opt_member "permissionDecision" decision_jsont ~enc:(fun o ->
114114+ o.decision)
115115+ |> Object.opt_member "permissionDecisionReason" string ~enc:(fun o ->
116116+ o.reason)
117117+ |> Object.opt_member "updatedInput" Value.t ~enc:(fun o ->
118118+ Option.map Tool_input.to_json o.updated_input)
119119+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
120120+ |> Object.seal
451214646- let output_to_proto output =
4747- match output.decision with
4848- | None -> Proto.Hooks.PreToolUse.Output.continue ()
4949- | Some Allow ->
5050- let updated_input =
5151- Option.map Tool_input.to_json output.updated_input
5252- in
5353- Proto.Hooks.PreToolUse.Output.allow ?reason:output.reason ?updated_input
5454- ()
5555- | Some Deny -> Proto.Hooks.PreToolUse.Output.deny ?reason:output.reason ()
5656- | Some Ask -> Proto.Hooks.PreToolUse.Output.ask ?reason:output.reason ()
122122+ type callback = input -> output
57123end
581245959-(** {1 PostToolUse Hook} *)
125125+(** {1 Post_tool_use Hook} *)
601266161-module PostToolUse = struct
127127+module Post_tool_use = struct
62128 type input = {
63129 session_id : string;
64130 transcript_path : string;
65131 tool_name : string;
66132 tool_input : Tool_input.t;
6767- tool_response : Jsont.json;
133133+ tool_response : Json.t;
68134 }
69135136136+ let input_jsont : input Json.codec =
137137+ let make session_id transcript_path tool_name tool_input tool_response
138138+ _unknown =
139139+ {
140140+ session_id;
141141+ transcript_path;
142142+ tool_name;
143143+ tool_input = Tool_input.of_json tool_input;
144144+ tool_response;
145145+ }
146146+ in
147147+ Object.map ~kind:"PostToolUseInput" make
148148+ |> Object.member "session_id" string ~enc:(fun i -> i.session_id)
149149+ |> Object.member "transcript_path" string ~enc:(fun i -> i.transcript_path)
150150+ |> Object.member "tool_name" string ~enc:(fun i -> i.tool_name)
151151+ |> Object.member "tool_input" Value.t ~enc:(fun i ->
152152+ Tool_input.to_json i.tool_input)
153153+ |> Object.member "tool_response" Value.t ~enc:(fun i -> i.tool_response)
154154+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
155155+ |> Object.seal
156156+70157 type output = {
71158 block : bool;
72159 reason : string option;
···79166 let block ?reason ?additional_context () =
80167 { block = true; reason; additional_context }
811688282- type callback = input -> output
169169+ let output_jsont : output Json.codec =
170170+ let make _hook_event_name decision reason additional_context _unknown =
171171+ {
172172+ block = (match decision with Some Block -> true | _ -> false);
173173+ reason;
174174+ additional_context;
175175+ }
176176+ in
177177+ Object.map ~kind:"PostToolUseOutput" make
178178+ |> Object.member "hookEventName" string ~enc:(fun _ -> "PostToolUse")
179179+ |> Object.opt_member "decision" decision_jsont ~enc:(fun o ->
180180+ if o.block then Some Block else None)
181181+ |> Object.opt_member "reason" string ~enc:(fun o -> o.reason)
182182+ |> Object.opt_member "additionalContext" string ~enc:(fun o ->
183183+ o.additional_context)
184184+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
185185+ |> Object.seal
831868484- let input_of_proto proto =
8585- {
8686- session_id = Proto.Hooks.PostToolUse.Input.session_id proto;
8787- transcript_path = Proto.Hooks.PostToolUse.Input.transcript_path proto;
8888- tool_name = Proto.Hooks.PostToolUse.Input.tool_name proto;
8989- tool_input =
9090- Tool_input.of_json (Proto.Hooks.PostToolUse.Input.tool_input proto);
9191- tool_response = Proto.Hooks.PostToolUse.Input.tool_response proto;
9292- }
9393-9494- let output_to_proto output =
9595- if output.block then
9696- Proto.Hooks.PostToolUse.Output.block ?reason:output.reason
9797- ?additional_context:output.additional_context ()
9898- else
9999- Proto.Hooks.PostToolUse.Output.continue
100100- ?additional_context:output.additional_context ()
187187+ type callback = input -> output
101188end
102189103103-(** {1 UserPromptSubmit Hook} *)
190190+(** {1 User_prompt_submit Hook} *)
104191105105-module UserPromptSubmit = struct
192192+module User_prompt_submit = struct
106193 type input = {
107194 session_id : string;
108195 transcript_path : string;
109196 prompt : string;
110197 }
111198199199+ let input_jsont : input Json.codec =
200200+ let make session_id transcript_path prompt _unknown =
201201+ { session_id; transcript_path; prompt }
202202+ in
203203+ Object.map ~kind:"UserPromptSubmitInput" make
204204+ |> Object.member "session_id" string ~enc:(fun i -> i.session_id)
205205+ |> Object.member "transcript_path" string ~enc:(fun i -> i.transcript_path)
206206+ |> Object.member "prompt" string ~enc:(fun i -> i.prompt)
207207+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
208208+ |> Object.seal
209209+112210 type output = {
113211 block : bool;
114212 reason : string option;
···119217 { block = false; reason = None; additional_context }
120218121219 let block ?reason () = { block = true; reason; additional_context = None }
220220+221221+ let output_jsont : output Json.codec =
222222+ let make _hook_event_name decision reason additional_context _unknown =
223223+ {
224224+ block = (match decision with Some Block -> true | _ -> false);
225225+ reason;
226226+ additional_context;
227227+ }
228228+ in
229229+ Object.map ~kind:"UserPromptSubmitOutput" make
230230+ |> Object.member "hookEventName" string ~enc:(fun _ -> "UserPromptSubmit")
231231+ |> Object.opt_member "decision" decision_jsont ~enc:(fun o ->
232232+ if o.block then Some Block else None)
233233+ |> Object.opt_member "reason" string ~enc:(fun o -> o.reason)
234234+ |> Object.opt_member "additionalContext" string ~enc:(fun o ->
235235+ o.additional_context)
236236+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
237237+ |> Object.seal
122238123239 type callback = input -> output
124124-125125- let input_of_proto proto =
126126- {
127127- session_id = Proto.Hooks.UserPromptSubmit.Input.session_id proto;
128128- transcript_path = Proto.Hooks.UserPromptSubmit.Input.transcript_path proto;
129129- prompt = Proto.Hooks.UserPromptSubmit.Input.prompt proto;
130130- }
131131-132132- let output_to_proto output =
133133- if output.block then
134134- Proto.Hooks.UserPromptSubmit.Output.block ?reason:output.reason ()
135135- else
136136- Proto.Hooks.UserPromptSubmit.Output.continue
137137- ?additional_context:output.additional_context ()
138240end
139241140242(** {1 Stop Hook} *)
···146248 stop_hook_active : bool;
147249 }
148250251251+ let input_jsont : input Json.codec =
252252+ let make session_id transcript_path stop_hook_active _unknown =
253253+ { session_id; transcript_path; stop_hook_active }
254254+ in
255255+ Object.map ~kind:"StopInput" make
256256+ |> Object.member "session_id" string ~enc:(fun i -> i.session_id)
257257+ |> Object.member "transcript_path" string ~enc:(fun i -> i.transcript_path)
258258+ |> Object.member "stop_hook_active" bool ~enc:(fun i -> i.stop_hook_active)
259259+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
260260+ |> Object.seal
261261+149262 type output = { block : bool; reason : string option }
150263151264 let continue () = { block = false; reason = None }
152265 let block ?reason () = { block = true; reason }
153266154154- type callback = input -> output
267267+ let output_jsont_with_event_name event_name : output Json.codec =
268268+ let make _hook_event_name decision reason _unknown =
269269+ {
270270+ block = (match decision with Some Block -> true | _ -> false);
271271+ reason;
272272+ }
273273+ in
274274+ Object.map ~kind:(event_name ^ "Output") make
275275+ |> Object.member "hookEventName" string ~enc:(fun _ -> event_name)
276276+ |> Object.opt_member "decision" decision_jsont ~enc:(fun o ->
277277+ if o.block then Some Block else None)
278278+ |> Object.opt_member "reason" string ~enc:(fun o -> o.reason)
279279+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
280280+ |> Object.seal
155281156156- let input_of_proto proto =
157157- {
158158- session_id = Proto.Hooks.Stop.Input.session_id proto;
159159- transcript_path = Proto.Hooks.Stop.Input.transcript_path proto;
160160- stop_hook_active = Proto.Hooks.Stop.Input.stop_hook_active proto;
161161- }
282282+ let output_jsont = output_jsont_with_event_name "Stop"
162283163163- let output_to_proto output =
164164- if output.block then Proto.Hooks.Stop.Output.block ?reason:output.reason ()
165165- else Proto.Hooks.Stop.Output.continue ()
284284+ type callback = input -> output
166285end
167286168168-(** {1 SubagentStop Hook} *)
287287+(** {1 Subagent_stop Hook} *)
169288170170-module SubagentStop = struct
289289+module Subagent_stop = struct
171290 type input = Stop.input
172291 type output = Stop.output
173292174293 let continue = Stop.continue
175294 let block = Stop.block
295295+ let input_jsont = Stop.input_jsont
296296+ let output_jsont = Stop.output_jsont_with_event_name "SubagentStop"
176297177298 type callback = input -> output
178178-179179- let input_of_proto = Stop.input_of_proto
180180-181181- (* Since Proto.Hooks.SubagentStop.Output.t = Proto.Hooks.Stop.Output.t,
182182- we can use Stop.output_to_proto directly *)
183183- let output_to_proto = Stop.output_to_proto
184299end
185300186186-(** {1 PreCompact Hook} *)
301301+(** {1 Pre_compact Hook} *)
187302188188-module PreCompact = struct
303303+module Pre_compact = struct
189304 type input = { session_id : string; transcript_path : string }
190190- type callback = input -> unit
191305192192- let input_of_proto proto =
193193- {
194194- session_id = Proto.Hooks.PreCompact.Input.session_id proto;
195195- transcript_path = Proto.Hooks.PreCompact.Input.transcript_path proto;
196196- }
306306+ let input_jsont : input Json.codec =
307307+ let make session_id transcript_path _unknown =
308308+ { session_id; transcript_path }
309309+ in
310310+ Object.map ~kind:"PreCompactInput" make
311311+ |> Object.member "session_id" string ~enc:(fun i -> i.session_id)
312312+ |> Object.member "transcript_path" string ~enc:(fun i -> i.transcript_path)
313313+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
314314+ |> Object.seal
315315+316316+ type callback = input -> unit
197317end
198318319319+(** {1 Generic Hook Result} *)
320320+321321+type result = {
322322+ decision : decision option;
323323+ system_message : string option;
324324+ hook_specific_output : Json.t option;
325325+}
326326+327327+let result_jsont : result Json.codec =
328328+ let make decision system_message hook_specific_output _unknown =
329329+ { decision; system_message; hook_specific_output }
330330+ in
331331+ Object.map ~kind:"Result" make
332332+ |> Object.opt_member "decision" decision_jsont ~enc:(fun r -> r.decision)
333333+ |> Object.opt_member "systemMessage" string ~enc:(fun r -> r.system_message)
334334+ |> Object.opt_member "hookSpecificOutput" Value.t ~enc:(fun r ->
335335+ r.hook_specific_output)
336336+ |> Object.keep_unknown Unknown.mems ~enc:(fun _ -> Unknown.empty)
337337+ |> Object.seal
338338+339339+let continue_result ?system_message ?hook_specific_output () =
340340+ { decision = None; system_message; hook_specific_output }
341341+342342+let block_result ?system_message ?hook_specific_output () =
343343+ { decision = Some Block; system_message; hook_specific_output }
344344+199345(** {1 Hook Configuration} *)
200346201201-(* Internal representation of hooks *)
202347type hook_entry =
203203- | PreToolUseHook of (string option * PreToolUse.callback)
204204- | PostToolUseHook of (string option * PostToolUse.callback)
205205- | UserPromptSubmitHook of UserPromptSubmit.callback
206206- | StopHook of Stop.callback
207207- | SubagentStopHook of SubagentStop.callback
208208- | PreCompactHook of PreCompact.callback
348348+ | Pre_tool_use_hook of (string option * Pre_tool_use.callback)
349349+ | Post_tool_use_hook of (string option * Post_tool_use.callback)
350350+ | User_prompt_submit_hook of User_prompt_submit.callback
351351+ | Stop_hook of Stop.callback
352352+ | Subagent_stop_hook of Subagent_stop.callback
353353+ | Pre_compact_hook of Pre_compact.callback
209354210355type t = hook_entry list
211356357357+let pp ppf t = Fmt.pf ppf "<hooks:%d>" (List.length t)
212358let empty = []
213359214360let on_pre_tool_use ?pattern callback config =
215215- PreToolUseHook (pattern, callback) :: config
361361+ Pre_tool_use_hook (pattern, callback) :: config
216362217363let on_post_tool_use ?pattern callback config =
218218- PostToolUseHook (pattern, callback) :: config
364364+ Post_tool_use_hook (pattern, callback) :: config
219365220366let on_user_prompt_submit callback config =
221221- UserPromptSubmitHook callback :: config
367367+ User_prompt_submit_hook callback :: config
222368223223-let on_stop callback config = StopHook callback :: config
224224-let on_subagent_stop callback config = SubagentStopHook callback :: config
225225-let on_pre_compact callback config = PreCompactHook callback :: config
369369+let on_stop callback config = Stop_hook callback :: config
370370+let on_subagent_stop callback config = Subagent_stop_hook callback :: config
371371+let on_pre_compact callback config = Pre_compact_hook callback :: config
226372227373(** {1 Internal - Conversion to Wire Format} *)
228374229229-let get_callbacks config =
230230- (* Group hooks by event type *)
231231- let pre_tool_use_hooks = ref [] in
232232- let post_tool_use_hooks = ref [] in
233233- let user_prompt_submit_hooks = ref [] in
234234- let stop_hooks = ref [] in
235235- let subagent_stop_hooks = ref [] in
236236- let pre_compact_hooks = ref [] in
375375+let decode_input name codec v =
376376+ match Json.decode codec v with
377377+ | Ok input -> input
378378+ | Error err ->
379379+ let msg = Json.Error.to_string err in
380380+ Log.err (fun m -> m "%s: failed to decode input: %s" name msg);
381381+ raise (Invalid_argument (name ^ " input: " ^ msg))
237382383383+let encode_output _name codec output = Json.encode codec output
384384+385385+let wire_callback ~name ~input_jsont ~output_jsont ~should_block callback json =
386386+ let typed_input = decode_input name input_jsont json in
387387+ let typed_output = callback typed_input in
388388+ let hook_specific_output = encode_output name output_jsont typed_output in
389389+ if should_block typed_output then block_result ~hook_specific_output ()
390390+ else continue_result ~hook_specific_output ()
391391+392392+let group_hooks config =
393393+ let pre_tool_use = ref [] in
394394+ let post_tool_use = ref [] in
395395+ let user_prompt_submit = ref [] in
396396+ let stop = ref [] in
397397+ let subagent_stop = ref [] in
398398+ let pre_compact = ref [] in
238399 List.iter
239400 (function
240240- | PreToolUseHook (pattern, callback) ->
241241- pre_tool_use_hooks := (pattern, callback) :: !pre_tool_use_hooks
242242- | PostToolUseHook (pattern, callback) ->
243243- post_tool_use_hooks := (pattern, callback) :: !post_tool_use_hooks
244244- | UserPromptSubmitHook callback ->
245245- user_prompt_submit_hooks :=
246246- (None, callback) :: !user_prompt_submit_hooks
247247- | StopHook callback -> stop_hooks := (None, callback) :: !stop_hooks
248248- | SubagentStopHook callback ->
249249- subagent_stop_hooks := (None, callback) :: !subagent_stop_hooks
250250- | PreCompactHook callback ->
251251- pre_compact_hooks := (None, callback) :: !pre_compact_hooks)
401401+ | Pre_tool_use_hook (pattern, callback) ->
402402+ pre_tool_use := (pattern, callback) :: !pre_tool_use
403403+ | Post_tool_use_hook (pattern, callback) ->
404404+ post_tool_use := (pattern, callback) :: !post_tool_use
405405+ | User_prompt_submit_hook callback ->
406406+ user_prompt_submit := (None, callback) :: !user_prompt_submit
407407+ | Stop_hook callback -> stop := (None, callback) :: !stop
408408+ | Subagent_stop_hook callback ->
409409+ subagent_stop := (None, callback) :: !subagent_stop
410410+ | Pre_compact_hook callback ->
411411+ pre_compact := (None, callback) :: !pre_compact)
252412 config;
253253-254254- (* Convert each group to wire format *)
255255- let result = [] in
413413+ ( !pre_tool_use,
414414+ !post_tool_use,
415415+ !user_prompt_submit,
416416+ !stop,
417417+ !subagent_stop,
418418+ !pre_compact )
256419257257- (* PreToolUse *)
258258- let result =
259259- if !pre_tool_use_hooks <> [] then
420420+let add_standard_event event hooks ~name ~input_jsont ~output_jsont
421421+ ~should_block result =
422422+ match hooks with
423423+ | [] -> result
424424+ | _ ->
260425 let wire_callbacks =
261426 List.map
262427 (fun (pattern, callback) ->
263263- let wire_callback json =
264264- (* Decode JSON to Proto input *)
265265- let proto_input =
266266- match
267267- Jsont.Json.decode Proto.Hooks.PreToolUse.Input.jsont json
268268- with
269269- | Ok input -> input
270270- | Error msg ->
271271- Log.err (fun m ->
272272- m "PreToolUse: failed to decode input: %s" msg);
273273- raise (Invalid_argument ("PreToolUse input: " ^ msg))
274274- in
275275- (* Convert to typed input *)
276276- let typed_input = PreToolUse.input_of_proto proto_input in
277277- (* Invoke user callback *)
278278- let typed_output = callback typed_input in
279279- (* Convert back to Proto output *)
280280- let proto_output = PreToolUse.output_to_proto typed_output in
281281- (* Encode as hook_specific_output *)
282282- let hook_specific_output =
283283- match
284284- Jsont.Json.encode Proto.Hooks.PreToolUse.Output.jsont
285285- proto_output
286286- with
287287- | Ok json -> json
288288- | Error msg -> failwith ("PreToolUse output encoding: " ^ msg)
289289- in
290290- (* Return wire format result *)
291291- Proto.Hooks.continue ~hook_specific_output ()
292292- in
293293- (pattern, wire_callback))
294294- !pre_tool_use_hooks
428428+ ( pattern,
429429+ wire_callback ~name ~input_jsont ~output_jsont ~should_block
430430+ callback ))
431431+ hooks
295432 in
296296- (Proto.Hooks.Pre_tool_use, wire_callbacks) :: result
297297- else result
298298- in
433433+ (event, wire_callbacks) :: result
299434300300- (* PostToolUse *)
301301- let result =
302302- if !post_tool_use_hooks <> [] then
435435+let add_pre_compact_event hooks result =
436436+ match hooks with
437437+ | [] -> result
438438+ | _ ->
303439 let wire_callbacks =
304440 List.map
305441 (fun (pattern, callback) ->
306442 let wire_callback json =
307307- let proto_input =
308308- match
309309- Jsont.Json.decode Proto.Hooks.PostToolUse.Input.jsont json
310310- with
311311- | Ok input -> input
312312- | Error msg ->
313313- Log.err (fun m ->
314314- m "PostToolUse: failed to decode input: %s" msg);
315315- raise (Invalid_argument ("PostToolUse input: " ^ msg))
443443+ let typed_input =
444444+ decode_input "PreCompact" Pre_compact.input_jsont json
316445 in
317317- let typed_input = PostToolUse.input_of_proto proto_input in
318318- let typed_output = callback typed_input in
319319- let proto_output = PostToolUse.output_to_proto typed_output in
320320- let hook_specific_output =
321321- match
322322- Jsont.Json.encode Proto.Hooks.PostToolUse.Output.jsont
323323- proto_output
324324- with
325325- | Ok json -> json
326326- | Error msg -> failwith ("PostToolUse output encoding: " ^ msg)
327327- in
328328- if typed_output.block then
329329- Proto.Hooks.block ~hook_specific_output ()
330330- else Proto.Hooks.continue ~hook_specific_output ()
331331- in
332332- (pattern, wire_callback))
333333- !post_tool_use_hooks
334334- in
335335- (Proto.Hooks.Post_tool_use, wire_callbacks) :: result
336336- else result
337337- in
338338-339339- (* UserPromptSubmit *)
340340- let result =
341341- if !user_prompt_submit_hooks <> [] then
342342- let wire_callbacks =
343343- List.map
344344- (fun (pattern, callback) ->
345345- let wire_callback json =
346346- let proto_input =
347347- match
348348- Jsont.Json.decode Proto.Hooks.UserPromptSubmit.Input.jsont
349349- json
350350- with
351351- | Ok input -> input
352352- | Error msg ->
353353- Log.err (fun m ->
354354- m "UserPromptSubmit: failed to decode input: %s" msg);
355355- raise (Invalid_argument ("UserPromptSubmit input: " ^ msg))
356356- in
357357- let typed_input = UserPromptSubmit.input_of_proto proto_input in
358358- let typed_output = callback typed_input in
359359- let proto_output =
360360- UserPromptSubmit.output_to_proto typed_output
361361- in
362362- let hook_specific_output =
363363- match
364364- Jsont.Json.encode Proto.Hooks.UserPromptSubmit.Output.jsont
365365- proto_output
366366- with
367367- | Ok json -> json
368368- | Error msg ->
369369- failwith ("UserPromptSubmit output encoding: " ^ msg)
370370- in
371371- if typed_output.block then
372372- Proto.Hooks.block ~hook_specific_output ()
373373- else Proto.Hooks.continue ~hook_specific_output ()
374374- in
375375- (pattern, wire_callback))
376376- !user_prompt_submit_hooks
377377- in
378378- (Proto.Hooks.User_prompt_submit, wire_callbacks) :: result
379379- else result
380380- in
381381-382382- (* Stop *)
383383- let result =
384384- if !stop_hooks <> [] then
385385- let wire_callbacks =
386386- List.map
387387- (fun (pattern, callback) ->
388388- let wire_callback json =
389389- let proto_input =
390390- match Jsont.Json.decode Proto.Hooks.Stop.Input.jsont json with
391391- | Ok input -> input
392392- | Error msg ->
393393- Log.err (fun m -> m "Stop: failed to decode input: %s" msg);
394394- raise (Invalid_argument ("Stop input: " ^ msg))
395395- in
396396- let typed_input = Stop.input_of_proto proto_input in
397397- let typed_output = callback typed_input in
398398- let proto_output = Stop.output_to_proto typed_output in
399399- let hook_specific_output =
400400- match
401401- Jsont.Json.encode Proto.Hooks.Stop.Output.jsont proto_output
402402- with
403403- | Ok json -> json
404404- | Error msg -> failwith ("Stop output encoding: " ^ msg)
405405- in
406406- if typed_output.block then
407407- Proto.Hooks.block ~hook_specific_output ()
408408- else Proto.Hooks.continue ~hook_specific_output ()
409409- in
410410- (pattern, wire_callback))
411411- !stop_hooks
412412- in
413413- (Proto.Hooks.Stop, wire_callbacks) :: result
414414- else result
415415- in
416416-417417- (* SubagentStop *)
418418- let result =
419419- if !subagent_stop_hooks <> [] then
420420- let wire_callbacks =
421421- List.map
422422- (fun (pattern, callback) ->
423423- let wire_callback json =
424424- let proto_input =
425425- match
426426- Jsont.Json.decode Proto.Hooks.SubagentStop.Input.jsont json
427427- with
428428- | Ok input -> input
429429- | Error msg ->
430430- Log.err (fun m ->
431431- m "SubagentStop: failed to decode input: %s" msg);
432432- raise (Invalid_argument ("SubagentStop input: " ^ msg))
433433- in
434434- let typed_input = SubagentStop.input_of_proto proto_input in
435435- let typed_output = callback typed_input in
436436- let proto_output = SubagentStop.output_to_proto typed_output in
437437- let hook_specific_output =
438438- match
439439- Jsont.Json.encode Proto.Hooks.SubagentStop.Output.jsont
440440- proto_output
441441- with
442442- | Ok json -> json
443443- | Error msg -> failwith ("SubagentStop output encoding: " ^ msg)
444444- in
445445- if typed_output.block then
446446- Proto.Hooks.block ~hook_specific_output ()
447447- else Proto.Hooks.continue ~hook_specific_output ()
448448- in
449449- (pattern, wire_callback))
450450- !subagent_stop_hooks
451451- in
452452- (Proto.Hooks.Subagent_stop, wire_callbacks) :: result
453453- else result
454454- in
455455-456456- (* PreCompact *)
457457- let result =
458458- if !pre_compact_hooks <> [] then
459459- let wire_callbacks =
460460- List.map
461461- (fun (pattern, callback) ->
462462- let wire_callback json =
463463- let proto_input =
464464- match
465465- Jsont.Json.decode Proto.Hooks.PreCompact.Input.jsont json
466466- with
467467- | Ok input -> input
468468- | Error msg ->
469469- Log.err (fun m ->
470470- m "PreCompact: failed to decode input: %s" msg);
471471- raise (Invalid_argument ("PreCompact input: " ^ msg))
472472- in
473473- let typed_input = PreCompact.input_of_proto proto_input in
474474- (* Invoke user callback (returns unit) *)
475446 callback typed_input;
476476- (* PreCompact has no specific output *)
477477- Proto.Hooks.continue ()
447447+ continue_result ()
478448 in
479449 (pattern, wire_callback))
480480- !pre_compact_hooks
450450+ hooks
481451 in
482482- (Proto.Hooks.Pre_compact, wire_callbacks) :: result
483483- else result
484484- in
452452+ (Pre_compact, wire_callbacks) :: result
485453486486- List.rev result
454454+let callbacks config =
455455+ let ptu, potu, ups, st, sas, pc = group_hooks config in
456456+ []
457457+ |> add_standard_event Pre_tool_use ptu ~name:"PreToolUse"
458458+ ~input_jsont:Pre_tool_use.input_jsont
459459+ ~output_jsont:Pre_tool_use.output_jsont ~should_block:(fun _ -> false)
460460+ |> add_standard_event Post_tool_use potu ~name:"PostToolUse"
461461+ ~input_jsont:Post_tool_use.input_jsont
462462+ ~output_jsont:Post_tool_use.output_jsont ~should_block:(fun o ->
463463+ o.Post_tool_use.block)
464464+ |> add_standard_event User_prompt_submit ups ~name:"UserPromptSubmit"
465465+ ~input_jsont:User_prompt_submit.input_jsont
466466+ ~output_jsont:User_prompt_submit.output_jsont ~should_block:(fun o ->
467467+ o.User_prompt_submit.block)
468468+ |> add_standard_event Stop st ~name:"Stop" ~input_jsont:Stop.input_jsont
469469+ ~output_jsont:Stop.output_jsont ~should_block:(fun o -> o.Stop.block)
470470+ |> add_standard_event Subagent_stop sas ~name:"SubagentStop"
471471+ ~input_jsont:Subagent_stop.input_jsont
472472+ ~output_jsont:Subagent_stop.output_jsont ~should_block:(fun o ->
473473+ o.Stop.block)
474474+ |> add_pre_compact_event pc |> List.rev
+86-208
lib/hooks.mli
···88 Hooks allow you to intercept and control events in Claude Code sessions,
99 using fully typed OCaml values instead of raw JSON.
10101111- {1 Overview}
1212-1313- This module provides a high-level, type-safe interface to hooks. Each hook
1414- type has:
1515- - Fully typed input records using {!Tool_input.t}
1616- - Fully typed output records
1717- - Helper functions for common responses
1818- - Conversion functions to/from wire format ({!Proto.Hooks})
1919-2011 {1 Example Usage}
21122213 {[
···24152516 (* Block dangerous bash commands *)
2617 let block_rm_rf input =
2727- if input.Hooks.PreToolUse.tool_name = "Bash" then
2828- match Tool_input.get_string input.tool_input "command" with
1818+ if input.Hooks.Pre_tool_use.tool_name = "Bash" then
1919+ match Tool_input.string input.tool_input "command" with
2920 | Some cmd when String.contains cmd "rm -rf" ->
3030- Hooks.PreToolUse.deny ~reason:"Dangerous command" ()
3131- | _ -> Hooks.PreToolUse.continue ()
3232- else Hooks.PreToolUse.continue ()
2121+ Hooks.Pre_tool_use.deny ~reason:"Dangerous command" ()
2222+ | _ -> Hooks.Pre_tool_use.continue ()
2323+ else Hooks.Pre_tool_use.continue ()
33243425 let hooks =
3526 Hooks.empty
3627 |> Hooks.on_pre_tool_use ~pattern:"Bash" block_rm_rf
37283829 let options = Claude.Options.create ~hooks () in
3939- let client = Claude.Client.create ~options ~sw ~process_mgr () in
3030+ let client = Claude.Client.v ~options ~sw ~process_mgr () in
4031 ]} *)
41324233val src : Logs.Src.t
4343-(** The log source for hooks *)
3434+(** The log source for hooks. *)
44354545-(** {1 Hook Types} *)
3636+(** {1 Hook Events} *)
46374747-(** PreToolUse hook - fires before tool execution *)
4848-module PreToolUse : sig
4949- (** {2 Input} *)
3838+type event =
3939+ | Pre_tool_use
4040+ | Post_tool_use
4141+ | User_prompt_submit
4242+ | Stop
4343+ | Subagent_stop
4444+ | Pre_compact
50454646+val event_to_string : event -> string
4747+(** Wire format strings: "PreToolUse", "PostToolUse", "UserPromptSubmit",
4848+ "Stop", "SubagentStop", "PreCompact". *)
4949+5050+val event_of_string : string -> event
5151+(** @raise Invalid_argument if the string is not a known event. *)
5252+5353+val event_jsont : event Json.codec
5454+5555+(** {1 Decision} *)
5656+5757+type decision = Continue | Block
5858+5959+val decision_jsont : decision Json.codec
6060+6161+(** {1 Hook Types} *)
6262+6363+(** Pre_tool_use hook - fires before tool execution. *)
6464+module Pre_tool_use : sig
5165 type input = {
5266 session_id : string;
5367 transcript_path : string;
5468 tool_name : string;
5569 tool_input : Tool_input.t;
5670 }
5757- (** Input provided to PreToolUse hooks. *)
7171+7272+ val input_jsont : input Json.codec
58735959- (** {2 Output} *)
7474+ type decision = Allow | Deny | Ask
60756161- type decision =
6262- | Allow
6363- | Deny
6464- | Ask (** Permission decision for tool usage. *)
7676+ val decision_jsont : decision Json.codec
65776678 type output = {
6779 decision : decision option;
6880 reason : string option;
6981 updated_input : Tool_input.t option;
7082 }
7171- (** Output from PreToolUse hooks. *)
72837373- (** {2 Response Builders} *)
7474-8484+ val output_jsont : output Json.codec
7585 val allow : ?reason:string -> ?updated_input:Tool_input.t -> unit -> output
7676- (** [allow ?reason ?updated_input ()] creates an allow response.
7777- @param reason Optional explanation for allowing
7878- @param updated_input Optional modified tool input *)
7979-8086 val deny : ?reason:string -> unit -> output
8181- (** [deny ?reason ()] creates a deny response.
8282- @param reason Optional explanation for denying *)
8383-8487 val ask : ?reason:string -> unit -> output
8585- (** [ask ?reason ()] creates an ask response to prompt the user.
8686- @param reason Optional explanation for asking *)
8787-8888 val continue : unit -> output
8989- (** [continue ()] creates a continue response with no decision. *)
9090-9191- (** {2 Callback Type} *)
92899390 type callback = input -> output
9494- (** Callback function type for PreToolUse hooks. *)
9595-9696- (** {2 Conversion Functions} *)
9797-9898- val input_of_proto : Proto.Hooks.PreToolUse.Input.t -> input
9999- (** [input_of_proto proto] converts wire format input to typed input. *)
100100-101101- val output_to_proto : output -> Proto.Hooks.PreToolUse.Output.t
102102- (** [output_to_proto output] converts typed output to wire format. *)
10391end
10492105105-(** PostToolUse hook - fires after tool execution *)
106106-module PostToolUse : sig
107107- (** {2 Input} *)
108108-9393+(** Post_tool_use hook - fires after tool execution. *)
9494+module Post_tool_use : sig
10995 type input = {
11096 session_id : string;
11197 transcript_path : string;
11298 tool_name : string;
11399 tool_input : Tool_input.t;
114114- tool_response : Jsont.json; (* Response varies by tool *)
100100+ tool_response : Json.t;
115101 }
116116- (** Input provided to PostToolUse hooks. Note: [tool_response] remains as
117117- {!type:Jsont.json} since response schemas vary by tool. *)
118102119119- (** {2 Output} *)
103103+ val input_jsont : input Json.codec
120104121105 type output = {
122106 block : bool;
123107 reason : string option;
124108 additional_context : string option;
125109 }
126126- (** Output from PostToolUse hooks. *)
127110128128- (** {2 Response Builders} *)
129129-111111+ val output_jsont : output Json.codec
130112 val continue : ?additional_context:string -> unit -> output
131131- (** [continue ?additional_context ()] creates a continue response.
132132- @param additional_context Optional context to add to the transcript *)
133133-134113 val block : ?reason:string -> ?additional_context:string -> unit -> output
135135- (** [block ?reason ?additional_context ()] creates a block response.
136136- @param reason Optional explanation for blocking
137137- @param additional_context Optional context to add to the transcript *)
138138-139139- (** {2 Callback Type} *)
140114141115 type callback = input -> output
142142- (** Callback function type for PostToolUse hooks. *)
143143-144144- (** {2 Conversion Functions} *)
145145-146146- val input_of_proto : Proto.Hooks.PostToolUse.Input.t -> input
147147- (** [input_of_proto proto] converts wire format input to typed input. *)
148148-149149- val output_to_proto : output -> Proto.Hooks.PostToolUse.Output.t
150150- (** [output_to_proto output] converts typed output to wire format. *)
151116end
152117153153-(** UserPromptSubmit hook - fires when user submits a prompt *)
154154-module UserPromptSubmit : sig
155155- (** {2 Input} *)
156156-118118+(** User_prompt_submit hook - fires when user submits a prompt. *)
119119+module User_prompt_submit : sig
157120 type input = {
158121 session_id : string;
159122 transcript_path : string;
160123 prompt : string;
161124 }
162162- (** Input provided to UserPromptSubmit hooks. *)
163125164164- (** {2 Output} *)
126126+ val input_jsont : input Json.codec
165127166128 type output = {
167129 block : bool;
168130 reason : string option;
169131 additional_context : string option;
170132 }
171171- (** Output from UserPromptSubmit hooks. *)
172133173173- (** {2 Response Builders} *)
174174-134134+ val output_jsont : output Json.codec
175135 val continue : ?additional_context:string -> unit -> output
176176- (** [continue ?additional_context ()] creates a continue response.
177177- @param additional_context Optional context to add to the transcript *)
178178-179136 val block : ?reason:string -> unit -> output
180180- (** [block ?reason ()] creates a block response.
181181- @param reason Optional explanation for blocking *)
182182-183183- (** {2 Callback Type} *)
184137185138 type callback = input -> output
186186- (** Callback function type for UserPromptSubmit hooks. *)
187187-188188- (** {2 Conversion Functions} *)
189189-190190- val input_of_proto : Proto.Hooks.UserPromptSubmit.Input.t -> input
191191- (** [input_of_proto proto] converts wire format input to typed input. *)
192192-193193- val output_to_proto : output -> Proto.Hooks.UserPromptSubmit.Output.t
194194- (** [output_to_proto output] converts typed output to wire format. *)
195139end
196140197197-(** Stop hook - fires when conversation stops *)
141141+(** Stop hook - fires when conversation stops. *)
198142module Stop : sig
199199- (** {2 Input} *)
200200-201143 type input = {
202144 session_id : string;
203145 transcript_path : string;
204146 stop_hook_active : bool;
205147 }
206206- (** Input provided to Stop hooks. *)
207148208208- (** {2 Output} *)
149149+ val input_jsont : input Json.codec
209150210151 type output = { block : bool; reason : string option }
211211- (** Output from Stop hooks. *)
212152213213- (** {2 Response Builders} *)
214214-153153+ val output_jsont : output Json.codec
215154 val continue : unit -> output
216216- (** [continue ()] creates a continue response. *)
217217-218155 val block : ?reason:string -> unit -> output
219219- (** [block ?reason ()] creates a block response.
220220- @param reason Optional explanation for blocking *)
221221-222222- (** {2 Callback Type} *)
223156224157 type callback = input -> output
225225- (** Callback function type for Stop hooks. *)
226226-227227- (** {2 Conversion Functions} *)
228228-229229- val input_of_proto : Proto.Hooks.Stop.Input.t -> input
230230- (** [input_of_proto proto] converts wire format input to typed input. *)
231231-232232- val output_to_proto : output -> Proto.Hooks.Stop.Output.t
233233- (** [output_to_proto output] converts typed output to wire format. *)
234158end
235159236236-(** SubagentStop hook - fires when a subagent stops *)
237237-module SubagentStop : sig
238238- (** {2 Input} *)
239239-160160+(** Subagent_stop hook - fires when a subagent stops. *)
161161+module Subagent_stop : sig
240162 type input = Stop.input
241241- (** Same structure as Stop.input *)
242242-243243- (** {2 Output} *)
244244-245163 type output = Stop.output
246246- (** Same structure as Stop.output *)
247164248248- (** {2 Response Builders} *)
249249-165165+ val input_jsont : input Json.codec
166166+ val output_jsont : output Json.codec
250167 val continue : unit -> output
251251- (** [continue ()] creates a continue response. *)
252252-253168 val block : ?reason:string -> unit -> output
254254- (** [block ?reason ()] creates a block response.
255255- @param reason Optional explanation for blocking *)
256256-257257- (** {2 Callback Type} *)
258169259170 type callback = input -> output
260260- (** Callback function type for SubagentStop hooks. *)
171171+end
261172262262- (** {2 Conversion Functions} *)
173173+(** Pre_compact hook - fires before message compaction. *)
174174+module Pre_compact : sig
175175+ type input = { session_id : string; transcript_path : string }
263176264264- val input_of_proto : Proto.Hooks.SubagentStop.Input.t -> input
265265- (** [input_of_proto proto] converts wire format input to typed input. *)
177177+ val input_jsont : input Json.codec
266178267267- val output_to_proto : output -> Proto.Hooks.SubagentStop.Output.t
268268- (** [output_to_proto output] converts typed output to wire format. *)
179179+ type callback = input -> unit
180180+ (** Pre_compact hooks have no output - they are notification-only. *)
269181end
270182271271-(** PreCompact hook - fires before message compaction *)
272272-module PreCompact : sig
273273- (** {2 Input} *)
183183+(** {1 Generic Hook Result} *)
274184275275- type input = { session_id : string; transcript_path : string }
276276- (** Input provided to PreCompact hooks. *)
185185+type result = {
186186+ decision : decision option;
187187+ system_message : string option;
188188+ hook_specific_output : Json.t option;
189189+}
277190278278- (** {2 Callback Type} *)
191191+val result_jsont : result Json.codec
279192280280- type callback = input -> unit
281281- (** Callback function type for PreCompact hooks. PreCompact hooks have no
282282- output - they are notification-only. *)
193193+val continue_result :
194194+ ?system_message:string -> ?hook_specific_output:Json.t -> unit -> result
283195284284- (** {2 Conversion Functions} *)
285285-286286- val input_of_proto : Proto.Hooks.PreCompact.Input.t -> input
287287- (** [input_of_proto proto] converts wire format input to typed input. *)
288288-end
196196+val block_result :
197197+ ?system_message:string -> ?hook_specific_output:Json.t -> unit -> result
289198290199(** {1 Hook Configuration} *)
291200···294203295204 Hooks are configured using a builder pattern:
296205 {[
297297- Hooks.empty
298298- |> Hooks.on_pre_tool_use ~pattern:"Bash" bash_handler
299299- |> Hooks.on_post_tool_use post_handler
206206+ Hooks.empty
207207+ |> Hooks.on_pre_tool_use ~pattern:"Bash" bash_handler
208208+ |> Hooks.on_post_tool_use post_handler
300209 ]} *)
301210211211+val pp : Format.formatter -> t -> unit
302212val empty : t
303303-(** [empty] is an empty hook configuration with no callbacks. *)
304304-305305-val on_pre_tool_use : ?pattern:string -> PreToolUse.callback -> t -> t
306306-(** [on_pre_tool_use ?pattern callback config] adds a PreToolUse hook.
307307- @param pattern
308308- Optional regex pattern to match tool names (e.g., "Bash|Edit")
309309- @param callback Function to invoke on matching events *)
310310-311311-val on_post_tool_use : ?pattern:string -> PostToolUse.callback -> t -> t
312312-(** [on_post_tool_use ?pattern callback config] adds a PostToolUse hook.
313313- @param pattern Optional regex pattern to match tool names
314314- @param callback Function to invoke on matching events *)
315315-316316-val on_user_prompt_submit : UserPromptSubmit.callback -> t -> t
317317-(** [on_user_prompt_submit callback config] adds a UserPromptSubmit hook.
318318- @param callback Function to invoke on prompt submission *)
319319-213213+val on_pre_tool_use : ?pattern:string -> Pre_tool_use.callback -> t -> t
214214+val on_post_tool_use : ?pattern:string -> Post_tool_use.callback -> t -> t
215215+val on_user_prompt_submit : User_prompt_submit.callback -> t -> t
320216val on_stop : Stop.callback -> t -> t
321321-(** [on_stop callback config] adds a Stop hook.
322322- @param callback Function to invoke on conversation stop *)
323323-324324-val on_subagent_stop : SubagentStop.callback -> t -> t
325325-(** [on_subagent_stop callback config] adds a SubagentStop hook.
326326- @param callback Function to invoke on subagent stop *)
327327-328328-val on_pre_compact : PreCompact.callback -> t -> t
329329-(** [on_pre_compact callback config] adds a PreCompact hook.
330330- @param callback Function to invoke before message compaction *)
217217+val on_subagent_stop : Subagent_stop.callback -> t -> t
218218+val on_pre_compact : Pre_compact.callback -> t -> t
331219332220(** {1 Internal - for client use} *)
333221334334-val get_callbacks :
335335- t ->
336336- (Proto.Hooks.event
337337- * (string option * (Jsont.json -> Proto.Hooks.result)) list)
338338- list
339339-(** [get_callbacks config] returns hook configuration in format suitable for
222222+val callbacks : t -> (event * (string option * (Json.t -> result)) list) list
223223+(** [callbacks config] returns hook configuration in format suitable for
340224 registration with the CLI.
341225342342- This function converts typed callbacks into wire format handlers that:
343343- - Parse JSON input using Proto.Hooks types
344344- - Convert to typed input using input_of_proto
345345- - Invoke the user's typed callback
346346- - Convert output back to wire format using output_to_proto
347347-348348- This is an internal function used by {!Client} - you should not need to call
349349- it directly. *)
226226+ Internal function used by {!Client}; you should not need to call it
227227+ directly. *)
+37-24
lib/incoming.ml
···10101111(** Incoming messages from Claude CLI.
12121313- This uses the Sdk_control module's control_request_jsont and
1414- control_response_jsont for control messages, and Message.jsont for
1515- conversation messages. The top-level discriminator is the "type" field. *)
1313+ The top-level discriminator is the "type" field. *)
16141715type t =
1816 | Message of Message.t
1919- | Control_response of Sdk_control.control_response
2020- | Control_request of Sdk_control.control_request
1717+ | Control_response of Control.control_response
1818+ | Control_request of Control.control_request
1919+ | Rate_limit_event
21202222-let jsont : t Jsont.t =
2121+let json : t Json.codec =
2222+ let open Json.Codec in
2323 (* Message types use "user", "assistant", "system", "result" as type values.
2424 Control uses "control_request" and "control_response".
25252626 We use case_mem for all types. Note: we use the inner message codecs
2727- (User.incoming_jsont, etc.) rather than Message.jsont to avoid nesting
2727+ (User.incoming_jsont, etc.) rather than Message.json to avoid nesting
2828 case_mem on the same "type" field. *)
2929 let case_control_request =
3030- Jsont.Object.Case.map "control_request" Sdk_control.control_request_jsont
3030+ Object.Case.map "control_request" Control.control_request_jsont
3131 ~dec:(fun v -> Control_request v)
3232 in
3333 let case_control_response =
3434- Jsont.Object.Case.map "control_response" Sdk_control.control_response_jsont
3434+ Object.Case.map "control_response" Control.control_response_jsont
3535 ~dec:(fun v -> Control_response v)
3636 in
3737 let case_user =
3838- Jsont.Object.Case.map "user" Message.User.incoming_jsont ~dec:(fun v ->
3838+ Object.Case.map "user" Message.User.incoming_jsont ~dec:(fun v ->
3939 Message (Message.User v))
4040 in
4141 let case_assistant =
4242- Jsont.Object.Case.map "assistant" Message.Assistant.incoming_jsont
4343- ~dec:(fun v -> Message (Message.Assistant v))
4242+ Object.Case.map "assistant" Message.Assistant.incoming_jsont ~dec:(fun v ->
4343+ Message (Message.Assistant v))
4444 in
4545 let case_system =
4646- Jsont.Object.Case.map "system" Message.System.jsont ~dec:(fun v ->
4646+ Object.Case.map "system" Message.System.json ~dec:(fun v ->
4747 Message (Message.System v))
4848 in
4949 let case_result =
5050- Jsont.Object.Case.map "result" Message.Result.jsont ~dec:(fun v ->
5050+ Object.Case.map "result" Message.Result.json ~dec:(fun v ->
5151 Message (Message.Result v))
5252 in
5353+ (* rate_limit_event: CLI sends these periodically with usage info.
5454+ We decode the type field and discard the rest. *)
5555+ let rate_limit_jsont =
5656+ Object.map ~kind:"RateLimit" () |> Object.skip_unknown |> Object.seal
5757+ in
5858+ let case_rate_limit =
5959+ Object.Case.map "rate_limit_event" rate_limit_jsont ~dec:(fun () ->
6060+ Rate_limit_event)
6161+ in
5362 let enc_case = function
5454- | Control_request v -> Jsont.Object.Case.value case_control_request v
5555- | Control_response v -> Jsont.Object.Case.value case_control_response v
6363+ | Control_request v -> Object.Case.value case_control_request v
6464+ | Control_response v -> Object.Case.value case_control_response v
6565+ | Rate_limit_event -> Object.Case.value case_rate_limit ()
5666 | Message msg -> (
5767 match msg with
5858- | Message.User u -> Jsont.Object.Case.value case_user u
5959- | Message.Assistant a -> Jsont.Object.Case.value case_assistant a
6060- | Message.System s -> Jsont.Object.Case.value case_system s
6161- | Message.Result r -> Jsont.Object.Case.value case_result r)
6868+ | Message.User u -> Object.Case.value case_user u
6969+ | Message.Assistant a -> Object.Case.value case_assistant a
7070+ | Message.System s -> Object.Case.value case_system s
7171+ | Message.Result r -> Object.Case.value case_result r)
6272 in
6373 let cases =
6464- Jsont.Object.Case.
7474+ Object.Case.
6575 [
6676 make case_control_request;
6777 make case_control_response;
···6979 make case_assistant;
7080 make case_system;
7181 make case_result;
8282+ make case_rate_limit;
7283 ]
7384 in
7474- Jsont.Object.map ~kind:"Incoming" Fun.id
7575- |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
8585+ Object.map ~kind:"Incoming" Fun.id
8686+ |> Object.case_member "type" string ~enc:Fun.id ~enc_case cases
7687 ~tag_to_string:Fun.id ~tag_compare:String.compare
7777- |> Jsont.Object.finish
8888+ |> Object.seal
8989+9090+let pp ppf t = Json.pp_value json ppf t
+9-5
lib/incoming.mli
···66(** Incoming messages from the Claude CLI.
7788 This module defines a discriminated union of all possible message types that
99- can be received from the Claude CLI, with a single jsont codec.
99+ can be received from the Claude CLI, with a single json codec.
10101111 The codec uses the "type" field to discriminate between message types:
1212 - "user", "assistant", "system", "result" -> Message variant
···18181919type t =
2020 | Message of Message.t
2121- | Control_response of Sdk_control.control_response
2222- | Control_request of Sdk_control.control_request
2121+ | Control_response of Control.control_response
2222+ | Control_request of Control.control_request
2323+ | Rate_limit_event (** Rate limit usage info from the CLI. *)
23242424-val jsont : t Jsont.t
2525+val json : t Json.codec
2526(** Codec for incoming messages. Uses the "type" field to discriminate. Use
2626- [Jsont.pp_value jsont ()] for pretty-printing. *)
2727+ [Json.pp_value json] for pretty-printing. *)
2828+2929+val pp : Format.formatter -> t -> unit
3030+(** [pp ppf t] pretty-prints the incoming message. *)
+89-57
lib/mcp_server.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-module J = Jsont.Json
77-86type t = {
97 name : string;
108 version : string;
···1210 tool_map : (string, Tool.t) Hashtbl.t;
1311}
14121515-let create ~name ?(version = "1.0.0") ~tools () =
1313+let v ~name ?(version = "1.0.0") ~tools () =
1614 let tool_map = Hashtbl.create (List.length tools) in
1715 List.iter (fun tool -> Hashtbl.add tool_map (Tool.name tool) tool) tools;
1816 { name; version; tools; tool_map }
···2119let version t = t.version
2220let tools t = t.tools
23212424-(* JSONRPC helpers using Jsont.Json builders *)
2222+(* JSONRPC helpers using Json.Json builders *)
25232624let jsonrpc_success ~id result =
2727- J.object'
2525+ Json.Value.object'
2826 [
2929- J.mem (J.name "jsonrpc") (J.string "2.0");
3030- J.mem (J.name "id") id;
3131- J.mem (J.name "result") result;
2727+ Json.Value.member (Json.Value.name "jsonrpc") (Json.Value.string "2.0");
2828+ Json.Value.member (Json.Value.name "id") id;
2929+ Json.Value.member (Json.Value.name "result") result;
3230 ]
33313432let jsonrpc_error ~id ~code ~message =
3535- J.object'
3333+ Json.Value.object'
3634 [
3737- J.mem (J.name "jsonrpc") (J.string "2.0");
3838- J.mem (J.name "id") id;
3939- J.mem (J.name "error")
4040- (J.object'
3535+ Json.Value.member (Json.Value.name "jsonrpc") (Json.Value.string "2.0");
3636+ Json.Value.member (Json.Value.name "id") id;
3737+ Json.Value.member (Json.Value.name "error")
3838+ (Json.Value.object'
4139 [
4242- J.mem (J.name "code") (J.number (Float.of_int code));
4343- J.mem (J.name "message") (J.string message);
4040+ Json.Value.member (Json.Value.name "code")
4141+ (Json.Value.number (Float.of_int code));
4242+ Json.Value.member
4343+ (Json.Value.name "message")
4444+ (Json.Value.string message);
4445 ]);
4546 ]
46474748(* Extract string from JSON *)
4848-let get_string key (obj : Jsont.json) =
4949+let string_of key (obj : Json.t) =
4950 match obj with
5050- | Jsont.Object (mems, _) -> (
5151- match J.find_mem key mems with
5252- | Some (_, Jsont.String (s, _)) -> Some s
5151+ | Json.Object (mems, _) -> (
5252+ match Json.Value.member_key key mems with
5353+ | Some (_, Json.String (s, _)) -> Some s
5354 | _ -> None)
5455 | _ -> None
55565657(* Extract object from JSON *)
5757-let get_object key (obj : Jsont.json) : Jsont.json option =
5858+let object_of key (obj : Json.t) : Json.t option =
5859 match obj with
5959- | Jsont.Object (mems, _) -> (
6060- match J.find_mem key mems with
6161- | Some (_, (Jsont.Object _ as o)) -> Some o
6060+ | Json.Object (mems, _) -> (
6161+ match Json.Value.member_key key mems with
6262+ | Some (_, (Json.Object _ as o)) -> Some o
6263 | _ -> None)
6364 | _ -> None
64656566(* Get ID from JSON message *)
6666-let get_id (msg : Jsont.json) : Jsont.json =
6767+let msg_id (msg : Json.t) : Json.t =
6768 match msg with
6868- | Jsont.Object (mems, _) -> (
6969- match J.find_mem "id" mems with Some (_, id) -> id | None -> J.null ())
7070- | _ -> J.null ()
6969+ | Json.Object (mems, _) -> (
7070+ match Json.Value.member_key "id" mems with
7171+ | Some (_, id) -> id
7272+ | None -> Json.Value.null ())
7373+ | _ -> Json.Value.null ()
71747275(* Handle initialize request *)
7376let handle_initialize t ~id =
7477 jsonrpc_success ~id
7575- (J.object'
7878+ (Json.Value.object'
7679 [
7777- J.mem (J.name "protocolVersion") (J.string "2024-11-05");
7878- J.mem (J.name "capabilities")
7979- (J.object' [ J.mem (J.name "tools") (J.object' []) ]);
8080- J.mem (J.name "serverInfo")
8181- (J.object'
8080+ Json.Value.member
8181+ (Json.Value.name "protocolVersion")
8282+ (Json.Value.string "2024-11-05");
8383+ Json.Value.member
8484+ (Json.Value.name "capabilities")
8585+ (Json.Value.object'
8286 [
8383- J.mem (J.name "name") (J.string t.name);
8484- J.mem (J.name "version") (J.string t.version);
8787+ Json.Value.member (Json.Value.name "tools")
8888+ (Json.Value.object' []);
8989+ ]);
9090+ Json.Value.member
9191+ (Json.Value.name "serverInfo")
9292+ (Json.Value.object'
9393+ [
9494+ Json.Value.member (Json.Value.name "name")
9595+ (Json.Value.string t.name);
9696+ Json.Value.member
9797+ (Json.Value.name "version")
9898+ (Json.Value.string t.version);
8599 ]);
86100 ])
87101···90104 let tools_json =
91105 List.map
92106 (fun tool ->
9393- J.object'
107107+ Json.Value.object'
94108 [
9595- J.mem (J.name "name") (J.string (Tool.name tool));
9696- J.mem (J.name "description") (J.string (Tool.description tool));
9797- J.mem (J.name "inputSchema") (Tool.input_schema tool);
109109+ Json.Value.member (Json.Value.name "name")
110110+ (Json.Value.string (Tool.name tool));
111111+ Json.Value.member
112112+ (Json.Value.name "description")
113113+ (Json.Value.string (Tool.description tool));
114114+ Json.Value.member
115115+ (Json.Value.name "inputSchema")
116116+ (Tool.input_schema tool);
98117 ])
99118 t.tools
100119 in
101101- jsonrpc_success ~id (J.object' [ J.mem (J.name "tools") (J.list tools_json) ])
120120+ jsonrpc_success ~id
121121+ (Json.Value.object'
122122+ [
123123+ Json.Value.member (Json.Value.name "tools")
124124+ (Json.Value.list tools_json);
125125+ ])
102126103127(* Handle tools/call request *)
104128let handle_tools_call t ~id ~params =
105105- match get_string "name" params with
129129+ match string_of "name" params with
106130 | None -> jsonrpc_error ~id ~code:(-32602) ~message:"Missing 'name' parameter"
107131 | Some tool_name -> (
108132 match Hashtbl.find_opt t.tool_map tool_name with
109133 | None ->
110134 jsonrpc_error ~id ~code:(-32601)
111111- ~message:(Printf.sprintf "Tool '%s' not found" tool_name)
135135+ ~message:(Fmt.str "Tool '%s' not found" tool_name)
112136 | Some tool -> (
113137 let arguments =
114114- match get_object "arguments" params with
138138+ match object_of "arguments" params with
115139 | Some args -> args
116116- | None -> J.object' []
140140+ | None -> Json.Value.object' []
117141 in
118142 let input = Tool_input.of_json arguments in
119143 match Tool.call tool input with
120144 | Ok content ->
121145 jsonrpc_success ~id
122122- (J.object' [ J.mem (J.name "content") content ])
146146+ (Json.Value.object'
147147+ [ Json.Value.member (Json.Value.name "content") content ])
123148 | Error msg ->
124149 (* Return error as content with is_error flag *)
125150 jsonrpc_success ~id
126126- (J.object'
151151+ (Json.Value.object'
127152 [
128128- J.mem (J.name "content")
129129- (J.list
153153+ Json.Value.member
154154+ (Json.Value.name "content")
155155+ (Json.Value.list
130156 [
131131- J.object'
157157+ Json.Value.object'
132158 [
133133- J.mem (J.name "type") (J.string "text");
134134- J.mem (J.name "text") (J.string msg);
159159+ Json.Value.member (Json.Value.name "type")
160160+ (Json.Value.string "text");
161161+ Json.Value.member (Json.Value.name "text")
162162+ (Json.Value.string msg);
135163 ];
136164 ]);
137137- J.mem (J.name "isError") (J.bool true);
165165+ Json.Value.member
166166+ (Json.Value.name "isError")
167167+ (Json.Value.bool true);
138168 ])))
139169140170let handle_request t ~method_ ~params ~id =
···144174 | "tools/call" -> handle_tools_call t ~id ~params
145175 | _ ->
146176 jsonrpc_error ~id ~code:(-32601)
147147- ~message:(Printf.sprintf "Method '%s' not found" method_)
177177+ ~message:(Fmt.str "Method '%s' not found" method_)
148178149149-let handle_json_message t (msg : Jsont.json) =
150150- let method_ = match get_string "method" msg with Some m -> m | None -> "" in
179179+let handle_json_message t (msg : Json.t) =
180180+ let method_ = match string_of "method" msg with Some m -> m | None -> "" in
151181 let params =
152152- match get_object "params" msg with Some p -> p | None -> J.object' []
182182+ match object_of "params" msg with
183183+ | Some p -> p
184184+ | None -> Json.Value.object' []
153185 in
154154- let id = get_id msg in
186186+ let id = msg_id msg in
155187 handle_request t ~method_ ~params ~id
+20-21
lib/mcp_server.mli
···1212 {2 Basic Usage}
13131414 {[
1515- let greet =
1616- Tool.create ~name:"greet" ~description:"Greet a user"
1717- ~input_schema:
1818- (Tool.schema_object
1919- [ ("name", Tool.schema_string) ]
2020- ~required:[ "name" ])
2121- ~handler:(fun args ->
2222- match Tool_input.get_string args "name" with
2323- | Some name ->
2424- Ok (Tool.text_result (Printf.sprintf "Hello, %s!" name))
2525- | None -> Error "Missing name")
1515+ let greet =
1616+ Tool.v ~name:"greet" ~description:"Greet a user"
1717+ ~input_schema:
1818+ (Tool.schema_object
1919+ [ ("name", Tool.schema_string) ]
2020+ ~required:[ "name" ])
2121+ ~handler:(fun args ->
2222+ match Tool_input.string args "name" with
2323+ | Some name ->
2424+ Ok (Tool.text_result (Printf.sprintf "Hello, %s!" name))
2525+ | None -> Error "Missing name")
26262727- let server = Mcp_server.create ~name:"my-tools" ~tools:[ greet ] ()
2727+ let server = Mcp_server.v ~name:"my-tools" ~tools:[ greet ] ()
28282929- let options =
3030- Options.default
3131- |> Options.with_mcp_server ~name:"tools" server
3232- |> Options.with_allowed_tools [ "mcp__tools__greet" ]
2929+ let options =
3030+ Options.default
3131+ |> Options.with_mcp_server ~name:"tools" server
3232+ |> Options.with_allowed_tools [ "mcp__tools__greet" ]
3333 ]}
34343535 {2 Tool Naming}
···4848type t
4949(** Abstract type for MCP servers. *)
50505151-val create : name:string -> ?version:string -> tools:Tool.t list -> unit -> t
5252-(** [create ~name ?version ~tools ()] creates an in-process MCP server.
5151+val v : name:string -> ?version:string -> tools:Tool.t list -> unit -> t
5252+(** [v ~name ?version ~tools ()] creates an in-process MCP server.
53535454 @param name Server identifier. Used in tool naming: [mcp__<name>__<tool>].
5555 @param version Server version string (default "1.0.0").
···66666767(** {1 MCP Protocol Handling} *)
68686969-val handle_request :
7070- t -> method_:string -> params:Jsont.json -> id:Jsont.json -> Jsont.json
6969+val handle_request : t -> method_:string -> params:Json.t -> id:Json.t -> Json.t
7170(** [handle_request t ~method_ ~params ~id] handles an MCP JSONRPC request.
72717372 Returns a JSONRPC response object with the given [id].
···79788079 Unknown methods return a JSONRPC error response. *)
81808282-val handle_json_message : t -> Jsont.json -> Jsont.json
8181+val handle_json_message : t -> Json.t -> Json.t
8382(** [handle_json_message t msg] handles a complete JSONRPC message.
84838584 Extracts method, params, and id from the message and delegates to
+327-99
lib/message.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566+open Json.Codec
77+68let src = Logs.Src.create "claude.message" ~doc:"Claude messages"
79810module Log = (val Logs.src_log src : Logs.LOG)
9111012module User = struct
1111- type t = Proto.Message.User.t
1212-1313- let of_string s = Proto.Message.User.create_string s
1313+ type content = String of string | Blocks of Content_block.t list
1414+ type t = { content : content; unknown : Unknown.t }
14151515- let of_blocks blocks =
1616- Proto.Message.User.create_blocks (List.map Content_block.to_proto blocks)
1616+ let of_string s = { content = String s; unknown = Unknown.empty }
1717+ let of_blocks blocks = { content = Blocks blocks; unknown = Unknown.empty }
17181819 let with_tool_result ~tool_use_id ~content ?is_error () =
1919- Proto.Message.User.create_with_tool_result ~tool_use_id ~content ?is_error
2020- ()
2020+ let tool_result =
2121+ Content_block.tool_result ~tool_use_id ~content ?is_error ()
2222+ in
2323+ { content = Blocks [ tool_result ]; unknown = Unknown.empty }
21242222- let as_text t =
2323- match Proto.Message.User.content t with
2424- | Proto.Message.User.String s -> Some s
2525- | Proto.Message.User.Blocks _ -> None
2525+ let make content unknown = { content; unknown }
2626+ let content t = t.content
2727+ let unknown t = t.unknown
2828+ let as_text t = match t.content with String s -> Some s | Blocks _ -> None
26292730 let blocks t =
2828- match Proto.Message.User.content t with
2929- | Proto.Message.User.String s -> [ Content_block.text s ]
3030- | Proto.Message.User.Blocks bs -> List.map Content_block.of_proto bs
3131+ match t.content with
3232+ | String s -> [ Content_block.text s ]
3333+ | Blocks bs -> bs
3434+3535+ let decode_content json =
3636+ match json with
3737+ | Json.String (s, _) -> String s
3838+ | Json.Array (items, _) ->
3939+ let blocks =
4040+ List.map
4141+ (fun j ->
4242+ match Json.decode Content_block.json j with
4343+ | Ok v -> v
4444+ | Error e ->
4545+ invalid_arg
4646+ ("Invalid content block: " ^ Json.Error.to_string e))
4747+ items
4848+ in
4949+ Blocks blocks
5050+ | _ -> failwith "Content must be string or array"
5151+5252+ let encode_content = function
5353+ | String s -> Json.String (s, Json.Meta.none)
5454+ | Blocks blocks ->
5555+ let jsons =
5656+ List.map (fun b -> Json.encode Content_block.json b) blocks
5757+ in
5858+ Json.Array (jsons, Json.Meta.none)
5959+6060+ let json : t Json.codec =
6161+ Object.map ~kind:"User" (fun json_content unknown ->
6262+ let content = decode_content json_content in
6363+ make content unknown)
6464+ |> Object.member "content" Value.t ~enc:(fun t ->
6565+ encode_content (content t))
6666+ |> Object.keep_unknown Unknown.mems ~enc:unknown
6767+ |> Object.seal
31683232- let of_proto proto = proto
3333- let to_proto t = t
6969+ let incoming_jsont : t Json.codec =
7070+ let message_jsont =
7171+ Object.map ~kind:"UserMessage" (fun json_content ->
7272+ let content = decode_content json_content in
7373+ { content; unknown = Unknown.empty })
7474+ |> Object.member "content" Value.t ~enc:(fun t ->
7575+ encode_content (content t))
7676+ |> Object.seal
7777+ in
7878+ Object.map ~kind:"UserEnvelope" Fun.id
7979+ |> Object.member "message" message_jsont ~enc:Fun.id
8080+ |> Object.seal
34813535- (* Internal wire format functions *)
3636- let incoming_jsont = Proto.Message.User.incoming_jsont
8282+ let outgoing_jsont : t Json.codec =
8383+ let message_jsont =
8484+ Object.map ~kind:"UserOutgoingMessage" (fun _role json_content ->
8585+ let content = decode_content json_content in
8686+ { content; unknown = Unknown.empty })
8787+ |> Object.member "role" string ~enc:(fun _ -> "user")
8888+ |> Object.member "content" Value.t ~enc:(fun t ->
8989+ encode_content (content t))
9090+ |> Object.seal
9191+ in
9292+ Object.map ~kind:"UserOutgoingEnvelope" Fun.id
9393+ |> Object.member "message" message_jsont ~enc:Fun.id
9494+ |> Object.seal
37953838- let to_json t =
3939- match Jsont.Json.encode Proto.Message.User.jsont t with
4040- | Ok json -> json
4141- | Error e -> invalid_arg ("User.to_json: " ^ e)
9696+ let to_json t = Json.encode json t
4297end
43984499module Assistant = struct
4545- type error = Proto.Message.Assistant.error
4646- type t = Proto.Message.Assistant.t
100100+ type error =
101101+ [ `Authentication_failed
102102+ | `Billing_error
103103+ | `Rate_limit
104104+ | `Invalid_request
105105+ | `Server_error
106106+ | `Unknown ]
107107+108108+ let error_jsont : error Json.codec =
109109+ enum
110110+ [
111111+ ("authentication_failed", `Authentication_failed);
112112+ ("billing_error", `Billing_error);
113113+ ("rate_limit", `Rate_limit);
114114+ ("invalid_request", `Invalid_request);
115115+ ("server_error", `Server_error);
116116+ ("unknown", `Unknown);
117117+ ]
118118+119119+ type t = {
120120+ content : Content_block.t list;
121121+ model : string;
122122+ error : error option;
123123+ unknown : Unknown.t;
124124+ }
471254848- let content t =
4949- List.map Content_block.of_proto (Proto.Message.Assistant.content t)
126126+ let create ~content ~model ?error () =
127127+ { content; model; error; unknown = Unknown.empty }
501285151- let model t = Proto.Message.Assistant.model t
5252- let error t = Proto.Message.Assistant.error t
129129+ let make content model error unknown = { content; model; error; unknown }
130130+ let content t = t.content
131131+ let model t = t.model
132132+ let error t = t.error
133133+ let unknown t = t.unknown
5313454135 let text_blocks t =
55136 List.filter_map
56137 (function
57138 | Content_block.Text text -> Some (Content_block.Text.text text)
58139 | _ -> None)
5959- (content t)
140140+ t.content
6014161142 let tool_uses t =
62143 List.filter_map
63144 (function Content_block.Tool_use tool -> Some tool | _ -> None)
6464- (content t)
145145+ t.content
6514666147 let thinking_blocks t =
67148 List.filter_map
68149 (function Content_block.Thinking thinking -> Some thinking | _ -> None)
6969- (content t)
150150+ t.content
7015171152 let has_tool_use t =
72153 List.exists
73154 (function Content_block.Tool_use _ -> true | _ -> false)
7474- (content t)
155155+ t.content
7515676157 let combined_text t = String.concat "\n" (text_blocks t)
7777- let of_proto proto = proto
7878- let to_proto t = t
791588080- (* Internal wire format functions *)
8181- let incoming_jsont = Proto.Message.Assistant.incoming_jsont
159159+ let json : t Json.codec =
160160+ Object.map ~kind:"Assistant" make
161161+ |> Object.member "content" (list Content_block.json) ~enc:content
162162+ |> Object.member "model" string ~enc:model
163163+ |> Object.opt_member "error" error_jsont ~enc:error
164164+ |> Object.keep_unknown Unknown.mems ~enc:unknown
165165+ |> Object.seal
821668383- let to_json t =
8484- match Jsont.Json.encode Proto.Message.Assistant.jsont t with
8585- | Ok json -> json
8686- | Error e -> invalid_arg ("Assistant.to_json: " ^ e)
167167+ let incoming_jsont : t Json.codec =
168168+ Object.map ~kind:"AssistantEnvelope" Fun.id
169169+ |> Object.member "message" json ~enc:Fun.id
170170+ |> Object.seal
171171+172172+ let to_json t = Json.encode json t
87173end
8817489175module System = struct
9090- type t = Proto.Message.System.t
176176+ type init = {
177177+ session_id : string option;
178178+ model : string option;
179179+ cwd : string option;
180180+ unknown : Unknown.t;
181181+ }
182182+183183+ type error = { error : string; unknown : Unknown.t }
184184+ type t = Init of init | Error of error
185185+186186+ let is_init = function Init _ -> true | _ -> false
187187+ let is_error = function Error _ -> true | _ -> false
188188+ let session_id = function Init i -> i.session_id | _ -> None
189189+ let model = function Init i -> i.model | _ -> None
190190+ let cwd = function Init i -> i.cwd | _ -> None
191191+ let error_message = function Error e -> Some e.error | _ -> None
192192+ let error_msg = error_message
193193+ let unknown = function Init i -> i.unknown | Error e -> e.unknown
911949292- let is_init = function Proto.Message.System.Init _ -> true | _ -> false
9393- let is_error = function Proto.Message.System.Error _ -> true | _ -> false
9494- let session_id = Proto.Message.System.session_id
9595- let model = Proto.Message.System.model
9696- let cwd = Proto.Message.System.cwd
9797- let error_message = Proto.Message.System.error_msg
9898- let of_proto proto = proto
9999- let to_proto t = t
195195+ let init ?session_id ?model ?cwd () =
196196+ Init { session_id; model; cwd; unknown = Unknown.empty }
100197101101- (* Internal wire format functions *)
102102- let jsont = Proto.Message.System.jsont
198198+ let error ~error = Error { error; unknown = Unknown.empty }
103199104104- let to_json t =
105105- match Jsont.Json.encode Proto.Message.System.jsont t with
106106- | Ok json -> json
107107- | Error e -> invalid_arg ("System.to_json: " ^ e)
200200+ let init_jsont : init Json.codec =
201201+ let make session_id model cwd unknown : init =
202202+ { session_id; model; cwd; unknown }
203203+ in
204204+ Object.map ~kind:"SystemInit" make
205205+ |> Object.opt_member "session_id" string ~enc:(fun (r : init) ->
206206+ r.session_id)
207207+ |> Object.opt_member "model" string ~enc:(fun (r : init) -> r.model)
208208+ |> Object.opt_member "cwd" string ~enc:(fun (r : init) -> r.cwd)
209209+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : init) -> r.unknown)
210210+ |> Object.seal
211211+212212+ let error_jsont : error Json.codec =
213213+ let make err unknown : error = { error = err; unknown } in
214214+ Object.map ~kind:"SystemError" make
215215+ |> Object.member "error" string ~enc:(fun (r : error) -> r.error)
216216+ |> Object.keep_unknown Unknown.mems ~enc:(fun (r : error) -> r.unknown)
217217+ |> Object.seal
218218+219219+ let json : t Json.codec =
220220+ let case_init = Object.Case.map "init" init_jsont ~dec:(fun v -> Init v) in
221221+ let case_error =
222222+ Object.Case.map "error" error_jsont ~dec:(fun v -> Error v)
223223+ in
224224+ let enc_case = function
225225+ | Init v -> Object.Case.value case_init v
226226+ | Error v -> Object.Case.value case_error v
227227+ in
228228+ let cases = Object.Case.[ make case_init; make case_error ] in
229229+ Object.map ~kind:"System" Fun.id
230230+ |> Object.case_member "subtype" string ~enc:Fun.id ~enc_case cases
231231+ ~tag_to_string:Fun.id ~tag_compare:String.compare
232232+ |> Object.seal
233233+234234+ let to_json t = Json.encode json t
108235end
109236110237module Result = struct
111238 module Usage = struct
112112- type t = Proto.Message.Result.Usage.t
239239+ type t = {
240240+ input_tokens : int option;
241241+ output_tokens : int option;
242242+ total_tokens : int option;
243243+ cache_creation_input_tokens : int option;
244244+ cache_read_input_tokens : int option;
245245+ unknown : Unknown.t;
246246+ }
113247114114- let input_tokens = Proto.Message.Result.Usage.input_tokens
115115- let output_tokens = Proto.Message.Result.Usage.output_tokens
116116- let total_tokens = Proto.Message.Result.Usage.total_tokens
248248+ let make input_tokens output_tokens total_tokens cache_creation_input_tokens
249249+ cache_read_input_tokens unknown =
250250+ {
251251+ input_tokens;
252252+ output_tokens;
253253+ total_tokens;
254254+ cache_creation_input_tokens;
255255+ cache_read_input_tokens;
256256+ unknown;
257257+ }
117258118118- let cache_creation_input_tokens =
119119- Proto.Message.Result.Usage.cache_creation_input_tokens
259259+ let create ?input_tokens ?output_tokens ?total_tokens
260260+ ?cache_creation_input_tokens ?cache_read_input_tokens () =
261261+ {
262262+ input_tokens;
263263+ output_tokens;
264264+ total_tokens;
265265+ cache_creation_input_tokens;
266266+ cache_read_input_tokens;
267267+ unknown = Unknown.empty;
268268+ }
120269121121- let cache_read_input_tokens =
122122- Proto.Message.Result.Usage.cache_read_input_tokens
270270+ let input_tokens t = t.input_tokens
271271+ let output_tokens t = t.output_tokens
272272+ let total_tokens t = t.total_tokens
273273+ let cache_creation_input_tokens t = t.cache_creation_input_tokens
274274+ let cache_read_input_tokens t = t.cache_read_input_tokens
275275+ let unknown t = t.unknown
123276124124- let of_proto proto = proto
277277+ let json : t Json.codec =
278278+ Object.map ~kind:"Usage" make
279279+ |> Object.opt_member "input_tokens" int ~enc:input_tokens
280280+ |> Object.opt_member "output_tokens" int ~enc:output_tokens
281281+ |> Object.opt_member "total_tokens" int ~enc:total_tokens
282282+ |> Object.opt_member "cache_creation_input_tokens" int
283283+ ~enc:cache_creation_input_tokens
284284+ |> Object.opt_member "cache_read_input_tokens" int
285285+ ~enc:cache_read_input_tokens
286286+ |> Object.keep_unknown Unknown.mems ~enc:unknown
287287+ |> Object.seal
125288 end
126289127127- type t = Proto.Message.Result.t
290290+ type t = {
291291+ subtype : string;
292292+ duration_ms : int;
293293+ duration_api_ms : int;
294294+ is_error : bool;
295295+ num_turns : int;
296296+ session_id : string;
297297+ total_cost_usd : float option;
298298+ usage : Usage.t option;
299299+ result : string option;
300300+ structured_output : Json.t option;
301301+ unknown : Unknown.t;
302302+ }
303303+304304+ let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
305305+ ~session_id ?total_cost_usd ?usage ?result ?structured_output () =
306306+ {
307307+ subtype;
308308+ duration_ms;
309309+ duration_api_ms;
310310+ is_error;
311311+ num_turns;
312312+ session_id;
313313+ total_cost_usd;
314314+ usage;
315315+ result;
316316+ structured_output;
317317+ unknown = Unknown.empty;
318318+ }
128319129129- let duration_ms = Proto.Message.Result.duration_ms
130130- let duration_api_ms = Proto.Message.Result.duration_api_ms
131131- let is_error = Proto.Message.Result.is_error
132132- let num_turns = Proto.Message.Result.num_turns
133133- let session_id = Proto.Message.Result.session_id
134134- let total_cost_usd = Proto.Message.Result.total_cost_usd
135135- let usage t = Option.map Usage.of_proto (Proto.Message.Result.usage t)
136136- let result_text = Proto.Message.Result.result
137137- let structured_output = Proto.Message.Result.structured_output
138138- let of_proto proto = proto
139139- let to_proto t = t
320320+ let make subtype duration_ms duration_api_ms is_error num_turns session_id
321321+ total_cost_usd usage result structured_output unknown =
322322+ {
323323+ subtype;
324324+ duration_ms;
325325+ duration_api_ms;
326326+ is_error;
327327+ num_turns;
328328+ session_id;
329329+ total_cost_usd;
330330+ usage;
331331+ result;
332332+ structured_output;
333333+ unknown;
334334+ }
140335141141- (* Internal wire format functions *)
142142- let jsont = Proto.Message.Result.jsont
336336+ let subtype t = t.subtype
337337+ let duration_ms t = t.duration_ms
338338+ let duration_api_ms t = t.duration_api_ms
339339+ let is_error t = t.is_error
340340+ let num_turns t = t.num_turns
341341+ let session_id t = t.session_id
342342+ let total_cost_usd t = t.total_cost_usd
343343+ let usage t = t.usage
344344+ let result t = t.result
345345+ let result_text = result
346346+ let structured_output t = t.structured_output
347347+ let unknown t = t.unknown
143348144144- let to_json t =
145145- match Jsont.Json.encode Proto.Message.Result.jsont t with
146146- | Ok json -> json
147147- | Error e -> invalid_arg ("Result.to_json: " ^ e)
349349+ let json : t Json.codec =
350350+ Object.map ~kind:"Result" make
351351+ |> Object.member "subtype" string ~enc:subtype
352352+ |> Object.member "duration_ms" int ~enc:duration_ms
353353+ |> Object.member "duration_api_ms" int ~enc:duration_api_ms
354354+ |> Object.member "is_error" bool ~enc:is_error
355355+ |> Object.member "num_turns" int ~enc:num_turns
356356+ |> Object.member "session_id" string ~enc:session_id
357357+ |> Object.opt_member "total_cost_usd" number ~enc:total_cost_usd
358358+ |> Object.opt_member "usage" Usage.json ~enc:usage
359359+ |> Object.opt_member "result" string ~enc:result
360360+ |> Object.opt_member "structured_output" Value.t ~enc:structured_output
361361+ |> Object.keep_unknown Unknown.mems ~enc:unknown
362362+ |> Object.seal
363363+364364+ let to_json t = Json.encode json t
148365end
149366150367type t =
···153370 | System of System.t
154371 | Result of Result.t
155372156156-let of_proto = function
157157- | Proto.Message.User u -> User (User.of_proto u)
158158- | Proto.Message.Assistant a -> Assistant (Assistant.of_proto a)
159159- | Proto.Message.System s -> System (System.of_proto s)
160160- | Proto.Message.Result r -> Result (Result.of_proto r)
161161-162162-let to_proto = function
163163- | User u -> Proto.Message.User (User.to_proto u)
164164- | Assistant a -> Proto.Message.Assistant (Assistant.to_proto a)
165165- | System s -> Proto.Message.System (System.to_proto s)
166166- | Result r -> Proto.Message.Result (Result.to_proto r)
373373+let json : t Json.codec =
374374+ let case_map kind obj dec = Object.Case.map kind obj ~dec in
375375+ let case_user = case_map "user" User.incoming_jsont (fun v -> User v) in
376376+ let case_assistant =
377377+ case_map "assistant" Assistant.incoming_jsont (fun v -> Assistant v)
378378+ in
379379+ let case_system = case_map "system" System.json (fun v -> System v) in
380380+ let case_result = case_map "result" Result.json (fun v -> Result v) in
381381+ let enc_case = function
382382+ | User v -> Object.Case.value case_user v
383383+ | Assistant v -> Object.Case.value case_assistant v
384384+ | System v -> Object.Case.value case_system v
385385+ | Result v -> Object.Case.value case_result v
386386+ in
387387+ let cases =
388388+ Object.Case.
389389+ [
390390+ make case_user; make case_assistant; make case_system; make case_result;
391391+ ]
392392+ in
393393+ Object.map ~kind:"Message" Fun.id
394394+ |> Object.case_member "type" string ~enc:Fun.id ~enc_case cases
395395+ ~tag_to_string:Fun.id ~tag_compare:String.compare
396396+ |> Object.seal
167397168398let is_user = function User _ -> true | _ -> false
169399let is_assistant = function Assistant _ -> true | _ -> false
···186416 | Assistant a -> Assistant.tool_uses a
187417 | _ -> []
188418189189-let get_session_id = function
419419+let session_id = function
190420 | System s -> System.session_id s
191421 | Result r -> Some (Result.session_id r)
192422 | _ -> None
193423194194-(* Wire format conversion *)
195424let to_json = function
196425 | User u -> User.to_json u
197426 | Assistant a -> Assistant.to_json a
198427 | System s -> System.to_json s
199428 | Result r -> Result.to_json r
200429201201-(* Convenience constructors *)
202430let user_string s = User (User.of_string s)
203431let user_blocks blocks = User (User.of_blocks blocks)
204204-let pp fmt t = Jsont.pp_value Proto.Message.jsont () fmt (to_proto t)
205205-let log_received t = Log.info (fun m -> m "← %a" pp t)
206206-let log_sending t = Log.info (fun m -> m "→ %a" pp t)
432432+let pp ppf t = Json.pp_value json ppf t
433433+let log_received t = Log.info (fun m -> m "<- %a" pp t)
434434+let log_sending t = Log.info (fun m -> m "-> %a" pp t)
+72-179
lib/message.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Messages exchanged with Claude. Opaque types.
77-88- This module provides opaque message types that wrap the proto types but hide
99- the unknown fields and wire format details from the public API. *)
66+(** Messages exchanged with Claude. *)
107118val src : Logs.Src.t
1212-(** The log source for message operations *)
99+(** The log source for message operations. *)
13101411(** {1 User Messages} *)
15121613module User : sig
1717- (** Messages sent by the user. *)
1818-1414+ type content = String of string | Blocks of Content_block.t list
1915 type t
2020- (** The type of user messages (opaque). *)
21162217 val of_string : string -> t
2318 (** [of_string s] creates a user message with simple text content. *)
···2621 (** [of_blocks blocks] creates a user message with content blocks. *)
27222823 val with_tool_result :
2929- tool_use_id:string -> content:Jsont.json -> ?is_error:bool -> unit -> t
2424+ tool_use_id:string -> content:Json.t -> ?is_error:bool -> unit -> t
3025 (** [with_tool_result ~tool_use_id ~content ?is_error ()] creates a user
3126 message containing a tool result. Content can be a string or array. *)
2727+2828+ val content : t -> content
2929+ val unknown : t -> Unknown.t
32303331 val as_text : t -> string option
3432 (** [as_text t] returns the text content if the message is a simple string,
···3836 (** [blocks t] returns the content blocks, or a single text block if it's a
3937 string message. *)
40384141- (** {1 Internal - for lib use only} *)
4242-4343- val of_proto : Proto.Message.User.t -> t
4444- (** [of_proto proto] wraps a proto user message. *)
4545-4646- val to_proto : t -> Proto.Message.User.t
4747- (** [to_proto t] extracts the proto user message. *)
4848-4949- val incoming_jsont : t Jsont.t
5050- (** Internal codec for parsing incoming messages. *)
5151-5252- val to_json : t -> Jsont.json
5353- (** Internal conversion to JSON for wire format. *)
3939+ val json : t Json.codec
4040+ val incoming_jsont : t Json.codec
4141+ val outgoing_jsont : t Json.codec
4242+ val to_json : t -> Json.t
5443end
55445645(** {1 Assistant Messages} *)
57465847module Assistant : sig
5959- (** Messages from Claude assistant. *)
6060-6148 type error =
6262- [ `Authentication_failed (** Authentication with Claude API failed *)
6363- | `Billing_error (** Billing or account issue *)
6464- | `Rate_limit (** Rate limit exceeded *)
6565- | `Invalid_request (** Request was invalid *)
6666- | `Server_error (** Internal server error *)
6767- | `Unknown (** Unknown error type *) ]
6868- (** The type of assistant message errors based on Python SDK error types. *)
4949+ [ `Authentication_failed
5050+ | `Billing_error
5151+ | `Rate_limit
5252+ | `Invalid_request
5353+ | `Server_error
5454+ | `Unknown ]
69557056 type t
7171- (** The type of assistant messages (opaque). *)
72577373- val content : t -> Content_block.t list
7474- (** [content t] returns the content blocks of the assistant message. *)
5858+ val create :
5959+ content:Content_block.t list -> model:string -> ?error:error -> unit -> t
75606161+ val content : t -> Content_block.t list
7662 val model : t -> string
7777- (** [model t] returns the model identifier. *)
7878-7963 val error : t -> error option
8080- (** [error t] returns the optional error that occurred during message
8181- generation. *)
8282-8383- (** {2 Convenience accessors} *)
8484-6464+ val unknown : t -> Unknown.t
8565 val text_blocks : t -> string list
8686- (** [text_blocks t] extracts all text content from the message. *)
8787-8866 val tool_uses : t -> Content_block.Tool_use.t list
8989- (** [tool_uses t] extracts all tool use blocks from the message. *)
9090-9167 val thinking_blocks : t -> Content_block.Thinking.t list
9292- (** [thinking_blocks t] extracts all thinking blocks from the message. *)
9393-9468 val combined_text : t -> string
9595- (** [combined_text t] concatenates all text blocks into a single string. *)
9696-9769 val has_tool_use : t -> bool
9898- (** [has_tool_use t] returns true if the message contains any tool use blocks.
9999- *)
100100-101101- (** {1 Internal - for lib use only} *)
102102-103103- val of_proto : Proto.Message.Assistant.t -> t
104104- (** [of_proto proto] wraps a proto assistant message. *)
105105-106106- val to_proto : t -> Proto.Message.Assistant.t
107107- (** [to_proto t] extracts the proto assistant message. *)
108108-109109- val incoming_jsont : t Jsont.t
110110- (** Internal codec for parsing incoming messages. *)
111111-112112- val to_json : t -> Jsont.json
113113- (** Internal conversion to JSON for wire format. *)
7070+ val json : t Json.codec
7171+ val incoming_jsont : t Json.codec
7272+ val to_json : t -> Json.t
11473end
1157411675(** {1 System Messages} *)
1177611877module System : sig
119119- (** System control and status messages. *)
7878+ type init = {
7979+ session_id : string option;
8080+ model : string option;
8181+ cwd : string option;
8282+ unknown : Unknown.t;
8383+ }
12084121121- type t
122122- (** The type of system messages (opaque). *)
8585+ type error = { error : string; unknown : Unknown.t }
8686+ type t = Init of init | Error of error
1238712488 val is_init : t -> bool
125125- (** [is_init t] returns true if the message is an init message. *)
126126-12789 val is_error : t -> bool
128128- (** [is_error t] returns true if the message is an error message. *)
129129-13090 val session_id : t -> string option
131131- (** [session_id t] returns session_id from Init, None otherwise. *)
132132-13391 val model : t -> string option
134134- (** [model t] returns model from Init, None otherwise. *)
135135-13692 val cwd : t -> string option
137137- (** [cwd t] returns cwd from Init, None otherwise. *)
138138-13993 val error_message : t -> string option
140140- (** [error_message t] returns error from Error, None otherwise. *)
141141-142142- (** {1 Internal - for lib use only} *)
143143-144144- val of_proto : Proto.Message.System.t -> t
145145- (** [of_proto proto] wraps a proto system message. *)
146146-147147- val to_proto : t -> Proto.Message.System.t
148148- (** [to_proto t] extracts the proto system message. *)
149149-150150- val jsont : t Jsont.t
151151- (** Internal codec for wire format. *)
152152-153153- val to_json : t -> Jsont.json
154154- (** Internal conversion to JSON for wire format. *)
9494+ val error_msg : t -> string option
9595+ val unknown : t -> Unknown.t
9696+ val init : ?session_id:string -> ?model:string -> ?cwd:string -> unit -> t
9797+ val error : error:string -> t
9898+ val json : t Json.codec
9999+ val to_json : t -> Json.t
155100end
156101157102(** {1 Result Messages} *)
158103159104module Result : sig
160160- (** Final result messages with metadata about the conversation. *)
161161-162105 module Usage : sig
163163- (** Usage statistics for API calls. *)
164164-165106 type t
166166- (** Type for usage statistics (opaque). *)
167107168168- val input_tokens : t -> int option
169169- (** [input_tokens t] returns the number of input tokens used. *)
108108+ val create :
109109+ ?input_tokens:int ->
110110+ ?output_tokens:int ->
111111+ ?total_tokens:int ->
112112+ ?cache_creation_input_tokens:int ->
113113+ ?cache_read_input_tokens:int ->
114114+ unit ->
115115+ t
170116117117+ val input_tokens : t -> int option
171118 val output_tokens : t -> int option
172172- (** [output_tokens t] returns the number of output tokens generated. *)
173173-174119 val total_tokens : t -> int option
175175- (** [total_tokens t] returns the total number of tokens. *)
176176-177120 val cache_creation_input_tokens : t -> int option
178178- (** [cache_creation_input_tokens t] returns cache creation input tokens. *)
179179-180121 val cache_read_input_tokens : t -> int option
181181- (** [cache_read_input_tokens t] returns cache read input tokens. *)
182182-183183- (** {1 Internal - for lib use only} *)
184184-185185- val of_proto : Proto.Message.Result.Usage.t -> t
186186- (** [of_proto proto] wraps a proto usage object. *)
122122+ val unknown : t -> Unknown.t
123123+ val json : t Json.codec
187124 end
188125189126 type t
190190- (** The type of result messages (opaque). *)
127127+128128+ val create :
129129+ subtype:string ->
130130+ duration_ms:int ->
131131+ duration_api_ms:int ->
132132+ is_error:bool ->
133133+ num_turns:int ->
134134+ session_id:string ->
135135+ ?total_cost_usd:float ->
136136+ ?usage:Usage.t ->
137137+ ?result:string ->
138138+ ?structured_output:Json.t ->
139139+ unit ->
140140+ t
191141142142+ val subtype : t -> string
192143 val duration_ms : t -> int
193193- (** [duration_ms t] returns the total duration in milliseconds. *)
194194-195144 val duration_api_ms : t -> int
196196- (** [duration_api_ms t] returns the API duration in milliseconds. *)
197197-198145 val is_error : t -> bool
199199- (** [is_error t] returns whether this result represents an error. *)
200200-201146 val num_turns : t -> int
202202- (** [num_turns t] returns the number of conversation turns. *)
203203-204147 val session_id : t -> string
205205- (** [session_id t] returns the session identifier. *)
206206-207148 val total_cost_usd : t -> float option
208208- (** [total_cost_usd t] returns the optional total cost in USD. *)
209209-210149 val usage : t -> Usage.t option
211211- (** [usage t] returns the optional usage statistics. *)
212212-150150+ val result : t -> string option
213151 val result_text : t -> string option
214214- (** [result_text t] returns the optional result string. *)
215215-216216- val structured_output : t -> Jsont.json option
217217- (** [structured_output t] returns the optional structured JSON output. *)
218218-219219- (** {1 Internal - for lib use only} *)
220220-221221- val of_proto : Proto.Message.Result.t -> t
222222- (** [of_proto proto] wraps a proto result message. *)
223223-224224- val to_proto : t -> Proto.Message.Result.t
225225- (** [to_proto t] extracts the proto result message. *)
226226-227227- val jsont : t Jsont.t
228228- (** Internal codec for wire format. *)
229229-230230- val to_json : t -> Jsont.json
231231- (** Internal conversion to JSON for wire format. *)
152152+ val structured_output : t -> Json.t option
153153+ val unknown : t -> Unknown.t
154154+ val json : t Json.codec
155155+ val to_json : t -> Json.t
232156end
233157234158(** {1 Message Union Type} *)
···238162 | Assistant of Assistant.t
239163 | System of System.t
240164 | Result of Result.t
241241- (** The type of messages, which can be user, assistant, system, or result.
242242- *)
243165244244-val of_proto : Proto.Message.t -> t
245245-(** [of_proto proto] converts a proto message to a lib message. *)
246246-247247-val to_proto : t -> Proto.Message.t
248248-(** [to_proto t] converts a lib message to a proto message. *)
166166+val json : t Json.codec
249167250168(** {1 Internal - wire format conversion} *)
251169252252-val to_json : t -> Jsont.json
253253-(** [to_json t] converts any message to its JSON wire format representation. *)
170170+val to_json : t -> Json.t
254171255172(** {1 Convenience Constructors} *)
256173257174val user_string : string -> t
258258-(** [user_string s] creates a user message with text content. *)
259259-260175val user_blocks : Content_block.t list -> t
261261-(** [user_blocks blocks] creates a user message with content blocks. *)
262176263177(** {1 Message Analysis} *)
264178265179val is_user : t -> bool
266266-(** [is_user t] returns true if the message is from a user. *)
267267-268180val is_assistant : t -> bool
269269-(** [is_assistant t] returns true if the message is from the assistant. *)
270270-271181val is_system : t -> bool
272272-(** [is_system t] returns true if the message is a system message. *)
273273-274182val is_result : t -> bool
275275-(** [is_result t] returns true if the message is a result message. *)
276276-277183val is_error : t -> bool
278278-(** [is_error t] returns true if the message represents an error. *)
279279-280184val extract_text : t -> string option
281281-(** [extract_text t] attempts to extract text content from any message type. *)
282282-283185val extract_tool_uses : t -> Content_block.Tool_use.t list
284284-(** [extract_tool_uses t] extracts tool use blocks from assistant messages. *)
285285-286286-val get_session_id : t -> string option
287287-(** [get_session_id t] extracts the session ID from system or result messages.
288288-*)
186186+val session_id : t -> string option
289187290188(** {1 Logging} *)
291189292190val pp : Format.formatter -> t -> unit
293293-(** [pp fmt t] pretty-prints any message. *)
294294-295191val log_received : t -> unit
296296-(** [log_received t] logs that a message was received. *)
297297-298192val log_sending : t -> unit
299299-(** [log_sending t] logs that a message is being sent. *)
···1010 escape hatch for future or unknown models. *)
11111212type t =
1313- [ `Sonnet_4_5 (** claude-sonnet-4-5 - Most recent Sonnet model *)
1313+ [ `Sonnet_4_6 (** claude-sonnet-4-6 - Most recent Sonnet model *)
1414+ | `Sonnet_4_5 (** claude-sonnet-4-5 - Sonnet 4.5 model *)
1415 | `Sonnet_4 (** claude-sonnet-4 - Sonnet 4 model *)
1516 | `Sonnet_3_5 (** claude-sonnet-3-5 - Sonnet 3.5 model *)
1616- | `Opus_4_5 (** claude-opus-4-5 - Most recent Opus model *)
1717+ | `Opus_4_6 (** claude-opus-4-6 - Most recent Opus model *)
1818+ | `Opus_4_5 (** claude-opus-4-5 - Opus 4.5 model *)
1719 | `Opus_4_1 (** claude-opus-4-1 - Opus 4.1 model *)
1820 | `Opus_4 (** claude-opus-4 - Opus 4 model for complex tasks *)
1919- | `Haiku_4 (** claude-haiku-4 - Fast, cost-effective Haiku model *)
2121+ | `Haiku_4_5 (** claude-haiku-4-5 - Most recent Haiku model *)
2222+ | `Haiku_4 (** claude-haiku-4 - Haiku 4 model *)
2023 | `Custom of string (** Custom model string for future/unknown models *) ]
2124(** The type of Claude models. *)
22252626+val pp : Format.formatter -> t -> unit
2727+(** [pp ppf t] pretty-prints the model identifier. *)
2828+2329val to_string : t -> string
2430(** [to_string t] converts a model to its CLI string representation.
25312632 Examples:
2727- - [`Sonnet_4_5] becomes "claude-sonnet-4-5"
2828- - [`Opus_4_5] becomes "claude-opus-4-5"
2929- - [`Opus_4] becomes "claude-opus-4"
3030- - [`Custom "my-model"] becomes "my-model" *)
3333+ - [`Sonnet_4_6] becomes "claude-sonnet-4-6"
3434+ - [`Opus_4_6] becomes "claude-opus-4-6"
3535+ - [`Haiku_4_5] becomes "claude-haiku-4-5"
3636+ - [`Custom "my-model"] becomes "my-model". *)
31373238val of_string : string -> t
3339(** [of_string s] parses a model string into a typed model.
···3642 become [`Custom s].
37433844 Examples:
3939- - "claude-sonnet-4-5" becomes [`Sonnet_4_5]
4040- - "future-model" becomes [`Custom "future-model"] *)
4545+ - "claude-sonnet-4-6" or "sonnet" becomes [`Sonnet_4_6].
4646+ - "claude-opus-4-6" or "opus" becomes [`Opus_4_6].
4747+ - "future-model" becomes [`Custom "future-model"]. *)
4848+4949+val json : t Json.codec
5050+(** [json] is the Jsont codec for model identifiers. *)
+218-93
lib/options.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-let src = Logs.Src.create "claudeio.options" ~doc:"Claude configuration options"
66+let src = Logs.Src.create "claude.options" ~doc:"Claude configuration options"
7788module Log = (val Logs.src_log src : Logs.LOG)
991010+module Wire = struct
1111+ type setting_source = User | Project | Local
1212+1313+ let setting_source_jsont : setting_source Json.codec =
1414+ let open Json.Codec in
1515+ enum [ ("user", User); ("project", Project); ("local", Local) ]
1616+1717+ type t = {
1818+ allowed_tools : string list;
1919+ disallowed_tools : string list;
2020+ max_thinking_tokens : int option;
2121+ system_prompt : string option;
2222+ append_system_prompt : string option;
2323+ permission_mode : Permissions.Mode.t option;
2424+ model : Model.t option;
2525+ continue_conversation : bool;
2626+ resume : string option;
2727+ max_turns : int option;
2828+ permission_prompt_tool_name : string option;
2929+ settings : string option;
3030+ add_dirs : string list;
3131+ max_budget_usd : float option;
3232+ fallback_model : Model.t option;
3333+ setting_sources : setting_source list option;
3434+ max_buffer_size : int option;
3535+ user : string option;
3636+ output_format : Structured_output.t option;
3737+ unknown : Unknown.t;
3838+ }
3939+4040+ let empty =
4141+ {
4242+ allowed_tools = [];
4343+ disallowed_tools = [];
4444+ max_thinking_tokens = None;
4545+ system_prompt = None;
4646+ append_system_prompt = None;
4747+ permission_mode = None;
4848+ model = None;
4949+ continue_conversation = false;
5050+ resume = None;
5151+ max_turns = None;
5252+ permission_prompt_tool_name = None;
5353+ settings = None;
5454+ add_dirs = [];
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 allowed_tools t = t.allowed_tools
6565+ let disallowed_tools t = t.disallowed_tools
6666+ let max_thinking_tokens t = t.max_thinking_tokens
6767+ let system_prompt t = t.system_prompt
6868+ let append_system_prompt t = t.append_system_prompt
6969+ let permission_mode t = t.permission_mode
7070+ let model t = t.model
7171+ let continue_conversation t = t.continue_conversation
7272+ let resume t = t.resume
7373+ let max_turns t = t.max_turns
7474+ let permission_prompt_tool_name t = t.permission_prompt_tool_name
7575+ let settings t = t.settings
7676+ let add_dirs t = t.add_dirs
7777+ let max_budget_usd t = t.max_budget_usd
7878+ let fallback_model t = t.fallback_model
7979+ let setting_sources t = t.setting_sources
8080+ let max_buffer_size t = t.max_buffer_size
8181+ let user t = t.user
8282+ let output_format t = t.output_format
8383+ let unknown t = t.unknown
8484+ let with_allowed_tools allowed_tools t = { t with allowed_tools }
8585+ let with_disallowed_tools disallowed_tools t = { t with disallowed_tools }
8686+8787+ let with_max_thinking_tokens max_thinking_tokens t =
8888+ { t with max_thinking_tokens = Some max_thinking_tokens }
8989+9090+ let with_system_prompt system_prompt t =
9191+ { t with system_prompt = Some system_prompt }
9292+9393+ let with_append_system_prompt append_system_prompt t =
9494+ { t with append_system_prompt = Some append_system_prompt }
9595+9696+ let with_permission_mode permission_mode t =
9797+ { t with permission_mode = Some permission_mode }
9898+9999+ let with_model model t = { t with model = Some model }
100100+101101+ let with_continue_conversation continue_conversation t =
102102+ { t with continue_conversation }
103103+104104+ let with_resume resume t = { t with resume = Some resume }
105105+ let with_max_turns max_turns t = { t with max_turns = Some max_turns }
106106+107107+ let with_permission_prompt_tool_name permission_prompt_tool_name t =
108108+ { t with permission_prompt_tool_name = Some permission_prompt_tool_name }
109109+110110+ let with_settings settings t = { t with settings = Some settings }
111111+ let with_add_dirs add_dirs t = { t with add_dirs }
112112+113113+ let with_max_budget_usd max_budget_usd t =
114114+ { t with max_budget_usd = Some max_budget_usd }
115115+116116+ let with_fallback_model fallback_model t =
117117+ { t with fallback_model = Some fallback_model }
118118+119119+ let with_setting_sources setting_sources t =
120120+ { t with setting_sources = Some setting_sources }
121121+122122+ let with_max_buffer_size max_buffer_size t =
123123+ { t with max_buffer_size = Some max_buffer_size }
124124+125125+ let with_user user t = { t with user = Some user }
126126+127127+ let with_output_format output_format t =
128128+ { t with output_format = Some output_format }
129129+130130+ let json : t Json.codec =
131131+ let make allowed_tools disallowed_tools max_thinking_tokens system_prompt
132132+ append_system_prompt permission_mode model continue_conversation resume
133133+ max_turns permission_prompt_tool_name settings add_dirs max_budget_usd
134134+ fallback_model setting_sources max_buffer_size user output_format
135135+ unknown =
136136+ {
137137+ allowed_tools;
138138+ disallowed_tools;
139139+ max_thinking_tokens;
140140+ system_prompt;
141141+ append_system_prompt;
142142+ permission_mode;
143143+ model;
144144+ continue_conversation;
145145+ resume;
146146+ max_turns;
147147+ permission_prompt_tool_name;
148148+ settings;
149149+ add_dirs;
150150+ max_budget_usd;
151151+ fallback_model;
152152+ setting_sources;
153153+ max_buffer_size;
154154+ user;
155155+ output_format;
156156+ unknown;
157157+ }
158158+ in
159159+ let open Json.Codec in
160160+ Object.map ~kind:"Options" make
161161+ |> Object.member "allowedTools" (list string) ~enc:allowed_tools
162162+ ~dec_absent:[]
163163+ |> Object.member "disallowedTools" (list string) ~enc:disallowed_tools
164164+ ~dec_absent:[]
165165+ |> Object.opt_member "maxThinkingTokens" int ~enc:max_thinking_tokens
166166+ |> Object.opt_member "systemPrompt" string ~enc:system_prompt
167167+ |> Object.opt_member "appendSystemPrompt" string ~enc:append_system_prompt
168168+ |> Object.opt_member "permissionMode" Permissions.Mode.json
169169+ ~enc:permission_mode
170170+ |> Object.opt_member "model" Model.json ~enc:model
171171+ |> Object.member "continueConversation" bool ~enc:continue_conversation
172172+ ~dec_absent:false
173173+ |> Object.opt_member "resume" string ~enc:resume
174174+ |> Object.opt_member "maxTurns" int ~enc:max_turns
175175+ |> Object.opt_member "permissionPromptToolName" string
176176+ ~enc:permission_prompt_tool_name
177177+ |> Object.opt_member "settings" string ~enc:settings
178178+ |> Object.member "addDirs" (list string) ~enc:add_dirs ~dec_absent:[]
179179+ |> Object.opt_member "maxBudgetUsd" number ~enc:max_budget_usd
180180+ |> Object.opt_member "fallbackModel" Model.json ~enc:fallback_model
181181+ |> Object.opt_member "settingSources"
182182+ (list setting_source_jsont)
183183+ ~enc:setting_sources
184184+ |> Object.opt_member "maxBufferSize" int ~enc:max_buffer_size
185185+ |> Object.opt_member "user" string ~enc:user
186186+ |> Object.opt_member "outputFormat" Structured_output.json
187187+ ~enc:output_format
188188+ |> Object.keep_unknown Unknown.mems ~enc:unknown
189189+ |> Object.seal
190190+191191+ let pp ppf t = Json.pp_value json ppf t
192192+end
193193+10194type t = {
11195 allowed_tools : string list;
12196 disallowed_tools : string list;
···15199 append_system_prompt : string option;
16200 permission_mode : Permissions.Mode.t option;
17201 permission_callback : Permissions.callback option;
1818- model : Proto.Model.t option;
202202+ model : Model.t option;
19203 cwd : Eio.Fs.dir_ty Eio.Path.t option;
20204 env : (string * string) list;
21205 continue_conversation : bool;
···28212 debug_stderr : Eio.Flow.sink_ty Eio.Flow.sink option;
29213 hooks : Hooks.t option;
30214 max_budget_usd : float option;
3131- fallback_model : Proto.Model.t option;
3232- setting_sources : Proto.Options.setting_source list option;
215215+ fallback_model : Model.t option;
216216+ setting_sources : Wire.setting_source list option;
33217 max_buffer_size : int option;
34218 user : string option;
3535- output_format : Proto.Structured_output.t option;
219219+ output_format : Structured_output.t option;
36220 mcp_servers : (string * Mcp_server.t) list;
37221}
38222···142326let log_options t =
143327 Log.debug (fun m ->
144328 m "Options: model=%s fallback=%s max_thinking_tokens=%d max_budget=%s"
145145- (match t.model with
146146- | None -> "default"
147147- | Some m -> Proto.Model.to_string m)
329329+ (match t.model with None -> "default" | Some m -> Model.to_string m)
148330 (match t.fallback_model with
149331 | None -> "none"
150150- | Some m -> Proto.Model.to_string m)
332332+ | Some m -> Model.to_string m)
151333 t.max_thinking_tokens
152334 (match t.max_budget_usd with
153335 | None -> "unlimited"
154154- | Some b -> Printf.sprintf "$%.2f" b))
336336+ | Some b -> Fmt.str "$%.2f" b))
155337156338module Advanced = struct
157157- let to_wire (t : t) : Proto.Options.t =
158158- let base = Proto.Options.empty in
159159- let base = Proto.Options.with_allowed_tools t.allowed_tools base in
160160- let base = Proto.Options.with_disallowed_tools t.disallowed_tools base in
161161- let base =
162162- Proto.Options.with_max_thinking_tokens t.max_thinking_tokens base
163163- in
164164- let base =
165165- match t.system_prompt with
166166- | None -> base
167167- | Some p -> Proto.Options.with_system_prompt p base
168168- in
169169- let base =
170170- match t.append_system_prompt with
171171- | None -> base
172172- | Some p -> Proto.Options.with_append_system_prompt p base
173173- in
174174- let base =
175175- match t.permission_mode with
176176- | None -> base
177177- | Some m ->
178178- Proto.Options.with_permission_mode (Permissions.Mode.to_proto m) base
179179- in
180180- let base =
181181- match t.model with
182182- | None -> base
183183- | Some m -> Proto.Options.with_model m base
184184- in
185185- let base =
186186- Proto.Options.with_continue_conversation t.continue_conversation base
187187- in
188188- let base =
189189- match t.resume with
190190- | None -> base
191191- | Some r -> Proto.Options.with_resume r base
192192- in
193193- let base =
194194- match t.max_turns with
195195- | None -> base
196196- | Some turns -> Proto.Options.with_max_turns turns base
197197- in
198198- let base =
199199- match t.permission_prompt_tool_name with
200200- | None -> base
201201- | Some tool -> Proto.Options.with_permission_prompt_tool_name tool base
202202- in
203203- let base =
204204- match t.settings with
205205- | None -> base
206206- | Some s -> Proto.Options.with_settings s base
207207- in
208208- let base = Proto.Options.with_add_dirs t.add_dirs base in
209209- let base =
210210- match t.max_budget_usd with
211211- | None -> base
212212- | Some b -> Proto.Options.with_max_budget_usd b base
213213- in
214214- let base =
215215- match t.fallback_model with
216216- | None -> base
217217- | Some m -> Proto.Options.with_fallback_model m base
218218- in
219219- let base =
220220- match t.setting_sources with
221221- | None -> base
222222- | Some sources -> Proto.Options.with_setting_sources sources base
223223- in
224224- let base =
225225- match t.max_buffer_size with
226226- | None -> base
227227- | Some size -> Proto.Options.with_max_buffer_size size base
228228- in
229229- let base =
230230- match t.user with
231231- | None -> base
232232- | Some u -> Proto.Options.with_user u base
233233- in
234234- let base =
235235- match t.output_format with
236236- | None -> base
237237- | Some format -> Proto.Options.with_output_format format base
238238- in
239239- base
339339+ let apply_opt opt f base = match opt with None -> base | Some v -> f v base
340340+341341+ let to_wire (t : t) : Wire.t =
342342+ Wire.empty
343343+ |> Wire.with_allowed_tools t.allowed_tools
344344+ |> Wire.with_disallowed_tools t.disallowed_tools
345345+ |> Wire.with_max_thinking_tokens t.max_thinking_tokens
346346+ |> apply_opt t.system_prompt Wire.with_system_prompt
347347+ |> apply_opt t.append_system_prompt Wire.with_append_system_prompt
348348+ |> apply_opt t.permission_mode Wire.with_permission_mode
349349+ |> apply_opt t.model Wire.with_model
350350+ |> Wire.with_continue_conversation t.continue_conversation
351351+ |> apply_opt t.resume Wire.with_resume
352352+ |> apply_opt t.max_turns Wire.with_max_turns
353353+ |> apply_opt t.permission_prompt_tool_name
354354+ Wire.with_permission_prompt_tool_name
355355+ |> apply_opt t.settings Wire.with_settings
356356+ |> Wire.with_add_dirs t.add_dirs
357357+ |> apply_opt t.max_budget_usd Wire.with_max_budget_usd
358358+ |> apply_opt t.fallback_model Wire.with_fallback_model
359359+ |> apply_opt t.setting_sources Wire.with_setting_sources
360360+ |> apply_opt t.max_buffer_size Wire.with_max_buffer_size
361361+ |> apply_opt t.user Wire.with_user
362362+ |> apply_opt t.output_format Wire.with_output_format
240363end
364364+365365+let pp ppf t = Json.pp_value Wire.json ppf (Advanced.to_wire t)
+90-30
lib/options.mli
···2626 new options value with the specified field updated:
27272828 {[
2929- let options =
3030- Options.default
3131- |> Options.with_model `Sonnet_4_5
3232- |> Options.with_max_budget_usd 1.0
3333- |> Options.with_permission_mode Permissions.Mode.Accept_edits
2929+ let options =
3030+ Options.default
3131+ |> Options.with_model `Sonnet_4_5
3232+ |> Options.with_max_budget_usd 1.0
3333+ |> Options.with_permission_mode Permissions.Mode.Accept_edits
3434 ]}
35353636 {2 Common Configuration Scenarios}
···3838 {3 CI/CD: Isolated, Reproducible Builds}
39394040 {[
4141- let ci_config =
4242- Options.default |> Options.with_no_settings (* Ignore user config *)
4343- |> Options.with_max_budget_usd 0.50 (* 50 cent limit *)
4444- |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
4545- |> Options.with_model `Haiku_4
4141+ let ci_config =
4242+ Options.default |> Options.with_no_settings (* Ignore user config *)
4343+ |> Options.with_max_budget_usd 0.50 (* 50 cent limit *)
4444+ |> Options.with_permission_mode Permissions.Mode.Bypass_permissions
4545+ |> Options.with_model `Haiku_4
4646 ]}
47474848 {3 Production: Cost Control with Fallback}
49495050 {[
5151- let prod_config =
5252- Options.default
5353- |> Options.with_model `Sonnet_4_5
5454- |> Options.with_fallback_model `Haiku_4
5555- |> Options.with_max_budget_usd 10.0 (* $10 daily limit *)
5656- |> Options.with_max_buffer_size 5_000_000
5151+ let prod_config =
5252+ Options.default
5353+ |> Options.with_model `Sonnet_4_5
5454+ |> Options.with_fallback_model `Haiku_4
5555+ |> Options.with_max_budget_usd 10.0 (* $10 daily limit *)
5656+ |> Options.with_max_buffer_size 5_000_000
5757 ]}
58585959 {3 Development: User Settings with Overrides}
60606161 {[
6262- let dev_config =
6363- Options.default
6464- |> Options.with_max_budget_usd 1.0
6565- |> Options.with_permission_mode Permissions.Mode.Default
6262+ let dev_config =
6363+ Options.default
6464+ |> Options.with_max_budget_usd 1.0
6565+ |> Options.with_permission_mode Permissions.Mode.Default
6666 ]}
67676868 {2 Advanced Options}
···8383 model is unavailable or overloaded. This improves reliability. *)
84848585val src : Logs.Src.t
8686-(** The log source for options operations *)
8686+(** The log source for options operations. *)
8787+8888+(** {1 Wire Format} *)
8989+9090+(** Wire-format encoding for options serialised in JSON config files.
9191+9292+ Field names use camelCase. Unknown fields are preserved for forward
9393+ compatibility. *)
9494+module Wire : sig
9595+ type setting_source = User | Project | Local
9696+9797+ val setting_source_jsont : setting_source Json.codec
9898+9999+ type t
100100+101101+ val empty : t
102102+ val pp : Format.formatter -> t -> unit
103103+ val json : t Json.codec
104104+ val allowed_tools : t -> string list
105105+ val disallowed_tools : t -> string list
106106+ val max_thinking_tokens : t -> int option
107107+ val system_prompt : t -> string option
108108+ val append_system_prompt : t -> string option
109109+ val permission_mode : t -> Permissions.Mode.t option
110110+ val model : t -> Model.t option
111111+ val continue_conversation : t -> bool
112112+ val resume : t -> string option
113113+ val max_turns : t -> int option
114114+ val permission_prompt_tool_name : t -> string option
115115+ val settings : t -> string option
116116+ val add_dirs : t -> string list
117117+ val max_budget_usd : t -> float option
118118+ val fallback_model : t -> Model.t option
119119+ val setting_sources : t -> setting_source list option
120120+ val max_buffer_size : t -> int option
121121+ val user : t -> string option
122122+ val output_format : t -> Structured_output.t option
123123+ val unknown : t -> Unknown.t
124124+ val with_allowed_tools : string list -> t -> t
125125+ val with_disallowed_tools : string list -> t -> t
126126+ val with_max_thinking_tokens : int -> t -> t
127127+ val with_system_prompt : string -> t -> t
128128+ val with_append_system_prompt : string -> t -> t
129129+ val with_permission_mode : Permissions.Mode.t -> t -> t
130130+ val with_model : Model.t -> t -> t
131131+ val with_continue_conversation : bool -> t -> t
132132+ val with_resume : string -> t -> t
133133+ val with_max_turns : int -> t -> t
134134+ val with_permission_prompt_tool_name : string -> t -> t
135135+ val with_settings : string -> t -> t
136136+ val with_add_dirs : string list -> t -> t
137137+ val with_max_budget_usd : float -> t -> t
138138+ val with_fallback_model : Model.t -> t -> t
139139+ val with_setting_sources : setting_source list -> t -> t
140140+ val with_max_buffer_size : int -> t -> t
141141+ val with_user : string -> t -> t
142142+ val with_output_format : Structured_output.t -> t -> t
143143+end
8714488145(** {1 Types} *)
8914690147type t
91148(** The type of configuration options. *)
92149150150+val pp : Format.formatter -> t -> unit
151151+(** [pp ppf t] pretty-prints the options configuration. *)
152152+93153val default : t
94154(** [default] returns the default configuration with sensible defaults:
95155 - No tool restrictions
96156 - 8000 max thinking tokens
97157 - Default allow permission callback
9898- - No custom prompts or model override *)
158158+ - No custom prompts or model override. *)
99159100160(** {1 Builder Pattern} *)
101161···120180val with_permission_callback : Permissions.callback -> t -> t
121181(** [with_permission_callback callback t] sets the permission callback. *)
122182123123-val with_model : Proto.Model.t -> t -> t
183183+val with_model : Model.t -> t -> t
124184(** [with_model model t] sets the model override using a typed Model.t. *)
125185126186val with_cwd : [> Eio.Fs.dir_ty ] Eio.Path.t -> t -> t
···159219(** [with_max_budget_usd budget t] sets the maximum spending limit in USD. The
160220 session will terminate if this limit is exceeded. *)
161221162162-val with_fallback_model : Proto.Model.t -> t -> t
222222+val with_fallback_model : Model.t -> t -> t
163223(** [with_fallback_model model t] sets the fallback model using a typed Model.t.
164224*)
165225···175235val with_user : string -> t -> t
176236(** [with_user user t] sets the Unix user for subprocess execution. *)
177237178178-val with_output_format : Proto.Structured_output.t -> t -> t
238238+val with_output_format : Structured_output.t -> t -> t
179239(** [with_output_format format t] sets the structured output format. *)
180240181241val with_extra_args : (string * string option) list -> t -> t
···210270val permission_callback : t -> Permissions.callback option
211271(** [permission_callback t] returns the optional permission callback. *)
212272213213-val model : t -> Proto.Model.t option
273273+val model : t -> Model.t option
214274(** [model t] returns the optional model override. *)
215275216276val cwd : t -> Eio.Fs.dir_ty Eio.Path.t option
···248308val max_budget_usd : t -> float option
249309(** [max_budget_usd t] returns the optional spending limit in USD. *)
250310251251-val fallback_model : t -> Proto.Model.t option
311311+val fallback_model : t -> Model.t option
252312(** [fallback_model t] returns the optional fallback model. *)
253313254254-val setting_sources : t -> Proto.Options.setting_source list option
314314+val setting_sources : t -> Wire.setting_source list option
255315(** [setting_sources t] returns the optional list of setting sources to load. *)
256316257317val max_buffer_size : t -> int option
···260320val user : t -> string option
261321(** [user t] returns the optional Unix user for subprocess execution. *)
262322263263-val output_format : t -> Proto.Structured_output.t option
323323+val output_format : t -> Structured_output.t option
264324(** [output_format t] returns the optional structured output format. *)
265325266326val extra_args : t -> (string * string option) list
···277337(** {1 Advanced: Wire Format Conversion} *)
278338279339module Advanced : sig
280280- val to_wire : t -> Proto.Options.t
340340+ val to_wire : t -> Wire.t
281341 (** [to_wire t] converts to wire format (excludes Eio types and callbacks).
282342 This is used internally by the client to send options to the Claude CLI.
283343 *)
+69
lib/outgoing.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t =
77+ | Message of Message.t
88+ | Control_request of Control.control_request
99+ | Control_response of Control.control_response
1010+1111+let json : t Json.codec =
1212+ let open Json.Codec in
1313+ let case_control_request =
1414+ Object.Case.map "control_request" Control.control_request_jsont
1515+ ~dec:(fun v -> Control_request v)
1616+ in
1717+ let case_control_response =
1818+ Object.Case.map "control_response" Control.control_response_jsont
1919+ ~dec:(fun v -> Control_response v)
2020+ in
2121+ let case_user =
2222+ Object.Case.map "user" Message.User.outgoing_jsont ~dec:(fun v ->
2323+ Message (Message.User v))
2424+ in
2525+ let case_assistant =
2626+ Object.Case.map "assistant" Message.Assistant.json ~dec:(fun v ->
2727+ Message (Message.Assistant v))
2828+ in
2929+ let case_system =
3030+ Object.Case.map "system" Message.System.json ~dec:(fun v ->
3131+ Message (Message.System v))
3232+ in
3333+ let case_result =
3434+ Object.Case.map "result" Message.Result.json ~dec:(fun v ->
3535+ Message (Message.Result v))
3636+ in
3737+ let enc_case = function
3838+ | Control_request v -> Object.Case.value case_control_request v
3939+ | Control_response v -> Object.Case.value case_control_response v
4040+ | Message msg -> (
4141+ match msg with
4242+ | Message.User u -> Object.Case.value case_user u
4343+ | Message.Assistant a -> Object.Case.value case_assistant a
4444+ | Message.System s -> Object.Case.value case_system s
4545+ | Message.Result r -> Object.Case.value case_result r)
4646+ in
4747+ let cases =
4848+ Object.Case.
4949+ [
5050+ make case_control_request;
5151+ make case_control_response;
5252+ make case_user;
5353+ make case_assistant;
5454+ make case_system;
5555+ make case_result;
5656+ ]
5757+ in
5858+ Object.map ~kind:"Outgoing" Fun.id
5959+ |> Object.case_member "type" string ~enc:Fun.id ~enc_case cases
6060+ ~tag_to_string:Fun.id ~tag_compare:String.compare
6161+ |> Object.seal
6262+6363+let pp ppf t = Json.pp_value json ppf t
6464+let to_json t = Json.encode json t
6565+6666+let of_json v =
6767+ match Json.decode json v with
6868+ | Ok v -> v
6969+ | Error e -> invalid_arg ("of_json: " ^ Json.Error.to_string e)
+24
lib/outgoing.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Outgoing messages to the Claude CLI. *)
77+88+type t =
99+ | Message of Message.t
1010+ | Control_request of Control.control_request
1111+ | Control_response of Control.control_response
1212+1313+val json : t Json.codec
1414+(** Codec for outgoing messages. *)
1515+1616+val pp : Format.formatter -> t -> unit
1717+(** [pp ppf t] pretty-prints the outgoing message. *)
1818+1919+val to_json : t -> Json.t
2020+(** [to_json t] converts an outgoing message to JSON. *)
2121+2222+val of_json : Json.t -> t
2323+(** [of_json json] parses an outgoing message from JSON.
2424+ @raise Invalid_argument if parsing fails. *)
+202-37
lib/permissions.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566+open Json.Codec
77+68let src = Logs.Src.create "claude.permission" ~doc:"Claude permission system"
79810module Log = (val Logs.src_log src : Logs.LOG)
9111010-(** Permission modes *)
1112module Mode = struct
1213 type t = Default | Accept_edits | Plan | Bypass_permissions
1314···2324 | "plan" -> Plan
2425 | "bypassPermissions" -> Bypass_permissions
2526 | s ->
2626- raise
2727- (Invalid_argument (Printf.sprintf "Mode.of_string: unknown mode %s" s))
2727+ raise (Invalid_argument (Fmt.str "Mode.of_string: unknown mode %s" s))
28282929- let of_proto : Proto.Permissions.Mode.t -> t = function
3030- | Proto.Permissions.Mode.Default -> Default
3131- | Proto.Permissions.Mode.Accept_edits -> Accept_edits
3232- | Proto.Permissions.Mode.Plan -> Plan
3333- | Proto.Permissions.Mode.Bypass_permissions -> Bypass_permissions
2929+ let json : t Json.codec =
3030+ enum
3131+ [
3232+ ("default", Default);
3333+ ("acceptEdits", Accept_edits);
3434+ ("plan", Plan);
3535+ ("bypassPermissions", Bypass_permissions);
3636+ ]
3737+end
3838+3939+module Behavior = struct
4040+ type t = Allow | Deny | Ask
34413535- let to_proto : t -> Proto.Permissions.Mode.t = function
3636- | Default -> Proto.Permissions.Mode.Default
3737- | Accept_edits -> Proto.Permissions.Mode.Accept_edits
3838- | Plan -> Proto.Permissions.Mode.Plan
3939- | Bypass_permissions -> Proto.Permissions.Mode.Bypass_permissions
4242+ let to_string = function Allow -> "allow" | Deny -> "deny" | Ask -> "ask"
4343+4444+ let of_string = function
4545+ | "allow" -> Allow
4646+ | "deny" -> Deny
4747+ | "ask" -> Ask
4848+ | s ->
4949+ raise
5050+ (Invalid_argument
5151+ (Fmt.str "Behavior.of_string: unknown behavior %s" s))
5252+5353+ let json : t Json.codec =
5454+ enum [ ("allow", Allow); ("deny", Deny); ("ask", Ask) ]
4055end
41564242-(** Permission rules *)
4357module Rule = struct
4444- type t = { tool_name : string; rule_content : string option }
5858+ type t = {
5959+ tool_name : string;
6060+ rule_content : string option;
6161+ unknown : Unknown.t;
6262+ }
45634646- let create ~tool_name ?rule_content () = { tool_name; rule_content }
6464+ let create ~tool_name ?rule_content ?(unknown = Unknown.empty) () =
6565+ { tool_name; rule_content; unknown }
6666+4767 let tool_name t = t.tool_name
4868 let rule_content t = t.rule_content
6969+ let unknown t = t.unknown
49705050- let of_proto (proto : Proto.Permissions.Rule.t) : t =
5151- {
5252- tool_name = Proto.Permissions.Rule.tool_name proto;
5353- rule_content = Proto.Permissions.Rule.rule_content proto;
5454- }
7171+ let json : t Json.codec =
7272+ let make tool_name rule_content unknown =
7373+ { tool_name; rule_content; unknown }
7474+ in
7575+ Object.map ~kind:"Rule" make
7676+ |> Object.member "toolName" string ~enc:tool_name
7777+ |> Object.opt_member "ruleContent" string ~enc:rule_content
7878+ |> Object.keep_unknown Unknown.mems ~enc:unknown
7979+ |> Object.seal
8080+end
8181+8282+module Update = struct
8383+ type destination =
8484+ | User_settings
8585+ | Project_settings
8686+ | Local_settings
8787+ | Session
8888+8989+ let destination_jsont : destination Json.codec =
9090+ enum
9191+ [
9292+ ("userSettings", User_settings);
9393+ ("projectSettings", Project_settings);
9494+ ("localSettings", Local_settings);
9595+ ("session", Session);
9696+ ]
9797+9898+ type update_type =
9999+ | Add_rules
100100+ | Replace_rules
101101+ | Remove_rules
102102+ | Set_mode
103103+ | Add_directories
104104+ | Remove_directories
105105+106106+ let update_type_jsont : update_type Json.codec =
107107+ enum
108108+ [
109109+ ("addRules", Add_rules);
110110+ ("replaceRules", Replace_rules);
111111+ ("removeRules", Remove_rules);
112112+ ("setMode", Set_mode);
113113+ ("addDirectories", Add_directories);
114114+ ("removeDirectories", Remove_directories);
115115+ ]
116116+117117+ type t = {
118118+ update_type : update_type;
119119+ rules : Rule.t list option;
120120+ behavior : Behavior.t option;
121121+ mode : Mode.t option;
122122+ directories : string list option;
123123+ destination : destination option;
124124+ unknown : Unknown.t;
125125+ }
126126+127127+ let create ~update_type ?rules ?behavior ?mode ?directories ?destination
128128+ ?(unknown = Unknown.empty) () =
129129+ { update_type; rules; behavior; mode; directories; destination; unknown }
130130+131131+ let update_type t = t.update_type
132132+ let rules t = t.rules
133133+ let behavior t = t.behavior
134134+ let mode t = t.mode
135135+ let directories t = t.directories
136136+ let destination t = t.destination
137137+ let unknown t = t.unknown
551385656- let to_proto (t : t) : Proto.Permissions.Rule.t =
5757- Proto.Permissions.Rule.create ~tool_name:t.tool_name
5858- ?rule_content:t.rule_content ()
139139+ let json : t Json.codec =
140140+ let make update_type rules behavior mode directories destination unknown =
141141+ { update_type; rules; behavior; mode; directories; destination; unknown }
142142+ in
143143+ Object.map ~kind:"Update" make
144144+ |> Object.member "type" update_type_jsont ~enc:update_type
145145+ |> Object.opt_member "rules" (list Rule.json) ~enc:rules
146146+ |> Object.opt_member "behavior" Behavior.json ~enc:behavior
147147+ |> Object.opt_member "mode" Mode.json ~enc:mode
148148+ |> Object.opt_member "directories" (list string) ~enc:directories
149149+ |> Object.opt_member "destination" destination_jsont ~enc:destination
150150+ |> Object.keep_unknown Unknown.mems ~enc:unknown
151151+ |> Object.seal
152152+end
153153+154154+module Context = struct
155155+ type t = { suggestions : Update.t list; unknown : Unknown.t }
156156+157157+ let create ?(suggestions = []) ?(unknown = Unknown.empty) () =
158158+ { suggestions; unknown }
159159+160160+ let suggestions t = t.suggestions
161161+ let unknown t = t.unknown
162162+163163+ let json : t Json.codec =
164164+ let make suggestions unknown = { suggestions; unknown } in
165165+ Object.map ~kind:"Context" make
166166+ |> Object.member "suggestions" (list Update.json) ~enc:suggestions
167167+ ~dec_absent:[]
168168+ |> Object.keep_unknown Unknown.mems ~enc:unknown
169169+ |> Object.seal
170170+end
171171+172172+module Result = struct
173173+ type t =
174174+ | Allow of {
175175+ updated_input : Json.t option;
176176+ updated_permissions : Update.t list option;
177177+ unknown : Unknown.t;
178178+ }
179179+ | Deny of { message : string; interrupt : bool; unknown : Unknown.t }
180180+181181+ let allow ?updated_input ?updated_permissions ?(unknown = Unknown.empty) () =
182182+ Allow { updated_input; updated_permissions; unknown }
183183+184184+ let deny ~message ~interrupt ?(unknown = Unknown.empty) () =
185185+ Deny { message; interrupt; unknown }
186186+187187+ let json : t Json.codec =
188188+ let allow_record =
189189+ let make updated_input updated_permissions unknown =
190190+ Allow { updated_input; updated_permissions; unknown }
191191+ in
192192+ Object.map ~kind:"AllowRecord" make
193193+ |> Object.member "updatedInput" (option Value.t)
194194+ ~enc:(function
195195+ | Allow { updated_input; _ } -> updated_input | _ -> None)
196196+ ~dec_absent:None
197197+ |> Object.opt_member "updatedPermissions" (list Update.json)
198198+ ~enc:(function
199199+ | Allow { updated_permissions; _ } -> updated_permissions
200200+ | _ -> None)
201201+ |> Object.keep_unknown Unknown.mems ~enc:(function
202202+ | Allow { unknown; _ } -> unknown
203203+ | _ -> Unknown.empty)
204204+ |> Object.seal
205205+ in
206206+ let deny_record =
207207+ let make message interrupt unknown =
208208+ Deny { message; interrupt; unknown }
209209+ in
210210+ Object.map ~kind:"DenyRecord" make
211211+ |> Object.member "message" string ~enc:(function
212212+ | Deny { message; _ } -> message
213213+ | _ -> "")
214214+ |> Object.member "interrupt" bool ~enc:(function
215215+ | Deny { interrupt; _ } -> interrupt
216216+ | _ -> false)
217217+ |> Object.keep_unknown Unknown.mems ~enc:(function
218218+ | Deny { unknown; _ } -> unknown
219219+ | _ -> Unknown.empty)
220220+ |> Object.seal
221221+ in
222222+ let case_allow = Object.Case.map "allow" allow_record ~dec:(fun v -> v) in
223223+ let case_deny = Object.Case.map "deny" deny_record ~dec:(fun v -> v) in
224224+ let enc_case = function
225225+ | Allow _ as v -> Object.Case.value case_allow v
226226+ | Deny _ as v -> Object.Case.value case_deny v
227227+ in
228228+ let cases = Object.Case.[ make case_allow; make case_deny ] in
229229+ Object.map ~kind:"Result" Fun.id
230230+ |> Object.case_member "behavior" string ~enc:Fun.id ~enc_case cases
231231+ ~tag_to_string:Fun.id ~tag_compare:String.compare
232232+ |> Object.seal
59233end
602346161-(** Permission decisions *)
62235module Decision = struct
63236 type t =
64237 | Allow of { updated_input : Tool_input.t option }
···81254 | Allow _ -> false
82255 | Deny { interrupt; _ } -> interrupt
832568484- let to_proto_result ~original_input (t : t) : Proto.Permissions.Result.t =
257257+ let to_proto_result ~original_input (t : t) : Result.t =
85258 match t with
86259 | Allow { updated_input } ->
87260 let updated_input_json =
88261 match updated_input with
89262 | Some input -> Some (Tool_input.to_json input)
90263 | None -> Some (Tool_input.to_json original_input)
9191- (* Return original when not modified *)
92264 in
9393- Proto.Permissions.Result.allow ?updated_input:updated_input_json ()
9494- | Deny { message; interrupt } ->
9595- Proto.Permissions.Result.deny ~message ~interrupt ()
265265+ Result.allow ?updated_input:updated_input_json ()
266266+ | Deny { message; interrupt } -> Result.deny ~message ~interrupt ()
96267end
9726898269type context = {
···100271 input : Tool_input.t;
101272 suggested_rules : Rule.t list;
102273}
103103-(** Permission context *)
104274105275let extract_rules_from_proto_updates updates =
106276 List.concat_map
107277 (fun update ->
108108- match Proto.Permissions.Update.rules update with
109109- | Some rules -> List.map Rule.of_proto rules
110110- | None -> [])
278278+ match Update.rules update with Some rules -> rules | None -> [])
111279 updates
112280113281type callback = context -> Decision.t
114114-(** Permission callback type *)
115282116116-(** Default callbacks *)
117283let default_allow _ctx = Decision.allow ()
118284119285let discovery log ctx =
120286 List.iter (fun rule -> log := rule :: !log) ctx.suggested_rules;
121287 Decision.allow ()
122288123123-(** Logging *)
124289let log_permission_check ~tool_name ~decision =
125290 match decision with
126291 | Decision.Allow _ ->
+97-86
lib/permissions.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Permission control for tool usage.
77-88- This module provides a permission system for controlling which tools Claude
99- can invoke and how they can be used. It includes support for permission
1010- modes, rules, decisions, and callbacks. *)
66+(** Permission control for tool usage. *)
117128val src : Logs.Src.t
1313-(** The log source for permission operations. *)
1491510(** {1 Permission Modes} *)
16111712module Mode : sig
1818- (** Permission modes control the overall behavior of the permission system. *)
1919-2020- (** The type of permission modes. *)
2121- type t =
2222- | Default (** Standard permission mode with normal checks *)
2323- | Accept_edits (** Automatically accept file edits *)
2424- | Plan (** Planning mode with restricted execution *)
2525- | Bypass_permissions (** Bypass all permission checks *)
1313+ type t = Default | Accept_edits | Plan | Bypass_permissions
26142715 val to_string : t -> string
2828- (** [to_string t] converts a mode to its string representation. *)
2929-3016 val of_string : string -> t
3131- (** [of_string s] parses a mode from its string representation.
3232- @raise Invalid_argument if the string is not a valid mode. *)
1717+ val json : t Json.codec
1818+end
33193434- val of_proto : Proto.Permissions.Mode.t -> t
3535- (** [of_proto proto] converts from the protocol representation. *)
2020+(** {1 Behaviors} *)
36213737- val to_proto : t -> Proto.Permissions.Mode.t
3838- (** [to_proto t] converts to the protocol representation. *)
2222+module Behavior : sig
2323+ type t = Allow | Deny | Ask
2424+2525+ val to_string : t -> string
2626+ val of_string : string -> t
2727+ val json : t Json.codec
3928end
40294130(** {1 Permission Rules} *)
42314332module Rule : sig
4444- (** Rules define specific permissions for tools. *)
3333+ type t
3434+3535+ val create :
3636+ tool_name:string -> ?rule_content:string -> ?unknown:Unknown.t -> unit -> t
3737+3838+ val tool_name : t -> string
3939+ val rule_content : t -> string option
4040+ val unknown : t -> Unknown.t
4141+ val json : t Json.codec
4242+end
4343+4444+(** {1 Permission Updates} *)
4545+4646+module Update : sig
4747+ type destination =
4848+ | User_settings
4949+ | Project_settings
5050+ | Local_settings
5151+ | Session
5252+5353+ val destination_jsont : destination Json.codec
5454+5555+ type update_type =
5656+ | Add_rules
5757+ | Replace_rules
5858+ | Remove_rules
5959+ | Set_mode
6060+ | Add_directories
6161+ | Remove_directories
6262+6363+ val update_type_jsont : update_type Json.codec
45644665 type t
4747- (** The type of permission rules. *)
6666+6767+ val create :
6868+ update_type:update_type ->
6969+ ?rules:Rule.t list ->
7070+ ?behavior:Behavior.t ->
7171+ ?mode:Mode.t ->
7272+ ?directories:string list ->
7373+ ?destination:destination ->
7474+ ?unknown:Unknown.t ->
7575+ unit ->
7676+ t
7777+7878+ val update_type : t -> update_type
7979+ val rules : t -> Rule.t list option
8080+ val behavior : t -> Behavior.t option
8181+ val mode : t -> Mode.t option
8282+ val directories : t -> string list option
8383+ val destination : t -> destination option
8484+ val unknown : t -> Unknown.t
8585+ val json : t Json.codec
8686+end
8787+8888+(** {1 Wire-level Permission Context} *)
8989+9090+module Context : sig
9191+ type t
48924949- val create : tool_name:string -> ?rule_content:string -> unit -> t
5050- (** [create ~tool_name ?rule_content ()] creates a new rule.
5151- @param tool_name The name of the tool this rule applies to
5252- @param rule_content Optional rule specification or pattern *)
9393+ val create : ?suggestions:Update.t list -> ?unknown:Unknown.t -> unit -> t
9494+ val suggestions : t -> Update.t list
9595+ val unknown : t -> Unknown.t
9696+ val json : t Json.codec
9797+end
53985454- val tool_name : t -> string
5555- (** [tool_name t] returns the tool name. *)
9999+(** {1 Wire-level Permission Result} *)
561005757- val rule_content : t -> string option
5858- (** [rule_content t] returns the optional rule content. *)
101101+module Result : sig
102102+ type t =
103103+ | Allow of {
104104+ updated_input : Json.t option;
105105+ updated_permissions : Update.t list option;
106106+ unknown : Unknown.t;
107107+ }
108108+ | Deny of { message : string; interrupt : bool; unknown : Unknown.t }
591096060- val of_proto : Proto.Permissions.Rule.t -> t
6161- (** [of_proto proto] converts from the protocol representation. *)
110110+ val allow :
111111+ ?updated_input:Json.t ->
112112+ ?updated_permissions:Update.t list ->
113113+ ?unknown:Unknown.t ->
114114+ unit ->
115115+ t
621166363- val to_proto : t -> Proto.Permissions.Rule.t
6464- (** [to_proto t] converts to the protocol representation. *)
117117+ val deny : message:string -> interrupt:bool -> ?unknown:Unknown.t -> unit -> t
118118+ val json : t Json.codec
65119end
661206767-(** {1 Permission Decisions} *)
121121+(** {1 Permission Decisions (typed)} *)
6812269123module Decision : sig
7070- (** Decisions represent the outcome of a permission check. *)
7171-72124 type t
7373- (** The type of permission decisions. *)
7412575126 val allow : ?updated_input:Tool_input.t -> unit -> t
7676- (** [allow ?updated_input ()] creates an allow decision.
7777- @param updated_input Optional modified tool input *)
7878-79127 val deny : message:string -> interrupt:bool -> t
8080- (** [deny ~message ~interrupt] creates a deny decision.
8181- @param message The reason for denying permission
8282- @param interrupt Whether to interrupt further execution *)
8383-84128 val is_allow : t -> bool
8585- (** [is_allow t] returns true if the decision allows the operation. *)
8686-87129 val is_deny : t -> bool
8888- (** [is_deny t] returns true if the decision denies the operation. *)
8989-90130 val updated_input : t -> Tool_input.t option
9191- (** [updated_input t] returns the optional updated tool input if the decision
9292- is allow. *)
9393-94131 val deny_message : t -> string option
9595- (** [deny_message t] returns the denial message if the decision is deny. *)
9696-97132 val deny_interrupt : t -> bool
9898- (** [deny_interrupt t] returns whether to interrupt if the decision is deny.
9999- *)
100100-101101- val to_proto_result :
102102- original_input:Tool_input.t -> t -> Proto.Permissions.Result.t
103103- (** [to_proto_result ~original_input t] converts to the protocol result
104104- representation. When the decision allows without modification, the
105105- original_input is returned. *)
133133+ val to_proto_result : original_input:Tool_input.t -> t -> Result.t
106134end
107135108108-(** {1 Permission Context} *)
136136+(** {1 Permission Context (typed)} *)
109137110138type context = {
111111- tool_name : string; (** Name of the tool being invoked *)
112112- input : Tool_input.t; (** Tool input parameters *)
113113- suggested_rules : Rule.t list; (** Suggested permission rules *)
139139+ tool_name : string;
140140+ input : Tool_input.t;
141141+ suggested_rules : Rule.t list;
114142}
115115-(** The context provided to permission callbacks. *)
116143117117-val extract_rules_from_proto_updates :
118118- Proto.Permissions.Update.t list -> Rule.t list
119119-(** [extract_rules_from_proto_updates updates] extracts rules from protocol
120120- permission updates. Used internally to convert protocol suggestions into
121121- context rules. *)
144144+val extract_rules_from_proto_updates : Update.t list -> Rule.t list
122145123146(** {1 Permission Callbacks} *)
124147125148type callback = context -> Decision.t
126126-(** The type of permission callbacks. Callbacks are invoked when Claude attempts
127127- to use a tool, allowing custom permission logic.
128128-129129- The callback receives a typed context with the tool name, input, and
130130- suggested rules, and returns a decision to allow or deny the operation. *)
131149132150val default_allow : callback
133133-(** [default_allow] always allows tool invocations. *)
134134-135151val discovery : Rule.t list ref -> callback
136136-(** [discovery log] creates a callback that collects suggested rules into the
137137- provided reference while allowing all operations. Useful for discovering
138138- what permissions an operation requires. *)
139152140153(** {1 Logging} *)
141154142155val log_permission_check : tool_name:string -> decision:Decision.t -> unit
143143-(** [log_permission_check ~tool_name ~decision] logs a permission check result.
144144-*)
···125125 val result_text : t -> string option
126126 (** [result_text t] returns the optional result string. *)
127127128128- val structured_output : t -> Jsont.json option
128128+ val structured_output : t -> Json.t option
129129 (** [structured_output t] returns the optional structured JSON output. *)
130130131131 val of_result : Message.Result.t -> t
···144144 | Init of Init.t (** Session initialization *)
145145 | Error of Error.t (** Error event *)
146146 | Complete of Complete.t (** Session completion *)
147147+148148+val pp : Format.formatter -> t -> unit
149149+(** [pp ppf t] pretty-prints the response event. *)
147150148151(** {1 Conversion} *)
149152
+5-15
lib/server_info.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Server capabilities and metadata. *)
77-86type t = {
97 version : string;
108 capabilities : string list;
···2018let supports_hooks t = has_capability t "hooks"
2119let supports_structured_output t = has_capability t "structured-output"
22202323-let of_proto (proto : Proto.Control.Server_info.t) : t =
2424- {
2525- version = Proto.Control.Server_info.version proto;
2626- capabilities = Proto.Control.Server_info.capabilities proto;
2727- commands = Proto.Control.Server_info.commands proto;
2828- output_styles = Proto.Control.Server_info.output_styles proto;
2929- }
3030-3131-let of_sdk_control (sdk : Sdk_control.Server_info.t) : t =
2121+let of_control (c : Control.Server_info.t) : t =
3222 {
3333- version = Sdk_control.Server_info.version sdk;
3434- capabilities = Sdk_control.Server_info.capabilities sdk;
3535- commands = Sdk_control.Server_info.commands sdk;
3636- output_styles = Sdk_control.Server_info.output_styles sdk;
2323+ version = Control.Server_info.version c;
2424+ capabilities = Control.Server_info.capabilities c;
2525+ commands = Control.Server_info.commands c;
2626+ output_styles = Control.Server_info.output_styles c;
3727 }
+2-5
lib/server_info.mli
···41414242(** {1 Internal} *)
43434444-val of_proto : Proto.Control.Server_info.t -> t
4545-(** [of_proto proto] converts from the protocol representation. *)
4646-4747-val of_sdk_control : Sdk_control.Server_info.t -> t
4848-(** [of_sdk_control sdk] converts from the SDK control representation. *)
4444+val of_control : Control.Server_info.t -> t
4545+(** [of_control c] converts from the control protocol representation. *)
+16-17
lib/structured_output.ml
···7788module Log = (val Logs.src_log src : Logs.LOG)
991010-type t = { json_schema : Jsont.json }
1010+type t = { json_schema : Json.t }
11111212-let json_to_string json =
1313- match Jsont_bytesrw.encode_string' Jsont.json json with
1414- | Ok str -> str
1515- | Error err -> failwith (Jsont.Error.to_string err)
1212+let pp ppf t = Json.pp ppf t.json_schema
1313+let json_to_string json = Json.Value.to_string json
16141715let of_json_schema schema =
1816 Log.debug (fun m ->
···2018 { json_schema = schema }
21192220let json_schema t = t.json_schema
2121+let to_json_schema = json_schema
23222424-(* Codec for serializing structured output format *)
2525-let jsont : t Jsont.t =
2626- Jsont.Object.map ~kind:"StructuredOutput" (fun json_schema -> { json_schema })
2727- |> Jsont.Object.mem "jsonSchema" Jsont.json ~enc:(fun t -> t.json_schema)
2828- |> Jsont.Object.finish
2323+let json : t Json.codec =
2424+ let open Json.Codec in
2525+ Object.map ~kind:"StructuredOutput" (fun json_schema -> { json_schema })
2626+ |> Object.member "jsonSchema" Value.t ~enc:(fun t -> t.json_schema)
2727+ |> Object.seal
29283030-let to_json t =
3131- match Jsont.Json.encode jsont t with
3232- | Ok json -> json
3333- | Error msg -> failwith ("Structured_output.to_json: " ^ msg)
2929+let to_json t = Json.encode json t
34303535-let of_json json =
3636- match Jsont.Json.decode jsont json with
3131+let of_json v =
3232+ match Json.decode json v with
3733 | Ok t -> t
3838- | Error msg -> raise (Invalid_argument ("Structured_output.of_json: " ^ msg))
3434+ | Error err ->
3535+ raise
3636+ (Invalid_argument
3737+ ("Structured_output.of_json: " ^ Json.Error.to_string err))
+18-139
lib/structured_output.mli
···88 This module provides structured output support for Claude, allowing you to
99 specify the expected output format using JSON schemas. When a structured
1010 output format is configured, Claude will return its response in the
1111- specified JSON format, validated against your schema.
1212-1313- {2 Overview}
1414-1515- Structured outputs ensure that Claude's responses conform to a specific JSON
1616- schema, making it easier to parse and use the results programmatically. This
1717- is particularly useful for:
1818-1919- - Extracting structured data from unstructured text
2020- - Building APIs that require consistent JSON responses
2121- - Integrating Claude into data pipelines
2222- - Ensuring type-safe parsing of Claude's outputs
2323-2424- {2 Creating Output Formats}
2525-2626- Use {!of_json_schema} to specify a JSON Schema as a {!type:Jsont.json}
2727- value:
2828- {[
2929- let meta = Jsont.Meta.none in
3030- let schema = Jsont.Object ([
3131- (("type", meta), Jsont.String ("object", meta));
3232- (("properties", meta), Jsont.Object ([
3333- (("name", meta), Jsont.Object ([
3434- (("type", meta), Jsont.String ("string", meta))
3535- ], meta));
3636- (("age", meta), Jsont.Object ([
3737- (("type", meta), Jsont.String ("integer", meta))
3838- ], meta));
3939- ], meta));
4040- (("required", meta), Jsont.Array ([
4141- Jsont.String ("name", meta);
4242- Jsont.String ("age", meta)
4343- ], meta));
4444- ], meta) in
4545-4646- let format = Structured_output.of_json_schema schema
4747- ]}
4848-4949- {3 Helper Functions for Building Schemas}
5050-5151- For complex schemas, you can use helper functions to make construction
5252- easier:
5353- {[
5454- let json_object fields = Jsont.Object (fields, Jsont.Meta.none)
5555- let json_string s = Jsont.String (s, Jsont.Meta.none)
5656- let json_array items = Jsont.Array (items, Jsont.Meta.none)
5757- let json_field name value = ((name, Jsont.Meta.none), value)
5858-5959- let person_schema =
6060- json_object
6161- [
6262- json_field "type" (json_string "object");
6363- json_field "properties"
6464- (json_object
6565- [
6666- json_field "name"
6767- (json_object [ json_field "type" (json_string "string") ]);
6868- json_field "age"
6969- (json_object [ json_field "type" (json_string "integer") ]);
7070- ]);
7171- json_field "required"
7272- (json_array [ json_string "name"; json_string "age" ]);
7373- ]
7474-7575- let format = Structured_output.of_json_schema person_schema
7676- ]}
7777-7878- {2 Usage with Claude Client}
7979-8080- {[
8181- let options = Options.default
8282- |> Options.with_output_format format
8383-8484- let client = Client.create ~sw ~process_mgr ~options () in
8585- Client.query client "Extract person info from: John is 30 years old";
8686-8787- let messages = Client.receive_all client in
8888- List.iter (function
8989- | Message.Result result ->
9090- (match Message.Result.structured_output result with
9191- | Some json -> (* Process validated JSON *)
9292- let json_str = match Jsont_bytesrw.encode_string' Jsont.json json with
9393- | Ok s -> s
9494- | Error err -> Jsont.Error.to_string err
9595- in
9696- Printf.printf "Structured output: %s\n" json_str
9797- | None -> ())
9898- | _ -> ()
9999- ) messages
100100- ]}
101101-102102- {2 JSON Schema Support}
103103-104104- The module supports standard JSON Schema Draft 7, including:
105105- - Primitive types (string, integer, number, boolean, null)
106106- - Objects with properties and required fields
107107- - Arrays with item schemas
108108- - Enumerations
109109- - Nested objects and arrays
110110- - Complex validation rules
111111-112112- @see <https://json-schema.org/> JSON Schema specification
113113- @see <https://erratique.ch/software/jsont> jsont documentation *)
1111+ specified JSON format, validated against your schema. *)
1141211513val src : Logs.Src.t
116116-(** The log source for structured output operations *)
1414+(** The log source for structured output operations. *)
1171511816(** {1 Output Format Configuration} *)
1191712018type t
12119(** The type of structured output format configurations. *)
12220123123-val of_json_schema : Jsont.json -> t
2121+val pp : Format.formatter -> t -> unit
2222+(** [pp ppf t] pretty-prints the structured output configuration. *)
2323+2424+val of_json_schema : Json.t -> t
12425(** [of_json_schema schema] creates an output format from a JSON Schema.
12526126126- The schema should be a valid JSON Schema Draft 7 as a {!type:Jsont.json}
127127- 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
2727+ The schema should be a valid JSON Schema Draft 7 as a {!type:Json.t} value.
2828+*)
14729148148- let format = Structured_output.of_json_schema schema
149149- ]} *)
3030+val json_schema : t -> Json.t
3131+(** [json_schema t] returns the underlying JSON Schema. *)
15032151151-val json_schema : t -> Jsont.json
152152-(** [json_schema t] returns the JSON Schema. *)
3333+val to_json_schema : t -> Json.t
3434+(** [to_json_schema t] is an alias of {!json_schema}. *)
15335154154-val jsont : t Jsont.t
3636+val json : t Json.codec
15537(** Codec for structured output format. *)
15638157157-(** {1 Serialization}
3939+(** {1 Serialization} *)
15840159159- Internal use for encoding/decoding with the CLI. *)
4141+val to_json : t -> Json.t
4242+(** [to_json t] converts the output format to its JSON representation. *)
16043161161-val to_json : t -> Jsont.json
162162-(** [to_json t] converts the output format to its JSON representation. Internal
163163- use only. *)
164164-165165-val of_json : Jsont.json -> t
166166-(** [of_json json] parses an output format from JSON. Internal use only.
4444+val of_json : Json.t -> t
4545+(** [of_json json] parses an output format from JSON.
16746 @raise Invalid_argument if the JSON is not a valid output format. *)
···1111 {2 Basic Usage}
12121313 {[
1414- let greet =
1515- Tool.create ~name:"greet" ~description:"Greet a user by name"
1616- ~input_schema:
1717- (`O
1818- [
1919- ("type", `String "object");
2020- ( "properties",
2121- `O [ ("name", `O [ ("type", `String "string") ]) ] );
2222- ("required", `A [ `String "name" ]);
2323- ])
2424- ~handler:(fun args ->
2525- match Tool_input.get_string args "name" with
2626- | Some name ->
2727- Ok
2828- (`A
2929- [
3030- `O
3131- [
3232- ("type", `String "text");
3333- ("text", `String (Printf.sprintf "Hello, %s!" name));
3434- ];
3535- ])
3636- | None -> Error "Missing 'name' parameter")
1414+ let greet =
1515+ Tool.v ~name:"greet" ~description:"Greet a user by name"
1616+ ~input_schema:
1717+ (`O
1818+ [
1919+ ("type", `String "object");
2020+ ("properties", `O [ ("name", `O [ ("type", `String "string") ]) ]);
2121+ ("required", `A [ `String "name" ]);
2222+ ])
2323+ ~handler:(fun args ->
2424+ match Tool_input.string args "name" with
2525+ | Some name ->
2626+ Ok
2727+ (`A
2828+ [
2929+ `O
3030+ [
3131+ ("type", `String "text");
3232+ ("text", `String (Printf.sprintf "Hello, %s!" name));
3333+ ];
3434+ ])
3535+ | None -> Error "Missing 'name' parameter")
3736 ]}
38373938 {2 Tool Response Format}
···44434544 Content blocks are typically:
4645 {[
4747- `A [ `O [ ("type", `String "text"); ("text", `String "result") ] ]
4646+ `A [ `O [ ("type", `String "text"); ("text", `String "result") ] ]
4847 ]} *)
49485049type t
5150(** Abstract type for tool definitions. *)
52515353-val create :
5252+val pp : Format.formatter -> t -> unit
5353+(** [pp ppf t] pretty-prints the tool definition. *)
5454+5555+val v :
5456 name:string ->
5557 description:string ->
5656- input_schema:Jsont.json ->
5757- handler:(Tool_input.t -> (Jsont.json, string) result) ->
5858+ input_schema:Json.t ->
5959+ handler:(Tool_input.t -> (Json.t, string) result) ->
5860 t
5959-(** [create ~name ~description ~input_schema ~handler] creates a custom tool.
6161+(** [v ~name ~description ~input_schema ~handler] creates a custom tool.
60626163 @param name
6264 Unique tool identifier. Claude uses this in function calls. When
···7779val description : t -> string
7880(** [description t] returns the tool's description. *)
79818080-val input_schema : t -> Jsont.json
8282+val input_schema : t -> Json.t
8183(** [input_schema t] returns the JSON Schema for inputs. *)
82848383-val call : t -> Tool_input.t -> (Jsont.json, string) result
8585+val call : t -> Tool_input.t -> (Json.t, string) result
8486(** [call t input] invokes the tool handler with the given input. *)
85878688(** {1 Convenience Constructors}
87898890 Helper functions for common tool patterns. *)
89919090-val text_result : string -> Jsont.json
9292+val text_result : string -> Json.t
9193(** [text_result s] creates a text content result:
9292- [\`A [\`O ["type", \`String "text"; "text", \`String s]]] *)
9494+ [\`A [\`O ["type", \`String "text"; "text", \`String s]]]. *)
93959494-val error_result : string -> Jsont.json
9696+val error_result : string -> Json.t
9597(** [error_result s] creates an error content result with is_error flag. *)
96989799(** {2 Schema Helpers}
9810099101 Build JSON Schema objects more easily. *)
100102101101-val schema_object :
102102- (string * Jsont.json) list -> required:string list -> Jsont.json
103103+val schema_object : (string * Json.t) list -> required:string list -> Json.t
103104(** [schema_object props ~required] creates an object schema.
104105 {[
105105- schema_object
106106- [ ("name", schema_string); ("age", schema_int) ]
107107- ~required:[ "name" ]
106106+ schema_object
107107+ [ ("name", schema_string); ("age", schema_int) ]
108108+ ~required:[ "name" ]
108109 ]} *)
109110110110-val schema_string : Jsont.json
111111-(** String type schema: [{"type": "string"}] *)
111111+val schema_string : Json.t
112112+(** [schema_string] is string type schema: [{"type": "string"}]. *)
112113113113-val schema_int : Jsont.json
114114-(** Integer type schema: [{"type": "integer"}] *)
114114+val schema_int : Json.t
115115+(** [schema_int] is integer type schema: [{"type": "integer"}]. *)
115116116116-val schema_number : Jsont.json
117117-(** Number type schema: [{"type": "number"}] *)
117117+val schema_number : Json.t
118118+(** [schema_number] is number type schema: [{"type": "number"}]. *)
118119119119-val schema_bool : Jsont.json
120120-(** Boolean type schema: [{"type": "boolean"}] *)
120120+val schema_bool : Json.t
121121+(** [schema_bool] is boolean type schema: [{"type": "boolean"}]. *)
121122122122-val schema_array : Jsont.json -> Jsont.json
123123+val schema_array : Json.t -> Json.t
123124(** [schema_array item_schema] creates array schema with given item type. *)
124125125125-val schema_string_enum : string list -> Jsont.json
126126+val schema_string_enum : string list -> Json.t
126127(** [schema_string_enum values] creates enum schema for string values. *)
+43-68
lib/tool_input.ml
···5566(** Opaque tool input with typed accessors. *)
7788-type t = Jsont.json
88+type t = Json.t
99+1010+let pp = Json.pp
9111012(** {1 Escape Hatch} *)
1113···1517(** {1 Helper Functions} *)
16181719(* Extract members from JSON object, or return empty list if not an object *)
1818-let get_members = function Jsont.Object (members, _) -> members | _ -> []
2020+let members = function Json.Object (members, _) -> members | _ -> []
19212022(* Find a member by key in the object *)
2121-let find_member key members =
2323+let member key mems =
2224 List.find_map
2325 (fun ((name, _), value) -> if name = key then Some value else None)
2424- members
2626+ mems
25272628(** {1 Typed Accessors} *)
27292828-let get_string t key =
2929- let members = get_members t in
3030- match find_member key members with
3030+let string t key =
3131+ let mems = members t in
3232+ match member key mems with
3133 | Some json -> (
3232- match Jsont.Json.decode Jsont.string json with
3434+ match Json.decode Json.Codec.string json with
3335 | Ok s -> Some s
3436 | Error _ -> None)
3537 | None -> None
36383737-let get_int t key =
3838- let members = get_members t in
3939- match find_member key members with
3939+let int t key =
4040+ let mems = members t in
4141+ match member key mems with
4042 | Some json -> (
4141- match Jsont.Json.decode Jsont.int json with
4343+ match Json.decode Json.Codec.int json with
4244 | Ok i -> Some i
4345 | Error _ -> None)
4446 | None -> None
45474646-let get_bool t key =
4747- let members = get_members t in
4848- match find_member key members with
4848+let bool t key =
4949+ let mems = members t in
5050+ match member key mems with
4951 | Some json -> (
5050- match Jsont.Json.decode Jsont.bool json with
5252+ match Json.decode Json.Codec.bool json with
5153 | Ok b -> Some b
5254 | Error _ -> None)
5355 | None -> None
54565555-let get_float t key =
5656- let members = get_members t in
5757- match find_member key members with
5757+let float t key =
5858+ let mems = members t in
5959+ match member key mems with
5860 | Some json -> (
5959- match Jsont.Json.decode Jsont.number json with
6161+ match Json.decode Json.Codec.number json with
6062 | Ok f -> Some f
6163 | Error _ -> None)
6264 | None -> None
63656464-let get_string_list t key =
6565- let members = get_members t in
6666- match find_member key members with
6666+let string_list t key =
6767+ let mems = members t in
6868+ match member key mems with
6769 | Some json -> (
6870 match json with
6969- | Jsont.Array (items, _) ->
7171+ | Json.Array (items, _) ->
7072 let strings =
7173 List.filter_map
7274 (fun item ->
7373- match Jsont.Json.decode Jsont.string item with
7575+ match Json.decode Json.Codec.string item with
7476 | Ok s -> Some s
7577 | Error _ -> None)
7678 items
7779 in
7878- (* Only return Some if all items were strings *)
7980 if List.length strings = List.length items then Some strings else None
8081 | _ -> None)
8182 | None -> None
82838384let keys t =
8484- let members = get_members t in
8585- List.map (fun ((name, _), _) -> name) members
8585+ let mems = members t in
8686+ List.map (fun ((name, _), _) -> name) mems
86878788let is_empty t =
8889 match t with
8989- | Jsont.Object ([], _) -> true
9090- | Jsont.Object _ -> false
9090+ | Json.Object ([], _) -> true
9191+ | Json.Object _ -> false
9192 | _ -> true
92939394(** {1 Construction} *)
94959595-let empty = Jsont.Object ([], Jsont.Meta.none)
9696+let empty = Json.Object ([], Json.Meta.none)
96979798let add_member key value t =
9898- let members = get_members t in
9999- let new_member = ((key, Jsont.Meta.none), value) in
9999+ let mems = members t in
100100+ let new_member = ((key, Json.Meta.none), value) in
100101 (* Replace existing member or add new one *)
101101- let filtered_members =
102102- List.filter (fun ((name, _), _) -> name <> key) members
103103- in
104104- Jsont.Object (new_member :: filtered_members, Jsont.Meta.none)
102102+ let filtered_members = List.filter (fun ((name, _), _) -> name <> key) mems in
103103+ Json.Object (new_member :: filtered_members, Json.Meta.none)
105104106106-let add_string key value t =
107107- let json_value =
108108- match Jsont.Json.encode Jsont.string value with
109109- | Ok json -> json
110110- | Error _ -> failwith "add_string: encoding failed"
111111- in
112112- add_member key json_value t
105105+let add_string key value t = add_member key (Json.Value.string value) t
113106114107let add_int key value t =
115115- let json_value =
116116- match Jsont.Json.encode Jsont.int value with
117117- | Ok json -> json
118118- | Error _ -> failwith "add_int: encoding failed"
119119- in
120120- add_member key json_value t
108108+ add_member key (Json.Value.number (Float.of_int value)) t
121109122122-let add_bool key value t =
123123- let json_value =
124124- match Jsont.Json.encode Jsont.bool value with
125125- | Ok json -> json
126126- | Error _ -> failwith "add_bool: encoding failed"
127127- in
128128- add_member key json_value t
129129-130130-let add_float key value t =
131131- let json_value =
132132- match Jsont.Json.encode Jsont.number value with
133133- | Ok json -> json
134134- | Error _ -> failwith "add_float: encoding failed"
135135- in
136136- add_member key json_value t
110110+let add_bool key value t = add_member key (Json.Value.bool value) t
111111+let add_float key value t = add_member key (Json.Value.number value) t
137112138113let of_assoc assoc =
139114 let members =
140140- List.map (fun (key, json) -> ((key, Jsont.Meta.none), json)) assoc
115115+ List.map (fun (key, json) -> ((key, Json.Meta.none), json)) assoc
141116 in
142142- Jsont.Object (members, Jsont.Meta.none)
117117+ Json.Object (members, Json.Meta.none)
143118144119let of_string_pairs pairs =
145120 let assoc =
146121 List.map
147147- (fun (key, value) -> (key, Jsont.String (value, Jsont.Meta.none)))
122122+ (fun (key, value) -> (key, Json.String (value, Json.Meta.none)))
148123 pairs
149124 in
150125 of_assoc assoc
+18-18
lib/tool_input.mli
···1212type t
1313(** Abstract type for tool inputs. *)
14141515+val pp : Format.formatter -> t -> unit
1616+(** [pp ppf t] pretty-prints the tool input. *)
1717+1518(** {1 Typed Accessors} *)
16191717-val get_string : t -> string -> string option
1818-(** [get_string t key] returns the string value for [key], if present and a
1919- string. *)
2020-2121-val get_int : t -> string -> int option
2222-(** [get_int t key] returns the integer value for [key], if present and an int.
2020+val string : t -> string -> string option
2121+(** [string t key] returns the string value for [key], if present and a string.
2322*)
24232525-val get_bool : t -> string -> bool option
2626-(** [get_bool t key] returns the boolean value for [key], if present and a bool.
2727-*)
2424+val int : t -> string -> int option
2525+(** [int t key] returns the integer value for [key], if present and an int. *)
28262929-val get_float : t -> string -> float option
3030-(** [get_float t key] returns the float value for [key], if present and a float.
3131-*)
2727+val bool : t -> string -> bool option
2828+(** [bool t key] returns the boolean value for [key], if present and a bool. *)
32293333-val get_string_list : t -> string -> string list option
3434-(** [get_string_list t key] returns the string list for [key], if present and a
3535- list of strings. *)
3030+val float : t -> string -> float option
3131+(** [float t key] returns the float value for [key], if present and a float. *)
3232+3333+val string_list : t -> string -> string list option
3434+(** [string_list t key] returns the string list for [key], if present and a list
3535+ of strings. *)
36363737val keys : t -> string list
3838(** [keys t] returns all keys in the input. *)
···42424343(** {1 Escape Hatch} *)
44444545-val to_json : t -> Jsont.json
4545+val to_json : t -> Json.t
4646(** [to_json t] returns the underlying JSON for advanced use cases. *)
47474848-val of_json : Jsont.json -> t
4848+val of_json : Json.t -> t
4949(** [of_json json] wraps JSON as a tool input. *)
50505151(** {1 Construction} *)
···6565val add_float : string -> float -> t -> t
6666(** [add_float key value t] adds a float field. *)
67676868-val of_assoc : (string * Jsont.json) list -> t
6868+val of_assoc : (string * Json.t) list -> t
6969(** [of_assoc assoc] creates tool input from an association list. *)
70707171val of_string_pairs : (string * string) list -> t
+81-130
lib/transport.ml
···2323}
24242525let setting_source_to_string = function
2626- | Proto.Options.User -> "user"
2727- | Proto.Options.Project -> "project"
2828- | Proto.Options.Local -> "local"
2929-3030-let build_command ~claude_path ~options =
3131- let cmd = [ claude_path; "--output-format"; "stream-json"; "--verbose" ] in
3232-3333- let cmd =
3434- match Options.system_prompt options with
3535- | Some prompt -> cmd @ [ "--system-prompt"; prompt ]
3636- | None -> cmd
3737- in
3838-3939- let cmd =
4040- match Options.append_system_prompt options with
4141- | Some prompt -> cmd @ [ "--append-system-prompt"; prompt ]
4242- | None -> cmd
4343- in
4444-4545- let cmd =
4646- match Options.allowed_tools options with
4747- | [] -> cmd
4848- | tools -> cmd @ [ "--allowedTools"; String.concat "," tools ]
4949- in
5050-5151- let cmd =
5252- match Options.disallowed_tools options with
5353- | [] -> cmd
5454- | tools -> cmd @ [ "--disallowedTools"; String.concat "," tools ]
5555- in
5656-5757- let cmd =
5858- match Options.model options with
5959- | Some model -> cmd @ [ "--model"; Model.to_string model ]
6060- | None -> cmd
6161- in
6262-6363- let cmd =
6464- match Options.permission_mode options with
6565- | Some mode ->
6666- let mode_str = Permissions.Mode.to_string mode in
6767- cmd @ [ "--permission-mode"; mode_str ]
6868- | None -> cmd
6969- in
7070-7171- let cmd =
7272- match Options.permission_prompt_tool_name options with
7373- | Some tool_name -> cmd @ [ "--permission-prompt-tool"; tool_name ]
7474- | None -> cmd
7575- in
2626+ | Options.Wire.User -> "user"
2727+ | Options.Wire.Project -> "project"
2828+ | Options.Wire.Local -> "local"
76297777- (* Advanced configuration options *)
7878- let cmd =
7979- match Options.max_budget_usd options with
8080- | Some budget -> cmd @ [ "--max-budget-usd"; Float.to_string budget ]
8181- | None -> cmd
8282- in
3030+let add_flag flag opt cmd =
3131+ match opt with None -> cmd | Some v -> cmd @ [ flag; v ]
83328484- let cmd =
8585- match Options.fallback_model options with
8686- | Some model -> cmd @ [ "--fallback-model"; Model.to_string model ]
8787- | None -> cmd
8888- in
3333+let add_list flag list cmd =
3434+ match list with [] -> cmd | items -> cmd @ [ flag; String.concat "," items ]
89359090- let cmd =
9191- match Options.setting_sources options with
9292- | Some sources ->
9393- let sources_str =
9494- String.concat "," (List.map setting_source_to_string sources)
9595- in
9696- cmd @ [ "--setting-sources"; sources_str ]
9797- | None -> cmd
9898- in
3636+let build_command ~claude_path ~options =
3737+ [ claude_path; "--output-format"; "stream-json"; "--verbose" ]
3838+ |> add_flag "--system-prompt" (Options.system_prompt options)
3939+ |> add_flag "--append-system-prompt" (Options.append_system_prompt options)
4040+ |> add_list "--allowedTools" (Options.allowed_tools options)
4141+ |> add_list "--disallowedTools" (Options.disallowed_tools options)
4242+ |> add_flag "--model" (Option.map Model.to_string (Options.model options))
4343+ |> add_flag "--permission-mode"
4444+ (Option.map Permissions.Mode.to_string (Options.permission_mode options))
4545+ |> add_flag "--permission-prompt-tool"
4646+ (Options.permission_prompt_tool_name options)
4747+ |> add_flag "--max-budget-usd"
4848+ (Option.map Float.to_string (Options.max_budget_usd options))
4949+ |> add_flag "--fallback-model"
5050+ (Option.map Model.to_string (Options.fallback_model options))
5151+ |> add_flag "--setting-sources"
5252+ (Option.map
5353+ (fun sources ->
5454+ String.concat "," (List.map setting_source_to_string sources))
5555+ (Options.setting_sources options))
5656+ |> add_flag "--json-schema"
5757+ (Option.map
5858+ (fun format ->
5959+ let schema = Structured_output.to_json_schema format in
6060+ Json.Value.to_string schema)
6161+ (Options.output_format options))
6262+ |> fun cmd -> cmd @ [ "--input-format"; "stream-json" ]
9963100100- (* Add JSON Schema if specified *)
101101- let cmd =
102102- match Options.output_format options with
103103- | Some format ->
104104- let schema = Proto.Structured_output.to_json_schema format in
105105- let schema_str =
106106- match Jsont_bytesrw.encode_string' Jsont.json schema with
107107- | Ok s -> s
108108- | Error err -> failwith (Jsont.Error.to_string err)
109109- in
110110- cmd @ [ "--json-schema"; schema_str ]
111111- | None -> cmd
112112- in
113113-114114- (* Use streaming input mode *)
115115- cmd @ [ "--input-format"; "stream-json" ]
116116-117117-let create ~sw ~process_mgr ~options () =
118118- let claude_path = "claude" in
119119- let cmd = build_command ~claude_path ~options in
120120-121121- (* Build environment - preserve essential vars for Claude config/auth access *)
6464+let build_environment ~options =
6565+ (* Preserve essential vars for Claude config/auth access *)
12266 let home = Option.value (Sys.getenv_opt "HOME") ~default:"/tmp" in
12367 let path = Option.value (Sys.getenv_opt "PATH") ~default:"/usr/bin:/bin" in
12468···14084 let preserved =
14185 List.filter_map
14286 (fun var ->
143143- Option.map
144144- (fun value -> Printf.sprintf "%s=%s" var value)
145145- (Sys.getenv_opt var))
8787+ Option.map (fun value -> Fmt.str "%s=%s" var value) (Sys.getenv_opt var))
14688 preserve_vars
14789 in
1489014991 let base_env =
15092 [
151151- Printf.sprintf "HOME=%s" home;
152152- Printf.sprintf "PATH=%s" path;
9393+ Fmt.str "HOME=%s" home;
9494+ Fmt.str "PATH=%s" path;
15395 "CLAUDE_CODE_ENTRYPOINT=sdk-ocaml";
15496 ]
15597 @ preserved
15698 in
15799158100 let custom_env =
159159- List.map (fun (k, v) -> Printf.sprintf "%s=%s" k v) (Options.env options)
101101+ List.map (fun (k, v) -> Fmt.str "%s=%s" k v) (Options.env options)
160102 in
161103 let env = Array.of_list (base_env @ custom_env) in
162104 Log.debug (fun m -> m "Environment: HOME=%s, PATH=%s" home path);
163105 Log.info (fun m ->
164106 m "Full environment variables: %s"
165107 (String.concat ", " (Array.to_list env)));
108108+ env
109109+110110+let spawn_process ~sw ~process_mgr ~env ~options ~cmd ~stdin_r ~stdout_w =
111111+ try
112112+ Log.info (fun m ->
113113+ m "Spawning claude with command: %s" (String.concat " " cmd));
114114+ Log.info (fun m -> m "Command arguments breakdown:");
115115+ List.iteri (fun i arg -> Log.info (fun m -> m " [%d]: %s" i arg)) cmd;
116116+ Eio.Process.spawn ~sw process_mgr ~env
117117+ ~stdin:(stdin_r :> Eio.Flow.source_ty r)
118118+ ~stdout:(stdout_w :> Eio.Flow.sink_ty r)
119119+ ?cwd:(Options.cwd options) cmd
120120+ with exn ->
121121+ Log.err (fun m ->
122122+ m "Failed to spawn claude CLI: %s" (Printexc.to_string exn));
123123+ Log.err (fun m -> m "Make sure 'claude' is installed and authenticated");
124124+ Log.err (fun m -> m "You may need to run 'claude login' first");
125125+ raise
126126+ (CLI_not_found
127127+ (Fmt.str "Failed to spawn claude CLI: %s" (Printexc.to_string exn)))
128128+129129+let v ~sw ~process_mgr ~options () =
130130+ let claude_path = "claude" in
131131+ let cmd = build_command ~claude_path ~options in
132132+ let env = build_environment ~options in
166133167134 let stdin_r, stdin_w = Eio.Process.pipe ~sw process_mgr in
168135 let stdout_r, stdout_w = Eio.Process.pipe ~sw process_mgr in
···172139 Eio.Flow.close stderr_w;
173140174141 let process =
175175- try
176176- Log.info (fun m ->
177177- m "Spawning claude with command: %s" (String.concat " " cmd));
178178- Log.info (fun m -> m "Command arguments breakdown:");
179179- List.iteri (fun i arg -> Log.info (fun m -> m " [%d]: %s" i arg)) cmd;
180180- Eio.Process.spawn ~sw process_mgr ~env
181181- ~stdin:(stdin_r :> Eio.Flow.source_ty r)
182182- ~stdout:(stdout_w :> Eio.Flow.sink_ty r)
183183- ?cwd:(Options.cwd options) cmd
184184- with exn ->
185185- Log.err (fun m ->
186186- m "Failed to spawn claude CLI: %s" (Printexc.to_string exn));
187187- Log.err (fun m -> m "Make sure 'claude' is installed and authenticated");
188188- Log.err (fun m -> m "You may need to run 'claude login' first");
189189- raise
190190- (CLI_not_found
191191- (Printf.sprintf "Failed to spawn claude CLI: %s"
192192- (Printexc.to_string exn)))
142142+ spawn_process ~sw ~process_mgr ~env ~options ~cmd ~stdin_r ~stdout_w
193143 in
194144195145 let stdin = (stdin_w :> Eio.Flow.sink_ty r) in
···206156 { process = P process; stdin; stdin_close; stdout }
207157208158let send t json =
209209- let data =
210210- match Jsont_bytesrw.encode_string' Jsont.json json with
211211- | Ok s -> s
212212- | Error err -> failwith (Jsont.Error.to_string err)
213213- in
159159+ let data = Json.Value.to_string json in
214160 Log.debug (fun m -> m "Sending: %s" data);
215161 try Eio.Flow.write t.stdin [ Cstruct.of_string (data ^ "\n") ]
216162 with exn ->
217163 Log.err (fun m -> m "Failed to send message: %s" (Printexc.to_string exn));
218164 raise
219165 (Connection_error
220220- (Printf.sprintf "Failed to send message: %s" (Printexc.to_string exn)))
166166+ (Fmt.str "Failed to send message: %s" (Printexc.to_string exn)))
221167222168let receive_line t =
223169 try
···233179 m "Failed to receive message: %s" (Printexc.to_string exn));
234180 raise
235181 (Connection_error
236236- (Printf.sprintf "Failed to receive message: %s"
237237- (Printexc.to_string exn)))
182182+ (Fmt.str "Failed to receive message: %s" (Printexc.to_string exn)))
238183239184let interrupt t =
240185 Log.info (fun m -> m "Sending interrupt signal");
241241- (* Create interrupt request using Proto types *)
242242- let request = Proto.Control.Request.interrupt () in
243243- let envelope = Proto.Control.create_request ~request_id:"" ~request () in
244244- let outgoing = Proto.Outgoing.Control_request envelope in
245245- let interrupt_msg = Proto.Outgoing.to_json outgoing in
186186+ let request = Control.Request.interrupt () in
187187+ let envelope : Control.control_request =
188188+ {
189189+ type_ = `Control_request;
190190+ request_id = "";
191191+ request;
192192+ unknown = Unknown.empty;
193193+ }
194194+ in
195195+ let outgoing = Outgoing.Control_request envelope in
196196+ let interrupt_msg = Outgoing.to_json outgoing in
246197 send t interrupt_msg
247198248199let close t =
···250201 Eio.Flow.close t.stdin_close;
251202 let (P process) = t.process in
252203 Eio.Process.await_exn process
253253- with _ -> ()
204204+ with Eio.Io _ -> ()
+13-3
lib/transport.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566+(** Claude CLI process transport. *)
77+68val src : Logs.Src.t
77-(** The log source for transport operations *)
99+(** The log source for transport operations. *)
810911exception CLI_not_found of string
1012exception Process_error of string
···12141315type t
14161515-val create :
1717+val v :
1618 sw:Eio.Switch.t ->
1719 process_mgr:_ Eio.Process.mgr ->
1820 options:Options.t ->
1921 unit ->
2022 t
2323+(** [v ~sw ~process_mgr ~options ()] creates a new transport. *)
21242222-val send : t -> Jsont.json -> unit
2525+val send : t -> Json.t -> unit
2626+(** Send a JSON message. *)
2727+2328val receive_line : t -> string option
2929+(** Receive a line from the transport. *)
3030+2431val interrupt : t -> unit
3232+(** Send an interrupt signal. *)
3333+2534val close : t -> unit
3535+(** Close the transport. *)
+43-14
lib/unknown.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Unknown fields for capturing extra JSON object members.
66+type t = (string * Json.t) list
7788- This module provides a type and utilities for preserving unknown/extra
99- fields when parsing JSON objects with jsont. Use with
1010- [Jsont.Object.keep_unknown] to capture fields not explicitly defined in your
1111- codec. *)
88+let pp ppf t =
99+ let pp_pair ppf (k, v) = Fmt.pf ppf "@[%s: %a@]" k Json.pp v in
1010+ Fmt.pf ppf "@[{%a}@]"
1111+ (Fmt.list ~sep:(fun ppf () -> Fmt.pf ppf ",@ ") pp_pair)
1212+ t
12131313-type t = Jsont.json
1414-(** The type of unknown fields - stored as raw JSON. *)
1414+let empty = []
1515+let is_empty = function [] -> true | _ -> false
1616+let of_assoc x = x
1717+let to_assoc x = x
15181616-(** An empty unknown fields value (empty JSON object). *)
1717-let empty = Jsont.Object ([], Jsont.Meta.none)
1818-1919-(** [is_empty t] returns [true] if there are no unknown fields. *)
2020-let is_empty = function Jsont.Object ([], _) -> true | _ -> false
1919+let json =
2020+ let open Json.Codec in
2121+ let dec obj =
2222+ match obj with
2323+ | Json.Object (fields, _) ->
2424+ List.map (fun ((name, _meta), json) -> (name, json)) fields
2525+ | _ -> invalid_arg "Expected object"
2626+ in
2727+ let enc fields =
2828+ let mems =
2929+ List.map (fun (name, json) -> ((name, Json.Meta.none), json)) fields
3030+ in
3131+ Json.Object (mems, Json.Meta.none)
3232+ in
3333+ map ~dec ~enc Value.t
21342222-(** Codec for unknown fields. *)
2323-let jsont = Jsont.json
3535+let mems : (t, Json.t, Json.member list) Json.Codec.Object.Members.map =
3636+ let open Json.Codec in
3737+ let dec_empty () = [] in
3838+ let dec_add meta name json acc = ((name, meta), json) :: acc in
3939+ let dec_finish _meta mems =
4040+ List.rev_map (fun ((name, _meta), json) -> (name, json)) mems
4141+ in
4242+ let enc =
4343+ {
4444+ Object.Members.enc =
4545+ (fun k fields acc ->
4646+ List.fold_left
4747+ (fun acc (name, json) -> k Json.Meta.none name json acc)
4848+ acc fields);
4949+ }
5050+ in
5151+ Object.Members.map ~kind:"Unknown" ~dec_empty ~dec_add ~dec_finish ~enc
5252+ Value.t
+24-11
lib/unknown.mli
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Unknown fields for capturing extra JSON object members.
66+(** Unknown fields for preserving extra JSON object members during
77+ round-tripping.
7888- This module provides a type and utilities for preserving unknown/extra
99- fields when parsing JSON objects with jsont. Use with
1010- [Jsont.Object.keep_unknown] to capture fields not explicitly defined in your
1111- codec. *)
99+ This module provides an opaque type for storing unknown JSON fields as an
1010+ association list. Useful for preserving fields that are not part of the
1111+ defined schema but should be maintained when reading and writing JSON. *)
1212+1313+type t
1414+(** The opaque type of unknown fields, stored as an association list of field
1515+ names to JSON values. *)
12161313-type t = Jsont.json
1414-(** The type of unknown fields - stored as raw JSON. *)
1717+val pp : Format.formatter -> t -> unit
1818+(** [pp ppf t] pretty-prints the unknown fields. *)
15191620val empty : t
1717-(** An empty unknown fields value (empty JSON object). *)
2121+(** [empty] is an empty set of unknown fields. *)
18221923val is_empty : t -> bool
2020-(** [is_empty t] returns [true] if there are no unknown fields. *)
2424+(** [is_empty t] returns [true] if there are no unknown fields stored in [t]. *)
2525+2626+val of_assoc : (string * Json.t) list -> t
2727+(** [of_assoc assoc] creates unknown fields from an association list. *)
2828+2929+val to_assoc : t -> (string * Json.t) list
3030+(** [to_assoc t] returns the association list of unknown fields. *)
21312222-val jsont : t Jsont.t
2323-(** Codec for unknown fields. *)
3232+val json : t Json.codec
3333+(** [json] is a codec for encoding and decoding unknown fields to/from JSON. *)
3434+3535+val mems : (t, Json.t, Json.member list) Json.Codec.Object.Members.map
3636+(** [mems] is a mems codec for use with [Json.Codec.Object.keep_unknown]. *)
···1111 - Mcp_server module for in-process MCP servers
1212 - Structured error handling *)
13131414-module J = Jsont.Json
1414+module J = Json.Value
15151616(* ============================================
1717 Protocol Tests - Incoming message codec
···2020let test_decode_user_message () =
2121 (* User messages from CLI come wrapped in a "message" envelope *)
2222 let json_str = {|{"type":"user","message":{"content":"Hello"}}|} in
2323- match Jsont_bytesrw.decode_string' Claude.Proto.Incoming.jsont json_str with
2424- | Ok (Claude.Proto.Incoming.Message (Claude.Proto.Message.User _)) -> ()
2323+ match Json.of_string Claude.Incoming.json json_str with
2424+ | Ok (Claude.Incoming.Message (Claude.Message.User _)) -> ()
2525 | Ok _ -> Alcotest.fail "Wrong message type decoded"
2626- | Error err -> Alcotest.fail (Jsont.Error.to_string err)
2626+ | Error err -> Alcotest.fail (Json.Error.to_string err)
27272828let test_decode_assistant_message () =
2929 (* Assistant messages from CLI come wrapped in a "message" envelope *)
3030 let json_str =
3131 {|{"type":"assistant","message":{"model":"claude-sonnet-4","content":[{"type":"text","text":"Hi"}]}}|}
3232 in
3333- match Jsont_bytesrw.decode_string' Claude.Proto.Incoming.jsont json_str with
3434- | Ok (Claude.Proto.Incoming.Message (Claude.Proto.Message.Assistant _)) -> ()
3333+ match Json.of_string Claude.Incoming.json json_str with
3434+ | Ok (Claude.Incoming.Message (Claude.Message.Assistant _)) -> ()
3535 | Ok _ -> Alcotest.fail "Wrong message type decoded"
3636- | Error err -> Alcotest.fail (Jsont.Error.to_string err)
3636+ | Error err -> Alcotest.fail (Json.Error.to_string err)
37373838let test_decode_system_message () =
3939 let json_str =
4040 {|{"type":"system","subtype":"init","data":{"session_id":"test-123"}}|}
4141 in
4242- match Jsont_bytesrw.decode_string' Claude.Proto.Incoming.jsont json_str with
4343- | Ok (Claude.Proto.Incoming.Message (Claude.Proto.Message.System _)) -> ()
4242+ match Json.of_string Claude.Incoming.json json_str with
4343+ | Ok (Claude.Incoming.Message (Claude.Message.System _)) -> ()
4444 | Ok _ -> Alcotest.fail "Wrong message type decoded"
4545- | Error err -> Alcotest.fail (Jsont.Error.to_string err)
4545+ | Error err -> Alcotest.fail (Json.Error.to_string err)
46464747let test_decode_control_response_success () =
4848 let json_str =
4949- {|{"type":"control_response","response":{"subtype":"success","requestId":"test-req-1"}}|}
4949+ {|{"type":"control_response","response":{"subtype":"success","request_id":"test-req-1"}}|}
5050 in
5151- match Jsont_bytesrw.decode_string' Claude.Proto.Incoming.jsont json_str with
5252- | Ok (Claude.Proto.Incoming.Control_response resp) -> (
5151+ match Json.of_string Claude.Incoming.json json_str with
5252+ | Ok (Claude.Incoming.Control_response resp) -> (
5353 match resp.response with
5454- | Claude.Proto.Control.Response.Success s ->
5454+ | Claude.Control.Response.Success s ->
5555 Alcotest.(check string) "request_id" "test-req-1" s.request_id
5656- | Claude.Proto.Control.Response.Error _ ->
5656+ | Claude.Control.Response.Error _ ->
5757 Alcotest.fail "Got error response instead of success")
5858 | Ok _ -> Alcotest.fail "Wrong message type decoded"
5959- | Error err -> Alcotest.fail (Jsont.Error.to_string err)
5959+ | Error err -> Alcotest.fail (Json.Error.to_string err)
60606161let test_decode_control_response_error () =
6262 let json_str =
6363- {|{"type":"control_response","response":{"subtype":"error","requestId":"test-req-2","error":{"code":-32603,"message":"Something went wrong"}}}|}
6363+ {|{"type":"control_response","response":{"subtype":"error","request_id":"test-req-2","error":{"code":-32603,"message":"Something went wrong"}}}|}
6464 in
6565- match Jsont_bytesrw.decode_string' Claude.Proto.Incoming.jsont json_str with
6666- | Ok (Claude.Proto.Incoming.Control_response resp) -> (
6565+ match Json.of_string Claude.Incoming.json json_str with
6666+ | Ok (Claude.Incoming.Control_response resp) -> (
6767 match resp.response with
6868- | Claude.Proto.Control.Response.Error e ->
6868+ | Claude.Control.Response.Error e ->
6969 Alcotest.(check string) "request_id" "test-req-2" e.request_id;
7070 Alcotest.(check int) "error code" (-32603) e.error.code;
7171 Alcotest.(check string)
7272 "error message" "Something went wrong" e.error.message
7373- | Claude.Proto.Control.Response.Success _ ->
7373+ | Claude.Control.Response.Success _ ->
7474 Alcotest.fail "Got success response instead of error")
7575 | Ok _ -> Alcotest.fail "Wrong message type decoded"
7676- | Error err -> Alcotest.fail (Jsont.Error.to_string err)
7676+ | Error err -> Alcotest.fail (Json.Error.to_string err)
77777878let protocol_tests =
7979 [
···93939494let json_testable =
9595 Alcotest.testable
9696- (fun fmt json ->
9797- match Jsont_bytesrw.encode_string' Jsont.json json with
9898- | Ok s -> Format.pp_print_string fmt s
9999- | Error e -> Format.pp_print_string fmt (Jsont.Error.to_string e))
100100- (fun a b ->
101101- match
102102- ( Jsont_bytesrw.encode_string' Jsont.json a,
103103- Jsont_bytesrw.encode_string' Jsont.json b )
104104- with
105105- | Ok sa, Ok sb -> String.equal sa sb
106106- | _ -> false)
9696+ (fun fmt json -> Format.pp_print_string fmt (Json.Value.to_string json))
9797+ (fun a b -> String.equal (Json.Value.to_string a) (Json.Value.to_string b))
1079810899let test_tool_schema_string () =
109100 let schema = Claude.Tool.schema_string in
110110- let expected = J.object' [ J.mem (J.name "type") (J.string "string") ] in
101101+ let expected = J.object' [ J.member (J.name "type") (J.string "string") ] in
111102 Alcotest.check json_testable "schema_string" expected schema
112103113104let test_tool_schema_int () =
114105 let schema = Claude.Tool.schema_int in
115115- let expected = J.object' [ J.mem (J.name "type") (J.string "integer") ] in
106106+ let expected = J.object' [ J.member (J.name "type") (J.string "integer") ] in
116107 Alcotest.check json_testable "schema_int" expected schema
117108118109let test_tool_schema_number () =
119110 let schema = Claude.Tool.schema_number in
120120- let expected = J.object' [ J.mem (J.name "type") (J.string "number") ] in
111111+ let expected = J.object' [ J.member (J.name "type") (J.string "number") ] in
121112 Alcotest.check json_testable "schema_number" expected schema
122113123114let test_tool_schema_bool () =
124115 let schema = Claude.Tool.schema_bool in
125125- let expected = J.object' [ J.mem (J.name "type") (J.string "boolean") ] in
116116+ let expected = J.object' [ J.member (J.name "type") (J.string "boolean") ] in
126117 Alcotest.check json_testable "schema_bool" expected schema
127118128119let test_tool_schema_array () =
···130121 let expected =
131122 J.object'
132123 [
133133- J.mem (J.name "type") (J.string "array");
134134- J.mem (J.name "items")
135135- (J.object' [ J.mem (J.name "type") (J.string "string") ]);
124124+ J.member (J.name "type") (J.string "array");
125125+ J.member (J.name "items")
126126+ (J.object' [ J.member (J.name "type") (J.string "string") ]);
136127 ]
137128 in
138129 Alcotest.check json_testable "schema_array" expected schema
···142133 let expected =
143134 J.object'
144135 [
145145- J.mem (J.name "type") (J.string "string");
146146- J.mem (J.name "enum")
136136+ J.member (J.name "type") (J.string "string");
137137+ J.member (J.name "enum")
147138 (J.list [ J.string "foo"; J.string "bar"; J.string "baz" ]);
148139 ]
149140 in
···158149 let expected =
159150 J.object'
160151 [
161161- J.mem (J.name "type") (J.string "object");
162162- J.mem (J.name "properties")
152152+ J.member (J.name "type") (J.string "object");
153153+ J.member (J.name "properties")
163154 (J.object'
164155 [
165165- J.mem (J.name "name")
166166- (J.object' [ J.mem (J.name "type") (J.string "string") ]);
167167- J.mem (J.name "age")
168168- (J.object' [ J.mem (J.name "type") (J.string "integer") ]);
156156+ J.member (J.name "name")
157157+ (J.object' [ J.member (J.name "type") (J.string "string") ]);
158158+ J.member (J.name "age")
159159+ (J.object' [ J.member (J.name "type") (J.string "integer") ]);
169160 ]);
170170- J.mem (J.name "required") (J.list [ J.string "name" ]);
161161+ J.member (J.name "required") (J.list [ J.string "name" ]);
171162 ]
172163 in
173164 Alcotest.check json_testable "schema_object" expected schema
···179170 [
180171 J.object'
181172 [
182182- J.mem (J.name "type") (J.string "text");
183183- J.mem (J.name "text") (J.string "Hello, world!");
173173+ J.member (J.name "type") (J.string "text");
174174+ J.member (J.name "text") (J.string "Hello, world!");
184175 ];
185176 ]
186177 in
···193184 [
194185 J.object'
195186 [
196196- J.mem (J.name "type") (J.string "text");
197197- J.mem (J.name "text") (J.string "Something went wrong");
198198- J.mem (J.name "is_error") (J.bool true);
187187+ J.member (J.name "type") (J.string "text");
188188+ J.member (J.name "text") (J.string "Something went wrong");
189189+ J.member (J.name "is_error") (J.bool true);
199190 ];
200191 ]
201192 in
···203194204195let test_tool_create_and_call () =
205196 let greet =
206206- Claude.Tool.create ~name:"greet" ~description:"Greet a user"
197197+ Claude.Tool.v ~name:"greet" ~description:"Greet a user"
207198 ~input_schema:
208199 (Claude.Tool.schema_object
209200 [ ("name", Claude.Tool.schema_string) ]
210201 ~required:[ "name" ])
211202 ~handler:(fun args ->
212212- match Claude.Tool_input.get_string args "name" with
203203+ match Claude.Tool_input.string args "name" with
213204 | Some name -> Ok (Claude.Tool.text_result ("Hello, " ^ name ^ "!"))
214205 | None -> Error "Missing name parameter")
215206 in
···219210 (Claude.Tool.description greet);
220211221212 (* Test successful call *)
222222- let input_json = J.object' [ J.mem (J.name "name") (J.string "Alice") ] in
213213+ let input_json = J.object' [ J.member (J.name "name") (J.string "Alice") ] in
223214 let input = Claude.Tool_input.of_json input_json in
224215 match Claude.Tool.call greet input with
225216 | Ok result ->
···229220230221let test_tool_call_error () =
231222 let tool =
232232- Claude.Tool.create ~name:"fail" ~description:"Always fails"
223223+ Claude.Tool.v ~name:"fail" ~description:"Always fails"
233224 ~input_schema:(Claude.Tool.schema_object [] ~required:[])
234225 ~handler:(fun _ -> Error "Intentional failure")
235226 in
···260251261252let test_mcp_server_create () =
262253 let tool =
263263- Claude.Tool.create ~name:"echo" ~description:"Echo input"
254254+ Claude.Tool.v ~name:"echo" ~description:"Echo input"
264255 ~input_schema:
265256 (Claude.Tool.schema_object
266257 [ ("text", Claude.Tool.schema_string) ]
267258 ~required:[ "text" ])
268259 ~handler:(fun args ->
269269- match Claude.Tool_input.get_string args "text" with
260260+ match Claude.Tool_input.string args "text" with
270261 | Some text -> Ok (Claude.Tool.text_result text)
271262 | None -> Error "Missing text")
272263 in
273264 let server =
274274- Claude.Mcp_server.create ~name:"test-server" ~version:"2.0.0"
275275- ~tools:[ tool ] ()
265265+ Claude.Mcp_server.v ~name:"test-server" ~version:"2.0.0" ~tools:[ tool ] ()
276266 in
277267 Alcotest.(check string)
278268 "server name" "test-server"
···285275 (List.length (Claude.Mcp_server.tools server))
286276287277let test_mcp_server_initialize () =
288288- let server = Claude.Mcp_server.create ~name:"init-test" ~tools:[] () in
278278+ let server = Claude.Mcp_server.v ~name:"init-test" ~tools:[] () in
289279 let request =
290280 J.object'
291281 [
292292- J.mem (J.name "jsonrpc") (J.string "2.0");
293293- J.mem (J.name "id") (J.number 1.0);
294294- J.mem (J.name "method") (J.string "initialize");
295295- J.mem (J.name "params") (J.object' []);
282282+ J.member (J.name "jsonrpc") (J.string "2.0");
283283+ J.member (J.name "id") (J.number 1.0);
284284+ J.member (J.name "method") (J.string "initialize");
285285+ J.member (J.name "params") (J.object' []);
296286 ]
297287 in
298288 let response = Claude.Mcp_server.handle_json_message server request in
299289 (* Check it's a success response with serverInfo *)
300290 match response with
301301- | Jsont.Object (mems, _) ->
291291+ | Json.Object (mems, _) ->
302292 let has_result = List.exists (fun ((k, _), _) -> k = "result") mems in
303293 Alcotest.(check bool) "has result" true has_result
304294 | _ -> Alcotest.fail "Expected object response"
305295306296let test_mcp_server_tools_list () =
307297 let tool =
308308- Claude.Tool.create ~name:"my_tool" ~description:"My test tool"
298298+ Claude.Tool.v ~name:"my_tool" ~description:"My test tool"
309299 ~input_schema:(Claude.Tool.schema_object [] ~required:[])
310300 ~handler:(fun _ -> Ok (Claude.Tool.text_result "ok"))
311301 in
312312- let server = Claude.Mcp_server.create ~name:"list-test" ~tools:[ tool ] () in
302302+ let server = Claude.Mcp_server.v ~name:"list-test" ~tools:[ tool ] () in
313303 let request =
314304 J.object'
315305 [
316316- J.mem (J.name "jsonrpc") (J.string "2.0");
317317- J.mem (J.name "id") (J.number 2.0);
318318- J.mem (J.name "method") (J.string "tools/list");
319319- J.mem (J.name "params") (J.object' []);
306306+ J.member (J.name "jsonrpc") (J.string "2.0");
307307+ J.member (J.name "id") (J.number 2.0);
308308+ J.member (J.name "method") (J.string "tools/list");
309309+ J.member (J.name "params") (J.object' []);
320310 ]
321311 in
322312 let response = Claude.Mcp_server.handle_json_message server request in
323313 match response with
324324- | Jsont.Object (mems, _) -> (
314314+ | Json.Object (mems, _) -> (
325315 match List.find_opt (fun ((k, _), _) -> k = "result") mems with
326326- | Some (_, Jsont.Object (result_mems, _)) -> (
316316+ | Some (_, Json.Object (result_mems, _)) -> (
327317 match List.find_opt (fun ((k, _), _) -> k = "tools") result_mems with
328328- | Some (_, Jsont.Array (tools, _)) ->
318318+ | Some (_, Json.Array (tools, _)) ->
329319 Alcotest.(check int) "tools count" 1 (List.length tools)
330320 | _ -> Alcotest.fail "Missing tools in result")
331321 | _ -> Alcotest.fail "Missing result in response")
···333323334324let test_mcp_server_tools_call () =
335325 let tool =
336336- Claude.Tool.create ~name:"uppercase" ~description:"Convert to uppercase"
326326+ Claude.Tool.v ~name:"uppercase" ~description:"Convert to uppercase"
337327 ~input_schema:
338328 (Claude.Tool.schema_object
339329 [ ("text", Claude.Tool.schema_string) ]
340330 ~required:[ "text" ])
341331 ~handler:(fun args ->
342342- match Claude.Tool_input.get_string args "text" with
332332+ match Claude.Tool_input.string args "text" with
343333 | Some text ->
344334 Ok (Claude.Tool.text_result (String.uppercase_ascii text))
345335 | None -> Error "Missing text")
346336 in
347347- let server = Claude.Mcp_server.create ~name:"call-test" ~tools:[ tool ] () in
337337+ let server = Claude.Mcp_server.v ~name:"call-test" ~tools:[ tool ] () in
348338 let request =
349339 J.object'
350340 [
351351- J.mem (J.name "jsonrpc") (J.string "2.0");
352352- J.mem (J.name "id") (J.number 3.0);
353353- J.mem (J.name "method") (J.string "tools/call");
354354- J.mem (J.name "params")
341341+ J.member (J.name "jsonrpc") (J.string "2.0");
342342+ J.member (J.name "id") (J.number 3.0);
343343+ J.member (J.name "method") (J.string "tools/call");
344344+ J.member (J.name "params")
355345 (J.object'
356346 [
357357- J.mem (J.name "name") (J.string "uppercase");
358358- J.mem (J.name "arguments")
359359- (J.object' [ J.mem (J.name "text") (J.string "hello") ]);
347347+ J.member (J.name "name") (J.string "uppercase");
348348+ J.member (J.name "arguments")
349349+ (J.object' [ J.member (J.name "text") (J.string "hello") ]);
360350 ]);
361351 ]
362352 in
363353 let response = Claude.Mcp_server.handle_json_message server request in
364354 (* Verify it contains the expected uppercase result *)
365365- let response_str =
366366- match Jsont_bytesrw.encode_string' Jsont.json response with
367367- | Ok s -> s
368368- | Error _ -> ""
369369- in
355355+ let response_str = Json.Value.to_string response in
370356 (* Simple substring check for HELLO in response *)
371357 let contains_hello =
372358 let rec check i =
···378364 in
379365 Alcotest.(check bool) "contains HELLO" true contains_hello
380366381381-let test_mcp_server_tool_not_found () =
382382- let server = Claude.Mcp_server.create ~name:"notfound-test" ~tools:[] () in
367367+let test_mcp_tool_not_found () =
368368+ let server = Claude.Mcp_server.v ~name:"notfound-test" ~tools:[] () in
383369 let request =
384370 J.object'
385371 [
386386- J.mem (J.name "jsonrpc") (J.string "2.0");
387387- J.mem (J.name "id") (J.number 4.0);
388388- J.mem (J.name "method") (J.string "tools/call");
389389- J.mem (J.name "params")
390390- (J.object' [ J.mem (J.name "name") (J.string "nonexistent") ]);
372372+ J.member (J.name "jsonrpc") (J.string "2.0");
373373+ J.member (J.name "id") (J.number 4.0);
374374+ J.member (J.name "method") (J.string "tools/call");
375375+ J.member (J.name "params")
376376+ (J.object' [ J.member (J.name "name") (J.string "nonexistent") ]);
391377 ]
392378 in
393379 let response = Claude.Mcp_server.handle_json_message server request in
394380 (* Should return an error response *)
395381 match response with
396396- | Jsont.Object (mems, _) ->
382382+ | Json.Object (mems, _) ->
397383 let has_error = List.exists (fun ((k, _), _) -> k = "error") mems in
398384 Alcotest.(check bool) "has error" true has_error
399385 | _ -> Alcotest.fail "Expected object response"
400386401401-let test_mcp_server_method_not_found () =
402402- let server =
403403- Claude.Mcp_server.create ~name:"method-notfound-test" ~tools:[] ()
404404- in
387387+let test_mcp_method_not_found () =
388388+ let server = Claude.Mcp_server.v ~name:"method-notfound-test" ~tools:[] () in
405389 let request =
406390 J.object'
407391 [
408408- J.mem (J.name "jsonrpc") (J.string "2.0");
409409- J.mem (J.name "id") (J.number 5.0);
410410- J.mem (J.name "method") (J.string "unknown/method");
411411- J.mem (J.name "params") (J.object' []);
392392+ J.member (J.name "jsonrpc") (J.string "2.0");
393393+ J.member (J.name "id") (J.number 5.0);
394394+ J.member (J.name "method") (J.string "unknown/method");
395395+ J.member (J.name "params") (J.object' []);
412396 ]
413397 in
414398 let response = Claude.Mcp_server.handle_json_message server request in
415399 match response with
416416- | Jsont.Object (mems, _) ->
400400+ | Json.Object (mems, _) ->
417401 let has_error = List.exists (fun ((k, _), _) -> k = "error") mems in
418402 Alcotest.(check bool) "has error" true has_error
419403 | _ -> Alcotest.fail "Expected object response"
···424408 Alcotest.test_case "initialize" `Quick test_mcp_server_initialize;
425409 Alcotest.test_case "tools/list" `Quick test_mcp_server_tools_list;
426410 Alcotest.test_case "tools/call" `Quick test_mcp_server_tools_call;
427427- Alcotest.test_case "tool not found" `Quick test_mcp_server_tool_not_found;
428428- Alcotest.test_case "method not found" `Quick
429429- test_mcp_server_method_not_found;
411411+ Alcotest.test_case "tool not found" `Quick test_mcp_tool_not_found;
412412+ Alcotest.test_case "method not found" `Quick test_mcp_method_not_found;
430413 ]
431414432415(* ============================================
···435418436419let test_error_detail_creation () =
437420 let error =
438438- Claude.Proto.Control.Response.error_detail ~code:`Method_not_found
421421+ Claude.Control.Response.error_detail ~code:`Method_not_found
439422 ~message:"Method not found" ()
440423 in
441424 Alcotest.(check int) "error code" (-32601) error.code;
···454437 in
455438 List.iter
456439 (fun (code, expected_int) ->
457457- let err =
458458- Claude.Proto.Control.Response.error_detail ~code ~message:"test" ()
459459- in
440440+ let err = Claude.Control.Response.error_detail ~code ~message:"test" () in
460441 Alcotest.(check int) "error code value" expected_int err.code)
461442 codes
462443463444let test_error_response_encoding () =
464445 let error_detail =
465465- Claude.Proto.Control.Response.error_detail ~code:`Invalid_params
446446+ Claude.Control.Response.error_detail ~code:`Invalid_params
466447 ~message:"Invalid parameters" ()
467448 in
468449 let error_resp =
469469- Claude.Proto.Control.Response.error ~request_id:"test-123"
470470- ~error:error_detail ()
450450+ Claude.Control.Response.error ~request_id:"test-123" ~error:error_detail ()
471451 in
472472- match Jsont.Json.encode Claude.Proto.Control.Response.jsont error_resp with
473473- | Ok json -> (
474474- match Jsont.Json.decode Claude.Proto.Control.Response.jsont json with
475475- | Ok (Claude.Proto.Control.Response.Error decoded) ->
476476- Alcotest.(check string) "request_id" "test-123" decoded.request_id;
477477- Alcotest.(check int) "error code" (-32602) decoded.error.code;
478478- Alcotest.(check string)
479479- "error message" "Invalid parameters" decoded.error.message
480480- | Ok _ -> Alcotest.fail "Wrong response type decoded"
481481- | Error e -> Alcotest.fail e)
482482- | Error e -> Alcotest.fail e
452452+ let json = Json.encode Claude.Control.Response.json error_resp in
453453+ match Json.decode Claude.Control.Response.json json with
454454+ | Ok (Claude.Control.Response.Error decoded) ->
455455+ Alcotest.(check string) "request_id" "test-123" decoded.request_id;
456456+ Alcotest.(check int) "error code" (-32602) decoded.error.code;
457457+ Alcotest.(check string)
458458+ "error message" "Invalid parameters" decoded.error.message
459459+ | Ok _ -> Alcotest.fail "Wrong response type decoded"
460460+ | Error e -> Alcotest.fail (Json.Error.to_string e)
483461484462let structured_error_tests =
485463 [
···494472 Tool_input Tests
495473 ============================================ *)
496474497497-let test_tool_input_get_string () =
498498- let json = J.object' [ J.mem (J.name "foo") (J.string "bar") ] in
475475+let test_tool_input_string () =
476476+ let json = J.object' [ J.member (J.name "foo") (J.string "bar") ] in
499477 let input = Claude.Tool_input.of_json json in
500478 Alcotest.(check (option string))
501501- "get_string foo" (Some "bar")
502502- (Claude.Tool_input.get_string input "foo");
479479+ "string foo" (Some "bar")
480480+ (Claude.Tool_input.string input "foo");
503481 Alcotest.(check (option string))
504504- "get_string missing" None
505505- (Claude.Tool_input.get_string input "missing")
482482+ "string missing" None
483483+ (Claude.Tool_input.string input "missing")
506484507507-let test_tool_input_get_int () =
508508- let json = J.object' [ J.mem (J.name "count") (J.number 42.0) ] in
485485+let test_tool_input_int () =
486486+ let json = J.object' [ J.member (J.name "count") (J.number 42.0) ] in
509487 let input = Claude.Tool_input.of_json json in
510488 Alcotest.(check (option int))
511511- "get_int count" (Some 42)
512512- (Claude.Tool_input.get_int input "count")
489489+ "int count" (Some 42)
490490+ (Claude.Tool_input.int input "count")
513491514514-let test_tool_input_get_float () =
515515- let json = J.object' [ J.mem (J.name "pi") (J.number 3.14159) ] in
492492+let test_tool_input_float () =
493493+ let json = J.object' [ J.member (J.name "pi") (J.number 3.14159) ] in
516494 let input = Claude.Tool_input.of_json json in
517517- match Claude.Tool_input.get_float input "pi" with
495495+ match Claude.Tool_input.float input "pi" with
518496 | Some f ->
519497 Alcotest.(check bool)
520520- "get_float pi approx" true
498498+ "float pi approx" true
521499 (abs_float (f -. 3.14159) < 0.0001)
522500 | None -> Alcotest.fail "Expected float"
523501524524-let test_tool_input_get_bool () =
502502+let test_tool_input_bool () =
525503 let json =
526504 J.object'
527527- [ J.mem (J.name "yes") (J.bool true); J.mem (J.name "no") (J.bool false) ]
505505+ [
506506+ J.member (J.name "yes") (J.bool true);
507507+ J.member (J.name "no") (J.bool false);
508508+ ]
528509 in
529510 let input = Claude.Tool_input.of_json json in
530511 Alcotest.(check (option bool))
531531- "get_bool yes" (Some true)
532532- (Claude.Tool_input.get_bool input "yes");
512512+ "bool yes" (Some true)
513513+ (Claude.Tool_input.bool input "yes");
533514 Alcotest.(check (option bool))
534534- "get_bool no" (Some false)
535535- (Claude.Tool_input.get_bool input "no")
515515+ "bool no" (Some false)
516516+ (Claude.Tool_input.bool input "no")
536517537537-let test_tool_input_get_string_list () =
518518+let test_tool_input_string_list () =
538519 let json =
539520 J.object'
540521 [
541541- J.mem (J.name "items")
522522+ J.member (J.name "items")
542523 (J.list [ J.string "a"; J.string "b"; J.string "c" ]);
543524 ]
544525 in
545526 let input = Claude.Tool_input.of_json json in
546527 Alcotest.(check (option (list string)))
547547- "get_string_list"
528528+ "string_list"
548529 (Some [ "a"; "b"; "c" ])
549549- (Claude.Tool_input.get_string_list input "items")
530530+ (Claude.Tool_input.string_list input "items")
550531551532let tool_input_tests =
552533 [
553553- Alcotest.test_case "get_string" `Quick test_tool_input_get_string;
554554- Alcotest.test_case "get_int" `Quick test_tool_input_get_int;
555555- Alcotest.test_case "get_float" `Quick test_tool_input_get_float;
556556- Alcotest.test_case "get_bool" `Quick test_tool_input_get_bool;
557557- Alcotest.test_case "get_string_list" `Quick test_tool_input_get_string_list;
534534+ Alcotest.test_case "string" `Quick test_tool_input_string;
535535+ Alcotest.test_case "int" `Quick test_tool_input_int;
536536+ Alcotest.test_case "float" `Quick test_tool_input_float;
537537+ Alcotest.test_case "bool" `Quick test_tool_input_bool;
538538+ Alcotest.test_case "string_list" `Quick test_tool_input_string_list;
558539 ]
559540560541(* ============================================
561542 Main test runner
562543 ============================================ *)
563544564564-let () =
565565- Alcotest.run "Claude SDK"
566566- [
567567- ("Protocol", protocol_tests);
568568- ("Tool", tool_tests);
569569- ("Mcp_server", mcp_server_tests);
570570- ("Structured errors", structured_error_tests);
571571- ("Tool_input", tool_input_tests);
572572- ]
545545+let suite =
546546+ ( "claude",
547547+ protocol_tests @ tool_tests @ mcp_server_tests @ structured_error_tests
548548+ @ tool_input_tests )
+2
test/test_claude.mli
···11+val suite : string * unit Alcotest.test_case list
22+(** Test suite. *)
+17
test/test_client.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Tests for Client module: pure helpers only (no I/O). The Client module is
77+ I/O heavy and requires Eio, so we test what we can without spawning
88+ processes. *)
99+1010+(* Client.src is a log source - verify it exists *)
1111+let test_log_source_exists () =
1212+ let name = Logs.Src.name Claude.Client.src in
1313+ Alcotest.(check bool) "has name" true (String.length name > 0)
1414+1515+let suite =
1616+ ( "client",
1717+ [ Alcotest.test_case "log source exists" `Quick test_log_source_exists ] )
+2
test/test_client.mli
···11+val suite : string * unit Alcotest.test_case list
22+(** Test suite. *)
+119
test/test_content_block.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Tests for Content_block module: constructors, accessors, proto roundtrip. *)
77+88+module CB = Claude.Content_block
99+1010+let test_text_block () =
1111+ match CB.text "hello world" with
1212+ | CB.Text t -> Alcotest.(check string) "text" "hello world" (CB.Text.text t)
1313+ | _ -> Alcotest.fail "Expected Text block"
1414+1515+let test_tool_use_block () =
1616+ let input =
1717+ Claude.Tool_input.empty |> Claude.Tool_input.add_string "cmd" "ls"
1818+ in
1919+ match CB.tool_use ~id:"tu-1" ~name:"Bash" ~input with
2020+ | CB.Tool_use tu ->
2121+ Alcotest.(check string) "id" "tu-1" (CB.Tool_use.id tu);
2222+ Alcotest.(check string) "name" "Bash" (CB.Tool_use.name tu);
2323+ Alcotest.(check (option string))
2424+ "input cmd" (Some "ls")
2525+ (Claude.Tool_input.string (CB.Tool_use.input tu) "cmd")
2626+ | _ -> Alcotest.fail "Expected Tool_use block"
2727+2828+let test_tool_result_block () =
2929+ let content = Json.Value.string "success" in
3030+ match CB.tool_result ~tool_use_id:"tu-1" ~content () with
3131+ | CB.Tool_result tr ->
3232+ Alcotest.(check string)
3333+ "tool_use_id" "tu-1"
3434+ (CB.Tool_result.tool_use_id tr);
3535+ Alcotest.(check bool)
3636+ "has content" true
3737+ (Option.is_some (CB.Tool_result.content tr))
3838+ | _ -> Alcotest.fail "Expected Tool_result block"
3939+4040+let test_tool_result_with_error () =
4141+ match CB.tool_result ~tool_use_id:"tu-2" ~is_error:true () with
4242+ | CB.Tool_result tr ->
4343+ Alcotest.(check (option bool))
4444+ "is_error" (Some true)
4545+ (CB.Tool_result.is_error tr)
4646+ | _ -> Alcotest.fail "Expected Tool_result block"
4747+4848+let test_tool_result_no_content () =
4949+ match CB.tool_result ~tool_use_id:"tu-3" () with
5050+ | CB.Tool_result tr ->
5151+ Alcotest.(check bool)
5252+ "no content" true
5353+ (Option.is_none (CB.Tool_result.content tr))
5454+ | _ -> Alcotest.fail "Expected Tool_result block"
5555+5656+let test_thinking_block () =
5757+ match CB.thinking ~thinking:"I need to think..." ~signature:"sig123" with
5858+ | CB.Thinking t ->
5959+ Alcotest.(check string)
6060+ "thinking" "I need to think..." (CB.Thinking.thinking t);
6161+ Alcotest.(check string) "signature" "sig123" (CB.Thinking.signature t)
6262+ | _ -> Alcotest.fail "Expected Thinking block"
6363+6464+let json_roundtrip block =
6565+ let json = Json.encode CB.json block in
6666+ match Json.decode CB.json json with
6767+ | Ok back -> back
6868+ | Error e -> Alcotest.fail (Json.Error.to_string e)
6969+7070+let test_jsont_roundtrip_text () =
7171+ let block = CB.text "roundtrip test" in
7272+ match json_roundtrip block with
7373+ | CB.Text t ->
7474+ Alcotest.(check string) "text" "roundtrip test" (CB.Text.text t)
7575+ | _ -> Alcotest.fail "Expected Text after roundtrip"
7676+7777+let test_jsont_roundtrip_tool_use () =
7878+ let input = Claude.Tool_input.empty |> Claude.Tool_input.add_string "f" "v" in
7979+ let block = CB.tool_use ~id:"id-1" ~name:"MyTool" ~input in
8080+ match json_roundtrip block with
8181+ | CB.Tool_use tu ->
8282+ Alcotest.(check string) "id" "id-1" (CB.Tool_use.id tu);
8383+ Alcotest.(check string) "name" "MyTool" (CB.Tool_use.name tu)
8484+ | _ -> Alcotest.fail "Expected Tool_use after roundtrip"
8585+8686+let test_jsont_roundtrip_thinking () =
8787+ let block = CB.thinking ~thinking:"hmm" ~signature:"s" in
8888+ match json_roundtrip block with
8989+ | CB.Thinking t ->
9090+ Alcotest.(check string) "thinking" "hmm" (CB.Thinking.thinking t);
9191+ Alcotest.(check string) "signature" "s" (CB.Thinking.signature t)
9292+ | _ -> Alcotest.fail "Expected Thinking after roundtrip"
9393+9494+let test_pp_does_not_crash () =
9595+ let block = CB.text "pp test" in
9696+ let buf = Buffer.create 64 in
9797+ let ppf = Format.formatter_of_buffer buf in
9898+ CB.pp ppf block;
9999+ Format.pp_print_flush ppf ();
100100+ Alcotest.(check bool) "pp output" true (Buffer.contents buf <> "")
101101+102102+let suite =
103103+ ( "content_block",
104104+ [
105105+ Alcotest.test_case "text block" `Quick test_text_block;
106106+ Alcotest.test_case "tool_use block" `Quick test_tool_use_block;
107107+ Alcotest.test_case "tool_result block" `Quick test_tool_result_block;
108108+ Alcotest.test_case "tool_result with error" `Quick
109109+ test_tool_result_with_error;
110110+ Alcotest.test_case "tool_result no content" `Quick
111111+ test_tool_result_no_content;
112112+ Alcotest.test_case "thinking block" `Quick test_thinking_block;
113113+ Alcotest.test_case "json roundtrip text" `Quick test_jsont_roundtrip_text;
114114+ Alcotest.test_case "json roundtrip tool_use" `Quick
115115+ test_jsont_roundtrip_tool_use;
116116+ Alcotest.test_case "json roundtrip thinking" `Quick
117117+ test_jsont_roundtrip_thinking;
118118+ Alcotest.test_case "pp does not crash" `Quick test_pp_does_not_crash;
119119+ ] )
+2
test/test_content_block.mli
···11+val suite : string * unit Alcotest.test_case list
22+(** Test suite. *)
+292
test/test_control.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module C = Claude.Control
77+88+let test_interrupt_request () =
99+ match C.Request.interrupt () with
1010+ | C.Request.Interrupt _ -> ()
1111+ | _ -> Alcotest.fail "Expected Interrupt"
1212+1313+let test_permission_request () =
1414+ let input = Json.Value.object' [] in
1515+ match C.Request.permission ~tool_name:"Bash" ~input () with
1616+ | C.Request.Permission p ->
1717+ Alcotest.(check string) "tool_name" "Bash" p.tool_name
1818+ | _ -> Alcotest.fail "Expected Permission"
1919+2020+let test_initialize_request () =
2121+ match C.Request.initialize () with
2222+ | C.Request.Initialize _ -> ()
2323+ | _ -> Alcotest.fail "Expected Initialize"
2424+2525+let test_set_permission_mode_request () =
2626+ match
2727+ C.Request.set_permission_mode ~mode:Claude.Permissions.Mode.Accept_edits ()
2828+ with
2929+ | C.Request.Set_permission_mode spm ->
3030+ Alcotest.(check string)
3131+ "mode" "acceptEdits"
3232+ (Claude.Permissions.Mode.to_string spm.mode)
3333+ | _ -> Alcotest.fail "Expected Set_permission_mode"
3434+3535+let test_set_model_request () =
3636+ match C.Request.set_model ~model:"claude-opus-4" () with
3737+ | C.Request.Set_model sm ->
3838+ Alcotest.(check string) "model" "claude-opus-4" sm.model
3939+ | _ -> Alcotest.fail "Expected Set_model"
4040+4141+let test_get_server_info_request () =
4242+ match C.Request.get_server_info () with
4343+ | C.Request.Get_server_info _ -> ()
4444+ | _ -> Alcotest.fail "Expected Get_server_info"
4545+4646+let test_success_response () =
4747+ match C.Response.success ~request_id:"req-1" () with
4848+ | C.Response.Success s ->
4949+ Alcotest.(check string) "request_id" "req-1" s.request_id
5050+ | _ -> Alcotest.fail "Expected Success"
5151+5252+let test_error_response () =
5353+ let detail =
5454+ C.Response.error_detail ~code:`Method_not_found ~message:"Not found" ()
5555+ in
5656+ match C.Response.error ~request_id:"req-3" ~error:detail () with
5757+ | C.Response.Error e ->
5858+ Alcotest.(check string) "request_id" "req-3" e.request_id;
5959+ Alcotest.(check int) "code" (-32601) e.error.code;
6060+ Alcotest.(check string) "message" "Not found" e.error.message
6161+ | _ -> Alcotest.fail "Expected Error"
6262+6363+let test_error_codes () =
6464+ let codes =
6565+ [
6666+ (`Parse_error, -32700);
6767+ (`Invalid_request, -32600);
6868+ (`Method_not_found, -32601);
6969+ (`Invalid_params, -32602);
7070+ (`Internal_error, -32603);
7171+ (`Custom 42, 42);
7272+ ]
7373+ in
7474+ List.iter
7575+ (fun (code, expected) ->
7676+ let detail = C.Response.error_detail ~code ~message:"test" () in
7777+ Alcotest.(check int) "code" expected detail.code)
7878+ codes
7979+8080+let test_error_code_of_int () =
8181+ let open C.Response.Error_code in
8282+ Alcotest.(check int) "parse_error" (-32700) (to_int (of_int (-32700)));
8383+ Alcotest.(check int) "invalid_request" (-32600) (to_int (of_int (-32600)));
8484+ Alcotest.(check int) "method_not_found" (-32601) (to_int (of_int (-32601)));
8585+ Alcotest.(check int) "invalid_params" (-32602) (to_int (of_int (-32602)));
8686+ Alcotest.(check int) "internal_error" (-32603) (to_int (of_int (-32603)));
8787+ Alcotest.(check int) "custom 42" 42 (to_int (of_int 42))
8888+8989+let test_request_jsont_interrupt () =
9090+ let req = C.Request.interrupt () in
9191+ let json = Json.encode C.Request.json req in
9292+ match Json.decode C.Request.json json with
9393+ | Ok (C.Request.Interrupt _) -> ()
9494+ | Ok _ -> Alcotest.fail "Wrong variant"
9595+ | Error e -> Alcotest.fail (Json.Error.to_string e)
9696+9797+let test_request_jsont_permission () =
9898+ let input =
9999+ Json.Value.object'
100100+ [ Json.Value.member (Json.Value.name "cmd") (Json.Value.string "ls") ]
101101+ in
102102+ let req = C.Request.permission ~tool_name:"Bash" ~input () in
103103+ let json = Json.encode C.Request.json req in
104104+ match Json.decode C.Request.json json with
105105+ | Ok (C.Request.Permission p) ->
106106+ Alcotest.(check string) "tool_name" "Bash" p.tool_name
107107+ | Ok _ -> Alcotest.fail "Wrong variant"
108108+ | Error e -> Alcotest.fail (Json.Error.to_string e)
109109+110110+let test_request_jsont_set_model () =
111111+ let req = C.Request.set_model ~model:"claude-haiku-4" () in
112112+ let json = Json.encode C.Request.json req in
113113+ match Json.decode C.Request.json json with
114114+ | Ok (C.Request.Set_model sm) ->
115115+ Alcotest.(check string) "model" "claude-haiku-4" sm.model
116116+ | Ok _ -> Alcotest.fail "Wrong variant"
117117+ | Error e -> Alcotest.fail (Json.Error.to_string e)
118118+119119+let test_request_jsont_get_server_info () =
120120+ let req = C.Request.get_server_info () in
121121+ let json = Json.encode C.Request.json req in
122122+ match Json.decode C.Request.json json with
123123+ | Ok (C.Request.Get_server_info _) -> ()
124124+ | Ok _ -> Alcotest.fail "Wrong variant"
125125+ | Error e -> Alcotest.fail (Json.Error.to_string e)
126126+127127+let test_request_jsont_hook_callback () =
128128+ let input = Json.Value.object' [] in
129129+ let req = C.Request.hook_callback ~callback_id:"cb-1" ~input () in
130130+ let json = Json.encode C.Request.json req in
131131+ match Json.decode C.Request.json json with
132132+ | Ok (C.Request.Hook_callback hc) ->
133133+ Alcotest.(check string) "callback_id" "cb-1" hc.callback_id
134134+ | Ok _ -> Alcotest.fail "Wrong variant"
135135+ | Error e -> Alcotest.fail (Json.Error.to_string e)
136136+137137+let test_request_jsont_mcp_message () =
138138+ let message = Json.Value.object' [] in
139139+ let req = C.Request.mcp_message ~server_name:"tools" ~message () in
140140+ let json = Json.encode C.Request.json req in
141141+ match Json.decode C.Request.json json with
142142+ | Ok (C.Request.Mcp_message mm) ->
143143+ Alcotest.(check string) "server_name" "tools" mm.server_name
144144+ | Ok _ -> Alcotest.fail "Wrong variant"
145145+ | Error e -> Alcotest.fail (Json.Error.to_string e)
146146+147147+let test_response_jsont_success () =
148148+ let resp = C.Response.success ~request_id:"r1" () in
149149+ let json = Json.encode C.Response.json resp in
150150+ match Json.decode C.Response.json json with
151151+ | Ok (C.Response.Success s) ->
152152+ Alcotest.(check string) "request_id" "r1" s.request_id
153153+ | Ok _ -> Alcotest.fail "Wrong variant"
154154+ | Error e -> Alcotest.fail (Json.Error.to_string e)
155155+156156+let test_response_success_data () =
157157+ let data = Json.Value.string "result_data" in
158158+ let resp = C.Response.success ~request_id:"r2" ~response:data () in
159159+ let json = Json.encode C.Response.json resp in
160160+ match Json.decode C.Response.json json with
161161+ | Ok (C.Response.Success s) ->
162162+ Alcotest.(check bool) "has response" true (Option.is_some s.response)
163163+ | Ok _ -> Alcotest.fail "Wrong variant"
164164+ | Error e -> Alcotest.fail (Json.Error.to_string e)
165165+166166+let test_response_jsont_error () =
167167+ let detail =
168168+ C.Response.error_detail ~code:`Internal_error ~message:"oops" ()
169169+ in
170170+ let resp = C.Response.error ~request_id:"r3" ~error:detail () in
171171+ let json = Json.encode C.Response.json resp in
172172+ match Json.decode C.Response.json json with
173173+ | Ok (C.Response.Error e) ->
174174+ Alcotest.(check string) "request_id" "r3" e.request_id;
175175+ Alcotest.(check int) "code" (-32603) e.error.code;
176176+ Alcotest.(check string) "message" "oops" e.error.message
177177+ | Ok _ -> Alcotest.fail "Wrong variant"
178178+ | Error e -> Alcotest.fail (Json.Error.to_string e)
179179+180180+let test_server_info () =
181181+ let info =
182182+ C.Server_info.create ~version:"2.0.0"
183183+ ~capabilities:[ "hooks"; "structured-output" ]
184184+ ~commands:[ "run" ] ~output_styles:[ "json" ] ()
185185+ in
186186+ Alcotest.(check string) "version" "2.0.0" (C.Server_info.version info);
187187+ Alcotest.(check (list string))
188188+ "capabilities"
189189+ [ "hooks"; "structured-output" ]
190190+ (C.Server_info.capabilities info)
191191+192192+let test_server_info_jsont_roundtrip () =
193193+ let info =
194194+ C.Server_info.create ~version:"1.0.0" ~capabilities:[ "mcp" ] ~commands:[]
195195+ ~output_styles:[] ()
196196+ in
197197+ let json = Json.encode C.Server_info.json info in
198198+ match Json.decode C.Server_info.json json with
199199+ | Ok back ->
200200+ Alcotest.(check string) "version" "1.0.0" (C.Server_info.version back)
201201+ | Error e -> Alcotest.fail (Json.Error.to_string e)
202202+203203+let test_request_envelope () =
204204+ let req = C.Request.interrupt () in
205205+ match C.request ~request_id:"env-1" ~request:req () with
206206+ | C.Request env -> Alcotest.(check string) "request_id" "env-1" env.request_id
207207+ | _ -> Alcotest.fail "Expected Request envelope"
208208+209209+let test_response_envelope () =
210210+ let resp = C.Response.success ~request_id:"x" () in
211211+ match C.response ~response:resp () with
212212+ | C.Response env -> (
213213+ match env.response with
214214+ | C.Response.Success _ -> ()
215215+ | _ -> Alcotest.fail "Expected success response")
216216+ | _ -> Alcotest.fail "Expected Response envelope"
217217+218218+let test_request_envelope_jsont () =
219219+ let req = C.Request.interrupt () in
220220+ let env : C.control_request =
221221+ {
222222+ type_ = `Control_request;
223223+ request_id = "env-1";
224224+ request = req;
225225+ unknown = Claude.Unknown.empty;
226226+ }
227227+ in
228228+ let json = Json.encode C.control_request_jsont env in
229229+ match Json.decode C.control_request_jsont json with
230230+ | Ok back -> Alcotest.(check string) "request_id" "env-1" back.request_id
231231+ | Error e -> Alcotest.fail (Json.Error.to_string e)
232232+233233+let test_response_envelope_jsont () =
234234+ let resp = C.Response.success ~request_id:"x" () in
235235+ let env : C.control_response =
236236+ {
237237+ type_ = `Control_response;
238238+ response = resp;
239239+ unknown = Claude.Unknown.empty;
240240+ }
241241+ in
242242+ let json = Json.encode C.control_response_jsont env in
243243+ match Json.decode C.control_response_jsont json with
244244+ | Ok back -> (
245245+ match back.response with
246246+ | C.Response.Success _ -> ()
247247+ | _ -> Alcotest.fail "Wrong variant")
248248+ | Error e -> Alcotest.fail (Json.Error.to_string e)
249249+250250+let suite =
251251+ ( "control",
252252+ [
253253+ Alcotest.test_case "interrupt request" `Quick test_interrupt_request;
254254+ Alcotest.test_case "permission request" `Quick test_permission_request;
255255+ Alcotest.test_case "initialize request" `Quick test_initialize_request;
256256+ Alcotest.test_case "set_permission_mode request" `Quick
257257+ test_set_permission_mode_request;
258258+ Alcotest.test_case "set_model request" `Quick test_set_model_request;
259259+ Alcotest.test_case "get_server_info request" `Quick
260260+ test_get_server_info_request;
261261+ Alcotest.test_case "success response" `Quick test_success_response;
262262+ Alcotest.test_case "error response" `Quick test_error_response;
263263+ Alcotest.test_case "error codes" `Quick test_error_codes;
264264+ Alcotest.test_case "error code of_int roundtrip" `Quick
265265+ test_error_code_of_int;
266266+ Alcotest.test_case "request json interrupt" `Quick
267267+ test_request_jsont_interrupt;
268268+ Alcotest.test_case "request json permission" `Quick
269269+ test_request_jsont_permission;
270270+ Alcotest.test_case "request json set_model" `Quick
271271+ test_request_jsont_set_model;
272272+ Alcotest.test_case "request json get_server_info" `Quick
273273+ test_request_jsont_get_server_info;
274274+ Alcotest.test_case "request json hook_callback" `Quick
275275+ test_request_jsont_hook_callback;
276276+ Alcotest.test_case "request json mcp_message" `Quick
277277+ test_request_jsont_mcp_message;
278278+ Alcotest.test_case "response json success" `Quick
279279+ test_response_jsont_success;
280280+ Alcotest.test_case "response json success with data" `Quick
281281+ test_response_success_data;
282282+ Alcotest.test_case "response json error" `Quick test_response_jsont_error;
283283+ Alcotest.test_case "server_info" `Quick test_server_info;
284284+ Alcotest.test_case "server_info json roundtrip" `Quick
285285+ test_server_info_jsont_roundtrip;
286286+ Alcotest.test_case "request envelope" `Quick test_request_envelope;
287287+ Alcotest.test_case "response envelope" `Quick test_response_envelope;
288288+ Alcotest.test_case "request envelope json" `Quick
289289+ test_request_envelope_jsont;
290290+ Alcotest.test_case "response envelope json" `Quick
291291+ test_response_envelope_jsont;
292292+ ] )
+2
test/test_control.mli
···11+val suite : string * unit Alcotest.test_case list
22+(** Test suite. *)