@consiliency/agent-board-cli 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ export interface BakedAssets {
2
+ /** `@consiliency/agent-board-mcp`'s version: the codex, grok and agy stanza pins it. */
3
+ mcpVersion: string;
4
+ piVersion: string;
5
+ opencodeVersion: string;
6
+ /** The train tag this build is released as (`v<x.y.z>`), from CHANGELOG.md's first heading. */
7
+ releaseTag: string;
8
+ }
9
+ export declare const BAKED_ASSET_KEYS: readonly ["mcpVersion", "piVersion", "opencodeVersion", "releaseTag"];
10
+ /**
11
+ * A missing or malformed bake. Never defaulted: an unpinned version is a different install. It
12
+ * carries no `code`: it is not a cause-code site (fleet_diagnosability #403), and setup names it.
13
+ */
14
+ export declare class BakedAssetsError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export declare const ASSETS_URL: URL;
18
+ export declare const KEYSLOT_URL: URL;
19
+ /** The build's baked versions and release tag, validated field by field. */
20
+ export declare function bakedAssets(file?: string | URL): BakedAssets;
21
+ /** The packaged keyslot directory (no trailing slash). Throws when the build did not copy it. */
22
+ export declare function resolveKeyslotDir(url?: URL): string;
package/dist/assets.js ADDED
@@ -0,0 +1,61 @@
1
+ // SETUP IF-0-SETUP-4 — what the cli build baked (`dist/assets.json`) and where the packaged keyslot
2
+ // scripts are. Both are written by `scripts/lib/cli-assets.mjs --write`, which the cli `build` runs;
3
+ // `cli-assets.mjs --check` holds them equal to the tree.
4
+ //
5
+ // Paths are resolved from this module's own URL, one level up, so the same spelling lands on the
6
+ // package's `dist/` and `keyslot/` whether this runs as `dist/assets.js` (installed) or as
7
+ // `src/assets.ts` (the test suite, after a build).
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ export const BAKED_ASSET_KEYS = Object.freeze(["mcpVersion", "piVersion", "opencodeVersion", "releaseTag"]);
11
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
12
+ const RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
13
+ /**
14
+ * A missing or malformed bake. Never defaulted: an unpinned version is a different install. It
15
+ * carries no `code`: it is not a cause-code site (fleet_diagnosability #403), and setup names it.
16
+ */
17
+ export class BakedAssetsError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "BakedAssetsError";
21
+ }
22
+ }
23
+ export const ASSETS_URL = new URL("../dist/assets.json", import.meta.url);
24
+ export const KEYSLOT_URL = new URL("../keyslot/", import.meta.url);
25
+ /** The build's baked versions and release tag, validated field by field. */
26
+ export function bakedAssets(file = ASSETS_URL) {
27
+ let raw;
28
+ try {
29
+ raw = JSON.parse(readFileSync(file, "utf8"));
30
+ }
31
+ catch {
32
+ throw new BakedAssetsError("dist/assets.json is absent or unreadable: the cli build did not run");
33
+ }
34
+ const record = raw;
35
+ if (!record || typeof record !== "object" || Array.isArray(record))
36
+ throw new BakedAssetsError("dist/assets.json is not an object");
37
+ const keys = Object.keys(record);
38
+ if (keys.length !== BAKED_ASSET_KEYS.length || !BAKED_ASSET_KEYS.every((key) => keys.includes(key))) {
39
+ throw new BakedAssetsError(`dist/assets.json must hold exactly ${BAKED_ASSET_KEYS.join(", ")}`);
40
+ }
41
+ for (const key of ["mcpVersion", "piVersion", "opencodeVersion"]) {
42
+ if (typeof record[key] !== "string" || !VERSION.test(record[key]))
43
+ throw new BakedAssetsError(`dist/assets.json ${key} is not a version`);
44
+ }
45
+ if (typeof record.releaseTag !== "string" || !RELEASE_TAG.test(record.releaseTag)) {
46
+ throw new BakedAssetsError("dist/assets.json releaseTag is not v<x.y.z>");
47
+ }
48
+ return {
49
+ mcpVersion: record.mcpVersion,
50
+ piVersion: record.piVersion,
51
+ opencodeVersion: record.opencodeVersion,
52
+ releaseTag: record.releaseTag,
53
+ };
54
+ }
55
+ /** The packaged keyslot directory (no trailing slash). Throws when the build did not copy it. */
56
+ export function resolveKeyslotDir(url = KEYSLOT_URL) {
57
+ const dir = fileURLToPath(url).replace(/\/+$/, "");
58
+ if (!existsSync(`${dir}/status.sh`))
59
+ throw new BakedAssetsError("keyslot/ is absent: the cli build did not copy the keyslot scripts");
60
+ return dir;
61
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "mcpVersion": "1.31.1",
3
+ "piVersion": "0.4.1",
4
+ "opencodeVersion": "0.2.1",
5
+ "releaseTag": "v1.53.0"
6
+ }
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
- import { type EnrollKeystore, type EnrollmentIntent, type FleetOnboardingIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike, type AgentJoinDeployment, type AgentJoinInstructions, type DeviceAuthorizationPrompt } from "@consiliency/agent-board-client";
2
+ import { createAgentBoardClient, type EnrollKeystore, type EnrollmentIntent, type FleetOnboardingIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike, type AgentJoinDeployment, type AgentJoinInstructions, type DeviceAuthorizationPrompt, resolveCredentials } from "@consiliency/agent-board-client";
3
3
  import { type DoctorOptions, type DoctorReport } from "./doctor.js";
