···4455My target is to use the OCaml EIO direct-style library, with an idiomatic as
66possible API that implements it. For JSON parsing, using the jsonm library is
77-right. For HTTPS, use cohttp-eio with the tls-eio library.
77+right. For HTTPS, use cohttp-eio with the tls-eio library. You have access to
88+an OCaml LSP via MCP which provides type hints and other language server
99+features after you complete a `dune build`.
810911# OCaml Zulip Library Design
1012···518520- `jsonm` - Streaming JSON codec
519521- `uri` - URI parsing and manipulation
520522- `base64` - Base64 encoding for authentication
523523+524524+# Architecture Analysis: zulip_bot vs zulip_botserver
525525+526526+## Library Separation
527527+528528+### `zulip_bot` - Individual Bot Framework
529529+**Purpose**: Library for building and running a single bot instance
530530+531531+**Key Components**:
532532+- `Bot_handler` - Interface for bot logic with EIO environment access
533533+- `Bot_runner` - Manages lifecycle of one bot (real-time events or webhook mode)
534534+- `Bot_config` - Configuration for a single bot
535535+- `Bot_storage` - Simple in-memory storage for bot state
536536+537537+**Usage Pattern**:
538538+```ocaml
539539+(* Run a single bot directly *)
540540+let my_bot = Bot_handler.create (module My_echo_bot) ~config ~storage ~identity in
541541+let runner = Bot_runner.create ~client ~handler:my_bot in
542542+Bot_runner.run_realtime runner (* Bot connects to Zulip events API directly *)
543543+```
544544+545545+### `zulip_botserver` - Multi-Bot Server Infrastructure
546546+**Purpose**: HTTP server that manages multiple bots via webhooks
547547+548548+**Key Components**:
549549+- `Bot_server` - HTTP server receiving webhook events from Zulip
550550+- `Bot_registry` - Manages multiple bot instances
551551+- `Server_config` - Configuration for multiple bots + server settings
552552+- `Webhook_handler` - Parses incoming webhook requests and routes to appropriate bots
553553+554554+**Usage Pattern**:
555555+```ocaml
556556+(* Run a server hosting multiple bots *)
557557+let registry = Bot_registry.create () in
558558+Bot_registry.register registry echo_bot_module;
559559+Bot_registry.register registry weather_bot_module;
560560+561561+let server = Bot_server.create ~env ~config ~registry in
562562+Bot_server.run server (* HTTP server waits for webhook calls *)
563563+```
564564+565565+## EIO Environment Requirements
566566+567567+### Why Bot Handlers Need Direct EIO Access
568568+569569+Bot handlers require direct access to the EIO environment for legitimate I/O operations beyond HTTP requests to Zulip:
570570+571571+1. **Network Operations**: Custom HTTP requests, API calls to external services
572572+2. **File System Operations**: Reading configuration files, CSV dictionaries, logs
573573+3. **Resource Management**: Proper cleanup via structured concurrency
574574+575575+### Example: URL Checker Bot
576576+```ocaml
577577+module Url_checker_bot : Zulip_bot.Bot_handler.Bot_handler = struct
578578+ let handle_message ~config ~storage ~identity ~message ~env =
579579+ match parse_command message with
580580+ | "!check", url ->
581581+ (* Direct EIO network access needed *)
582582+ Eio.Switch.run @@ fun sw ->
583583+ let client = Cohttp_eio.Client.make ~sw env#net in
584584+ let response = Cohttp_eio.Client.head ~sw client (Uri.of_string url) in
585585+ let status = Cohttp.Code.code_of_status response.status in
586586+ Ok (Response.reply ~content:(format_status_message url status))
587587+ | _ -> Ok Response.none
588588+end
589589+```
590590+591591+### Example: CSV Dictionary Bot
592592+```ocaml
593593+module Csv_dict_bot : Zulip_bot.Bot_handler.Bot_handler = struct
594594+ let handle_message ~config ~storage ~identity ~message ~env =
595595+ match parse_command message with
596596+ | "!lookup", term ->
597597+ (* Direct EIO file system access needed *)
598598+ let csv_path = Bot_config.get_required config ~key:"csv_file" in
599599+ let content = Eio.Path.load env#fs (Eio.Path.parse csv_path) in
600600+ let matches = search_csv_content content term in
601601+ Ok (Response.reply ~content:(format_matches matches))
602602+ | _ -> Ok Response.none
603603+end
604604+```
605605+606606+## Refined Bot Handler Interface
607607+608608+Based on analysis, the current EIO environment plumbing is **essential** and should be cleaned up:
609609+610610+```ocaml
611611+(** Clean bot handler interface with direct EIO access *)
612612+module type Bot_handler = sig
613613+ val initialize : Bot_config.t -> (unit, Zulip.Error.t) result
614614+ val usage : unit -> string
615615+ val description : unit -> string
616616+617617+ (** Handle message with full EIO environment access *)
618618+ val handle_message :
619619+ config:Bot_config.t ->
620620+ storage:Bot_storage.t ->
621621+ identity:Identity.t ->
622622+ message:Message_context.t ->
623623+ env:#Eio.Env.t -> (* Essential for custom I/O *)
624624+ (Response.t, Zulip.Error.t) result
625625+end
626626+627627+type t
628628+629629+(** Single creation interface *)
630630+val create :
631631+ (module Bot_handler) ->
632632+ config:Bot_config.t ->
633633+ storage:Bot_storage.t ->
634634+ identity:Identity.t ->
635635+ t
636636+637637+(** Single message handler requiring EIO environment *)
638638+val handle_message : t -> #Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result
639639+```
640640+641641+## Storage Strategy
642642+643643+Bot storage can be simplified to in-memory key-value storage since it's server-side:
644644+645645+```ocaml
646646+(* In zulip_bot - storage per bot instance *)
647647+module Bot_storage = struct
648648+ type t = (string, string) Hashtbl.t (* Simple in-memory key-value *)
649649+650650+ let create () = Hashtbl.create 16
651651+ let get t ~key = Hashtbl.find_opt t key
652652+ let put t ~key ~value = Hashtbl.replace t key value
653653+ let contains t ~key = Hashtbl.mem t key
654654+end
655655+656656+(* In zulip_botserver - storage shared across bots *)
657657+module Server_storage = struct
658658+ type t = (string * string, string) Hashtbl.t (* (bot_email, key) -> value *)
659659+660660+ let create () = Hashtbl.create 64
661661+ let get t ~bot_email ~key = Hashtbl.find_opt t (bot_email, key)
662662+ let put t ~bot_email ~key ~value = Hashtbl.replace t (bot_email, key) value
663663+end
664664+```
665665+666666+## Interface Cleanup Recommendations
667667+668668+1. **Remove** the problematic `handle_message` function with mock environment
669669+2. **Keep** `handle_message_with_env` but rename to `handle_message`
670670+3. **Use** `#Eio.Env.t` constraint for clean typing
671671+4. **Document** that bot handlers have full EIO access for custom I/O operations
672672+673673+This design maintains flexibility for real-world bot functionality while providing clean, type-safe interfaces.
521674522675## Sources and References
523676