@generativereality/herd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: herd
3
+ description: Manage Claude Code sessions across terminal tabs — list running sessions, open new ones, fork, close, inspect output, and send input. Use this when working with multiple parallel Claude Code sessions.
4
+ ---
5
+
6
+ You are managing Claude Code sessions using the `herd` CLI (Agent Herder).
7
+
8
+ ## First: Ensure herd is installed
9
+
10
+ Before running any `herd` command, verify the CLI is available:
11
+
12
+ ```bash
13
+ which herd
14
+ ```
15
+
16
+ If that returns nothing, install it silently before proceeding:
17
+
18
+ ```bash
19
+ npm install -g @generativereality/herd
20
+ ```
21
+
22
+ Do this automatically without asking the user — it's a lightweight install and required for any further action. After installing, confirm with `herd --version`.
23
+
24
+ ---
25
+
26
+ Each session runs in its own terminal tab. `herd` lets you — and other Claude Code sessions — introspect and orchestrate the full session fleet.
27
+
28
+ ## Quick Reference
29
+
30
+ ```bash
31
+ herd sessions # list all tabs with session status
32
+ herd list # list all workspaces, tabs, and blocks
33
+ herd new <name> [dir] [-w workspace] # new tab + claude
34
+ herd resume <name> [dir] # new tab + claude --continue
35
+ herd fork <tab-name> [-n new-name] # fork a session into a new tab
36
+ herd close <name-or-id> # close a tab
37
+ herd rename <name-or-id> <new-name> # rename a tab
38
+ herd scrollback <tab-or-block> [n] # read terminal output (default: 50 lines)
39
+ herd send <tab-or-block> [text] # send input — arg, --file, or stdin pipe
40
+ herd config # show config and path
41
+ ```
42
+
43
+ ## Workflow: Checking What's Running
44
+
45
+ Before starting new sessions, always check what's already active:
46
+
47
+ ```bash
48
+ herd sessions
49
+ ```
50
+
51
+ Output example:
52
+ ```
53
+ Sessions
54
+ ==================================================
55
+
56
+ Workspace: work (current)
57
+
58
+ [a1b2c3d4] "auth" ◄ ~/Dev/myapp
59
+ ● active
60
+ [e5f6a7b8] "api" ~/Dev/myapp
61
+ ○ idle
62
+ [c9d0e1f2] "infra" ~/Dev/myapp
63
+ terminal
64
+ last: $ git status
65
+ ```
66
+
67
+ ## Workflow: Opening a Session Batch
68
+
69
+ ```bash
70
+ herd new auth ~/Dev/myapp
71
+ herd new api ~/Dev/myapp
72
+ herd new infra ~/Dev/myapp
73
+ ```
74
+
75
+ Each tab is automatically named and the claude session name is synced to the tab title.
76
+
77
+ ## Workflow: Resuming After Restart
78
+
79
+ ```bash
80
+ herd sessions # identify which tabs need resuming
81
+ herd resume auth ~/Dev/myapp
82
+ herd resume api ~/Dev/myapp
83
+ ```
84
+
85
+ ## Workflow: Forking a Session
86
+
87
+ Use `fork` when you want to try an alternative approach without disrupting the original:
88
+
89
+ ```bash
90
+ herd fork auth # creates "auth-fork" tab
91
+ herd fork auth -n "auth-v2" # creates "auth-v2" tab
92
+ ```
93
+
94
+ The forked session runs `claude --resume <session-id> --fork-session` — it shares context from the original but creates an independent new session.
95
+
96
+ ## Workflow: Spawning a Parallel Agent
97
+
98
+ As a Claude Code session, you can spawn a sibling session to work on a parallel task:
99
+
100
+ ```bash
101
+ herd new payments ~/Dev/myapp
102
+ herd send payments --file /tmp/task.txt # send a prompt from a file
103
+ echo "implement the billing endpoint" | herd send payments # or via stdin
104
+ herd send payments "yes\n" # or inline for quick replies
105
+ ```
106
+
107
+ ## Workflow: Monitoring Another Session
108
+
109
+ ```bash
110
+ herd scrollback auth # last 50 lines
111
+ herd scrollback auth 200 # last 200 lines
112
+ ```
113
+
114
+ ## Workflow: Sending Input to a Session
115
+
116
+ ```bash
117
+ herd send auth "yes\n" # approve a tool call
118
+ herd send auth "\n" # press enter (confirm a prompt)
119
+ herd send auth "/clear\n" # send a slash command
120
+ herd send auth --file ~/prompts/task.txt # send a full prompt from file
121
+ echo "do the thing" | herd send auth # pipe via stdin
122
+ ```
123
+
124
+ ## Workflow: Cleanup
125
+
126
+ ```bash
127
+ herd sessions # find idle/terminal tabs
128
+ herd close old-feature # close by name (prefix match)
129
+ herd close e5f6a7b8 # close by block ID prefix
130
+ ```
131
+
132
+ ## Tab Naming Conventions
133
+
134
+ Name tabs after the **project or task**:
135
+ - `auth` — authentication work
136
+ - `api` — API service
137
+ - `infra` — infrastructure
138
+ - `pr-1234` — specific PR work
139
+ - `auth-v2` — forked attempt
140
+
141
+ ## Notes
142
+
143
+ - Tab names are matched by exact name or prefix (case-insensitive)
144
+ - Block IDs can be abbreviated to the first 8 characters
145
+ - `herd new` and `herd resume` automatically pass `--name <tab-name>` to claude, syncing the session display name with the tab title
146
+ - Configured `claude.flags` in `~/.config/herd/config.toml` are applied to every session
147
+ - `herd send` resolves tab names to their terminal block automatically
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # Agent Herder
2
+
3
+ **Run a fleet of Claude Code sessions. From the CLI — or from Claude itself.**
4
+
5
+ CLI command: `herd` · Website: [agentherder.com](https://agentherder.com)
6
+
7
+ ```bash
8
+ herd new auth ~/Dev/myapp # new tab, claude starts
9
+ herd new api ~/Dev/myapp
10
+ herd new infra ~/Dev/myapp
11
+
12
+ herd sessions # what's running across all tabs
13
+ herd scrollback auth # read what auth is doing without switching tabs
14
+ herd send api --file task.txt # drop a prompt into any session
15
+ herd fork auth -n auth-v2 # branch a conversation, keep the original
16
+ ```
17
+
18
+ No tmux. No dashboard. Your terminal tabs are the UI.
19
+
20
+ ---
21
+
22
+ ## The idea
23
+
24
+ When you're running multiple Claude Code sessions in parallel, you lose track fast. Which tab is working on what? Did it finish? Is it waiting for input?
25
+
26
+ herd solves this with a simple CLI that treats **terminal tabs as the unit of orchestration** — open them by name, read their output, send them prompts, fork them, close them. Everything stays in sync: the tab title, the Claude session name, and the working directory.
27
+
28
+ The killer feature: **Claude can run herd itself.** Install the skill and your Claude Code session can spawn parallel sibling sessions, monitor their output, and coordinate across them — without you switching tabs.
29
+
30
+ ## Install
31
+
32
+ **As a Claude Code plugin** (installs the CLI + skill in one step):
33
+
34
+ ```bash
35
+ /plugin marketplace add generativereality/plugins
36
+ /plugin install agentherder@generativereality
37
+ ```
38
+
39
+ **Via npm** (CLI only):
40
+
41
+ ```bash
42
+ npm install -g @generativereality/herd
43
+ ```
44
+
45
+ **Skill only** (if you already have the CLI):
46
+
47
+ ```bash
48
+ mkdir -p .claude/skills/herd
49
+ curl -fsSL https://raw.githubusercontent.com/generativereality/agentherder/main/skills/herd/SKILL.md \
50
+ -o .claude/skills/herd/SKILL.md
51
+ ```
52
+
53
+ **Requirements:** [Wave Terminal](https://waveterm.dev) · macOS · Node.js 20+
54
+
55
+ **One-time:** Wave needs Accessibility permission — System Settings → Privacy & Security → Accessibility → Wave ✓
56
+
57
+ ## Usage
58
+
59
+ ```
60
+ herd sessions what's running (active/idle status)
61
+ herd list all workspaces, tabs, and blocks
62
+ herd new <name> [dir] [-w workspace] open tab, start claude
63
+ herd resume <name> [dir] open tab, run claude --continue
64
+ herd fork <tab> [-n new-name] fork a session into a new tab
65
+ herd close <tab> close a tab
66
+ herd rename <tab> <new-name> rename a tab
67
+ herd scrollback <tab> [lines] read terminal output (default: 50 lines)
68
+ herd send <tab> [text] send input — arg, --file, or stdin pipe
69
+ herd config show config path and values
70
+ ```
71
+
72
+ Tab names match by prefix. Block IDs can be shortened to 8 chars.
73
+
74
+ ### Spin up a session fleet
75
+
76
+ ```bash
77
+ herd sessions # check what's already running first
78
+
79
+ herd new auth ~/Dev/myapp
80
+ herd new payments ~/Dev/myapp
81
+ herd new infra ~/Dev/myapp
82
+ ```
83
+
84
+ Each tab gets named, Claude's session name syncs to the tab title via `--name`.
85
+
86
+ ### Send a prompt
87
+
88
+ ```bash
89
+ # From a file (good for long context-heavy prompts)
90
+ herd send auth --file ~/prompts/task.txt
91
+
92
+ # Via stdin
93
+ echo "focus on the edge cases in the OAuth flow" | herd send auth
94
+
95
+ # Quick reply or approval
96
+ herd send auth "yes\n"
97
+ herd send auth "/clear\n"
98
+ ```
99
+
100
+ ### Check in without switching tabs
101
+
102
+ ```bash
103
+ herd scrollback auth # last 50 lines
104
+ herd scrollback auth 200 # last 200 lines
105
+ ```
106
+
107
+ ### Fork a session
108
+
109
+ ```bash
110
+ # Try a different approach without losing the original conversation
111
+ herd fork auth -n auth-v2
112
+ ```
113
+
114
+ Runs `claude --resume <id> --fork-session` — new independent session, full shared context from the original.
115
+
116
+ ### Target a workspace
117
+
118
+ ```bash
119
+ herd new api ~/Dev/myapp -w work
120
+ ```
121
+
122
+ ## Claude Code Skill
123
+
124
+ The real unlock: install the plugin (see [Install](#install)) so **Claude Code can herd itself**.
125
+
126
+ With the skill installed, Claude can:
127
+
128
+ - Check what's running before starting duplicate work (`herd sessions`)
129
+ - Spawn a parallel session for an independent subtask (`herd new payments ~/Dev/myapp`)
130
+ - Monitor siblings without interrupting them (`herd scrollback payments`)
131
+ - Drop a prompt into any session (`herd send payments --file spec.txt`)
132
+ - Fork its own session to explore an alternative approach (`herd fork auth`)
133
+
134
+ Claude becomes the orchestrator of its own fleet.
135
+
136
+ ## Tip: pair with Claude Code Remote Control
137
+
138
+ Claude Code's [Remote Control](https://docs.anthropic.com/en/docs/claude-code/remote-control) lets you access a local session from any device — phone, tablet, browser — via `claude.ai/code`. The session still runs on your machine, with full filesystem and tool access.
139
+
140
+ Paired with Agent Herder, the pattern is:
141
+
142
+ 1. Start a **command session** with Remote Control enabled:
143
+ ```bash
144
+ claude --remote-control "command"
145
+ ```
146
+ 2. From your phone or browser, connect to that session and assign work:
147
+ > *"Spawn three sessions — auth, payments, infra — and start them on these tasks..."*
148
+ 3. The command session uses `herd` to open tabs, send prompts, and check in on workers
149
+ 4. You monitor and steer the whole fleet from your phone while the machine does the work
150
+
151
+ One remote-controlled session orchestrating a local fleet.
152
+
153
+ ## Config
154
+
155
+ ```toml
156
+ # ~/.config/herd/config.toml
157
+
158
+ [claude]
159
+ # Flags passed to every claude invocation
160
+ flags = ["--allow-dangerously-skip-permissions"]
161
+
162
+ [defaults]
163
+ # Default Wave workspace for new sessions
164
+ # workspace = ""
165
+ ```
166
+
167
+ ## Terminal support
168
+
169
+ | Terminal | Status |
170
+ |----------|--------|
171
+ | [Wave Terminal](https://waveterm.dev) | ✅ Full support |
172
+ | iTerm2 | Planned |
173
+ | Ghostty | Planned |
174
+ | Warp | Planned |
175
+
176
+ Wave is supported via its unix socket RPC. Other terminals will follow as adapters — PRs welcome.
177
+
178
+ ## License
179
+
180
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,927 @@
1
+ #!/usr/bin/env node
2
+ import updateNotifier from "update-notifier";
3
+ import { cli, define } from "gunshi";
4
+ import { createConnection } from "net";
5
+ import { execFileSync, spawnSync } from "child_process";
6
+ import { randomUUID } from "crypto";
7
+ import { homedir } from "os";
8
+ import { basename, dirname, extname, join, resolve } from "path";
9
+ import { consola } from "consola";
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
11
+ //#region package.json
12
+ var name = "@generativereality/herd";
13
+ var version = "0.1.0";
14
+ var description = "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.";
15
+ var package_default = {
16
+ name,
17
+ version,
18
+ description,
19
+ type: "module",
20
+ bin: { "herd": "dist/index.js" },
21
+ files: ["dist", ".claude"],
22
+ scripts: {
23
+ "dev": "bun run ./src/index.ts",
24
+ "build": "tsdown",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "eslint src/",
27
+ "check": "bun run typecheck && bun run build",
28
+ "release": "bumpp && npm publish",
29
+ "prepack": "bun run build"
30
+ },
31
+ keywords: [
32
+ "claude-code",
33
+ "ai-agents",
34
+ "session-manager",
35
+ "wave-terminal",
36
+ "herd",
37
+ "agentherder"
38
+ ],
39
+ author: "motin",
40
+ license: "MIT",
41
+ repository: {
42
+ "type": "git",
43
+ "url": "https://github.com/generativereality/agentherder"
44
+ },
45
+ homepage: "https://agentherder.com",
46
+ engines: { "node": ">=20.19.4" },
47
+ publishConfig: {
48
+ "registry": "https://registry.npmjs.org",
49
+ "access": "public"
50
+ },
51
+ dependencies: {
52
+ "@clack/prompts": "^0.9.1",
53
+ "consola": "^3.4.0",
54
+ "gunshi": "^0.23.0",
55
+ "update-notifier": "^7.3.1"
56
+ },
57
+ devDependencies: {
58
+ "@types/node": "^22.0.0",
59
+ "@types/update-notifier": "^6.0.8",
60
+ "bumpp": "^9.11.1",
61
+ "tsdown": "^0.12.0",
62
+ "typescript": "^5.8.0"
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/core/terminal.ts
67
+ function detectTerminal() {
68
+ if (process.env.WAVETERM_JWT) return "wave";
69
+ const prog = process.env.TERM_PROGRAM ?? "";
70
+ const term = process.env.TERM ?? "";
71
+ if (prog === "iTerm.app") return "iterm2";
72
+ if (prog === "ghostty" || process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
73
+ if (prog === "WarpTerminal") return "warp";
74
+ if (prog === "vscode") return "vscode";
75
+ if (prog === "Hyper") return "hyper";
76
+ if (prog === "Apple_Terminal") return "apple-terminal";
77
+ if (term === "xterm-kitty" || process.env.KITTY_WINDOW_ID) return "kitty";
78
+ if (term === "alacritty") return "alacritty";
79
+ return "unknown";
80
+ }
81
+ const TERMINAL_NAMES = {
82
+ wave: "Wave Terminal",
83
+ iterm2: "iTerm2",
84
+ ghostty: "Ghostty",
85
+ warp: "Warp",
86
+ kitty: "Kitty",
87
+ vscode: "VS Code terminal",
88
+ hyper: "Hyper",
89
+ alacritty: "Alacritty",
90
+ "apple-terminal": "Terminal.app",
91
+ unknown: "an unrecognised terminal"
92
+ };
93
+ function printUnsupportedTerminalError(terminal) {
94
+ const name = TERMINAL_NAMES[terminal];
95
+ const lines = [
96
+ "",
97
+ ` Agent Herder currently requires Wave Terminal.`,
98
+ ` You appear to be running in: ${name}`,
99
+ "",
100
+ ` Option 1 — Switch to Wave Terminal (full support today):`,
101
+ ` brew install --cask wave`,
102
+ ` https://waveterm.dev`,
103
+ "",
104
+ ` Option 2 — Add ${name} support (one adapter file, PRs welcome):`,
105
+ ` git clone https://github.com/generativereality/agentherder`,
106
+ ` cd agentherder`,
107
+ ` claude # ask Claude to implement the ${name} adapter`,
108
+ "",
109
+ ` Claude will find src/core/wave.ts, use it as the reference`,
110
+ ` implementation, create src/core/${adapterFileName(terminal)},`,
111
+ ` wire it up, and open a PR — all in one session.`,
112
+ ""
113
+ ];
114
+ console.error(lines.join("\n"));
115
+ }
116
+ function adapterFileName(terminal) {
117
+ if (terminal === "unknown") return "<terminal>.ts";
118
+ return `${terminal}.ts`;
119
+ }
120
+ //#endregion
121
+ //#region src/core/wave.ts
122
+ const SOCK_PATH = join(homedir(), "Library", "Application Support", "waveterm", "wave.sock");
123
+ var WaveSocket = class {
124
+ socket;
125
+ buffer = "";
126
+ pendingReaders = [];
127
+ routeId = "";
128
+ jwt;
129
+ constructor(jwt) {
130
+ this.jwt = jwt;
131
+ this.socket = createConnection(SOCK_PATH);
132
+ this.socket.on("data", (chunk) => {
133
+ this.buffer += chunk.toString();
134
+ let nl;
135
+ while ((nl = this.buffer.indexOf("\n")) !== -1) {
136
+ const line = this.buffer.slice(0, nl).trim();
137
+ this.buffer = this.buffer.slice(nl + 1);
138
+ if (!line) continue;
139
+ try {
140
+ const msg = JSON.parse(line);
141
+ this.pendingReaders.shift()?.(msg);
142
+ } catch {}
143
+ }
144
+ });
145
+ }
146
+ waitForMessage(timeoutMs = 8e3) {
147
+ return new Promise((resolve, reject) => {
148
+ const timer = setTimeout(() => {
149
+ const idx = this.pendingReaders.indexOf(resolve);
150
+ if (idx !== -1) this.pendingReaders.splice(idx, 1);
151
+ reject(/* @__PURE__ */ new Error("Wave socket timeout"));
152
+ }, timeoutMs);
153
+ this.pendingReaders.push((msg) => {
154
+ clearTimeout(timer);
155
+ resolve(msg);
156
+ });
157
+ });
158
+ }
159
+ send(msg) {
160
+ this.socket.write(JSON.stringify(msg) + "\n");
161
+ }
162
+ async connect() {
163
+ await new Promise((resolve, reject) => {
164
+ this.socket.once("connect", resolve);
165
+ this.socket.once("error", reject);
166
+ });
167
+ this.send({
168
+ command: "authenticate",
169
+ reqid: randomUUID(),
170
+ route: "$control",
171
+ data: this.jwt
172
+ });
173
+ this.routeId = (await this.waitForMessage()).data.routeid;
174
+ }
175
+ async command(command, data, route = "wavesrv") {
176
+ this.send({
177
+ command,
178
+ reqid: randomUUID(),
179
+ route,
180
+ source: this.routeId,
181
+ data
182
+ });
183
+ try {
184
+ return await this.waitForMessage();
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ destroy() {
190
+ this.socket.destroy();
191
+ }
192
+ };
193
+ var WaveAdapter = class {
194
+ socket = null;
195
+ jwt;
196
+ constructor() {
197
+ this.jwt = process.env.WAVETERM_JWT ?? "";
198
+ }
199
+ blocksList() {
200
+ try {
201
+ const out = execFileSync("wsh", [
202
+ "blocks",
203
+ "list",
204
+ "--json"
205
+ ], { encoding: "utf-8" });
206
+ return JSON.parse(out);
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+ scrollback(blockId, lastN = 50) {
212
+ return spawnSync("wsh", [
213
+ "termscrollback",
214
+ "-b",
215
+ blockId,
216
+ "--start",
217
+ `-${lastN}`
218
+ ], { encoding: "utf-8" }).stdout ?? "";
219
+ }
220
+ deleteBlock(blockId) {
221
+ spawnSync("wsh", [
222
+ "deleteblock",
223
+ "-b",
224
+ blockId
225
+ ], { encoding: "utf-8" });
226
+ }
227
+ async newTab(focusWindowId) {
228
+ if (focusWindowId) {
229
+ await this.focusWindow(focusWindowId);
230
+ await sleep(300);
231
+ }
232
+ const r = spawnSync("osascript", ["-e", [
233
+ "tell application \"Wave\" to activate",
234
+ "delay 0.25",
235
+ "tell application \"System Events\" to keystroke \"t\" using command down"
236
+ ].join("\n")], { encoding: "utf-8" });
237
+ if (r.status !== 0) {
238
+ const msg = r.stderr?.trim();
239
+ throw new Error(msg ? `osascript failed: ${msg}` : "Failed to open new tab — ensure Wave Terminal has Accessibility permission:\n System Settings → Privacy & Security → Accessibility → Wave ✓");
240
+ }
241
+ return true;
242
+ }
243
+ async waitForNewBlock(beforeIds, timeoutMs = 5e3) {
244
+ const deadline = Date.now() + timeoutMs;
245
+ while (Date.now() < deadline) {
246
+ await sleep(250);
247
+ for (const b of this.blocksList()) if (b.view === "term" && !beforeIds.has(b.blockid)) return {
248
+ blockId: b.blockid,
249
+ tabId: b.tabid
250
+ };
251
+ }
252
+ return null;
253
+ }
254
+ async sock() {
255
+ if (!this.socket) {
256
+ const s = new WaveSocket(this.jwt);
257
+ await s.connect();
258
+ this.socket = s;
259
+ }
260
+ return this.socket;
261
+ }
262
+ closeSocket() {
263
+ this.socket?.destroy();
264
+ this.socket = null;
265
+ }
266
+ async getTab(tabId) {
267
+ return (await (await this.sock()).command("gettab", tabId))?.data ?? {};
268
+ }
269
+ async workspaceList() {
270
+ return (await (await this.sock()).command("workspacelist", null))?.data ?? [];
271
+ }
272
+ async focusWindow(windowId) {
273
+ await (await this.sock()).command("focuswindow", windowId, "electron");
274
+ }
275
+ async renameTab(tabId, name) {
276
+ await (await this.sock()).command("updatetabname", { args: [tabId, name] });
277
+ }
278
+ async sendInput(blockId, text) {
279
+ const s = await this.sock();
280
+ const inputdata64 = Buffer.from(text).toString("base64");
281
+ return s.command("controllerinput", {
282
+ blockid: blockId,
283
+ inputdata64
284
+ });
285
+ }
286
+ async getAllData() {
287
+ const blocks = this.blocksList();
288
+ const tabsById = /* @__PURE__ */ new Map();
289
+ for (const b of blocks) {
290
+ const arr = tabsById.get(b.tabid) ?? [];
291
+ arr.push(b);
292
+ tabsById.set(b.tabid, arr);
293
+ }
294
+ const tabNames = /* @__PURE__ */ new Map();
295
+ let workspaces = [];
296
+ try {
297
+ for (const tabId of tabsById.keys()) {
298
+ const td = await this.getTab(tabId);
299
+ tabNames.set(tabId, td.name ?? tabId.slice(0, 8));
300
+ }
301
+ workspaces = await this.workspaceList();
302
+ } catch {} finally {
303
+ this.closeSocket();
304
+ }
305
+ if (!workspaces.length) {
306
+ const wsId = process.env.WAVETERM_WORKSPACEID ?? "";
307
+ workspaces = [{
308
+ workspacedata: {
309
+ oid: wsId,
310
+ name: wsId.slice(0, 8) || "default",
311
+ tabids: [...tabsById.keys()]
312
+ },
313
+ windowid: ""
314
+ }];
315
+ }
316
+ return {
317
+ blocks,
318
+ tabsById,
319
+ workspaces,
320
+ tabNames
321
+ };
322
+ }
323
+ resolveTab(query, tabsById, tabNames) {
324
+ const q = query.toLowerCase();
325
+ return [...tabsById.keys()].filter((tid) => {
326
+ const name = tabNames.get(tid) ?? "";
327
+ return name.toLowerCase() === q || tid.startsWith(query) || name.toLowerCase().startsWith(q);
328
+ });
329
+ }
330
+ resolveBlock(query, blocks) {
331
+ return blocks.filter((b) => b.blockid.startsWith(query));
332
+ }
333
+ resolveWorkspace(workspaces, query) {
334
+ const q = query.toLowerCase();
335
+ return workspaces.filter(({ workspacedata: wd }) => {
336
+ const name = wd.name ?? "";
337
+ return name.toLowerCase() === q || wd.oid.startsWith(query) || name.toLowerCase().startsWith(q);
338
+ }).map((w) => ({
339
+ data: w.workspacedata,
340
+ windowId: w.windowid
341
+ }));
342
+ }
343
+ };
344
+ function requireWaveAdapter() {
345
+ if (!process.env.WAVETERM_JWT) {
346
+ printUnsupportedTerminalError(detectTerminal());
347
+ process.exit(1);
348
+ }
349
+ return new WaveAdapter();
350
+ }
351
+ function sleep(ms) {
352
+ return new Promise((r) => setTimeout(r, ms));
353
+ }
354
+ //#endregion
355
+ //#region src/commands/sessions.ts
356
+ const sessionsCommand = define({
357
+ name: "sessions",
358
+ description: "List tabs with active/idle session status",
359
+ args: {},
360
+ async run() {
361
+ const adapter = requireWaveAdapter();
362
+ const { tabsById, workspaces, tabNames } = await adapter.getAllData();
363
+ const currentTab = process.env.WAVETERM_TABID ?? "";
364
+ const currentWs = process.env.WAVETERM_WORKSPACEID ?? "";
365
+ console.log("Sessions");
366
+ console.log("=".repeat(50));
367
+ for (const wsp of workspaces) {
368
+ const { oid, name, tabids } = wsp.workspacedata;
369
+ const wsMarker = oid === currentWs ? " (current)" : "";
370
+ const tabIds = tabids.filter((t) => tabsById.has(t));
371
+ if (!tabIds.length) continue;
372
+ console.log(`\nWorkspace: ${name}${wsMarker}`);
373
+ for (const tabId of tabIds) {
374
+ const termBlocks = (tabsById.get(tabId) ?? []).filter((b) => b.view === "term");
375
+ if (!termBlocks.length) continue;
376
+ const name = tabNames.get(tabId) ?? tabId.slice(0, 8);
377
+ const cur = tabId === currentTab ? " ◄" : "";
378
+ const b = termBlocks[0];
379
+ const cwd = (b.meta?.["cmd:cwd"] ?? "").replace(process.env.HOME ?? "", "~");
380
+ const tail = adapter.scrollback(b.blockid, 5);
381
+ const lastLine = tail.split("\n").map((l) => l.trim()).filter(Boolean).at(-1) ?? "";
382
+ let status = "terminal";
383
+ if ([
384
+ "Claude Code",
385
+ "claude.ai/code",
386
+ "✻ Thinking",
387
+ "✽ Hatching",
388
+ "⏵⏵ bypass"
389
+ ].some((s) => tail.includes(s))) status = "active";
390
+ else if (lastLine.toLowerCase().includes("claude")) status = "idle";
391
+ const statusLabel = status === "active" ? "● active" : status === "idle" ? "○ idle" : " terminal";
392
+ console.log(` [${tabId.slice(0, 8)}] "${name}"${cur} ${cwd}`);
393
+ console.log(` ${statusLabel}`);
394
+ if (status === "terminal" && lastLine) console.log(` last: ${lastLine.slice(0, 80)}`);
395
+ }
396
+ }
397
+ }
398
+ });
399
+ //#endregion
400
+ //#region src/commands/list.ts
401
+ const listCommand = define({
402
+ name: "list",
403
+ description: "List all workspaces, tabs, and blocks",
404
+ args: {},
405
+ async run() {
406
+ const { tabsById, workspaces, tabNames } = await requireWaveAdapter().getAllData();
407
+ const currentBlock = process.env.WAVETERM_BLOCKID ?? "";
408
+ const currentTab = process.env.WAVETERM_TABID ?? "";
409
+ const currentWs = process.env.WAVETERM_WORKSPACEID ?? "";
410
+ for (const wsp of workspaces) {
411
+ const { oid, name, tabids } = wsp.workspacedata;
412
+ const noWindow = !wsp.windowid ? " (no window)" : "";
413
+ const wsMarker = oid === currentWs ? " ◄ current" : noWindow;
414
+ console.log(`Workspace: ${name} [${oid.slice(0, 8)}]${wsMarker}`);
415
+ console.log();
416
+ const tabIds = tabids.filter((t) => tabsById.has(t));
417
+ if (!tabIds.length) {
418
+ console.log(" (no open tabs)");
419
+ console.log();
420
+ continue;
421
+ }
422
+ for (const tabId of tabIds) {
423
+ const tabName = tabNames.get(tabId) ?? tabId.slice(0, 8);
424
+ const cur = tabId === currentTab ? " ◄" : "";
425
+ console.log(` Tab "${tabName}" [${tabId.slice(0, 8)}]${cur}`);
426
+ for (const b of tabsById.get(tabId) ?? []) {
427
+ const here = b.blockid === currentBlock ? " ◄ here" : "";
428
+ const cwd = b.meta?.["cmd:cwd"] ?? "";
429
+ console.log(` ${b.view.padEnd(8)} ${b.blockid.slice(0, 8)}${cwd ? ` ${cwd}` : ""}${here}`);
430
+ }
431
+ console.log();
432
+ }
433
+ }
434
+ }
435
+ });
436
+ //#endregion
437
+ //#region src/core/config.ts
438
+ const CONFIG_PATH = join(homedir(), ".config", "herd", "config.toml");
439
+ const DEFAULT_CONFIG = {
440
+ claude: { flags: ["--allow-dangerously-skip-permissions"] },
441
+ defaults: { workspace: "" }
442
+ };
443
+ const DEFAULT_CONFIG_FILE = `# herd configuration
444
+ # https://agentherder.com
445
+
446
+ [claude]
447
+ # Extra flags passed to every \`claude\` invocation.
448
+ flags = ["--allow-dangerously-skip-permissions"]
449
+
450
+ [defaults]
451
+ # Default Wave workspace to open new sessions in.
452
+ # workspace = ""
453
+ `;
454
+ function parseToml(text) {
455
+ const result = {};
456
+ let section = null;
457
+ for (const raw of text.split("\n")) {
458
+ const line = raw.trim();
459
+ if (!line || line.startsWith("#")) continue;
460
+ if (line.startsWith("[") && line.endsWith("]")) {
461
+ section = line.slice(1, -1).trim();
462
+ result[section] ??= {};
463
+ continue;
464
+ }
465
+ if (section && line.includes("=")) {
466
+ const [rawKey, ...rest] = line.split("=");
467
+ const key = rawKey.trim();
468
+ const val = rest.join("=").trim();
469
+ if (val.startsWith("[")) {
470
+ const items = [...val.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
471
+ result[section][key] = items;
472
+ } else if (val.startsWith("\"") && val.endsWith("\"")) result[section][key] = val.slice(1, -1);
473
+ else if (val === "true" || val === "false") result[section][key] = val === "true";
474
+ }
475
+ }
476
+ return result;
477
+ }
478
+ function loadConfig() {
479
+ const config = {
480
+ claude: { ...DEFAULT_CONFIG.claude },
481
+ defaults: { ...DEFAULT_CONFIG.defaults }
482
+ };
483
+ if (!existsSync(CONFIG_PATH)) return config;
484
+ try {
485
+ const parsed = parseToml(readFileSync(CONFIG_PATH, "utf-8"));
486
+ if (parsed.claude) Object.assign(config.claude, parsed.claude);
487
+ if (parsed.defaults) Object.assign(config.defaults, parsed.defaults);
488
+ } catch {}
489
+ return config;
490
+ }
491
+ function ensureConfigExists() {
492
+ if (!existsSync(CONFIG_PATH)) {
493
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
494
+ writeFileSync(CONFIG_PATH, DEFAULT_CONFIG_FILE);
495
+ }
496
+ return CONFIG_PATH;
497
+ }
498
+ //#endregion
499
+ //#region src/core/open-session.ts
500
+ async function openSession(opts) {
501
+ const { tabName, claudeCmd, workspaceQuery } = opts;
502
+ const dir = resolve(opts.dir.replace(/^~/, homedir()));
503
+ const config = loadConfig();
504
+ const adapter = requireWaveAdapter();
505
+ let focusWindowId;
506
+ if (workspaceQuery) {
507
+ const { workspaces } = await adapter.getAllData();
508
+ const matches = adapter.resolveWorkspace(workspaces, workspaceQuery);
509
+ if (!matches.length) {
510
+ consola.error(`No workspace matching '${workspaceQuery}'`);
511
+ process.exit(1);
512
+ }
513
+ const { data, windowId } = matches[0];
514
+ if (!windowId) {
515
+ consola.error(`Workspace '${data.name}' has no open window`);
516
+ process.exit(1);
517
+ }
518
+ focusWindowId = windowId;
519
+ consola.info(`Workspace: ${data.name}`);
520
+ }
521
+ const beforeIds = new Set(adapter.blocksList().filter((b) => b.view === "term").map((b) => b.blockid));
522
+ await adapter.newTab(focusWindowId);
523
+ const result = await adapter.waitForNewBlock(beforeIds);
524
+ if (!result) {
525
+ consola.error("Timed out waiting for new terminal block");
526
+ process.exit(1);
527
+ }
528
+ const { blockId, tabId } = result;
529
+ await adapter.renameTab(tabId, tabName);
530
+ const extraFlags = config.claude.flags.join(" ");
531
+ const cmd = `cd ${JSON.stringify(dir)} && ${claudeCmd} --name ${JSON.stringify(tabName)}${extraFlags ? " " + extraFlags : ""}\n`;
532
+ await adapter.sendInput(blockId, cmd);
533
+ adapter.closeSocket();
534
+ return tabId;
535
+ }
536
+ //#endregion
537
+ //#region src/commands/new.ts
538
+ const newCommand = define({
539
+ name: "new",
540
+ description: "Open a new tab and launch claude",
541
+ args: {
542
+ name: {
543
+ type: "positional",
544
+ description: "Tab name"
545
+ },
546
+ dir: {
547
+ type: "positional",
548
+ description: "Working directory (default: cwd)"
549
+ },
550
+ workspace: {
551
+ type: "string",
552
+ short: "w",
553
+ description: "Target workspace"
554
+ }
555
+ },
556
+ async run(ctx) {
557
+ const name = ctx.positionals[1];
558
+ const dir = ctx.positionals[2] ?? process.cwd();
559
+ const workspace = ctx.values.workspace;
560
+ if (!name) {
561
+ consola.error("Tab name is required");
562
+ process.exit(1);
563
+ }
564
+ const tabId = await openSession({
565
+ tabName: name,
566
+ dir,
567
+ claudeCmd: "claude",
568
+ workspaceQuery: workspace
569
+ });
570
+ consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude at ${dir}`);
571
+ }
572
+ });
573
+ //#endregion
574
+ //#region src/commands/resume.ts
575
+ const resumeCommand = define({
576
+ name: "resume",
577
+ description: "Open a new tab and run: claude --continue",
578
+ args: {
579
+ name: {
580
+ type: "positional",
581
+ description: "Tab name"
582
+ },
583
+ dir: {
584
+ type: "positional",
585
+ description: "Working directory (default: cwd)"
586
+ }
587
+ },
588
+ async run(ctx) {
589
+ const name = ctx.positionals[1];
590
+ const dir = ctx.positionals[2] ?? process.cwd();
591
+ if (!name) {
592
+ consola.error("Tab name is required");
593
+ process.exit(1);
594
+ }
595
+ const tabId = await openSession({
596
+ tabName: name,
597
+ dir,
598
+ claudeCmd: "claude --continue"
599
+ });
600
+ consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude --continue at ${dir}`);
601
+ }
602
+ });
603
+ //#endregion
604
+ //#region src/core/session.ts
605
+ /** Convert an absolute path to Claude's project slug (/ → -) */
606
+ function pathToProjectSlug(dir) {
607
+ return resolve(dir).replace(/\//g, "-");
608
+ }
609
+ /** Find the most recent Claude Code session ID for a directory */
610
+ function findLatestSessionId(dir) {
611
+ const slug = pathToProjectSlug(dir);
612
+ const projectDir = join(homedir(), ".claude", "projects", slug);
613
+ if (!existsSync(projectDir)) return null;
614
+ const jsonlFiles = readdirSync(projectDir).filter((f) => extname(f) === ".jsonl").map((f) => ({
615
+ name: f,
616
+ mtime: statSync(join(projectDir, f)).mtimeMs
617
+ })).sort((a, b) => b.mtime - a.mtime);
618
+ if (!jsonlFiles.length) return null;
619
+ return basename(jsonlFiles[0].name, ".jsonl");
620
+ }
621
+ //#endregion
622
+ //#region src/commands/fork.ts
623
+ const forkCommand = define({
624
+ name: "fork",
625
+ description: "Fork a session into a new tab (claude --resume <id> --fork-session)",
626
+ args: {
627
+ tab: {
628
+ type: "positional",
629
+ description: "Source tab name or ID prefix"
630
+ },
631
+ name: {
632
+ type: "string",
633
+ short: "n",
634
+ description: "Name for the new tab"
635
+ }
636
+ },
637
+ async run(ctx) {
638
+ const sourceQuery = ctx.positionals[1];
639
+ const customName = ctx.values.name;
640
+ if (!sourceQuery) {
641
+ consola.error("Source tab name is required");
642
+ process.exit(1);
643
+ }
644
+ const adapter = requireWaveAdapter();
645
+ const { tabsById, tabNames } = await adapter.getAllData();
646
+ const matches = adapter.resolveTab(sourceQuery, tabsById, tabNames);
647
+ if (!matches.length) {
648
+ consola.error(`No tab matching '${sourceQuery}'`);
649
+ process.exit(1);
650
+ }
651
+ if (matches.length > 1) {
652
+ consola.error(`Multiple tabs match '${sourceQuery}':`);
653
+ for (const tid of matches) consola.log(` "${tabNames.get(tid)}" [${tid.slice(0, 8)}]`);
654
+ process.exit(1);
655
+ }
656
+ const tabId = matches[0];
657
+ const tabName = tabNames.get(tabId) ?? tabId.slice(0, 8);
658
+ const newName = customName ?? `${tabName}-fork`;
659
+ const termBlocks = (tabsById.get(tabId) ?? []).filter((b) => b.view === "term");
660
+ if (!termBlocks.length) {
661
+ consola.error(`Tab "${tabName}" has no terminal block`);
662
+ process.exit(1);
663
+ }
664
+ const sourceDir = termBlocks[0].meta?.["cmd:cwd"] ?? process.cwd();
665
+ const sessionId = findLatestSessionId(sourceDir);
666
+ if (!sessionId) {
667
+ consola.error(`No Claude session found for ${sourceDir}`);
668
+ consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sourceDir)}/`);
669
+ process.exit(1);
670
+ }
671
+ const newTabId = await openSession({
672
+ tabName: newName,
673
+ dir: sourceDir,
674
+ claudeCmd: `claude --resume ${sessionId} --fork-session`
675
+ });
676
+ consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
677
+ consola.info(`session: ${sessionId}`);
678
+ }
679
+ });
680
+ //#endregion
681
+ //#region src/commands/close.ts
682
+ const closeCommand = define({
683
+ name: "close",
684
+ description: "Close a tab by name or ID prefix",
685
+ args: { tab: {
686
+ type: "positional",
687
+ description: "Tab name or ID prefix"
688
+ } },
689
+ async run(ctx) {
690
+ const query = ctx.positionals[1];
691
+ if (!query) {
692
+ consola.error("Tab name or ID is required");
693
+ process.exit(1);
694
+ }
695
+ const adapter = requireWaveAdapter();
696
+ const { tabsById, tabNames } = await adapter.getAllData();
697
+ const matches = adapter.resolveTab(query, tabsById, tabNames);
698
+ if (!matches.length) {
699
+ consola.error(`No tab matching '${query}'`);
700
+ process.exit(1);
701
+ }
702
+ if (matches.length > 1) {
703
+ consola.error(`Multiple tabs match '${query}':`);
704
+ for (const tid of matches) consola.log(` "${tabNames.get(tid)}" [${tid.slice(0, 8)}]`);
705
+ process.exit(1);
706
+ }
707
+ const tabId = matches[0];
708
+ const name = tabNames.get(tabId) ?? tabId.slice(0, 8);
709
+ for (const b of tabsById.get(tabId) ?? []) adapter.deleteBlock(b.blockid);
710
+ adapter.closeSocket();
711
+ consola.success(`Closed "${name}" [${tabId.slice(0, 8)}]`);
712
+ }
713
+ });
714
+ //#endregion
715
+ //#region src/commands/rename.ts
716
+ const renameCommand = define({
717
+ name: "rename",
718
+ description: "Rename a tab",
719
+ args: {
720
+ tab: {
721
+ type: "positional",
722
+ description: "Tab name or ID prefix"
723
+ },
724
+ newName: {
725
+ type: "positional",
726
+ description: "New name"
727
+ }
728
+ },
729
+ async run(ctx) {
730
+ const query = ctx.positionals[1];
731
+ const newName = ctx.positionals[2];
732
+ if (!query || !newName) {
733
+ consola.error("Usage: herd rename <tab> <new-name>");
734
+ process.exit(1);
735
+ }
736
+ const adapter = requireWaveAdapter();
737
+ const { tabsById, tabNames } = await adapter.getAllData();
738
+ const matches = adapter.resolveTab(query, tabsById, tabNames);
739
+ if (!matches.length) {
740
+ consola.error(`No tab matching '${query}'`);
741
+ process.exit(1);
742
+ }
743
+ if (matches.length > 1) {
744
+ consola.error(`Multiple tabs match '${query}':`);
745
+ for (const tid of matches) consola.log(` "${tabNames.get(tid)}" [${tid.slice(0, 8)}]`);
746
+ process.exit(1);
747
+ }
748
+ const oldName = tabNames.get(matches[0]) ?? matches[0].slice(0, 8);
749
+ await adapter.renameTab(matches[0], newName);
750
+ adapter.closeSocket();
751
+ consola.success(`Renamed "${oldName}" → "${newName}"`);
752
+ }
753
+ });
754
+ //#endregion
755
+ //#region src/commands/scrollback.ts
756
+ const scrollbackCommand = define({
757
+ name: "scrollback",
758
+ description: "Show terminal output for a block (default: last 50 lines)",
759
+ args: {
760
+ block: {
761
+ type: "positional",
762
+ description: "Block ID prefix"
763
+ },
764
+ lines: {
765
+ type: "number",
766
+ description: "Number of lines to show",
767
+ default: 50
768
+ }
769
+ },
770
+ async run(ctx) {
771
+ const query = ctx.positionals[1];
772
+ const lines = ctx.values.lines ?? 50;
773
+ if (!query) {
774
+ consola.error("Block ID is required");
775
+ process.exit(1);
776
+ }
777
+ const adapter = requireWaveAdapter();
778
+ const blocks = adapter.blocksList();
779
+ const matches = adapter.resolveBlock(query, blocks);
780
+ if (!matches.length) {
781
+ consola.error(`No block matching '${query}'`);
782
+ process.exit(1);
783
+ }
784
+ if (matches.length > 1) {
785
+ consola.error(`Multiple blocks match '${query}':`);
786
+ for (const b of matches) consola.log(` ${b.blockid}`);
787
+ process.exit(1);
788
+ }
789
+ process.stdout.write(adapter.scrollback(matches[0].blockid, lines));
790
+ }
791
+ });
792
+ //#endregion
793
+ //#region src/commands/send.ts
794
+ function readStdin() {
795
+ return new Promise((resolve) => {
796
+ const chunks = [];
797
+ process.stdin.on("data", (c) => chunks.push(c));
798
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString()));
799
+ });
800
+ }
801
+ const sendCommand = define({
802
+ name: "send",
803
+ description: "Send input to a tab or block (text arg, --file, or stdin pipe)",
804
+ args: {
805
+ target: {
806
+ type: "positional",
807
+ description: "Tab name, tab ID prefix, or block ID prefix"
808
+ },
809
+ file: {
810
+ type: "string",
811
+ short: "f",
812
+ description: "Read text from file"
813
+ },
814
+ enter: {
815
+ type: "boolean",
816
+ short: "e",
817
+ description: "Append newline after text (default: true)"
818
+ }
819
+ },
820
+ async run(ctx) {
821
+ const query = ctx.positionals[1];
822
+ const inlineText = ctx.positionals[2];
823
+ const filePath = ctx.values.file;
824
+ const appendEnter = ctx.values.enter ?? true;
825
+ if (!query) {
826
+ consola.error("Usage: herd send <tab-or-block> [text]");
827
+ process.exit(1);
828
+ }
829
+ let rawText;
830
+ if (inlineText !== void 0) rawText = inlineText.replace(/\\n/g, "\n").replace(/\\t/g, " ");
831
+ else if (filePath) rawText = readFileSync(filePath, "utf-8");
832
+ else rawText = await readStdin();
833
+ if (appendEnter && !rawText.endsWith("\n")) rawText += "\n";
834
+ const adapter = requireWaveAdapter();
835
+ const { tabsById, tabNames } = await adapter.getAllData();
836
+ const tabMatches = adapter.resolveTab(query, tabsById, tabNames);
837
+ let blockId;
838
+ if (tabMatches.length === 1) {
839
+ const blocks = (tabsById.get(tabMatches[0]) ?? []).filter((b) => b.view === "term");
840
+ if (!blocks.length) {
841
+ consola.error(`Tab "${tabNames.get(tabMatches[0])}" has no terminal block`);
842
+ process.exit(1);
843
+ }
844
+ blockId = blocks[0].blockid;
845
+ } else if (tabMatches.length > 1) {
846
+ consola.error(`Multiple tabs match '${query}':`);
847
+ for (const tid of tabMatches) consola.log(` "${tabNames.get(tid)}" [${tid.slice(0, 8)}]`);
848
+ process.exit(1);
849
+ } else {
850
+ const allBlocks = adapter.blocksList();
851
+ const blockMatches = adapter.resolveBlock(query, allBlocks);
852
+ if (!blockMatches.length) {
853
+ consola.error(`No tab or block matching '${query}'`);
854
+ process.exit(1);
855
+ }
856
+ if (blockMatches.length > 1) {
857
+ consola.error(`Multiple blocks match '${query}':`);
858
+ for (const b of blockMatches) consola.log(` ${b.blockid}`);
859
+ process.exit(1);
860
+ }
861
+ blockId = blockMatches[0].blockid;
862
+ }
863
+ const resp = await adapter.sendInput(blockId, rawText);
864
+ adapter.closeSocket();
865
+ if (resp && resp.error) {
866
+ consola.error(String(resp.error));
867
+ process.exit(1);
868
+ }
869
+ const preview = rawText.slice(0, 80).replace(/\n/g, "↵").replace(/\t/g, "→");
870
+ consola.success(`Sent to ${blockId.slice(0, 8)}: ${JSON.stringify(preview)}${rawText.length > 80 ? "…" : ""}`);
871
+ }
872
+ });
873
+ //#endregion
874
+ //#region src/commands/config-cmd.ts
875
+ const configCommand = define({
876
+ name: "config",
877
+ description: "Show config file path and current values",
878
+ args: {},
879
+ async run() {
880
+ ensureConfigExists();
881
+ const config = loadConfig();
882
+ consola.info(`Config: ${CONFIG_PATH}`);
883
+ console.log();
884
+ console.log(`claude.flags = ${config.claude.flags.length ? JSON.stringify(config.claude.flags) : "(none)"}`);
885
+ console.log(`defaults.workspace = ${config.defaults.workspace || "(none)"}`);
886
+ }
887
+ });
888
+ //#endregion
889
+ //#region src/commands/index.ts
890
+ const defaultCommand = define({
891
+ name: "herd",
892
+ description,
893
+ args: {},
894
+ async run() {
895
+ await sessionsCommand.run?.call(this, { args: {} });
896
+ }
897
+ });
898
+ const subCommands = new Map([
899
+ ["sessions", sessionsCommand],
900
+ ["list", listCommand],
901
+ ["ls", listCommand],
902
+ ["new", newCommand],
903
+ ["resume", resumeCommand],
904
+ ["fork", forkCommand],
905
+ ["close", closeCommand],
906
+ ["rename", renameCommand],
907
+ ["scrollback", scrollbackCommand],
908
+ ["send", sendCommand],
909
+ ["config", configCommand]
910
+ ]);
911
+ async function run() {
912
+ await cli(process.argv.slice(2), defaultCommand, {
913
+ name,
914
+ version,
915
+ description,
916
+ subCommands
917
+ });
918
+ }
919
+ //#endregion
920
+ //#region src/index.ts
921
+ updateNotifier({ pkg: package_default }).notify();
922
+ run().catch((err) => {
923
+ console.error(err instanceof Error ? err.message : String(err));
924
+ process.exit(1);
925
+ });
926
+ //#endregion
927
+ export {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@generativereality/herd",
3
+ "version": "0.1.0",
4
+ "description": "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.",
5
+ "type": "module",
6
+ "bin": {
7
+ "herd": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ ".claude"
12
+ ],
13
+ "scripts": {
14
+ "dev": "bun run ./src/index.ts",
15
+ "build": "tsdown",
16
+ "typecheck": "tsc --noEmit",
17
+ "lint": "eslint src/",
18
+ "check": "bun run typecheck && bun run build",
19
+ "release": "bumpp && npm publish",
20
+ "prepack": "bun run build"
21
+ },
22
+ "keywords": [
23
+ "claude-code",
24
+ "ai-agents",
25
+ "session-manager",
26
+ "wave-terminal",
27
+ "herd",
28
+ "agentherder"
29
+ ],
30
+ "author": "motin",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/generativereality/agentherder"
35
+ },
36
+ "homepage": "https://agentherder.com",
37
+ "engines": {
38
+ "node": ">=20.19.4"
39
+ },
40
+ "publishConfig": {
41
+ "registry": "https://registry.npmjs.org",
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@clack/prompts": "^0.9.1",
46
+ "consola": "^3.4.0",
47
+ "gunshi": "^0.23.0",
48
+ "update-notifier": "^7.3.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "@types/update-notifier": "^6.0.8",
53
+ "bumpp": "^9.11.1",
54
+ "tsdown": "^0.12.0",
55
+ "typescript": "^5.8.0"
56
+ }
57
+ }