4
+ import { type SetupEnroller, type SetupRunner } from "./setup.js";
4
5
  import { DevicePendingEnrollmentStore } from "./enroll_runtime.js";
5
6
  type JsonObject = Record<string, unknown>;
6
7
  export interface CliIo {
@@ -208,6 +209,11 @@ export interface DefaultEnrollmentRunnerDeps {
208
209
  sessionUrl?: string;
209
210
  /** Anon apikey for the post-enroll board RPCs. Default: resolveEnrollPublishableKey(env). */
210
211
  publishableKey?: string;
212
+ /**
213
+ * SETUP: the key reference the `key` step produced, handed over directly (never an env var the
214
+ * user must set). Wins over `MESSAGE_BOARD_ENROLL_KEY_REFERENCE`. Default: env.
215
+ */
216
+ keyReference?: string;
211
217
  /** Clock (tests). Default: wall clock. */
212
218
  now?: () => number;
213
219
  /**
@@ -224,6 +230,11 @@ export interface DefaultEnrollmentRunnerDeps {
224
230
  };
225
231
  }
226
232
  export declare function defaultEnrollmentRunner(env?: NodeJS.ProcessEnv, deps?: DefaultEnrollmentRunnerDeps): EnrollmentCommandRunner;
233
+ /**
234
+ * SETUP — the enrollment `agent-board setup` runs: this runner's device flow, built from setup's
235
+ * env with the `key` step's reference injected (never an env var the user must set).
236
+ */
237
+ export declare const setupEnroller: SetupEnroller;
227
238
  /**
228
239
  * How each field of a `FleetOnboardingIntent` is turned from an argv string into the value
229
240
  * that goes on the wire and into the template (Consiliency/message-board#254).
@@ -306,6 +317,29 @@ type FleetTupleField = StringFieldsOf<FleetOnboardingIntent>;
306
317
  export declare const FLEET_TUPLE_DISPOSITION: Readonly<Record<FleetTupleField, FleetTupleDisposition>>;
307
318
  /** `runDoctor`'s shape; a seam so the exit-code wiring is testable without a live board. */
308
319
  export type DoctorRunner = (options: DoctorOptions) => Promise<DoctorReport>;
309
- export declare function createProgram(client: ClientLike, io: CliIo, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner): Command;
310
- export declare function runCli(argv: string[], io?: CliIo, client?: ClientLike, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner): Promise<void>;
320
+ /**
321
+ * SETUP IF-0-SETUP-1 — the global `--as <harness>`: which harness's identity FILE every command's
322
+ * credentials come from. The client is constructed before argv is parsed, so the choice is a
323
+ * holder the parse fills and the client's credential resolver reads at FIRST USE (see
324
+ * `fileAwareCredentials`). Unset, the resolver is today's env chain, unchanged.
325
+ */
326
+ export interface CredentialSelection {
327
+ as?: string;
328
+ }
329
+ /**
330
+ * The lazy credential source. With `--as <h>`: `<h>`'s identity file through the client's trusted
331
+ * open, and ONLY the file — `fileIdentityEnv` strips every ambient identity and generic name, as
332
+ * doctor does, because `--as` is an explicit choice of the file. The runtime descriptor is ignored
333
+ * for the same reason. Without it: `resolveCredentials(descriptor, env)`, exactly as before.
334
+ */
335
+ export declare function fileAwareCredentials(selection: CredentialSelection, env?: NodeJS.ProcessEnv): (descriptor?: Parameters<typeof resolveCredentials>[0]) => Promise<import("@consiliency/agent-board-client").ResolvedCredentials>;
336
+ export declare function createProgram(client: ClientLike, io: CliIo, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner, selection?: CredentialSelection, setup?: SetupRunner): Command;
337
+ export interface RunCliOptions {
338
+ /** The env the default client's credential resolver reads. Default `process.env`. */
339
+ env?: NodeJS.ProcessEnv;
340
+ /** Extra options for the DEFAULT client (tests: a `dataSourceFactory`); never its resolver. */
341
+ clientOptions?: Omit<NonNullable<Parameters<typeof createAgentBoardClient>[0]>, "resolveCredentials">;
342
+ setup?: SetupRunner;
343
+ }
344
+ export declare function runCli(argv: string[], io?: CliIo, client?: ClientLike, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner, options?: RunCliOptions): Promise<void>;
311
345
  export {};
package/dist/cli.js CHANGED
@@ -3,8 +3,9 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  import * as nodeFs from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
- import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, AUTHORED_CAUSE_CODES, codeProvenance, markAuthoredCode, assertIdentityHarness, identityFilePath, resolveHostSegment, identityConfigRoot, walkTrustedDirectory, IDENTITY_HARNESSES, CredentialResolutionError, } from "@consiliency/agent-board-client";
7
- import { formatDoctorReport, runDoctor } from "./doctor.js";
6
+ import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, AUTHORED_CAUSE_CODES, codeProvenance, markAuthoredCode, assertIdentityHarness, identityFilePath, resolveHostSegment, identityConfigRoot, walkTrustedDirectory, IDENTITY_HARNESSES, CredentialResolutionError, readIdentityFile, resolveCredentials, } from "@consiliency/agent-board-client";
7
+ import { fileIdentityEnv, formatDoctorReport, runDoctor } from "./doctor.js";
8
+ import { addSetupCommand, runSetup } from "./setup.js";
8
9
  import { identityValuesFromEnrollment, resolveIdentityDeployment, writeIdentityFile, } from "./identity_writer.js";
