My agentic slop goes here. Not intended for anyone else!
1let src = Logs.Src.create "claude.client" ~doc:"Claude client"
2module Log = (val Logs.src_log src : Logs.LOG)
3
4(** Control response builders using jsont *)
5module Control_response = struct
6 let success ~request_id ~response =
7 Jsont.Json.object' [
8 Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
9 Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
10 Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "success");
11 Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
12 Jsont.Json.mem (Jsont.Json.name "response") response;
13 ]);
14 ]
15
16 let error ~request_id ~message =
17 Jsont.Json.object' [
18 Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
19 Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
20 Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "error");
21 Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
22 Jsont.Json.mem (Jsont.Json.name "error") (Jsont.Json.string message);
23 ]);
24 ]
25end
26
27(* Helper functions for JSON manipulation using jsont *)
28let json_to_string json =
29 match Jsont_bytesrw.encode_string' Jsont.json json with
30 | Ok s -> s
31 | Error err -> failwith (Jsont.Error.to_string err)
32
33(* JSON construction helpers using jsont *)
34let json_string s = Jsont.Json.string s
35let json_null () = Jsont.Json.null ()
36
37let json_object pairs =
38 Jsont.Json.object' (List.map (fun (k, v) -> Jsont.Json.mem (Jsont.Json.name k) v) pairs)
39
40type t = {
41 transport : Transport.t;
42 permission_callback : Permissions.callback option;
43 permission_log : Permissions.Rule.t list ref option;
44 hook_callbacks : (string, Hooks.callback) Hashtbl.t;
45 mutable next_callback_id : int;
46 mutable session_id : string option;
47 control_responses : (string, Jsont.json) Hashtbl.t;
48 control_mutex : Eio.Mutex.t;
49 control_condition : Eio.Condition.t;
50}
51
52let handle_control_request t (ctrl_req : Incoming.Control_request.t) =
53 let request_id = Incoming.Control_request.request_id ctrl_req in
54 Log.info (fun m -> m "Handling control request: %s" (Incoming.Control_request.subtype ctrl_req));
55
56 match Incoming.Control_request.request ctrl_req with
57 | Incoming.Control_request.Can_use_tool req ->
58 let tool_name = Incoming.Control_request.Can_use_tool.tool_name req in
59 let input = Incoming.Control_request.Can_use_tool.input req in
60 Log.info (fun m -> m "Permission request for tool '%s' with input: %s"
61 tool_name (json_to_string input));
62 (* TODO: Parse permission_suggestions properly *)
63 let context = Permissions.Context.create ~suggestions:[] () in
64
65 Log.info (fun m -> m "Invoking permission callback for tool: %s" tool_name);
66 let result = match t.permission_callback with
67 | Some callback ->
68 Log.info (fun m -> m "Using custom permission callback");
69 callback ~tool_name ~input ~context
70 | None ->
71 Log.info (fun m -> m "Using default allow callback");
72 Permissions.default_allow_callback ~tool_name ~input ~context
73 in
74 Log.info (fun m -> m "Permission callback returned: %s"
75 (match result with
76 | Permissions.Result.Allow _ -> "ALLOW"
77 | Permissions.Result.Deny _ -> "DENY"));
78
79 (* Convert permission result to CLI format *)
80 let response_data = match result with
81 | Permissions.Result.Allow { updated_input; updated_permissions = _; unknown = _ } ->
82 let updated_input = Option.value updated_input ~default:input in
83 json_object [
84 ("behavior", json_string "allow");
85 ("updatedInput", updated_input);
86 ]
87 | Permissions.Result.Deny { message; interrupt = _; unknown = _ } ->
88 json_object [
89 ("behavior", json_string "deny");
90 ("message", json_string message);
91 ]
92 in
93 let response = Control_response.success ~request_id ~response:response_data in
94 Log.info (fun m -> m "Sending control response: %s" (json_to_string response));
95 Transport.send t.transport response
96
97 | Incoming.Control_request.Hook_callback req ->
98 let callback_id = Incoming.Control_request.Hook_callback.callback_id req in
99 let input = Incoming.Control_request.Hook_callback.input req in
100 let tool_use_id = Incoming.Control_request.Hook_callback.tool_use_id req in
101 Log.info (fun m -> m "Hook callback request for callback_id: %s" callback_id);
102
103 (try
104 let callback = Hashtbl.find t.hook_callbacks callback_id in
105 let context = Hooks.Context.create () in
106 let result = callback ~input ~tool_use_id ~context in
107
108 let result_json = match Jsont.Json.encode Hooks.result_jsont result with
109 | Ok j -> j
110 | Error msg -> failwith ("Failed to encode hook result: " ^ msg)
111 in
112 let response = Control_response.success ~request_id ~response:result_json in
113 Log.info (fun m -> m "Hook callback succeeded, sending response");
114 Transport.send t.transport response
115 with
116 | Not_found ->
117 let error_msg = Printf.sprintf "Hook callback not found: %s" callback_id in
118 Log.err (fun m -> m "%s" error_msg);
119 Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
120 | exn ->
121 let error_msg = Printf.sprintf "Hook callback error: %s" (Printexc.to_string exn) in
122 Log.err (fun m -> m "%s" error_msg);
123 Transport.send t.transport (Control_response.error ~request_id ~message:error_msg))
124
125 | Incoming.Control_request.Unknown (subtype, _) ->
126 let error_msg = Printf.sprintf "Unsupported control request: %s" subtype in
127 Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
128
129let handle_control_response t control_resp =
130 let request_id = match control_resp.Sdk_control.response with
131 | Sdk_control.Response.Success s -> s.request_id
132 | Sdk_control.Response.Error e -> e.request_id
133 in
134 Log.debug (fun m -> m "Received control response for request_id: %s" request_id);
135
136 (* Store the response as JSON and signal waiting threads *)
137 let json = match Jsont.Json.encode Sdk_control.control_response_jsont control_resp with
138 | Ok j -> j
139 | Error err -> failwith ("Failed to encode control response: " ^ err)
140 in
141 Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
142 Hashtbl.replace t.control_responses request_id json;
143 Eio.Condition.broadcast t.control_condition
144 )
145
146let handle_messages t =
147 let rec loop () =
148 match Transport.receive_line t.transport with
149 | None ->
150 (* EOF *)
151 Log.debug (fun m -> m "Handle messages: EOF received");
152 Seq.Nil
153 | Some line ->
154 (* Use unified Incoming codec for all message types *)
155 match Jsont_bytesrw.decode_string' Incoming.jsont line with
156 | Ok (Incoming.Message msg) ->
157 Log.info (fun m -> m "← %a" Message.pp msg);
158
159 (* Extract session ID from system messages *)
160 (match msg with
161 | Message.System sys ->
162 (match Message.System.session_id sys with
163 | Some session_id ->
164 t.session_id <- Some session_id;
165 Log.debug (fun m -> m "Stored session ID: %s" session_id)
166 | None -> ())
167 | _ -> ());
168
169 Seq.Cons (msg, loop)
170
171 | Ok (Incoming.Control_response resp) ->
172 handle_control_response t resp;
173 loop ()
174
175 | Ok (Incoming.Control_request ctrl_req) ->
176 Log.info (fun m -> m "Received control request: %s (request_id: %s)"
177 (Incoming.Control_request.subtype ctrl_req)
178 (Incoming.Control_request.request_id ctrl_req));
179 handle_control_request t ctrl_req;
180 loop ()
181
182 | Error err ->
183 Log.err (fun m -> m "Failed to decode incoming message: %s\nLine: %s"
184 (Jsont.Error.to_string err) line);
185 loop ()
186 in
187 Log.debug (fun m -> m "Starting message handler");
188 loop
189
190let create ?(options = Options.default) ~sw ~process_mgr () =
191 (* Automatically enable permission prompt tool when callback is configured
192 (matching Python SDK behavior in client.py:104-121) *)
193 let options =
194 match Options.permission_callback options with
195 | Some _ when Options.permission_prompt_tool_name options = None ->
196 (* Set permission_prompt_tool_name to "stdio" to enable control protocol *)
197 Options.with_permission_prompt_tool_name "stdio" options
198 | _ -> options
199 in
200 let transport = Transport.create ~sw ~process_mgr ~options () in
201
202 (* Setup hook callbacks *)
203 let hook_callbacks = Hashtbl.create 16 in
204 let next_callback_id = ref 0 in
205
206 let t = {
207 transport;
208 permission_callback = Options.permission_callback options;
209 permission_log = None;
210 hook_callbacks;
211 next_callback_id = 0;
212 session_id = None;
213 control_responses = Hashtbl.create 16;
214 control_mutex = Eio.Mutex.create ();
215 control_condition = Eio.Condition.create ();
216 } in
217
218 (* Register hooks and send initialize if hooks are configured *)
219 (match Options.hooks options with
220 | Some hooks_config ->
221 Log.info (fun m -> m "Registering hooks...");
222
223 (* Build hooks configuration with callback IDs *)
224 let hooks_json = List.fold_left (fun acc (event, matchers) ->
225 let event_name = Hooks.event_to_string event in
226 let matchers_json = List.map (fun matcher ->
227 let callback_ids = List.map (fun callback ->
228 let callback_id = Printf.sprintf "hook_%d" !next_callback_id in
229 incr next_callback_id;
230 Hashtbl.add hook_callbacks callback_id callback;
231 Log.debug (fun m -> m "Registered callback: %s for event: %s" callback_id event_name);
232 callback_id
233 ) matcher.Hooks.callbacks in
234 json_object [
235 "matcher", (match matcher.Hooks.matcher with
236 | Some p -> json_string p
237 | None -> json_null ());
238 "hookCallbackIds", Jsont.Json.list (List.map (fun id -> json_string id) callback_ids);
239 ]
240 ) matchers in
241 (event_name, Jsont.Json.list matchers_json) :: acc
242 ) [] hooks_config in
243
244 (* Send initialize control request *)
245 let initialize_msg = json_object [
246 "type", json_string "control_request";
247 "request_id", json_string "init_hooks";
248 "request", json_object [
249 "subtype", json_string "initialize";
250 "hooks", json_object hooks_json;
251 ]
252 ] in
253 Log.info (fun m -> m "Sending hooks initialize request");
254 Transport.send t.transport initialize_msg;
255 t.next_callback_id <- !next_callback_id
256 | None -> ());
257
258 t
259
260let query t prompt =
261 let msg = Message.user_string prompt in
262 Log.info (fun m -> m "→ %a" Message.pp msg);
263 let json = Message.to_json msg in
264 Transport.send t.transport json
265
266let send_message t msg =
267 Log.info (fun m -> m "→ %a" Message.pp msg);
268 let json = Message.to_json msg in
269 Transport.send t.transport json
270
271let send_user_message t user_msg =
272 let msg = Message.User user_msg in
273 Log.info (fun m -> m "→ %a" Message.pp msg);
274 let json = Message.User.to_json user_msg in
275 Transport.send t.transport json
276
277let receive t =
278 handle_messages t
279
280let receive_all t =
281 let rec collect acc seq =
282 match seq () with
283 | Seq.Nil ->
284 Log.debug (fun m -> m "End of message sequence (%d messages)" (List.length acc));
285 List.rev acc
286 | Seq.Cons (Message.Result _ as msg, _) ->
287 Log.debug (fun m -> m "Received final Result message");
288 List.rev (msg :: acc)
289 | Seq.Cons (msg, rest) ->
290 collect (msg :: acc) rest
291 in
292 collect [] (handle_messages t)
293
294let interrupt t =
295 Transport.interrupt t.transport
296
297let discover_permissions t =
298 let log = ref [] in
299 let callback = Permissions.discovery_callback log in
300 { t with
301 permission_callback = Some callback;
302 permission_log = Some log
303 }
304
305let get_discovered_permissions t =
306 match t.permission_log with
307 | Some log -> !log
308 | None -> []
309
310let with_permission_callback t callback =
311 { t with permission_callback = Some callback }
312
313(* Helper to send a control request and wait for response *)
314let send_control_request t ~request_id request =
315 (* Send the control request *)
316 let control_msg = Sdk_control.create_request ~request_id ~request () in
317 let json = match Jsont.Json.encode Sdk_control.jsont control_msg with
318 | Ok j -> j
319 | Error msg -> failwith ("Failed to encode control request: " ^ msg)
320 in
321 Log.info (fun m -> m "Sending control request: %s" (json_to_string json));
322 Transport.send t.transport json;
323
324 (* Wait for the response with timeout *)
325 let max_wait = 10.0 in (* 10 seconds timeout *)
326 let start_time = Unix.gettimeofday () in
327
328 let rec wait_for_response () =
329 Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
330 match Hashtbl.find_opt t.control_responses request_id with
331 | Some response_json ->
332 (* Remove it from the table *)
333 Hashtbl.remove t.control_responses request_id;
334 response_json
335 | None ->
336 let elapsed = Unix.gettimeofday () -. start_time in
337 if elapsed > max_wait then
338 raise (Failure (Printf.sprintf "Timeout waiting for control response: %s" request_id))
339 else (
340 (* Release mutex and wait for signal *)
341 Eio.Condition.await_no_mutex t.control_condition;
342 wait_for_response ()
343 )
344 )
345 in
346
347 let response_json = wait_for_response () in
348 Log.debug (fun m -> m "Received control response: %s" (json_to_string response_json));
349
350 (* Parse the response - extract the "response" field using jsont codec *)
351 let response_field_codec = Jsont.Object.map ~kind:"ResponseField" Fun.id
352 |> Jsont.Object.mem "response" Jsont.json ~enc:Fun.id
353 |> Jsont.Object.finish
354 in
355 let response_data = match Jsont.Json.decode response_field_codec response_json with
356 | Ok r -> r
357 | Error msg -> raise (Invalid_argument ("Failed to extract response field: " ^ msg))
358 in
359 let response = match Jsont.Json.decode Sdk_control.Response.jsont response_data with
360 | Ok r -> r
361 | Error msg -> raise (Invalid_argument ("Failed to decode response: " ^ msg))
362 in
363 match response with
364 | Sdk_control.Response.Success s -> s.response
365 | Sdk_control.Response.Error e ->
366 raise (Failure (Printf.sprintf "Control request failed: %s" e.error))
367
368let set_permission_mode t mode =
369 let request_id = Printf.sprintf "set_perm_mode_%f" (Unix.gettimeofday ()) in
370 let request = Sdk_control.Request.set_permission_mode ~mode () in
371 let _response = send_control_request t ~request_id request in
372 Log.info (fun m -> m "Permission mode set to: %a" Permissions.Mode.pp mode)
373
374let set_model t model =
375 let model_str = Model.to_string model in
376 let request_id = Printf.sprintf "set_model_%f" (Unix.gettimeofday ()) in
377 let request = Sdk_control.Request.set_model ~model:model_str () in
378 let _response = send_control_request t ~request_id request in
379 Log.info (fun m -> m "Model set to: %a" Model.pp model)
380
381let set_model_string t model_str =
382 set_model t (Model.of_string model_str)
383
384let get_server_info t =
385 let request_id = Printf.sprintf "get_server_info_%f" (Unix.gettimeofday ()) in
386 let request = Sdk_control.Request.get_server_info () in
387 match send_control_request t ~request_id request with
388 | Some response_data ->
389 let server_info = match Jsont.Json.decode Sdk_control.Server_info.jsont response_data with
390 | Ok si -> si
391 | Error msg -> raise (Invalid_argument ("Failed to decode server info: " ^ msg))
392 in
393 Log.info (fun m -> m "Retrieved server info: %a" Sdk_control.Server_info.pp server_info);
394 server_info
395 | None ->
396 raise (Failure "No response data from get_server_info request")