+536
ocaml-zulip/CLAUDE.md
+536
ocaml-zulip/CLAUDE.md
···+The library follows OCaml best practices with abstract types (`type t`) per module, comprehensive constructors/accessors, and proper pretty printers. Each core concept gets its own module with a clean interface.+val edit : Client.t -> message_id:int -> ?content:string -> ?topic:string -> unit -> (unit, Error.t) result+val get_events : t -> Client.t -> ?last_event_id:int -> unit -> (Event.t list, Error.t) result+- https://github.com/zulip/python-zulip-api/blob/main/zulip_botserver/zulip_botserver/server.py+- Bot framework: https://github.com/zulip/python-zulip-api/blob/main/zulip_bots/zulip_bots/lib.py+- Bot server: https://github.com/zulip/python-zulip-api/blob/main/zulip_botserver/zulip_botserver/server.py+The design adapts these Python patterns to idiomatic OCaml with abstract types, proper error handling, and EIO's structured concurrency for robust, type-safe Zulip integration.
+45
ocaml-zulip/dune-project
+45
ocaml-zulip/dune-project
···+(description "High-quality OCaml bindings to the Zulip REST API using EIO for async operations")
+20
ocaml-zulip/examples/bot_config.toml
+20
ocaml-zulip/examples/bot_config.toml
···
+44
ocaml-zulip/examples/bot_example.ml
+44
ocaml-zulip/examples/bot_example.ml
···+Printf.printf "✅ Message topic: %s\n" (match Zulip.Message.topic message with Some t -> t | None -> "none");+Printf.printf "Note: This uses mock responses since we're not connected to a real Zulip server.\n"
+1
ocaml-zulip/examples/bot_example.mli
+1
ocaml-zulip/examples/bot_example.mli
···
+23
ocaml-zulip/examples/dune
+23
ocaml-zulip/examples/dune
···
+47
ocaml-zulip/examples/example.ml
+47
ocaml-zulip/examples/example.ml
···
+1
ocaml-zulip/examples/example.mli
+1
ocaml-zulip/examples/example.mli
···
+16
ocaml-zulip/examples/example_bot_config.toml
+16
ocaml-zulip/examples/example_bot_config.toml
···
+9
ocaml-zulip/examples/example_zuliprc.toml
+9
ocaml-zulip/examples/example_zuliprc.toml
+346
ocaml-zulip/examples/realistic_bot_example.ml
+346
ocaml-zulip/examples/realistic_bot_example.ml
···+let weather_url = Printf.sprintf "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s" city api_key in+(match Zulip_bot.Bot_handler.handle_message_with_env weather_handler env test_weather_message with
+1
ocaml-zulip/examples/realistic_bot_example.mli
+1
ocaml-zulip/examples/realistic_bot_example.mli
···
+100
ocaml-zulip/examples/toml_example.ml
+100
ocaml-zulip/examples/toml_example.ml
···
+1
ocaml-zulip/examples/toml_example.mli
+1
ocaml-zulip/examples/toml_example.mli
···
+1
ocaml-zulip/lib/dune
+1
ocaml-zulip/lib/dune
···
+58
ocaml-zulip/lib/zulip/lib/auth.ml
+58
ocaml-zulip/lib/zulip/lib/auth.ml
···+Error (Error.create ~code:(Other "parse_error") ~msg:("Error parsing zuliprc: " ^ Printexc.to_string exn) ())
+8
ocaml-zulip/lib/zulip/lib/auth.mli
+8
ocaml-zulip/lib/zulip/lib/auth.mli
···
+50
ocaml-zulip/lib/zulip/lib/channel.ml
+50
ocaml-zulip/lib/zulip/lib/channel.ml
···+let create ~name ~description ?(invite_only = false) ?(history_public_to_subscribers = true) () =+Error (Error.create ~code:(Other "json_parse_error") ~msg:("Channel JSON parsing failed: " ^ Printexc.to_string exn) ())
+16
ocaml-zulip/lib/zulip/lib/channel.mli
+16
ocaml-zulip/lib/zulip/lib/channel.mli
···
+58
ocaml-zulip/lib/zulip/lib/channels.ml
+58
ocaml-zulip/lib/zulip/lib/channels.ml
···+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Invalid streams response format" ()))+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Streams response must be an object" ()))
+5
ocaml-zulip/lib/zulip/lib/channels.mli
+5
ocaml-zulip/lib/zulip/lib/channels.mli
···
+27
ocaml-zulip/lib/zulip/lib/client.ml
+27
ocaml-zulip/lib/zulip/lib/client.ml
···
+23
ocaml-zulip/lib/zulip/lib/client.mli
+23
ocaml-zulip/lib/zulip/lib/client.mli
···
+4
ocaml-zulip/lib/zulip/lib/dune
+4
ocaml-zulip/lib/zulip/lib/dune
+53
ocaml-zulip/lib/zulip/lib/error.ml
+53
ocaml-zulip/lib/zulip/lib/error.ml
···+type json = [`Null | `Bool of bool | `Float of float | `String of string | `A of json list | `O of (string * json) list]
+19
ocaml-zulip/lib/zulip/lib/error.mli
+19
ocaml-zulip/lib/zulip/lib/error.mli
···+type json = [`Null | `Bool of bool | `Float of float | `String of string | `A of json list | `O of (string * json) list]
+40
ocaml-zulip/lib/zulip/lib/event.ml
+40
ocaml-zulip/lib/zulip/lib/event.ml
···+Error (Error.create ~code:(Other "json_parse_error") ~msg:("Event JSON parsing failed: " ^ Printexc.to_string exn) ())
+7
ocaml-zulip/lib/zulip/lib/event.mli
+7
ocaml-zulip/lib/zulip/lib/event.mli
+51
ocaml-zulip/lib/zulip/lib/event_queue.ml
+51
ocaml-zulip/lib/zulip/lib/event_queue.ml
···+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Invalid register response: missing queue_id" ()))+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Register response must be an object" ()))+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Invalid events response format" ()))+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Events response must be an object" ()))
+12
ocaml-zulip/lib/zulip/lib/event_queue.mli
+12
ocaml-zulip/lib/zulip/lib/event_queue.mli
···
+19
ocaml-zulip/lib/zulip/lib/event_type.ml
+19
ocaml-zulip/lib/zulip/lib/event_type.ml
···
+9
ocaml-zulip/lib/zulip/lib/event_type.mli
+9
ocaml-zulip/lib/zulip/lib/event_type.mli
+43
ocaml-zulip/lib/zulip/lib/message.ml
+43
ocaml-zulip/lib/zulip/lib/message.ml
···
+21
ocaml-zulip/lib/zulip/lib/message.mli
+21
ocaml-zulip/lib/zulip/lib/message.mli
···
+33
ocaml-zulip/lib/zulip/lib/message_response.ml
+33
ocaml-zulip/lib/zulip/lib/message_response.ml
···+Error (Error.create ~code:(Other "parse_error") ~msg:("Failed to parse message response: " ^ msg) ())+Error (Error.create ~code:(Other "parse_error") ~msg:"Failed to parse message response: missing field" ())+Error (Error.create ~code:(Other "parse_error") ~msg:"Expected JSON object for message response" ())
+6
ocaml-zulip/lib/zulip/lib/message_response.mli
+6
ocaml-zulip/lib/zulip/lib/message_response.mli
+12
ocaml-zulip/lib/zulip/lib/message_type.ml
+12
ocaml-zulip/lib/zulip/lib/message_type.ml
···
+5
ocaml-zulip/lib/zulip/lib/message_type.mli
+5
ocaml-zulip/lib/zulip/lib/message_type.mli
+46
ocaml-zulip/lib/zulip/lib/messages.ml
+46
ocaml-zulip/lib/zulip/lib/messages.ml
···+match Client.request client ~method_:`PATCH ~path:("/messages/" ^ string_of_int message_id) ~params () with+match Client.request client ~method_:`DELETE ~path:("/messages/" ^ string_of_int message_id) () with+(match narrow with Some n -> List.mapi (fun i s -> ("narrow[" ^ string_of_int i ^ "]", s)) n | None -> []) in
+12
ocaml-zulip/lib/zulip/lib/messages.mli
+12
ocaml-zulip/lib/zulip/lib/messages.mli
···+val edit : Client.t -> message_id:int -> ?content:string -> ?topic:string -> unit -> (unit, Error.t) result
+54
ocaml-zulip/lib/zulip/lib/user.ml
+54
ocaml-zulip/lib/zulip/lib/user.ml
···+Error (Error.create ~code:(Other "json_parse_error") ~msg:("User JSON parsing failed: " ^ Printexc.to_string exn) ())
+18
ocaml-zulip/lib/zulip/lib/user.mli
+18
ocaml-zulip/lib/zulip/lib/user.mli
···
+46
ocaml-zulip/lib/zulip/lib/users.ml
+46
ocaml-zulip/lib/zulip/lib/users.ml
···+| _ -> Error (Error.create ~code:(Other "api_error") ~msg:"Users response must be an object" ()))
+4
ocaml-zulip/lib/zulip/lib/users.mli
+4
ocaml-zulip/lib/zulip/lib/users.mli
+14
ocaml-zulip/lib/zulip/test/dune
+14
ocaml-zulip/lib/zulip/test/dune
+121
ocaml-zulip/lib/zulip/test/test_eio.ml
+121
ocaml-zulip/lib/zulip/test/test_eio.ml
···+Alcotest.(check string) "client server url" "Client(server=https://test.zulipchat.com)" pp_result;+Alcotest.(check string) "server_url" "https://eio-test.zulipchat.com" (Zulip.Auth.server_url auth);+Alcotest.(check (option string)) "message topic" (Some "Testing") (Zulip.Message.topic message);
+1
ocaml-zulip/lib/zulip/test/test_eio.mli
+1
ocaml-zulip/lib/zulip/test/test_eio.mli
···
+76
ocaml-zulip/lib/zulip/test/test_toml_support.ml
+76
ocaml-zulip/lib/zulip/test/test_toml_support.ml
···+Alcotest.(check string) "server_url" "https://test.zulipchat.com" (Zulip.Auth.server_url auth);+Alcotest.(check string) "server_url" "https://root.zulipchat.com" (Zulip.Auth.server_url auth);
+1
ocaml-zulip/lib/zulip/test/test_toml_support.mli
+1
ocaml-zulip/lib/zulip/test/test_toml_support.mli
···
+128
ocaml-zulip/lib/zulip/test/test_zulip.ml
+128
ocaml-zulip/lib/zulip/test/test_zulip.ml
···+Alcotest.(check string) "channel message type" "stream" (Zulip.Message_type.to_string `Channel);
+1
ocaml-zulip/lib/zulip/test/test_zulip.mli
+1
ocaml-zulip/lib/zulip/test/test_zulip.mli
···
+90
ocaml-zulip/lib/zulip_bot/lib/bot_config.ml
+90
ocaml-zulip/lib/zulip_bot/lib/bot_config.ml
···+Error (Zulip.Error.create ~code:(Other "toml_parse_error") ~msg:("Failed to parse TOML: " ^ msg) ())+Error (Zulip.Error.create ~code:(Other "file_error") ~msg:("Cannot read config file: " ^ msg) ())+Error (Zulip.Error.create ~code:(Other "parse_error") ~msg:("Error parsing config: " ^ Printexc.to_string exn) ())+let config_key = String.sub key (String.length prefix) (String.length key - String.length prefix) in+Error (Zulip.Error.create ~code:(Other "env_error") ~msg:("Error reading environment: " ^ Printexc.to_string exn) ())+| None -> Error (Zulip.Error.create ~code:(Other "config_missing") ~msg:("Required config key missing: " ^ key) ())
+24
ocaml-zulip/lib/zulip_bot/lib/bot_config.mli
+24
ocaml-zulip/lib/zulip_bot/lib/bot_config.mli
···
+108
ocaml-zulip/lib/zulip_bot/lib/bot_handler.ml
+108
ocaml-zulip/lib/zulip_bot/lib/bot_handler.ml
···+let create ~message_id ~sender_email ~sender_full_name ~content ~message_type ?topic ?channel () =+Handler.handle_message ~config:t.config ~storage:t.storage ~identity:t.identity ~message ~env:mock_env
+110
ocaml-zulip/lib/zulip_bot/lib/bot_handler.mli
+110
ocaml-zulip/lib/zulip_bot/lib/bot_handler.mli
···+val handle_message_with_env : t -> Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result
+63
ocaml-zulip/lib/zulip_bot/lib/bot_runner.ml
+63
ocaml-zulip/lib/zulip_bot/lib/bot_runner.ml
···+Error (Zulip.Error.create ~code:(Other "webhook_error") ~msg:("Webhook processing failed: " ^ Printexc.to_string exn) ())
+25
ocaml-zulip/lib/zulip_bot/lib/bot_runner.mli
+25
ocaml-zulip/lib/zulip_bot/lib/bot_runner.mli
···
+28
ocaml-zulip/lib/zulip_bot/lib/bot_storage.ml
+28
ocaml-zulip/lib/zulip_bot/lib/bot_storage.ml
···
+21
ocaml-zulip/lib/zulip_bot/lib/bot_storage.mli
+21
ocaml-zulip/lib/zulip_bot/lib/bot_storage.mli
···
+4
ocaml-zulip/lib/zulip_bot/lib/dune
+4
ocaml-zulip/lib/zulip_bot/lib/dune
+4
ocaml-zulip/lib/zulip_bot/test/dune
+4
ocaml-zulip/lib/zulip_bot/test/dune
+73
ocaml-zulip/lib/zulip_bot/test/test_bot_config.ml
+73
ocaml-zulip/lib/zulip_bot/test/test_bot_config.ml
···+Alcotest.(check (option string)) "name" (Some "Test Bot") (Zulip_bot.Bot_config.get config ~key:"name");+Alcotest.(check (option string)) "version" (Some "1.0") (Zulip_bot.Bot_config.get config ~key:"version");+Alcotest.(check (option string)) "api_key" (Some "test-key") (Zulip_bot.Bot_config.get config ~key:"api_key");+Alcotest.(check (option string)) "timeout" (Some "30") (Zulip_bot.Bot_config.get config ~key:"timeout");+Alcotest.(check (option string)) "enabled" (Some "true") (Zulip_bot.Bot_config.get config ~key:"enabled");+Alcotest.(check (option string)) "caching" (Some "false") (Zulip_bot.Bot_config.get config ~key:"caching");
+35
ocaml-zulip/lib/zulip_botserver/lib/bot_registry.mli
+35
ocaml-zulip/lib/zulip_botserver/lib/bot_registry.mli
···+create_config:(Server_config.Bot_config.t -> (Zulip_bot.Bot_config.t, Zulip.Error.t) result) ->+val create_handler : t -> Server_config.Bot_config.t -> Zulip.Client.t -> (Zulip_bot.Bot_handler.t, Zulip.Error.t) result
+22
ocaml-zulip/lib/zulip_botserver/lib/bot_server.mli
+22
ocaml-zulip/lib/zulip_botserver/lib/bot_server.mli
···
+5
ocaml-zulip/lib/zulip_botserver/lib/dune
+5
ocaml-zulip/lib/zulip_botserver/lib/dune
+40
ocaml-zulip/lib/zulip_botserver/lib/server_config.mli
+40
ocaml-zulip/lib/zulip_botserver/lib/server_config.mli
···
+33
ocaml-zulip/lib/zulip_botserver/lib/webhook_handler.mli
+33
ocaml-zulip/lib/zulip_botserver/lib/webhook_handler.mli
···