9
10
  import { DevicePendingEnrollmentStore, FilePendingEnrollmentStore, FleetDeviceContinuationStore, makeBrowserOpener, prepareFleetRuntimeOutput, publishFinalizedFleetTemplate, } from "./enroll_runtime.js";
10
11
  function isRuntimeStatus(value) {
@@ -1628,11 +1629,15 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1628
1629
  const boardFetch = deps.boardFetch ?? nodeFetchAdapter;
1629
1630
  // Post-enroll board-op wiring (raw Supabase domain): explicit /session URL + anon
1630
1631
  // apikey. Optional — absent preserves the ${boardUrl}/session default + no apikey.
1631
- const sessionUrl = deps.sessionUrl ?? resolveEnrollSessionUrl(env);
1632
- const publishableKey = deps.publishableKey ?? resolveEnrollPublishableKey(env);
1632
+ // LAZY (SETUP): the runner is built before parsing, for every command, so constructing it
1633
+ // reads nothing from env; each flow method resolves these when enrollment actually runs.
1634
+ const sessionUrl = () => deps.sessionUrl ?? resolveEnrollSessionUrl(env);
1635
+ const publishableKey = () => deps.publishableKey ?? resolveEnrollPublishableKey(env);
1636
+ // SETUP: the `key` step's reference is injected, and wins over env.
1637
+ const descriptorOf = () => resolveEnrollmentDescriptor(env, deps.keyReference === undefined ? {} : { keyReference: deps.keyReference });
1633
1638
  return {
1634
1639
  async run(input) {
1635
- const descriptor = resolveEnrollmentDescriptor(env);
1640
+ const descriptor = descriptorOf();
1636
1641
  const enrollEndpoint = deps.enrollEndpoint ?? resolveEnrollEndpoint(env);
1637
1642
  const keystore = deps.keystore ?? buildEnrollKeystore(descriptor, env, {
1638
1643
  ...(input.intent.kind === "fleet_onboarding" ? { fleetOnboarding: input.intent } : {}),
@@ -1649,14 +1654,14 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1649
1654
  keystore,
1650
1655
  boardFetch,
1651
1656
  keystoreBackends: keystore.backends,
1652
- sessionUrl,
1653
- publishableKey,
1657
+ sessionUrl: sessionUrl(),
1658
+ publishableKey: publishableKey(),
1654
1659
  openAuthorization,
1655
1660
  now: deps.now,
1656
1661
  });
1657
1662
  },
1658
1663
  async device(input) {
1659
- const descriptor = resolveEnrollmentDescriptor(env);
1664
+ const descriptor = descriptorOf();
1660
1665
  // Fail CLEAN and actionable when the host descriptor carries no device
1661
1666
  // endpoint — otherwise the shared flow throws a bare `no_device_endpoint`.
1662
1667
  // The endpoint is optional in resolveEnrollmentDescriptor (it is not required
@@ -1684,15 +1689,15 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1684
1689
  keystore,
1685
1690
  boardFetch,
1686
1691
  keystoreBackends: keystore.backends,
1687
- sessionUrl,
1688
- publishableKey,
1692
+ sessionUrl: sessionUrl(),
1693
+ publishableKey: publishableKey(),
1689
1694
  onDeviceAuthorization: input.onDeviceAuthorization,
1690
1695
  sleep: deps.sleep,
1691
1696
  now: deps.now,
1692
1697
  });
1693
1698
  },
