@byokit/herdr 0.1.0 → 0.1.2
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 +38 -4
- package/README.md +10 -2
- package/dist/agents.d.ts +1 -0
- package/dist/agents.js +18 -6
- package/dist/index.d.ts +1 -1
- package/dist/kit.d.ts +4 -2
- package/dist/kit.js +43 -5
- package/dist/testing/contract.js +23 -7
- package/dist/testing/fake-herdr/bin.js +41 -12
- package/dist/testing/fake-herdr/server.d.ts +10 -1
- package/dist/testing/fake-herdr/server.js +237 -35
- package/dist/testing/fake-herdr/world.d.ts +31 -0
- package/dist/testing/fake-herdr/world.js +21 -1
- package/dist/testing/index.js +9 -0
- package/dist/types.d.ts +8 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,21 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.2 (2026-09-29)
|
|
6
|
+
|
|
7
|
+
- FIX: `prompt` now reaches an agent that was already running when the app connected. Herdr marks only
|
|
8
|
+
the agents its own `agent.start` launched as interactive-ready, so an agent started by hand or by
|
|
9
|
+
another app was refused forever; and a snapshot read while an agent was still launching kept it
|
|
10
|
+
marked as launching, because Herdr never pushes the end of a launch. The kit no longer gates on
|
|
11
|
+
interactive-ready, and before refusing a prompt it re-reads the agent once from Herdr.
|
|
12
|
+
- FIX: when Herdr itself refuses a prompt as not ready, `prompt` now fails with `agent-not-ready`
|
|
13
|
+
like the kit's own check, not with Herdr's raw `agent_not_ready` error.
|
|
14
|
+
- The test fake leaves `launch_pending` and `interactive_ready` out when they are false, as Herdr v0.9.1 does.
|
|
15
|
+
- Depends on @byokit/relay 0.2.1.
|
|
16
|
+
|
|
17
|
+
## 0.1.1 (2026-09-29)
|
|
18
|
+
|
|
19
|
+
- SECURITY: (from @byokit/relay 0.2.0) Removing a phone now reliably stops its push notifications, even if the relay was down or restarting when you removed it. Before, `RelayClient.revoke` dropped the device's grant and sent one unsubscribe that was lost if the offline queue overflowed or the host stopped or restarted before the relay was back, so the relay kept the phone's push address. Now the unsubscribe is saved first and resent on every connection until the relay confirms it; `revoke()` resolves then. Pass a durable `store` to `RelayClient` so a pending unsubscribe survives a restart (the default is memory).
|
|
20
|
+
- FIX: `prompt` now reaches an idle agent that was already running when the app connected. The kit
|
|
21
|
+
read every live status push as the agent kind (the push carries the kind as a plain `agent`
|
|
22
|
+
string) and dropped the status, so an agent caught at `unknown` stayed there and the readiness
|
|
23
|
+
gate refused every prompt. The push now lands; and a status change between the snapshot read and
|
|
24
|
+
the per-pane watch's ack, which reached no socket, is re-read once the watch is live.
|
|
25
|
+
- FIX subscribe errors and type (K4): `kit.subscribe(subs, on, onError?)` forwards `onError`
|
|
26
|
+
to the transport and returns its control surface (`HerdrSubscribeStop`: `ready`/`onReconnect`/
|
|
27
|
+
`onDisconnect`); a rejected filtered batch reports `invalid_subscription` once and never retries.
|
|
28
|
+
The kit's own event and per-pane watches log rejections to a new `HerdrKitOptions.onLog` hook
|
|
29
|
+
instead of swallowing them.
|
|
30
|
+
- Depends on @byokit/relay 0.2.0.
|
|
31
|
+
- Depends on @byokit/link 0.3.2.
|
|
32
|
+
- K8 fake coverage (`./testing`): `fake.world` is the live mutable world the server answers from
|
|
33
|
+
(seeded ids via `world.seed`, seeded plugins via `world.plugins`); `workspace.rename`, `tab.rename`,
|
|
34
|
+
`pane.rename` and `agent.rename` (refusing bad names with `invalid_agent_name` and taken ones with
|
|
35
|
+
`agent_name_taken`); `plugin.list`; the boot-window `agent.start` reply both shapes read
|
|
36
|
+
(`{ type: 'agent_started', agent, argv }`); filtered emit with `watching()`, `snapshotCount()` and the
|
|
37
|
+
`holdSnapshot`/`failNextSnapshot`/`holdAck`/`failNextAck` hooks; real terminal frames
|
|
38
|
+
(`{ type: 'terminal.frame', full, bytes }` plus the `data` alias) with `terminal.resize`/`terminal.input`;
|
|
39
|
+
scroll state on `pane.get` with `pane.scroll`; and `api schema` printing the pinned v0.9.1 snapshot.
|
|
40
|
+
|
|
41
|
+
## 0.1.0 (2026-09-29)
|
|
5
42
|
- Opt-in pinned Herdr fetch (G11, `./binary`): `ensureHerdr({ dir, platform? })` (also spelled
|
|
6
43
|
`fetchHerdr`) downloads the pinned v0.9.1 asset for the current platform into an app-owned `dir`
|
|
7
44
|
only when called, verifies it against a committed per-platform sha256 table (linux x64/arm64,
|
|
8
45
|
darwin x64/arm64 — each hash recomputed over the official release asset), writes atomically, marks
|
|
9
46
|
executable, and returns the absolute `own`-mode `bin`. Never PATH, never `~/.local/bin`, nothing on
|
|
10
47
|
install or import; a hash mismatch (nothing left behind) or unsupported platform fails in plain words.
|
|
11
|
-
|
|
12
48
|
- K11 protocol range: `HerdrKitOptions` takes `protocolRange?: { min?: number; max?: number }`
|
|
13
49
|
(each bound defaults to the pinned `HERDR_PROTOCOL`). A server below the floor still fails
|
|
14
50
|
closed; a newer server is the steady state `needs-update`/`version` instead of a throw — the
|
|
15
51
|
snapshot installs and the kit stays usable — so a Herdr protocol bump does not take the host
|
|
16
52
|
down before the kit's pin moves.
|
|
17
|
-
- `herdrContract(make, { test })` accepts the runner's `test` (node:test's by default), so a vitest host suite can run the contract against its lab Herdr.
|
|
18
|
-
|
|
19
|
-
## 0.1.0 (2026-09-29)
|
|
20
53
|
- Typed device pass-through (G4, docs/runtime-kits.md 7.1/7.2): `herdrDevice(link).call<M>(method, params)` returns
|
|
21
54
|
`HerdrResult<M>` from the generated table, `events()` yields `HerdrLinkEvent` frames, and `./device` re-exports the
|
|
22
55
|
table types. New link ops `hd.kinds` (*view*, `agentKinds()`), `hd.wait` (control, pane in scope) and `hd.subscribe`
|
|
@@ -30,6 +63,7 @@
|
|
|
30
63
|
titles and labels merge in place with no re-bootstrap; a per-pane lifecycle epoch keeps a status
|
|
31
64
|
push that races a snapshot read (the live status and revision win over the read). The fake seeds
|
|
32
65
|
and serves every field (agent `title`, workspace `number`/`tokens`, `workspace.report_metadata`).
|
|
66
|
+
|
|
33
67
|
- `words`, `stateWords`, `agentWords` and `WORDS` are exported from `.` and `./device`, so a host or phone UI
|
|
34
68
|
shows the kit's own sentences for Herdr and agent states instead of writing its own.
|
|
35
69
|
- FIX `herdrDevice(link).events()` ends its link stream when the reader stops (`break`, or `return()` from a
|
package/README.md
CHANGED
|
@@ -25,10 +25,18 @@ Herdr.</sub></p>
|
|
|
25
25
|
</p>
|
|
26
26
|
<p align="center"><sub>The same example's host terminal (<code>host.ts</code>), started with <code>BYOKIT_EXAMPLE_FAKE=1</code> so it runs the kit's stand-in Herdr.</sub></p>
|
|
27
27
|
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install @byokit/herdr
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
[](https://www.npmjs.com/package/@byokit/herdr) · [Latest release](https://github.com/umeranjum17/byokit/releases?q=herdr-v) · [All releases](https://github.com/umeranjum17/byokit/releases)
|
|
35
|
+
|
|
28
36
|
## Quickstart
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
[`examples/herdr-kit`](../../examples/herdr-kit) does
|
|
38
|
+
Available on npm; it is also used from this repo, or from its packed packages as
|
|
39
|
+
[`examples/herdr-kit`](../../examples/herdr-kit) does:
|
|
32
40
|
|
|
33
41
|
```sh
|
|
34
42
|
npm install @byokit/herdr
|
package/dist/agents.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export type Call = (method: string, params: Record<string, unknown>, timeoutMs?:
|
|
|
4
4
|
export declare function createAgents(ctx: {
|
|
5
5
|
call: Call;
|
|
6
6
|
snapshot(): HerdrSnapshot;
|
|
7
|
+
reread(paneId: string): Promise<void>;
|
|
7
8
|
}): Pick<HerdrKit, 'startAgent' | 'prompt' | 'sendKeys' | 'wait' | 'read' | 'agentKinds' | 'installedAgentKinds'>;
|
package/dist/agents.js
CHANGED
|
@@ -109,11 +109,18 @@ export function createAgents(ctx) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
async function prompt(target, text, o) {
|
|
112
|
-
// The readiness gate reads the kit's own tree
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
// The readiness gate reads the kit's own tree, re-read once before a refusal: `launch_pending`
|
|
113
|
+
// rides reads only, so a snapshot taken mid-launch goes stale. `interactive_ready` is not gated:
|
|
114
|
+
// Herdr sets it only for agents its own `agent.start` settled, and its `agent.prompt` accepts the
|
|
115
|
+
// rest. A refusal never sends `agent.prompt`.
|
|
116
|
+
const ready = () => {
|
|
117
|
+
const agent = agentOf(ctx.snapshot(), target.paneId);
|
|
118
|
+
return agent !== undefined && agent.launchPending !== true && PROMPTABLE.includes(agent.status);
|
|
119
|
+
};
|
|
120
|
+
if (!ready()) {
|
|
121
|
+
await ctx.reread(target.paneId);
|
|
122
|
+
if (!ready())
|
|
123
|
+
throw fail('agent-not-ready', 'That agent is not ready for a prompt yet.');
|
|
117
124
|
}
|
|
118
125
|
const wait = o?.wait === undefined ? undefined
|
|
119
126
|
: { timeout_ms: o.wait.timeoutMs, ...(o.wait.until === undefined ? {} : { until: o.wait.until }) };
|
|
@@ -123,9 +130,14 @@ export function createAgents(ctx) {
|
|
|
123
130
|
...(wait === undefined ? {} : { wait }) });
|
|
124
131
|
}
|
|
125
132
|
catch (error) {
|
|
126
|
-
|
|
133
|
+
const code = error?.code;
|
|
134
|
+
if (code === 'agent_blocked') {
|
|
127
135
|
throw fail('agent-blocked', 'That agent is blocked and needs an answer before a new prompt.');
|
|
128
136
|
}
|
|
137
|
+
// Herdr's own gate (unknown agent kind, launch pending, not the pane's foreground process).
|
|
138
|
+
if (code === 'agent_not_ready') {
|
|
139
|
+
throw fail('agent-not-ready', 'That agent is not ready for a prompt yet.');
|
|
140
|
+
}
|
|
129
141
|
throw error;
|
|
130
142
|
}
|
|
131
143
|
const body = isObj(receipt) ? receipt : undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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, HerdrTransport, PromptReceipt, StartAgent, TerminalSession, } from './types.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';
|
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, PromptReceipt, StartAgent, TerminalSession } from './types.ts';
|
|
1
|
+
import type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, PromptReceipt, StartAgent, TerminalSession } from './types.ts';
|
|
2
2
|
export declare class HerdrKit {
|
|
3
3
|
private readonly supervisor;
|
|
4
4
|
private transport?;
|
|
@@ -20,8 +20,10 @@ export declare class HerdrKit {
|
|
|
20
20
|
get state(): HerdrState;
|
|
21
21
|
private publish;
|
|
22
22
|
private emitRaw;
|
|
23
|
+
private watchError;
|
|
23
24
|
private update;
|
|
24
25
|
private bootstrap;
|
|
26
|
+
private reread;
|
|
25
27
|
/**
|
|
26
28
|
* Connect and bootstrap. Non-fatal: a rejected `start()` (e.g. `failed/socket` while Herdr is
|
|
27
29
|
* down) leaves the kit stopped, and `start()` may be called again afterwards — the host comes up
|
|
@@ -32,7 +34,7 @@ export declare class HerdrKit {
|
|
|
32
34
|
call<M extends HerdrMethod>(method: M, params: HerdrParams<M>, o?: {
|
|
33
35
|
timeoutMs?: number;
|
|
34
36
|
}): Promise<HerdrResult<M>>;
|
|
35
|
-
subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void, onError?: (code: string, message: string) => void):
|
|
37
|
+
subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void, onError?: (code: string, message: string) => void): HerdrSubscribeStop;
|
|
36
38
|
cli(args: string[], o?: {
|
|
37
39
|
timeoutMs?: number;
|
|
38
40
|
}): Promise<{
|
package/dist/kit.js
CHANGED
|
@@ -131,7 +131,8 @@ export class HerdrKit {
|
|
|
131
131
|
constructor(o) {
|
|
132
132
|
this.o = o;
|
|
133
133
|
this.supervisor = new Supervisor(o, (s) => { this.current = s; o.onState?.(s); });
|
|
134
|
-
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot()
|
|
134
|
+
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot(),
|
|
135
|
+
reread: (paneId) => this.reread(paneId) });
|
|
135
136
|
this.blockedList = new Blocked({ call: this.callAny });
|
|
136
137
|
}
|
|
137
138
|
get state() { return this.current; }
|
|
@@ -139,6 +140,11 @@ export class HerdrKit {
|
|
|
139
140
|
fn(this.snapshot()); }
|
|
140
141
|
emitRaw(e) { for (const fn of [...this.rawListeners])
|
|
141
142
|
fn(e); }
|
|
143
|
+
// The kit's own watches report rejections here so the host keeps its invalid_subscription
|
|
144
|
+
// logging without tapping the transport.
|
|
145
|
+
watchError(where) {
|
|
146
|
+
return (code, message) => this.o.onLog?.(`herdr: ${where} subscription rejected: ${code}: ${message}`);
|
|
147
|
+
}
|
|
142
148
|
update(e) {
|
|
143
149
|
const raw = e;
|
|
144
150
|
// The pinned schema spells subscription kinds with dots (`pane.created`) while live
|
|
@@ -158,7 +164,9 @@ export class HerdrKit {
|
|
|
158
164
|
}
|
|
159
165
|
if (pane !== undefined && owner !== undefined &&
|
|
160
166
|
(name === 'pane.agent.status.changed' || name === 'pane.agent.detected' || name === 'pane.updated')) {
|
|
161
|
-
|
|
167
|
+
// `pane.updated` nests its record under `pane`; the status push is flat and its `agent` is
|
|
168
|
+
// the kind string (PaneAgentStatusChangedEvent), never the record to merge.
|
|
169
|
+
const a = typeof raw.pane === 'object' && raw.pane !== null ? raw.pane : raw;
|
|
162
170
|
mergePane(pane, a);
|
|
163
171
|
pane.agent = mergeAgent(pane.agent, a);
|
|
164
172
|
// Every asserted status stamps the pane's lifecycle epoch — even an unchanged one, since a
|
|
@@ -280,18 +288,48 @@ export class HerdrKit {
|
|
|
280
288
|
for (const pane of this.tree.workspaces.flatMap((w) => w.tabs).flatMap((t) => t.panes)) {
|
|
281
289
|
if (!pane.agent)
|
|
282
290
|
continue;
|
|
283
|
-
const stop = this.transport.subscribe([{ type: 'pane.agent_status_changed', pane_id: pane.id }], (e) => { this.emitRaw(e); this.update(e); }, ()
|
|
291
|
+
const stop = this.transport.subscribe([{ type: 'pane.agent_status_changed', pane_id: pane.id }], (e) => { this.emitRaw(e); this.update(e); }, this.watchError('status'));
|
|
284
292
|
this.stops.add(stop);
|
|
285
293
|
this.statusStops.add(stop);
|
|
286
294
|
const ready = stop.ready;
|
|
287
295
|
if (ready)
|
|
288
|
-
statusAcks.push(ready);
|
|
296
|
+
statusAcks.push(ready.then((ok) => (ok === false ? undefined : this.reread(pane.id, token))));
|
|
289
297
|
}
|
|
290
298
|
void Promise.allSettled(statusAcks).then(() => { if (token === this.generation)
|
|
291
299
|
resolveStatus(); });
|
|
292
300
|
this.current = newer ? { phase: 'needs-update', why: 'version' } : { phase: 'ready' };
|
|
293
301
|
this.o.onState?.(this.current);
|
|
294
302
|
}
|
|
303
|
+
// Re-read one agent and land its status, revision and launch flags: after a watch acks (a status
|
|
304
|
+
// change before the ack reached no socket) and before `prompt` refuses (`launch_pending` rides
|
|
305
|
+
// reads only, never a push). A push landing meanwhile is newer and wins status and revision (epoch
|
|
306
|
+
// guard); the launch flags still land, since no push carries them.
|
|
307
|
+
async reread(paneId, token = this.generation) {
|
|
308
|
+
const epoch = this.statusEpoch.get(paneId);
|
|
309
|
+
let made;
|
|
310
|
+
try {
|
|
311
|
+
made = await this.transport?.call('agent.get', { target: paneId });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const agent = made?.agent;
|
|
317
|
+
if (token !== this.generation || typeof agent?.agent_status !== 'string')
|
|
318
|
+
return;
|
|
319
|
+
// v0.9.1 leaves both flags out when false (`skip_serializing_if = is_false`): absent means false.
|
|
320
|
+
const flags = { launch_pending: agent.launch_pending === true, interactive_ready: agent.interactive_ready === true };
|
|
321
|
+
if (this.statusEpoch.get(paneId) !== epoch) {
|
|
322
|
+
const pane = findPane(this.tree, paneId);
|
|
323
|
+
if (pane?.agent !== undefined) {
|
|
324
|
+
pane.agent = mergeAgent(pane.agent, flags);
|
|
325
|
+
this.publish();
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// The read's other fields may be staler than a merged pane.updated.
|
|
330
|
+
this.update({ type: 'pane_agent_status_changed', pane_id: paneId, agent_status: agent.agent_status,
|
|
331
|
+
...(typeof agent.revision === 'number' ? { revision: agent.revision } : {}), ...flags });
|
|
332
|
+
}
|
|
295
333
|
/**
|
|
296
334
|
* Connect and bootstrap. Non-fatal: a rejected `start()` (e.g. `failed/socket` while Herdr is
|
|
297
335
|
* down) leaves the kit stopped, and `start()` may be called again afterwards — the host comes up
|
|
@@ -323,7 +361,7 @@ export class HerdrKit {
|
|
|
323
361
|
}
|
|
324
362
|
else
|
|
325
363
|
this.update(e);
|
|
326
|
-
}, ()
|
|
364
|
+
}, this.watchError('events'));
|
|
327
365
|
this.stops.add(stop);
|
|
328
366
|
try {
|
|
329
367
|
const control = stop;
|
package/dist/testing/contract.js
CHANGED
|
@@ -31,6 +31,14 @@ export function herdrContract(make, options) {
|
|
|
31
31
|
await made.fake?.stop().catch(() => { });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
// Transport doubles that never exercise the control surface still satisfy its type.
|
|
35
|
+
function ackedStop() {
|
|
36
|
+
return Object.assign(() => { }, {
|
|
37
|
+
ready: Promise.resolve(true),
|
|
38
|
+
onReconnect() { },
|
|
39
|
+
onDisconnect() { },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
34
42
|
function agentStatusOf(snapshot, paneId) {
|
|
35
43
|
let panes = 0;
|
|
36
44
|
let status;
|
|
@@ -70,7 +78,7 @@ export function herdrContract(make, options) {
|
|
|
70
78
|
};
|
|
71
79
|
const kit = withTransport({
|
|
72
80
|
call: async (method) => (method === 'session.snapshot' ? snapshot : { protocol: HERDR_PROTOCOL + 999 }),
|
|
73
|
-
subscribe: () => ()
|
|
81
|
+
subscribe: () => ackedStop(),
|
|
74
82
|
close: () => { },
|
|
75
83
|
});
|
|
76
84
|
try {
|
|
@@ -92,7 +100,7 @@ export function herdrContract(make, options) {
|
|
|
92
100
|
return t.skip('make provides no transport double');
|
|
93
101
|
const kit = withTransport({
|
|
94
102
|
call: async () => ({ protocol: HERDR_PROTOCOL - 1 }),
|
|
95
|
-
subscribe: () => ()
|
|
103
|
+
subscribe: () => ackedStop(),
|
|
96
104
|
close: () => { },
|
|
97
105
|
});
|
|
98
106
|
try {
|
|
@@ -120,7 +128,7 @@ export function herdrContract(make, options) {
|
|
|
120
128
|
};
|
|
121
129
|
const transport = {
|
|
122
130
|
call: async (method) => (method === 'session.snapshot' ? snapshot : { protocol: HERDR_PROTOCOL + 1 }),
|
|
123
|
-
subscribe: () => ()
|
|
131
|
+
subscribe: () => ackedStop(),
|
|
124
132
|
close: () => { },
|
|
125
133
|
};
|
|
126
134
|
const inRange = withTransport(transport);
|
|
@@ -162,7 +170,7 @@ export function herdrContract(make, options) {
|
|
|
162
170
|
call: async (method) => (method === 'session.snapshot' ? snapshot : { protocol: HERDR_PROTOCOL }),
|
|
163
171
|
subscribe: (_subs, on) => {
|
|
164
172
|
queueMicrotask(() => on({ type: 'pane_updated', pane: { pane_id: 'w1:p2', agent_status: 'working' } }));
|
|
165
|
-
return ()
|
|
173
|
+
return ackedStop();
|
|
166
174
|
},
|
|
167
175
|
close: () => { },
|
|
168
176
|
});
|
|
@@ -190,7 +198,7 @@ export function herdrContract(make, options) {
|
|
|
190
198
|
if (subs.some((s) => s.type === 'pane.agent_status_changed')) {
|
|
191
199
|
onError('invalid_subscription', 'pane.agent_status_changed needs a pane_id');
|
|
192
200
|
}
|
|
193
|
-
return ()
|
|
201
|
+
return ackedStop();
|
|
194
202
|
},
|
|
195
203
|
close: () => { },
|
|
196
204
|
});
|
|
@@ -200,9 +208,17 @@ export function herdrContract(make, options) {
|
|
|
200
208
|
// Deliberately schema-invalid: the pinned schema (6.7) requires pane_id for this filtered
|
|
201
209
|
// kind, so the server rejects the batch — the exact answer the transport double gives here.
|
|
202
210
|
const rejected = { type: 'pane.agent_status_changed' };
|
|
203
|
-
|
|
211
|
+
const errors = [];
|
|
212
|
+
const stop = kit.subscribe([rejected], () => { }, (code, message) => errors.push({ code, message }));
|
|
213
|
+
assert.equal(typeof stop, 'function');
|
|
214
|
+
assert.ok(stop.ready instanceof Promise, 'the stop carries the ack promise');
|
|
215
|
+
assert.equal(typeof stop.onReconnect, 'function');
|
|
216
|
+
assert.equal(typeof stop.onDisconnect, 'function');
|
|
204
217
|
await settle(1200); // longer than the 1 s reconnect delay a retry would use
|
|
218
|
+
assert.deepEqual(errors.map((e) => e.code), ['invalid_subscription'], 'the rejection reaches onError exactly once');
|
|
219
|
+
assert.match(errors[0].message, /pane_id/);
|
|
205
220
|
assert.equal(subscribeCalls, bootstrapCalls + 1, 'the rejected subscription is never retried');
|
|
221
|
+
stop();
|
|
206
222
|
}
|
|
207
223
|
finally {
|
|
208
224
|
await kit.stop().catch(() => { });
|
|
@@ -311,7 +327,7 @@ export function herdrContract(make, options) {
|
|
|
311
327
|
}
|
|
312
328
|
return { protocol: HERDR_PROTOCOL };
|
|
313
329
|
},
|
|
314
|
-
subscribe: () => ()
|
|
330
|
+
subscribe: () => ackedStop(),
|
|
315
331
|
close: () => { },
|
|
316
332
|
});
|
|
317
333
|
try {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// `api schema`, the `server` verb, JSON CLI verbs and `terminal session`. World state lives on the
|
|
3
3
|
// control socket; this process never speaks JSON-RPC except to forward a CLI verb. Ported from muxr
|
|
4
4
|
// `perf/fake-herdr/bin.mjs` with the perf byte rates and graphics probes removed.
|
|
5
|
-
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { createConnection } from 'node:net';
|
|
7
|
-
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
import { HERDR_PROTOCOL, HERDR_VERSION } from "../../constants.js";
|
|
10
10
|
const SELF = fileURLToPath(import.meta.url);
|
|
@@ -82,13 +82,20 @@ async function runServer() {
|
|
|
82
82
|
process.on('SIGTERM', stop);
|
|
83
83
|
process.on('SIGINT', stop);
|
|
84
84
|
}
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
85
|
+
// Terminal frames carry the real Herdr shape — `{ type: 'terminal.frame', pane_id, full, bytes }`
|
|
86
|
+
// with `bytes` the UTF-8 length of `full` — plus the fake's `data` alias (same string as `full`),
|
|
87
|
+
// so older assertions reading `data` keep passing. A `{"type":"terminal.resize",cols,rows}` line
|
|
88
|
+
// resizes the session and answers one empty frame stamped with the new size; `terminal.release`
|
|
89
|
+
// exits; `{"type":"fake.stream","count":n,"size":b,"progress":file}` streams n frames with full
|
|
90
|
+
// `<i>:` plus b bytes as fast as the pipe takes them, recording the frames-written count in
|
|
91
|
+
// `progress` after each write so a test can see how far a paused reader let the stream run.
|
|
92
|
+
function frame(paneId, full, extra = {}) {
|
|
93
|
+
return `${JSON.stringify({ type: 'terminal.frame', pane_id: paneId, data: full, full, bytes: Buffer.byteLength(full), ...extra })}\n`;
|
|
94
|
+
}
|
|
88
95
|
async function stream(paneId, o) {
|
|
89
96
|
const pad = 'x'.repeat(o.size ?? 1024);
|
|
90
97
|
for (let i = 0; i < (o.count ?? 0); i += 1) {
|
|
91
|
-
const ok = process.stdout.write(
|
|
98
|
+
const ok = process.stdout.write(frame(paneId, `${i}:${pad}`));
|
|
92
99
|
if (o.progress)
|
|
93
100
|
writeFileSync(o.progress, String(i + 1));
|
|
94
101
|
if (!ok)
|
|
@@ -98,12 +105,12 @@ async function stream(paneId, o) {
|
|
|
98
105
|
function runTerminal(args) {
|
|
99
106
|
// `herdr terminal session <control|observe> <pane> [--takeover] --cols <n> --rows <n>`.
|
|
100
107
|
// The fake needs no server for this: one ready frame, then every send line echoes back as an
|
|
101
|
-
// output frame (docs/runtime-kits.md 6.8), except `fake.stream`
|
|
108
|
+
// output frame (docs/runtime-kits.md 6.8), except `fake.stream` and `terminal.resize` above.
|
|
102
109
|
const mode = args[0] === 'observe' ? 'observe' : 'control';
|
|
103
110
|
const rest = args[0] === 'control' || args[0] === 'observe' ? args.slice(1) : args;
|
|
104
111
|
const paneId = rest[0] ?? 'w1:p1';
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
let cols = Number(flag(args, '--cols')) || 80;
|
|
113
|
+
let rows = Number(flag(args, '--rows')) || 24;
|
|
107
114
|
const takeover = args.includes('--takeover');
|
|
108
115
|
process.stdout.write(`${JSON.stringify({ type: 'terminal.ready', pane_id: paneId, mode, takeover, cols, rows })}\n`);
|
|
109
116
|
let buffer = '';
|
|
@@ -116,6 +123,7 @@ function runTerminal(args) {
|
|
|
116
123
|
if (line.trim() === '')
|
|
117
124
|
continue;
|
|
118
125
|
let data = line;
|
|
126
|
+
let extra = {};
|
|
119
127
|
try {
|
|
120
128
|
const message = JSON.parse(line);
|
|
121
129
|
if (message.type === 'terminal.release')
|
|
@@ -124,11 +132,20 @@ function runTerminal(args) {
|
|
|
124
132
|
void stream(paneId, message);
|
|
125
133
|
continue;
|
|
126
134
|
}
|
|
135
|
+
if (message.type === 'terminal.resize') {
|
|
136
|
+
if (typeof message.cols === 'number' && message.cols > 0)
|
|
137
|
+
cols = Math.floor(message.cols);
|
|
138
|
+
if (typeof message.rows === 'number' && message.rows > 0)
|
|
139
|
+
rows = Math.floor(message.rows);
|
|
140
|
+
process.stdout.write(frame(paneId, '', { cols, rows }));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// `terminal.input` carries the keys in `data`; anything else with a string `data` echoes it.
|
|
127
144
|
if (typeof message.data === 'string')
|
|
128
145
|
data = message.data;
|
|
129
146
|
}
|
|
130
147
|
catch { /* a raw line echoes as-is */ }
|
|
131
|
-
process.stdout.write(
|
|
148
|
+
process.stdout.write(frame(paneId, data, { cols, rows }));
|
|
132
149
|
}
|
|
133
150
|
});
|
|
134
151
|
process.stdin.on('end', () => process.exit(0));
|
|
@@ -186,8 +203,20 @@ export async function main(argv) {
|
|
|
186
203
|
return;
|
|
187
204
|
}
|
|
188
205
|
if (cmd === 'api' && sub === 'schema') {
|
|
189
|
-
//
|
|
190
|
-
|
|
206
|
+
// Prints the pinned v0.9.1 snapshot itself (schema/herdr-api-0.9.1.json, docs/runtime-kits.md
|
|
207
|
+
// 6.7) — the same bytes `scripts/gen-types.ts` generates the typed surface from — exactly as
|
|
208
|
+
// the real `herdr api schema` prints its bundled schema. Only when the snapshot file is
|
|
209
|
+
// unreachable (a packed install without it) does the shim fall back to the pinned identity.
|
|
210
|
+
try {
|
|
211
|
+
const snapshot = readFileSync(join(dirname(SELF), '../../../schema/herdr-api-0.9.1.json'), 'utf8');
|
|
212
|
+
const parsed = JSON.parse(snapshot);
|
|
213
|
+
if (parsed.protocol !== HERDR_PROTOCOL)
|
|
214
|
+
throw new Error('fake-herdr bin: schema snapshot drift');
|
|
215
|
+
process.stdout.write(snapshot.endsWith('\n') ? snapshot : `${snapshot}\n`);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
process.stdout.write(`${JSON.stringify({ protocol: HERDR_PROTOCOL, version: HERDR_VERSION })}\n`);
|
|
219
|
+
}
|
|
191
220
|
return;
|
|
192
221
|
}
|
|
193
222
|
if (cmd === 'server') {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { type FakeWorld } from './world.ts';
|
|
1
|
+
import { type FakePluginSeed, type FakeWorld } from './world.ts';
|
|
2
2
|
export type FakeHerdrOptions = {
|
|
3
3
|
dir: string;
|
|
4
4
|
socketPath?: string;
|
|
5
5
|
world?: {
|
|
6
6
|
cwd?: string;
|
|
7
7
|
kinds?: readonly string[];
|
|
8
|
+
seed?: number;
|
|
9
|
+
plugins?: readonly FakePluginSeed[];
|
|
8
10
|
};
|
|
9
11
|
agentStartFaults?: AgentStartFault[];
|
|
10
12
|
onStop?: () => void;
|
|
@@ -22,6 +24,13 @@ export type FakeHerdr = {
|
|
|
22
24
|
type: string;
|
|
23
25
|
} & Record<string, unknown>): void;
|
|
24
26
|
setStatus(paneId: string, status: string): void;
|
|
27
|
+
watching(): string[];
|
|
28
|
+
watching(paneId: string): boolean;
|
|
29
|
+
snapshotCount(): number;
|
|
30
|
+
holdSnapshot(): () => void;
|
|
31
|
+
failNextSnapshot(code?: string, message?: string): void;
|
|
32
|
+
holdAck(): () => void;
|
|
33
|
+
failNextAck(code?: string, message?: string): void;
|
|
25
34
|
subscriptionCount(): number;
|
|
26
35
|
stop(): Promise<void>;
|
|
27
36
|
};
|
|
@@ -7,9 +7,16 @@ import { createServer } from 'node:net';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { HERDR_PROTOCOL, HERDR_VERSION } from "../../constants.js";
|
|
9
9
|
import { writeBinShim } from "./bin.js";
|
|
10
|
-
import { agentRecord, createWorld,
|
|
10
|
+
import { agentRecord, createWorld, paneScroll, relayoutTab } from "./world.js";
|
|
11
11
|
const PROMPT_SETTLE_MS = 50; // 6.8: the agent settles 50 ms after a prompt
|
|
12
12
|
const WAIT_POLL_MS = 5;
|
|
13
|
+
// The subscription kinds that need a `pane_id` filter and ride their own socket each
|
|
14
|
+
// (src/generated/events.ts HerdrEventFilters), in the live underscore wire spelling —
|
|
15
|
+
// subscriptions arrive in either spelling and are normalized with wireName below. A batch
|
|
16
|
+
// holding one rejects the whole batch when the filter is missing or when it shares the
|
|
17
|
+
// batch with another kind.
|
|
18
|
+
const FILTERED = new Set(['pane_agent_status_changed', 'pane_output_matched', 'pane_scroll_changed']);
|
|
19
|
+
const AGENT_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
13
20
|
export async function startFakeHerdr(options) {
|
|
14
21
|
const dir = options.dir;
|
|
15
22
|
if (typeof dir !== 'string' || dir.length === 0)
|
|
@@ -19,10 +26,11 @@ export async function startFakeHerdr(options) {
|
|
|
19
26
|
const cwd = join(dir, 'project');
|
|
20
27
|
mkdirSync(cwd, { recursive: true });
|
|
21
28
|
const kinds = [...(options.world?.kinds ?? ['pi'])];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const seed = options.world?.seed ?? 0;
|
|
30
|
+
const live = Object.assign(createWorld({ cwd, kinds, plugins: options.world?.plugins }), {
|
|
31
|
+
nextPane: 3 + seed,
|
|
32
|
+
nextWorkspace: 2 + seed,
|
|
33
|
+
nextRevision: seed,
|
|
26
34
|
zoomed: false,
|
|
27
35
|
});
|
|
28
36
|
// Queued `agent.start` failures (K7): each start shifts one and fails with its code, so a test
|
|
@@ -31,9 +39,25 @@ export async function startFakeHerdr(options) {
|
|
|
31
39
|
const agentStartFaults = [...(options.agentStartFaults ?? [])];
|
|
32
40
|
const sockets = new Set();
|
|
33
41
|
const eventSubs = new Set();
|
|
34
|
-
const
|
|
42
|
+
const filteredSubs = new Set();
|
|
35
43
|
const timers = [];
|
|
36
44
|
let stopping = false;
|
|
45
|
+
let snapshots = 0;
|
|
46
|
+
let snapshotGates = [];
|
|
47
|
+
let nextSnapshotFailure;
|
|
48
|
+
let ackGates = [];
|
|
49
|
+
let nextAckFailure;
|
|
50
|
+
const gate = (list) => {
|
|
51
|
+
let release;
|
|
52
|
+
const promise = new Promise((resolve) => { release = resolve; });
|
|
53
|
+
const entry = { promise, release: () => { } };
|
|
54
|
+
entry.release = () => {
|
|
55
|
+
release();
|
|
56
|
+
list.splice(list.indexOf(entry), 1);
|
|
57
|
+
};
|
|
58
|
+
list.push(entry);
|
|
59
|
+
return entry.release;
|
|
60
|
+
};
|
|
37
61
|
const server = createServer((socket) => {
|
|
38
62
|
sockets.add(socket);
|
|
39
63
|
let buffer = '';
|
|
@@ -59,9 +83,9 @@ export async function startFakeHerdr(options) {
|
|
|
59
83
|
for (const sub of eventSubs)
|
|
60
84
|
if (sub.socket === socket)
|
|
61
85
|
eventSubs.delete(sub);
|
|
62
|
-
for (const sub of
|
|
86
|
+
for (const sub of filteredSubs)
|
|
63
87
|
if (sub.socket === socket)
|
|
64
|
-
|
|
88
|
+
filteredSubs.delete(sub);
|
|
65
89
|
});
|
|
66
90
|
socket.on('error', () => { });
|
|
67
91
|
});
|
|
@@ -70,7 +94,18 @@ export async function startFakeHerdr(options) {
|
|
|
70
94
|
const bin = writeBinShim({ dir, socketPath });
|
|
71
95
|
const methods = {
|
|
72
96
|
ping: () => ({ protocol: HERDR_PROTOCOL, version: HERDR_VERSION }),
|
|
73
|
-
'session.snapshot': () =>
|
|
97
|
+
'session.snapshot': async () => {
|
|
98
|
+
snapshots += 1;
|
|
99
|
+
for (const held of snapshotGates)
|
|
100
|
+
await held.promise;
|
|
101
|
+
if (stopping)
|
|
102
|
+
throw fail('error', 'the fake is stopping');
|
|
103
|
+
const failure = nextSnapshotFailure;
|
|
104
|
+
nextSnapshotFailure = undefined;
|
|
105
|
+
if (failure !== undefined)
|
|
106
|
+
throw fail(failure.code, failure.message);
|
|
107
|
+
return { type: 'session_snapshot', snapshot: snapshotOf() };
|
|
108
|
+
},
|
|
74
109
|
'workspace.list': () => ({ workspaces: live.workspaces.map((w) => workspaceView(w)) }),
|
|
75
110
|
'workspace.get': (p) => {
|
|
76
111
|
const workspace = live.workspaces.find((row) => row.workspace_id === p.workspace_id);
|
|
@@ -119,6 +154,16 @@ export async function startFakeHerdr(options) {
|
|
|
119
154
|
}
|
|
120
155
|
return {};
|
|
121
156
|
},
|
|
157
|
+
'workspace.rename': (p) => {
|
|
158
|
+
const workspace = live.workspaces.find((row) => row.workspace_id === p.workspace_id);
|
|
159
|
+
if (workspace === undefined)
|
|
160
|
+
throw fail('workspace_not_found', 'workspace not found');
|
|
161
|
+
if (typeof p.label !== 'string' || p.label.length === 0)
|
|
162
|
+
throw fail('invalid_label', 'a label is required');
|
|
163
|
+
workspace.label = p.label;
|
|
164
|
+
emitEvent('workspace.renamed', { workspace_id: workspace.workspace_id, label: workspace.label });
|
|
165
|
+
return { type: 'workspace_info', workspace: workspaceView(workspace) };
|
|
166
|
+
},
|
|
122
167
|
'worktree.create': (p) => {
|
|
123
168
|
const checkout = typeof p.cwd === 'string' && p.cwd.length > 0 ? p.cwd : join(live.cwd, `wt-${live.nextWorkspace}`);
|
|
124
169
|
const workspace = addWorkspace(checkout);
|
|
@@ -180,6 +225,16 @@ export async function startFakeHerdr(options) {
|
|
|
180
225
|
focusPane(pane.pane_id);
|
|
181
226
|
return {};
|
|
182
227
|
},
|
|
228
|
+
'tab.rename': (p) => {
|
|
229
|
+
const tab = live.tabs.find((row) => row.tab_id === p.tab_id);
|
|
230
|
+
if (tab === undefined)
|
|
231
|
+
throw fail('tab_not_found', 'tab not found');
|
|
232
|
+
if (typeof p.label !== 'string' || p.label.length === 0)
|
|
233
|
+
throw fail('invalid_label', 'a label is required');
|
|
234
|
+
tab.label = p.label;
|
|
235
|
+
emitEvent('tab.renamed', { tab_id: tab.tab_id, workspace_id: tab.workspace_id, label: tab.label });
|
|
236
|
+
return { type: 'tab_info', tab: tabView(tab) };
|
|
237
|
+
},
|
|
183
238
|
'pane.get': (p) => ({ pane: paneView(paneOf(p.pane_id)) }),
|
|
184
239
|
'pane.close': (p) => {
|
|
185
240
|
const pane = paneOf(p.pane_id);
|
|
@@ -223,6 +278,31 @@ export async function startFakeHerdr(options) {
|
|
|
223
278
|
}
|
|
224
279
|
return {};
|
|
225
280
|
},
|
|
281
|
+
'pane.rename': (p) => {
|
|
282
|
+
const pane = paneOf(p.pane_id);
|
|
283
|
+
// A null label clears back to the shell default; a rename never moves the revision,
|
|
284
|
+
// so an open approval question stays answerable at its recorded revision.
|
|
285
|
+
if (p.label !== undefined && p.label !== null) {
|
|
286
|
+
if (typeof p.label !== 'string' || p.label.length === 0)
|
|
287
|
+
throw fail('invalid_label', 'a label is required');
|
|
288
|
+
pane.label = p.label;
|
|
289
|
+
}
|
|
290
|
+
else if (p.label === null) {
|
|
291
|
+
pane.label = 'zsh';
|
|
292
|
+
}
|
|
293
|
+
return { type: 'pane_info', pane: paneView(pane) };
|
|
294
|
+
},
|
|
295
|
+
'pane.scroll': (p) => {
|
|
296
|
+
const pane = paneOf(p.pane_id);
|
|
297
|
+
const offset = typeof p.offset_from_bottom === 'number' && Number.isSafeInteger(p.offset_from_bottom)
|
|
298
|
+
? Math.max(0, Math.min(pane.scroll.max_offset_from_bottom, p.offset_from_bottom))
|
|
299
|
+
: pane.scroll.offset_from_bottom;
|
|
300
|
+
pane.scroll = { ...pane.scroll, offset_from_bottom: offset };
|
|
301
|
+
emitEvent('pane.scroll_changed', {
|
|
302
|
+
pane_id: pane.pane_id, workspace_id: pane.workspace_id, scroll: { ...pane.scroll },
|
|
303
|
+
});
|
|
304
|
+
return { type: 'pane_info', pane: paneView(pane) };
|
|
305
|
+
},
|
|
226
306
|
'pane.zoom': (p) => {
|
|
227
307
|
paneOf(p.pane_id);
|
|
228
308
|
const mode = p.mode ?? 'toggle';
|
|
@@ -245,15 +325,37 @@ export async function startFakeHerdr(options) {
|
|
|
245
325
|
pane.terminal_title_stripped = pane.terminal_title;
|
|
246
326
|
const agent = agentRecord({
|
|
247
327
|
paneId: pane.pane_id, tabId: pane.tab_id, workspaceId: pane.workspace_id, cwd: pane.cwd,
|
|
248
|
-
name, kind, revision: ++live.nextRevision,
|
|
328
|
+
name, kind, revision: ++live.nextRevision, focused: pane.focused,
|
|
249
329
|
});
|
|
250
330
|
const index = live.agents.findIndex((row) => row.pane_id === pane.pane_id);
|
|
251
331
|
if (index === -1)
|
|
252
332
|
live.agents.push(agent);
|
|
253
333
|
else
|
|
254
334
|
live.agents[index] = agent;
|
|
335
|
+
pane.revision = agent.revision;
|
|
255
336
|
emitEvent('pane.agent_detected', { pane_id: pane.pane_id, workspace_id: pane.workspace_id, agent: kind });
|
|
256
|
-
|
|
337
|
+
// The boot-window reply: the 0.8 shape muxr reads (`agent`) plus the pinned 0.9.1 envelope
|
|
338
|
+
// (`type`/`argv`), so a launch flow keeps its assertions against either shape.
|
|
339
|
+
return { type: 'agent_started', agent, argv: Array.isArray(p.args) ? p.args : [] };
|
|
340
|
+
},
|
|
341
|
+
'agent.rename': (p) => {
|
|
342
|
+
const agent = live.agents.find((row) => row.pane_id === p.target);
|
|
343
|
+
if (agent === undefined)
|
|
344
|
+
throw fail('agent_not_found', 'agent not found');
|
|
345
|
+
const name = p.name;
|
|
346
|
+
if (name !== undefined && name !== null) {
|
|
347
|
+
if (typeof name !== 'string' || !AGENT_NAME.test(name)) {
|
|
348
|
+
throw fail('invalid_agent_name', 'an agent name matches /^[a-z][a-z0-9_-]{0,31}$/');
|
|
349
|
+
}
|
|
350
|
+
if (live.agents.some((row) => row.pane_id !== agent.pane_id && row.name === name)) {
|
|
351
|
+
throw fail('agent_name_taken', `another agent is already named ${name}`);
|
|
352
|
+
}
|
|
353
|
+
const pane = live.panes.find((row) => row.pane_id === agent.pane_id);
|
|
354
|
+
if (pane !== undefined && pane.label === agent.name)
|
|
355
|
+
pane.label = name;
|
|
356
|
+
agent.name = name;
|
|
357
|
+
}
|
|
358
|
+
return { type: 'agent_info', agent: { ...agent } };
|
|
257
359
|
},
|
|
258
360
|
'agent.prompt': (p) => {
|
|
259
361
|
const pane = paneOf(p.target);
|
|
@@ -321,14 +423,21 @@ export async function startFakeHerdr(options) {
|
|
|
321
423
|
}
|
|
322
424
|
return { target: p.target, keys };
|
|
323
425
|
},
|
|
324
|
-
'agent.list': () => ({ agents: live.agents.map(
|
|
426
|
+
'agent.list': () => ({ agents: live.agents.map(agentWire) }),
|
|
325
427
|
'agent.get': (p) => {
|
|
326
428
|
const agent = live.agents.find((row) => row.pane_id === p.target);
|
|
327
429
|
if (agent === undefined)
|
|
328
430
|
throw fail('agent_not_found', 'agent not found');
|
|
329
|
-
return { agent };
|
|
431
|
+
return { agent: agentWire(agent) };
|
|
330
432
|
},
|
|
331
433
|
'server.agent_manifests': () => ({ manifests: kinds.map((kind) => ({ agent: kind })) }),
|
|
434
|
+
'plugin.list': (p) => {
|
|
435
|
+
const plugins = live.plugins ?? [];
|
|
436
|
+
const wanted = typeof p.plugin_id === 'string' ? p.plugin_id : undefined;
|
|
437
|
+
const list = (wanted === undefined ? plugins : plugins.filter((row) => row.plugin_id === wanted))
|
|
438
|
+
.map((row) => ({ ...row }));
|
|
439
|
+
return { type: 'plugin_list', plugins: list };
|
|
440
|
+
},
|
|
332
441
|
'server.stop': () => {
|
|
333
442
|
timers.push(setTimeout(() => { void shutdown(); }, 0));
|
|
334
443
|
return {};
|
|
@@ -341,11 +450,17 @@ export async function startFakeHerdr(options) {
|
|
|
341
450
|
for (const timer of timers)
|
|
342
451
|
clearTimeout(timer);
|
|
343
452
|
timers.length = 0;
|
|
453
|
+
for (const held of snapshotGates)
|
|
454
|
+
held.release();
|
|
455
|
+
snapshotGates = [];
|
|
456
|
+
for (const held of ackGates)
|
|
457
|
+
held.release();
|
|
458
|
+
ackGates = [];
|
|
344
459
|
for (const socket of sockets)
|
|
345
460
|
socket.destroy();
|
|
346
461
|
sockets.clear();
|
|
347
462
|
eventSubs.clear();
|
|
348
|
-
|
|
463
|
+
filteredSubs.clear();
|
|
349
464
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
350
465
|
unlinkQuiet(socketPath);
|
|
351
466
|
options.onStop?.();
|
|
@@ -390,29 +505,55 @@ export async function startFakeHerdr(options) {
|
|
|
390
505
|
const wireName = (type) => type.replace(/\./g, '_');
|
|
391
506
|
function subscribe(socket, id, params) {
|
|
392
507
|
const subs = Array.isArray(params.subscriptions) ? params.subscriptions : [];
|
|
393
|
-
const
|
|
508
|
+
const ackFailure = nextAckFailure;
|
|
509
|
+
nextAckFailure = undefined;
|
|
510
|
+
const invalid = (message, code = 'invalid_subscription') => {
|
|
394
511
|
// A rejected subscribe answers `id: ""` once and the server closes; the client never retries it.
|
|
395
|
-
writeJson(socket, { id: '', error: { code
|
|
512
|
+
writeJson(socket, { id: '', error: { code, message } });
|
|
396
513
|
socket.end();
|
|
397
514
|
};
|
|
515
|
+
if (ackFailure !== undefined) {
|
|
516
|
+
invalid(ackFailure.message, ackFailure.code);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
398
519
|
for (const s of subs) {
|
|
399
520
|
if (typeof s?.type !== 'string')
|
|
400
521
|
return invalid('every subscription needs a type');
|
|
401
|
-
|
|
402
|
-
|
|
522
|
+
const wire = wireName(s.type);
|
|
523
|
+
if (FILTERED.has(wire) && typeof s.pane_id !== 'string') {
|
|
524
|
+
return invalid(`${s.type} needs a pane_id`);
|
|
525
|
+
}
|
|
526
|
+
if (wire === 'pane_output_matched'
|
|
527
|
+
&& (typeof s.match !== 'object' || s.match === null || typeof s.source !== 'string')) {
|
|
528
|
+
return invalid('pane.output_matched needs a pane_id, a match and a source');
|
|
403
529
|
}
|
|
404
530
|
}
|
|
405
|
-
const filtered = subs.filter((s) => typeof s.type === 'string' && wireName(s.type)
|
|
406
|
-
if (filtered.length > 0 && filtered.length !== subs.length) {
|
|
407
|
-
return invalid('pane.agent_status_changed cannot share a batch');
|
|
408
|
-
}
|
|
409
|
-
writeJson(socket, { id, result: { type: 'subscribed' } });
|
|
531
|
+
const filtered = subs.filter((s) => typeof s.type === 'string' && FILTERED.has(wireName(s.type)));
|
|
410
532
|
if (filtered.length > 0) {
|
|
411
|
-
|
|
412
|
-
|
|
533
|
+
const kinds = new Set(filtered.map((s) => wireName(s.type)));
|
|
534
|
+
if (kinds.size > 1 || filtered.length !== subs.length) {
|
|
535
|
+
return invalid(`${filtered[0].type} cannot share a batch`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const held = [...ackGates];
|
|
539
|
+
const answer = () => {
|
|
540
|
+
writeJson(socket, { id, result: { type: 'subscribed' } });
|
|
541
|
+
if (filtered.length > 0) {
|
|
542
|
+
for (const s of filtered) {
|
|
543
|
+
filteredSubs.add({ socket, type: wireName(s.type), paneId: s.pane_id });
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
eventSubs.add({ socket, kinds: new Set(subs.map((s) => wireName(s.type))) });
|
|
548
|
+
};
|
|
549
|
+
if (held.length === 0) {
|
|
550
|
+
answer();
|
|
413
551
|
return;
|
|
414
552
|
}
|
|
415
|
-
|
|
553
|
+
void Promise.all(held.map((h) => h.promise)).then(() => {
|
|
554
|
+
if (!socket.destroyed)
|
|
555
|
+
answer();
|
|
556
|
+
});
|
|
416
557
|
}
|
|
417
558
|
// Payloads follow the pinned schema's event data (src/generated/events.ts).
|
|
418
559
|
function emitEvent(type, data) {
|
|
@@ -428,12 +569,28 @@ export async function startFakeHerdr(options) {
|
|
|
428
569
|
}
|
|
429
570
|
}
|
|
430
571
|
function emitStatus(paneId, agentStatus, revision) {
|
|
572
|
+
const agent = live.agents.find((row) => row.pane_id === paneId);
|
|
431
573
|
const frame = `${JSON.stringify({
|
|
432
574
|
event: 'pane_agent_status_changed',
|
|
433
|
-
|
|
575
|
+
// Schema-shaped (PaneAgentStatusChangedEvent): the agent kind rides as a plain `agent`
|
|
576
|
+
// string beside `workspace_id`, exactly like the real server's frame.
|
|
577
|
+
data: { type: 'pane_agent_status_changed', pane_id: paneId, workspace_id: agent?.workspace_id,
|
|
578
|
+
agent: agent?.agent, display_agent: agent?.display_agent, agent_status: agentStatus, revision },
|
|
434
579
|
})}\n`;
|
|
435
|
-
for (const sub of
|
|
436
|
-
if (sub.paneId !== paneId)
|
|
580
|
+
for (const sub of filteredSubs) {
|
|
581
|
+
if (sub.type !== 'pane_agent_status_changed' || sub.paneId !== paneId)
|
|
582
|
+
continue;
|
|
583
|
+
try {
|
|
584
|
+
sub.socket.write(frame);
|
|
585
|
+
}
|
|
586
|
+
catch { /* closed mid-emit */ }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Filtered emit: a frame for a filtered kind reaches only the sockets watching that pane.
|
|
590
|
+
function emitFiltered(type, paneId, data) {
|
|
591
|
+
const frame = `${JSON.stringify({ event: type, data: { type, ...data } })}\n`;
|
|
592
|
+
for (const sub of filteredSubs) {
|
|
593
|
+
if (sub.type !== type || sub.paneId !== paneId)
|
|
437
594
|
continue;
|
|
438
595
|
try {
|
|
439
596
|
sub.socket.write(frame);
|
|
@@ -450,10 +607,15 @@ export async function startFakeHerdr(options) {
|
|
|
450
607
|
function agentOf(pane) {
|
|
451
608
|
return live.agents.find((row) => row.pane_id === pane.pane_id);
|
|
452
609
|
}
|
|
610
|
+
// v0.9.1 serializes `launch_pending`/`interactive_ready` only when true (`skip_serializing_if = is_false`).
|
|
611
|
+
function agentWire(agent) {
|
|
612
|
+
const { launch_pending, interactive_ready, ...rest } = agent;
|
|
613
|
+
return { ...rest, ...(launch_pending ? { launch_pending } : {}), ...(interactive_ready ? { interactive_ready } : {}) };
|
|
614
|
+
}
|
|
453
615
|
function agentView(paneId) {
|
|
454
616
|
const agent = live.agents.find((row) => row.pane_id === paneId);
|
|
455
617
|
if (agent !== undefined)
|
|
456
|
-
return { agent:
|
|
618
|
+
return { agent: agentWire(agent) };
|
|
457
619
|
const pane = live.panes.find((row) => row.pane_id === paneId);
|
|
458
620
|
return { agent: { pane_id: paneId, agent_status: pane?.agent_status ?? 'unknown', interactive_ready: true } };
|
|
459
621
|
}
|
|
@@ -463,6 +625,7 @@ export async function startFakeHerdr(options) {
|
|
|
463
625
|
if (agent !== undefined) {
|
|
464
626
|
agent.agent_status = status;
|
|
465
627
|
agent.revision = ++live.nextRevision;
|
|
628
|
+
pane.revision = agent.revision;
|
|
466
629
|
// A blocked agent always has detection text (6.4 reads it as the question), whichever path
|
|
467
630
|
// got it there — `agent.prompt` sets it first, `setStatus` arrives with none.
|
|
468
631
|
if (status === 'blocked' && agent.detection === undefined)
|
|
@@ -495,10 +658,13 @@ export async function startFakeHerdr(options) {
|
|
|
495
658
|
}
|
|
496
659
|
function snapshotOf() {
|
|
497
660
|
return {
|
|
661
|
+
protocol: HERDR_PROTOCOL,
|
|
662
|
+
version: HERDR_VERSION,
|
|
498
663
|
workspaces: live.workspaces.map(workspaceView),
|
|
499
664
|
tabs: live.tabs.map((tab) => ({ ...tab })),
|
|
500
665
|
panes: live.panes.map(paneView),
|
|
501
|
-
agents: live.agents.map(
|
|
666
|
+
agents: live.agents.map(agentWire),
|
|
667
|
+
layouts: [],
|
|
502
668
|
};
|
|
503
669
|
}
|
|
504
670
|
function addWorkspace(labelText) {
|
|
@@ -546,6 +712,8 @@ export async function startFakeHerdr(options) {
|
|
|
546
712
|
tokens: {},
|
|
547
713
|
rect: { x: 0, y: 0, width: 80, height: 24 },
|
|
548
714
|
text: ['ready.'],
|
|
715
|
+
revision: 0,
|
|
716
|
+
scroll: paneScroll(),
|
|
549
717
|
};
|
|
550
718
|
live.panes.push(pane);
|
|
551
719
|
relayoutTab(live, tabId);
|
|
@@ -616,11 +784,21 @@ export async function startFakeHerdr(options) {
|
|
|
616
784
|
return {
|
|
617
785
|
socketPath,
|
|
618
786
|
bin,
|
|
619
|
-
world:
|
|
787
|
+
world: live,
|
|
620
788
|
agentStartFaults,
|
|
621
789
|
emit(event) {
|
|
622
|
-
|
|
623
|
-
|
|
790
|
+
// Filtered kinds route to the sockets watching that pane only; every other kind
|
|
791
|
+
// broadcasts to the batch subscriptions holding it. Either spelling is accepted and
|
|
792
|
+
// frames ride the live underscore wire spelling. Emitting never mutates the world —
|
|
793
|
+
// `setStatus` and the `pane.*` methods are the state changes; this only routes frames.
|
|
794
|
+
const wire = wireName(event.type);
|
|
795
|
+
if (FILTERED.has(wire) && typeof event.pane_id === 'string') {
|
|
796
|
+
if (wire === 'pane_agent_status_changed') {
|
|
797
|
+
emitStatus(event.pane_id, String(event.agent_status ?? 'unknown'), Number(event.revision ?? 0));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const { type: _type, ...data } = event;
|
|
801
|
+
emitFiltered(wire, event.pane_id, data);
|
|
624
802
|
return;
|
|
625
803
|
}
|
|
626
804
|
const { type, ...data } = event;
|
|
@@ -631,11 +809,32 @@ export async function startFakeHerdr(options) {
|
|
|
631
809
|
if (pane !== undefined)
|
|
632
810
|
transition(pane, status);
|
|
633
811
|
},
|
|
812
|
+
watching: ((paneId) => {
|
|
813
|
+
const watched = [...new Set([...filteredSubs].map((sub) => sub.paneId))].sort();
|
|
814
|
+
if (paneId === undefined)
|
|
815
|
+
return watched;
|
|
816
|
+
return watched.includes(paneId);
|
|
817
|
+
}),
|
|
818
|
+
snapshotCount() {
|
|
819
|
+
return snapshots;
|
|
820
|
+
},
|
|
821
|
+
holdSnapshot() {
|
|
822
|
+
return gate(snapshotGates);
|
|
823
|
+
},
|
|
824
|
+
failNextSnapshot(code = 'error', message = 'the next snapshot fails') {
|
|
825
|
+
nextSnapshotFailure = { code, message };
|
|
826
|
+
},
|
|
827
|
+
holdAck() {
|
|
828
|
+
return gate(ackGates);
|
|
829
|
+
},
|
|
830
|
+
failNextAck(code = 'invalid_subscription', message = 'the next subscribe is rejected') {
|
|
831
|
+
nextAckFailure = { code, message };
|
|
832
|
+
},
|
|
634
833
|
subscriptionCount() {
|
|
635
834
|
const held = new Set();
|
|
636
835
|
for (const sub of eventSubs)
|
|
637
836
|
held.add(sub.socket);
|
|
638
|
-
for (const sub of
|
|
837
|
+
for (const sub of filteredSubs)
|
|
639
838
|
held.add(sub.socket);
|
|
640
839
|
return held.size;
|
|
641
840
|
},
|
|
@@ -655,6 +854,9 @@ function paneView(pane) {
|
|
|
655
854
|
terminal_title_stripped: pane.terminal_title_stripped,
|
|
656
855
|
focused: pane.focused,
|
|
657
856
|
...(pane.env === undefined ? {} : { env: { ...pane.env } }),
|
|
857
|
+
terminal_id: `term-${pane.pane_id}`,
|
|
858
|
+
revision: pane.revision,
|
|
859
|
+
scroll: { ...pane.scroll },
|
|
658
860
|
...(Object.keys(pane.tokens).length === 0 ? {} : { tokens: pane.tokens }),
|
|
659
861
|
};
|
|
660
862
|
}
|
|
@@ -4,6 +4,11 @@ export type FakeRect = {
|
|
|
4
4
|
width: number;
|
|
5
5
|
height: number;
|
|
6
6
|
};
|
|
7
|
+
export type FakeScroll = {
|
|
8
|
+
offset_from_bottom: number;
|
|
9
|
+
max_offset_from_bottom: number;
|
|
10
|
+
viewport_rows: number;
|
|
11
|
+
};
|
|
7
12
|
export type FakePane = {
|
|
8
13
|
pane_id: string;
|
|
9
14
|
tab_id: string;
|
|
@@ -19,6 +24,8 @@ export type FakePane = {
|
|
|
19
24
|
env?: Record<string, string>;
|
|
20
25
|
rect: FakeRect;
|
|
21
26
|
text: string[];
|
|
27
|
+
revision: number;
|
|
28
|
+
scroll: FakeScroll;
|
|
22
29
|
};
|
|
23
30
|
export type FakeAgent = {
|
|
24
31
|
pane_id: string;
|
|
@@ -34,6 +41,8 @@ export type FakeAgent = {
|
|
|
34
41
|
interactive_ready: boolean;
|
|
35
42
|
launch_pending: boolean;
|
|
36
43
|
revision: number;
|
|
44
|
+
terminal_id: string;
|
|
45
|
+
focused: boolean;
|
|
37
46
|
detection?: string;
|
|
38
47
|
agent_session: {
|
|
39
48
|
source: string;
|
|
@@ -42,6 +51,20 @@ export type FakeAgent = {
|
|
|
42
51
|
value: string;
|
|
43
52
|
};
|
|
44
53
|
};
|
|
54
|
+
export type FakePluginSeed = {
|
|
55
|
+
plugin_id: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
version?: string;
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type FakePlugin = {
|
|
61
|
+
plugin_id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
version: string;
|
|
64
|
+
manifest_path: string;
|
|
65
|
+
plugin_root: string;
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
};
|
|
45
68
|
export type FakeTab = {
|
|
46
69
|
tab_id: string;
|
|
47
70
|
workspace_id: string;
|
|
@@ -67,6 +90,7 @@ export type FakeWorld = {
|
|
|
67
90
|
tabs: FakeTab[];
|
|
68
91
|
panes: FakePane[];
|
|
69
92
|
agents: FakeAgent[];
|
|
93
|
+
plugins: FakePlugin[];
|
|
70
94
|
};
|
|
71
95
|
export declare const COLS = 80;
|
|
72
96
|
export declare const ROWS = 24;
|
|
@@ -79,10 +103,17 @@ export declare function agentRecord(o: {
|
|
|
79
103
|
name: string;
|
|
80
104
|
kind: string;
|
|
81
105
|
revision: number;
|
|
106
|
+
focused?: boolean;
|
|
82
107
|
}): FakeAgent;
|
|
108
|
+
export declare function pluginRecord(o: {
|
|
109
|
+
dir: string;
|
|
110
|
+
seed: FakePluginSeed;
|
|
111
|
+
}): FakePlugin;
|
|
112
|
+
export declare function paneScroll(): FakeScroll;
|
|
83
113
|
export declare function createWorld(o: {
|
|
84
114
|
cwd: string;
|
|
85
115
|
kinds: readonly string[];
|
|
116
|
+
plugins?: readonly FakePluginSeed[];
|
|
86
117
|
}): FakeWorld;
|
|
87
118
|
export declare function relayoutTab(world: FakeWorld, tabId: string): void;
|
|
88
119
|
export declare function freezeWorld(world: FakeWorld): FakeWorld;
|
|
@@ -29,9 +29,24 @@ export function agentRecord(o) {
|
|
|
29
29
|
interactive_ready: true,
|
|
30
30
|
launch_pending: false,
|
|
31
31
|
revision: o.revision,
|
|
32
|
+
terminal_id: `term-${o.paneId}`,
|
|
33
|
+
focused: o.focused ?? false,
|
|
32
34
|
agent_session: { source: `herdr:${o.kind}`, agent: o.kind, kind: 'id', value: `gen-${o.paneId}` },
|
|
33
35
|
};
|
|
34
36
|
}
|
|
37
|
+
export function pluginRecord(o) {
|
|
38
|
+
return {
|
|
39
|
+
plugin_id: o.seed.plugin_id,
|
|
40
|
+
name: o.seed.name ?? o.seed.plugin_id,
|
|
41
|
+
version: o.seed.version ?? '0.0.0',
|
|
42
|
+
manifest_path: `${o.dir}/plugins/${o.seed.plugin_id}/herdr.plugin.json`,
|
|
43
|
+
plugin_root: `${o.dir}/plugins/${o.seed.plugin_id}`,
|
|
44
|
+
enabled: o.seed.enabled ?? true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function paneScroll() {
|
|
48
|
+
return { offset_from_bottom: 0, max_offset_from_bottom: 0, viewport_rows: ROWS };
|
|
49
|
+
}
|
|
35
50
|
export function createWorld(o) {
|
|
36
51
|
const workspace = {
|
|
37
52
|
workspace_id: 'w1',
|
|
@@ -61,6 +76,8 @@ export function createWorld(o) {
|
|
|
61
76
|
tokens: {},
|
|
62
77
|
rect: paneRect(0, 2),
|
|
63
78
|
text: ['ready.'],
|
|
79
|
+
revision: 0,
|
|
80
|
+
scroll: paneScroll(),
|
|
64
81
|
};
|
|
65
82
|
const kind = o.kinds[0] ?? 'pi';
|
|
66
83
|
const agentPane = {
|
|
@@ -72,12 +89,15 @@ export function createWorld(o) {
|
|
|
72
89
|
label: kind,
|
|
73
90
|
terminal_title: `${kind} · ${kind}`,
|
|
74
91
|
terminal_title_stripped: `${kind} · ${kind}`,
|
|
92
|
+
revision: 0,
|
|
93
|
+
scroll: paneScroll(), // its own scroll: the spread would alias the shell's object
|
|
75
94
|
};
|
|
76
95
|
const agent = agentRecord({
|
|
77
96
|
paneId: agentPane.pane_id, tabId: tab.tab_id, workspaceId: 'w1', cwd: o.cwd,
|
|
78
97
|
name: kind, kind, revision: 0,
|
|
79
98
|
});
|
|
80
|
-
|
|
99
|
+
const plugins = (o.plugins ?? []).map((seed) => pluginRecord({ dir: o.cwd, seed }));
|
|
100
|
+
return { cwd: o.cwd, workspaces: [workspace], tabs: [tab], panes: [shell, agentPane], agents: [agent], plugins };
|
|
81
101
|
}
|
|
82
102
|
export function relayoutTab(world, tabId) {
|
|
83
103
|
const panes = world.panes.filter((pane) => pane.tab_id === tabId);
|
package/dist/testing/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// The kit's fake Herdr and contract suite (docs/runtime-kits.md 6.8). Built in H6; H3/H5 run the
|
|
2
2
|
// contract against the fake, H9 against the real pinned Herdr. Tests import this by relative path —
|
|
3
3
|
// the package's public exports stay the kit surface.
|
|
4
|
+
//
|
|
5
|
+
// The fake's documented test handle is `FakeHerdr`: `fake.world` is the LIVE world the server
|
|
6
|
+
// answers from (mutable, with seeded ids from `world.seed`), `fake.emit` routes frames to the
|
|
7
|
+
// sockets watching them (filtered kinds reach only their pane's watchers), `fake.watching()`
|
|
8
|
+
// lists the watched panes, `fake.snapshotCount()` counts `session.snapshot` calls, and the
|
|
9
|
+
// `holdSnapshot`/`failNextSnapshot`/`holdAck`/`failNextAck` hooks script slow or failing answers.
|
|
10
|
+
// The bin shim speaks real terminal frames (`{ type: 'terminal.frame', full, bytes }`, plus a
|
|
11
|
+
// `data` alias) with `terminal.resize`/`terminal.input` on stdin, and `api schema` prints the
|
|
12
|
+
// pinned v0.9.1 snapshot.
|
|
4
13
|
export { startFakeHerdr } from "./fake-herdr/server.js";
|
|
5
14
|
export { writeBinShim, main as fakeHerdrBinMain } from "./fake-herdr/bin.js";
|
|
6
15
|
export { createWorld } from "./fake-herdr/world.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type HerdrKitOptions = {
|
|
|
15
15
|
transport?: HerdrTransport;
|
|
16
16
|
protocolRange?: HerdrProtocolRange;
|
|
17
17
|
onState?: (s: HerdrState) => void;
|
|
18
|
+
onLog?: (message: string) => void;
|
|
18
19
|
} | {
|
|
19
20
|
mode: 'own';
|
|
20
21
|
bin: string;
|
|
@@ -24,13 +25,19 @@ export type HerdrKitOptions = {
|
|
|
24
25
|
transport?: HerdrTransport;
|
|
25
26
|
protocolRange?: HerdrProtocolRange;
|
|
26
27
|
onState?: (s: HerdrState) => void;
|
|
28
|
+
onLog?: (message: string) => void;
|
|
29
|
+
};
|
|
30
|
+
export type HerdrSubscribeStop = (() => void) & {
|
|
31
|
+
ready: Promise<boolean>;
|
|
32
|
+
onReconnect(fn: () => void): void;
|
|
33
|
+
onDisconnect(fn: () => void): void;
|
|
27
34
|
};
|
|
28
35
|
export interface HerdrTransport {
|
|
29
36
|
call(method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
30
37
|
subscribe(subs: {
|
|
31
38
|
type: string;
|
|
32
39
|
[k: string]: unknown;
|
|
33
|
-
}[], on: (e: HerdrEvent) => void, onError: (code: string, message: string) => void):
|
|
40
|
+
}[], on: (e: HerdrEvent) => void, onError: (code: string, message: string) => void): HerdrSubscribeStop;
|
|
34
41
|
close(): void;
|
|
35
42
|
}
|
|
36
43
|
export type HerdrEvent = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/herdr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,8 +52,8 @@
|
|
|
52
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@byokit/link": "0.3.
|
|
56
|
-
"@byokit/relay": "0.1
|
|
55
|
+
"@byokit/link": "0.3.2",
|
|
56
|
+
"@byokit/relay": "0.2.1",
|
|
57
57
|
"@byokit/reach": "0.2.0",
|
|
58
58
|
"@byokit/seal": "0.1.0",
|
|
59
59
|
"ws": "8.21.3"
|