@byokit/herdr 0.7.3 → 0.8.1
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 +32 -0
- package/README.md +10 -3
- package/dist/accounts.d.ts +2 -1
- package/dist/accounts.js +34 -29
- package/dist/agents.js +60 -25
- package/dist/close.js +15 -6
- package/dist/constants.js +1 -1
- package/dist/device.d.ts +18 -0
- package/dist/device.js +7 -0
- package/dist/kit.js +2 -1
- package/dist/link.js +137 -3
- package/dist/muse.d.ts +2 -0
- package/dist/muse.js +7 -1
- package/dist/notices.js +10 -16
- package/dist/testing/contract.js +108 -4
- package/dist/testing/fake-herdr/server.js +61 -1
- package/dist/turn-files.d.ts +1 -1
- package/dist/turn-files.js +6 -1
- package/dist/turns.js +7 -5
- package/dist/types.d.ts +8 -2
- package/dist/words.json +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.1 (2026-10-12)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/discover 0.8.1.
|
|
8
|
+
- FIX: (from @byokit/discover 0.8.1) `browse()` and `scan()` accept resolved services whose `fullName` carries the service type without a trailing dot (`host._type._tcp`, as Android's react-native-zeroconf delivers) or with a trailing `.local` domain suffix, case-insensitive, instead of only the `host.local._type._tcp.` shape. Android phones now discover computers over mDNS instead of always scanning empty.
|
|
9
|
+
|
|
10
|
+
## 0.8.0 (2026-10-12)
|
|
11
|
+
|
|
12
|
+
- Dependency update: pins @byokit/discover 0.8.0.
|
|
13
|
+
- Dependency update: pins @byokit/relay 0.6.1.
|
|
14
|
+
- Dependency update: pins @byokit/pair 0.10.0.
|
|
15
|
+
|
|
16
|
+
- FIX: A Node script (or any non-shell interpreter) whose path contains `mise` is now a real installed binary, so `agentStatus` runs its sign-in check instead of skipping it as an auto-install shim; only a shell launcher that mentions mise still reads `installs-on-first-start`.
|
|
17
|
+
- `startAgent` accepts a workspace label as `place.workspace`: the agent lands in the one workspace with that label (new tab, `created: false`) or in a workspace created with that label (`created: true`). Two workspaces with the label refuse with `workspace-ambiguous` and create nothing, and a failed start rolls back only what the kit made (the fresh pane, or the fresh workspace), never a reused workspace. `AgentRef` carries the new `created` flag.
|
|
18
|
+
- New control link ops `hd.rename { pane | tab | workspace, label }`, `hd.focus { pane | tab | workspace }` and
|
|
19
|
+
`hd.layout { root, focus?, tabId?, tabLabel?, workspaceId? }`, with device `rename()`, `focus()` and
|
|
20
|
+
`layout()`. Each copies `hd.close`'s scope rule: the one pane, tab (through its owning workspace) or
|
|
21
|
+
workspace it names must be in `scopeOf(grant)`, or the device hears the `link.notAllowed` sentence and no
|
|
22
|
+
Herdr call is made; a `view` grant is refused. They map only to the typed `pane.rename`/`tab.rename`/
|
|
23
|
+
`workspace.rename`, `pane.focus`/`tab.focus`/`workspace.focus` and `layout.apply` calls, never through
|
|
24
|
+
`hd.call`. The fake Herdr now answers `layout.apply` and emits `layout.updated`.
|
|
25
|
+
- `sealNotice`/`openNotice` (and the portable `./device` re-export) now delegate to `@byokit/seal`'s generic
|
|
26
|
+
`sealNotice`/`openNotice` instead of sealing and opening the box here: the device seed becomes the box secret
|
|
27
|
+
through `boxKeyPairFromSeed`, and the opened value is still shape-checked as a `BlockedAgent`. The wire format is
|
|
28
|
+
unchanged, so a notice sealed by an earlier kit still opens, and `seal.openNotice` opens a herdr notice. The
|
|
29
|
+
hand-rolled base64url helpers stay only for the box-key registration text (`boxPublicKeyB64`, link.ts's
|
|
30
|
+
`decodeB64Url`), because seal exports no base64url.
|
|
31
|
+
- FIX: A refused widening close now tells the app which scope it would widen to: `closePane`/`closeTab`/`closeWorkspace` still throw `pane-close-would-widen`/`tab-close-would-widen`/`workspace-close-would-widen`, but each also carries the wider target as `{ scope: 'tab' | 'workspace', id, label }` (a pane's tab, a tab's workspace, a worktree group named by its root), so the UI can offer the bigger close instead. `hd.close` over a link maps all three codes to a `PublicLinkError` with the `close.wouldWiden` words, so a device hears plain words instead of a bare failure.
|
|
32
|
+
- FIX: `moveToAccount` refuses signed-out managed targets before waiting or creating a pane, preserving the source; generic `move` remains caller-gated and unknown readiness still proceeds.
|
|
33
|
+
- FIX: Muse readiness routes its mise check through the shared shell-launcher predicate, so a non-shell binary whose interpreter path contains `mise` reads installed instead of launcher-only; genuine mise shell shims still read launcher-only.
|
|
34
|
+
- FIX: `runTurn` accepts an agent that is already `done` when the turn starts, treating it like `idle` at turn start; previously the call was refused with `turn-not-idle`/`turn-busy`.
|
|
35
|
+
- FIX: `runTurn` accepts `files: false` to skip the file scan entirely (`changedFiles` is then always empty), so a turn rooted in a folder with more than 10000 entries no longer fails with `turn: file limit`.
|
|
36
|
+
|
|
5
37
|
## 0.7.3 (2026-10-11)
|
|
6
38
|
|
|
7
39
|
- Dependency update: pins @byokit/discover 0.7.0.
|
package/README.md
CHANGED
|
@@ -205,6 +205,7 @@ if (moved.ok) console.log(moved.session); // new pane to follow
|
|
|
205
205
|
|
|
206
206
|
A move accepts mapped managed kinds, including Claude (`CLAUDE_CONFIG_DIR`), Codex (`CODEX_HOME`) and Pi
|
|
207
207
|
(`PI_CODING_AGENT_DIR`, `--session <id|absolute path>`). Hosts own cross-account history sharing.
|
|
208
|
+
`moveToAccount` checks the target managed folder once with `agentStatus`, after immediate refusals and before waiting or splitting. A signed-out target returns `signed_out` and “Sign in to {name} first.” (`name` defaults to “this account”), with `live` naming the unchanged source and no new pane. Unknown readiness proceeds (including Pi); generic `move` has no check, so Accounts hosts call `cliAccounts.launch` and pass its `set`/`unset` only on success. The check is not a lock: a later sign-out still starts only in the selected folder, with existing `start_failed` rollback and no default-login retry.
|
|
208
209
|
It verifies the new shell's effective
|
|
209
210
|
account folder, resumes the conversation there, waits for a ready session, then closes the old pane. A failed
|
|
210
211
|
start preserves the original. A failed source close rolls back the new pane only after fresh reads verify
|
|
@@ -286,12 +287,17 @@ the extra install dirs a service PATH omits (`extraPathDirs`: `~/.local/bin`, th
|
|
|
286
287
|
Sign-in comes only from each CLI's own documented non-secret status command, with a 10 s timeout;
|
|
287
288
|
a kind whose CLI has none, or whose command gives no answer, reads `unknown`. Credential files
|
|
288
289
|
are never opened, read or statted — only the signed-in boolean is kept. Install detection tells a
|
|
289
|
-
real runnable binary apart from an auto-install launcher: a
|
|
290
|
-
on PATH at all, retains the
|
|
290
|
+
real runnable binary apart from an auto-install launcher: a shell script (a `sh`, `bash`, `zsh`,
|
|
291
|
+
`dash`, `ksh` or `ash` shebang) whose text mentions mise, or nothing on PATH at all, retains the
|
|
292
|
+
legacy `installState: 'installs-on-first-start'` with `installed: false`. A binary whose interpreter
|
|
293
|
+
merely lives under mise (for example a Node script) reads `installed`.
|
|
291
294
|
**Muse is different:** no executable, an auto-install shim, or an official launcher without its selected native release,
|
|
292
295
|
reads `installState: 'missing'`. Stock Herdr does not install an absent Muse command. That covers `pi`, which
|
|
293
296
|
Herdr installs via mise: a missing or shimmed `pi` reads `installed: false` with `installs on
|
|
294
|
-
first start`, while a real `pi` binary reads `installed: true`.
|
|
297
|
+
first start`, while a real `pi` binary reads `installed: true`. The `museReadiness()` probe's
|
|
298
|
+
`launcher-only` state is a separate check, not this shell-shebang rule: it flags any `#!` file whose
|
|
299
|
+
head contains `mise`, so a Node script under mise reads `launcher-only` there but `installed` on the
|
|
300
|
+
agent path.
|
|
295
301
|
|
|
296
302
|
```ts
|
|
297
303
|
import { HerdrKit } from '@byokit/herdr';
|
|
@@ -456,6 +462,7 @@ targets, do not traverse symlinks, always exclude `.git`, and ignore special fil
|
|
|
456
462
|
and restored during the turn do not appear. Use `files.exclude` to prune generated or sensitive
|
|
457
463
|
folders. Each scan defaults to 10000 entries and 128 MiB of regular-file content; set
|
|
458
464
|
`files.maxFiles`/`maxBytes` to change these limits. An incomplete scan rejects the call.
|
|
465
|
+
Pass `files: false` to skip the scan entirely (`changedFiles` is then always empty).
|
|
459
466
|
|
|
460
467
|
Reserve the pane and folder exclusively while the turn runs. Overlapping calls in one kit are
|
|
461
468
|
refused, but changes by other apps or processes cannot be attributed to this agent. The supplied
|
package/dist/accounts.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Call } from './agents.ts';
|
|
2
|
-
import type { AgentRef, Move, MoveResult, MoveToAccount, MoveToAccountResult, OpenSignInTab, StartAgent } from './types.ts';
|
|
2
|
+
import type { AgentReadiness, AgentStatusOptions, AgentRef, Move, MoveResult, MoveToAccount, MoveToAccountResult, OpenSignInTab, StartAgent } from './types.ts';
|
|
3
3
|
export declare function createAccountPanes(ctx: {
|
|
4
4
|
call: Call;
|
|
5
5
|
startAgent(o: StartAgent): Promise<AgentRef>;
|
|
6
|
+
agentStatus(kinds: readonly string[], o?: AgentStatusOptions): Promise<AgentReadiness[]>;
|
|
6
7
|
}): {
|
|
7
8
|
openSignInTab: (o: OpenSignInTab) => Promise<AgentRef>;
|
|
8
9
|
move: (o: Move) => Promise<MoveResult>;
|
package/dist/accounts.js
CHANGED
|
@@ -78,7 +78,7 @@ export function createAccountPanes(ctx) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
async function perform(target, o, request) {
|
|
81
|
-
const failed = (code, live = target.paneId) => ({ ok: false, code, message: words(`move.${code}
|
|
81
|
+
const failed = (code, live = target.paneId) => ({ ok: false, code, message: words(`move.${code}`, { name: o.name ?? 'this account' }), ...(live === null ? {} : { live }) });
|
|
82
82
|
if (moving.has(target.paneId))
|
|
83
83
|
return failed('busy');
|
|
84
84
|
moving.add(target.paneId);
|
|
@@ -114,34 +114,6 @@ export function createAccountPanes(ctx) {
|
|
|
114
114
|
if (!Number.isFinite(handoff.waitMs) || handoff.waitMs <= 0 || handoff.waitMs > 300_000
|
|
115
115
|
|| !Number.isSafeInteger(agent.state_change_seq) || agent.state_change_seq < 0)
|
|
116
116
|
return failed('unsupported');
|
|
117
|
-
if (agent.agent_status === 'working') {
|
|
118
|
-
try {
|
|
119
|
-
await call('agent.wait', { target: target.paneId, until: ['idle', 'done', 'blocked'],
|
|
120
|
-
timeout_ms: handoff.waitMs }, handoff.waitMs + 5000);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return failed('busy');
|
|
124
|
-
}
|
|
125
|
-
let current;
|
|
126
|
-
try {
|
|
127
|
-
current = (await call('agent.get', { target: target.paneId }))?.agent;
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return failed('changed', null);
|
|
131
|
-
}
|
|
132
|
-
if (!current || current.launch_pending === true || current.agent !== agent.agent
|
|
133
|
-
|| current.terminal_id !== agent.terminal_id || current.agent_session?.agent !== session.agent
|
|
134
|
-
|| current.agent_session?.kind !== session.kind || current.agent_session?.value !== session.value)
|
|
135
|
-
return failed('changed', null);
|
|
136
|
-
if (current.agent_status === 'blocked')
|
|
137
|
-
return failed('blocked');
|
|
138
|
-
if (!['idle', 'done'].includes(current.agent_status))
|
|
139
|
-
return failed('busy');
|
|
140
|
-
if (!Number.isSafeInteger(current.state_change_seq) || current.state_change_seq < 0)
|
|
141
|
-
return failed('unsupported');
|
|
142
|
-
agent = current;
|
|
143
|
-
session = current.agent_session;
|
|
144
|
-
}
|
|
145
117
|
}
|
|
146
118
|
const kind = agent.agent;
|
|
147
119
|
const metadata = accountKind(o.provider);
|
|
@@ -161,6 +133,39 @@ export function createAccountPanes(ctx) {
|
|
|
161
133
|
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
162
134
|
return failed('start_failed');
|
|
163
135
|
const variable = metadata.folderVar;
|
|
136
|
+
if (request === undefined) {
|
|
137
|
+
const [status] = await ctx.agentStatus([kind], { folders: { [kind]: o.folder }, env: o.env });
|
|
138
|
+
if (status?.signedIn === 'no')
|
|
139
|
+
return failed('signed_out');
|
|
140
|
+
}
|
|
141
|
+
if (handoff?.busy === 'wait' && agent.agent_status === 'working') {
|
|
142
|
+
try {
|
|
143
|
+
await call('agent.wait', { target: target.paneId, until: ['idle', 'done', 'blocked'],
|
|
144
|
+
timeout_ms: handoff.waitMs }, handoff.waitMs + 5000);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return failed('busy');
|
|
148
|
+
}
|
|
149
|
+
let current;
|
|
150
|
+
try {
|
|
151
|
+
current = (await call('agent.get', { target: target.paneId }))?.agent;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return failed('changed', null);
|
|
155
|
+
}
|
|
156
|
+
if (!current || current.launch_pending === true || current.agent !== agent.agent
|
|
157
|
+
|| current.terminal_id !== agent.terminal_id || current.agent_session?.agent !== session.agent
|
|
158
|
+
|| current.agent_session?.kind !== session.kind || current.agent_session?.value !== session.value)
|
|
159
|
+
return failed('changed', null);
|
|
160
|
+
if (current.agent_status === 'blocked')
|
|
161
|
+
return failed('blocked');
|
|
162
|
+
if (!['idle', 'done'].includes(current.agent_status))
|
|
163
|
+
return failed('busy');
|
|
164
|
+
if (!Number.isSafeInteger(current.state_change_seq) || current.state_change_seq < 0)
|
|
165
|
+
return failed('unsupported');
|
|
166
|
+
agent = current;
|
|
167
|
+
session = current.agent_session;
|
|
168
|
+
}
|
|
164
169
|
let paneId;
|
|
165
170
|
let replacement;
|
|
166
171
|
try {
|
package/dist/agents.js
CHANGED
|
@@ -9,7 +9,7 @@ import { delimiter, isAbsolute, join } from 'node:path';
|
|
|
9
9
|
import { words } from "./words.js";
|
|
10
10
|
import { accountKind } from "./kinds.js";
|
|
11
11
|
import { prepareLaunchEnv } from "./launch-env.js";
|
|
12
|
-
import { museNative, MUSE_INSTALL_URL } from "./muse.js";
|
|
12
|
+
import { museNative, MUSE_INSTALL_URL, isMiseLauncher } from "./muse.js";
|
|
13
13
|
const PROMPTABLE = ['idle', 'working', 'blocked', 'done'];
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
15
15
|
const WAIT_PROMPTABLE_POLL_MS = 50;
|
|
@@ -62,7 +62,9 @@ export function createAgents(ctx) {
|
|
|
62
62
|
};
|
|
63
63
|
// Every placement takes its pane id from the server's answer, never a prediction (6.4).
|
|
64
64
|
// Placements that make a pane (every path but `pane`) roll it back with `pane.close` when the
|
|
65
|
-
// start fails, so a failed start leaves no extra pane behind.
|
|
65
|
+
// start fails, so a failed start leaves no extra pane behind. A label placement that created
|
|
66
|
+
// its workspace rolls the workspace back with `workspace.close` instead, so no empty workspace
|
|
67
|
+
// survives either; a reused workspace is never closed.
|
|
66
68
|
async function startAgent(o) {
|
|
67
69
|
const emit = (e) => {
|
|
68
70
|
try {
|
|
@@ -130,7 +132,24 @@ export function createAgents(ctx) {
|
|
|
130
132
|
XDG_STATE_HOME: join(home, '.local', 'state'), XDG_CACHE_HOME: join(home, '.cache'), ...o.env
|
|
131
133
|
} };
|
|
132
134
|
let paneId;
|
|
133
|
-
let
|
|
135
|
+
let createdPane = false; // a pane this start made: roll it back on failure
|
|
136
|
+
let createdWorkspace; // a workspace this start made: roll it back instead
|
|
137
|
+
let workspaceCreated = false; // reported as `created`: a workspace was made
|
|
138
|
+
async function rollback() {
|
|
139
|
+
if (createdWorkspace !== undefined) {
|
|
140
|
+
try {
|
|
141
|
+
await call('workspace.close', { workspace_id: createdWorkspace });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
catch { /* fall through to the pane when the workspace close misses */ }
|
|
145
|
+
}
|
|
146
|
+
if (createdPane && paneId !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
await call('pane.close', { pane_id: paneId });
|
|
149
|
+
}
|
|
150
|
+
catch { /* rollback is best-effort */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
134
153
|
if (o.worktree !== undefined) {
|
|
135
154
|
const made = await call('worktree.create', { cwd: o.cwd, focus: false, ...env,
|
|
136
155
|
...(o.worktree.branch === undefined ? {} : { branch: o.worktree.branch }),
|
|
@@ -143,23 +162,50 @@ export function createAgents(ctx) {
|
|
|
143
162
|
paneId = rootPaneOf(made) ?? rootPaneOf(await call('tab.create', {
|
|
144
163
|
workspace_id: made?.workspace?.workspace_id, cwd: checkout, focus: false, ...env,
|
|
145
164
|
}));
|
|
146
|
-
|
|
165
|
+
createdPane = true;
|
|
166
|
+
workspaceCreated = true;
|
|
167
|
+
}
|
|
168
|
+
else if ('workspace' in o.place && o.place.workspace !== 'new') {
|
|
169
|
+
// A workspace label (6.4): reuse the one workspace with this label, or create it.
|
|
170
|
+
// The lookup is a fresh `workspace.list`, never the kit's tree, so a workspace made
|
|
171
|
+
// moments ago is found. Two matches refuse with `workspace-ambiguous` and pick nothing.
|
|
172
|
+
const label = o.place.workspace;
|
|
173
|
+
const listed = await call('workspace.list', {});
|
|
174
|
+
const workspaces = Array.isArray(listed?.workspaces) ? listed.workspaces : [];
|
|
175
|
+
const matches = workspaces.filter((w) => isObj(w) && w.label === label);
|
|
176
|
+
if (matches.length > 1)
|
|
177
|
+
throw fail('workspace-ambiguous', `More than one workspace is named "${label}". Rename one, then try again.`);
|
|
178
|
+
if (matches.length === 1) {
|
|
179
|
+
paneId = rootPaneOf(await call('tab.create', { workspace_id: matches[0].workspace_id, cwd: o.cwd,
|
|
180
|
+
...(o.place.label === undefined ? {} : { label: o.place.label }), focus: false, ...env }));
|
|
181
|
+
createdPane = true;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const made = await call('workspace.create', { cwd: o.cwd, label, focus: false, ...env });
|
|
185
|
+
paneId = rootPaneOf(made);
|
|
186
|
+
createdPane = true;
|
|
187
|
+
workspaceCreated = true;
|
|
188
|
+
if (isObj(made?.workspace) && typeof made.workspace.workspace_id === 'string') {
|
|
189
|
+
createdWorkspace = made.workspace.workspace_id;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
147
192
|
}
|
|
148
193
|
else if ('workspace' in o.place) {
|
|
149
194
|
paneId = rootPaneOf(await call('workspace.create', { cwd: o.cwd,
|
|
150
195
|
...(o.place.label === undefined ? {} : { label: o.place.label }), focus: false, ...env }));
|
|
151
|
-
|
|
196
|
+
createdPane = true;
|
|
197
|
+
workspaceCreated = true;
|
|
152
198
|
}
|
|
153
199
|
else if ('tab' in o.place) {
|
|
154
200
|
paneId = rootPaneOf(await call('tab.create', { workspace_id: o.place.workspaceId, cwd: o.cwd,
|
|
155
201
|
...(o.place.label === undefined ? {} : { label: o.place.label }), focus: false, ...env }));
|
|
156
|
-
|
|
202
|
+
createdPane = true;
|
|
157
203
|
}
|
|
158
204
|
else if ('split' in o.place) {
|
|
159
205
|
const made = await call('pane.split', { target_pane_id: o.place.split,
|
|
160
206
|
direction: o.place.direction, focus: false, ...env });
|
|
161
207
|
paneId = typeof made?.pane?.pane_id === 'string' ? made.pane.pane_id : undefined;
|
|
162
|
-
|
|
208
|
+
createdPane = true;
|
|
163
209
|
}
|
|
164
210
|
else {
|
|
165
211
|
paneId = o.place.pane;
|
|
@@ -171,12 +217,7 @@ export function createAgents(ctx) {
|
|
|
171
217
|
await prepareLaunchEnv(call, paneId, launch, timeout);
|
|
172
218
|
}
|
|
173
219
|
catch (error) {
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
await call('pane.close', { pane_id: paneId });
|
|
177
|
-
}
|
|
178
|
-
catch { /* best effort */ }
|
|
179
|
-
}
|
|
220
|
+
await rollback();
|
|
180
221
|
throw error;
|
|
181
222
|
}
|
|
182
223
|
}
|
|
@@ -188,19 +229,14 @@ export function createAgents(ctx) {
|
|
|
188
229
|
for (;;) {
|
|
189
230
|
try {
|
|
190
231
|
await call('agent.start', params, timeout + 5000);
|
|
191
|
-
return { paneId, name };
|
|
232
|
+
return { paneId, name, created: workspaceCreated };
|
|
192
233
|
}
|
|
193
234
|
catch (error) {
|
|
194
235
|
if (START_RETRYABLE.has(codeOf(error) ?? '') && Date.now() < deadline) {
|
|
195
236
|
await sleep(START_RETRY_DELAY_MS);
|
|
196
237
|
continue;
|
|
197
238
|
}
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
await call('pane.close', { pane_id: paneId });
|
|
201
|
-
}
|
|
202
|
-
catch { /* rollback is best-effort */ }
|
|
203
|
-
}
|
|
239
|
+
await rollback();
|
|
204
240
|
if (launch)
|
|
205
241
|
throw fail('start_failed', words('agent.notReady'));
|
|
206
242
|
throw error;
|
|
@@ -385,9 +421,8 @@ function readHead(file) {
|
|
|
385
421
|
catch { /* already gone */ }
|
|
386
422
|
}
|
|
387
423
|
}
|
|
388
|
-
// A mise-style auto-install launcher
|
|
389
|
-
//
|
|
390
|
-
// Mach-O) never match: the shebang gate keeps a stray "mise" substring ("promise", …) out.
|
|
424
|
+
// A mise-style auto-install launcher is a shell script that goes through mise — the found
|
|
425
|
+
// "binary" runs, but the first run installs the agent instead (see isMiseLauncher in muse.ts).
|
|
391
426
|
export function isAutoInstallShim(file, readFile = readHead) {
|
|
392
427
|
let head;
|
|
393
428
|
try {
|
|
@@ -396,7 +431,7 @@ export function isAutoInstallShim(file, readFile = readHead) {
|
|
|
396
431
|
catch {
|
|
397
432
|
return false;
|
|
398
433
|
}
|
|
399
|
-
return head !== undefined &&
|
|
434
|
+
return head !== undefined && isMiseLauncher(head);
|
|
400
435
|
}
|
|
401
436
|
// Muse has no absent-CLI auto-installer in stock Herdr. Its official launcher needs the
|
|
402
437
|
// selected native release too. Other kinds retain their legacy shim classification.
|
|
@@ -414,7 +449,7 @@ export function agentInstallState(kind, o) {
|
|
|
414
449
|
return { kind, state: 'missing', path: found.path };
|
|
415
450
|
}
|
|
416
451
|
const head = (o?.readFile ?? readHead)(found.path);
|
|
417
|
-
if (head
|
|
452
|
+
if (head !== undefined && (isMiseLauncher(head) || (head.startsWith('#!') && head.includes('MUSE_CHANNEL') && !museNative(found.path)))) {
|
|
418
453
|
return { kind, state: 'missing', path: found.path };
|
|
419
454
|
}
|
|
420
455
|
return { kind, state: 'installed', path: found.path };
|
package/dist/close.js
CHANGED
|
@@ -4,7 +4,7 @@ const codeOf = (error) => {
|
|
|
4
4
|
return code;
|
|
5
5
|
return /herdr: ([a-z0-9_]+):/i.exec(error instanceof Error ? error.message : String(error))?.[1];
|
|
6
6
|
};
|
|
7
|
-
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
7
|
+
const fail = (code, message, wider) => Object.assign(new Error(message), { code }, wider ?? {});
|
|
8
8
|
const unavailable = (kind) => fail(`${kind}-unavailable`, `That ${kind} is no longer available.`);
|
|
9
9
|
const count = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
10
10
|
export async function closePane(call, paneId) {
|
|
@@ -21,17 +21,20 @@ export async function closePane(call, paneId) {
|
|
|
21
21
|
if (typeof tabId !== 'string')
|
|
22
22
|
throw unavailable('pane');
|
|
23
23
|
let paneCount = 0;
|
|
24
|
+
let tabLabel;
|
|
24
25
|
try {
|
|
25
26
|
const made = (await call('tab.get', { tab_id: tabId }));
|
|
26
27
|
paneCount = count(made?.tab?.pane_count);
|
|
28
|
+
tabLabel = typeof made?.tab?.label === 'string' ? made.tab.label : undefined;
|
|
27
29
|
}
|
|
28
30
|
catch (error) {
|
|
29
31
|
if (codeOf(error) === 'tab_not_found')
|
|
30
32
|
throw unavailable('pane');
|
|
31
33
|
throw error;
|
|
32
34
|
}
|
|
33
|
-
if (paneCount <= 1)
|
|
34
|
-
throw fail('pane-close-would-widen', 'Closing this pane would close its tab; close the tab instead.');
|
|
35
|
+
if (paneCount <= 1) {
|
|
36
|
+
throw fail('pane-close-would-widen', 'Closing this pane would close its tab; close the tab instead.', { scope: 'tab', id: tabId, label: tabLabel ?? '' });
|
|
37
|
+
}
|
|
35
38
|
await call('pane.close', { pane_id: paneId });
|
|
36
39
|
}
|
|
37
40
|
export async function closeTab(call, tabId) {
|
|
@@ -48,24 +51,29 @@ export async function closeTab(call, tabId) {
|
|
|
48
51
|
if (typeof workspaceId !== 'string')
|
|
49
52
|
throw unavailable('tab');
|
|
50
53
|
let tabCount = 0;
|
|
54
|
+
let workspaceLabel;
|
|
51
55
|
try {
|
|
52
56
|
const made = (await call('workspace.get', { workspace_id: workspaceId }));
|
|
53
57
|
tabCount = count(made?.workspace?.tab_count);
|
|
58
|
+
workspaceLabel = typeof made?.workspace?.label === 'string' ? made.workspace.label : undefined;
|
|
54
59
|
}
|
|
55
60
|
catch (error) {
|
|
56
61
|
if (codeOf(error) === 'workspace_not_found')
|
|
57
62
|
throw unavailable('tab');
|
|
58
63
|
throw error;
|
|
59
64
|
}
|
|
60
|
-
if (tabCount <= 1)
|
|
61
|
-
throw fail('tab-close-would-widen', 'Closing this tab would close its workspace; close the workspace instead.');
|
|
65
|
+
if (tabCount <= 1) {
|
|
66
|
+
throw fail('tab-close-would-widen', 'Closing this tab would close its workspace; close the workspace instead.', { scope: 'workspace', id: workspaceId, label: workspaceLabel ?? '' });
|
|
67
|
+
}
|
|
62
68
|
await call('tab.close', { tab_id: tabId });
|
|
63
69
|
}
|
|
64
70
|
export async function closeWorkspace(call, workspaceId) {
|
|
65
71
|
let worktree;
|
|
72
|
+
let workspaceLabel;
|
|
66
73
|
try {
|
|
67
74
|
const made = (await call('workspace.get', { workspace_id: workspaceId }));
|
|
68
75
|
worktree = made?.workspace?.worktree;
|
|
76
|
+
workspaceLabel = typeof made?.workspace?.label === 'string' ? made.workspace.label : undefined;
|
|
69
77
|
}
|
|
70
78
|
catch (error) {
|
|
71
79
|
if (codeOf(error) === 'workspace_not_found')
|
|
@@ -78,7 +86,8 @@ export async function closeWorkspace(call, workspaceId) {
|
|
|
78
86
|
const group = (Array.isArray(made?.workspaces) ? made.workspaces : [])
|
|
79
87
|
.filter((w) => w?.worktree?.repo_key === worktree.repo_key).length;
|
|
80
88
|
if (group >= 2) {
|
|
81
|
-
|
|
89
|
+
// The group is anchored at this root, so the wider target is named by that root workspace.
|
|
90
|
+
throw fail('workspace-close-would-widen', 'Closing this workspace would close its worktree group; close the linked checkouts instead.', { scope: 'workspace', id: workspaceId, label: workspaceLabel ?? '' });
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
await call('workspace.close', { workspace_id: workspaceId });
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The pinned Herdr release (docs/runtime-kits.md D5) and the protocol this kit speaks.
|
|
2
2
|
export const HERDR_VERSION = '0.9.1';
|
|
3
3
|
// From the v0.9.1 schema snapshot (schema/herdr-api-0.9.1.json, docs/runtime-kits.md 6.7);
|
|
4
|
-
// regenerated by scripts/gen-types.ts
|
|
4
|
+
// regenerated by scripts/gen-types.ts.
|
|
5
5
|
export const HERDR_PROTOCOL = 22;
|
|
6
6
|
// Effective accepted server protocols for K11: each undeclared bound means exactly the pin.
|
|
7
7
|
export function protocolBounds(range) {
|
package/dist/device.d.ts
CHANGED
|
@@ -53,6 +53,24 @@ export declare function herdrDevice(link: DeviceLink): {
|
|
|
53
53
|
tab?: string;
|
|
54
54
|
workspace?: string;
|
|
55
55
|
}): Promise<void>;
|
|
56
|
+
rename(o: {
|
|
57
|
+
pane?: string;
|
|
58
|
+
tab?: string;
|
|
59
|
+
workspace?: string;
|
|
60
|
+
label: string;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
focus(o: {
|
|
63
|
+
pane?: string;
|
|
64
|
+
tab?: string;
|
|
65
|
+
workspace?: string;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
layout(o: {
|
|
68
|
+
root: HerdrParams<'layout.apply'>['root'];
|
|
69
|
+
focus?: boolean;
|
|
70
|
+
tabId?: string;
|
|
71
|
+
tabLabel?: string;
|
|
72
|
+
workspaceId?: string;
|
|
73
|
+
}): Promise<HerdrResult<'layout.apply'>>;
|
|
56
74
|
events(): AsyncIterable<HerdrLinkEvent>;
|
|
57
75
|
subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void, onError?: (message: string) => void): () => void;
|
|
58
76
|
registerNotices(seed: Uint8Array): Promise<void>;
|
package/dist/device.js
CHANGED
|
@@ -219,6 +219,13 @@ export function herdrDevice(link) {
|
|
|
219
219
|
close: async (o) => {
|
|
220
220
|
await link.request('hd.close', o);
|
|
221
221
|
},
|
|
222
|
+
rename: async (o) => {
|
|
223
|
+
await link.request('hd.rename', o);
|
|
224
|
+
},
|
|
225
|
+
focus: async (o) => {
|
|
226
|
+
await link.request('hd.focus', o);
|
|
227
|
+
},
|
|
228
|
+
layout: (o) => link.request('hd.layout', o),
|
|
222
229
|
events: () => streamFrames(link, 'hd.events', {}),
|
|
223
230
|
subscribe: (subs, on, onError) => subscription(link.stream('hd.subscribe', { subs }), on, onError),
|
|
224
231
|
registerNotices: async (seed) => {
|
package/dist/kit.js
CHANGED
|
@@ -136,7 +136,8 @@ export class HerdrKit {
|
|
|
136
136
|
constructor(o) {
|
|
137
137
|
this.o = o;
|
|
138
138
|
this.supervisor = new Supervisor(o, (s) => { this.current = s; o.onState?.(s); });
|
|
139
|
-
this.accountPanes = createAccountPanes({ call: this.callAny, startAgent: (o) => this.startAgent(o)
|
|
139
|
+
this.accountPanes = createAccountPanes({ call: this.callAny, startAgent: (o) => this.startAgent(o),
|
|
140
|
+
agentStatus: (kinds, options) => this.agentStatus(kinds, options) });
|
|
140
141
|
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot(),
|
|
141
142
|
reread: (paneId) => this.reread(paneId),
|
|
142
143
|
launchEnv: () => o.mode === 'own' ? this.supervisor.env() : undefined,
|
package/dist/link.js
CHANGED
|
@@ -15,6 +15,9 @@ const VIEW_OPS = new Set(['hd.state', 'hd.tree', 'hd.kinds', 'hd.read', 'hd.bloc
|
|
|
15
15
|
const deny = () => {
|
|
16
16
|
throw new PublicLinkError(words('link.notAllowed'));
|
|
17
17
|
};
|
|
18
|
+
// A refused widening close is the kit's plain sentence for the device, not a bare failure (6.4, 6.9).
|
|
19
|
+
const WIDEN_CODES = new Set(['pane-close-would-widen', 'tab-close-would-widen', 'workspace-close-would-widen']);
|
|
20
|
+
const widenError = (e) => WIDEN_CODES.has(e?.code ?? '') ? new PublicLinkError(words('close.wouldWiden')) : undefined;
|
|
18
21
|
const findPane = (snap, paneId) => {
|
|
19
22
|
for (const w of snap.workspaces) {
|
|
20
23
|
for (const t of w.tabs) {
|
|
@@ -132,6 +135,14 @@ export function herdrLink(kit, o) {
|
|
|
132
135
|
if (!wid || !kit.snapshot().workspaces.some((w) => w.id === wid && inScope(scope, w.id)))
|
|
133
136
|
deny();
|
|
134
137
|
}
|
|
138
|
+
if (typeof place.workspace === 'string' && place.workspace !== 'new') {
|
|
139
|
+
// A label placement lands its tab in the matching workspace: every match must already
|
|
140
|
+
// be in scope, or the device would reach a workspace its grant never named. No match
|
|
141
|
+
// creates, which stays creatable like a 'new' workspace.
|
|
142
|
+
const label = place.workspace;
|
|
143
|
+
if (kit.snapshot().workspaces.some((w) => w.label === label && !inScope(scope, w.id)))
|
|
144
|
+
deny();
|
|
145
|
+
}
|
|
135
146
|
return kit.startAgent(args);
|
|
136
147
|
}
|
|
137
148
|
case 'hd.prompt': {
|
|
@@ -237,21 +248,144 @@ export function herdrLink(kit, o) {
|
|
|
237
248
|
const where = findPane(snap, paneId);
|
|
238
249
|
if (!where || !inScope(scope, where.workspaceId))
|
|
239
250
|
deny();
|
|
240
|
-
|
|
251
|
+
try {
|
|
252
|
+
await kit.closePane(paneId);
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
throw widenError(e) ?? e;
|
|
256
|
+
}
|
|
241
257
|
return null;
|
|
242
258
|
}
|
|
243
259
|
if (tabId !== undefined) {
|
|
244
260
|
const owner = snap.workspaces.find((w) => w.tabs.some((t) => t.id === tabId));
|
|
245
261
|
if (!owner || !inScope(scope, owner.id))
|
|
246
262
|
deny();
|
|
247
|
-
|
|
263
|
+
try {
|
|
264
|
+
await kit.closeTab(tabId);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
throw widenError(e) ?? e;
|
|
268
|
+
}
|
|
248
269
|
return null;
|
|
249
270
|
}
|
|
250
271
|
if (!snap.workspaces.some((w) => w.id === workspaceId && inScope(scope, w.id)))
|
|
251
272
|
deny();
|
|
252
|
-
|
|
273
|
+
try {
|
|
274
|
+
await kit.closeWorkspace(workspaceId);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
throw widenError(e) ?? e;
|
|
278
|
+
}
|
|
253
279
|
return null;
|
|
254
280
|
}
|
|
281
|
+
// The typed edit ops copy hd.close's shape (7.1): exactly one target, control only, and the
|
|
282
|
+
// target's owning workspace checked against scope before any Herdr call is made (a tab through
|
|
283
|
+
// its workspace, a workspace through itself). Out of scope — or unknown — is link.notAllowed.
|
|
284
|
+
case 'hd.rename': {
|
|
285
|
+
if (grant.role !== 'control')
|
|
286
|
+
deny();
|
|
287
|
+
const snap = kit.snapshot();
|
|
288
|
+
const paneId = asString(args.pane);
|
|
289
|
+
const tabId = asString(args.tab);
|
|
290
|
+
const workspaceId = asString(args.workspace);
|
|
291
|
+
const label = asString(args.label);
|
|
292
|
+
const picked = [paneId, tabId, workspaceId].filter((v) => v !== undefined);
|
|
293
|
+
if (picked.length !== 1)
|
|
294
|
+
throw new Error('herdr: hd.rename picks exactly one of pane, tab or workspace');
|
|
295
|
+
if (label === undefined || label.length === 0)
|
|
296
|
+
throw new Error('herdr: hd.rename needs a label');
|
|
297
|
+
if (paneId !== undefined) {
|
|
298
|
+
const where = findPane(snap, paneId);
|
|
299
|
+
if (!where || !inScope(scope, where.workspaceId))
|
|
300
|
+
deny();
|
|
301
|
+
await kit.call('pane.rename', { pane_id: paneId, label });
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (tabId !== undefined) {
|
|
305
|
+
const owner = snap.workspaces.find((w) => w.tabs.some((t) => t.id === tabId));
|
|
306
|
+
if (!owner || !inScope(scope, owner.id))
|
|
307
|
+
deny();
|
|
308
|
+
await kit.call('tab.rename', { tab_id: tabId, label });
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
if (!snap.workspaces.some((w) => w.id === workspaceId && inScope(scope, w.id)))
|
|
312
|
+
deny();
|
|
313
|
+
await kit.call('workspace.rename', { workspace_id: workspaceId, label });
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
case 'hd.focus': {
|
|
317
|
+
if (grant.role !== 'control')
|
|
318
|
+
deny();
|
|
319
|
+
const snap = kit.snapshot();
|
|
320
|
+
const paneId = asString(args.pane);
|
|
321
|
+
const tabId = asString(args.tab);
|
|
322
|
+
const workspaceId = asString(args.workspace);
|
|
323
|
+
const picked = [paneId, tabId, workspaceId].filter((v) => v !== undefined);
|
|
324
|
+
if (picked.length !== 1)
|
|
325
|
+
throw new Error('herdr: hd.focus picks exactly one of pane, tab or workspace');
|
|
326
|
+
if (paneId !== undefined) {
|
|
327
|
+
const where = findPane(snap, paneId);
|
|
328
|
+
if (!where || !inScope(scope, where.workspaceId))
|
|
329
|
+
deny();
|
|
330
|
+
await kit.call('pane.focus', { pane_id: paneId });
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (tabId !== undefined) {
|
|
334
|
+
const owner = snap.workspaces.find((w) => w.tabs.some((t) => t.id === tabId));
|
|
335
|
+
if (!owner || !inScope(scope, owner.id))
|
|
336
|
+
deny();
|
|
337
|
+
await kit.call('tab.focus', { tab_id: tabId });
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (!snap.workspaces.some((w) => w.id === workspaceId && inScope(scope, w.id)))
|
|
341
|
+
deny();
|
|
342
|
+
await kit.call('workspace.focus', { workspace_id: workspaceId });
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
case 'hd.layout': {
|
|
346
|
+
if (grant.role !== 'control')
|
|
347
|
+
deny();
|
|
348
|
+
const root = args.root;
|
|
349
|
+
if (typeof root !== 'object' || root === null || Array.isArray(root)) {
|
|
350
|
+
throw new Error('herdr: hd.layout needs a root');
|
|
351
|
+
}
|
|
352
|
+
const snap = kit.snapshot();
|
|
353
|
+
const tabId = asString(args.tabId);
|
|
354
|
+
const workspaceId = asString(args.workspaceId);
|
|
355
|
+
// The layout picks exactly one tab or workspace, in scope, before any call.
|
|
356
|
+
if ([tabId, workspaceId].filter((v) => v !== undefined).length !== 1) {
|
|
357
|
+
throw new Error('herdr: hd.layout picks exactly one of tabId or workspaceId');
|
|
358
|
+
}
|
|
359
|
+
if (tabId !== undefined) {
|
|
360
|
+
const owner = snap.workspaces.find((w) => w.tabs.some((t) => t.id === tabId));
|
|
361
|
+
if (!owner || !inScope(scope, owner.id))
|
|
362
|
+
deny();
|
|
363
|
+
}
|
|
364
|
+
else if (!snap.workspaces.some((w) => w.id === workspaceId && inScope(scope, w.id)))
|
|
365
|
+
deny();
|
|
366
|
+
// Every named pane leaf must be known and in scope before any call; an unnamed leaf is a new pane.
|
|
367
|
+
const pending = [root];
|
|
368
|
+
while (pending.length > 0) {
|
|
369
|
+
const node = asRecord(pending.pop());
|
|
370
|
+
if (typeof node.pane_id === 'string') {
|
|
371
|
+
const where = findPane(snap, node.pane_id);
|
|
372
|
+
if (!where || !inScope(scope, where.workspaceId))
|
|
373
|
+
deny();
|
|
374
|
+
}
|
|
375
|
+
if (node.first !== undefined)
|
|
376
|
+
pending.push(node.first);
|
|
377
|
+
if (node.second !== undefined)
|
|
378
|
+
pending.push(node.second);
|
|
379
|
+
}
|
|
380
|
+
const tabLabel = asString(args.tabLabel);
|
|
381
|
+
return kit.call('layout.apply', {
|
|
382
|
+
root: root,
|
|
383
|
+
...(args.focus === true ? { focus: true } : {}),
|
|
384
|
+
...(tabId !== undefined ? { tab_id: tabId } : {}),
|
|
385
|
+
...(workspaceId !== undefined ? { workspace_id: workspaceId } : {}),
|
|
386
|
+
...(tabLabel !== undefined ? { tab_label: tabLabel } : {}),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
255
389
|
case 'hd.notices.register': {
|
|
256
390
|
if (grant.role !== 'control')
|
|
257
391
|
deny();
|
package/dist/muse.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare function museNative(launcher: string): {
|
|
|
4
4
|
native: string;
|
|
5
5
|
version: string;
|
|
6
6
|
} | undefined;
|
|
7
|
+
/** A mise-style auto-install launcher is a shell script that goes through mise. Shared with agents.ts. */
|
|
8
|
+
export declare function isMiseLauncher(head: string): boolean;
|
|
7
9
|
export type MuseReadiness = {
|
|
8
10
|
state: 'unknown' | 'missing' | 'launcher-only' | 'installed';
|
|
9
11
|
signedIn: 'unknown';
|
package/dist/muse.js
CHANGED
|
@@ -56,6 +56,12 @@ export function museNative(launcher) {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
/** A mise-style auto-install launcher is a shell script that goes through mise. Shared with agents.ts. */
|
|
60
|
+
export function isMiseLauncher(head) {
|
|
61
|
+
const shebang = head.split('\n', 1)[0] ?? '';
|
|
62
|
+
return /^#!\s*(?:\S*\/)?(?:env\s+)?(?:sh|bash|zsh|dash|ksh|ash)(?:\s|$)/.test(shebang)
|
|
63
|
+
&& head.includes('mise');
|
|
64
|
+
}
|
|
59
65
|
/** Only the supplied effective pane PATH is probed, never the controller's personal directories. */
|
|
60
66
|
export function museReadiness(env) {
|
|
61
67
|
if (env?.PATH === undefined)
|
|
@@ -70,7 +76,7 @@ export function museReadiness(env) {
|
|
|
70
76
|
accessSync(bin, constants.X_OK);
|
|
71
77
|
const selected = museNative(bin);
|
|
72
78
|
const head = launcherHead(bin);
|
|
73
|
-
const launcherOnly =
|
|
79
|
+
const launcherOnly = isMiseLauncher(head) || (!selected && head.includes('MUSE_CHANNEL'));
|
|
74
80
|
return { state: launcherOnly ? 'launcher-only' : 'installed', signedIn: 'unknown', bin,
|
|
75
81
|
...(selected ? { version: selected.version } : {}) };
|
|
76
82
|
}
|
package/dist/notices.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Sealed blocked-approval notices: the relay reads only a generic title, the phone opens the sealed body with its
|
|
2
|
-
// own key (docs/runtime-kits.md 7.3).
|
|
3
|
-
|
|
2
|
+
// own key (docs/runtime-kits.md 7.3). The envelope comes from `@byokit/seal`'s generic notice API, so one opener
|
|
3
|
+
// serves every kit; this file keeps the blocked-agent shape check and the box-key text. Portable: no Node import
|
|
4
|
+
// anywhere in it — `./device` re-exports `openNotice`.
|
|
5
|
+
import { boxKeyPairFromSeed, openNotice as sealOpenNotice, sealNotice as sealSealNotice } from '@byokit/seal';
|
|
4
6
|
const ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
5
7
|
/** Base64url without padding, hand-rolled so this file stays portable (no Buffer, no atob). */
|
|
6
8
|
export function encodeB64Url(bytes) {
|
|
@@ -57,28 +59,20 @@ export function sealNotice(b, boxPublicKey) {
|
|
|
57
59
|
}
|
|
58
60
|
if (!isBlockedAgent(b))
|
|
59
61
|
throw new Error('herdr: cannot seal that notice');
|
|
60
|
-
return
|
|
62
|
+
return sealSealNotice(b, boxPublicKey);
|
|
61
63
|
}
|
|
62
64
|
/** Open a notice envelope with the device's 32-byte seed; null when it is not ours or not a notice. */
|
|
63
65
|
export function openNotice(data, seed) {
|
|
64
|
-
if (typeof data !== 'object' || data === null || data.v !== 1 || typeof data.sealed !== 'string')
|
|
65
|
-
return null;
|
|
66
66
|
if (!(seed instanceof Uint8Array) || seed.length !== 32)
|
|
67
67
|
return null;
|
|
68
|
-
const
|
|
69
|
-
if (bundle === null)
|
|
70
|
-
return null;
|
|
71
|
-
const bytes = openBoxFromSeed(bundle, seed);
|
|
72
|
-
if (bytes === null)
|
|
73
|
-
return null;
|
|
74
|
-
let parsed;
|
|
68
|
+
const { secretKey } = boxKeyPairFromSeed(seed);
|
|
75
69
|
try {
|
|
76
|
-
parsed =
|
|
70
|
+
const parsed = sealOpenNotice(data, secretKey);
|
|
71
|
+
return isBlockedAgent(parsed) ? parsed : null;
|
|
77
72
|
}
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
finally {
|
|
74
|
+
secretKey.fill(0);
|
|
80
75
|
}
|
|
81
|
-
return isBlockedAgent(parsed) ? parsed : null;
|
|
82
76
|
}
|
|
83
77
|
/** The box public key a device registers for a seed (the host seals to this). */
|
|
84
78
|
export function boxPublicKeyB64(seed) {
|
package/dist/testing/contract.js
CHANGED
|
@@ -53,6 +53,15 @@ export function herdrContract(make, options) {
|
|
|
53
53
|
}
|
|
54
54
|
return { status, panes };
|
|
55
55
|
}
|
|
56
|
+
function ownerOf(snapshot, paneId) {
|
|
57
|
+
for (const workspace of snapshot.workspaces) {
|
|
58
|
+
for (const tab of workspace.tabs) {
|
|
59
|
+
if (tab.panes.some((pane) => pane.id === paneId))
|
|
60
|
+
return { workspace, tab };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
56
65
|
test('contract: start reaches ready and ping speaks the pinned protocol', async () => {
|
|
57
66
|
await bench(async ({ kit }) => {
|
|
58
67
|
await kit.start();
|
|
@@ -279,10 +288,93 @@ export function herdrContract(make, options) {
|
|
|
279
288
|
}
|
|
280
289
|
assert.equal(pane.paneId, 'w1:p1', 'the pane placement uses the pane as given');
|
|
281
290
|
assert.equal(typeof ws.name, 'string');
|
|
291
|
+
// `created` tells a workspace-making placement from one landing in an existing workspace.
|
|
292
|
+
assert.equal(ws.created, true, 'a new workspace reports created');
|
|
293
|
+
assert.equal(tree.created, true, 'a worktree checkout reports created');
|
|
294
|
+
assert.equal(tab.created, false, 'a new tab reports reuse');
|
|
295
|
+
assert.equal(split.created, false, 'a split reports reuse');
|
|
296
|
+
assert.equal(pane.created, false, 'a caller-owned pane reports reuse');
|
|
282
297
|
const workspaces = (await kit.call('workspace.list', {}));
|
|
283
298
|
assert.ok(workspaces.workspaces?.some((workspace) => workspace.worktree?.is_linked_worktree === true), 'the worktree placement registers a linked checkout');
|
|
284
299
|
});
|
|
285
300
|
});
|
|
301
|
+
test('contract: startAgent with a workspace label reuses the matching workspace', async () => {
|
|
302
|
+
await bench(async ({ kit }) => {
|
|
303
|
+
await kit.start();
|
|
304
|
+
const before = kit.snapshot().workspaces.length;
|
|
305
|
+
const made = (await kit.call('workspace.create', { cwd: '/tmp/h6-label', label: 'byokit-reuse' }));
|
|
306
|
+
const wid = made.workspace?.workspace_id;
|
|
307
|
+
assert.equal(typeof wid, 'string');
|
|
308
|
+
await until(() => kit.snapshot().workspaces.find((w) => w.id === wid), (w) => w !== undefined);
|
|
309
|
+
const first = await kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'byokit-reuse', label: 'agent-tab' } });
|
|
310
|
+
assert.equal(first.created, false, 'landing in an existing workspace reports reuse');
|
|
311
|
+
assert.match(first.paneId, /^w\d+:p\d+$/);
|
|
312
|
+
const tree = await until(() => {
|
|
313
|
+
const snap = kit.snapshot();
|
|
314
|
+
const owner = ownerOf(snap, first.paneId);
|
|
315
|
+
return owner !== undefined && snap.workspaces.length === before + 1 ? { snap, owner } : undefined;
|
|
316
|
+
}, () => true);
|
|
317
|
+
assert.equal(tree.owner.workspace.id, wid, 'the agent lands in the workspace with that label');
|
|
318
|
+
assert.equal(tree.owner.tab.label, 'agent-tab', 'the tab label rides the reuse');
|
|
319
|
+
const second = await kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'byokit-reuse' } });
|
|
320
|
+
assert.equal(second.created, false);
|
|
321
|
+
assert.notEqual(second.paneId, first.paneId, 'the second start gets its own tab pane');
|
|
322
|
+
const again = await until(() => {
|
|
323
|
+
const snap = kit.snapshot();
|
|
324
|
+
return ownerOf(snap, second.paneId) !== undefined && snap.workspaces.length === before + 1 ? snap : undefined;
|
|
325
|
+
}, () => true);
|
|
326
|
+
assert.equal(again.workspaces.length, before + 1, 'reuse adds a tab, never a workspace');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
test('contract: startAgent with an unknown workspace label creates it', async () => {
|
|
330
|
+
await bench(async ({ kit }) => {
|
|
331
|
+
await kit.start();
|
|
332
|
+
const before = kit.snapshot().workspaces.length;
|
|
333
|
+
const ref = await kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'byokit-fresh' } });
|
|
334
|
+
assert.equal(ref.created, true, 'no match creates the workspace');
|
|
335
|
+
assert.match(ref.paneId, /^w\d+:p\d+$/);
|
|
336
|
+
const snap = await until(() => {
|
|
337
|
+
const s = kit.snapshot();
|
|
338
|
+
const owner = ownerOf(s, ref.paneId);
|
|
339
|
+
return owner !== undefined && owner.workspace.label === 'byokit-fresh' && s.workspaces.length === before + 1
|
|
340
|
+
? s : undefined;
|
|
341
|
+
}, () => true);
|
|
342
|
+
assert.equal(snap.workspaces.length, before + 1);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
test('contract: a failed start into a reused workspace keeps the workspace', async () => {
|
|
346
|
+
await bench(async ({ kit, fake }) => {
|
|
347
|
+
if (!fake)
|
|
348
|
+
return;
|
|
349
|
+
await kit.start();
|
|
350
|
+
const made = (await kit.call('workspace.create', { cwd: '/tmp/h6-label', label: 'byokit-keep' }));
|
|
351
|
+
const wid = made.workspace?.workspace_id;
|
|
352
|
+
assert.equal(typeof wid, 'string');
|
|
353
|
+
await until(() => kit.snapshot().workspaces.find((w) => w.id === wid), (w) => w !== undefined);
|
|
354
|
+
const panesBefore = agentStatusOf(kit.snapshot(), 'none').panes;
|
|
355
|
+
fake.agentStartFaults.push({ code: 'agent_failed', message: 'boom' });
|
|
356
|
+
await assert.rejects(kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'byokit-keep' } }), /boom/);
|
|
357
|
+
await until(() => agentStatusOf(kit.snapshot(), 'none').panes, (n) => n === panesBefore);
|
|
358
|
+
assert.ok(kit.snapshot().workspaces.some((w) => w.id === wid && w.label === 'byokit-keep'), 'the reused workspace is never closed by a failed start');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
test('contract: two workspaces with one label refuse as ambiguous and pick nothing', async () => {
|
|
362
|
+
await bench(async ({ kit }) => {
|
|
363
|
+
await kit.start();
|
|
364
|
+
await kit.call('workspace.create', { cwd: '/tmp/h6-label', label: 'byokit-dup' });
|
|
365
|
+
await kit.call('workspace.create', { cwd: '/tmp/h6-label', label: 'byokit-dup' });
|
|
366
|
+
const settled = await until(() => {
|
|
367
|
+
const snap = kit.snapshot();
|
|
368
|
+
return snap.workspaces.filter((w) => w.label === 'byokit-dup').length === 2 ? snap : undefined;
|
|
369
|
+
}, () => true);
|
|
370
|
+
const workspacesBefore = settled.workspaces.length;
|
|
371
|
+
const panesBefore = agentStatusOf(settled, 'none').panes;
|
|
372
|
+
await assert.rejects(kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'byokit-dup' } }), (e) => e.code === 'workspace-ambiguous', 'two matching labels refuse with the named ambiguous error');
|
|
373
|
+
assert.equal(kit.snapshot().workspaces.filter((w) => w.label === 'byokit-dup').length, 2);
|
|
374
|
+
assert.equal(kit.snapshot().workspaces.length, workspacesBefore, 'the refusal creates no workspace');
|
|
375
|
+
assert.equal(agentStatusOf(kit.snapshot(), 'none').panes, panesBefore, 'the refusal creates no pane');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
286
378
|
test('contract: a started agent joins snapshot() and its status stays current there', async () => {
|
|
287
379
|
await bench(async ({ kit }) => {
|
|
288
380
|
await kit.start();
|
|
@@ -374,14 +466,26 @@ export function herdrContract(make, options) {
|
|
|
374
466
|
await bench(async ({ kit }) => {
|
|
375
467
|
await kit.start();
|
|
376
468
|
// A fresh workspace holds exactly one tab with exactly one pane: the widening cases.
|
|
377
|
-
const made = (await kit.call('workspace.create', { cwd: '/tmp/h6-other' }));
|
|
469
|
+
const made = (await kit.call('workspace.create', { cwd: '/tmp/h6-other', label: 'other' }));
|
|
378
470
|
const w2 = made.workspace?.workspace_id;
|
|
379
471
|
assert.equal(typeof w2, 'string');
|
|
380
|
-
|
|
381
|
-
|
|
472
|
+
// A refusal carries the wider target (scope, id, label) the app would close instead (6.4).
|
|
473
|
+
const widen = async (work) => {
|
|
474
|
+
try {
|
|
475
|
+
await work;
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
const { scope, id, label, code } = e;
|
|
479
|
+
return { scope, id, label, code };
|
|
480
|
+
}
|
|
481
|
+
assert.fail('expected a would-widen refusal');
|
|
482
|
+
};
|
|
483
|
+
assert.deepEqual(await widen(kit.closePane(`${w2}:p1`)), { scope: 'tab', id: `${w2}:t1`, label: 'main', code: 'pane-close-would-widen' }, 'closing the last pane widens to its tab');
|
|
484
|
+
assert.deepEqual(await widen(kit.closeTab(`${w2}:t1`)), { scope: 'workspace', id: w2, label: 'other', code: 'tab-close-would-widen' }, 'closing the only tab widens to its workspace');
|
|
382
485
|
// worktree.create links a checkout into w1's group: the root now widens on close.
|
|
486
|
+
const w1Label = kit.snapshot().workspaces.find((w) => w.id === 'w1')?.label;
|
|
383
487
|
await kit.startAgent({ kind: 'pi', cwd: '/tmp/h6', place: { workspace: 'new' }, worktree: { branch: 'h6' } });
|
|
384
|
-
|
|
488
|
+
assert.deepEqual(await widen(kit.closeWorkspace('w1')), { scope: 'workspace', id: 'w1', label: w1Label, code: 'workspace-close-would-widen' }, 'closing a worktree group parent widens to the group, named by its root');
|
|
385
489
|
// The exact closes still go through.
|
|
386
490
|
await kit.call('pane.split', { target_pane_id: `${w2}:p1`, direction: 'right' });
|
|
387
491
|
await kit.closePane(`${w2}:p1`);
|
|
@@ -8,7 +8,7 @@ import { createServer } from 'node:net';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { HERDR_PROTOCOL, HERDR_VERSION } from "../../constants.js";
|
|
10
10
|
import { writeBinShim } from "./bin.js";
|
|
11
|
-
import { agentRecord, createWorld, paneScroll, relayoutTab } from "./world.js";
|
|
11
|
+
import { COLS, ROWS, agentRecord, createWorld, paneScroll, relayoutTab } from "./world.js";
|
|
12
12
|
const PROMPT_SETTLE_MS = 50; // 6.8: the agent settles 50 ms after a prompt
|
|
13
13
|
const WAIT_POLL_MS = 5;
|
|
14
14
|
// The subscription kinds that need a `pane_id` filter and ride their own socket each
|
|
@@ -18,6 +18,27 @@ const WAIT_POLL_MS = 5;
|
|
|
18
18
|
// batch with another kind.
|
|
19
19
|
const FILTERED = new Set(['pane_agent_status_changed', 'pane_output_matched', 'pane_scroll_changed']);
|
|
20
20
|
const AGENT_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
21
|
+
// Position every pane leaf of a layout tree inside the tab's area: a split divides its rect at its
|
|
22
|
+
// ratio, a pane leaf lands where the recursion left it. Leaves in tree order, so `focus` can pick
|
|
23
|
+
// the first one.
|
|
24
|
+
function layoutLeaves(node, rect, out) {
|
|
25
|
+
if (node.type === 'split' && node.first !== undefined && node.second !== undefined) {
|
|
26
|
+
const ratio = typeof node.ratio === 'number' ? Math.min(1, Math.max(0, node.ratio)) : 0.5;
|
|
27
|
+
if (node.direction === 'down') {
|
|
28
|
+
const top = Math.max(1, Math.round(rect.height * ratio));
|
|
29
|
+
layoutLeaves(node.first, { x: rect.x, y: rect.y, width: rect.width, height: top }, out);
|
|
30
|
+
layoutLeaves(node.second, { x: rect.x, y: rect.y + top, width: rect.width, height: Math.max(1, rect.height - top) }, out);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const left = Math.max(1, Math.round(rect.width * ratio));
|
|
34
|
+
layoutLeaves(node.first, { x: rect.x, y: rect.y, width: left, height: rect.height }, out);
|
|
35
|
+
layoutLeaves(node.second, { x: rect.x + left, y: rect.y, width: Math.max(1, rect.width - left), height: rect.height }, out);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (typeof node.pane_id === 'string')
|
|
40
|
+
out.push({ paneId: node.pane_id, rect });
|
|
41
|
+
}
|
|
21
42
|
export async function startFakeHerdr(options) {
|
|
22
43
|
const dir = options.dir;
|
|
23
44
|
if (typeof dir !== 'string' || dir.length === 0)
|
|
@@ -335,6 +356,45 @@ export async function startFakeHerdr(options) {
|
|
|
335
356
|
return { zoom: { changed, zoomed: live.zoomed } };
|
|
336
357
|
},
|
|
337
358
|
'pane.layout': (p) => ({ layout: tabLayout(paneOf(p.pane_id).tab_id) }),
|
|
359
|
+
'layout.apply': (p) => {
|
|
360
|
+
const root = p.root;
|
|
361
|
+
if (typeof root !== 'object' || root === null || Array.isArray(root)) {
|
|
362
|
+
throw fail('invalid_layout', 'a root layout is required');
|
|
363
|
+
}
|
|
364
|
+
// The target tab: an explicit tab_id, else the workspace's first tab (the pinned server needs one).
|
|
365
|
+
let tab;
|
|
366
|
+
if (typeof p.tab_id === 'string') {
|
|
367
|
+
tab = live.tabs.find((row) => row.tab_id === p.tab_id);
|
|
368
|
+
if (tab === undefined)
|
|
369
|
+
throw fail('tab_not_found', 'tab not found');
|
|
370
|
+
}
|
|
371
|
+
else if (typeof p.workspace_id === 'string') {
|
|
372
|
+
if (!live.workspaces.some((row) => row.workspace_id === p.workspace_id)) {
|
|
373
|
+
throw fail('workspace_not_found', 'workspace not found');
|
|
374
|
+
}
|
|
375
|
+
tab = live.tabs.find((row) => row.workspace_id === p.workspace_id);
|
|
376
|
+
if (tab === undefined)
|
|
377
|
+
throw fail('tab_not_found', 'tab not found');
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
throw fail('invalid_target', 'layout.apply needs a tab_id or workspace_id');
|
|
381
|
+
}
|
|
382
|
+
// Every leaf must be a pane of the target tab; the layout repositions exactly those panes.
|
|
383
|
+
const leaves = [];
|
|
384
|
+
layoutLeaves(root, { x: 0, y: 0, width: COLS, height: ROWS }, leaves);
|
|
385
|
+
for (const leaf of leaves) {
|
|
386
|
+
const pane = live.panes.find((row) => row.pane_id === leaf.paneId);
|
|
387
|
+
if (pane === undefined || pane.tab_id !== tab.tab_id) {
|
|
388
|
+
throw fail('pane_not_found', `pane not found in tab: ${leaf.paneId}`);
|
|
389
|
+
}
|
|
390
|
+
pane.rect = leaf.rect;
|
|
391
|
+
}
|
|
392
|
+
if (p.focus === true && leaves[0] !== undefined)
|
|
393
|
+
focusPane(leaves[0].paneId);
|
|
394
|
+
const layout = tabLayout(tab.tab_id);
|
|
395
|
+
emitEvent('layout.updated', { layout });
|
|
396
|
+
return { type: 'layout_apply', layout: { ...layout, root } };
|
|
397
|
+
},
|
|
338
398
|
'agent.start': (p) => {
|
|
339
399
|
const fault = agentStartFaults.shift();
|
|
340
400
|
if (fault !== undefined)
|
package/dist/turn-files.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { AgentTurnEnd, AgentTurnFiles } from './types.ts';
|
|
2
2
|
export declare function positiveLimit(value: number | undefined, fallback: number): number;
|
|
3
|
-
export declare function scanFiles(root: string, policy: AgentTurnFiles | undefined, resultName: string, active: () => void): Promise<Map<string, string>>;
|
|
3
|
+
export declare function scanFiles(root: string, policy: AgentTurnFiles | false | undefined, resultName: string, active: () => void): Promise<Map<string, string>>;
|
|
4
4
|
export declare function changedFiles(before: Map<string, string>, after: Map<string, string>): AgentTurnEnd['changedFiles'];
|
package/dist/turn-files.js
CHANGED
|
@@ -11,8 +11,13 @@ export function positiveLimit(value, fallback) {
|
|
|
11
11
|
// A baseline of the actual tree, independent of Git status and mtime granularity. Never traverse
|
|
12
12
|
// symlinks or read special files; a changing/unreadable tree fails rather than claiming a full diff.
|
|
13
13
|
export async function scanFiles(root, policy, resultName, active) {
|
|
14
|
+
// files:false skips the scan entirely: no walk, so a large tree (e.g. node_modules) cannot fail the turn.
|
|
15
|
+
if (policy === false)
|
|
16
|
+
return new Map();
|
|
14
17
|
const maxFiles = positiveLimit(policy?.maxFiles, 10000);
|
|
15
18
|
const maxBytes = positiveLimit(policy?.maxBytes, 128 * 1024 * 1024);
|
|
19
|
+
// Read once: narrowing on the parameter does not persist into the walk closure below.
|
|
20
|
+
const exclude = policy?.exclude;
|
|
16
21
|
let entries = 0;
|
|
17
22
|
let bytes = 0;
|
|
18
23
|
const found = new Map();
|
|
@@ -27,7 +32,7 @@ export async function scanFiles(root, policy, resultName, active) {
|
|
|
27
32
|
active();
|
|
28
33
|
const path = join(dir, name);
|
|
29
34
|
const rel = relative(root, path).split(sep).join('/');
|
|
30
|
-
if (name === '.git' || rel === resultName ||
|
|
35
|
+
if (name === '.git' || rel === resultName || exclude?.(rel))
|
|
31
36
|
continue;
|
|
32
37
|
if (++entries > maxFiles)
|
|
33
38
|
throw new Error('turn: file limit');
|
package/dist/turns.js
CHANGED
|
@@ -73,9 +73,11 @@ export function createTurns(kit) {
|
|
|
73
73
|
if (!isAbsolute(o.cwd))
|
|
74
74
|
throw fail('turn-cwd');
|
|
75
75
|
const timeoutMs = positiveLimit(o.timeoutMs, 300000);
|
|
76
|
-
// Fail invalid configuration before delivering a prompt.
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Fail invalid configuration before delivering a prompt (no scan means no file limits to check).
|
|
77
|
+
if (o.files !== false) {
|
|
78
|
+
positiveLimit(o.files?.maxFiles, 10000);
|
|
79
|
+
positiveLimit(o.files?.maxBytes, 128 * 1024 * 1024);
|
|
80
|
+
}
|
|
79
81
|
if (o.result)
|
|
80
82
|
positiveLimit(o.result.maxBytes, 1024 * 1024);
|
|
81
83
|
const schema = o.result ? JSON.stringify(o.result.schema) : undefined;
|
|
@@ -125,7 +127,7 @@ export function createTurns(kit) {
|
|
|
125
127
|
const endSignal = new Promise((resolve) => { resolveEnd = resolve; });
|
|
126
128
|
const watch = kit.subscribe([{ type: 'pane.agent_status_changed', pane_id: target.paneId }], (e) => {
|
|
127
129
|
if (!armed) {
|
|
128
|
-
if (e.agent_status !== 'idle')
|
|
130
|
+
if (e.agent_status !== 'idle' && e.agent_status !== 'done')
|
|
129
131
|
abort('turn-busy');
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
@@ -153,7 +155,7 @@ export function createTurns(kit) {
|
|
|
153
155
|
throw fail('turn-watch-lost');
|
|
154
156
|
const beforeAgent = await gate(kit.call('agent.get', { target: target.paneId }));
|
|
155
157
|
const agent = beforeAgent.agent;
|
|
156
|
-
if (agent.agent_status !== 'idle' || agent.launch_pending === true)
|
|
158
|
+
if ((agent.agent_status !== 'idle' && agent.agent_status !== 'done') || agent.launch_pending === true)
|
|
157
159
|
throw fail('turn-not-idle');
|
|
158
160
|
if (target.name && agent.name !== target.name)
|
|
159
161
|
throw fail('turn-unavailable');
|
package/dist/types.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export type AgentStartEvent = {
|
|
|
98
98
|
export type AgentRef = {
|
|
99
99
|
paneId: string;
|
|
100
100
|
name?: string;
|
|
101
|
+
created?: boolean;
|
|
101
102
|
};
|
|
102
103
|
export type StartAgent = {
|
|
103
104
|
kind: string;
|
|
@@ -106,6 +107,9 @@ export type StartAgent = {
|
|
|
106
107
|
place: {
|
|
107
108
|
workspace: 'new';
|
|
108
109
|
label?: string;
|
|
110
|
+
} | {
|
|
111
|
+
workspace: string;
|
|
112
|
+
label?: string;
|
|
109
113
|
} | {
|
|
110
114
|
tab: 'new';
|
|
111
115
|
workspaceId: string;
|
|
@@ -155,6 +159,7 @@ export type MoveToAccount = {
|
|
|
155
159
|
provider: string;
|
|
156
160
|
folder: string;
|
|
157
161
|
env?: Record<string, string>;
|
|
162
|
+
name?: string;
|
|
158
163
|
direction?: 'right' | 'down';
|
|
159
164
|
timeoutMs?: number;
|
|
160
165
|
whenBusy?: BusyHandoff;
|
|
@@ -164,7 +169,7 @@ export type MoveToAccountResult = {
|
|
|
164
169
|
session: string;
|
|
165
170
|
} | {
|
|
166
171
|
ok: false;
|
|
167
|
-
code: 'too_early' | 'busy' | 'unsupported' | 'env_mismatch' | 'close_failed' | 'start_failed' | 'blocked' | 'changed' | 'interrupt_unsupported';
|
|
172
|
+
code: 'too_early' | 'busy' | 'unsupported' | 'env_mismatch' | 'close_failed' | 'start_failed' | 'blocked' | 'changed' | 'interrupt_unsupported' | 'signed_out';
|
|
168
173
|
message: string;
|
|
169
174
|
live?: string;
|
|
170
175
|
};
|
|
@@ -209,7 +214,8 @@ export type AgentTurnOptions<T = unknown> = {
|
|
|
209
214
|
cwd: string;
|
|
210
215
|
timeoutMs?: number;
|
|
211
216
|
signal?: AbortSignal;
|
|
212
|
-
|
|
217
|
+
/** Pass false to skip the file scan entirely (changedFiles is then always empty). */
|
|
218
|
+
files?: AgentTurnFiles | false;
|
|
213
219
|
result?: AgentTurnResultPolicy<T>;
|
|
214
220
|
onEnd?: (end: AgentTurnEnd<T>) => void;
|
|
215
221
|
};
|
package/dist/words.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"approval.stale": "That question already changed. Look again before answering.",
|
|
21
21
|
"close.wouldWiden": "Closing this would close more than you picked. Close the bigger one instead.",
|
|
22
22
|
"link.notAllowed": "This device can't do that. Ask the person at the computer.",
|
|
23
|
+
"move.signed_out": "Sign in to {name} first.",
|
|
23
24
|
"move.too_early": "This conversation has not started yet. Try again in a moment.",
|
|
24
25
|
"move.busy": "Wait for this conversation to finish before moving it.",
|
|
25
26
|
"move.unsupported": "This conversation cannot move between these accounts.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/herdr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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/pair.",
|
|
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/pair": "0.
|
|
56
|
-
"@byokit/relay": "0.6.
|
|
57
|
-
"@byokit/discover": "0.
|
|
55
|
+
"@byokit/pair": "0.10.0",
|
|
56
|
+
"@byokit/relay": "0.6.1",
|
|
57
|
+
"@byokit/discover": "0.8.1",
|
|
58
58
|
"@byokit/seal": "0.3.0",
|
|
59
59
|
"ws": "8.22.0"
|
|
60
60
|
},
|