1694
1699
  async deviceBegin(input) {
1695
- const descriptor = resolveEnrollmentDescriptor(env);
1700
+ const descriptor = descriptorOf();
1696
1701
  if (!descriptor.issuer.deviceAuthorizationEndpoint) {
1697
1702
  throw new EnrollmentDescriptorError("missing_device_endpoint", "device enrollment needs a device authorization endpoint (set MESSAGE_BOARD_ENROLL_DEVICE_AUTHORIZATION_ENDPOINT)");
1698
1703
  }
@@ -1703,8 +1708,8 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1703
1708
  ...(input.intent.kind === "fleet_onboarding" ? { fleetOnboarding: input.intent } : {}),
1704
1709
  }),
1705
1710
  boardFetch,
1706
- sessionUrl,
1707
- publishableKey,
1711
+ sessionUrl: sessionUrl(),
1712
+ publishableKey: publishableKey(),
1708
1713
  onDeviceAuthorization: async () => { },
1709
1714
  now: deps.now,
1710
1715
  });
@@ -1776,12 +1781,12 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1776
1781
  if (pending.slug !== input.expectedFleet.slug) {
1777
1782
  throw new EnrollmentCommandError("fleet_device_slug_mismatch", "device enrollment agent slug does not match");
1778
1783
  }
1779
- const currentReference = normalizeKeyReference(resolveEnrollmentDescriptor(env).keyReference);
1784
+ const currentReference = normalizeKeyReference(descriptorOf().keyReference);
1780
1785
  if (`${currentReference.resolver}:${currentReference.handle}` !== input.expectedFleet.keyReference) {
1781
1786
  throw new EnrollmentCommandError("fleet_device_key_reference_mismatch", "device enrollment key reference does not match current configuration");
1782
1787
  }
1783
1788
  }
1784
- const descriptor = resolveEnrollmentDescriptor(env);
1789
+ const descriptor = descriptorOf();
1785
1790
  if (!descriptor.issuer.deviceAuthorizationEndpoint) {
1786
1791
  throw new EnrollmentDescriptorError("missing_device_endpoint", "device enrollment needs a device authorization endpoint (set MESSAGE_BOARD_ENROLL_DEVICE_AUTHORIZATION_ENDPOINT)");
1787
1792
  }
@@ -1809,8 +1814,8 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1809
1814
  keystore,
1810
1815
  boardFetch,
1811
1816
  keystoreBackends: keystore.backends,
1812
- sessionUrl,
1813
- publishableKey,
1817
+ sessionUrl: sessionUrl(),
1818
+ publishableKey: publishableKey(),
1814
1819
  onDeviceAuthorization: async () => { },
1815
1820
  sleep: deps.sleep,
1816
1821
  now: deps.now,
