diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 02:43:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 03:47:07 +0300 |
| commit | fbd20ad6e488fb3b9efc1612fdffee6e672976c1 (patch) | |
| tree | d21daf93241786f8aa6fde23b41cdb9e15182b79 /docs | |
| parent | d35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (diff) | |
feat: add plugin v1 system
Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs.
Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/next/CHANGELOG.md | 10 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/cli-reference.mdx | 81 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/configuration.mdx | 11 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/index.mdx | 10 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/marketplace.mdx | 36 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/plugins.mdx | 189 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/socket-api.mdx | 247 |
7 files changed, 574 insertions, 10 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index c4271cf6..13e84347 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,16 @@ ## Unreleased ### Added +- Added local plugin v1 support with `plugin.link/list/unlink/enable/disable`, + manifest-declared actions, event hooks, managed plugin panes, link handlers, + command logs, keybinding integration, and authoring docs under Preview docs. +- Added `herdr plugin install <owner>/<repo>[/subdir...]`, `plugin uninstall`, + source metadata in `plugin.list`, offline registry fallback, and a + human-readable default `plugin list` with `--json` for scripts. +- Added supporting plugin host APIs for `pane.current`, `pane.process_info`, + `client.window_title.set/clear`, `layout.export/apply`, plugin pane placement, + plugin invocation context/env injection, and plugin pane ownership across + `pane.move`. - Added `pane.move` and `herdr pane move` to relocate a running pane into another tab, a new tab, or a new workspace without restarting its terminal process. (#299) ### Changed diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx index fc6be710..106823b3 100644 --- a/docs/next/website/src/content/docs/cli-reference.mdx +++ b/docs/next/website/src/content/docs/cli-reference.mdx @@ -69,7 +69,7 @@ Use `default` as the session name when you need to stop the default session expl ```bash herdr workspace list -herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus] +herdr workspace create [--cwd PATH] [--label TEXT] [--env KEY=VALUE] [--focus] [--no-focus] herdr workspace get <workspace_id> herdr workspace focus <workspace_id> herdr workspace rename <workspace_id> <label> @@ -99,7 +99,7 @@ Worktrees are normal Herdr workspaces with Git checkout provenance. `worktree cr ```bash herdr tab list [--workspace <workspace_id>] -herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus] +herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--env KEY=VALUE] [--focus] [--no-focus] herdr tab get <tab_id> herdr tab focus <tab_id> herdr tab rename <tab_id> <label> @@ -110,15 +110,17 @@ herdr tab close <tab_id> ```bash herdr pane list [--workspace <workspace_id>] +herdr pane current [--pane ID|--current] herdr pane get <pane_id> herdr pane layout [--pane ID|--current] +herdr pane process-info [--pane ID|--current] herdr pane neighbor --direction left|right|up|down [--pane ID|--current] herdr pane edges [--pane ID|--current] herdr pane focus --direction left|right|up|down [--pane ID|--current] herdr pane resize --direction left|right|up|down [--amount FLOAT] [--pane ID|--current] herdr pane zoom [<pane_id>|--pane ID|--current] [--toggle|--on|--off] herdr pane rename <pane_id> <label>|--clear -herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--focus] [--no-focus] +herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--focus] [--no-focus] herdr pane swap --direction left|right|up|down [--pane ID|--current] herdr pane swap --source-pane ID --target-pane ID herdr pane move <pane_id> --tab <tab_id> --split right|down [--target-pane ID] [--ratio FLOAT] [--focus|--no-focus] @@ -181,6 +183,10 @@ herdr pane report-metadata <pane_id> \ `STATUS` is one of `idle`, `working`, `blocked`, `done`, or `unknown`. `--agent` is a guard for the authoritative agent label. `--applies-to-source` is a guard for the active lifecycle authority source. Use `--display-agent` to change the visible name. +Metadata text is normalized before storage. Herdr trims surrounding whitespace, removes control characters, caps `--custom-status` at 32 characters, and caps `--title`, `--display-agent`, and each `--state-label` value at 80 characters. Empty normalized values are ignored. + +`--source` and `--applies-to-source` must be 80 characters or fewer and may contain only ASCII letters, digits, colon, dot, underscore, and hyphen. `--ttl-ms` makes metadata expire automatically and must be between `1` and `86400000` milliseconds. Omit it for metadata that should stay until replaced, cleared, or the pane closes. `--seq` lets Herdr ignore stale reports from the same `--source`; stale reports are accepted by the API but ignored by pane state. + ## Agents ```bash @@ -192,7 +198,7 @@ herdr agent rename <target> <name>|--clear herdr agent focus <target> herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS] herdr agent attach <target> [--takeover] -herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...> +herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--env KEY=VALUE] [--focus|--no-focus] -- <argv...> herdr agent explain <target> [--json|--verbose] herdr agent explain --file PATH --agent LABEL [--json|--verbose] ``` @@ -209,9 +215,12 @@ Use `pane send-text`, `pane send-keys`, `pane run`, and `terminal attach` for or ```bash herdr terminal attach <terminal_id> [--takeover] +herdr terminal title set <title> +herdr terminal title clear ``` Detach from direct attach with `ctrl+b q`. Send literal `ctrl+b` with `ctrl+b ctrl+b`. +`terminal title clear` restores Herdr's default outer terminal window title. ## Waits @@ -259,6 +268,66 @@ herdr integration uninstall cursor herdr integration status [--outdated-only] ``` +## Plugins + +Plugin commands install and run local executable workflow plugins. A plugin is a manifest plus out-of-process commands; Herdr owns the host surface and plugins own their implementation language. + +Install, list, and remove plugins: + +```bash +herdr plugin install <owner>/<repo>[/subdir...] [--ref REF] [--yes] +herdr plugin list [--plugin ID] [--json] +herdr plugin uninstall <plugin_id|owner/repo[/subdir...]> +herdr plugin enable <plugin_id> +herdr plugin disable <plugin_id> +``` + +`plugin install` accepts GitHub shorthand only, such as `ogulcancelik/herdr-plugin-examples/worktree-bootstrap`. It uses `git`, shows a trust preview in interactive terminals, runs supported manifest build commands, and stores GitHub installs in a Herdr-managed directory. Use `--yes` for noninteractive installs. Reinstalling a GitHub-managed plugin replaces that managed checkout. Installing over a locally linked plugin is refused. `plugin list` is human-readable by default; pass `--json` for the raw API response. + +Local development: + +```bash +herdr plugin link <path> [--disabled] +herdr plugin unlink <plugin_id> +``` + +`plugin link` accepts a plugin directory containing `herdr-plugin.toml` or a direct manifest path. It is still the right command while authoring or testing a plugin from a local checkout. `plugin unlink` unregisters the plugin and leaves files alone. `plugin uninstall` unregisters a plugin and also removes Herdr-managed GitHub checkout files. For GitHub installs, uninstall accepts either the plugin id or the same `owner/repo[/subdir...]` shorthand used by install. Actions, event hooks, panes, and link handlers are declared in the manifest; runtime action registration is not part of v1. + +Actions: + +```bash +herdr plugin action list [--plugin ID] +herdr plugin action invoke <action_id> [--plugin ID] +``` + +`plugin action invoke` starts the manifest command for an installed, enabled, +platform-compatible plugin action and prints the started command log record in +the JSON response. Use the qualified action id (`plugin.id.action`) when more +than one plugin uses the same action id. Local action ids cannot contain dots, +so qualified ids remain unambiguous even when plugin ids contain dots. + +Logs: + +```bash +herdr plugin log list [--plugin ID] [--limit N] +``` + +Managed terminal panes: + +```bash +herdr plugin pane open --plugin ID --entrypoint ID [--placement overlay|split|tab|zoomed] [--workspace ID] [--target-pane PANE] [--direction right|down] [--cwd PATH] [--env KEY=VALUE] [--focus|--no-focus] +herdr plugin pane focus <pane_id> +herdr plugin pane close <pane_id> +``` + +`plugin pane open` requires the plugin to be linked, enabled, and compatible +with the current platform. It starts a manifest-declared `[[panes]]` command as +a Herdr-managed terminal pane. The manifest default is `overlay`, which opens a +temporary zoomed overlay over the active pane. It can also open as a split, a +new tab, or a zoomed pane. Native non-terminal plugin panes are a later surface. + +`--env KEY=VALUE` can be repeated on process-launching commands. It applies to the newly launched process only. Herdr-managed variables such as `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ENV`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, `HERDR_PANE_ID`, `HERDR_PLUGIN_ID`, `HERDR_PLUGIN_ROOT`, `HERDR_PLUGIN_CONFIG_DIR`, `HERDR_PLUGIN_STATE_DIR`, `HERDR_PLUGIN_ENTRYPOINT_ID`, and `HERDR_PLUGIN_CONTEXT_JSON` stay authoritative when they conflict with caller-provided env. + ## Read sources | Source | Meaning | @@ -275,5 +344,9 @@ herdr integration status [--outdated-only] | `HERDR_CONFIG_PATH` | Override the config file path. | | `HERDR_SESSION` | Select a named session for CLI commands. | | `HERDR_SOCKET_PATH` | Low-level socket path override. | +| `HERDR_ENV` | Set to `1` inside Herdr-managed pane processes. | +| `HERDR_PANE_ID` | Public pane id for the running pane process. | +| `HERDR_TAB_ID` | Public tab id for the running pane process. | +| `HERDR_WORKSPACE_ID` | Public workspace id for the running pane process. | | `HERDR_LOG` | Set log filter, for example `HERDR_LOG=herdr=debug`. | | `HERDR_DISABLE_SOUND` | Disable sound playback even when sound notifications are enabled. | diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index c22e0107..8fc35afb 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -224,6 +224,17 @@ description = "run lazygit" `type = "shell"` runs detached in the background. +`type = "plugin_action"` invokes an installed plugin action id. Use the +qualified id when action ids are not globally unique: + +```toml +[[keys.command]] +key = "prefix+l" +type = "plugin_action" +command = "example.layout.apply" +description = "apply layout" +``` + An optional `description` can be provided. When specified, this description is displayed in the keybind help panel (opened with `prefix+?`) in place of the default `'custom command'` label. Custom commands receive `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ACTIVE_WORKSPACE_ID`, `HERDR_ACTIVE_TAB_ID`, `HERDR_ACTIVE_PANE_ID`, and `HERDR_ACTIVE_PANE_CWD` when those values are available. Shell commands run from the focused pane's working directory when Herdr can detect it. diff --git a/docs/next/website/src/content/docs/index.mdx b/docs/next/website/src/content/docs/index.mdx index 68e1d869..1502462d 100644 --- a/docs/next/website/src/content/docs/index.mdx +++ b/docs/next/website/src/content/docs/index.mdx @@ -64,4 +64,14 @@ The guide teaches your agent Herdr's concepts, setup, configuration, and common [Read the API guide →](/docs/socket-api/) </Card> + <Card title="Plugins"> + Author local executable workflow plugins with manifest actions and event hooks. + + [Write a plugin →](/docs/plugins/) + </Card> + <Card title="Marketplace"> + Share plugins from GitHub today and tag your repo to be listed when the marketplace launches. + + [Publish a plugin →](/docs/marketplace/) + </Card> </CardGrid> diff --git a/docs/next/website/src/content/docs/marketplace.mdx b/docs/next/website/src/content/docs/marketplace.mdx new file mode 100644 index 00000000..e06a1f79 --- /dev/null +++ b/docs/next/website/src/content/docs/marketplace.mdx @@ -0,0 +1,36 @@ +--- +title: Marketplace +description: Publish and share Herdr plugins, and get listed when the marketplace launches. +--- + +The Herdr plugin marketplace is a discoverable index of installable plugins. It +is not live yet. This page is how to be ready for it. + +## Share a plugin today + +The marketplace adds discovery on top of installation; it does not replace it. +Anyone can already install your plugin straight from GitHub: + +```bash +herdr plugin install owner/repo[/subdir...] +``` + +Publish a normal public GitHub repository with a `herdr-plugin.toml` manifest at +its root, or in a subdirectory, and that command works now. See +[Plugins](/docs/plugins/) for the manifest and authoring reference. + +## Get listed when it launches + +Add the GitHub topic `herdr-plugin` to your repository. That topic is the exact +signal the future index will use for discovery, so tagging now means your plugin +is indexed on day one, with no migration and no resubmission. + +## What a listing will show + +A marketplace card will surface your plugin's `id`, `name`, `description`, +declared `platforms`, and a link back to the source repository. Getting that +manifest metadata right today is what makes your listing accurate later. Declare +`platforms` honestly, because the index will use it to show where a plugin runs. + +Plugins stay ordinary GitHub repositories. The marketplace is a discovery and +trust layer on top of the same `herdr plugin install` path you can use now. diff --git a/docs/next/website/src/content/docs/plugins.mdx b/docs/next/website/src/content/docs/plugins.mdx new file mode 100644 index 00000000..652b44a0 --- /dev/null +++ b/docs/next/website/src/content/docs/plugins.mdx @@ -0,0 +1,189 @@ +--- +title: Plugins +description: Author local Herdr plugins with manifest actions, event hooks, and panes. +--- + +Herdr plugins are shareable, executable workflow packages. A plugin can be a +Bash script, JavaScript app, Lua script, Rust binary, or any other argv command +your machine can run. Herdr owns the host surface: installation, manifest +validation, keybindings, terminal panes, events, invocation context, and socket +access. The plugin owns its implementation language, dependencies, files, and +durable state. + +A plugin directory contains `herdr-plugin.toml`. Herdr installs or links that +manifest, then runs declared action, event, and pane commands out of process. +The manifest is the contract. Runtime action registration and native +non-terminal plugin UI are not part of plugin v1. + +## Manifest + +```toml +id = "example.layout" +name = "Layout" +version = "0.1.0" +description = "Apply project layouts" +platforms = ["linux", "macos", "windows"] + +[[build]] +command = ["npm", "ci"] + +[[build]] +command = ["npm", "run", "build"] +platforms = ["linux", "macos"] + +[[actions]] +id = "apply" +title = "Apply layout" +contexts = ["workspace"] +command = ["node", "dist/apply.js"] + +[[events]] +on = "worktree.created" +command = ["herdr", "workspace", "list"] + +[[panes]] +id = "board" +title = "Project board" +placement = "overlay" +command = ["herdr-board"] + +[[link_handlers]] +id = "github-issue" +title = "Open GitHub issue" +pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$" +action = "apply" +``` + +Actions are manifest-only. Pane commands are manifest-only too; +`plugin.pane.open` selects a declared `[[panes]]` entrypoint and does not +accept runtime argv. Action ids, pane ids, and link handler ids are local ids +inside the plugin; they may use ASCII letters, digits, colon, underscore, and +hyphen, but not dots. Each id type must be unique inside a plugin. + +Use `platforms` to declare where the plugin can run. Local plugins without +`platforms` link with a warning. + +## Install and link + +Install an example plugin: + +```bash +herdr plugin install ogulcancelik/herdr-plugin-examples/agent-telegram-notify +herdr plugin list +herdr plugin action list --plugin examples.agent-telegram-notify +``` + +When you are authoring a local plugin, link the working directory instead: + +```bash +herdr plugin link /path/to/plugin +herdr plugin action list --plugin example.layout +herdr plugin action invoke example.layout.apply +herdr plugin pane open --plugin example.layout --entrypoint board +herdr plugin log list --plugin example.layout +``` + +`plugin install` accepts GitHub shorthand only, such as +`owner/repo/subdir`. It clones with `git`, shows a preview in interactive +terminals, runs supported build commands, then stores the checkout under +Herdr-managed plugin data and registers it. Use `--yes` for noninteractive +installs. Reinstalling a GitHub-managed plugin replaces that managed checkout. +Installing over a locally linked plugin is refused; unlink or uninstall the +local plugin first. + +`plugin uninstall <id-or-source>` unregisters the plugin. For GitHub-managed +installs it also removes the managed checkout, and it accepts either the plugin +id or the same `owner/repo[/subdir...]` shorthand used by install. +`plugin unlink <id>` only unregisters a plugin and leaves files alone, which is +useful for local development. There is no separate `plugin update` in v1; +reinstall from GitHub to refresh a managed plugin. + +The example cookbook repo is `ogulcancelik/herdr-plugin-examples`. It contains +separate example plugins in subdirectories, including `agent-telegram-notify`, +`github-link-preview`, and `dev-layout-bootstrap`. These are examples to copy, +not maintained official plugins. + +## Build commands + +Build commands run during GitHub `plugin install` after confirmation and before +Herdr registers the plugin. If a build command fails, install aborts and the +plugin is not registered. `plugin link` does not run build commands; local +authors build their working tree themselves. Build commands may generate files, +but changing `herdr-plugin.toml` after the install preview aborts install. Build +failures show the plugin id, build index, working directory, command, exit +status or spawn error, and capped stdout/stderr without interpreting tool output. + +Build commands are plain argv commands too, but they do not receive runtime +plugin context or Herdr socket env. Plugin authors should document required +system tools such as `cargo`, `npm`, `bun`, or `lua`; Herdr reports build +failures but does not install missing toolchains. + +## Commands and environment + +Runtime commands run with the plugin directory as their working directory. Herdr +injects `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ENV=1`, +`HERDR_PLUGIN_ID`, `HERDR_PLUGIN_ROOT`, `HERDR_PLUGIN_CONFIG_DIR`, +`HERDR_PLUGIN_STATE_DIR`, `HERDR_PLUGIN_CONTEXT_JSON`, and any available +`HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID`. Action commands also +receive `HERDR_PLUGIN_ACTION_ID`; event hooks receive `HERDR_PLUGIN_EVENT` and +`HERDR_PLUGIN_EVENT_JSON`; pane commands receive `HERDR_PLUGIN_ENTRYPOINT_ID`. + +`HERDR_PLUGIN_ROOT` is the installed or linked plugin directory. Do not store +user credentials or durable state there, because GitHub-installed plugin roots +are managed source checkouts. Put user-editable config such as `.env` files +under `HERDR_PLUGIN_CONFIG_DIR`, and put local runtime state under +`HERDR_PLUGIN_STATE_DIR`. Herdr does not read, create, migrate, validate, sync, +or delete their contents. The plugin owns the file format and lifecycle. + +`HERDR_PLUGIN_CONTEXT_JSON` can include workspace, tab, focused pane, worktree, +agent, selected text, clicked URL, and link handler fields when they are +available for that invocation. Shell plugins can read the individual env vars +for common ids, or parse the context JSON for the full shape. + +## Panes + +Manifest pane `placement` defaults to `overlay`, which opens a temporary zoomed +overlay over the active pane and restores the previous focus and zoom when it +closes. A `plugin.pane.open` request can override the manifest placement with +`overlay`, `split`, `tab`, or `zoomed`. + +Plugin panes are normal Herdr panes after they open. Plugins can call standard +pane APIs such as `pane.move`, `pane.swap`, `pane.resize`, and `pane.zoom` +through the socket or CLI; Herdr keeps plugin pane ownership attached to the +underlying pane when it moves across tabs or workspaces. + +## Keybindings + +Bind a key to an installed plugin action: + +```toml +[[keys.command]] +key = "prefix+l" +type = "plugin_action" +command = "example.layout.apply" +description = "apply layout" +``` + +## Link handlers + +Use `[[link_handlers]]` to route modified clicks on matching terminal URLs to a +plugin action instead of opening the URL in the browser. The modified-click +modifier is Control on Linux and Windows, and Command/Super on macOS. `pattern` +is a Rust regular expression matched against the clicked URL, and `action` must +name an action declared by the same plugin. Link handler actions receive +`invocation_source = "link_click"`, `clicked_url`, and `link_handler_id` in +`HERDR_PLUGIN_CONTEXT_JSON`; shell plugins can also read +`HERDR_PLUGIN_CLICKED_URL` and `HERDR_PLUGIN_LINK_HANDLER_ID`. Handlers are +checked in manifest order inside each plugin. + +## Storage + +There is no Herdr-managed plugin storage API in v1. Plugins that need durable +state should own their files or database. + +## Marketplace + +A plugin marketplace is coming. You can already publish a GitHub repository and +share it with `herdr plugin install owner/repo[/subdir]`. Tag your repo with the +GitHub topic `herdr-plugin` now to be listed when the marketplace launches. See +[Marketplace](/docs/marketplace/) for how publishing and discovery will work. diff --git a/docs/next/website/src/content/docs/socket-api.mdx b/docs/next/website/src/content/docs/socket-api.mdx index fb124ac9..4f8071ad 100644 --- a/docs/next/website/src/content/docs/socket-api.mdx +++ b/docs/next/website/src/content/docs/socket-api.mdx @@ -82,13 +82,16 @@ Raw socket method names use dot notation: | --- | --- | | Server | `ping`, `server.stop`, `server.reload_config`, `server.agent_manifests`, `server.reload_agent_manifests` | | Notification | `notification.show` | +| Client | `client.window_title.set`, `client.window_title.clear` | | Workspace | `workspace.create`, `workspace.list`, `workspace.get`, `workspace.focus`, `workspace.rename`, `workspace.close` | | Worktree | `worktree.list`, `worktree.create`, `worktree.open`, `worktree.remove` | | Tab | `tab.create`, `tab.list`, `tab.get`, `tab.focus`, `tab.rename`, `tab.close` | -| Pane | `pane.split`, `pane.swap`, `pane.move`, `pane.zoom`, `pane.layout`, `pane.neighbor`, `pane.edges`, `pane.focus_direction`, `pane.resize`, `pane.list`, `pane.get`, `pane.rename`, `pane.send_text`, `pane.send_keys`, `pane.send_input`, `pane.read`, `pane.report_agent`, `pane.report_agent_session`, `pane.report_metadata`, `pane.clear_agent_authority`, `pane.release_agent`, `pane.close`, `pane.wait_for_output` | +| Pane | `pane.split`, `pane.swap`, `pane.move`, `pane.zoom`, `pane.layout`, `pane.process_info`, `pane.neighbor`, `pane.edges`, `pane.focus_direction`, `pane.resize`, `pane.list`, `pane.current`, `pane.get`, `pane.rename`, `pane.send_text`, `pane.send_keys`, `pane.send_input`, `pane.read`, `pane.report_agent`, `pane.report_agent_session`, `pane.report_metadata`, `pane.clear_agent_authority`, `pane.release_agent`, `pane.close`, `pane.wait_for_output` | +| Layout | `layout.export`, `layout.apply` | | Agent | `agent.list`, `agent.get`, `agent.read`, `agent.explain`, `agent.send`, `agent.rename`, `agent.focus`, `agent.start` | | Events | `events.subscribe`, `events.wait` | | Integrations | `integration.install`, `integration.uninstall` | +| Plugins | `plugin.link`, `plugin.list`, `plugin.unlink`, `plugin.enable`, `plugin.disable`, `plugin.action.list`, `plugin.action.invoke`, `plugin.log.list`, `plugin.pane.open`, `plugin.pane.focus`, `plugin.pane.close` | Some CLI commands are conveniences around these methods. For example, `herdr agent wait` resolves an agent target and then subscribes to pane agent state events. @@ -97,20 +100,83 @@ schema makes `pane_id` optional use the server's active focused pane when it is omitted. `pane.move` always requires the source `pane_id`. ```json +{"id":"req_current","method":"pane.current","params":{"caller_pane_id":"w1:p1"}} {"id":"req_layout","method":"pane.layout","params":{"pane_id":"w1:p1"}} {"id":"req_neighbor","method":"pane.neighbor","params":{"pane_id":"w1:p1","direction":"right"}} {"id":"req_edges","method":"pane.edges","params":{"pane_id":"w1:p1"}} {"id":"req_focus","method":"pane.focus_direction","params":{"direction":"right"}} {"id":"req_resize","method":"pane.resize","params":{"pane_id":"w1:p1","direction":"right","amount":0.1}} {"id":"req_zoom","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"toggle"}} -{"id":"req_split","method":"pane.split","params":{"direction":"right","ratio":0.333}} +{"id":"req_split","method":"pane.split","params":{"direction":"right","ratio":0.333,"env":{"HERDR_ROLE":"tests"}}} +{"id":"req_process","method":"pane.process_info","params":{"pane_id":"w1:p1"}} ``` +`pane.current` returns a single `PaneInfo`. When `caller_pane_id` is present, +Herdr returns that pane. When it is omitted, Herdr returns the active focused +pane. + `pane.layout` returns the tab layout snapshot with `workspace_id`, `tab_id`, `zoomed`, outer `area`, `focused_pane_id`, pane rects, and split rects/ratios. `pane.neighbor` and `pane.edges` include that same layout snapshot so clients can make the next decision without private layout state. +`pane.process_info` returns the pane's shell pid, foreground process group id +when available, and foreground processes with pid, name, argv/cmdline, and cwd +when the platform exposes them. + +`layout.export` returns a portable tab layout tree. Omit `tab_id` and `pane_id` +to export the active tab, pass `tab_id` to export that tab, or pass `pane_id` to +export the tab containing that pane. + +```json +{"id":"req_export","method":"layout.export","params":{"tab_id":"w1:t1"}} +``` + +The response includes `workspace_id`, `tab_id`, `zoomed`, `focused_pane_id`, and +`root`. `root` is a BSP tree of `pane` and `split` nodes. Pane nodes can include +`pane_id`, `label`, `cwd`, and argv `command`. Split nodes use +`direction` (`right` or `down`), `ratio`, `first`, and `second`. + +`layout.apply` creates a fresh tab from a declarative tree. If `tab_id` is +provided, Herdr creates the replacement tab first and then closes the old tab. +This restores structure, labels, cwd, env, and optional argv commands; it does +not preserve live PTYs, scrollback, or running processes. + +```json +{ + "id": "req_apply", + "method": "layout.apply", + "params": { + "workspace_id": "wabc", + "tab_label": "dev", + "focus": true, + "root": { + "type": "split", + "direction": "right", + "ratio": 0.65, + "first": { + "type": "pane", + "label": "editor", + "cwd": "/repo" + }, + "second": { + "type": "pane", + "label": "tests", + "cwd": "/repo", + "command": ["sh", "-c", "just test"], + "env": { "HERDR_ROLE": "tests" } + } + } + } +} +``` + +Process-launching methods accept an `env` object. Herdr applies those key/value +pairs to the newly launched process only. Herdr also injects `HERDR_SOCKET_PATH`, +`HERDR_ENV=1`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID` into +managed pane processes. Herdr-managed variables are authoritative when they +conflict with caller-provided env. + `pane.swap` supports directional and explicit forms: ```json @@ -185,6 +251,17 @@ The response reports whether anything was shown: Possible reasons are `shown`, `disabled`, `rate_limited`, `no_foreground_client`, and `busy`. `disabled` means `ui.toast.delivery = "off"`. `busy` means an existing in-app toast was not replaced. Terminal and system delivery are best-effort through the current foreground attached Herdr client. +Set or clear the foreground client's outer terminal window title: + +```json +{"id":"req_title","method":"client.window_title.set","params":{"title":"herdr api"}} +{"id":"req_title_clear","method":"client.window_title.clear","params":{}} +``` + +`client.window_title.clear` restores Herdr's default title. The response is +`type: "client_window_title"` with `changed` and reason `set`, `cleared`, or +`no_foreground_client`. + Worktree methods manage Git checkouts as Herdr workspaces. `worktree.create` creates a checkout and returns the new `workspace`, `tab`, `root_pane`, and `worktree` records. `worktree.open` opens an existing checkout or returns the already-open workspace. `worktree.remove` runs `git worktree remove` against a linked child workspace and never deletes the branch. Create a worktree from a source workspace: @@ -207,6 +284,154 @@ Remove a linked checkout: Use at most one of `workspace_id` or `cwd` for `worktree.list`, `worktree.create`, and `worktree.open`; omit both to use the active workspace. Use exactly one of `path` or `branch` for `worktree.open`. Raw socket `cwd` and `path` values must be absolute; the CLI expands relative `--cwd` and `--path` values before sending requests. Workspace responses include optional `worktree` provenance when a workspace belongs to a Herdr worktree group. Worktree commands can emit `workspace.updated` when an existing workspace gains or changes worktree provenance. +Worktree commands also emit lifecycle events. `worktree.create` emits `workspace.created`, `tab.created`, `pane.created`, and `worktree.created`. `worktree.open` emits `worktree.opened`, and it also emits workspace/tab/pane creation events when it opens a new Herdr workspace. `worktree.remove` emits `worktree.removed`; if the linked workspace is still open, it also emits `workspace.closed`. + +## Plugin APIs + +The plugin API is an early host surface for executable workflow tools. A plugin +is a package with a `herdr-plugin.toml` manifest. The manifest declares +shareable actions, event hooks, terminal pane entrypoints, and link handlers. +Actions and panes are manifest-only; runtime action registration and runtime +argv pane creation are not part of v1. + +Installed and linked plugins persist across restarts. Herdr writes a +`plugins.json` registry file alongside `session.json` on `plugin.link`, +`plugin.unlink`, `plugin.enable`, and `plugin.disable`. The +`herdr plugin install` CLI also writes the same registry when Herdr is not +running, then startup loads it automatically. On startup, Herdr re-reads each +manifest from its original path; if the file is missing or unparseable the entry +is kept with a `warnings` field so `plugin.list` surfaces it. + +Event hook `on` values are validated against the known Herdr event names at link time. An unrecognised name is not an error — the link still succeeds — but the returned plugin info includes a warning (e.g. `"unknown event 'worktree.craeted'"`). Check the `warnings` field in the `plugin.link` and `plugin.list` responses. + +Link a local plugin manifest: + +```json +{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/path/to/plugin","enabled":true}} +``` + +`plugin.link` also accepts optional `source` metadata. The CLI uses this when it installs from GitHub so `plugin.list` can show origin, requested ref, resolved commit, and managed checkout path: + +```json +{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/managed/plugin/herdr-plugin.toml","enabled":true,"source":{"kind":"github","owner":"ogulcancelik","repo":"herdr-plugin-examples","subdir":"worktree-bootstrap","requested_ref":"main","resolved_commit":"abc123","managed_path":"/data/plugins/github/example.worktree-bootstrap","installed_unix_ms":1780000000000}}} +``` + +The path can be a plugin directory containing `herdr-plugin.toml` or a direct +manifest path. The manifest shape is: + +```toml +id = "example.worktree-bootstrap" +name = "Worktree Bootstrap" +version = "0.1.0" +description = "Prepare new worktrees" +platforms = ["linux", "macos"] + +[[build]] +command = ["bun", "install"] + +[[actions]] +id = "bootstrap" +title = "Bootstrap worktree" +contexts = ["workspace"] +command = ["bun", "run", "bootstrap.ts"] + +[[actions]] +id = "bootstrap-win" +title = "Bootstrap worktree (Windows)" +contexts = ["workspace"] +platforms = ["windows"] +command = ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", "bootstrap.ps1"] + +[[events]] +on = "worktree.created" +command = ["bun", "run", "bootstrap.ts"] + +[[panes]] +id = "board" +title = "Worktree board" +placement = "overlay" +command = ["bun", "run", "board.ts"] + +[[link_handlers]] +id = "github-issue" +title = "Open GitHub issue" +pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$" +action = "bootstrap" +``` + +Declare `platforms` at the top level with the OS identifiers (`linux`, `macos`, +`windows`) your plugin supports. Omitting `platforms` is allowed for local +development — `plugin.link` succeeds but the response includes a warning. +Individual build commands, actions, event hooks, panes, and link handlers can +declare their own `platforms` to override the plugin-level list; if omitted they +inherit from the plugin. Invoking an action or opening a pane whose effective +platforms do not include the current OS returns a `platform_unsupported` error. + +List, enable, disable, or unlink linked plugins: + +```json +{"id":"req_plugin_list","method":"plugin.list","params":{}} +{"id":"req_plugin_disable","method":"plugin.disable","params":{"plugin_id":"example.worktree-bootstrap"}} +{"id":"req_plugin_enable","method":"plugin.enable","params":{"plugin_id":"example.worktree-bootstrap"}} +{"id":"req_plugin_unlink","method":"plugin.unlink","params":{"plugin_id":"example.worktree-bootstrap"}} +``` + +Actions are resolved from the linked manifest. `plugin.action.list` returns all actions across installed plugins; pass `plugin_id` to filter. + +```json +{"id":"req_plugin_actions","method":"plugin.action.list","params":{}} +{"id":"req_plugin_actions_filtered","method":"plugin.action.list","params":{"plugin_id":"example.worktree-bootstrap"}} +``` + +Invoke an action by its qualified id or bare action id: + +```json +{"id":"req_plugin_invoke","method":"plugin.action.invoke","params":{"action_id":"example.worktree-bootstrap.bootstrap","context":{"invocation_source":"keybinding"}}} +``` + +`plugin.action.invoke` resolves the manifest action, starts the manifest +command, and returns the Herdr-built invocation context plus the started command +log record. Missing context fields are filled from the active workspace, tab, +focused pane, worktree provenance, and request id. Invoking an action from a +disabled plugin returns a `plugin_disabled` error. + +Herdr injects `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ENV=1`, +`HERDR_PLUGIN_ID`, `HERDR_PLUGIN_ROOT`, `HERDR_PLUGIN_CONFIG_DIR`, +`HERDR_PLUGIN_STATE_DIR`, `HERDR_PLUGIN_CONTEXT_JSON`, and available +`HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID` values. Action +commands also receive `HERDR_PLUGIN_ACTION_ID`; event hooks receive +`HERDR_PLUGIN_EVENT` and `HERDR_PLUGIN_EVENT_JSON`; pane commands receive +`HERDR_PLUGIN_ENTRYPOINT_ID`. + +List recent action and event command logs: + +```json +{"id":"req_plugin_logs","method":"plugin.log.list","params":{"plugin_id":"example.worktree-bootstrap","limit":20}} +``` + +Event hooks run for enabled installed plugins when Herdr emits a matching event +name such as `worktree.created`. + +There is no Herdr-managed plugin storage API in v1. `HERDR_PLUGIN_CONFIG_DIR` +and `HERDR_PLUGIN_STATE_DIR` are path discovery only; plugins own their files, +schemas, migrations, and cleanup. + +Open a managed terminal UI: + +```json +{"id":"req_plugin_pane","method":"plugin.pane.open","params":{"plugin_id":"example.board","entrypoint":"board","placement":"zoomed","target_pane_id":"w1:p1","env":{"HERDR_ROLE":"board"},"focus":true}} +``` + +`plugin.pane.open` requires an installed, enabled, platform-compatible plugin, +then launches the requested manifest `[[panes]]` entrypoint as an argv-backed +terminal pane. Manifest pane `placement` defaults to `overlay`; request +`placement` overrides the manifest with `overlay`, `split`, `tab`, or `zoomed`. +Overlay panes target the active pane. Split and zoomed panes target an existing +pane; tab panes can target a workspace. The pane +behaves like a normal Herdr pane, but `plugin.pane.focus` and +`plugin.pane.close` only operate on panes opened through the plugin API. Focus +returns `plugin_pane_focused`; close returns `plugin_pane_closed`. + ## Socket transport Herdr uses newline-delimited JSON over a Unix domain socket. @@ -266,7 +491,7 @@ Integrations report agent state with `pane.report_agent`. `state` is semantic. It affects waits, notifications, and rollups. -`custom_status` is visual. It can show a short label like `indexing` without changing semantic behavior. +`custom_status` is visual. It can show a short activity label like `indexing` without changing semantic behavior. Session-only official integrations report native session references with `pane.report_agent_session`. State-reporting integrations can still include native session references in `pane.report_agent`. State-independent session reports do not affect waits, notifications, or rollups. @@ -323,7 +548,15 @@ Use `pane.report_metadata` when a user hook wants to customize presentation with } ``` -Metadata reports are display-only. Valid metadata can override the pane title, displayed agent name, compact custom status, and visible state labels. `working`, `blocked`, `idle`, waits, notifications, and rollups still come from semantic state. Native session restore comes from stored official session references. `agent` is an optional guard for the authoritative agent label; `applies_to_source` is an optional guard for the active lifecycle authority source. Use `display_agent` to change the visible name. `state_labels` keys must be `idle`, `working`, `blocked`, `done`, or `unknown`. Use clear fields such as `clear_custom_status: true` with the same `source` to remove one presentation override. +Metadata reports are display-only. Valid metadata can override the pane title, displayed agent name, compact activity label, and visible state labels. `working`, `blocked`, `idle`, waits, notifications, and rollups still come from semantic state. Native session restore comes from stored official session references. `agent` is an optional guard for the authoritative agent label; `applies_to_source` is an optional guard for the active lifecycle authority source. Use `display_agent` to change the visible name. `state_labels` keys must be `idle`, `working`, `blocked`, `done`, or `unknown`. Use clear fields such as `clear_custom_status: true` with the same `source` to remove one presentation override. + +Presentation text is normalized before storage. Herdr trims surrounding whitespace, removes control characters, caps `custom_status` at 32 characters, and caps `title`, `display_agent`, and each state label at 80 characters. Empty normalized values are ignored. + +`source` and `applies_to_source` are source identifiers. They must be 80 characters or fewer and may contain only ASCII letters, digits, colon, dot, underscore, and hyphen. + +Use `ttl_ms` for short-lived metadata. It must be between `1` and `86400000` milliseconds. Omit `ttl_ms` for metadata that should stay until replaced, cleared, or the pane closes. When the TTL expires, Herdr removes that source's metadata and emits a presentation change if the visible pane presentation changed. + +Use `seq` when a hook may send updates out of order. For the same `source`, reports with a sequence number less than or equal to the last accepted sequence are accepted by the API but ignored by the pane state. ## Event subscriptions @@ -343,12 +576,14 @@ Subscribe to events when you need a long-lived stream: The first response acknowledges the subscription. Later lines are pushed events. -Workspace event subscriptions include `workspace.created`, `workspace.updated`, `workspace.renamed`, `workspace.closed`, and `workspace.focused`. +Workspace event subscriptions include `workspace.created`, `workspace.updated`, `workspace.renamed`, `workspace.closed`, and `workspace.focused`. Workspace events describe Herdr UI/runtime lifecycle. `workspace.created` includes optional `workspace.worktree` provenance when the workspace belongs to a worktree group. `workspace.closed` includes a final `workspace` snapshot when Herdr can still identify it before removal. Pane event subscriptions include `pane.created`, `pane.closed`, `pane.focused`, `pane.moved`, `pane.exited`, `pane.agent_detected`, `pane.output_matched`, and `pane.agent_status_changed`. -Use `events.wait` when you want one matching event and then a response. +Worktree event subscriptions include `worktree.created`, `worktree.opened`, and `worktree.removed`. Worktree events describe Git checkout lifecycle. `worktree.created` includes the opened `workspace` and created `worktree`. `worktree.opened` includes the target `workspace`, opened `worktree`, and `already_open`. `worktree.removed` includes the `workspace_id`, removed `worktree`, and `forced`. + +Use `events.subscribe` for lifecycle events. Dedicated wait helpers are documented separately when a one-shot wait is supported. ## Reading panes |
