PHPackages                             martin-ro/laravel-herdr - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [CLI &amp; Console](/categories/cli)
4. /
5. martin-ro/laravel-herdr

ActiveLibrary[CLI &amp; Console](/categories/cli)

martin-ro/laravel-herdr
=======================

A fluent, Laravel-style client for the Herdr terminal workspace socket API.

v0.2.0(yesterday)00MITPHPPHP ^8.2CI passing

Since Jul 31Pushed yesterdayCompare

[ Source](https://github.com/martin-ro/laravel-herdr)[ Packagist](https://packagist.org/packages/martin-ro/laravel-herdr)[ Docs](https://github.com/martin-ro/laravel-herdr)[ RSS](/packages/martin-ro-laravel-herdr/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (4)Versions (3)Used By (0)

Laravel Herdr
=============

[](#laravel-herdr)

[![Latest Version on Packagist](https://camo.githubusercontent.com/967852975e68e271273a9d0c8cba856c6bd296fb2ccd0e7ad54deaa018266cda/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d617274696e2d726f2f6c61726176656c2d68657264722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/martin-ro/laravel-herdr)[![Tests](https://camo.githubusercontent.com/ac577ef5e91cc0fecb56ff14bf7ad4cd8312168c0a9451ece7c3640ca5a80a50/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d617274696e2d726f2f6c61726176656c2d68657264722f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/martin-ro/laravel-herdr/actions/workflows/tests.yml)[![Total Downloads](https://camo.githubusercontent.com/a6fc656243a51f9e698448f944032f01668b4c3230c9518441081f361a2cdf47/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d617274696e2d726f2f6c61726176656c2d68657264722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/martin-ro/laravel-herdr)[![License](https://camo.githubusercontent.com/9aa0de8e846dc3be2b2ead0e55e38178dc0c2f06c1e524cfdd44e2a8a6ac79af/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d617274696e2d726f2f6c61726176656c2d68657264722e7376673f7374796c653d666c61742d737175617265)](https://github.com/martin-ro/laravel-herdr/blob/main/LICENSE)

A fluent, Laravel-style client for the [Herdr](https://herdr.dev) terminal workspace **socket API**. It gives you an Eloquent-flavoured way to drive workspaces, tabs, panes, agents and worktrees, and to react to Herdr events — all from PHP.

> Herdr's socket API is **newline-delimited JSON over a local socket** (a Unix domain socket on Linux/macOS, a named pipe on Windows). There is no network access and no authentication — your code talks to the Herdr instance running on the same machine. This package speaks that protocol for you.

```
use MartinRo\Herdr\Facades\Herdr;

// Split the focused pane, run the test suite in the new pane, read the output.
$pane = Herdr::pane()->split('right', ratio: 0.4);

$pane->run('php artisan test');

$output = $pane->waitForOutput('Tests:')->text();
```

---

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 10, 11, 12 or 13
- A running Herdr instance on the same host

Installation
------------

[](#installation)

```
composer require martin-ro/laravel-herdr
```

The service provider and `Herdr` facade are auto-discovered. To tweak defaults, publish the config:

```
php artisan vendor:publish --tag=herdr-config
```

Configuration
-------------

[](#configuration)

`config/herdr.php` (all values are env-driven):

KeyEnvDefaultPurpose`socket_path``HERDR_SOCKET_PATH`—Explicit socket/pipe path (highest precedence).`session``HERDR_SESSION`—Named session to talk to.`base_path``HERDR_CONFIG_PATH``~/.config/herdr`Where Herdr keeps its sockets.`connect_timeout``HERDR_CONNECT_TIMEOUT``5.0`Seconds to wait for the connection.`read_timeout``HERDR_READ_TIMEOUT``null` (block)Seconds a single read may block.**Socket resolution order** (mirrors Herdr's CLI): an explicit `Herdr::session('name')` → `socket_path` → `session` → the default `~/.config/herdr/herdr.sock`.

---

Usage
-----

[](#usage)

Everything hangs off the `Herdr` facade (or the `herdr()` helper, or type-hinting `MartinRo\Herdr\HerdrManager`).

### Workspaces, tabs, panes

[](#workspaces-tabs-panes)

```
use MartinRo\Herdr\Facades\Herdr;

Herdr::workspaces()->all();              // Collection
Herdr::workspaces()->focused();          // ?Workspace
$ws = Herdr::workspaces()->create(['label' => 'api']);

$ws->rename('api-v2');
$ws->tabs();                             // Collection
$ws->panes();                            // Collection
$tab = $ws->createTab(['label' => 'tests']);

Herdr::panes()->all();                   // Collection
Herdr::panes()->current();               // the focused pane
Herdr::panes()->withAgentStatus('blocked');
```

Objects returned from reads are **live** — they carry a connection, so you can act on them directly:

```
foreach (Herdr::panes()->withAgentStatus('blocked') as $pane) {
    $pane->sendText('continue')->enter();
}
```

### Driving a pane

[](#driving-a-pane)

```
$pane = Herdr::pane('w1:p1');            // a handle by id
$pane = Herdr::pane();                   // ...or the focused pane

$pane->run('npm run build');             // sendText + enter
$pane->type('y')->enter();               // chainable
$pane->sendKeys('ctrl+c');               // key combos: enter, esc, ctrl+c, f1...
$pane->sendKeys(['g', 'g']);             // a sequence

$pane->read(lines: 100)->text();         // recent output as a string
$pane->read(source: 'recent_unwrapped')->lines();  // sources: visible|recent|recent_unwrapped|detection
$pane->read(format: 'ansi');                       // raw output with escape sequences
$pane->read(stripAnsi: false);                     // override the server's strip default

$pane->waitForOutput('Compiled successfully');            // literal substring
$pane->waitForPattern('Tests:\s+\d+ passed', [            // regex
    'timeout_ms' => 10000,
]);

$right = $pane->split('right', ratio: 0.33, options: [
    'env' => ['HERDR_ROLE' => 'tests'],
]);

$pane->zoom();                           // toggle
$pane->zoom('on');                       // or 'off'
$pane->focus();
$pane->rename('build');

$pane->moveToTab('w1:t2', 'down');       // move into an existing tab
$pane->moveToNewTab();                   // ...or a new tab
$pane->moveToNewWorkspace('lab');        // ...or a new workspace

$pane->layout();                         // the containing tab's layout snapshot
$pane->close();
```

### Agents

[](#agents)

Agents are addressed by a **target**: their unique live **name**, or the id of the **pane** hosting them. The `agent` field is the kind, e.g. `"claude"`.

```
Herdr::agents()->all();                  // Collection

// Start an agent inside an existing pane. The name becomes its target.
$pane = Herdr::pane()->split('right');
$agent = Herdr::agents()->start('claude', 'reviewer', $pane, [
    'args' => ['--continue'],
]);

// Prompt: Herdr types the text and presses Enter atomically. Optionally wait
// server-side until the agent settles.
$agent->prompt('summarise the failing test');
$agent->prompt('fix it', until: 'idle', timeoutMs: 120000);

Herdr::agent('reviewer')->sendKeys('esc');   // target by name
Herdr::agent('wA:p2')->wait('idle');         // ...or by pane id

$agent->read()->text();
$agent->explain();
$agent->focus();
$agent->kind();                          // "claude"
$agent->name();                          // "reviewer"
$agent->status();                        // idle|working|blocked|done|unknown
```

Declarative agent views filter and sort the sidebar's agent list:

```
Herdr::agents()->setView('my-tool', [
    'label'  => 'Blocked',
    'filter' => ['op' => 'eq', 'field' => 'agent_status', 'value' => 'blocked'],
]);
Herdr::agents()->clearView('my-tool');
```

Report agent state for a pane you control (e.g. from a custom CI runner):

```
Herdr::pane('w1:p1')->reportAgent([
    'source'  => 'custom:ci',
    'agent'   => 'ci-bot',
    'state'   => 'working',
    'message' => 'running tests',
]);
```

### Worktrees

[](#worktrees)

```
$ws = Herdr::workspaces()->focused();

$worktree = $ws->createWorktree('worktree/api', ['focus' => false]);
$ws->worktrees();                        // Collection
$worktree->open();
$worktree->remove();
```

### Notifications &amp; window title

[](#notifications--window-title)

```
Herdr::notify('Build failed', 'api workspace', [
    'position' => 'top-left',
    'sound'    => 'request',
]);

Herdr::windowTitle('Deploying…');
Herdr::windowTitle(null);                 // clear
```

### Events

[](#events)

Herdr pushes events over the socket after you subscribe. Because reading events **blocks**, run an event loop on its own connection — typically an artisan command or queue worker, not a web request.

Subscription types use dotted names (`pane.created`). The pushed envelopes use underscore names and nest the payload under `data`, with full info objects embedded where applicable; `$event->name()`, `$event->data()` and `$event->pane()` unwrap this for you. Data-bearing subscriptions require scope fields: `pane.agent_status_changed` and `pane.scroll_changed` require `pane_id`; `pane.output_matched` requires `pane_id`, `source` and a `match`.

```
Herdr::events()->listen(function (MartinRo\Herdr\Data\Event $event) {
    if ($event->is('pane_agent_status_changed') && $event->data()->agent_status === 'blocked') {
        logger()->warning("Agent blocked in {$event->paneId()}");
    }
    // return false to stop listening
}, subscriptions: [
    ['type' => 'pane.agent_status_changed', 'pane_id' => 'w1:p1', 'agent_status' => 'blocked'],
    'pane.exited',                         // a bare type string also works
]);
```

Block until a single event arrives (server-side, on a one-shot request). As of Herdr 0.7.5 the server only supports pane agent-status matches here:

```
$event = Herdr::events()->waitFor('pane_agent_status_changed', [
    'pane_id'      => 'w1:p1',
    'agent_status' => 'idle',
], timeoutMs: 30000);
```

Or client-side over a subscription stream:

```
$event = Herdr::events()->wait(
    [['type' => 'pane.agent_status_changed', 'pane_id' => 'w1:p1', 'agent_status' => 'done']],
);
```

### Session snapshot &amp; layouts

[](#session-snapshot--layouts)

```
$snapshot = Herdr::snapshot();           // workspaces, tabs, panes, agents, layouts, focus
$snapshot->protocol;                     // 17

$layout = Herdr::layouts()->export();    // the focused tab's declarative layout
Herdr::layouts()->apply($layout->get('layout.root'), ['tab_label' => 'restored']);
Herdr::layouts()->setSplitRatio([false], 0.3, ['tab_id' => 'w1:t1']);
```

### Plugins &amp; integrations

[](#plugins--integrations)

```
Herdr::plugins()->all();
Herdr::plugins()->link('/path/to/plugin', ['enabled' => true]);
Herdr::plugins()->enable('picker');                       // by plugin id
Herdr::plugins()->actions();                              // declared actions (all plugins)
Herdr::plugins()->invoke('jump-back', 'picker');          // action id, plugin id
Herdr::plugins()->logs('picker', limit: 20);
Herdr::plugins()->openPane('picker', 'main', ['placement' => 'popup']);

Herdr::integrations()->install('claude');                 // built-in agent integrations
Herdr::closePopup();                                      // close an open popup pane
```

### Multiple sessions

[](#multiple-sessions)

```
Herdr::session('staging')->panes()->all();
```

### Escape hatch

[](#escape-hatch)

Every documented method is reachable, but for anything not yet wrapped (or new Herdr methods) call the raw API directly:

```
$result = Herdr::call('pane.process_info', ['pane_id' => 'w1:p1']);  // array
$result = Herdr::result('server.agent_manifests')->toArray();        // Result wrapper
```

Returned `Data`/`Result` objects are permissive: typed accessors exist for the documented fields, and **every** field is reachable via `->field`, `['field']`, `->get('dot.path')`, or `->toArray()` — so undocumented or newly-added fields are never lost.

---

Error handling
--------------

[](#error-handling)

```
use MartinRo\Herdr\Exceptions\ConnectionException; // socket couldn't open / closed / timed out
use MartinRo\Herdr\Exceptions\RequestException;    // Herdr replied with an { error } payload

try {
    Herdr::pane('w9:p9')->focus();
} catch (RequestException $e) {
    $e->code();         // e.g. "pane_not_found", "invalid_request"
    $e->isNotFound();   // true for not_found / *_not_found codes
    $e->getMessage();
}
```

`find()` helpers swallow not-found errors (Herdr namespaces them per resource, e.g. `pane_not_found`, `workspace_not_found`) and return `null`:

```
Herdr::panes()->find('w9:p9');           // ?Pane
```

---

Testing
-------

[](#testing)

Fake the transport with `Herdr::fake()` — no socket required. Stub responses by method name and assert what was sent, just like `Http::fake()`.

```
use MartinRo\Herdr\Facades\Herdr;
use MartinRo\Herdr\Testing\FakeResponse;

Herdr::fake([
    'workspace.list' => ['workspaces' => [['workspace_id' => 'w1', 'label' => 'api']]],
    'pane.get'       => FakeResponse::error('pane_not_found', 'pane not found'),
    'pane.read'      => fn (array $params) => ['lines' => ["ran {$params['pane_id']}"]],
]);

$names = Herdr::workspaces()->all()->map->name();   // ['api']

Herdr::pane('w1:p1')->run('php artisan test');

Herdr::assertSent(fn ($method, $params) =>
    $method === 'pane.send_text' && $params['text'] === 'php artisan test');

Herdr::assertSentCount(2);
```

Drive the event stream in tests by queuing events:

```
Herdr::fake(['events.subscribe' => ['type' => 'subscription_started']]);
Herdr::pushEvent([
    'event' => 'pane_exited',
    'data'  => ['type' => 'pane_exited', 'pane_id' => 'w1:p1'],
]);

$event = Herdr::events()->next();        // MartinRo\Herdr\Data\Event
```

Run the package's own suite:

```
composer install
vendor/bin/phpunit
```

---

Architecture
------------

[](#architecture)

```
HerdrManager ──┬─ ping()/call()/result()        low-level escape hatch
               ├─ snapshot()                      full session state in one call
               ├─ workspaces()/panes()/agents() … plural query resources
               ├─ pane()/workspace()/agent()    … live, actionable objects
               ├─ layouts()/plugins()/integrations() … declarative & extension APIs
               ├─ events()                        subscribe + stream, waitFor()
               └─ fake()/assertSent()             test doubles

Connection      one socket per request; assigns ids, correlates responses;
   │            holds a persistent socket for event streams
   └─ Transport (interface)
        ├─ SocketTransport   real Unix socket / named pipe, buffered line reader
        └─ FakeTransport     in-memory, for tests

```

- **One request per connection.** Herdr serves a single response per socket and then closes it, so each call opens a fresh connection. Subscriptions are the exception: the socket stays open and the server pushes events down it. The `Herdr` manager is a safe long-lived singleton — it opens connections on demand, so you never hold a dead socket.
- **Framing** is exact: one JSON object per line, terminated by `\n`; the reader buffers partial reads and splits multiple messages that arrive in one chunk.
- **Correlation**: each request gets a unique `id`; responses are matched by it, and any unsolicited event lines (which carry no `id`) are parked for the event stream.
- **Data objects** wrap raw attributes permissively, so the package keeps working even where Herdr's response fields are undocumented or evolve.

> Field names and the connection model in this package were verified against a live **Herdr 0.7.5** server (socket protocol 17).

License
-------

[](#license)

MIT.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/10107779?v=4)[Martin Rohrlack](/maintainers/martin-ro)[@martin-ro](https://github.com/martin-ro)

---

Top Contributors

[![martin-ro](https://avatars.githubusercontent.com/u/10107779?v=4)](https://github.com/martin-ro "martin-ro (6 commits)")[![beecodebot[bot]](https://avatars.githubusercontent.com/in/4193078?v=4)](https://github.com/beecodebot[bot] "beecodebot[bot] (2 commits)")

---

Tags

terminallaravelSocketagentsherdrmultiplexer

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/martin-ro-laravel-herdr/health.svg)

```
[![Health](https://phpackages.com/badges/martin-ro-laravel-herdr/health.svg)](https://phpackages.com/packages/martin-ro-laravel-herdr)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M352](/packages/psalm-plugin-laravel)[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.2k99.8M338](/packages/laravel-horizon)[laravel/sail

Docker files for running a basic Laravel application.

1.9k212.4M1.4k](/packages/laravel-sail)[illuminate/database

The Illuminate Database package.

2.8k55.8M12.6k](/packages/illuminate-database)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M275](/packages/laravel-ai)[moonshine/moonshine

Laravel administration panel

1.3k268.2k86](/packages/moonshine-moonshine)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