@@ -1846,7 +1851,7 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1846
1851
  async begin(input) {
1847
1852
  // Force OOB: a loopback receiver cannot bridge two separate CLI processes, so
1848
1853
  // the two-step path is always out-of-band paste (no redirectUri ⇒ OOB).
1849
- const descriptor = resolveEnrollmentDescriptor(env);
1854
+ const descriptor = descriptorOf();
1850
1855
  const store = deps.store ?? new FilePendingEnrollmentStore(env);
1851
1856
  return beginEnrollment({
1852
1857
  descriptor,
@@ -1861,13 +1866,13 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1861
1866
  clientId: descriptor.issuer.clientId,
1862
1867
  // #173 — pass the provisioning config so begin refuses a doomed raw-Supabase
1863
1868
  // enrollment BEFORE the human is asked to approve anything.
1864
- sessionUrl,
1865
- publishableKey,
1869
+ sessionUrl: sessionUrl(),
1870
+ publishableKey: publishableKey(),
1866
1871
  now: deps.now,
1867
1872
  });
1868
1873
  },
1869
1874
  async complete(input) {
1870
- const descriptor = resolveEnrollmentDescriptor(env);
1875
+ const descriptor = descriptorOf();
1871
1876
  const enrollEndpoint = deps.enrollEndpoint ?? resolveEnrollEndpoint(env);
1872
1877
  const store = deps.store ?? new FilePendingEnrollmentStore(env);
1873
1878
  const pending = await store.get(input.enrollmentId);
@@ -1885,13 +1890,21 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
1885
1890
  keystore,
1886
1891
  boardFetch,
1887
1892
  keystoreBackends: keystore.backends,
1888
- sessionUrl,
1889
- publishableKey,
1893
+ sessionUrl: sessionUrl(),
1894
+ publishableKey: publishableKey(),
1890
1895
  now: deps.now,
1891
1896
  });
1892
1897
  },
1893
1898
  };
1894
1899
  }
1900
+ /**
1901
+ * SETUP — the enrollment `agent-board setup` runs: this runner's device flow, built from setup's
1902
+ * env with the `key` step's reference injected (never an env var the user must set).
1903
+ */
1904
+ export const setupEnroller = async (input) => {
1905
+ const runner = defaultEnrollmentRunner(input.env, { keyReference: input.keyReference });
1906
+ return (await runner.device({ slug: input.slug, intent: input.intent, onDeviceAuthorization: input.onDeviceAuthorization }));
1907
+ };
1895
1908
  function intentFromOptions(options) {
1896
1909
  if (options.board === false) {
1897
1910
  return { kind: "none" };
@@ -3199,11 +3212,33 @@ function addDoctorCommand(program, io, doctor) {
3199
3212
  process.exitCode = report.exit;
3200
3213
  });
3201
3214
  }
