@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/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
- const env = o.env === undefined || launch ? {} : { env: o.env };
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 === 'pi' ? PI_INSTALL_HINT : `Install the ${binary} command, then check again.`;
259
- // A shim or nothing on PATH means Herdr installs on first start: not installed, with the
260
- // readiness words instead of a missing-install error (pi included — a real pi binary is
261
- // the only case that reads installed).
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
- if (statusProbe === undefined)
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(statusProbe.command, statusProbe.args, { ...(statusProbe.stdin === undefined ? {} : { stdin: statusProbe.stdin }), timeoutMs });
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
- // Per-kind install readiness sharing the B5 probe path: `installed` for a real runnable binary,
333
- // `installs-on-first-start` for an auto-install launcher/shim or nothing on PATH (Herdr fetches
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
- return isRecord(message.result) && isRecord(message.result.account) ? 'yes' : 'no';
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?.timeoutMs ?? STATUS_TIMEOUT_MS });
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
- const timer = setTimeout(() => { child.kill('SIGKILL'); finish(undefined); }, (o?.timeoutMs ?? STATUS_TIMEOUT_MS) + 500);
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";
@@ -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
+ }