A Zulip bot agent to sit in our Black Sun. Ever evolving
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add refresh command to poe bot for manual git pull and broadcast

Adds a new command (refresh/pull/sync/update) that can be triggered via
DM to the bot. The command:
1. Pulls latest changes from remote (git pull --ff-only)
2. Regenerates changelog (monopam changes --daily --aggregate)
3. Broadcasts new changes to the configured channel

This allows manual triggering of the same flow that runs in the polling
loop, useful for immediate updates without waiting for the next interval.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+111
+2
lib/commands.ml
··· 14 14 | Help 15 15 | Status 16 16 | Broadcast 17 + | Refresh 17 18 | Admin of admin_command 18 19 | Unknown of string 19 20 ··· 39 40 | "help" | "?" -> Help 40 41 | "status" -> Status 41 42 | "broadcast" | "post changes" | "post" | "changes" -> Broadcast 43 + | "refresh" | "pull" | "sync" | "update" -> Refresh 42 44 | _ -> 43 45 if String.starts_with ~prefix:"admin " content then 44 46 let args = String.sub content 6 (String.length content - 6) in
+1
lib/commands.mli
··· 21 21 | Help (** Show help message *) 22 22 | Status (** Show bot configuration status *) 23 23 | Broadcast (** Broadcast new changes *) 24 + | Refresh (** Pull from remote, regenerate changes, and broadcast *) 24 25 | Admin of admin_command (** Admin commands (require authorization) *) 25 26 | Unknown of string (** Unrecognized command - pass to Claude *) 26 27
+108
lib/handler.ml
··· 19 19 let path = fs / config.Config.monorepo_path / config.Config.changes_file in 20 20 try Some (load path) with _ -> None 21 21 22 + let run_git_pull ~proc ~cwd = 23 + Log.info (fun m -> m "Pulling latest changes from remote"); 24 + Eio.Switch.run @@ fun sw -> 25 + let buf_stdout = Buffer.create 256 in 26 + let buf_stderr = Buffer.create 256 in 27 + let child = Eio.Process.spawn proc ~sw ~cwd 28 + ~stdout:(Eio.Flow.buffer_sink buf_stdout) 29 + ~stderr:(Eio.Flow.buffer_sink buf_stderr) 30 + ["git"; "pull"; "--ff-only"] 31 + in 32 + match Eio.Process.await child with 33 + | `Exited 0 -> 34 + let output = String.trim (Buffer.contents buf_stdout) in 35 + if output = "Already up to date." then begin 36 + Log.info (fun m -> m "Repository already up to date"); 37 + Ok `Up_to_date 38 + end else begin 39 + Log.info (fun m -> m "Pulled new changes from remote"); 40 + Ok (`Updated output) 41 + end 42 + | `Exited code -> 43 + let stderr = String.trim (Buffer.contents buf_stderr) in 44 + Log.warn (fun m -> m "git pull exited with code %d: %s" code stderr); 45 + Error (Printf.sprintf "git pull failed (code %d): %s" code stderr) 46 + | `Signaled sig_ -> 47 + Log.warn (fun m -> m "git pull killed by signal %d" sig_); 48 + Error (Printf.sprintf "git pull killed by signal %d" sig_) 49 + 50 + let run_monopam_changes ~proc ~cwd = 51 + Log.info (fun m -> m "Running monopam changes --daily --aggregate"); 52 + Eio.Switch.run @@ fun sw -> 53 + let buf_stderr = Buffer.create 256 in 54 + let child = Eio.Process.spawn proc ~sw ~cwd 55 + ~stderr:(Eio.Flow.buffer_sink buf_stderr) 56 + ["opam"; "exec"; "--"; "dune"; "exec"; "--"; "monopam"; "changes"; "--daily"; "--aggregate"] 57 + in 58 + match Eio.Process.await child with 59 + | `Exited 0 -> 60 + Log.info (fun m -> m "monopam changes completed successfully"); 61 + Ok () 62 + | `Exited code -> 63 + let stderr = String.trim (Buffer.contents buf_stderr) in 64 + Log.warn (fun m -> m "monopam changes exited with code %d" code); 65 + Error (Printf.sprintf "monopam changes failed (code %d): %s" code stderr) 66 + | `Signaled sig_ -> 67 + Log.warn (fun m -> m "monopam changes killed by signal %d" sig_); 68 + Error (Printf.sprintf "monopam changes killed by signal %d" sig_) 69 + 22 70 let create_claude_client env = 23 71 let options = 24 72 Claude.Options.default ··· 56 104 - `help` or `?` - Show this help message 57 105 - `status` - Show bot configuration status 58 106 - `broadcast` / `post` / `changes` - Broadcast new changes to configured channel 107 + - `refresh` / `pull` / `sync` / `update` - Pull from remote, regenerate changes, and broadcast 59 108 60 109 **Admin Commands:** (require authorization) 61 110 - `admin last-broadcast` - Show last broadcast time and git HEAD ··· 93 142 config.Config.channel config.Config.topic config.Config.changes_file 94 143 config.Config.monorepo_path config.Config.changes_dir admin_list) 95 144 145 + let handle_refresh env ~fs ~storage ~config = 146 + let monorepo_path = Eio.Path.(fs / config.Config.monorepo_path) in 147 + let changes_dir = Fpath.v (config.Config.monorepo_path ^ "/" ^ config.Config.changes_dir) in 148 + 149 + (* Step 1: Git pull *) 150 + let pull_result = run_git_pull ~proc:env.process_mgr ~cwd:monorepo_path in 151 + match pull_result with 152 + | Error e -> 153 + Zulip_bot.Response.reply (Printf.sprintf "**Refresh failed:**\n\n%s" e) 154 + | Ok pull_status -> 155 + let pull_msg = match pull_status with 156 + | `Up_to_date -> "Repository already up to date" 157 + | `Updated _ -> "Pulled new changes from remote" 158 + in 159 + 160 + (* Step 2: Run monopam changes *) 161 + let changes_result = run_monopam_changes ~proc:env.process_mgr ~cwd:monorepo_path in 162 + match changes_result with 163 + | Error e -> 164 + Zulip_bot.Response.reply 165 + (Printf.sprintf "**Refresh partially failed:**\n\n- %s\n- monopam changes error: %s" pull_msg e) 166 + | Ok () -> 167 + (* Step 3: Load and broadcast changes *) 168 + let last_broadcast = Admin.get_last_broadcast_time storage in 169 + let since = match last_broadcast with 170 + | None -> 171 + let now = Ptime_clock.now () in 172 + let day_ago = Ptime.Span.of_int_s (24 * 60 * 60) in 173 + Option.value ~default:Ptime.epoch (Ptime.sub_span now day_ago) 174 + | Some t -> t 175 + in 176 + 177 + match Monopam.Changes.Query.changes_since ~fs ~changes_dir ~since with 178 + | Error e -> 179 + Zulip_bot.Response.reply 180 + (Printf.sprintf "**Refresh completed but failed to load changes:**\n\n- %s\n- Error: %s" pull_msg e) 181 + | Ok entries when entries = [] -> 182 + Zulip_bot.Response.reply 183 + (Printf.sprintf "**Refresh completed:**\n\n- %s\n- No new changes to broadcast" pull_msg) 184 + | Ok entries -> 185 + (* Update broadcast time *) 186 + let now = Ptime_clock.now () in 187 + Admin.set_last_broadcast_time storage now; 188 + 189 + (* Format and send to channel *) 190 + let content = Monopam.Changes.Query.format_for_zulip 191 + ~entries ~include_date:true ~date:None 192 + in 193 + let summary = Monopam.Changes.Query.format_summary ~entries in 194 + Log.info (fun m -> m "Refresh broadcasting: %s" summary); 195 + 196 + (* Send to channel - reply will confirm what was sent *) 197 + Zulip_bot.Response.stream 198 + ~stream:config.Config.channel 199 + ~topic:config.Config.topic 200 + ~content:(Printf.sprintf "**Refresh triggered manually**\n\n%s" content) 201 + 96 202 let handle_claude_query env msg = 97 203 let content = Zulip_bot.Message.content msg in 98 204 Log.info (fun m -> m "Asking Claude: %s" content); ··· 146 252 | Commands.Status -> handle_status config 147 253 | Commands.Broadcast -> 148 254 Broadcast.run ~fs:env.fs ~storage ~config 255 + | Commands.Refresh -> 256 + handle_refresh env ~fs:env.fs ~storage ~config 149 257 | Commands.Admin cmd -> 150 258 if is_admin config ~storage msg then 151 259 Zulip_bot.Response.reply (Admin.handle ~storage cmd)