My agentic slop goes here. Not intended for anyone else!

sync

Changed files
+162 -356
ocaml-zulip
+154 -1
ocaml-zulip/CLAUDE.md
···
My target is to use the OCaml EIO direct-style library, with an idiomatic as
possible API that implements it. For JSON parsing, using the jsonm library is
-
right. For HTTPS, use cohttp-eio with the tls-eio library.
+
right. For HTTPS, use cohttp-eio with the tls-eio library. You have access to
+
an OCaml LSP via MCP which provides type hints and other language server
+
features after you complete a `dune build`.
# OCaml Zulip Library Design
···
- `jsonm` - Streaming JSON codec
- `uri` - URI parsing and manipulation
- `base64` - Base64 encoding for authentication
+
+
# Architecture Analysis: zulip_bot vs zulip_botserver
+
+
## Library Separation
+
+
### `zulip_bot` - Individual Bot Framework
+
**Purpose**: Library for building and running a single bot instance
+
+
**Key Components**:
+
- `Bot_handler` - Interface for bot logic with EIO environment access
+
- `Bot_runner` - Manages lifecycle of one bot (real-time events or webhook mode)
+
- `Bot_config` - Configuration for a single bot
+
- `Bot_storage` - Simple in-memory storage for bot state
+
+
**Usage Pattern**:
+
```ocaml
+
(* Run a single bot directly *)
+
let my_bot = Bot_handler.create (module My_echo_bot) ~config ~storage ~identity in
+
let runner = Bot_runner.create ~client ~handler:my_bot in
+
Bot_runner.run_realtime runner (* Bot connects to Zulip events API directly *)
+
```
+
+
### `zulip_botserver` - Multi-Bot Server Infrastructure
+
**Purpose**: HTTP server that manages multiple bots via webhooks
+
+
**Key Components**:
+
- `Bot_server` - HTTP server receiving webhook events from Zulip
+
- `Bot_registry` - Manages multiple bot instances
+
- `Server_config` - Configuration for multiple bots + server settings
+
- `Webhook_handler` - Parses incoming webhook requests and routes to appropriate bots
+
+
**Usage Pattern**:
+
```ocaml
+
(* Run a server hosting multiple bots *)
+
let registry = Bot_registry.create () in
+
Bot_registry.register registry echo_bot_module;
+
Bot_registry.register registry weather_bot_module;
+
+
let server = Bot_server.create ~env ~config ~registry in
+
Bot_server.run server (* HTTP server waits for webhook calls *)
+
```
+
+
## EIO Environment Requirements
+
+
### Why Bot Handlers Need Direct EIO Access
+
+
Bot handlers require direct access to the EIO environment for legitimate I/O operations beyond HTTP requests to Zulip:
+
+
1. **Network Operations**: Custom HTTP requests, API calls to external services
+
2. **File System Operations**: Reading configuration files, CSV dictionaries, logs
+
3. **Resource Management**: Proper cleanup via structured concurrency
+
+
### Example: URL Checker Bot
+
```ocaml
+
module Url_checker_bot : Zulip_bot.Bot_handler.Bot_handler = struct
+
let handle_message ~config ~storage ~identity ~message ~env =
+
match parse_command message with
+
| "!check", url ->
+
(* Direct EIO network access needed *)
+
Eio.Switch.run @@ fun sw ->
+
let client = Cohttp_eio.Client.make ~sw env#net in
+
let response = Cohttp_eio.Client.head ~sw client (Uri.of_string url) in
+
let status = Cohttp.Code.code_of_status response.status in
+
Ok (Response.reply ~content:(format_status_message url status))
+
| _ -> Ok Response.none
+
end
+
```
+
+
### Example: CSV Dictionary Bot
+
```ocaml
+
module Csv_dict_bot : Zulip_bot.Bot_handler.Bot_handler = struct
+
let handle_message ~config ~storage ~identity ~message ~env =
+
match parse_command message with
+
| "!lookup", term ->
+
(* Direct EIO file system access needed *)
+
let csv_path = Bot_config.get_required config ~key:"csv_file" in
+
let content = Eio.Path.load env#fs (Eio.Path.parse csv_path) in
+
let matches = search_csv_content content term in
+
Ok (Response.reply ~content:(format_matches matches))
+
| _ -> Ok Response.none
+
end
+
```
+
+
## Refined Bot Handler Interface
+
+
Based on analysis, the current EIO environment plumbing is **essential** and should be cleaned up:
+
+
```ocaml
+
(** Clean bot handler interface with direct EIO access *)
+
module type Bot_handler = sig
+
val initialize : Bot_config.t -> (unit, Zulip.Error.t) result
+
val usage : unit -> string
+
val description : unit -> string
+
+
(** Handle message with full EIO environment access *)
+
val handle_message :
+
config:Bot_config.t ->
+
storage:Bot_storage.t ->
+
identity:Identity.t ->
+
message:Message_context.t ->
+
env:#Eio.Env.t -> (* Essential for custom I/O *)
+
(Response.t, Zulip.Error.t) result
+
end
+
+
type t
+
+
(** Single creation interface *)
+
val create :
+
(module Bot_handler) ->
+
config:Bot_config.t ->
+
storage:Bot_storage.t ->
+
identity:Identity.t ->
+
t
+
+
(** Single message handler requiring EIO environment *)
+
val handle_message : t -> #Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result
+
```
+
+
## Storage Strategy
+
+
Bot storage can be simplified to in-memory key-value storage since it's server-side:
+
+
```ocaml
+
(* In zulip_bot - storage per bot instance *)
+
module Bot_storage = struct
+
type t = (string, string) Hashtbl.t (* Simple in-memory key-value *)
+
+
let create () = Hashtbl.create 16
+
let get t ~key = Hashtbl.find_opt t key
+
let put t ~key ~value = Hashtbl.replace t key value
+
let contains t ~key = Hashtbl.mem t key
+
end
+
+
(* In zulip_botserver - storage shared across bots *)
+
module Server_storage = struct
+
type t = (string * string, string) Hashtbl.t (* (bot_email, key) -> value *)
+
+
let create () = Hashtbl.create 64
+
let get t ~bot_email ~key = Hashtbl.find_opt t (bot_email, key)
+
let put t ~bot_email ~key ~value = Hashtbl.replace t (bot_email, key) value
+
end
+
```
+
+
## Interface Cleanup Recommendations
+
+
1. **Remove** the problematic `handle_message` function with mock environment
+
2. **Keep** `handle_message_with_env` but rename to `handle_message`
+
3. **Use** `#Eio.Env.t` constraint for clean typing
+
4. **Document** that bot handlers have full EIO access for custom I/O operations
+
+
This design maintains flexibility for real-world bot functionality while providing clean, type-safe interfaces.
## Sources and References
+5 -5
ocaml-zulip/examples/dune
···
(package zulip_bot)
(libraries zulip zulip_bot))
-
(executable
-
(public_name zulip_realistic_bot_example)
-
(name realistic_bot_example)
-
(package zulip_bot)
-
(libraries zulip zulip_bot unix eio eio_main toml))
+
; (executable
+
; (public_name zulip_realistic_bot_example)
+
; (name realistic_bot_example)
+
; (package zulip_bot)
+
; (libraries zulip zulip_bot unix eio eio_main toml))
-346
ocaml-zulip/examples/realistic_bot_example.ml
···
-
(* Realistic Bot Example with Real EIO Operations *)
-
-
(* Weather Bot that:
-
1. Reads TOML configuration from filesystem using real EIO
-
2. Makes external HTTP API calls for weather data
-
3. Logs bot activity to files using real EIO filesystem access
-
4. Demonstrates proper EIO usage in bot handlers
-
*)
-
-
let load_bot_config env =
-
(* Read TOML config file using real EIO filesystem *)
-
match Eio.Path.with_open_in (env#fs / "examples" / "bot_config.toml") (fun flow ->
-
let content = Eio.Flow.read_all flow in
-
Toml.Parser.from_string content
-
) with
-
| exception Eio.Io (Eio.Fs.E Not_found, _) ->
-
Printf.printf "[CONFIG] Config file not found, using defaults\n";
-
Ok (Toml.table [])
-
| Ok toml -> Ok toml
-
| Error (`Msg msg) ->
-
Error (Printf.sprintf "TOML parse error: %s" msg)
-
-
let log_to_file env log_file message =
-
(* Write to log file using real EIO filesystem *)
-
try
-
let timestamp = Unix.time () |> Unix.gmtime |> fun tm ->
-
Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d"
-
(tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday
-
tm.tm_hour tm.tm_min tm.tm_sec in
-
let log_entry = Printf.sprintf "[%s] %s\n" timestamp message in
-
-
Eio.Path.with_open_out ~create:(`If_missing 0o644) ~append:true
-
(env#fs / log_file) (fun flow ->
-
Eio.Flow.write_string flow log_entry
-
);
-
Printf.printf "[FS] Logged to %s: %s" log_file (String.trim log_entry);
-
Ok ()
-
with
-
| Eio.Io (err, _) ->
-
Error (Printf.sprintf "Log write failed: %s" (Printexc.to_string err))
-
-
let make_http_request env url =
-
(* Make real HTTP request using EIO network *)
-
try
-
Printf.printf "[NET] Making HTTP request to %s\n" url;
-
(* In a real implementation, you would use cohttp-eio here *)
-
(* For now, we'll simulate the network call with a delay *)
-
Eio.Time.sleep env#clock 0.1; (* Simulate network latency *)
-
-
(* Simulate successful API response *)
-
let response_body = `O [
-
("status", `String "success");
-
("temperature", `Float 22.5);
-
("condition", `String "Sunny");
-
("humidity", `Float 65.0);
-
] in
-
Ok response_body
-
with
-
| Eio.Io (err, _) ->
-
Error (Printf.sprintf "HTTP request failed: %s" (Printexc.to_string err))
-
-
let read_toml_value toml section key =
-
try
-
match Toml.Lenses.(get toml (key section |-- table |-- key key |-- string)) with
-
| Some value -> Some value
-
| None -> None
-
with _ -> None
-
-
(* Weather Bot Implementation with Real EIO *)
-
module Weather_bot = struct
-
let initialize _config =
-
Printf.printf "Weather bot initialized with real EIO support!\n";
-
Ok ()
-
-
let usage () =
-
"Weather bot - get weather for cities. Usage: @bot weather <city>"
-
-
let description () =
-
"A weather bot that demonstrates real EIO filesystem and network access"
-
-
let handle_message ~config:_ ~storage ~identity:_ ~message ~env =
-
let content = Zulip_bot.Bot_handler.Message_context.content message in
-
let sender = Zulip_bot.Bot_handler.Message_context.sender_full_name message in
-
-
(* Log the incoming message using real EIO *)
-
(match log_to_file env "tmp/weather_bot.log"
-
(Printf.sprintf "Received message from %s: %s" sender content) with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
-
(* Parse command *)
-
if String.length content > 8 && String.sub content 0 8 = "weather " then (
-
let city = String.sub content 8 (String.length content - 8) in
-
-
(* Read bot configuration from filesystem using real EIO *)
-
(match load_bot_config env with
-
| Ok toml ->
-
let api_key = match read_toml_value toml "weather_bot" "default_api_key" with
-
| Some key -> key
-
| None -> "demo-key" in
-
let log_level = match read_toml_value toml "weather_bot" "log_level" with
-
| Some level -> level
-
| None -> "info" in
-
-
Printf.printf "[CONFIG] Using API key: %s, Log level: %s\n" api_key log_level;
-
-
(* Store request in bot storage *)
-
(match Zulip_bot.Bot_storage.put storage ~key:("last_request_" ^ sender) ~value:city with
-
| Ok () ->
-
(* Make external API call using real EIO network *)
-
let weather_url = Printf.sprintf "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s" city api_key in
-
(match make_http_request env weather_url with
-
| Ok weather_json ->
-
(* Parse weather response *)
-
let weather_info = match weather_json with
-
| `O fields ->
-
let temp = List.assoc_opt "temperature" fields
-
|> Option.value ~default:(`Float 0.0) in
-
let condition = List.assoc_opt "condition" fields
-
|> Option.value ~default:(`String "Unknown") in
-
(match temp, condition with
-
| `Float t, `String c ->
-
Printf.sprintf "Weather in %s: %.1f°C, %s" city t c
-
| _ -> Printf.sprintf "Weather in %s: Data unavailable" city)
-
| _ -> Printf.sprintf "Weather in %s: Invalid response format" city in
-
-
(* Log successful API call *)
-
(match log_to_file env "tmp/weather_bot.log"
-
(Printf.sprintf "Successfully retrieved weather for %s" city) with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:weather_info)
-
| Error msg ->
-
let error_msg = Printf.sprintf "Weather API error: %s" msg in
-
(match log_to_file env "tmp/weather_bot.log" error_msg with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:("Weather service unavailable: " ^ msg)))
-
| Error err ->
-
Error err)
-
| Error msg ->
-
let error_msg = Printf.sprintf "Config read error: %s" msg in
-
(match log_to_file env "tmp/weather_bot.log" error_msg with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Bot configuration error"))
-
) else if content = "help" then (
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:(usage ()))
-
) else (
-
let error_msg = Printf.sprintf "Unknown command: %s" content in
-
(match log_to_file env "tmp/weather_bot.log" error_msg with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Unknown command. Type 'help' for usage.")
-
)
-
end
-
-
(* File Logger Bot Implementation with Real EIO *)
-
module Logger_bot = struct
-
let initialize _config =
-
Printf.printf "Logger bot initialized with real EIO support!\n";
-
Ok ()
-
-
let usage () =
-
"Logger bot - logs all messages to files. Usage: @bot log <message>"
-
-
let description () =
-
"A logging bot that demonstrates real EIO filesystem access"
-
-
let handle_message ~config:_ ~storage:_ ~identity:_ ~message ~env =
-
let content = Zulip_bot.Bot_handler.Message_context.content message in
-
let sender = Zulip_bot.Bot_handler.Message_context.sender_full_name message in
-
let message_id = Zulip_bot.Bot_handler.Message_context.message_id message in
-
-
if String.length content > 4 && String.sub content 0 4 = "log " then (
-
let log_content = String.sub content 4 (String.length content - 4) in
-
-
(* Read logging configuration from TOML *)
-
let (log_file, max_size) = match load_bot_config env with
-
| Ok toml ->
-
let file = read_toml_value toml "logger_bot" "log_file"
-
|> Option.value ~default:"tmp/user_messages.log" in
-
let size = read_toml_value toml "logger_bot" "max_log_size_mb"
-
|> Option.map int_of_string |> Option.value ~default:10 in
-
(file, size)
-
| Error _ -> ("tmp/user_messages.log", 10) in
-
-
let log_entry = Printf.sprintf "Message %d from %s: %s"
-
message_id sender log_content in
-
-
(* Write to log file using real EIO filesystem *)
-
(match log_to_file env log_file log_entry with
-
| Ok () ->
-
Printf.printf "[FS] Successfully wrote to %s\n" log_file;
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Message logged successfully!")
-
| Error err ->
-
Printf.printf "[ERROR] Failed to write log: %s\n" err;
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:("Logging failed: " ^ err)))
-
) else (
-
Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Usage: @bot log <your message>")
-
)
-
end
-
-
let create_temp_dirs env =
-
(* Create temporary directories for logging using real EIO *)
-
try
-
let tmp_dir = env#fs / "tmp" in
-
Eio.Path.mkdir ~perm:0o755 tmp_dir;
-
Printf.printf "[FS] Created tmp/ directory for logging\n";
-
Ok ()
-
with
-
| Eio.Io (Eio.Fs.E Already_exists, _) ->
-
Printf.printf "[FS] tmp/ directory already exists\n";
-
Ok ()
-
| Eio.Io (err, _) ->
-
Error (Printf.sprintf "Failed to create directories: %s" (Printexc.to_string err))
-
-
let () =
-
Printf.printf "Realistic OCaml Zulip Bot with Real EIO Operations\n";
-
Printf.printf "================================================\n\n";
-
-
(* Run with real EIO environment *)
-
Eio_main.run @@ fun env ->
-
-
(* Create necessary directories *)
-
(match create_temp_dirs env with
-
| Ok () -> ()
-
| Error err -> Printf.printf "[ERROR] %s\n" err);
-
-
(* Create test authentication *)
-
let auth = Zulip.Auth.create
-
~server_url:"https://company.zulipchat.com"
-
~email:"weather-bot@company.com"
-
~api_key:"real-api-key-here" in
-
-
Printf.printf "✅ Created authentication for: %s\n" (Zulip.Auth.email auth);
-
-
(* Create client with real EIO environment *)
-
let client = Zulip.Client.create env auth in
-
Printf.printf "✅ Created EIO-capable client\n";
-
-
(* Load TOML configuration *)
-
(match load_bot_config env with
-
| Ok toml ->
-
Printf.printf "✅ Loaded TOML configuration\n";
-
let server_url = read_toml_value toml "general" "server_url"
-
|> Option.value ~default:"https://default.zulipchat.com" in
-
Printf.printf "[CONFIG] Server URL: %s\n" server_url
-
| Error err ->
-
Printf.printf "⚠️ Config load failed: %s\n" err);
-
-
(* Create bot config *)
-
let config = Zulip_bot.Bot_config.create [
-
("weather_api_key", "api-key-12345");
-
("log_level", "info");
-
("data_dir", "/tmp/bot");
-
] in
-
-
(* Create bot storage *)
-
let storage = Zulip_bot.Bot_storage.create client ~bot_email:"weather-bot@company.com" in
-
-
(* Create bot identity *)
-
let identity = Zulip_bot.Bot_handler.Identity.create
-
~full_name:"Weather Bot"
-
~email:"weather-bot@company.com"
-
~mention_name:"WeatherBot" in
-
-
(* Create Weather Bot handler *)
-
let weather_handler = Zulip_bot.Bot_handler.create
-
(module Weather_bot) ~config ~storage ~identity in
-
-
(* Create Logger Bot handler *)
-
let logger_handler = Zulip_bot.Bot_handler.create
-
(module Logger_bot) ~config ~storage ~identity in
-
-
Printf.printf "✅ Created bot handlers with real EIO support\n";
-
-
(* Test Weather Bot with real EIO operations *)
-
Printf.printf "\n=== Testing Weather Bot with Real EIO ===\n";
-
-
let test_weather_message = Zulip_bot.Bot_handler.Message_context.create
-
~message_id:1001
-
~sender_email:"user@company.com"
-
~sender_full_name:"Alice Smith"
-
~content:"weather London"
-
~message_type:`Direct
-
() in
-
-
(match Zulip_bot.Bot_handler.handle_message_with_env weather_handler env test_weather_message with
-
| Ok response ->
-
Printf.printf "✅ Weather bot response: %s\n"
-
(match response with
-
| Zulip_bot.Bot_handler.Response.Reply content -> content
-
| _ -> "Other response type")
-
| Error err ->
-
Printf.printf "❌ Weather bot error: %s\n" (Zulip.Error.message err));
-
-
(* Test Logger Bot with real EIO operations *)
-
Printf.printf "\n=== Testing Logger Bot with Real EIO ===\n";
-
-
let test_log_message = Zulip_bot.Bot_handler.Message_context.create
-
~message_id:1002
-
~sender_email:"user@company.com"
-
~sender_full_name:"Bob Johnson"
-
~content:"log This is important information stored with real EIO"
-
~message_type:`Direct
-
() in
-
-
(match Zulip_bot.Bot_handler.handle_message_with_env logger_handler env test_log_message with
-
| Ok response ->
-
Printf.printf "✅ Logger bot response: %s\n"
-
(match response with
-
| Zulip_bot.Bot_handler.Response.Reply content -> content
-
| _ -> "Other response type")
-
| Error err ->
-
Printf.printf "❌ Logger bot error: %s\n" (Zulip.Error.message err));
-
-
(* Demonstrate bot runner with real EIO environment *)
-
Printf.printf "\n=== Testing Bot Runner with Real EIO ===\n";
-
-
let bot_runner = Zulip_bot.Bot_runner.create
-
~env ~client ~handler:weather_handler in
-
-
Printf.printf "✅ Created bot runner with real EIO environment\n";
-
-
Printf.printf "\n🎉 Realistic bot demo with real EIO completed!\n";
-
Printf.printf "\nFeatures demonstrated:\n";
-
Printf.printf "• Real EIO environment passed to bot handlers\n";
-
Printf.printf "• Real filesystem access for TOML config and logging\n";
-
Printf.printf "• Real network operations with simulated HTTP calls\n";
-
Printf.printf "• Bot storage for state management\n";
-
Printf.printf "• Proper EIO error handling throughout\n";
-
Printf.printf "• TOML configuration file support\n";
-
Printf.printf "• Structured concurrency with EIO resource management\n";
-
-
(* Check that log files were created *)
-
Printf.printf "\n=== Checking Created Files ===\n";
-
(try
-
let files = Eio.Path.read_dir (env#fs / "tmp") in
-
List.iter (fun file ->
-
Printf.printf "📁 Created: tmp/%s\n" file
-
) files
-
with
-
| Eio.Io (err, _) ->
-
Printf.printf "⚠️ Could not list tmp/ directory: %s\n" (Printexc.to_string err))
-1
ocaml-zulip/examples/realistic_bot_example.mli
···
-
(** Realistic bot example demonstrating EIO filesystem and network access *)
+1 -1
ocaml-zulip/lib/zulip_bot/lib/bot_handler.ml
···
storage:Bot_storage.t ->
identity:Identity.t ->
message:Message_context.t ->
-
env:Eio.Env.t ->
+
env:_ ->
(Response.t, Zulip.Error.t) result
end
+2 -2
ocaml-zulip/lib/zulip_bot/lib/bot_handler.mli
···
storage:Bot_storage.t ->
identity:Identity.t ->
message:Message_context.t ->
-
env:Eio.Env.t ->
+
env:_ ->
(Response.t, Zulip.Error.t) result
end
···
val handle_message : t -> Message_context.t -> (Response.t, Zulip.Error.t) result
(** Process an incoming message with EIO environment *)
-
val handle_message_with_env : t -> Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result
+
val handle_message_with_env : t -> _ -> Message_context.t -> (Response.t, Zulip.Error.t) result
(** Get bot identity *)
val identity : t -> Identity.t