3202
- export function createProgram(client, io, enrollment = defaultEnrollmentRunner(), doctor = runDoctor) {
3215
+ /**
3216
+ * The lazy credential source. With `--as <h>`: `<h>`'s identity file through the client's trusted
3217
+ * open, and ONLY the file — `fileIdentityEnv` strips every ambient identity and generic name, as
3218
+ * doctor does, because `--as` is an explicit choice of the file. The runtime descriptor is ignored
3219
+ * for the same reason. Without it: `resolveCredentials(descriptor, env)`, exactly as before.
3220
+ */
3221
+ export function fileAwareCredentials(selection, env = process.env) {
3222
+ return async (descriptor) => {
3223
+ const harness = selection.as;
3224
+ if (harness === undefined)
3225
+ return resolveCredentials(descriptor, env);
3226
+ assertIdentityHarness(harness);
3227
+ const { values } = await readIdentityFile({ harness, env });
3228
+ return resolveCredentials(undefined, fileIdentityEnv(env, values));
3229
+ };
3230
+ }
3231
+ export function createProgram(client, io, enrollment = defaultEnrollmentRunner(), doctor = runDoctor, selection = {}, setup) {
3203
3232
  const program = new Command()
3204
3233
  .name("agent-board")
3205
3234
  .description("Thin CLI surface for the agent_board RPC and canonical read contracts")
3235
+ .option("--as <harness>", "take credentials from this harness's identity file (agy, claude, codex, grok, opencode, pi), ignoring any identity in the environment")
3206
3236
  .addHelpText("after", "\nAuth: user-scoped agent sessions are the default. Service-role execution is reserved for callback workers and vetted operational scripts through explicit client configuration, not the normal agent CLI path. Signing configuration is inherited from metadata-safe descriptors; raw private-key arguments are not accepted.\n");
3237
+ // Filled at parse time, read by the client's resolver at first use.
3238
+ program.hook("preAction", () => {
3239
+ const chosen = program.opts().as;
3240
+ selection.as = chosen;
3241
+ });
3207
3242
  program
3208
3243
  .command("signing-status")
3209
3244
  .description("Print metadata-safe signing descriptor status; raw private-key arguments are not accepted")
@@ -3224,12 +3259,19 @@ export function createProgram(client, io, enrollment = defaultEnrollmentRunner()
3224
3259
  addInviteCommands(program, client, io);
3225
3260
  addEnrollCommands(program, io, enrollment);
3226
3261
  addDoctorCommand(program, io, doctor);
3262
+ addSetupCommand(program, io, setup ?? ((options) => runSetup(options, { enroll: setupEnroller })));
3227
3263
  return program;
3228
3264
  }
3229
3265
  export async function runCli(argv, io = {
3230
3266
  writeStdout: (message) => process.stdout.write(message),
3231
3267
  writeStderr: (message) => process.stderr.write(message),
3232
- }, client = createAgentBoardClient(), enrollment = defaultEnrollmentRunner(), doctor = runDoctor) {
3233
- const program = createProgram(client, io, enrollment, doctor);
3268
+ }, client, enrollment = defaultEnrollmentRunner(), doctor = runDoctor, options = {}) {
3269
+ const selection = {};
3270
+ // Built BEFORE parsing, as always; `--as` reaches it through `selection` at first use.
3271
+ const resolved = client ?? createAgentBoardClient({
3272
+ ...options.clientOptions,
3273
+ resolveCredentials: fileAwareCredentials(selection, options.env ?? process.env),
3274
+ });
3275
+ const program = createProgram(resolved, io, enrollment, doctor, selection, options.setup);
3234
3276
  await program.parseAsync(argv, { from: "user" });
3235
3277
  }
@@ -0,0 +1,37 @@
1
+ /** What an edit found and produced. `text` equals the input when `prior` is `exact`. */
2
+ export interface ConfigEdit {
3
+ prior: "absent" | "exact" | "differs";
4
+ text: string;
5
+ }
6
+ /** A refusal: the reason is an authored code, never file content. */
7
+ export declare class ConfigEditRefused extends Error {
8
+ readonly reason: "config_unparseable" | "config_layout_unrecognised" | "config_not_an_object" | "config_inline_table" | "config_array_table";
9
+ constructor(reason: "config_unparseable" | "config_layout_unrecognised" | "config_not_an_object" | "config_inline_table" | "config_array_table");
10
+ }
11
+ /** A value an entry may carry: what setup writes, plus whatever a user kept in the entry. */
12
+ export type ConfigValue = string | number | boolean | readonly ConfigValue[] | {
13
+ readonly [key: string]: ConfigValue;
14
+ };
15
+ export type ConfigEntry = {
16
+ readonly [key: string]: ConfigValue;
17
+ };
18
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
19
+ /** The block setup writes for `path` (a table header plus one line per key, in the entry's order). */
20
+ export declare function renderTomlTable(path: readonly string[], entry: ConfigEntry): string;
21
+ /**
22
+ * Put `entry` at the table `path` (e.g. `["mcp_servers", "agent-board"]`) of a TOML document.
23
+ * Absent → appended as its own block; equal → the text unchanged; different → the table's own
24
+ * block(s) (its header, its sub-tables' headers, and their keys) replaced in place by the rendered
25
+ * block. Keys defined for it any other way (a dotted key under a parent table, an inline table)
26
+ * are a layout this edit does not own: refused, never appended beside.
27
+ */
28
+ export declare function upsertTomlTable(text: string, path: readonly string[], entry: ConfigEntry): ConfigEdit;
29
+ /**
30
+ * Put `entry` at `path` of a JSON/JSONC object document. Empty text is an empty object. Missing
31
+ * intermediate objects are created; an existing member is replaced in its span; a new member is
32
+ * inserted before its object's closing brace. `strict` also requires the input to be plain JSON,
33
+ * for a harness whose reader is strict (an edit never makes a file unreadable to its harness).
34
+ */
35
+ export declare function upsertJsonMember(text: string, path: readonly string[], entry: ConfigEntry, options?: {
36
+ strict?: boolean;
37
+ }): ConfigEdit;