@byokit/herdr 0.5.0 → 0.7.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 +29 -0
- package/README.md +97 -8
- package/dist/accounts.js +150 -67
- package/dist/agents.d.ts +2 -0
- package/dist/agents.js +88 -28
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/kinds.d.ts +20 -0
- package/dist/kinds.js +6 -0
- package/dist/kinds.json +503 -0
- package/dist/kit.js +1 -0
- package/dist/link.d.ts +2 -2
- package/dist/link.js +1 -1
- package/dist/muse.d.ts +47 -0
- package/dist/muse.js +257 -0
- package/dist/types.d.ts +28 -3
- package/dist/words.json +4 -0
- package/package.json +5 -5
package/dist/agents.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// disagree the schema wins. `pane.split` takes `target_pane_id` (muxr agrees), and `agent.start`
|
|
4
4
|
// has no `env` param — a start's env belongs to the placement create/split call (src/generated/methods.ts).
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { accessSync, constants as fsConstants, openSync, readSync, closeSync } from 'node:fs';
|
|
6
|
+
import { accessSync, constants as fsConstants, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { delimiter, join } from 'node:path';
|
|
8
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
9
9
|
import { words } from "./words.js";
|
|
10
|
+
import { accountKind } from "./kinds.js";
|
|
10
11
|
import { prepareLaunchEnv } from "./launch-env.js";
|
|
12
|
+
import { museNative, MUSE_INSTALL_URL } from "./muse.js";
|
|
11
13
|
const PROMPTABLE = ['idle', 'working', 'blocked', 'done'];
|
|
12
14
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
13
15
|
// `agent.start` lands while the fresh pane is still at its shell prompt: the server answers
|
|
@@ -62,19 +64,37 @@ export function createAgents(ctx) {
|
|
|
62
64
|
catch { /* a listener never breaks a start */ }
|
|
63
65
|
emitStart(e);
|
|
64
66
|
};
|
|
65
|
-
// Install pre-check: a shim or nothing on PATH means Herdr installs on first start, so the
|
|
66
|
-
// app hears `installing` before the long start instead of watching a blank pane.
|
|
67
67
|
let installExpected = false;
|
|
68
|
-
try {
|
|
69
|
-
installExpected = agentInstallState(o.kind, o.installProbe).state === 'installs-on-first-start';
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
installExpected = false;
|
|
73
|
-
}
|
|
74
|
-
if (installExpected)
|
|
75
|
-
emit({ phase: 'installing', kind: o.kind, message: words('agent.installing', { agent: o.kind }) });
|
|
76
68
|
let stage = 'placement';
|
|
77
69
|
try {
|
|
70
|
+
if (o.kind === 'muse') {
|
|
71
|
+
const replacement = o.env && typeof o.env.env === 'object' && Array.isArray(o.env.unset)
|
|
72
|
+
? o.env : undefined;
|
|
73
|
+
const existingPane = 'pane' in o.place && o.worktree === undefined;
|
|
74
|
+
// An ordinary env overlay cannot replace an existing shell's environment.
|
|
75
|
+
if (existingPane && !replacement && Object.keys(o.env ?? {}).length > 0) {
|
|
76
|
+
throw fail('env_mismatch', 'This pane needs to be opened again to use that sign-in.');
|
|
77
|
+
}
|
|
78
|
+
const effective = replacement?.env ?? (existingPane ? undefined
|
|
79
|
+
: { ...ctx.launchEnv?.(), ...o.env });
|
|
80
|
+
// No controller/global extras and no probe override when the effective PATH is known.
|
|
81
|
+
// Existing unobserved panes remain unknown; the exact RPC pass-through remains available.
|
|
82
|
+
const path = effective?.PATH?.split(delimiter).filter(isAbsolute);
|
|
83
|
+
const probe = path === undefined ? (existingPane ? undefined : o.installProbe) : { path };
|
|
84
|
+
if (probe && agentInstallState('muse', probe).state === 'missing') {
|
|
85
|
+
throw fail('agent_not_installed', words('agent.notInstalled'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
installExpected = agentInstallState(o.kind, o.installProbe).state === 'installs-on-first-start';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
installExpected = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (installExpected)
|
|
97
|
+
emit({ phase: 'installing', kind: o.kind, message: words('agent.installing', { agent: o.kind }) });
|
|
78
98
|
const ref = await startAgentInner(o, () => { stage = 'start'; });
|
|
79
99
|
emit({ phase: 'ready', kind: o.kind, ref });
|
|
80
100
|
return ref;
|
|
@@ -82,6 +102,8 @@ export function createAgents(ctx) {
|
|
|
82
102
|
catch (error) {
|
|
83
103
|
const reason = classifyStartFailure(error, { installExpected, stage });
|
|
84
104
|
emit({ phase: 'launchFailed', kind: o.kind, reason, message: launchFailureWords(reason, o.kind) });
|
|
105
|
+
if (codeOf(error) === 'agent_not_installed')
|
|
106
|
+
throw error;
|
|
85
107
|
if (o.env && typeof o.env.env === 'object')
|
|
86
108
|
throw fail(codeOf(error) === 'env_mismatch' ? 'env_mismatch' : 'start_failed', words('agent.notReady'));
|
|
87
109
|
throw error;
|
|
@@ -94,7 +116,12 @@ export function createAgents(ctx) {
|
|
|
94
116
|
throw fail('env_mismatch', 'This pane needs to be opened again to use that sign-in.');
|
|
95
117
|
}
|
|
96
118
|
const timeout = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
97
|
-
|
|
119
|
+
// A pane HOME alone keeps the server's XDG dirs, so a sign-in would land outside it: confine them too.
|
|
120
|
+
const home = launch ? undefined : o.env?.HOME;
|
|
121
|
+
const env = o.env === undefined || launch ? {} : { env: home === undefined ? o.env : {
|
|
122
|
+
XDG_CONFIG_HOME: join(home, '.config'), XDG_DATA_HOME: join(home, '.local', 'share'),
|
|
123
|
+
XDG_STATE_HOME: join(home, '.local', 'state'), XDG_CACHE_HOME: join(home, '.cache'), ...o.env
|
|
124
|
+
} };
|
|
98
125
|
let paneId;
|
|
99
126
|
let created = false;
|
|
100
127
|
if (o.worktree !== undefined) {
|
|
@@ -247,7 +274,7 @@ export function createAgents(ctx) {
|
|
|
247
274
|
return kinds.filter((kind) => resolveAgentBinary(kind, o).path !== undefined);
|
|
248
275
|
}
|
|
249
276
|
async function agentStatus(kinds, o) {
|
|
250
|
-
const path = o?.path ?? agentProbePath();
|
|
277
|
+
const path = o?.path ?? ctx.launchEnv?.()?.PATH?.split(delimiter) ?? agentProbePath();
|
|
251
278
|
const run = o?.run ?? runStatusCommand;
|
|
252
279
|
const timeoutMs = o?.timeoutMs ?? STATUS_TIMEOUT_MS;
|
|
253
280
|
const probe = { path, ...(o?.aliases === undefined ? {} : { aliases: o.aliases }),
|
|
@@ -255,18 +282,27 @@ export function createAgents(ctx) {
|
|
|
255
282
|
return Promise.all(kinds.map(async (kind) => {
|
|
256
283
|
const install = agentInstallState(kind, probe);
|
|
257
284
|
const binary = o?.aliases?.[kind]?.[0] ?? kind;
|
|
258
|
-
const installHint = kind === '
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
//
|
|
285
|
+
const installHint = kind === 'muse' ? `Install Muse explicitly from ${MUSE_INSTALL_URL} into the private launch environment.`
|
|
286
|
+
: kind === 'pi' ? PI_INSTALL_HINT : `Install the ${binary} command, then check again.`;
|
|
287
|
+
// Muse missing/launcher-only stays missing; legacy kinds retain their shim readiness.
|
|
288
|
+
// Install state never proves account or catalog readiness.
|
|
262
289
|
if (install.state !== 'installed')
|
|
263
290
|
return { kind, installed: false, installState: install.state, signedIn: 'unknown', installHint };
|
|
264
291
|
const statusProbe = STATUS_PROBES[kind];
|
|
265
|
-
|
|
292
|
+
const variable = accountKind(kind)?.folderVar;
|
|
293
|
+
const folder = o?.folders?.[kind];
|
|
294
|
+
if (statusProbe === undefined || variable === undefined || !folder || !isAbsolute(folder) || /[\r\n\0]/.test(folder))
|
|
266
295
|
return { kind, installed: true, installState: 'installed', signedIn: 'unknown', installHint };
|
|
267
296
|
let answer;
|
|
297
|
+
// A folder override alone is not isolation: the CLI can fall back to HOME,
|
|
298
|
+
// XDG or platform stores. Supply a clean env and confine every home to this account.
|
|
299
|
+
const env = { ...o?.env, PATH: path.join(delimiter), HOME: folder, USERPROFILE: folder,
|
|
300
|
+
XDG_CONFIG_HOME: join(folder, '.config'), XDG_STATE_HOME: join(folder, '.local', 'state'),
|
|
301
|
+
XDG_DATA_HOME: join(folder, '.local', 'share'), XDG_CACHE_HOME: join(folder, '.cache'),
|
|
302
|
+
APPDATA: join(folder, 'AppData', 'Roaming'), LOCALAPPDATA: join(folder, 'AppData', 'Local'),
|
|
303
|
+
[variable]: folder };
|
|
268
304
|
try {
|
|
269
|
-
answer = await run(
|
|
305
|
+
answer = await run(install.path, statusProbe.args, { ...(statusProbe.stdin === undefined ? {} : { stdin: statusProbe.stdin }), timeoutMs, env });
|
|
270
306
|
}
|
|
271
307
|
catch {
|
|
272
308
|
answer = undefined;
|
|
@@ -329,14 +365,27 @@ export function isAutoInstallShim(file, readFile = readHead) {
|
|
|
329
365
|
}
|
|
330
366
|
return head !== undefined && head.startsWith('#!') && head.includes('mise');
|
|
331
367
|
}
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
// the agent on first start). `missing` stays for callers that probe kinds Herdr never installs.
|
|
368
|
+
// Muse has no absent-CLI auto-installer in stock Herdr. Its official launcher needs the
|
|
369
|
+
// selected native release too. Other kinds retain their legacy shim classification.
|
|
335
370
|
export function agentInstallState(kind, o) {
|
|
336
371
|
const path = o?.path ?? agentProbePath();
|
|
337
372
|
const found = resolveAgentBinary(kind, { path, ...(o?.aliases === undefined ? {} : { aliases: o.aliases }) });
|
|
338
373
|
if (found.path === undefined)
|
|
339
|
-
return { kind, state: 'installs-on-first-start' };
|
|
374
|
+
return { kind, state: kind === 'muse' ? 'missing' : 'installs-on-first-start' };
|
|
375
|
+
if (kind === 'muse') {
|
|
376
|
+
try {
|
|
377
|
+
if (!statSync(found.path).isFile())
|
|
378
|
+
return { kind, state: 'missing', path: found.path };
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return { kind, state: 'missing', path: found.path };
|
|
382
|
+
}
|
|
383
|
+
const head = (o?.readFile ?? readHead)(found.path);
|
|
384
|
+
if (head?.startsWith('#!') && (head.includes('mise') || (head.includes('MUSE_CHANNEL') && !museNative(found.path)))) {
|
|
385
|
+
return { kind, state: 'missing', path: found.path };
|
|
386
|
+
}
|
|
387
|
+
return { kind, state: 'installed', path: found.path };
|
|
388
|
+
}
|
|
340
389
|
const shim = isAutoInstallShim(found.path, o?.readFile);
|
|
341
390
|
return shim ? { kind, state: 'installs-on-first-start', path: found.path }
|
|
342
391
|
: { kind, state: 'installed', path: found.path };
|
|
@@ -346,6 +395,8 @@ export function agentInstallState(kind, o) {
|
|
|
346
395
|
// finished, a placement that made no pane, or any other rejected start.
|
|
347
396
|
export function classifyStartFailure(error, o) {
|
|
348
397
|
const code = codeOf(error);
|
|
398
|
+
if (code === 'agent_not_installed')
|
|
399
|
+
return 'not-installed';
|
|
349
400
|
if (code !== undefined && START_RETRYABLE.has(code))
|
|
350
401
|
return 'pane-busy';
|
|
351
402
|
if (o?.stage === 'placement')
|
|
@@ -355,6 +406,8 @@ export function classifyStartFailure(error, o) {
|
|
|
355
406
|
return 'start-rejected';
|
|
356
407
|
}
|
|
357
408
|
function launchFailureWords(reason, kind) {
|
|
409
|
+
if (reason === 'not-installed')
|
|
410
|
+
return words('agent.notInstalled');
|
|
358
411
|
if (reason === 'pane-busy')
|
|
359
412
|
return words('agent.notReady');
|
|
360
413
|
if (reason === 'install-failed')
|
|
@@ -396,7 +449,7 @@ const STATUS_PROBES = {
|
|
|
396
449
|
}
|
|
397
450
|
if (!isRecord(status))
|
|
398
451
|
return 'unknown';
|
|
399
|
-
return status.loggedIn === true ? 'yes' : 'no';
|
|
452
|
+
return status.loggedIn === true ? 'yes' : status.loggedIn === false ? 'no' : 'unknown';
|
|
400
453
|
} },
|
|
401
454
|
// Codex exposes sign-in only over its app-server protocol, so the probe pipes a pipelined
|
|
402
455
|
// `initialize` + `account/read` round (same method muxr's planIdentity uses) and reads the
|
|
@@ -413,7 +466,10 @@ const STATUS_PROBES = {
|
|
|
413
466
|
continue;
|
|
414
467
|
}
|
|
415
468
|
if (isRecord(message) && message.id === 2) {
|
|
416
|
-
|
|
469
|
+
if (!isRecord(message.result))
|
|
470
|
+
return 'unknown';
|
|
471
|
+
return isRecord(message.result.account) ? 'yes'
|
|
472
|
+
: message.result.account === null || !Object.hasOwn(message.result, 'account') ? 'no' : 'unknown';
|
|
417
473
|
}
|
|
418
474
|
}
|
|
419
475
|
return 'unknown';
|
|
@@ -422,8 +478,12 @@ const STATUS_PROBES = {
|
|
|
422
478
|
// Default runner: one bounded spawn per probe, stdin piped when the protocol needs it (codex).
|
|
423
479
|
// Failures (missing binary, timeout, non-empty stderr, empty stdout) read as no answer — never throw.
|
|
424
480
|
export async function runStatusCommand(command, args, o) {
|
|
481
|
+
// Public direct calls without an explicit isolated HOME are not allowed to read a login.
|
|
482
|
+
if (!o?.env?.HOME || !isAbsolute(o.env.HOME) || /[\r\n\0]/.test(o.env.HOME))
|
|
483
|
+
return undefined;
|
|
425
484
|
return new Promise((resolve) => {
|
|
426
485
|
let done = false;
|
|
486
|
+
let timer;
|
|
427
487
|
const finish = (value) => {
|
|
428
488
|
if (done)
|
|
429
489
|
return;
|
|
@@ -433,14 +493,14 @@ export async function runStatusCommand(command, args, o) {
|
|
|
433
493
|
};
|
|
434
494
|
let child;
|
|
435
495
|
try {
|
|
436
|
-
child = spawn(command, args, { stdio: ['pipe', 'pipe', 'ignore'], timeout: o
|
|
496
|
+
child = spawn(command, args, { env: o.env, stdio: ['pipe', 'pipe', 'ignore'], timeout: o.timeoutMs ?? STATUS_TIMEOUT_MS });
|
|
437
497
|
}
|
|
438
498
|
catch {
|
|
439
499
|
finish(undefined);
|
|
440
500
|
return;
|
|
441
501
|
}
|
|
442
502
|
let out = '';
|
|
443
|
-
|
|
503
|
+
timer = setTimeout(() => { child.kill('SIGKILL'); finish(undefined); }, (o?.timeoutMs ?? STATUS_TIMEOUT_MS) + 500);
|
|
444
504
|
child.on('error', () => finish(undefined));
|
|
445
505
|
const stdout = child.stdout;
|
|
446
506
|
const stdin = child.stdin;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export { installMuse, museReadiness, MUSE_INSTALL_URL } from './muse.ts';
|
|
2
|
+
export type { InstallMuseOptions, MuseReadiness, MuseInstallReceipt, MuseInstallResult } from './muse.ts';
|
|
1
3
|
export { HERDR_VERSION, HERDR_PROTOCOL } from './constants.ts';
|
|
4
|
+
export { accountKinds, accountKind, type AccountKind } from './kinds.ts';
|
|
2
5
|
export { HerdrKit } from './kit.ts';
|
|
3
6
|
export { agentWords, stateWords, words, WORDS, type WordKey } from './words.ts';
|
|
4
7
|
export { agentProbePath, extraPathDirs, runStatusCommand, agentInstallState, isAutoInstallShim, resolveAgentBinary, classifyStartFailure } from './agents.ts';
|
|
5
|
-
export type { AgentCliSignIn, AgentInstallProbe, AgentInstallState, AgentLaunchFailureReason, AgentReadiness, AgentStartEvent, AgentStatusOptions, AgentStatusRunner, AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrProtocolRange, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, HerdrTransport, Move, MoveToAccount, MoveToAccountResult, MoveResult, OpenSignInTab, PromptReceipt, StartAgent, TerminalSession, AgentTurnEnd, AgentTurnOptions, AgentTurnFiles, AgentTurnResult, AgentTurnResultPolicy, } from './types.ts';
|
|
8
|
+
export type { AgentCliSignIn, AgentInstallProbe, AgentInstallState, AgentLaunchFailureReason, AgentReadiness, AgentStartEvent, AgentStatusOptions, AgentStatusRunner, AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrProtocolRange, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrSubscribeStop, HerdrTransport, BusyHandoff, Move, MoveToAccount, MoveToAccountResult, MoveResult, OpenSignInTab, PromptReceipt, StartAgent, TerminalSession, AgentTurnEnd, AgentTurnOptions, AgentTurnFiles, AgentTurnResult, AgentTurnResultPolicy, } from './types.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// `@byokit/herdr` — the Herdr runtime kit (Node host side). The phone and browser side lives in `./device`, the
|
|
2
2
|
// host-side link adapter in `./link`; the fake Herdr and contract suite arrive with H6 in `./testing`.
|
|
3
|
+
export { installMuse, museReadiness, MUSE_INSTALL_URL } from "./muse.js";
|
|
3
4
|
export { HERDR_VERSION, HERDR_PROTOCOL } from "./constants.js";
|
|
5
|
+
export { accountKinds, accountKind } from "./kinds.js";
|
|
4
6
|
export { HerdrKit } from "./kit.js";
|
|
5
7
|
export { agentWords, stateWords, words, WORDS } from "./words.js";
|
|
6
8
|
export { agentProbePath, extraPathDirs, runStatusCommand, agentInstallState, isAutoInstallShim, resolveAgentBinary, classifyStartFailure } from "./agents.js";
|
package/dist/kinds.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Pinned capability data, not login readiness or native move qualification. */
|
|
2
|
+
export type AccountKind = {
|
|
3
|
+
kind: string;
|
|
4
|
+
aliases: string[];
|
|
5
|
+
aliasPattern?: string;
|
|
6
|
+
folderVar?: string;
|
|
7
|
+
resume: boolean;
|
|
8
|
+
sessionKinds: ('id' | 'path')[];
|
|
9
|
+
resumeArgs?: string[];
|
|
10
|
+
loginLabel: string;
|
|
11
|
+
billing: 'unknown';
|
|
12
|
+
offer: 'explicit';
|
|
13
|
+
upstream: {
|
|
14
|
+
version: string;
|
|
15
|
+
revision: string;
|
|
16
|
+
source: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare function accountKinds(): AccountKind[];
|
|
20
|
+
export declare function accountKind(kind: string): AccountKind | undefined;
|
package/dist/kinds.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import table from './kinds.json' with { type: 'json' };
|
|
2
|
+
export function accountKinds() { return structuredClone(table); }
|
|
3
|
+
export function accountKind(kind) {
|
|
4
|
+
return accountKinds().find((row) => row.kind === kind || row.aliases.includes(kind)
|
|
5
|
+
|| (row.aliasPattern !== undefined && new RegExp(row.aliasPattern).test(kind)));
|
|
6
|
+
}
|