@generativereality/agentherder 0.1.6 → 0.1.7
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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +41 -4
- package/package.json +1 -1
- package/skills/herd/SKILL.md +12 -15
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentherder",
|
|
3
3
|
"description": "Run a fleet of Claude Code sessions. Terminal tabs as the UI, no tmux. Claude can orchestrate its own sibling sessions.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "generativereality",
|
|
7
7
|
"url": "https://agentherder.com"
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,13 @@ import { cli, define } from "gunshi";
|
|
|
4
4
|
import { createConnection } from "net";
|
|
5
5
|
import { execFileSync, spawnSync } from "child_process";
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
7
|
-
import { homedir } from "os";
|
|
7
|
+
import { homedir, tmpdir } from "os";
|
|
8
8
|
import { basename, dirname, extname, join, resolve } from "path";
|
|
9
9
|
import { consola } from "consola";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
11
11
|
//#region package.json
|
|
12
12
|
var name = "@generativereality/agentherder";
|
|
13
|
-
var version = "0.1.
|
|
13
|
+
var version = "0.1.7";
|
|
14
14
|
var description = "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.";
|
|
15
15
|
var package_default = {
|
|
16
16
|
name,
|
|
@@ -502,8 +502,21 @@ function ensureConfigExists() {
|
|
|
502
502
|
}
|
|
503
503
|
//#endregion
|
|
504
504
|
//#region src/core/open-session.ts
|
|
505
|
+
/** Poll scrollback until Claude's ❯ prompt is visible, then return */
|
|
506
|
+
async function waitForClaudePrompt(adapter, blockId, timeoutMs = 3e4) {
|
|
507
|
+
const POLL_INTERVAL = 1e3;
|
|
508
|
+
const deadline = Date.now() + timeoutMs;
|
|
509
|
+
while (Date.now() < deadline) {
|
|
510
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
511
|
+
try {
|
|
512
|
+
const lines = adapter.scrollback(blockId, 10);
|
|
513
|
+
if (lines && lines.includes("❯")) return;
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
consola.warn("Timed out waiting for Claude prompt — sending task anyway");
|
|
517
|
+
}
|
|
505
518
|
async function openSession(opts) {
|
|
506
|
-
const { tabName, claudeCmd, workspaceQuery } = opts;
|
|
519
|
+
const { tabName, claudeCmd, workspaceQuery, initialPromptFile } = opts;
|
|
507
520
|
const dir = resolve(opts.dir.replace(/^~/, homedir()));
|
|
508
521
|
if (!existsSync(dir)) {
|
|
509
522
|
consola.error(`Directory does not exist: ${dir}`);
|
|
@@ -539,6 +552,12 @@ async function openSession(opts) {
|
|
|
539
552
|
const extraFlags = config.claude.flags.join(" ");
|
|
540
553
|
const cmd = `cd ${JSON.stringify(dir)} && ${claudeCmd} --name ${JSON.stringify(tabName)}${extraFlags ? " " + extraFlags : ""}\n`;
|
|
541
554
|
await adapter.sendInput(blockId, cmd);
|
|
555
|
+
if (initialPromptFile) {
|
|
556
|
+
await waitForClaudePrompt(adapter, blockId);
|
|
557
|
+
const prompt = readFileSync(initialPromptFile, "utf-8");
|
|
558
|
+
const text = prompt.endsWith("\n") ? prompt : prompt + "\n";
|
|
559
|
+
await adapter.sendInput(blockId, text);
|
|
560
|
+
}
|
|
542
561
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
543
562
|
adapter.closeSocket();
|
|
544
563
|
return tabId;
|
|
@@ -566,6 +585,16 @@ const newCommand = define({
|
|
|
566
585
|
type: "boolean",
|
|
567
586
|
short: "W",
|
|
568
587
|
description: "Launch claude with --worktree <name> for isolated branch work"
|
|
588
|
+
},
|
|
589
|
+
file: {
|
|
590
|
+
type: "string",
|
|
591
|
+
short: "f",
|
|
592
|
+
description: "Send initial prompt from file once Claude is ready"
|
|
593
|
+
},
|
|
594
|
+
prompt: {
|
|
595
|
+
type: "string",
|
|
596
|
+
short: "p",
|
|
597
|
+
description: "Send initial prompt text once Claude is ready"
|
|
569
598
|
}
|
|
570
599
|
},
|
|
571
600
|
async run(ctx) {
|
|
@@ -573,15 +602,23 @@ const newCommand = define({
|
|
|
573
602
|
const dir = ctx.positionals[2] ?? process.cwd();
|
|
574
603
|
const workspace = ctx.values.workspace;
|
|
575
604
|
const useWorktree = ctx.values.worktree ?? false;
|
|
605
|
+
const promptFile = ctx.values.file;
|
|
606
|
+
const promptText = ctx.values.prompt;
|
|
576
607
|
if (!name) {
|
|
577
608
|
consola.error("Tab name is required");
|
|
578
609
|
process.exit(1);
|
|
579
610
|
}
|
|
611
|
+
let initialPromptFile;
|
|
612
|
+
if (promptText) {
|
|
613
|
+
initialPromptFile = join(tmpdir(), `herd-prompt-${Date.now()}.txt`);
|
|
614
|
+
writeFileSync(initialPromptFile, promptText);
|
|
615
|
+
} else if (promptFile) initialPromptFile = promptFile;
|
|
580
616
|
const tabId = await openSession({
|
|
581
617
|
tabName: name,
|
|
582
618
|
dir,
|
|
583
619
|
claudeCmd: useWorktree ? `claude --worktree ${JSON.stringify(name)}` : "claude",
|
|
584
|
-
workspaceQuery: workspace
|
|
620
|
+
workspaceQuery: workspace,
|
|
621
|
+
initialPromptFile
|
|
585
622
|
});
|
|
586
623
|
const suffix = useWorktree ? ` (worktree: .claude/worktrees/${name})` : "";
|
|
587
624
|
consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude at ${dir}${suffix}`);
|
package/package.json
CHANGED
package/skills/herd/SKILL.md
CHANGED
|
@@ -30,7 +30,7 @@ Each session runs in its own terminal tab. `herd` lets you — and other Claude
|
|
|
30
30
|
```bash
|
|
31
31
|
herd sessions # list all tabs with session status
|
|
32
32
|
herd list # list all workspaces, tabs, and blocks
|
|
33
|
-
herd new <name> [dir] [-w workspace]
|
|
33
|
+
herd new <name> [dir] [-w workspace] [-p "prompt"] [-f file] # new tab + claude
|
|
34
34
|
herd resume <name> [dir] # new tab + claude --continue
|
|
35
35
|
herd fork <tab-name> [-n new-name] # fork a session into a new tab
|
|
36
36
|
herd close <name-or-id> # close a tab
|
|
@@ -103,28 +103,25 @@ use `herd resume <name> <dir>` afterwards to open the resulting session in a new
|
|
|
103
103
|
|
|
104
104
|
As a Claude Code session, you can spawn a sibling session to work on a parallel task:
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
herd new payments ~/Dev/myapp
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**CRITICAL: Wait for Claude to be ready before sending tasks.** After `herd new` returns, Claude is still starting up (loading MCP servers, showing the initial prompt). Sending immediately causes the task to arrive as raw shell commands, not as a Claude prompt.
|
|
111
|
-
|
|
112
|
-
Poll `herd scrollback` until you see the Claude prompt (the `❯` line with no pending output):
|
|
106
|
+
**Preferred: pass the initial task directly to `herd new`** using `--prompt` or `--file`. This polls internally until Claude's `❯` prompt appears before sending — no race condition:
|
|
113
107
|
|
|
114
108
|
```bash
|
|
115
|
-
|
|
116
|
-
herd
|
|
117
|
-
# Repeat every few seconds until you see Claude's prompt — typically 10-15s
|
|
109
|
+
herd new payments ~/Dev/myapp --prompt "implement the billing endpoint"
|
|
110
|
+
herd new payments ~/Dev/myapp --file /tmp/task.txt
|
|
118
111
|
```
|
|
119
112
|
|
|
120
|
-
|
|
113
|
+
If you need to send a task after the fact, poll first:
|
|
121
114
|
|
|
122
115
|
```bash
|
|
123
|
-
herd
|
|
124
|
-
|
|
125
|
-
herd
|
|
116
|
+
herd new payments ~/Dev/myapp
|
|
117
|
+
# Poll until ❯ appears (typically 10-15s with MCP servers)
|
|
118
|
+
herd scrollback payments 5 # repeat until you see ❯
|
|
119
|
+
herd send payments --file /tmp/task.txt
|
|
120
|
+
herd send payments "yes\n" # quick replies
|
|
126
121
|
```
|
|
127
122
|
|
|
123
|
+
**Do NOT call `herd send` immediately after `herd new`** — Claude is still starting up and the text will land as raw shell commands.
|
|
124
|
+
|
|
128
125
|
## Workflow: Monitoring Another Session
|
|
129
126
|
|
|
130
127
|
```bash
|