@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 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
+ };
@@ -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
- }): Pick<HerdrKit, 'startAgent' | 'prompt' | 'sendKeys' | 'wait' | 'read' | 'agentKinds' | 'installedAgentKinds'>;
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 { accessSync, constants as fsConstants } from 'node:fs';
6
- import { join } from 'node:path';
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
- return { paneId: a.pane_id, terminalId: a.terminal_id, revision: a.revision, status: a.agent_status };
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
- const installed = [];
178
- for (const kind of kinds) {
179
- const names = [kind, ...(o.aliases?.[kind] ?? [])];
180
- if (names.some((name) => o.path.some((dir) => {
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
- accessSync(join(dir, name), fsConstants.X_OK);
183
- return true;
389
+ message = JSON.parse(line);
184
390
  }
185
391
  catch {
186
- return false;
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
- installed.push(kind);
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
- return installed;
192
- }
193
- return { startAgent, prompt, sendKeys, wait, read, agentKinds, installedAgentKinds };
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 type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrProtocolRange, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, HerdrTransport, PromptReceipt, StartAgent, TerminalSession, } from './types.ts';
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.1.10",
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.6.0",
56
- "@byokit/relay": "0.4.2",
57
- "@byokit/reach": "0.4.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
  },