Command
ActiveCli.Command.Command — a CLI invocation being assembled, and the live session it belongs to.
ActiveRecord builds a query by accumulating intent and executing at the end: where(...).limit(...) changes nothing until to_a runs, and to_sql shows what would run without running it. This is the same arrangement for a command line — setters accumulate, toArgv() shows, send() executes.
class Command { static open(provider: AbstractProvider, options?: CommandOptions): Command; static resume(sessionId: string, provider: AbstractProvider, options?: CommandOptions): Command;
readonly sessionId: string; readonly workingDir: string | null;
get provider(): AbstractProvider; set provider(provider: AbstractProvider); setProvider(provider: AbstractProvider): this;
get message(): string; set message(message: string); setMessage(message: string): this;
get attachments(): readonly AbstractAttachment[]; set attachments(attachments: readonly (AbstractAttachment | string)[]); setAttachment(attachment: AbstractAttachment | string): this; setAttachments(attachments: readonly (AbstractAttachment | string)[]): this;
get model(): string | null; set model(model: string | null); setModel(model: string | null): this;
get permissions(): PermissionMode | null; set permissions(permissions: PermissionMode | string | null); setPermissions(permissions: PermissionMode | string | null): this;
get session(): Session | null; isLive(): boolean;
subscribe(listener: EventListener): this; onPermission(responder: PermissionResponder): this;
toArgv(): string[]; toPayload(): Record<string, unknown>; inspect(): string; toString(): string;
send(): Promise<this>; interrupt(): this; close(): this;
get mcp(): McpCommands; get auth(): AuthCommands;
version(): Promise<string | null>; usage(): Promise<string>; update(): Promise<CommandResult>; describe(options?: { timeoutMs?: number }): Promise<Record<string, unknown> | null>; supportsModel(model: string, options?: { timeoutMs?: number }): Promise<boolean>; exec(args: readonly string[], options?: { timeoutMs?: number }): Promise<CommandResult>;}There is no public constructor. open() begins a new conversation under an id it mints; resume() continues the one an id names.
Building and inspecting
Section titled “Building and inspecting”| Member | Description |
|---|---|
open(provider, options) |
Describes intent. No process starts. |
resume(sessionId, provider, options) |
The same, for a conversation that already exists on disk. |
setProvider(p) |
Takes effect at the next send(). A CLI process cannot become another one, so switching replaces the process — and there is no reason to pay for that until there is something to run. The running process is retired immediately so provider and session never disagree. |
setPermissions(p) |
Accepts an instance or a name, with hyphens or underscores. Throws on an unknown name — silently running under a different mode than the caller asked for ends in edits nobody approved. |
setAttachment(a) |
Singular replaces the whole list. A bare string is read as a path. |
attachments |
Cleared by send() — an attachment belongs to the turn that carried it. |
toArgv() |
The counterpart of to_sql. The message is not here — a session-mode CLI reads its prompt from stdin. |
toPayload() |
The JSON line the next turn would be written to stdin as. |
inspect() |
Everything about this command, for a human. toString() delegates here, so a command logged or interpolated reads as its full state rather than as [object Object]. |
const command = Command.open(Claude, { workingDir: '/proj' });command.setModel('opus').setPermissions('plan').setMessage('Plan the migration');
command.toArgv();// [...streamFlags, '--session-id', '…', '--permission-mode', 'plan', '--model', 'opus']
console.log(command.inspect());// provider: claude// command: claude -p --output-format stream-json …// stdin: {"type":"user","message":{"role":"user","content":"Plan the migration"}}// model: opus// permissions: plan// attachments: 0// workingDir: /proj// session: 3f2a… (not started)Running
Section titled “Running”| Member | Description |
|---|---|
send() |
Spawns if needed, then sends the assembled turn. The only point at which anything reaches the CLI. A process is started on the first call and reused afterwards, unless the provider or the permission mode changed — the latter is a spawn-time flag. Clears attachments. |
interrupt() |
Stop the turn in progress without discarding the conversation. See the sequence note below. |
close() |
Stop the CLI and everything it spawned, through the process tree. |
subscribe(listener) |
Be told about each event the CLI emits. Chains. |
onPermission(responder) |
Answer permission requests. Returning null leaves the request unanswered, which stalls the CLI — do that only when something else will answer it. |
session / isLive() |
Exposed for inspection — pid, mode, stderr — rather than for driving. A caller that reaches past this to spawn or write is working around the arrangement rather than with it. |
One-off commands
Section titled “One-off commands”These do not use the session: each runs the CLI, reads what it printed, and exits. A session is a conversation; this is a question with an answer.
| Member | Description |
|---|---|
version() |
The version number pulled out of claude --version (whose output reads 2.1.170 (Claude Code)). null rather than throwing — a CLI that will not answer is a fact to handle, not an exception to propagate. When the output has no recognisable number but is non-empty, that text is returned rather than claiming the CLI said nothing. |
usage() |
The usage report as the CLI prints it. Runs with --no-session-persistence. |
update() |
Updates the CLI in place. 120-second timeout, because an update downloads and links. Returns the whole CommandResult, since what counts as success differs by install method. |
describe() |
What this installation can do here, right now: slash commands, accepted models, settings in force for this directory. None of it is guessable and all of it differs per install and per project. Runs in its own short-lived ephemeral session so it disturbs nothing and leaves no transcript entry. null on timeout (default 15s). |
supportsModel(m) |
Whether a model can actually be run here, asked by sending the cheapest possible turn and seeing whether it comes back. There is no command that answers “may I use this model” — entitlement depends on the account, the org, and sometimes a balance. Costs a request; cache the answer. Checks the output as well as the exit code, because some refusals exit zero and say so in the body. |
exec(args) |
The escape hatch. A CLI gains subcommands faster than this library wraps them, and nobody should have to wait for us. Passed without a shell, so quoting and metacharacters stay literal. |
await command.version(); // '2.1.170'await command.supportsModel('haiku');await command.exec(['mcp', 'list']);
const description = await command.describe();description?.['models'];description?.['agents'];CommandOptions
Section titled “CommandOptions”ActiveCli.Command.CommandOptions — what a caller may hand Command.open or Command.resume.
interface CommandOptions { workingDir?: string; permissions?: PermissionMode | string; model?: string; env?: NodeJS.ProcessEnv;}All four are optional, and each has an equivalent setter on the command — these exist so the common case reads as one call.
PermissionResponder
Section titled “PermissionResponder”type PermissionResponder = ( request: PermissionRequestEvent,) => Record<string, unknown> | null;Build the return value with request.approve() or request.deny(reason); those carry the request_id the CLI matches on. Returning null deliberately leaves the request unanswered.
Subcommands reached through Command
Section titled “Subcommands reached through Command”command.mcp and command.auth return the classes that wrap the CLI’s own subcommands.
AuthCommands
Section titled “AuthCommands”ActiveCli.Auth.AuthCommands — the claude auth commands, as methods.
class AuthCommands { constructor( runner: CommandRunner, command: string, workingDir: string | null, augmentedPath?: AugmentedPath, );
login(method?: LoginMethod | string): LoginSession; status(): Promise<AuthStatus>;}status() runs claude auth status --json with an 8-second timeout and reads stderr when stdout is empty.
class AuthStatus { constructor(loggedIn: boolean, raw: Readonly<Record<string, unknown>>, text: string);
readonly loggedIn: boolean; readonly raw: Readonly<Record<string, unknown>>; readonly text: string;
static parse(stdout: string): AuthStatus;}Falls back to reading the prose when --json produces nothing parseable — an older CLI may not support the flag, and “not logged in” is the answer most worth still getting right. The CLI has spelled the logged-in flag differently across versions, so loggedIn consults loggedIn, logged_in and authenticated in turn, then the presence of an account object, and only then the text.
class LoginMethod { static readonly SUBSCRIPTION: LoginMethod; // name 'claudeai', flag '--claudeai' static readonly CONSOLE: LoginMethod; // name 'console', flag '--console'
readonly name: string; readonly flag: string;
static named(name: string | undefined | null): LoginMethod;}
class LoginSession { constructor(process: ChildProcess);
get pid(): number | null; get output(): string;
onUrl(listener: (url: string) => void): this; onFinished(listener: (code: number | null) => void): this; submitCode(code: string): this; cancel(): this;}LoginMethod.named() defaults rather than refusing, unlike PermissionMode.named(). That is the CLI’s own default, and an unrecognised name most likely means a caller that has not been updated.
claude auth login is a conversation with the user, not a question with an answer: it prints an OAuth URL, waits, and may need a code pasted back. Two things LoginSession deliberately does not do:
| Not done | Why |
|---|---|
| It does not open the URL | The CLI already tries to — open on macOS, rundll32 on Windows, the registry from WSL — and does not report whether that worked. Opening it ourselves double-opens wherever the CLI succeeded. |
| It does not infer whether a code is needed | The CLI prints Paste code here if prompted in every flow, identically. Whether one is actually required depends on whether the browser’s callback page can reach the CLI’s loopback server — decided browser-side and never surfaced. Offer the field and let the user use it. |
The URL is scanned for on both stdout and stderr, because it has been observed on either depending on platform.
const login = command.auth.login('console');login.onUrl((url) => showToUser(url));login.onFinished((code) => console.log('exited', code));login.submitCode(pastedCode);McpCommands
Section titled “McpCommands”ActiveCli.Mcp.McpCommands — the claude mcp commands, as methods.
class McpCommands { constructor( runner: CommandRunner, command: string, workingDir: string | null, parser?: McpOutputParser, );
list(): Promise<McpListEntry[]>; get(name: string): Promise<McpServer | null>; all(): Promise<McpServer[]>; add(name: string, config: Record<string, unknown>, options?: { scope?: McpServerScope | string }): Promise<void>; remove(name: string, options?: { scope?: McpServerScope | string }): Promise<void>;}| Method | Timeout | Notes |
|---|---|---|
list() |
20s | Names and statuses only — that is all claude mcp list reports. Listing probes every server, which is why it is the slowest. Returns [] when the command fails: no MCP configured is the ordinary case and is indistinguishable from failure in the output. |
get(name) |
12s | One server in full, or null when the CLI does not describe it. |
all() |
— | List, then describe each. Two commands per server, so noticeably slower; worth it only when transport details are needed. |
add(…) |
15s | Runs mcp add-json. “Already exists” is accepted as a state rather than raised — the CLI reports failure in its output rather than by exit code. |
remove(…) |
10s | Pass the scope for project- and local-scoped servers, or the CLI looks in the wrong configuration file and finds nothing to remove. |
await command.mcp.add('playwright', { command: 'npx', args: ['@executeautomation/playwright-mcp-server'],}, { scope: 'user' });