@byokit/herdr 0.1.10 → 0.3.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.
- package/CHANGELOG.md +31 -0
- package/README.md +105 -1
- package/dist/accounts.d.ts +9 -0
- package/dist/accounts.js +130 -0
- package/dist/agents.d.ts +28 -2
- package/dist/agents.js +268 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/kit.d.ts +7 -1
- package/dist/kit.js +21 -1
- package/dist/types.d.ts +66 -0
- package/dist/words.json +10 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/reach 0.5.0.
|
|
8
|
+
- Dependency update: pins @byokit/relay 0.5.0.
|
|
9
|
+
- Dependency update: pins @byokit/link 0.7.0.
|
|
10
|
+
|
|
11
|
+
- `PromptReceipt.agentSession` optionally identifies the conversation from the prompt response without an extra read.
|
|
12
|
+
|
|
13
|
+
- FIX: Refuse non-empty `StartAgent.env` on existing panes instead of silently using the old subscription.
|
|
14
|
+
- Move a conversation to another subscription account with `moveToAccount`: verify the target shell folder,
|
|
15
|
+
resume and wait before closing the original pane, and roll back the new pane on failure.
|
|
16
|
+
- Open each CLI’s own sign-in in an account-specific tab with `openSignInTab` and the host’s explicit env.
|
|
17
|
+
|
|
18
|
+
- `HerdrKit.startAgent` lifecycle events: `installing` (with the progress words) before a start
|
|
19
|
+
that needs an install, `ready` with the fresh ref, and `launchFailed` with a typed reason
|
|
20
|
+
(`placement-failed` | `pane-busy` | `install-failed` | `start-rejected`) plus plain words —
|
|
21
|
+
subscribable per call (`StartAgent.onEvent`) or app-wide (`kit.onStartAgent`), no polling.
|
|
22
|
+
The shape and rejection are unchanged. Install detection (`agentInstallState`, also on
|
|
23
|
+
`agentStatus` as `installState`) tells a real runnable binary apart from an auto-install
|
|
24
|
+
launcher such as a mise shim: shimmed or absent reads `installs-on-first-start`, sharing the
|
|
25
|
+
one probe path with `installedAgentKinds` (`resolveAgentBinary`, `isAutoInstallShim`).
|
|
26
|
+
|
|
27
|
+
## 0.2.0 (2026-09-30)
|
|
28
|
+
|
|
29
|
+
- `HerdrKit.agentStatus(kinds)` (B5): per-kind onboarding readiness — installed plus CLI
|
|
30
|
+
sign-in (`yes`/`no`/`unknown`) from each CLI's own non-secret status command (`claude auth
|
|
31
|
+
status`, Codex `app-server` `account/read`; `unknown` where a CLI has none), install and
|
|
32
|
+
sign-in hint words, and the extra install dirs (mise shims, `~/.local/bin`, Homebrew) moved
|
|
33
|
+
in from muxr's PATH probe. Pi reads `installed: false` with `installs on first start`. The
|
|
34
|
+
command runner is injectable for tests; credential files are never read.
|
|
35
|
+
|
|
5
36
|
## 0.1.10 (2026-09-30)
|
|
6
37
|
|
|
7
38
|
- Dependency update: pins @byokit/relay 0.4.2.
|
package/README.md
CHANGED
|
@@ -149,7 +149,8 @@ persistent grant store, and the phone page above.
|
|
|
149
149
|
|
|
150
150
|
| Export | What it does |
|
|
151
151
|
|---|---|
|
|
152
|
-
| `HerdrKit` (`@byokit/herdr`) | The host-side kit, in `adopt` or `own` mode: `start`/`stop`, `state`, `snapshot`/`onChange`, `startAgent`, `prompt`, `sendKeys`, `wait`, `read`, `blocked`/`onBlocked`/`answer`, `closePane`/`closeTab`/`closeWorkspace`, `agentKinds`, `installedAgentKinds`, `terminal`, `onEvent`, `statusWatchReady`, and the pass-throughs `call`, `subscribe` and `cli` |
|
|
152
|
+
| `HerdrKit` (`@byokit/herdr`) | The host-side kit, in `adopt` or `own` mode: `start`/`stop`, `state`, `snapshot`/`onChange`, `startAgent`, `openSignInTab`, `moveToAccount`, `onStartAgent`, `prompt`, `sendKeys`, `wait`, `read`, `blocked`/`onBlocked`/`answer`, `closePane`/`closeTab`/`closeWorkspace`, `agentKinds`, `installedAgentKinds`, `agentStatus`, `agentInstallState`, `terminal`, `onEvent`, `statusWatchReady`, and the pass-throughs `call`, `subscribe` and `cli` |
|
|
153
|
+
| `agentStatus`, `agentInstallState`, `agentProbePath`, `extraPathDirs`, `runStatusCommand`, `isAutoInstallShim`, `resolveAgentBinary` (`@byokit/herdr`) | Onboarding readiness: per-kind install + CLI sign-in without a Herdr connection (see below) |
|
|
153
154
|
| `HERDR_VERSION`, `HERDR_PROTOCOL` | The pinned Herdr release (`0.9.1`) and the protocol the kit speaks (`22`) |
|
|
154
155
|
| `words`, `agentWords`, `stateWords`, `WORDS` | Plain sentences for agent statuses and kit states |
|
|
155
156
|
| `herdrLink` (`@byokit/herdr/link`) | The host side of the `hd.*` link ops, spread into `Host.open`; scopes each grant to its workspaces and can push sealed approval notices through a relay |
|
|
@@ -159,15 +160,118 @@ persistent grant store, and the phone page above.
|
|
|
159
160
|
| `startFakeHerdr`, `writeBinShim`, `herdrContract` (`@byokit/herdr/testing`) | The kit's stand-in Herdr, a bin shim that runs it, and the contract suite the kit passes |
|
|
160
161
|
| `ensureHerdr`, `fetchHerdr`, `HERDR_ASSETS` (`@byokit/herdr/binary`) | Opt-in pinned fetch: the v0.9.1 asset for the current platform into an app-owned dir, sha256-verified, executable, returned as the absolute `own`-mode `bin`; refused (nothing left) on a hash mismatch or unsupported platform |
|
|
161
162
|
|
|
163
|
+
`PromptReceipt` contains `paneId`, `terminalId`, `revision`, and `status`. Its optional
|
|
164
|
+
`agentSession` (`{ source, agent, kind, value }`) identifies the conversation that received
|
|
165
|
+
this prompt, taken directly from the `agent.prompt` response without another read.
|
|
166
|
+
`kind` distinguishes a session id from a session path; `value` is that id or path.
|
|
167
|
+
Herdr exposes no separate generation counter. When Herdr omits the session (or returns
|
|
168
|
+
null), `agentSession` is absent; callers can fall back to their existing checks.
|
|
169
|
+
|
|
162
170
|
Types (`HerdrKitOptions`, `HerdrState`, `StartAgent`, `PromptReceipt`, `BlockedAgent`, `HerdrSnapshot`,
|
|
163
171
|
`HerdrMethod`/`HerdrParams`/`HerdrResult`, `HerdrEventName`/`HerdrEventOf`, ...) come from the main entry.
|
|
164
172
|
|
|
173
|
+
## Account-specific sign-ins and moves
|
|
174
|
+
|
|
175
|
+
The host supplies account folders and shares conversation history as needed. The kit never reads or copies
|
|
176
|
+
credentials. To connect an account, open its CLI in a new tab and let the person complete the CLI's own login:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import type { HerdrKit } from '@byokit/herdr';
|
|
180
|
+
|
|
181
|
+
declare const kit: HerdrKit; // the host's started kit
|
|
182
|
+
const signIn = await kit.openSignInTab({
|
|
183
|
+
workspaceId: 'w1', kind: 'codex', cwd: '/home/me/project', env: { CODEX_HOME: '/app/accounts/work' },
|
|
184
|
+
});
|
|
185
|
+
// Attach terminal(signIn.paneId, …) for the CLI's own sign-in screen.
|
|
186
|
+
const moved = await kit.moveToAccount({ paneId: 'w1:p2' }, {
|
|
187
|
+
provider: 'codex', folder: '/app/accounts/work',
|
|
188
|
+
});
|
|
189
|
+
if (moved.ok) console.log(moved.session); // new pane to follow
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A move accepts Claude/Pi for Claude folders and Codex/Pi for Codex folders. It verifies the new shell's effective
|
|
193
|
+
account folder, resumes the conversation there, waits for a ready session, then closes the old pane. A failed
|
|
194
|
+
start preserves the original; a failed source close rolls back the new pane. Failures return a plain `message`
|
|
195
|
+
and a `live` pane id for recovery, including when cleanup fails. Wait until a conversation is idle or done before
|
|
196
|
+
moving it. `StartAgent.env` applies to newly created placements; existing shells cannot receive a new env.
|
|
197
|
+
Tokens stay on the device and are never logged. Each provider's own terms apply to how you use your plan.
|
|
198
|
+
|
|
165
199
|
## Two ways in
|
|
166
200
|
|
|
167
201
|
- `adopt` talks to the socket path the app passes: the person's own Herdr, nothing spawned, nothing written.
|
|
168
202
|
- `own` runs the Herdr binary the app names, in an app-owned state directory.
|
|
169
203
|
|
|
170
204
|
Helpers cover the common paths (start an agent, deliver a prompt with a receipt, watch blocked agents, exact closes).
|
|
205
|
+
|
|
206
|
+
## Agent readiness
|
|
207
|
+
|
|
208
|
+
`kit.agentStatus(kinds)` reports, per kind, whether the agent's CLI is installed and whether
|
|
209
|
+
it is signed in — the onboarding agents-and-pickers check. It needs no Herdr connection: it
|
|
210
|
+
probes the local machine. Install detection reuses `installedAgentKinds` over the host PATH plus
|
|
211
|
+
the extra install dirs a service PATH omits (`extraPathDirs`: `~/.local/bin`, the mise shims,
|
|
212
|
+
`~/.npm-global/bin`, Homebrew and the system dirs — the set muxr probed before this kit did).
|
|
213
|
+
Sign-in comes only from each CLI's own documented non-secret status command, with a 10 s timeout;
|
|
214
|
+
a kind whose CLI has none, or whose command gives no answer, reads `unknown`. Credential files
|
|
215
|
+
are never opened, read or statted — only the signed-in boolean is kept. Install detection tells a
|
|
216
|
+
real runnable binary apart from an auto-install launcher: a mise-style shim on PATH, or nothing
|
|
217
|
+
on PATH at all, reads `installState: 'installs-on-first-start'` with `installed: false` — Herdr
|
|
218
|
+
fetches the agent on first start — instead of a missing-install error. That covers `pi`, which
|
|
219
|
+
Herdr installs via mise: a missing or shimmed `pi` reads `installed: false` with `installs on
|
|
220
|
+
first start`, while a real `pi` binary reads `installed: true`.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { HerdrKit } from '@byokit/herdr';
|
|
224
|
+
|
|
225
|
+
const kit = new HerdrKit({ mode: 'adopt', bin: 'herdr', socketPath: '/tmp/herdr.sock' });
|
|
226
|
+
// Local probe only — the kit need not be started.
|
|
227
|
+
console.log(await kit.agentStatus(['pi', 'claude', 'codex']));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
[
|
|
232
|
+
{ kind: 'pi', installed: false, installState: 'installs-on-first-start', signedIn: 'unknown',
|
|
233
|
+
installHint: 'installs on first start' },
|
|
234
|
+
{ kind: 'claude', installed: true, installState: 'installed', signedIn: 'yes',
|
|
235
|
+
installHint: 'Install the claude command, then check again.' },
|
|
236
|
+
{ kind: 'codex', installed: true, installState: 'installed', signedIn: 'no',
|
|
237
|
+
installHint: 'Install the codex command, then check again.',
|
|
238
|
+
signInHint: 'On this computer run `codex`, sign in, then come back.' },
|
|
239
|
+
]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Kind | Status command | Why this one |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `claude` | `claude auth status` (JSON `loggedIn`) | The CLI's own documented status; a signed-out CLI still prints its JSON, so only an empty answer reads `unknown`. |
|
|
245
|
+
| `codex` | `codex app-server` with a pipelined `initialize` + `account/read` round | Codex exposes sign-in only over its app-server protocol; a record `account` in the second answer means signed in. |
|
|
246
|
+
| `pi` | none (Herdr installs it via mise) | Missing or shimmed: `installed: false`, `installs on first start`. A real binary reads `installed: true`. |
|
|
247
|
+
| any other kind | none | No documented non-secret status command, so `unknown` until the kit covers it. |
|
|
248
|
+
|
|
249
|
+
Options: `{ path, aliases }` as in `installedAgentKinds`; `{ readFile }` overrides the shim
|
|
250
|
+
sniff's file-head reader (tests use fakes); `{ run }` injects the command runner
|
|
251
|
+
(`(command, args, { stdin, timeoutMs }) => Promise<{ stdout } | undefined>`, so tests use fakes);
|
|
252
|
+
`{ timeoutMs }` bounds each probe. `signInHint` rides kinds the kit knows how to check whenever they
|
|
253
|
+
are not signed in; `installHint` always rides along.
|
|
254
|
+
|
|
255
|
+
## Agent start lifecycle
|
|
256
|
+
|
|
257
|
+
`startAgent` keeps its typed shape and rejection, and additionally reports the launch as events
|
|
258
|
+
so an app shows `Installing…` instead of a blank start: `installing` (with the progress words)
|
|
259
|
+
before a start that needs an install, `ready` with the fresh ref, and `launchFailed` with a typed
|
|
260
|
+
`reason` (`placement-failed` | `pane-busy` | `install-failed` | `start-rejected`) plus plain words.
|
|
261
|
+
Subscribe per call with `onEvent`, or app-wide with `kit.onStartAgent` — no polling either way.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { HerdrKit } from '@byokit/herdr';
|
|
265
|
+
|
|
266
|
+
const kit = new HerdrKit({ mode: 'adopt', bin: 'herdr', socketPath: '/tmp/herdr.sock' });
|
|
267
|
+
const off = kit.onStartAgent((e) => {
|
|
268
|
+
if (e.phase === 'installing') console.log(e.message); // Installing pi…
|
|
269
|
+
if (e.phase === 'launchFailed') console.log(e.reason, e.message);
|
|
270
|
+
});
|
|
271
|
+
await kit.startAgent({ kind: 'pi', cwd: '/home/me/project', place: { workspace: 'new' },
|
|
272
|
+
onEvent: (e) => { if (e.phase === 'ready') console.log(e.ref.paneId); } });
|
|
273
|
+
off();
|
|
274
|
+
```
|
|
171
275
|
`call` and `subscribe` are typed pass-throughs to the complete socket API, and `cli()` reaches the full CLI.
|
|
172
276
|
|
|
173
277
|
`start()` is non-fatal: if Herdr is down it rejects (state `failed`, e.g. `failed/socket`) and may be called again
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Call } from './agents.ts';
|
|
2
|
+
import type { AgentRef, MoveResult, MoveToAccount, OpenSignInTab, StartAgent } from './types.ts';
|
|
3
|
+
export declare function createAccountPanes(ctx: {
|
|
4
|
+
call: Call;
|
|
5
|
+
startAgent(o: StartAgent): Promise<AgentRef>;
|
|
6
|
+
}): {
|
|
7
|
+
openSignInTab: (o: OpenSignInTab) => Promise<AgentRef>;
|
|
8
|
+
moveToAccount: (target: AgentRef, o: MoveToAccount) => Promise<MoveResult>;
|
|
9
|
+
};
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Account-specific CLI panes (D11/6.6). No credentials are read or copied. Moves follow
|
|
2
|
+
// the decided start-then-close order; only fake transports exercise this in tests.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { words } from "./words.js";
|
|
5
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
export function createAccountPanes(ctx) {
|
|
7
|
+
const moving = new Set();
|
|
8
|
+
const call = ctx.call;
|
|
9
|
+
async function close(paneId) {
|
|
10
|
+
try {
|
|
11
|
+
await call('pane.close', { pane_id: paneId });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// A shell rc can override placement env. Verify what the shell actually resolves, rather
|
|
19
|
+
// than pane.get's launch env. The variable name is fixed by the provider, never caller text.
|
|
20
|
+
async function checkEnv(paneId, variable, folder, timeout) {
|
|
21
|
+
const marker = `BYOKIT_ACCOUNT_${randomBytes(8).toString('hex')}`;
|
|
22
|
+
const deadline = Date.now() + timeout;
|
|
23
|
+
try {
|
|
24
|
+
await call('pane.send_text', { pane_id: paneId, text: `echo ${marker}="$${variable}"\n` }, timeout);
|
|
25
|
+
for (;;) {
|
|
26
|
+
const result = await call('pane.read', { pane_id: paneId, source: 'recent_unwrapped', lines: 40,
|
|
27
|
+
format: 'text', strip_ansi: true }, Math.max(1, deadline - Date.now()));
|
|
28
|
+
const readings = String(result?.read?.text ?? '').split('\n').map((line) => line.trim())
|
|
29
|
+
.filter((line) => line.startsWith(`${marker}=`)).map((line) => line.slice(marker.length + 1));
|
|
30
|
+
if (readings.length > 0)
|
|
31
|
+
return readings.includes(folder);
|
|
32
|
+
if (Date.now() >= deadline)
|
|
33
|
+
return false;
|
|
34
|
+
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function openSignInTab(o) {
|
|
42
|
+
const { workspaceId, label, ...start } = o;
|
|
43
|
+
try {
|
|
44
|
+
return await ctx.startAgent({ ...start, place: { tab: 'new', workspaceId,
|
|
45
|
+
...(label === undefined ? {} : { label }) } });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A runtime error can contain account env. Never relay it to the sign-in screen.
|
|
49
|
+
throw Object.assign(new Error(words('agent.notReady')), { code: 'sign_in_failed' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function moveToAccount(target, o) {
|
|
53
|
+
const failed = (code, live = target.paneId) => ({ ok: false, code, message: words(`move.${code}`), live });
|
|
54
|
+
if (moving.has(target.paneId))
|
|
55
|
+
return failed('busy');
|
|
56
|
+
moving.add(target.paneId);
|
|
57
|
+
try {
|
|
58
|
+
let agent;
|
|
59
|
+
try {
|
|
60
|
+
agent = (await call('agent.get', { target: target.paneId }))?.agent;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return failed('too_early');
|
|
64
|
+
}
|
|
65
|
+
const session = agent?.agent_session;
|
|
66
|
+
if (!agent || agent.launch_pending === true || !session || typeof session.value !== 'string'
|
|
67
|
+
|| session.value.length === 0)
|
|
68
|
+
return failed('too_early');
|
|
69
|
+
if (!['idle', 'done'].includes(agent.agent_status))
|
|
70
|
+
return failed('busy');
|
|
71
|
+
const kind = agent.agent;
|
|
72
|
+
if (!['claude', 'codex'].includes(o.provider) || ![o.provider, 'pi'].includes(kind)
|
|
73
|
+
|| session.agent !== kind)
|
|
74
|
+
return failed('unsupported');
|
|
75
|
+
const args = kind === 'pi' && session.kind === 'path' ? ['--session', session.value]
|
|
76
|
+
: kind === 'claude' && session.kind === 'id' ? ['--resume', session.value]
|
|
77
|
+
: kind === 'codex' && session.kind === 'id' ? ['resume', session.value] : undefined;
|
|
78
|
+
if (args === undefined)
|
|
79
|
+
return failed('unsupported');
|
|
80
|
+
if (!o.folder || /[\r\n\0]/.test(o.folder))
|
|
81
|
+
return failed('env_mismatch');
|
|
82
|
+
const timeout = o.timeoutMs ?? 15_000;
|
|
83
|
+
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
84
|
+
return failed('start_failed');
|
|
85
|
+
const variable = o.provider === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME';
|
|
86
|
+
let paneId;
|
|
87
|
+
try {
|
|
88
|
+
const split = await call('pane.split', { target_pane_id: target.paneId,
|
|
89
|
+
direction: o.direction ?? 'right', focus: false,
|
|
90
|
+
...(typeof (agent.foreground_cwd ?? agent.cwd) === 'string' ? { cwd: agent.foreground_cwd ?? agent.cwd } : {}),
|
|
91
|
+
env: { ...o.env, [variable]: o.folder } }, timeout);
|
|
92
|
+
if (typeof split?.pane?.pane_id !== 'string')
|
|
93
|
+
return failed('start_failed');
|
|
94
|
+
paneId = split.pane.pane_id;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return failed('start_failed');
|
|
98
|
+
}
|
|
99
|
+
if (!await checkEnv(paneId, variable, o.folder, timeout)) {
|
|
100
|
+
await close(paneId);
|
|
101
|
+
return failed('env_mismatch');
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
// Unique names avoid colliding with the still-live source agent.
|
|
105
|
+
await ctx.startAgent({ kind, cwd: agent.foreground_cwd ?? agent.cwd ?? '.',
|
|
106
|
+
name: `move-${randomBytes(8).toString('hex')}`, place: { pane: paneId }, args, timeoutMs: timeout });
|
|
107
|
+
await call('agent.wait', { target: paneId, until: ['idle', 'done'], timeout_ms: timeout }, timeout + 5000);
|
|
108
|
+
const ready = (await call('agent.get', { target: paneId }))?.agent;
|
|
109
|
+
if (!ready || ready.launch_pending === true || !['idle', 'done'].includes(ready.agent_status)
|
|
110
|
+
|| ready.agent !== kind || ready.agent_session?.agent !== kind
|
|
111
|
+
|| ready.agent_session?.kind !== session.kind || typeof ready.agent_session?.value !== 'string'
|
|
112
|
+
|| ready.agent_session.value.length === 0)
|
|
113
|
+
throw new Error('not ready');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
const cleaned = await close(paneId);
|
|
117
|
+
return failed('start_failed', cleaned ? target.paneId : paneId);
|
|
118
|
+
}
|
|
119
|
+
if (!await close(target.paneId)) {
|
|
120
|
+
const cleaned = await close(paneId);
|
|
121
|
+
return failed('close_failed', cleaned ? target.paneId : paneId);
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, session: paneId };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
moving.delete(target.paneId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { openSignInTab, moveToAccount };
|
|
130
|
+
}
|
package/dist/agents.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import type { HerdrKit } from './kit.ts';
|
|
2
|
-
import type { HerdrSnapshot } from './types.ts';
|
|
2
|
+
import type { AgentInstallProbe, AgentInstallState, AgentLaunchFailureReason, AgentStartEvent, HerdrSnapshot } from './types.ts';
|
|
3
3
|
export type Call = (method: string, params: Record<string, unknown>, timeoutMs?: number) => Promise<unknown>;
|
|
4
4
|
export declare function createAgents(ctx: {
|
|
5
5
|
call: Call;
|
|
6
6
|
snapshot(): HerdrSnapshot;
|
|
7
7
|
reread(paneId: string): Promise<void>;
|
|
8
|
-
|
|
8
|
+
emitStart?(e: AgentStartEvent): void;
|
|
9
|
+
}): Pick<HerdrKit, 'startAgent' | 'prompt' | 'sendKeys' | 'wait' | 'read' | 'agentKinds' | 'installedAgentKinds' | 'agentStatus'>;
|
|
10
|
+
export declare function resolveAgentBinary(kind: string, o: {
|
|
11
|
+
path: string[];
|
|
12
|
+
aliases?: Record<string, string[]>;
|
|
13
|
+
}): {
|
|
14
|
+
binary: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function isAutoInstallShim(file: string, readFile?: (file: string) => string | undefined): boolean;
|
|
18
|
+
export declare function agentInstallState(kind: string, o?: AgentInstallProbe): {
|
|
19
|
+
kind: string;
|
|
20
|
+
state: AgentInstallState;
|
|
21
|
+
path?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function classifyStartFailure(error: unknown, o?: {
|
|
24
|
+
installExpected?: boolean;
|
|
25
|
+
stage?: 'placement' | 'start';
|
|
26
|
+
}): AgentLaunchFailureReason;
|
|
27
|
+
export declare function extraPathDirs(home?: string): string[];
|
|
28
|
+
export declare function agentProbePath(path?: string[], home?: string): string[];
|
|
29
|
+
export declare function runStatusCommand(command: string, args: string[], o?: {
|
|
30
|
+
stdin?: string;
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
stdout: string;
|
|
34
|
+
} | undefined>;
|
package/dist/agents.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// Exact param spellings come from the generated v0.9.1 table (6.7); where muxr and the schema
|
|
3
3
|
// disagree the schema wins. `pane.split` takes `target_pane_id` (muxr agrees), and `agent.start`
|
|
4
4
|
// has no `env` param — a start's env belongs to the placement create/split call (src/generated/methods.ts).
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { accessSync, constants as fsConstants, openSync, readSync, closeSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { delimiter, join } from 'node:path';
|
|
9
|
+
import { words } from "./words.js";
|
|
7
10
|
const PROMPTABLE = ['idle', 'working', 'blocked', 'done'];
|
|
8
11
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
9
12
|
// `agent.start` lands while the fresh pane is still at its shell prompt: the server answers
|
|
@@ -41,10 +44,50 @@ function rootPaneOf(result) {
|
|
|
41
44
|
}
|
|
42
45
|
export function createAgents(ctx) {
|
|
43
46
|
const call = ctx.call;
|
|
47
|
+
const emitStart = (e) => {
|
|
48
|
+
try {
|
|
49
|
+
ctx.emitStart?.(e);
|
|
50
|
+
}
|
|
51
|
+
catch { /* a listener never breaks a start */ }
|
|
52
|
+
};
|
|
44
53
|
// Every placement takes its pane id from the server's answer, never a prediction (6.4).
|
|
45
54
|
// Placements that make a pane (every path but `pane`) roll it back with `pane.close` when the
|
|
46
55
|
// start fails, so a failed start leaves no extra pane behind.
|
|
47
56
|
async function startAgent(o) {
|
|
57
|
+
const emit = (e) => {
|
|
58
|
+
try {
|
|
59
|
+
o.onEvent?.(e);
|
|
60
|
+
}
|
|
61
|
+
catch { /* a listener never breaks a start */ }
|
|
62
|
+
emitStart(e);
|
|
63
|
+
};
|
|
64
|
+
// Install pre-check: a shim or nothing on PATH means Herdr installs on first start, so the
|
|
65
|
+
// app hears `installing` before the long start instead of watching a blank pane.
|
|
66
|
+
let installExpected = false;
|
|
67
|
+
try {
|
|
68
|
+
installExpected = agentInstallState(o.kind, o.installProbe).state === 'installs-on-first-start';
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
installExpected = false;
|
|
72
|
+
}
|
|
73
|
+
if (installExpected)
|
|
74
|
+
emit({ phase: 'installing', kind: o.kind, message: words('agent.installing', { agent: o.kind }) });
|
|
75
|
+
let stage = 'placement';
|
|
76
|
+
try {
|
|
77
|
+
const ref = await startAgentInner(o, () => { stage = 'start'; });
|
|
78
|
+
emit({ phase: 'ready', kind: o.kind, ref });
|
|
79
|
+
return ref;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const reason = classifyStartFailure(error, { installExpected, stage });
|
|
83
|
+
emit({ phase: 'launchFailed', kind: o.kind, reason, message: launchFailureWords(reason, o.kind) });
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function startAgentInner(o, onStartPhase) {
|
|
88
|
+
if ('pane' in o.place && Object.keys(o.env ?? {}).length > 0) {
|
|
89
|
+
throw fail('env_mismatch', 'This pane needs to be opened again to use that sign-in.');
|
|
90
|
+
}
|
|
48
91
|
const timeout = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
49
92
|
const env = o.env === undefined ? {} : { env: o.env };
|
|
50
93
|
let paneId;
|
|
@@ -84,6 +127,7 @@ export function createAgents(ctx) {
|
|
|
84
127
|
}
|
|
85
128
|
if (paneId === undefined)
|
|
86
129
|
throw new Error('herdr: the placement answered no pane id');
|
|
130
|
+
onStartPhase?.();
|
|
87
131
|
const name = agentName(o);
|
|
88
132
|
const params = { pane_id: paneId, kind: o.kind, name,
|
|
89
133
|
...(o.args === undefined ? {} : { args: o.args }), timeout_ms: timeout };
|
|
@@ -150,7 +194,12 @@ export function createAgents(ctx) {
|
|
|
150
194
|
|| a.pane_id !== target.paneId) {
|
|
151
195
|
throw new Error('Herdr did not queue the prompt.');
|
|
152
196
|
}
|
|
153
|
-
|
|
197
|
+
const session = a.agent_session;
|
|
198
|
+
const agentSession = isObj(session) && typeof session.source === 'string'
|
|
199
|
+
&& typeof session.agent === 'string' && typeof session.kind === 'string' && typeof session.value === 'string'
|
|
200
|
+
? { source: session.source, agent: session.agent, kind: session.kind, value: session.value } : undefined;
|
|
201
|
+
return { paneId: a.pane_id, terminalId: a.terminal_id, revision: a.revision, status: a.agent_status,
|
|
202
|
+
...(agentSession === undefined ? {} : { agentSession }) };
|
|
154
203
|
}
|
|
155
204
|
async function wait(target, o) {
|
|
156
205
|
const made = await call('agent.wait', { target: target.paneId,
|
|
@@ -174,21 +223,225 @@ export function createAgents(ctx) {
|
|
|
174
223
|
return manifests.map((m) => (typeof m?.agent === 'string' ? m.agent : '')).filter((kind) => kind !== '');
|
|
175
224
|
}
|
|
176
225
|
function installedAgentKinds(kinds, o) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
226
|
+
return kinds.filter((kind) => resolveAgentBinary(kind, o).path !== undefined);
|
|
227
|
+
}
|
|
228
|
+
async function agentStatus(kinds, o) {
|
|
229
|
+
const path = o?.path ?? agentProbePath();
|
|
230
|
+
const run = o?.run ?? runStatusCommand;
|
|
231
|
+
const timeoutMs = o?.timeoutMs ?? STATUS_TIMEOUT_MS;
|
|
232
|
+
const probe = { path, ...(o?.aliases === undefined ? {} : { aliases: o.aliases }),
|
|
233
|
+
...(o?.readFile === undefined ? {} : { readFile: o.readFile }) };
|
|
234
|
+
return Promise.all(kinds.map(async (kind) => {
|
|
235
|
+
const install = agentInstallState(kind, probe);
|
|
236
|
+
const binary = o?.aliases?.[kind]?.[0] ?? kind;
|
|
237
|
+
const installHint = kind === 'pi' ? PI_INSTALL_HINT : `Install the ${binary} command, then check again.`;
|
|
238
|
+
// A shim or nothing on PATH means Herdr installs on first start: not installed, with the
|
|
239
|
+
// readiness words instead of a missing-install error (pi included — a real pi binary is
|
|
240
|
+
// the only case that reads installed).
|
|
241
|
+
if (install.state !== 'installed')
|
|
242
|
+
return { kind, installed: false, installState: install.state, signedIn: 'unknown', installHint };
|
|
243
|
+
const statusProbe = STATUS_PROBES[kind];
|
|
244
|
+
if (statusProbe === undefined)
|
|
245
|
+
return { kind, installed: true, installState: 'installed', signedIn: 'unknown', installHint };
|
|
246
|
+
let answer;
|
|
247
|
+
try {
|
|
248
|
+
answer = await run(statusProbe.command, statusProbe.args, { ...(statusProbe.stdin === undefined ? {} : { stdin: statusProbe.stdin }), timeoutMs });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
answer = undefined;
|
|
252
|
+
}
|
|
253
|
+
const signedIn = answer === undefined ? 'unknown' : statusProbe.parse(answer.stdout);
|
|
254
|
+
return { kind, installed: true, installState: 'installed', signedIn, installHint,
|
|
255
|
+
...(signedIn === 'yes' ? {} : { signInHint: SIGNIN_HINTS[kind] ?? `Sign in to ${kind} on this computer, then check again.` }) };
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
return { startAgent, prompt, sendKeys, wait, read, agentKinds, installedAgentKinds, agentStatus };
|
|
259
|
+
}
|
|
260
|
+
// One source of truth for install detection (B5's probe plus the shim layer below): the first
|
|
261
|
+
// executable file named for the kind across the probe path, with its absolute path.
|
|
262
|
+
export function resolveAgentBinary(kind, o) {
|
|
263
|
+
const names = [kind, ...(o.aliases?.[kind] ?? [])];
|
|
264
|
+
for (const name of names) {
|
|
265
|
+
for (const dir of o.path) {
|
|
266
|
+
if (dir === '')
|
|
267
|
+
continue;
|
|
268
|
+
const file = join(dir, name);
|
|
269
|
+
try {
|
|
270
|
+
accessSync(file, fsConstants.X_OK);
|
|
271
|
+
return { binary: name, path: file };
|
|
272
|
+
}
|
|
273
|
+
catch { /* keep looking */ }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { binary: o.aliases?.[kind]?.[0] ?? kind };
|
|
277
|
+
}
|
|
278
|
+
// Bounded head read for the shim sniff: the first 2 KiB as text, undefined when unreadable.
|
|
279
|
+
function readHead(file) {
|
|
280
|
+
let fd;
|
|
281
|
+
try {
|
|
282
|
+
fd = openSync(file, 'r');
|
|
283
|
+
const buf = Buffer.alloc(2048);
|
|
284
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
285
|
+
return buf.subarray(0, n).toString('utf8');
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
if (fd !== undefined)
|
|
292
|
+
try {
|
|
293
|
+
closeSync(fd);
|
|
294
|
+
}
|
|
295
|
+
catch { /* already gone */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// A mise-style auto-install launcher reads as a shell script that execs through mise — the
|
|
299
|
+
// found "binary" runs, but the first run installs the agent instead. Real binaries (ELF,
|
|
300
|
+
// Mach-O) never match: the shebang gate keeps a stray "mise" substring ("promise", …) out.
|
|
301
|
+
export function isAutoInstallShim(file, readFile = readHead) {
|
|
302
|
+
let head;
|
|
303
|
+
try {
|
|
304
|
+
head = readFile(file);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return head !== undefined && head.startsWith('#!') && head.includes('mise');
|
|
310
|
+
}
|
|
311
|
+
// Per-kind install readiness sharing the B5 probe path: `installed` for a real runnable binary,
|
|
312
|
+
// `installs-on-first-start` for an auto-install launcher/shim or nothing on PATH (Herdr fetches
|
|
313
|
+
// the agent on first start). `missing` stays for callers that probe kinds Herdr never installs.
|
|
314
|
+
export function agentInstallState(kind, o) {
|
|
315
|
+
const path = o?.path ?? agentProbePath();
|
|
316
|
+
const found = resolveAgentBinary(kind, { path, ...(o?.aliases === undefined ? {} : { aliases: o.aliases }) });
|
|
317
|
+
if (found.path === undefined)
|
|
318
|
+
return { kind, state: 'installs-on-first-start' };
|
|
319
|
+
const shim = isAutoInstallShim(found.path, o?.readFile);
|
|
320
|
+
return shim ? { kind, state: 'installs-on-first-start', path: found.path }
|
|
321
|
+
: { kind, state: 'installed', path: found.path };
|
|
322
|
+
}
|
|
323
|
+
// Typed launch failure for the `launchFailed` event. The rejection itself is unchanged — this
|
|
324
|
+
// only classifies it: a busy pane that outlasted the retry budget, an install that never
|
|
325
|
+
// finished, a placement that made no pane, or any other rejected start.
|
|
326
|
+
export function classifyStartFailure(error, o) {
|
|
327
|
+
const code = codeOf(error);
|
|
328
|
+
if (code !== undefined && START_RETRYABLE.has(code))
|
|
329
|
+
return 'pane-busy';
|
|
330
|
+
if (o?.stage === 'placement')
|
|
331
|
+
return 'placement-failed';
|
|
332
|
+
if (o?.installExpected === true)
|
|
333
|
+
return 'install-failed';
|
|
334
|
+
return 'start-rejected';
|
|
335
|
+
}
|
|
336
|
+
function launchFailureWords(reason, kind) {
|
|
337
|
+
if (reason === 'pane-busy')
|
|
338
|
+
return words('agent.notReady');
|
|
339
|
+
if (reason === 'install-failed')
|
|
340
|
+
return words('agent.installFailed', { agent: kind });
|
|
341
|
+
return words('agent.launchFailed');
|
|
342
|
+
}
|
|
343
|
+
// Extra install dirs muxr's `executableOnPath`
|
|
344
|
+
// (`apps/host/src/agent/infrastructure/herdrSessionSource.ts`) probes past PATH: user-install
|
|
345
|
+
// dirs a service or GUI daemon's inherited PATH omits — mise shims (Herdr installs Pi through
|
|
346
|
+
// mise), `~/.local/bin`, npm-global, Homebrew and the system dirs.
|
|
347
|
+
export function extraPathDirs(home = homedir()) {
|
|
348
|
+
return [join(home, '.local', 'bin'), join(home, '.local', 'share', 'mise', 'shims'),
|
|
349
|
+
join(home, '.npm-global', 'bin'), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'];
|
|
350
|
+
}
|
|
351
|
+
// The full install probe: the host's own PATH plus the extra dirs, de-duplicated.
|
|
352
|
+
export function agentProbePath(path = (process.env.PATH ?? '').split(delimiter), home = homedir()) {
|
|
353
|
+
return [...new Set([...path, ...extraPathDirs(home)])].filter((dir) => dir !== '');
|
|
354
|
+
}
|
|
355
|
+
const STATUS_TIMEOUT_MS = 10_000;
|
|
356
|
+
const PI_INSTALL_HINT = 'installs on first start';
|
|
357
|
+
const SIGNIN_HINTS = {
|
|
358
|
+
claude: 'On this computer run `claude`, sign in, then come back.',
|
|
359
|
+
codex: 'On this computer run `codex`, sign in, then come back.',
|
|
360
|
+
};
|
|
361
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
362
|
+
// Per-kind CLI status commands (see the README table for why each one). Every probe asks the
|
|
363
|
+
// CLI itself and keeps only the signed-in boolean — email, plan and everything secret stays out.
|
|
364
|
+
// A kind with no documented non-secret status command has no entry and reads 'unknown'.
|
|
365
|
+
const STATUS_PROBES = {
|
|
366
|
+
// `claude auth status` prints JSON with `loggedIn` (muxr's planIdentity reads the same);
|
|
367
|
+
// a signed-out CLI still prints its JSON, so only an empty answer means 'unknown'.
|
|
368
|
+
claude: { command: 'claude', args: ['auth', 'status'], parse: (stdout) => {
|
|
369
|
+
let status;
|
|
370
|
+
try {
|
|
371
|
+
status = JSON.parse(stdout);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return 'unknown';
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(status))
|
|
377
|
+
return 'unknown';
|
|
378
|
+
return status.loggedIn === true ? 'yes' : 'no';
|
|
379
|
+
} },
|
|
380
|
+
// Codex exposes sign-in only over its app-server protocol, so the probe pipes a pipelined
|
|
381
|
+
// `initialize` + `account/read` round (same method muxr's planIdentity uses) and reads the
|
|
382
|
+
// second answer: a record `account` means signed in, its absence means signed out.
|
|
383
|
+
codex: { command: 'codex', args: ['app-server'],
|
|
384
|
+
stdin: '{"id":1,"method":"initialize","params":{"clientInfo":{"name":"byokit","version":"1"}}}\n{"id":2,"method":"account/read","params":{}}\n',
|
|
385
|
+
parse: (stdout) => {
|
|
386
|
+
for (const line of stdout.split('\n')) {
|
|
387
|
+
let message;
|
|
181
388
|
try {
|
|
182
|
-
|
|
183
|
-
return true;
|
|
389
|
+
message = JSON.parse(line);
|
|
184
390
|
}
|
|
185
391
|
catch {
|
|
186
|
-
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (isRecord(message) && message.id === 2) {
|
|
395
|
+
return isRecord(message.result) && isRecord(message.result.account) ? 'yes' : 'no';
|
|
187
396
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
397
|
+
}
|
|
398
|
+
return 'unknown';
|
|
399
|
+
} },
|
|
400
|
+
};
|
|
401
|
+
// Default runner: one bounded spawn per probe, stdin piped when the protocol needs it (codex).
|
|
402
|
+
// Failures (missing binary, timeout, non-empty stderr, empty stdout) read as no answer — never throw.
|
|
403
|
+
export async function runStatusCommand(command, args, o) {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
let done = false;
|
|
406
|
+
const finish = (value) => {
|
|
407
|
+
if (done)
|
|
408
|
+
return;
|
|
409
|
+
done = true;
|
|
410
|
+
clearTimeout(timer);
|
|
411
|
+
resolve(value);
|
|
412
|
+
};
|
|
413
|
+
let child;
|
|
414
|
+
try {
|
|
415
|
+
child = spawn(command, args, { stdio: ['pipe', 'pipe', 'ignore'], timeout: o?.timeoutMs ?? STATUS_TIMEOUT_MS });
|
|
190
416
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
417
|
+
catch {
|
|
418
|
+
finish(undefined);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let out = '';
|
|
422
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); finish(undefined); }, (o?.timeoutMs ?? STATUS_TIMEOUT_MS) + 500);
|
|
423
|
+
child.on('error', () => finish(undefined));
|
|
424
|
+
const stdout = child.stdout;
|
|
425
|
+
const stdin = child.stdin;
|
|
426
|
+
if (stdout === null || stdin === null) {
|
|
427
|
+
finish(undefined);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
stdout.setEncoding('utf8');
|
|
431
|
+
stdout.on('data', (chunk) => {
|
|
432
|
+
out += chunk;
|
|
433
|
+
if (out.length > 64 * 1024) {
|
|
434
|
+
child.kill('SIGKILL');
|
|
435
|
+
finish(undefined);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
child.on('close', () => finish(out.trim() === '' ? undefined : { stdout: out.slice(0, 64 * 1024) }));
|
|
439
|
+
stdin.on('error', () => { });
|
|
440
|
+
try {
|
|
441
|
+
if (o?.stdin !== undefined)
|
|
442
|
+
stdin.write(o.stdin);
|
|
443
|
+
stdin.end();
|
|
444
|
+
}
|
|
445
|
+
catch { /* a dead stdin settles through close/error */ }
|
|
446
|
+
});
|
|
194
447
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { HERDR_VERSION, HERDR_PROTOCOL } from './constants.ts';
|
|
2
2
|
export { HerdrKit } from './kit.ts';
|
|
3
3
|
export { agentWords, stateWords, words, WORDS, type WordKey } from './words.ts';
|
|
4
|
-
export
|
|
4
|
+
export { agentProbePath, extraPathDirs, runStatusCommand, agentInstallState, isAutoInstallShim, resolveAgentBinary, classifyStartFailure } from './agents.ts';
|
|
5
|
+
export type { AgentCliSignIn, AgentInstallProbe, AgentInstallState, AgentLaunchFailureReason, AgentReadiness, AgentStartEvent, AgentStatusOptions, AgentStatusRunner, AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrProtocolRange, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, HerdrTransport, MoveToAccount, MoveResult, OpenSignInTab, PromptReceipt, StartAgent, TerminalSession, } from './types.ts';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
export { HERDR_VERSION, HERDR_PROTOCOL } from "./constants.js";
|
|
4
4
|
export { HerdrKit } from "./kit.js";
|
|
5
5
|
export { agentWords, stateWords, words, WORDS } from "./words.js";
|
|
6
|
+
export { agentProbePath, extraPathDirs, runStatusCommand, agentInstallState, isAutoInstallShim, resolveAgentBinary, classifyStartFailure } from "./agents.js";
|
package/dist/kit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, PromptReceipt, StartAgent, TerminalSession } from './types.ts';
|
|
1
|
+
import type { AgentReadiness, AgentStartEvent, AgentStatusOptions, AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, PromptReceipt, StartAgent, TerminalSession, MoveToAccount, MoveResult, OpenSignInTab } from './types.ts';
|
|
2
2
|
export declare class HerdrKit {
|
|
3
3
|
private readonly supervisor;
|
|
4
4
|
private transport?;
|
|
@@ -14,7 +14,9 @@ export declare class HerdrKit {
|
|
|
14
14
|
private statusEpoch;
|
|
15
15
|
private readonly o;
|
|
16
16
|
private readonly callAny;
|
|
17
|
+
private readonly accountPanes;
|
|
17
18
|
private readonly agents;
|
|
19
|
+
private readonly startListeners;
|
|
18
20
|
private readonly blockedList;
|
|
19
21
|
constructor(o: HerdrKitOptions);
|
|
20
22
|
get state(): HerdrState;
|
|
@@ -65,7 +67,10 @@ export declare class HerdrKit {
|
|
|
65
67
|
* status events instead of opening a second watch.
|
|
66
68
|
*/
|
|
67
69
|
statusWatchReady(): Promise<void>;
|
|
70
|
+
openSignInTab(o: OpenSignInTab): Promise<AgentRef>;
|
|
71
|
+
moveToAccount(target: AgentRef, o: MoveToAccount): Promise<MoveResult>;
|
|
68
72
|
startAgent(o: StartAgent): Promise<AgentRef>;
|
|
73
|
+
onStartAgent(fn: (e: AgentStartEvent) => void): () => void;
|
|
69
74
|
prompt(target: AgentRef, text: string, o?: {
|
|
70
75
|
wait?: {
|
|
71
76
|
until?: AgentStatus[];
|
|
@@ -98,4 +103,5 @@ export declare class HerdrKit {
|
|
|
98
103
|
path: string[];
|
|
99
104
|
aliases?: Record<string, string[]>;
|
|
100
105
|
}): string[];
|
|
106
|
+
agentStatus(kinds: readonly string[], o?: AgentStatusOptions): Promise<AgentReadiness[]>;
|
|
101
107
|
}
|
package/dist/kit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAccountPanes } from "./accounts.js";
|
|
1
2
|
import { createAgents } from "./agents.js";
|
|
2
3
|
import { Blocked } from "./approvals.js";
|
|
3
4
|
import { runCli } from "./cli.js";
|
|
@@ -126,13 +127,21 @@ export class HerdrKit {
|
|
|
126
127
|
// a snapshot read wins over the read (see bootstrap)
|
|
127
128
|
o;
|
|
128
129
|
callAny = (method, params, timeoutMs) => this.call(method, params, timeoutMs === undefined ? undefined : { timeoutMs });
|
|
130
|
+
accountPanes;
|
|
129
131
|
agents;
|
|
132
|
+
startListeners = new Set();
|
|
130
133
|
blockedList;
|
|
131
134
|
constructor(o) {
|
|
132
135
|
this.o = o;
|
|
133
136
|
this.supervisor = new Supervisor(o, (s) => { this.current = s; o.onState?.(s); });
|
|
137
|
+
this.accountPanes = createAccountPanes({ call: this.callAny, startAgent: (o) => this.startAgent(o) });
|
|
134
138
|
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot(),
|
|
135
|
-
reread: (paneId) => this.reread(paneId)
|
|
139
|
+
reread: (paneId) => this.reread(paneId),
|
|
140
|
+
emitStart: (e) => { for (const fn of [...this.startListeners])
|
|
141
|
+
try {
|
|
142
|
+
fn(e);
|
|
143
|
+
}
|
|
144
|
+
catch { /* a listener never breaks a start */ } } });
|
|
136
145
|
this.blockedList = new Blocked({ call: this.callAny });
|
|
137
146
|
}
|
|
138
147
|
get state() { return this.current; }
|
|
@@ -437,7 +446,15 @@ export class HerdrKit {
|
|
|
437
446
|
* status events instead of opening a second watch.
|
|
438
447
|
*/
|
|
439
448
|
statusWatchReady() { return this.statusReadyPromise; }
|
|
449
|
+
openSignInTab(o) { return this.accountPanes.openSignInTab(o); }
|
|
450
|
+
moveToAccount(target, o) { return this.accountPanes.moveToAccount(target, o); }
|
|
440
451
|
startAgent(o) { return this.agents.startAgent(o); }
|
|
452
|
+
// App-wide start lifecycle without polling: every startAgent's installing/ready/launchFailed
|
|
453
|
+
// lands here as well as on that call's own `onEvent`. Returns the unsubscribe function.
|
|
454
|
+
onStartAgent(fn) {
|
|
455
|
+
this.startListeners.add(fn);
|
|
456
|
+
return () => { this.startListeners.delete(fn); };
|
|
457
|
+
}
|
|
441
458
|
prompt(target, text, o) { return this.agents.prompt(target, text, o); }
|
|
442
459
|
sendKeys(target, keys) { return this.agents.sendKeys(target, keys); }
|
|
443
460
|
wait(target, o) { return this.agents.wait(target, o); }
|
|
@@ -450,4 +467,7 @@ export class HerdrKit {
|
|
|
450
467
|
closeWorkspace(workspaceId) { return closeWorkspace(this.callAny, workspaceId); }
|
|
451
468
|
agentKinds() { return this.agents.agentKinds(); }
|
|
452
469
|
installedAgentKinds(kinds, o) { return this.agents.installedAgentKinds(kinds, o); }
|
|
470
|
+
// Onboarding readiness (B5): a local PATH probe plus each installed CLI's own status
|
|
471
|
+
// command — no Herdr connection needed, credential files never read.
|
|
472
|
+
agentStatus(kinds, o) { return this.agents.agentStatus(kinds, o); }
|
|
453
473
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,49 @@ export type HerdrEvent = {
|
|
|
47
47
|
export type { HerdrMethods, HerdrMethod, HerdrParams, HerdrResult } from './generated/methods.ts';
|
|
48
48
|
export type { HerdrEvents, HerdrEventName, HerdrEventOf, HerdrSubscription } from './generated/events.ts';
|
|
49
49
|
export type AgentStatus = 'idle' | 'working' | 'blocked' | 'done' | 'unknown';
|
|
50
|
+
export type AgentCliSignIn = 'yes' | 'no' | 'unknown';
|
|
51
|
+
export type AgentInstallState = 'installed' | 'installs-on-first-start' | 'missing';
|
|
52
|
+
export type AgentInstallProbe = {
|
|
53
|
+
path?: string[];
|
|
54
|
+
aliases?: Record<string, string[]>;
|
|
55
|
+
readFile?: (file: string) => string | undefined;
|
|
56
|
+
};
|
|
57
|
+
export type AgentReadiness = {
|
|
58
|
+
kind: string;
|
|
59
|
+
installed: boolean;
|
|
60
|
+
installState: AgentInstallState;
|
|
61
|
+
signedIn: AgentCliSignIn;
|
|
62
|
+
installHint: string;
|
|
63
|
+
signInHint?: string;
|
|
64
|
+
};
|
|
65
|
+
export type AgentStatusRunner = (command: string, args: string[], o?: {
|
|
66
|
+
stdin?: string;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
stdout: string;
|
|
70
|
+
} | undefined>;
|
|
71
|
+
export type AgentStatusOptions = {
|
|
72
|
+
path?: string[];
|
|
73
|
+
aliases?: Record<string, string[]>;
|
|
74
|
+
readFile?: (file: string) => string | undefined;
|
|
75
|
+
run?: AgentStatusRunner;
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
};
|
|
78
|
+
export type AgentLaunchFailureReason = 'placement-failed' | 'pane-busy' | 'install-failed' | 'start-rejected';
|
|
79
|
+
export type AgentStartEvent = {
|
|
80
|
+
phase: 'installing';
|
|
81
|
+
kind: string;
|
|
82
|
+
message: string;
|
|
83
|
+
} | {
|
|
84
|
+
phase: 'ready';
|
|
85
|
+
kind: string;
|
|
86
|
+
ref: AgentRef;
|
|
87
|
+
} | {
|
|
88
|
+
phase: 'launchFailed';
|
|
89
|
+
kind: string;
|
|
90
|
+
reason: AgentLaunchFailureReason;
|
|
91
|
+
message: string;
|
|
92
|
+
};
|
|
50
93
|
export type AgentRef = {
|
|
51
94
|
paneId: string;
|
|
52
95
|
name?: string;
|
|
@@ -75,12 +118,35 @@ export type StartAgent = {
|
|
|
75
118
|
args?: string[];
|
|
76
119
|
env?: Record<string, string>;
|
|
77
120
|
timeoutMs?: number;
|
|
121
|
+
onEvent?: (e: AgentStartEvent) => void;
|
|
122
|
+
installProbe?: AgentInstallProbe;
|
|
123
|
+
};
|
|
124
|
+
export type OpenSignInTab = Omit<StartAgent, 'place' | 'worktree'> & {
|
|
125
|
+
workspaceId: string;
|
|
126
|
+
label?: string;
|
|
127
|
+
};
|
|
128
|
+
export type MoveToAccount = {
|
|
129
|
+
provider: 'claude' | 'codex';
|
|
130
|
+
folder: string;
|
|
131
|
+
env?: Record<string, string>;
|
|
132
|
+
direction?: 'right' | 'down';
|
|
133
|
+
timeoutMs?: number;
|
|
134
|
+
};
|
|
135
|
+
export type MoveResult = {
|
|
136
|
+
ok: true;
|
|
137
|
+
session: string;
|
|
138
|
+
} | {
|
|
139
|
+
ok: false;
|
|
140
|
+
code: 'too_early' | 'busy' | 'unsupported' | 'env_mismatch' | 'close_failed' | 'start_failed';
|
|
141
|
+
message: string;
|
|
142
|
+
live?: string;
|
|
78
143
|
};
|
|
79
144
|
export type PromptReceipt = {
|
|
80
145
|
paneId: string;
|
|
81
146
|
terminalId: string;
|
|
82
147
|
revision: number;
|
|
83
148
|
status: AgentStatus;
|
|
149
|
+
agentSession?: AgentSessionRef;
|
|
84
150
|
};
|
|
85
151
|
export type BlockedAgent = {
|
|
86
152
|
paneId: string;
|
package/dist/words.json
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
"agent.unknown": "Running.",
|
|
14
14
|
"agent.signIn": "Sign in inside {agent}: follow its own steps on the screen.",
|
|
15
15
|
"agent.notReady": "This helper isn't ready yet. Try again in a moment.",
|
|
16
|
+
"agent.installing": "Installing {agent}…",
|
|
17
|
+
"agent.installFailed": "Installing {agent} didn't finish. Try again in a moment.",
|
|
18
|
+
"agent.launchFailed": "That helper couldn't start. Try again in a moment.",
|
|
16
19
|
"approval.stale": "That question already changed. Look again before answering.",
|
|
17
20
|
"close.wouldWiden": "Closing this would close more than you picked. Close the bigger one instead.",
|
|
18
|
-
"link.notAllowed": "This device can't do that. Ask the person at the computer."
|
|
21
|
+
"link.notAllowed": "This device can't do that. Ask the person at the computer.",
|
|
22
|
+
"move.too_early": "This conversation has not started yet. Try again in a moment.",
|
|
23
|
+
"move.busy": "Wait for this conversation to finish before moving it.",
|
|
24
|
+
"move.unsupported": "This conversation cannot move between these accounts.",
|
|
25
|
+
"move.env_mismatch": "The new pane did not receive that sign-in. Try again.",
|
|
26
|
+
"move.close_failed": "The old pane could not close. The move was undone where possible.",
|
|
27
|
+
"move.start_failed": "The new account could not take over. Try again."
|
|
19
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/herdr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Drive the Herdr on this computer — workspaces, tabs, panes, agents and blocked-approval answers — from an app, or hand it to a phone over @byokit/link.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@byokit/link": "0.
|
|
56
|
-
"@byokit/relay": "0.
|
|
57
|
-
"@byokit/reach": "0.
|
|
55
|
+
"@byokit/link": "0.7.0",
|
|
56
|
+
"@byokit/relay": "0.5.0",
|
|
57
|
+
"@byokit/reach": "0.5.0",
|
|
58
58
|
"@byokit/seal": "0.2.0",
|
|
59
59
|
"ws": "8.21.3"
|
|
60
60
|
},
|