Development & debugging

How to iterate on a plugin, drive BeSwarm from it, and debug it.

Calling back into BeSwarm

Plugins drive BeSwarm through the same CLI you use, at $BESWARM_BIN. Commands run from a plugin act on the plugin's session by default, since $BESWARM_SESSION is set; pass --session "$BESWARM_SESSION" to be explicit:

"$BESWARM_BIN" terminal list --session "$BESWARM_SESSION"
"$BESWARM_BIN" agent list --session "$BESWARM_SESSION" --json

Run beswarm --help (or beswarm <command> for one command's manual) for the full set: agent, terminal, workspace, worktree, plugin and so on.

warning

Don't let a hooked command trigger its own event again — for example, a tab.opened hook that opens a tab. It loops.

Development loop

  1. Edit your scripts. No re-add needed: commands run fresh from disk every time.
  2. Edit beswarm.toml? Add it again (beswarm plugin add <dir>), or pick Reload in the plugin's menu in the Plugins panel. The registry stores a snapshot of the manifest, so edits don't take effect until then. Running TUIs pick up registry changes on their own.
  3. Run it: beswarm plugin run <command> or beswarm plugin view <view>.
  4. Debug: beswarm plugin history <id> shows each run's command, status, exit code, and the tail of its stdout and stderr; --limit <n> keeps only the newest runs, --json gives everything. History in the plugin's menu does the same in the TUI.
  5. Pause it with beswarm plugin off <id> and resume with on. remove <id> takes it out of the registry and leaves your files alone.

If you move or delete the plugin directory, the plugin still shows in plugin list — and as missing in the panel — but won't run (plugin_unavailable) until you add it again from its new location.

Error codes

CodeMeaning
plugin_not_foundNo added plugin with that id.
plugin_disabledThe plugin is off; plugin on it.
plugin_unavailableIts manifest is gone; add it again.
plugin_action_not_foundNo command (or view) with that name.
plugin_action_ambiguousSeveral plugins define the name; use <plugin-id>:<name>.
plugin_platform_unsupportedThe command's or view's os excludes this OS.
plugin_command_limit_reachedThe plugin already has 4 runs going.

Example: event feed

A log of every event in the plugin's data directory, a view that follows it live, and commands that call back into BeSwarm and clear the log. The scripts read the context with python3.

beswarm.toml:

[plugin]
id = "examples.event-feed"
name = "Event feed"
version = "0.1.0"
requires = "0.1.0"
about = "Log every beswarm event and watch it live"
os = ["linux", "macos"]
 
[commands.record]
run = ["sh", "record.sh"]
 
[commands.summary]
label = "Log a session summary"
run = ["sh", "summary.sh"]
 
[commands.clear]
label = "Clear the event log"
run = ["sh", "clear.sh"]
 
[views.feed]
label = "Event feed"
run = ["sh", "feed.sh"]
 
[hooks]
"tui.started" = "record"
"agent.*" = "record"
"tab.*" = "record"
"workspace.*" = "record"
"worktree.*" = "record"

record.sh — hooked to every event; one line per event:

#!/bin/sh
# One line per event: time, event name, the whole run context.
ctx=$(cat)
field() { printf '%s' "$ctx" | python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; }
printf '%s %-22s %s\n' "$(date +%H:%M:%S)" "$(field 'd.get("event",{}).get("name","?")')" \
    "$ctx" >> "$(field 'd["dirs"]["data"]')/events.log"

feed.sh — the view, reading its context from BESWARM_CONTEXT_FILE:

#!/bin/sh
# The view: follow the event log. Ctrl-C returns to the shell.
data=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["dirs"]["data"])' "$BESWARM_CONTEXT_FILE")
log="$data/events.log"
touch "$log"
clear
echo "Event feed ($log)"
echo
tail -n 40 -f "$log"

summary.sh:

#!/bin/sh
# Calls back into beswarm through the binary it was started by, and
# records what it saw. Its output also shows in `beswarm plugin history`.
ctx=$(cat)
data=$(printf '%s' "$ctx" | python3 -c 'import json,sys; print(json.load(sys.stdin)["dirs"]["data"])')
{
    printf '%s %-22s %s\n' "$(date +%H:%M:%S)" "command.summary" "$ctx"
    "$BESWARM_BIN" terminal list | sed 's/^/    /'
} | tee -a "$data/events.log"

clear.sh:

#!/bin/sh
# Empties the event log in the plugin's data dir.
data=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["dirs"]["data"])')
: > "$data/events.log"

Add it, then open Event feed from the Plugins panel, or:

beswarm plugin add ./event-feed
beswarm plugin view feed

Tips

  • Keep commands short. A plugin has 4 run slots, shared by its hooks and on-request runs; long work belongs in a view.
  • Set a timeout on anything that talks to the network.
  • Use sh -c '...' for one-liners and a script file for anything longer.
  • Parse the context with jq, Python or similar. Don't grep it.
  • Hide plumbing commands — ones only hooks run — by leaving out label.
  • Set os if your plugin uses OS-specific tools (osascript, notify-send, …).