From c9bdb0e7ac66af693acfa9a65ebaeb3026f18452 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 16 May 2026 16:35:14 +0000 Subject: [PATCH 1/3] feat(lib): add CLI-channel mu-plugin registrar for DMC CLI transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce lib/cli-channel.sh, a shared helper that writes a mu-plugin file at wp-content/mu-plugins/wp-coding-agents-channels.php registering chat bridges with the Data Machine Code generic CLI transport runtime via the datamachine_code_cli_channels filter. Bridge install/upgrade calls cli_channel_register; bridge uninstall calls cli_channel_unregister. Marker-delimited blocks make multi-bridge rewrites idempotent — per-bridge upserts and removals do not disturb other bridges' entries. The mu-plugin file is the discovery surface DMC#412 reads to resolve agents/dispatch-message channel names to command + argv templates with {recipient} / {message} substitution tokens. Wired into setup.sh + upgrade.sh lib loader so bridges/*.sh can call the helper directly during their install / sync_config hooks. Refs Extra-Chill/wp-coding-agents#129 Depends-on Extra-Chill/data-machine-code#412 --- lib/cli-channel.sh | 379 +++++++++++++++++++++++++++++++++++++++++++++ setup.sh | 2 +- upgrade.sh | 2 +- 3 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 lib/cli-channel.sh diff --git a/lib/cli-channel.sh b/lib/cli-channel.sh new file mode 100644 index 0000000..e24172d --- /dev/null +++ b/lib/cli-channel.sh @@ -0,0 +1,379 @@ +#!/bin/bash +# lib/cli-channel.sh — Per-bridge CLI-channel config writer. +# +# Each chat bridge in bridges/.sh exposes a CLI surface that can deliver +# messages to a recipient on its platform (kimaki → Discord, cc-connect → +# multi-platform, opencode-telegram → Telegram). Data Machine Code's generic +# CLI transport runtime (Extra-Chill/data-machine-code#412) shells those CLIs +# on behalf of the `agents/dispatch-message` ability, but only if it can +# discover a channel definition mapping `` → command + argv. +# +# This module owns that discovery surface. It writes a mu-plugin file in the +# target WordPress install that registers each bridge's channel entry via the +# `datamachine_code_cli_channels` filter. Each bridge install/sync calls +# cli_channel_register; each bridge uninstall calls cli_channel_unregister. +# +# Design choices: +# +# * mu-plugins file (not a wp_option write). File-based so an operator can +# `cat` it and see exactly what the agent will spawn. Always loaded, no +# activation step. Survives plugin churn. Matches how the rest of +# wp-coding-agents drops config on disk (/opt/kimaki-config/, etc.) and +# keeps install-time WP_CLI dependence out of the chat-bridge install +# flow — DB writes there race against multisite + Redis caching and have +# been a source of intermittent install failures historically. +# +# * Marker-delimited blocks (`// BEGIN bridge:` … `// END +# bridge:`) so each bridge's block can be inserted, replaced, or +# removed idempotently without re-parsing PHP. Same idea as the +# well-trodden `wp-config.php` / `/etc/hosts` block-marker pattern. +# +# * Pure-bash with awk for in-place edits. No PHP or wp-cli required — +# this script runs inside setup.sh / upgrade.sh and may not have a +# running WordPress to invoke. The mu-plugin file is just text on disk. +# +# Resolved file: $SITE_PATH/wp-content/mu-plugins/wp-coding-agents-channels.php +# +# Public surface: +# cli_channel_mu_plugin_path — echo resolved file path +# cli_channel_ensure_mu_plugin_file — create stub if missing +# cli_channel_register \ +# [detach] [timeout] — upsert a bridge's block +# cli_channel_unregister — remove a bridge's block +# +# `args_json` is a JSON array literal (e.g. '["send","--channel","{recipient}", +# "--prompt","{message}"]'). Substitution tokens supported by the DMC runtime: +# {recipient} — the bridge-specific target identifier (see per-bridge docs) +# {message} — the message body to deliver +# +# Honors DRY_RUN (logs intent, makes no changes). + +# --------------------------------------------------------------------------- +# Path resolution +# --------------------------------------------------------------------------- + +# cli_channel_mu_plugin_path +# +# Print the absolute path of the mu-plugin file. Requires SITE_PATH (set by +# lib/detect.sh during setup/upgrade). Empty + return 1 if SITE_PATH unset. +cli_channel_mu_plugin_path() { + if [ -z "${SITE_PATH:-}" ]; then + return 1 + fi + printf '%s' "$SITE_PATH/wp-content/mu-plugins/wp-coding-agents-channels.php" +} + +# --------------------------------------------------------------------------- +# File scaffolding +# --------------------------------------------------------------------------- + +# cli_channel_ensure_mu_plugin_file +# +# Create the mu-plugin file with the filter scaffold if it does not exist. +# Idempotent — does nothing if the file already exists. The scaffold contains +# a single `add_filter( 'datamachine_code_cli_channels', … )` callback whose +# body is the marker-delimited region that bridges write into. +# +# The PHP closure walks the existing $channels array, applies each +# // BEGIN/END marker block in source order, and returns the merged map. If +# DMC's CLI runtime is not loaded (no filter consumers), the array is built +# and discarded — no harm. +cli_channel_ensure_mu_plugin_file() { + local file + file="$(cli_channel_mu_plugin_path)" || { + warn " cli_channel_ensure_mu_plugin_file: SITE_PATH not set — skipping" + return 1 + } + + if [ -f "$file" ]; then + return 0 + fi + + local dir="${file%/*}" + if [ "${DRY_RUN:-false}" = true ]; then + echo -e "${BLUE}[dry-run]${NC} Would mkdir -p $dir" + echo -e "${BLUE}[dry-run]${NC} Would write CLI-channel mu-plugin to $file" + return 0 + fi + + mkdir -p "$dir" + + cat > "$file" <<'PHP' + + * $channels[''] = [ + * 'command' => '/absolute/path/to/cli', + * 'args' => [ 'send', '--channel', '{recipient}', '--prompt', '{message}' ], + * 'detach' => true, + * 'timeout' => 600, + * ]; + * // END bridge: + * + * Substitution tokens are resolved by the DMC CLI runtime at dispatch time: + * {recipient} — bridge-specific target identifier (see bridge docs). + * {message} — the message body delivered by agents/dispatch-message. + * + * Filter contract: Extra-Chill/data-machine-code#412. + * + * @package wp-coding-agents + */ + +defined( 'ABSPATH' ) || exit; + +add_filter( 'datamachine_code_cli_channels', function ( $channels ) { + if ( ! is_array( $channels ) ) { + $channels = []; + } + + // BEGIN bridges + // END bridges + + return $channels; +} ); +PHP + + log " Wrote CLI-channel mu-plugin scaffold: $file" + if [ -n "${UPDATED_ITEMS+x}" ]; then + UPDATED_ITEMS+=("created $file") + fi +} + +# --------------------------------------------------------------------------- +# Block render +# --------------------------------------------------------------------------- + +# _cli_channel_render_block +# +# Print the marker-delimited PHP block for a single bridge, including +# surrounding BEGIN/END markers, indented to match the scaffold's filter +# body. Strings are escaped for PHP single-quoted literals (backslash, single +# quote); args_json is emitted verbatim because it is a literal JSON array +# the caller has already constructed. +_cli_channel_render_block() { + local name="$1" command="$2" args_json="$3" detach="$4" timeout="$5" + local esc_name esc_command esc_args + esc_name=$(_cli_channel_php_escape "$name") + esc_command=$(_cli_channel_php_escape "$command") + # Convert JSON array to PHP array literal: [...] is valid in both. Single + # quotes inside the JSON would be a problem; the caller is responsible for + # passing JSON with double-quoted strings, which we rewrite to single-quoted + # PHP strings via sed (no embedded quotes expected in real argv tokens). + esc_args=$(_cli_channel_json_to_php_array "$args_json") + + cat < '${esc_command}', + 'args' => ${esc_args}, + 'detach' => ${detach}, + 'timeout' => ${timeout}, + ]; + // END bridge:${esc_name} +PHP +} + +# _cli_channel_php_escape +# +# Escape a string for inclusion inside a PHP single-quoted literal. PHP +# single-quoted strings only require escaping `\` and `'`. +_cli_channel_php_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\'/\\\'}" + printf '%s' "$s" +} + +# _cli_channel_json_to_php_array +# +# Convert a JSON array of strings like ["send","--channel","{recipient}"] into +# a PHP array literal [ 'send', '--channel', '{recipient}' ]. Uses python3 if +# available (handles edge cases properly); falls back to a sed transform for +# the simple case (no embedded quotes, no nested arrays) which is all we ship. +_cli_channel_json_to_php_array() { + local json="$1" + if command -v python3 >/dev/null 2>&1; then + python3 - "$json" <<'PY' +import json, sys +arr = json.loads(sys.argv[1]) +def esc(s): + return s.replace("\\", "\\\\").replace("'", "\\'") +parts = ", ".join("'" + esc(x) + "'" for x in arr) +print("[ " + parts + " ]") +PY + return $? + fi + # Fallback: naive substitution. Works for our argv vocabulary (no embedded + # single quotes, no escapes) — every bridge argv template ships as plain + # ASCII flags and substitution tokens. + local out="$json" + out="${out//\"/\'}" + out="${out//,/, }" + out="${out//\[/[ }" + out="${out//\]/ ]}" + printf '%s' "$out" +} + +# --------------------------------------------------------------------------- +# Register / unregister +# --------------------------------------------------------------------------- + +# cli_channel_register [detach] [timeout] +# +# Upsert 's block in the mu-plugin file. Idempotent: re-running with the +# same arguments leaves the file unchanged; re-running with different +# arguments replaces just 's block. Other bridges' blocks are preserved. +# +# Defaults: +# detach — "true" (CLI bridges dispatch fire-and-forget by default) +# timeout — "600" (10 minutes; matches DMC's default per #412) +cli_channel_register() { + local name="$1" command="$2" args_json="$3" + local detach="${4:-true}" + local timeout="${5:-600}" + + if [ -z "$name" ] || [ -z "$command" ] || [ -z "$args_json" ]; then + warn " cli_channel_register: missing required args (name=$name command=$command args=$args_json)" + return 1 + fi + + local file + file="$(cli_channel_mu_plugin_path)" || { + warn " cli_channel_register: SITE_PATH not set — skipping channel '$name'" + return 1 + } + + cli_channel_ensure_mu_plugin_file || return 1 + + local new_block + new_block=$(_cli_channel_render_block "$name" "$command" "$args_json" "$detach" "$timeout") + + if [ "${DRY_RUN:-false}" = true ]; then + echo -e "${BLUE}[dry-run]${NC} Would register CLI channel '$name' in $file" + echo -e "${BLUE}[dry-run]${NC} Block:" + echo "$new_block" | sed 's/^/ /' + return 0 + fi + + # Read current file, check if the bridge's block already matches. + if _cli_channel_block_matches "$file" "$name" "$new_block"; then + return 0 + fi + + local tmp + tmp=$(mktemp "${file}.XXXXXX") + _cli_channel_rewrite "$file" "$name" "$new_block" > "$tmp" + + if cmp -s "$file" "$tmp"; then + rm -f "$tmp" + return 0 + fi + + mv "$tmp" "$file" + log " Registered CLI channel '$name' in $file" + if [ -n "${UPDATED_ITEMS+x}" ]; then + UPDATED_ITEMS+=("CLI channel: $name") + fi +} + +# cli_channel_unregister +# +# Remove 's block from the mu-plugin file. No-op if the file does not +# exist, or if the block is not present. Other bridges' blocks are preserved. +cli_channel_unregister() { + local name="$1" + if [ -z "$name" ]; then + warn " cli_channel_unregister: missing name" + return 1 + fi + + local file + file="$(cli_channel_mu_plugin_path)" || { + return 0 + } + [ -f "$file" ] || return 0 + + if ! grep -q "// BEGIN bridge:${name}\$" "$file"; then + return 0 + fi + + if [ "${DRY_RUN:-false}" = true ]; then + echo -e "${BLUE}[dry-run]${NC} Would unregister CLI channel '$name' from $file" + return 0 + fi + + local tmp + tmp=$(mktemp "${file}.XXXXXX") + _cli_channel_rewrite "$file" "$name" "" > "$tmp" + mv "$tmp" "$file" + log " Unregistered CLI channel '$name' from $file" + if [ -n "${UPDATED_ITEMS+x}" ]; then + UPDATED_ITEMS+=("CLI channel removed: $name") + fi +} + +# _cli_channel_block_matches +# +# Return 0 if the existing block for in already equals +# verbatim; otherwise 1. Used to short-circuit no-op rewrites. +_cli_channel_block_matches() { + local file="$1" name="$2" new_block="$3" + local existing + existing=$(awk -v name="$name" ' + $0 == " // BEGIN bridge:" name { capturing=1 } + capturing { print } + $0 == " // END bridge:" name { exit } + ' "$file") + [ "$existing" = "$new_block" ] +} + +# _cli_channel_rewrite +# +# Stream to stdout, replacing the existing block for with +# , or inserting immediately before the +# `// END bridges` marker if no block for exists yet. Empty +# removes the bridge's block entirely (used by unregister). +_cli_channel_rewrite() { + local file="$1" name="$2" new_block="$3" + awk -v name="$name" -v new_block="$new_block" ' + BEGIN { + begin_marker = " // BEGIN bridge:" name + end_marker = " // END bridge:" name + inserted = 0 + skipping = 0 + } + { + if ($0 == begin_marker) { + skipping = 1 + if (new_block != "") { + print new_block + inserted = 1 + } + next + } + if (skipping) { + if ($0 == end_marker) { + skipping = 0 + } + next + } + if (!inserted && $0 == " // END bridges") { + if (new_block != "") { + print new_block + inserted = 1 + } + } + print + } + ' "$file" +} diff --git a/setup.sh b/setup.sh index 4bcfca7..5c4d08d 100755 --- a/setup.sh +++ b/setup.sh @@ -23,7 +23,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source shared modules -for lib in common detect wordpress infrastructure data-machine homeboy skills summary; do +for lib in common detect wordpress infrastructure data-machine homeboy skills summary cli-channel; do source "$SCRIPT_DIR/lib/${lib}.sh" done diff --git a/upgrade.sh b/upgrade.sh index a0d031e..1e2299c 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -66,7 +66,7 @@ TIMESTAMP="$(date +%Y%m%d-%H%M%S)" # Source shared modules (common, detect needed for environment resolution; # wordpress is needed for wp_cmd helper used by compose and plugin updates). -for lib in common detect wordpress data-machine homeboy skills; do +for lib in common detect wordpress data-machine homeboy skills cli-channel; do source "$SCRIPT_DIR/lib/${lib}.sh" done From a426df1eb00fbd21deb1892492769cb563989d7d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 16 May 2026 16:35:25 +0000 Subject: [PATCH 2/3] feat(bridges): register CLI channels for agents/dispatch-message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each chat bridge now writes its DMC CLI-channel config entry during install and upgrade-time sync_config, so the Data Machine Code generic CLI transport runtime (#412) can route agents/dispatch-message calls without bespoke webhooks or HTTP hops. Per bridge: - kimaki: registers the local datamachine-kimaki adapter shim (falls back to the global kimaki binary). recipient = Discord channel ID. argv = send --channel {recipient} --prompt {message}. - cc-connect: registers cc-connect send. recipient = cc-connect project name (the platform binding lives in config.toml). argv = send --project {recipient} {message}. Assumption to validate upstream: if --project isn't supported, the argv collapses to [send,{message}] and recipient is informational only. - telegram: opencode-telegram-bot is inbound-only — no outbound send CLI. Registers curl against Telegram's sendMessage Bot API with TELEGRAM_BOT_TOKEN baked in at install/upgrade time. recipient = Telegram chat ID. Re-running upgrade refreshes the token from the bot .env file if rotated. Refs Extra-Chill/wp-coding-agents#129 Depends-on Extra-Chill/data-machine-code#412 --- bridges/cc-connect.sh | 43 +++++++++++++++++++++++++++++++++ bridges/kimaki.sh | 35 +++++++++++++++++++++++++++ bridges/telegram.sh | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/bridges/cc-connect.sh b/bridges/cc-connect.sh index 6a40b99..abd4630 100644 --- a/bridges/cc-connect.sh +++ b/bridges/cc-connect.sh @@ -41,6 +41,43 @@ bridge_install() { else _cc_connect_install_systemd fi + + _cc_connect_register_cli_channel +} + +# _cc_connect_register_cli_channel +# +# Register cc-connect with the Data Machine Code CLI transport runtime. +# +# `recipient` semantics: cc-connect routes outgoing `send` commands through +# its currently-active project/session as configured in $CC_DATA_DIR/config.toml +# (which can bind multiple platforms — Feishu, DingTalk, Slack, Telegram, +# Discord, etc.). cc-connect's documented `send` subcommand (v1.3.x) takes +# a message body and optional --image/--file flags; it does NOT accept an +# explicit `--channel` / `--recipient` flag — the destination is determined +# by the binding in config.toml. We therefore pass `recipient` to the +# `--project` flag, which is a defensible assumption: a project name is the +# closest analogue to a routable address in cc-connect's model. Operators +# who only run a single project can ignore the value entirely. +# +# Follow-up: validate against cc-connect upstream +# (https://github.com/chenhg5/cc-connect). If `--project` is not supported, +# downgrade the argv to `["send","{message}"]` and document that recipient +# is informational only. +_cc_connect_register_cli_channel() { + local cmd + if [ -n "${CC_BIN:-}" ]; then + cmd="$CC_BIN" + else + cmd="$(command -v cc-connect 2>/dev/null || echo cc-connect)" + fi + + cli_channel_register \ + "cc-connect" \ + "$cmd" \ + '["send","--project","{recipient}","{message}"]' \ + "true" \ + "600" } _cc_connect_write_config() { @@ -131,6 +168,12 @@ bridge_sync_config() { fi log " To update upstream: npm update -g cc-connect" log " User config (never touched): \$CC_DATA_DIR/config.toml (defaults to \$HOME/.cc-connect/config.toml)" + + # Resolve CC_BIN so the channel registration uses the actual path on this host. + if [ -z "${CC_BIN:-}" ]; then + CC_BIN=$(command -v cc-connect 2>/dev/null || echo "cc-connect") + fi + _cc_connect_register_cli_channel } # ============================================================================ diff --git a/bridges/kimaki.sh b/bridges/kimaki.sh index afbc685..3030df4 100644 --- a/bridges/kimaki.sh +++ b/bridges/kimaki.sh @@ -55,6 +55,37 @@ bridge_install() { fi _kimaki_sync_bin_helpers + _kimaki_register_cli_channel +} + +# _kimaki_register_cli_channel +# +# Register kimaki with the Data Machine Code CLI transport runtime so that +# `agents/dispatch-message` (substrate: Automattic/agents-api) can deliver +# messages to Discord channels by shelling kimaki. `recipient` is the Discord +# channel ID (numeric string) the message is delivered to. `message` is the +# message body. +# +# We register the local-mode adapter shim (`datamachine-kimaki`) installed by +# _kimaki_sync_bin_helpers; it normalises Kimaki send flags across versions. +# Falls back to the resolved global `kimaki` binary if the adapter isn't on +# disk yet (early VPS installs predating the adapter shim). +_kimaki_register_cli_channel() { + local cmd + if [ -n "${RESOLVED_DATAMACHINE_KIMAKI:-}" ] && [ -x "$RESOLVED_DATAMACHINE_KIMAKI" ]; then + cmd="$RESOLVED_DATAMACHINE_KIMAKI" + elif [ -n "${KIMAKI_BIN:-}" ]; then + cmd="$KIMAKI_BIN" + else + cmd="$(command -v kimaki 2>/dev/null || echo kimaki)" + fi + + cli_channel_register \ + "kimaki" \ + "$cmd" \ + '["send","--channel","{recipient}","--prompt","{message}"]' \ + "true" \ + "600" } _kimaki_sync_bin_helpers() { @@ -365,6 +396,10 @@ bridge_sync_config() { fi fi + # Refresh the CLI-channel registration so DMC's dispatch runtime picks up + # the latest adapter path (npm-global moves between hosts). + _kimaki_register_cli_channel + log " Done." # Export resolved paths so print_summary can reference them diff --git a/bridges/telegram.sh b/bridges/telegram.sh index 52561f6..2514cac 100644 --- a/bridges/telegram.sh +++ b/bridges/telegram.sh @@ -103,6 +103,50 @@ LOG_LEVEL=info" else _telegram_install_systemd fi + + _telegram_register_cli_channel +} + +# _telegram_register_cli_channel +# +# Register telegram with the Data Machine Code CLI transport runtime. +# +# Mismatch caveat: opencode-telegram-bot is a long-running INBOUND bot — it +# polls Telegram for incoming messages and forwards them to a local opencode +# server. It has no `send` CLI subcommand for pushing outbound messages on +# behalf of agents/dispatch-message. To preserve the channel/recipient +# routing model anyway, this bridge registers `curl` against Telegram's +# sendMessage Bot API, parameterised with the same TELEGRAM_BOT_TOKEN the bot +# already uses. `recipient` is the Telegram chat ID (numeric, e.g. a user ID +# from @userinfobot or a group chat ID). `message` is the message body. +# +# Documented assumptions: +# - TELEGRAM_BOT_TOKEN is set in the environment at register time (it is, +# because bridge_install requires it). The token is baked into the +# `command` URL fragment for simplicity; rotation requires re-running +# setup / upgrade. +# - `curl` is available on the host. Every supported VPS distro ships it +# by default; if absent on a custom box the dispatch will fail loudly, +# which is acceptable for a not-yet-shipped runtime. +# - We do NOT shell out to opencode-telegram-bot for outbound — that bot's +# job is the inbound side, and conflating the two via a single CLI would +# require upstream changes we don't control. +_telegram_register_cli_channel() { + if [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then + warn " TELEGRAM_BOT_TOKEN not set — skipping CLI-channel registration for telegram" + warn " Re-run setup/upgrade with TELEGRAM_BOT_TOKEN exported once configured" + return 0 + fi + + local curl_bin + curl_bin=$(command -v curl 2>/dev/null || echo "/usr/bin/curl") + + cli_channel_register \ + "telegram" \ + "$curl_bin" \ + "[\"-sS\",\"-X\",\"POST\",\"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage\",\"--data-urlencode\",\"chat_id={recipient}\",\"--data-urlencode\",\"text={message}\"]" \ + "true" \ + "60" } _telegram_install_launchd() { @@ -180,6 +224,17 @@ bridge_sync_config() { log " User env files (never touched):" log " \$HOME/.config/opencode-serve.env" log " \$HOME/.config/opencode-telegram-bot/.env" + + # Re-read TELEGRAM_BOT_TOKEN from the bot .env if not in current env; the + # upgrade path typically runs without secrets in env, but the .env file is + # the durable source. + if [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then + local tg_env="$SERVICE_HOME/.config/opencode-telegram-bot/.env" + if [ -r "$tg_env" ]; then + TELEGRAM_BOT_TOKEN=$(grep -E '^TELEGRAM_BOT_TOKEN=' "$tg_env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r') + fi + fi + _telegram_register_cli_channel } # ============================================================================ From 95e52be38436fc50633c987f06aecb3ca177437f Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 16 May 2026 16:35:29 +0000 Subject: [PATCH 3/3] docs: document agents/dispatch-message integration + legacy retirement Add an 'Outbound Dispatch (agents/dispatch-message)' section to README covering: how each bridge registers a CLI channel via the mu-plugin discovery file, the channel/recipient semantics per bridge, and a manual migration runbook for retiring the legacy /opt/agent-ping-webhook/ stack. The retirement guidance is intentionally prose + numbered list rather than an automated helper script. The stack exists on exactly one host and shipping permanent automation for a one-shot deletion would be technical debt. Refs Extra-Chill/wp-coding-agents#129 --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/README.md b/README.md index 82f5131..cc4c774 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,82 @@ Optional: Credentials are stored in `~/.config/opencode-telegram-bot/.env` (chmod 600). You can set them as environment variables during setup or edit the file after install. +## Outbound Dispatch (`agents/dispatch-message`) + +Each chat bridge installed by wp-coding-agents registers itself as a **CLI channel** with the Data Machine Code generic CLI transport runtime (Extra-Chill/data-machine-code#412). That runtime backs the `agents/dispatch-message` ability, so Data Machine flows can push messages out to whatever platform your bridge speaks — without bespoke webhooks, without HTTP detours, without per-bridge plumbing in DMC itself. + +### How it wires up + +On install (and every `upgrade.sh` run), each bridge writes its channel config into a mu-plugin file at: + +``` +$WP_PATH/wp-content/mu-plugins/wp-coding-agents-channels.php +``` + +The file is owned end-to-end by bridge installers: it is created on first registration, each bridge contributes a marker-delimited block (`// BEGIN bridge:` … `// END bridge:`), and the same install path can rewrite or remove its block idempotently. The file registers entries via the `datamachine_code_cli_channels` filter: + +```php +$channels['kimaki'] = [ + 'command' => '/usr/local/bin/datamachine-kimaki', + 'args' => [ 'send', '--channel', '{recipient}', '--prompt', '{message}' ], + 'detach' => true, + 'timeout' => 600, +]; +``` + +At dispatch time the runtime substitutes `{recipient}` and `{message}` and shells the configured command. No HTTP hop, no nginx detour, no Python. + +### Channel names + recipient semantics + +| Bridge | Channel name | `recipient` is… | `message` is… | +|-----------------|--------------|----------------------------------------------|------------------------------| +| `bridges/kimaki.sh` | `kimaki` | Discord channel ID (numeric string) | Prompt body delivered to the agent in that channel | +| `bridges/cc-connect.sh` | `cc-connect` | cc-connect project name (from `config.toml`) | Message body sent via `cc-connect send` | +| `bridges/telegram.sh` | `telegram` | Telegram chat ID (numeric, user or group) | Message body posted via Telegram's `sendMessage` Bot API | + +A flow step calling the ability looks like: + +``` +agents/dispatch-message + channel: 'kimaki' + recipient: '1476075959806590989' + message: 'Time for your check-in.' +``` + +### Bridge-specific notes + +- **kimaki** registers the local `datamachine-kimaki` adapter shim wp-coding-agents installs alongside the kimaki binary. The shim normalises Kimaki send flags across versions. If it isn't on disk yet (very early installs), the bridge falls back to the resolved global `kimaki` binary. +- **cc-connect** assumes `cc-connect send` accepts `--project `. cc-connect routes outgoing messages through its currently-bound platform per project (Feishu/DingTalk/Slack/Telegram/Discord/etc.), so `recipient` is the cc-connect project, not a raw chat ID. **Assumption to validate against upstream:** if `--project` is unsupported, the argv collapses to `["send","{message}"]` and `recipient` becomes informational only. Tracked alongside Extra-Chill/wp-coding-agents#129. +- **telegram** is the odd one out. `opencode-telegram-bot` is inbound-only (polls Telegram, forwards to a local opencode server); it has no outbound `send` subcommand. To preserve the channel/recipient model, the bridge registers `curl` against Telegram's `sendMessage` Bot API with `TELEGRAM_BOT_TOKEN` baked in. `recipient` is a Telegram chat ID. The token is captured at install/upgrade time from the existing bot `.env` if not in the current shell — rotating the token requires re-running `upgrade.sh` so the channel config picks up the new value. + +### Migrating from the legacy `agent-ping` webhook + +Earlier wp-coding-agents installs on Extra Chill's VPS shipped an out-of-band Python webhook at `/opt/agent-ping-webhook/` that POSTed to a local HTTP listener and shelled `kimaki send`. Once the CLI-channel runtime (Extra-Chill/data-machine-code#412) and these bridge registrations are deployed, that stack is dead weight — the same dispatch goes through `agents/dispatch-message` with no HTTP hop. + +Retirement is a one-time manual cleanup. There is no helper script: this stack only exists on one host, and shipping permanent automation for a one-shot deletion would be technical debt for an experimental feature. On a host that has the legacy stack: + +1. **Reconfigure the dispatch flow first.** Edit the data-machine flow that was POSTing to `https:///agent-ping/` to instead invoke `agents/dispatch-message` with `channel='kimaki'` and `recipient=''`. Run it manually (`wp datamachine flow run `) and confirm the message lands. +2. **Disable and remove the systemd unit.** + ```bash + sudo systemctl disable --now agent-ping-webhook.service + sudo rm /etc/systemd/system/agent-ping-webhook.service + sudo systemctl daemon-reload + ``` +3. **Remove the webhook directory.** + ```bash + sudo rm -rf /opt/agent-ping-webhook/ + ``` +4. **Drop the `location /agent-ping/` block from your nginx site config** (e.g. `/etc/nginx/sites-available/`), then: + ```bash + sudo nginx -t && sudo systemctl reload nginx + ``` +5. **Free the local port (8422 in the EC setup) and remove the token file.** + ```bash + sudo rm -f /home/opencode/.secrets/agent-ping-token.txt + ``` + +After this, the only outbound message path is `agents/dispatch-message` → DMC CLI runtime → bridge CLI. If you weren't running the legacy webhook to begin with (every install of wp-coding-agents from v0.5.x onward), skip the migration entirely — the bridge registrations land on a clean disk. + ## Requirements **VPS:**