Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983/** * `shard` CLI entrypoint. * * Dispatches on `process.argv` subcommand: * `shard web` (or no subcommand) → daemon-backed browser client. * `shard agent` → headless stdio server speaking the per-agent protocol. * `shard config <show|path>` → print loaded config (redacted) / the XDG config path. * `shard models <list|show>` → `/v1/models` discovery + config overrides. * * Configuration (config is canonical; env stays as overrides + secret fallback, per saga * 9mre6k): provider/model resolved from `~/.config/shard/config.json` (XDG) via * `loadConfig()` + `resolveModel()`. `--model`/`--provider` flags + `SHARD_MODEL`/ * `OPENAI_MODEL`/`SHARD_PROVIDER` env override config (flag > env > config — CI / one-off * runs win). API key read from the resolved provider's `auth.envVar` (bearer) at runtime — * NEVER persisted or logged. `--api-key`/`--base-url`/`OPENAI_BASE_URL` stay as test-only * escape hatches that override the resolver's output. Session path from `--session` or a * generated one under `~/.shard/sessions/<id>.jsonl`; cwd = `process.cwd()`. No hardcoded * secrets. */import { homedir } from 'node:os';import { join, resolve } from 'node:path';import { mkdirSync, existsSync } from 'node:fs';import { isIP } from 'node:net';import { createAgentSessionFromCore, createDefaultCore, DEFAULT_CONTEXT_WINDOW,} from '@/core/default';import { handleInlineApproval } from '@/cli/commands/inline-approval';import type { DaemonOptions } from '@/daemon';import type { ModelDescriptor } from '@/contracts/model';import type { ShardConfig } from '@/cli/config/schema';
const SUBCOMMANDS = new Set(['agent', 'config', 'models', 'web', 'daemon', 'shardd', 'gc', 'setup', 'new', 'worktree']);
/** Flags that consume the following argv element as their value. */const VALUE_FLAGS = new Set([ '--model', '-m', '--provider', '--session', '-s', '--api-key', '--base-url', '--cwd', '--id', '--socket', '--home', '--managed-manifest', '--daemon-socket', '--web-host', '--web-port', '--public-host', '--allowed-origin', '--trust-proxy',]);
/** Print usage to stderr + exit non-zero. */function usageExit(): never { process.stderr.write( 'Usage: shard [web|agent|daemon|config|models|setup|gc|new|worktree] [options]\n' + ' agent headless stdio server speaking the per-agent protocol (PROTOCOL v0.1)\n' + ' daemon long-lived multi-agent daemon `shardd` (unix-socket, PROTOCOL §6/§8/§10)\n' + ' config config subcommand: `config show` | `config path` | `config init`\n' + ' models model catalog: `models list [--provider p]` | `models show <alias>`\n' + ' new create a managed worktree or launch a fresh Agent in an existing workspace\n' + ' worktree list, inspect, and safely retire daemon-managed worktrees\n' + ' web start the daemon + browser client (prints URL); admin subcommands:\n' + ' approve <code>, clients, revoke <id|--all>, token create|rotate\n' + ' setup interactive first-provider config wizard\n' + ' gc remove empty orphaned agent directories under ~/.shard/agents\n' + 'Options: --model/-m, --provider, --session/-s, --api-key, --base-url, --cwd, --id, --socket, --home, --trust-project\n' + 'Daemon: --web-host <ip>, --web-port <port>, --public-host <host:port>, --allowed-origin <origin> (repeatable), --trust-proxy <loopback|ip>, --allow-insecure-remote\n', ); process.exit(1);}
/** Resolve the CLI options from argv + env. */interface CliOptions { modelName?: string; providerKey?: string; sessionPath: string; /** Working directory for tools. Defaults to `process.cwd()`. */ cwd: string; /** Agent id (stdio server); generated if not supplied. */ agentId?: string; /** Test-only escape hatch: overrides the resolver's bearer token. */ apiKey?: string; /** Test-only escape hatch: overrides the resolver's base URL. */ baseURL?: string; /** Explicitly trust project-local instructions, rules, skills, agents, and extensions. */ trustProject: boolean; runtimeGeneration?: string; daemonLease?: string; /** Managed-manifest path (saga u4uo9m.1): a daemon-managed subagent loads pinned * system_prompt/launch_policy/depth/timeout/alias/definition config from here instead * of argv, and connects back to the daemon over `daemonSocket`. */ managedManifest?: string; /** Daemon unix-socket path for a managed child to connect back to (saga u4uo9m.1). */ daemonSocket?: string;}
function parseOptions(rest: string[]): CliOptions { let modelName: string | undefined; let providerKey: string | undefined; let sessionPath: string | undefined; let apiKey: string | undefined; let baseURL: string | undefined; let cwd: string | undefined; let agentId: string | undefined; let trustProject = false; let runtimeGeneration: string | undefined; let daemonLease: string | undefined; let managedManifest: string | undefined; let daemonSocket: string | undefined;
for (let i = 0; i < rest.length; i++) { const arg = rest[i]; if (arg === undefined) continue; if (arg === '--model' || arg === '-m') { modelName = rest[++i]; } else if (arg === '--provider') { providerKey = rest[++i]; } else if (arg === '--session' || arg === '-s') { sessionPath = rest[++i]; } else if (arg === '--api-key') { apiKey = rest[++i]; } else if (arg === '--base-url') { baseURL = rest[++i]; } else if (arg === '--cwd') { cwd = rest[++i]; } else if (arg === '--id') { agentId = rest[++i]; } else if (arg === '--trust-project') { trustProject = true; } else if (arg === '--runtime-generation') { runtimeGeneration = rest[++i]; } else if (arg === '--daemon-lease') { daemonLease = rest[++i]; } else if (arg === '--managed-manifest') { managedManifest = rest[++i]; } else if (arg === '--daemon-socket') { daemonSocket = rest[++i]; } else if (arg.startsWith('--model=')) { modelName = arg.slice('--model='.length); } else if (arg.startsWith('--provider=')) { providerKey = arg.slice('--provider='.length); } else if (arg.startsWith('--session=')) { sessionPath = arg.slice('--session='.length); } else if (arg.startsWith('--api-key=')) { apiKey = arg.slice('--api-key='.length); } else if (arg.startsWith('--base-url=')) { baseURL = arg.slice('--base-url='.length); } else if (arg.startsWith('--cwd=')) { cwd = arg.slice('--cwd='.length); } else if (arg.startsWith('--id=')) { agentId = arg.slice('--id='.length); } else { process.stderr.write(`shard: unknown argument: ${arg}\n`); usageExit(); } }
const resolvedSession = sessionPath ?? defaultSessionPath(); // Ensure the parent dir exists (createDefaultCore's SessionStore.create also mkdir -p's, // but doing it here yields a cleaner error path before the SDK is built). mkdirSync(resolve(resolvedSession, '..'), { recursive: true });
return { ...(modelName !== undefined ? { modelName } : undefined), ...(providerKey !== undefined ? { providerKey } : undefined), sessionPath: resolvedSession, cwd: cwd ?? process.cwd(), trustProject, ...(agentId !== undefined ? { agentId } : undefined), ...(apiKey !== undefined ? { apiKey } : undefined), ...(baseURL !== undefined ? { baseURL } : undefined), ...(runtimeGeneration !== undefined ? { runtimeGeneration } : undefined), ...(daemonLease !== undefined ? { daemonLease } : undefined), ...(managedManifest !== undefined ? { managedManifest } : undefined), ...(daemonSocket !== undefined ? { daemonSocket } : undefined), };}
/** Default per-agent session path under `~/.shard/sessions/<id>.jsonl`. */function defaultSessionPath(): string { const sessionsDir = join(homedir(), '.shard', 'sessions'); if (!existsSync(sessionsDir)) { mkdirSync(sessionsDir, { recursive: true }); } const id = crypto.randomUUID(); return join(sessionsDir, `${id}.jsonl`);}
/** Extract a `--provider <value>` (or `--provider=value`) from a rest array. */function extractProviderFlag(rest: string[]): { providerKey?: string; leftover: string[] } { const leftover: string[] = []; let providerKey: string | undefined; for (let i = 0; i < rest.length; i++) { const a = rest[i]; if (a === undefined) continue; if (a === '--provider') { providerKey = rest[++i]; } else if (a.startsWith('--provider=')) { providerKey = a.slice('--provider='.length); } else { leftover.push(a); } } return { ...(providerKey !== undefined ? { providerKey } : undefined), leftover };}
/** Route `shard config <sub>` sub-subcommands. Returns exit code (process.exit'd inside). */async function runConfigSub(rest: string[]): Promise<number> { const { runConfigShow, runConfigPath, runConfigInit } = await import('@/cli/commands/config-cmd'); const sub = rest[0]; if (sub === 'show') return runConfigShow(); if (sub === 'path') return runConfigPath(); if (sub === 'init') return runConfigInit(); process.stderr.write('Usage: shard config <init|show|path>\n'); return 1;}
/** Parsed daemon/web flags shared by `shard daemon` and `shard web` (no admin sub). */interface DaemonFlagResult { socketPath?: string; home?: string; restartPolicy?: string; trustProject: boolean; /** Remote/web daemon options assembled from the --web-* / --public-host / origin flags. */ remoteOpts: DaemonOptions; help: boolean; /** First parse error (invalid flag value); the caller surfaces it. */ error?: { message: string; code: number };}
/** * Parse the daemon/web flag grammar shared by `shard daemon` and `shard web` (no admin * subcommand). Handles `--socket`, `--home`, `--restart`, `--trust-project`, and the * remote/web surface (`--web-host`, `--web-port`, `--public-host`, `--allowed-origin`, * `--trust-proxy`, `--allow-insecure-remote`). Unknown args are silently skipped * (matching the historical daemon parser). Returns the parsed result; `error`/`help` are * surfaced by the caller so each subcommand can print its own usage text. */function parseDaemonFlags(rest: string[]): DaemonFlagResult { let socketPath: string | undefined; let home: string | undefined; let restartPolicy: string | undefined; let trustProject = false; let webHost: string | undefined; let webPort: number | undefined; let publicHost: string | undefined; const allowedOrigins: string[] = []; let trustedProxyUpstream: string | undefined; let allowInsecureRemote = false; let help = false; let error: { message: string; code: number } | undefined; for (let i = 0; i < rest.length; i++) { const a = rest[i]; if (a === undefined) continue; if (a === '--socket') { const value = rest[++i]; if (socketPath !== undefined || value === undefined || value.length === 0 || value.startsWith('-')) { error = { message: 'shard: invalid or duplicate --socket flag', code: 2 }; break; } socketPath = value; } else if (a.startsWith('--socket=')) { const value = a.slice('--socket='.length); if (socketPath !== undefined || value.length === 0) { error = { message: 'shard: invalid or duplicate --socket flag', code: 2 }; break; } socketPath = value; } else if (a === '--home') { home = rest[++i]; } else if (a.startsWith('--home=')) { home = a.slice('--home='.length); } else if (a === '--restart') { restartPolicy = rest[++i]; } else if (a === '--trust-project') { trustProject = true; } else if (a.startsWith('--restart=')) { restartPolicy = a.slice('--restart='.length); } else if (a === '--web-host') { webHost = rest[++i]; if (webHost === undefined || webHost.length === 0) { error = { message: 'shard: --web-host requires a value', code: 2 }; break; } } else if (a.startsWith('--web-host=')) { webHost = a.slice('--web-host='.length); } else if (a === '--web-port') { const value = rest[++i]; const parsed = value === undefined ? NaN : Number(value); if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) { error = { message: `shard: invalid --web-port: ${value ?? ''}`, code: 2 }; break; } webPort = parsed; } else if (a.startsWith('--web-port=')) { const parsed = Number(a.slice('--web-port='.length)); if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) { error = { message: `shard: invalid --web-port: ${a.slice('--web-port='.length)}`, code: 2 }; break; } webPort = parsed; } else if (a === '--public-host') { publicHost = rest[++i]; if (publicHost === undefined || publicHost.length === 0) { error = { message: 'shard: --public-host requires a value', code: 2 }; break; } } else if (a.startsWith('--public-host=')) { publicHost = a.slice('--public-host='.length); } else if (a === '--allowed-origin') { const value = rest[++i]; if (value === undefined || value.length === 0) { error = { message: 'shard: --allowed-origin requires a value', code: 2 }; break; } allowedOrigins.push(value); } else if (a.startsWith('--allowed-origin=')) { allowedOrigins.push(a.slice('--allowed-origin='.length)); } else if (a === '--trust-proxy') { const value = rest[++i]; // `loopback` or an IPv4/IPv6 literal; reject anything else loudly (exit 2). if (value === undefined || (value !== 'loopback' && isIP(value) === 0)) { error = { message: `shard: --trust-proxy must be 'loopback' or an IP literal, got: ${value ?? ''}`, code: 2 }; break; } trustedProxyUpstream = value; } else if (a.startsWith('--trust-proxy=')) { const value = a.slice('--trust-proxy='.length); if (value.length === 0 || (value !== 'loopback' && isIP(value) === 0)) { error = { message: `shard: --trust-proxy must be 'loopback' or an IP literal, got: ${value}`, code: 2 }; break; } trustedProxyUpstream = value; } else if (a === '--allow-insecure-remote') { allowInsecureRemote = true; } else if (a === '-h' || a === '--help') { help = true; } // Unknown args are silently skipped (historical daemon parser behavior). }
const remoteOpts: DaemonOptions = { ...(webHost !== undefined ? { webHost } : {}), ...(webPort !== undefined ? { webPort } : {}), ...(publicHost !== undefined ? { publicHost } : {}), ...(allowedOrigins.length > 0 ? { allowedOrigins } : {}), ...(trustedProxyUpstream !== undefined ? { trustedProxy: { upstream: trustedProxyUpstream } } : {}), ...(allowInsecureRemote ? { allowInsecureRemote: true } : {}), };
return { ...(socketPath !== undefined ? { socketPath } : undefined), ...(home !== undefined ? { home } : undefined), ...(restartPolicy !== undefined ? { restartPolicy } : undefined), trustProject, remoteOpts, help, ...(error !== undefined ? { error } : undefined), };}
/** Info passed to the announce callback after the daemon boots. */interface DaemonAnnounceInfo { socketPath: string; webPort?: number; /** Effective web bind host (for URL construction). */ webHost: string; /** * True when no non-revoked browser principal is enrolled yet (first run). The announce * callback uses this — together with inline approval — to decide whether the printed URL * should carry `?flow=approve`: only the first approval run needs it; an already-approved * operator gets the plain URL. Absent when the web surface is disabled. */ needsEnrollment?: boolean;}
/** Options for {@link bootDaemon}. */interface BootDaemonOpts { /** * When true, the daemon process listens for browser enrollment requests and prompts the * operator to approve them inline (readline y/n) in the same terminal — no separate * `shard web approve <code>` shell. Enabled for `setup --web` and for `shard web` when * stdin is a TTY (saga vr7kgx.19). */ inlineApproval?: boolean;}
/** * Build a lazy `administer` closure bound to a daemon holder. The daemon is not yet * constructed when the enrollment callback is registered (it is created by `startDaemon` * one tick later), so we capture a mutable holder and read `.current` at call time. */function makeAdminister(getDaemon: () => { administer?: (cmd: { type: 'approve'; code: string }) => Promise<unknown> } | undefined) { return async (code: string): Promise<void> => { const daemon = getDaemon(); if (daemon?.administer === undefined) throw new Error('daemon is not ready'); await daemon.administer({ type: 'approve', code }); };}
/** * Boot the daemon from parsed flags: validate, start, announce, then wire SIGINT/SIGTERM * for graceful shutdown. Runs until a signal arrives (returns a never-resolving promise). * The `announce` callback lets `shard daemon` print the socket and `shard web` print the * URL from the same boot path. */async function bootDaemon( parsed: DaemonFlagResult, announce: (info: DaemonAnnounceInfo) => void, bootOpts?: BootDaemonOpts,): Promise<number> { if (parsed.restartPolicy !== undefined && !['never', 'always', 'on-crash'].includes(parsed.restartPolicy)) { process.stderr.write(`shard: invalid --restart policy: ${parsed.restartPolicy}\n`); return 2; } const { startDaemon, validateRemoteDaemonOptions } = await import('@/daemon'); try { validateRemoteDaemonOptions(parsed.remoteOpts); } catch (err) { process.stderr.write(`shard: ${(err as Error).message}\n`); return 1; } const webHost = parsed.remoteOpts.webHost ?? '127.0.0.1'; const inlineApproval = bootOpts?.inlineApproval ?? false; // The daemon is assigned after startDaemon resolves, but the enrollment callback is // registered DURING startDaemon. Capture a mutable holder so the closure reads the live // daemon when a browser actually connects (always after boot completes). let daemon: { address: string; stop(): Promise<void>; administer?: (cmd: { type: 'approve'; code: string }) => Promise<unknown> } | undefined; const administer = inlineApproval ? makeAdminister(() => daemon) : undefined; // Serialize prompts: skip a request that arrives while another is pending. let approvalPending = false; const onEnrollmentRequest = inlineApproval ? (enrollment: { code: string; expiresAt: number }): void => { if (approvalPending) return; approvalPending = true; void handleInlineApproval(enrollment, administer!).finally(() => { approvalPending = false; }); } : undefined; let webPort: number | undefined; let needsEnrollment = false; try { daemon = await startDaemon({ ...(parsed.socketPath !== undefined ? { socketPath: parsed.socketPath } : undefined), ...(parsed.home !== undefined ? { home: parsed.home } : undefined), ...(parsed.restartPolicy !== undefined ? { supervisor: { restart: parsed.restartPolicy as 'never' | 'always' | 'on-crash' } } : undefined), trustProject: parsed.trustProject, ...parsed.remoteOpts, ...(onEnrollmentRequest !== undefined ? { onEnrollmentRequest } : undefined), onReady: (info) => { webPort = info.webPort; if (info.webNeedsEnrollment !== undefined) needsEnrollment = info.webNeedsEnrollment; }, }); } catch (err) { process.stderr.write(`shard: failed to start daemon: ${(err as Error).message}\n`); return 1; } announce({ socketPath: daemon.address, webHost, ...(webPort !== undefined ? { webPort, needsEnrollment } : undefined), });
// Graceful shutdown on SIGINT/SIGTERM — the response path (`daemon.shutdown`) is for // clients; signals are for the operator. Stop is idempotent. let stopping = false; const stop = async (sig: NodeJS.Signals): Promise<void> => { if (stopping) return; stopping = true; process.stderr.write(`\nshardd: ${sig} received, shutting down...\n`); try { await daemon!.stop(); } catch (e) { process.stderr.write(`shardd: shutdown error: ${(e as Error).message}\n`); } process.exit(0); }; process.on('SIGINT', (sig) => void stop(sig)); process.on('SIGTERM', (sig) => void stop(sig)); // Keep the process alive until a signal arrives. return new Promise<number>(() => {});}
const DAEMON_HELP = 'Usage: shard daemon [--socket <path>] [--home <dir>] [--restart <never|always|on-crash>] [--trust-project]\n' + ' [--web-host <ip>] [--web-port <port>] [--public-host <host:port>]\n' + ' [--allowed-origin <origin> ...] [--trust-proxy <loopback|ip>] [--allow-insecure-remote]\n';
const WEB_HELP = 'Usage: shard web [options] start the daemon + browser client (prints URL)\n' + ' shard web <admin-subcommand> web administration\n' + 'Admin: approve <code>, clients, revoke <id|--all>, token create --name <n>|rotate\n' + 'Options: [--socket <path>] [--home <dir>] [--web-host <ip>] [--web-port <port>]\n' + ' [--public-host <host:port>] [--allowed-origin <origin> ...]\n' + ' [--trust-proxy <loopback|ip>] [--allow-insecure-remote] [--trust-project]\n';
/** * `shard daemon` / `shard shardd` — boot the long-lived multi-agent daemon (PROTOCOL §6/§8). * Parses `--socket <path>` / `--home <dir>` overrides + optional `--restart <policy>` and * runs until SIGINT/SIGTERM, then exits 0. No model resolution (the daemon spawns * `shard agent` children that resolve their own config). */async function runDaemonSub(rest: string[]): Promise<number> { const parsed = parseDaemonFlags(rest); if (parsed.help) { process.stderr.write(DAEMON_HELP); return 0; } if (parsed.error !== undefined) { process.stderr.write(`${parsed.error.message}\n`); return parsed.error.code; } return bootDaemon(parsed, (info) => { process.stderr.write(`shardd: listening on ${info.socketPath}\n`); });}
/** * `shard web` with no admin subcommand — start the localhost daemon + browser client and * print the URL. Accepts the same advanced daemon flags as `shard daemon`. Admin * subcommands (approve/clients/revoke/token) are routed separately in `main()` before * this is called, so they are preserved unchanged. * * When `inlineApproval` is set (saga vr7kgx.19), the daemon process watches for the first * browser enrollment request and prompts the operator to approve it inline — the printed * URL carries `?flow=approve` so the browser defaults to the approval-code flow and the * operator never needs a second terminal. The approval-flow link is shown ONLY for the * first approval run: once a browser is already enrolled (`needsEnrollment` is false) the * plain URL is printed instead, since no approval will be needed. */async function runWebDaemon(rest: string[], opts?: BootDaemonOpts): Promise<number> { const parsed = parseDaemonFlags(rest); if (parsed.help) { process.stderr.write(WEB_HELP); return 0; } if (parsed.error !== undefined) { process.stderr.write(`${parsed.error.message}\n`); return parsed.error.code; } const inlineApproval = opts?.inlineApproval ?? false; return bootDaemon(parsed, (info) => { if (info.webPort !== undefined) { const useApprovalFlow = inlineApproval && (info.needsEnrollment ?? false); const flow = useApprovalFlow ? '?flow=approve' : ''; process.stdout.write(`Shard web ready: http://${info.webHost}:${info.webPort}${flow}\n`); process.stderr.write(`shardd: listening on ${info.socketPath}\n`); if (useApprovalFlow) { process.stderr.write(`Open the URL in a browser — I'll prompt you here to approve the connection.\n`); } else { process.stderr.write(`Open the URL in a browser. Ctrl-C to stop.\n`); } } else { process.stderr.write(`shardd: listening on ${info.socketPath} (web disabled)\n`); } }, opts);}
/** Route `shard models <sub> [args]` sub-subcommands. Returns exit code. */async function runModelsSub(rest: string[]): Promise<number> { const { runModelsList, runModelsShow } = await import('@/cli/commands/models-cmd'); const sub = rest[0]; const args = rest.slice(1); if (sub === 'list') { const { providerKey } = extractProviderFlag(args); return runModelsList({ ...(providerKey !== undefined ? { providerKey } : undefined) }); } if (sub === 'show') { const { providerKey, leftover } = extractProviderFlag(args); const alias = leftover[0]; if (alias === undefined) { process.stderr.write('Usage: shard models show <alias> [--provider p]\n'); return 1; } return runModelsShow(alias, { ...(providerKey !== undefined ? { providerKey } : undefined) }); } process.stderr.write('Usage: shard models <list|show> [--provider p] [alias]\n'); return 1;}
/** * Build per-model reasoning metadata for the {@link SessionModelController} (saga p7m8jp.2). * Resolves each catalog descriptor via `resolveCatalogModel` and extracts the supported * effort IDs + the configured default. Descriptors without `reasoningEfforts` get no entry * (no selectable effort surface). Secrets are never carried — only effort IDs and defaults. */function buildReasoningMeta( models: ModelDescriptor[], config: ShardConfig, env: Record<string, string | undefined>, resolveFn: typeof import('@/cli/config/model-catalog').resolveCatalogModel,): Record<string, { efforts?: string[]; defaultEffort?: string }> { const meta: Record<string, { efforts?: string[]; defaultEffort?: string }> = {}; for (const descriptor of models) { try { const resolved = resolveFn(descriptor, config, env); if (resolved.reasoningEfforts !== undefined || resolved.reasoningEffort !== undefined) { meta[descriptor.id] = { ...(resolved.reasoningEfforts !== undefined ? { efforts: resolved.reasoningEfforts } : undefined), ...(resolved.reasoningEffort !== undefined ? { defaultEffort: resolved.reasoningEffort } : undefined), }; } } catch { // A descriptor that fails to resolve (e.g. auth env unset for a non-active provider) // simply has no reasoning metadata — it won't appear in the reasoning catalog. } } return meta;}
/** Entry. */async function main(): Promise<void> { const argv = process.argv.slice(2); // Find the first subcommand (a bare positional that is NOT a flag value). We must skip // the value of any preceding value-taking flag so a flag value is not mistaken for the // subcommand. Subcommand may also be omitted (defaults to `web`). let sub: string | undefined; let restStart = 0; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === undefined) continue; if (a.startsWith('-')) { // If this flag takes a value, skip the next element. if (VALUE_FLAGS.has(a)) i++; continue; } sub = a; restStart = i + 1; break; }
if (sub === undefined) { sub = 'web'; // default to the browser experience } else if (!SUBCOMMANDS.has(sub)) { process.stderr.write(`shard: unknown subcommand: ${sub}\n`); usageExit(); }
const rest = argv.slice(restStart);
// Commands that do not directly run an agent route before provider resolution. if (sub === 'new') { const { runNewSub } = await import('@/cli/commands/worktree-cmd'); process.exit(await runNewSub(rest)); } if (sub === 'worktree') { const { runWorktreeSub } = await import('@/cli/commands/worktree-cmd'); process.exit(await runWorktreeSub(rest)); } if (sub === 'config') { process.exit(await runConfigSub(rest)); } if (sub === 'models') { process.exit(await runModelsSub(rest)); } if (sub === 'web') { // Admin subcommands (approve, clients, revoke, token) route to the web admin client. // No admin subcommand → start the localhost daemon + browser client and print the URL. // We detect the admin path by the first bare positional (flags like --socket are // skipped) so `shard web --web-port 8787` starts the daemon while // `shard web token create --name x` routes to admin. const WEB_ADMIN_SUBS = new Set(['approve', 'clients', 'revoke', 'token']); const firstPositional = rest.find((a) => !a.startsWith('-')); if (firstPositional !== undefined && WEB_ADMIN_SUBS.has(firstPositional)) { const { runWebSub } = await import('@/cli/commands/web-cmd'); process.exit(await runWebSub(rest)); } process.exit(await runWebDaemon(rest, { inlineApproval: (process.stdin as { isTTY?: boolean }).isTTY ?? false })); } if (sub === 'setup') { const { runSetupSub } = await import('@/cli/commands/setup-cmd'); const code = await runSetupSub(rest); if (code !== 0 || !rest.includes('--web')) process.exit(code); process.stdout.write('\nStarting the local web experience…\n'); process.exit(await runWebDaemon([], { inlineApproval: true })); } if (sub === 'gc') { const { runGcSub } = await import('@/cli/commands/gc-cmd'); process.exit(await runGcSub(rest)); } if (sub === 'daemon' || sub === 'shardd') { process.exit(await runDaemonSub(rest)); }
const opts = parseOptions(rest);
// Build the wired core for the headless agent. Provider/ // model now flow through the config resolver (`@/cli/config`): loadConfig() reads // `~/.config/shard/config.json` (XDG), resolveModel() applies the 6-step override matrix // (flag > env > config). The `--api-key`/`--base-url` flags + `OPENAI_BASE_URL` env stay // as test-only escape hatches that override the resolver's output. Failure is loud: // resolveModel throws on any stuck case → exit 2 (matching the old `src/cli/index.ts:118` // precedent). const { loadConfig, resolveModel } = await import('@/cli/config'); let config; try { ({ config } = loadConfig()); } catch (err) { // Missing config, malformed JSON, or schema failure — fail loudly with exit 2. process.stderr.write(`${(err as Error).message}\n`); process.exit(2); } let resolvedModel: string; let resolvedProvider: string; let resolvedApiKey: string | undefined; let resolvedAuth: 'bearer' | 'none' = 'bearer'; let resolvedBaseURL: string | undefined; // saga hihsjg: wire-adapter + reasoning flags surfaced by the resolver so the provider // factory can pick OpenAIResponsesProvider vs OpenAIProvider. let resolvedApi: 'chat-completions' | 'responses' = 'chat-completions'; let resolvedReasoning = false; // saga p7m8jp.1: resolved reasoning effort (startup default; undefined → adapter default). let resolvedReasoningEffort: string | undefined;
// saga u4uo9m.3 plan item 1: a daemon-managed subagent is spawned with // `--managed-manifest <path>` + `--daemon-socket <path>`. Load + validate the manifest // BEFORE model resolution/bootstrap and treat its pinned values as AUTHORITATIVE — // managed model/provider/mode/cwd/prompt/tools/skills/depth/policy/timeout override // conflicting direct flags. Direct/test paths (no `--managed-manifest`) are unchanged. // Prompt/task/config never appear on argv (saga u4uo9m non-disclosure). let managedAuthority: import('@/cli/managed-authority').ManagedAuthority | undefined; // saga u4uo9m.5: the bundled subagent tools bootstrap contribution factory. Lazily // imported when a managed manifest is present; passed into `createDefaultCore` so the // canonical bootstrap installs the six tools through the ExtensionHost. let contributionFactory: import('@/contracts/bootstrap-contribution').BootstrapContributionFactory | undefined; if (opts.managedManifest !== undefined) { const { loadManifestFile } = await import('@/daemon/agent-catalog'); const manifest = loadManifestFile(opts.managedManifest); if (manifest === undefined) { process.stderr.write(`shard: managed manifest not found or invalid: ${opts.managedManifest}\n`); process.exit(2); } // A managed child connects back to the daemon over the supplied socket; the manifest's // pinned system prompt + capability policy drive the bootstrap (full authority). const daemonSocket = opts.daemonSocket; if (daemonSocket === undefined) { process.stderr.write('shard: --managed-manifest requires --daemon-socket\n'); process.exit(2); } const { buildManagedAuthority } = await import('@/cli/managed-authority'); managedAuthority = buildManagedAuthority(manifest, daemonSocket); // saga u4uo9m.5: the bundled subagent management tools install as a bootstrap // contribution (the factory is invoked by the canonical bootstrap with the trusted // agent-definition snapshot + the pinned managed context). Imported lazily so a // direct (non-managed) run pays no L3 import cost. contributionFactory = (await import('@/subagent/extension')).buildSubagentContribution(); }
// Build the env snapshot passed to the (pure) resolver. `--api-key` is a test-only // escape hatch: when set, pre-populate EVERY bearer envVar in the catalog so the // resolver's auth step succeeds regardless of which provider resolves (then the // override below pins the final token to the flag value). Without this, the resolver // would throw on a missing auth env before the override could apply. const resolverEnv: Record<string, string | undefined> = { ...process.env }; if (opts.apiKey !== undefined) { for (const p of Object.values(config.providers)) { if (p.auth.kind === 'bearer') resolverEnv[p.auth.envVar] = opts.apiKey; } }
let resolvedContextWindow: number | undefined; try { // saga u4uo9m.3: managed manifest authority — pinned provider/model win over direct // flags (the manifest is loaded + validated above; the override falls back to the flag // when the manifest omits that field). const managedProvider = managedAuthority?.providerOverride ?? opts.providerKey; const managedModel = managedAuthority?.modelOverride ?? opts.modelName; const r = resolveModel({ config, env: resolverEnv, ...(managedProvider !== undefined ? { providerKey: managedProvider } : undefined), ...(managedModel !== undefined ? { modelName: managedModel } : undefined), }); resolvedModel = r.modelName; resolvedProvider = r.provider; resolvedBaseURL = r.baseURL; resolvedAuth = r.auth.kind; if (r.auth.kind === 'bearer') resolvedApiKey = r.auth.token; resolvedContextWindow = r.contextWindow; resolvedApi = r.api ?? 'chat-completions'; resolvedReasoning = r.reasoning === true; resolvedReasoningEffort = r.reasoningEffort; } catch (err) { process.stderr.write(`${(err as Error).message}\n`); process.exit(2); }
// Escape-hatch overrides (test-only): explicit flags + `OPENAI_BASE_URL` env win over // the resolver's output, preserving the legacy env-override path for one-off runs. const apiKey = opts.apiKey ?? resolvedApiKey; const baseURL = opts.baseURL ?? process.env.OPENAI_BASE_URL ?? resolvedBaseURL;
// saga dfcvo6: compaction is ON by default. Resolve the context window: // per-model override (r.contextWindow) → global config.compaction.contextWindow → 128k. // Then build the partial CompactionConfig (createDefaultCore applies the // reserveTokens/keepRecentTokens/enabled defaults). `enabled: false` opts out. const compactionContextWindow = resolvedContextWindow ?? config.compaction?.contextWindow ?? DEFAULT_CONTEXT_WINDOW; const compaction = { contextWindow: compactionContextWindow, ...(config.compaction?.enabled !== undefined ? { enabled: config.compaction.enabled } : undefined), ...(config.compaction?.reserveTokens !== undefined ? { reserveTokens: config.compaction.reserveTokens } : undefined), ...(config.compaction?.keepRecentTokens !== undefined ? { keepRecentTokens: config.compaction.keepRecentTokens } : undefined), };
const bootstrapExtensions: import('@/core/default').BootstrapExtension[] = []; if (config.codeIndex?.enabled !== false) { const { createCodeIndexExtension } = await import('@/codeindex/extension'); bootstrapExtensions.push( createCodeIndexExtension({ cwd: managedAuthority?.cwdOverride ?? opts.cwd }), ); }
const core = await createDefaultCore({ model: resolvedModel, providerKey: resolvedProvider, auth: resolvedAuth, // saga hihsjg: surface the resolved wire adapter + reasoning flag. api: resolvedApi, ...(resolvedReasoning ? { reasoning: true } : undefined), // saga p7m8jp.1: carry the resolved reasoning effort through the bootstrap. ...(resolvedReasoningEffort !== undefined ? { reasoningEffort: resolvedReasoningEffort } : undefined), ...(apiKey !== undefined ? { apiKey } : undefined), ...(baseURL !== undefined ? { baseURL } : undefined), sessionPath: opts.sessionPath, ...(managedAuthority?.sessionIdOverride !== undefined ? { sessionId: managedAuthority.sessionIdOverride } : undefined), // saga u4uo9m.3: managed manifest authority — pinned cwd/agentId/mode win over flags. cwd: managedAuthority?.cwdOverride ?? opts.cwd, ...(managedAuthority?.agentIdOverride !== undefined ? { agentId: managedAuthority.agentIdOverride } : opts.agentId !== undefined ? { agentId: opts.agentId } : undefined), ...(managedAuthority?.modeOverride !== undefined ? { mode: managedAuthority.modeOverride } : undefined), // saga u4uo9m.3: pinned definition body is the base identity (restart/relaunch // determinism — same body/config regardless of source-file edits). ...(managedAuthority?.systemPrompt !== undefined ? { systemPrompt: managedAuthority.systemPrompt } : undefined), // saga u4uo9m.3: managed-child bootstrap restrictions — strict capability filtering, // fresh child RealPermissionGate (read-only-allow / mutate-ask), carried managed // context for later L3 extension injection. L2 never imports L3; this is plain data. ...(managedAuthority !== undefined ? { managed: managedAuthority.managed } : undefined), // saga u4uo9m.5 plan item 1: inject the bundled subagent management tools as a // bootstrap CONTRIBUTION FACTORY. The bootstrap calls it with the trusted // agent-definition snapshot (discovered below) + the pinned managed context, then // installs the six tools through the ExtensionHost before the capability snapshot — // so they are live and subject to the capability policy. L4 imports the L3 factory; // the core only calls the L1 function type (it never imports `@/subagent/**`). ...(managedAuthority !== undefined && contributionFactory !== undefined ? { contributionFactory } : undefined), // duoh76.7: opt-in permission gate. A `permissions` block in config.json // instantiates a RealPermissionGate sharing the core's bus/modeController. // Absent → AllowAllGate (no regression). Covered calls gate immediately; uncovered // calls reach `ask`, answerable once duoh76.10's command→event bridge ships. ...(config.permissions ? { permissions: config.permissions } : undefined), // dfcvo6: compaction ON by default (enabled defaults true in createDefaultCore). compaction, extensions: bootstrapExtensions, // d4ks8c.7: the canonical bootstrap discovers typed resources (skills/context/agents/ // rules) and trusted dynamic extensions through ExtensionApi. Project instruction/code // resources require affirmative trust; `projectTrusted` defaults to `false` (filtered, // no project code executed) until the CLI grows an explicit trust flag. resources: { cwd: opts.cwd, userHome: homedir(), projectTrusted: opts.trustProject, }, }); const session = createAgentSessionFromCore(core);
if (sub === 'agent') { // saga u4uo9m.3: the managed manifest was already loaded + validated before model // resolution and its authority applied to the core above (pinned model/provider/mode/ // cwd/system_prompt + the managed bootstrap restrictions). No prompt/task/config ever // appears on argv. Build the per-session model catalog in the CLI layer. Descriptors // contain no auth material; provider construction resolves credentials only when a // selection is made. const { buildModelCatalog, resolveCatalogModel } = await import('@/cli/config/model-catalog'); const { SessionModelController } = await import('@/core/model-controller'); const { OpenAIProvider } = await import('@/core/provider/openai'); const { OpenAIResponsesProvider } = await import('@/core/provider/openai-responses'); const catalog = await buildModelCatalog({ config, env: resolverEnv, activeProvider: resolvedProvider, activeModel: resolvedModel, // saga vr7kgx.7: stamp the resolved compaction context window onto descriptors that // lack a per-alias override, so the web UI's context-size display has the same total the // compactor fires against (per-model → global → default). fallbackContextWindow: compactionContextWindow, }); // saga p7m8jp.2: one session-stable cache key reused across all Responses provider // reconstructions (model/effort switches) so prefix caching is not lost. const modelControllerCacheKey = crypto.randomUUID(); const modelController = new SessionModelController({ models: catalog.models, active: catalog.active, queueProvider: (provider) => session.queueProvider(provider), // saga p7m8jp.2: per-model reasoning metadata (efforts + default) keyed by descriptor ID. reasoning: buildReasoningMeta(catalog.models, config, resolverEnv, resolveCatalogModel), // saga p7m8jp.2: session-stable cache key reused across Responses provider reconstructions. promptCacheKey: crypto.randomUUID(), createProvider: (descriptor, effort) => { const resolved = resolveCatalogModel(descriptor, config, resolverEnv); const common = { model: resolved.modelName, modelDescriptor: descriptor, baseURL: resolved.baseURL, auth: resolved.auth.kind, ...(resolved.auth.kind === 'bearer' ? { apiKey: resolved.auth.token } : {}), } as const; // saga p7m8jp.2: the `effort` parameter is the controller's authoritative active value // (the model's default on a switch, or the new value on an effort change). For // Responses, always pass it (the provider defaults to `medium` when undefined). For // Chat Completions, send it ONLY when the model declared `reasoningEfforts` // (explicit opt-in); generic endpoints receive no new field by default. const responsesEffortOpt = effort !== undefined ? { reasoningEffort: effort } : undefined; const chatEffortOpt = effort !== undefined && resolved.reasoningEfforts !== undefined ? { reasoningEffort: effort } : undefined; // saga hihsjg: branch on the resolved wire adapter. return resolved.api === 'responses' ? new OpenAIResponsesProvider({ ...common, ...(resolved.reasoning ? { reasoning: true } : undefined), ...responsesEffortOpt, // saga p7m8jp.2: reuse the session-stable cache key. ...(modelControllerCacheKey !== undefined ? { promptCacheKey: modelControllerCacheKey } : undefined), }) : new OpenAIProvider({ ...common, ...chatEffortOpt, }); }, }); // Stdio headless server speaking the per-agent protocol (PROTOCOL v0.1, §2.1/§5.2/§9). // `shard agent [--id <id>]` boots a direct-connection stdio server over the same wired // pieces. `--socket <path>` is RESERVED for the daemon-owned unix-socket mode (build // step 6); this subcommand speaks stdio only in step 5. const { runStdioAgentServer } = await import('@/transports/stdio'); try { await runStdioAgentServer({ session, modeController: core.modeController, sessionStore: core.sessionStore, eventBus: core.eventBus, agentId: core.agentId, modelController, writerLease: { runtimeGeneration: opts.runtimeGeneration ?? `direct-${process.pid}`, daemonLease: opts.daemonLease ?? 'direct-stdio', }, // saga u4uo9m.3 plan item 7: lease-loss (stdin EOF) disposes extensions (in-flight // dialogs unblock) and the production forced-exit safety net routes through // `process.exit` so a hung keepalive cannot pin a leased child. dispose: () => core.host.dispose(), exit: (code) => process.exit(code), }); } finally { await core.host.dispose(); } return; }
// Unreachable: SUBCOMMANDS guard above. usageExit();}
await main();