AppServerClient
AppServerClient is the main client API exposed by the package. It manages the app-server connection lifecycle, exposes typed request helpers, and provides event and server-request hooks for interactive flows.
Construct A Client
Most applications can start with createClient():
import { createClient } from "codex-app-server-client";
const client = await createClient();That factory:
- spawns a local
codex app-server --listen stdio:// - uses
process.cwd()by default - completes
initialize->initialized - returns a managed client whose
close()also shuts down the child process
When you need transport-level control, the lower-level path is still available:
import { spawn } from "node:child_process";
import { AppServerClient, StdioTransport } from "codex-app-server-client";
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
cwd: process.cwd(),
stdio: ["pipe", "pipe", "inherit"]
});
const transport = new StdioTransport({
input: child.stdout,
output: child.stdin
});
const client = new AppServerClient({
transport,
defaultRequestTimeoutMs: 30_000
});Constructor options:
transport: the transport implementation to userequestIdFactory: optional custom request id generatordefaultRequestTimeoutMs: optional default timeout applied by the underlying RPC session
Lifecycle
client.state
Exposes the underlying transport state.
console.log(client.state);client.initializationState
Exposes the RPC session initialization lifecycle state.
console.log(client.initializationState);client.start()
Starts the underlying session without sending initialize.
Use this when you want explicit control over when the transport starts.
await client.start();client.initialize(params, options?)
Sends initialize, caches the response, and by default follows it with initialized.
Clients returned by createClient() have already completed this handshake.
await client.initialize({
clientInfo: {
name: "example-client",
title: "Example Client",
version: "0.0.1"
},
capabilities: {
optOutNotificationMethods: []
}
});Important behavior:
- repeated calls reuse the first successful initialize response
- the client rejects attempts to reuse the same session with different initialize params
options.sendInitializeddefaults totrueoptions.requestforwards request options such as timeouts and abort signals
If you need to delay the second handshake step:
await client.initialize(
{
clientInfo: {
name: "example-client",
title: "Example Client",
version: "0.0.1"
},
capabilities: null
},
{
sendInitialized: false
}
);
await client.initialized();client.initialized()
Sends the protocol initialized notification if it has not already been sent.
await client.initialized();client.close()
Closes the underlying session and transport.
await client.close();Shared Request Options
Most request helpers accept AppServerClientRequestOptions, which forward to the underlying RPC layer.
Common uses:
timeoutMsfor per-request timeoutssignalfor cancellation viaAbortController
const controller = new AbortController();
const models = await client.modelList(
{},
{
timeoutMs: 5_000,
signal: controller.signal
}
);Catalog Methods
client.appList(params?, options?)
Lists app metadata exposed by the server.
const apps = await client.appList();
console.log(apps.data.map((app) => app.name));client.modelList(params?, options?)
Lists available models.
const models = await client.modelList();
console.log(models.data.map((model) => model.id));client.skillsList(params?, options?)
Lists available skills for the current context.
const skills = await client.skillsList({
cwd: process.cwd()
});
console.log(skills.skills.map((skill) => skill.name));client.thread
Thread helpers cover thread creation, inspection, resumption, and the combined thread-plus-first-turn flow.
client.thread.start(params, options?)
Starts a new thread.
const startedThread = await client.thread.start();
console.log(startedThread.thread.id);When you omit experimentalRawEvents or persistExtendedHistory, the client defaults both to false.
client.thread.resume(params, options?)
Resumes a persisted thread from stored rollout history.
const resumed = await client.thread.resume({
threadId: "thread_123"
});Freshly started threads are not always resumable immediately. A thread generally becomes resumable only after the server has materialized the backing rollout history.
client.thread.read(params, options?)
Reads one thread by id.
const thread = await client.thread.read({
threadId: "thread_123"
});
console.log(thread.thread.status);client.thread.list(params?, options?)
Lists known threads.
const threads = await client.thread.list({
limit: 20
});
console.log(threads.data.length);client.thread.loadedList(params?, options?)
Lists currently loaded threads on the active server process.
const loadedThreads = await client.thread.loadedList();client.thread.run(params, options?)
Starts a thread and immediately runs its first turn.
const run = await client.thread.run({
turn: {
effort: "low",
input: [
{
type: "text",
text: "Reply with exactly ready.",
text_elements: []
}
]
}
});
console.log(run.thread.thread.id);
console.log(run.turn.completed.params.turn.status);Important behavior:
- the helper fills
threadIdinto the initial turn automatically - if the thread starts successfully but the initial turn fails, the helper throws
AppServerClientThreadRunError AppServerClientThreadRunError.threadpreserves the created thread response so callers can recover
client.turn
Turn helpers cover direct turn requests and the streamed helper that waits for completion.
client.turn.start(params, options?)
Starts a turn on an existing thread.
const startedTurn = await client.turn.start({
threadId: "thread_123",
effort: "low",
input: [
{
type: "text",
text: "Summarize this repository in one sentence.",
text_elements: []
}
]
});
console.log(startedTurn.turn.id);This returns the immediate turn/start response. Completion arrives later through notifications.
client.turn.steer(params, options?)
Adds more user input to an active turn.
await client.turn.steer({
threadId: "thread_123",
turnId: "turn_456",
input: [
{
type: "text",
text: "Now make it shorter.",
text_elements: []
}
]
});client.turn.interrupt(params, options?)
Requests that the server stop an active turn.
await client.turn.interrupt({
threadId: "thread_123",
turnId: "turn_456"
});The final interrupted state still arrives asynchronously through later notifications or a later thread read.
client.turn.run(params, options?)
Starts a turn and collects its matching lifecycle notifications until turn/completed.
const run = await client.turn.run({
threadId: "thread_123",
effort: "low",
input: [
{
type: "text",
text: "Reply with exactly helper-check.",
text_elements: []
}
]
});
console.log(run.start.turn.id);
console.log(run.completed.params.turn.status);
console.log(run.completedItems.length);
console.log(run.agentMessageDeltas);Returned data includes:
start: the immediateturn/startresponsestarted: the matchingturn/startedevent when that method is not suppressedcompleted: the terminalturn/completedeventevents: all collected lifecycle events in arrival ordercompletedItems: completed items in arrival orderagentMessageDeltas: reconstructeditem/agentMessage/deltatext by item id
Helper options:
request: forwarded to the underlyingturn/startRPC callcompletionTimeoutMs: timeout for waiting onturn/completedsignal: abort signal for the overall helperonEvent: callback for each collected lifecycle event
const run = await client.turn.run(
{
threadId: "thread_123",
effort: "low",
input: [
{
type: "text",
text: "Stream a short answer.",
text_elements: []
}
]
},
{
completionTimeoutMs: 10_000,
onEvent(event) {
console.log(event.method);
}
}
);client.command
Standalone command helpers execute processes outside thread turn execution.
client.command.exec(params, options?)
Executes a command.
const result = await client.command.exec({
command: ["pwd"],
cwd: process.cwd(),
waitForExit: true
});
console.log(result.exitCode);If you need follow-up writes, PTY resizing, termination, or streaming output, start the command with a stable processId.
await client.command.exec({
processId: "shell-1",
command: ["bash"],
cwd: process.cwd(),
tty: true,
waitForExit: false
});client.command.write(params, options?)
Writes base64-encoded stdin bytes to a running command session.
await client.command.write({
processId: "shell-1",
inputBase64: Buffer.from("echo ready\n").toString("base64")
});client.command.resize(params, options?)
Resizes the PTY for a running command started with tty: true.
await client.command.resize({
processId: "shell-1",
cols: 120,
rows: 40
});client.command.terminate(params, options?)
Terminates a running command session.
await client.command.terminate({
processId: "shell-1"
});client.fs
Filesystem helpers operate on the host filesystem exposed through app-server.
client.fs.readFile(params, options?)
Reads a file as base64.
const file = await client.fs.readFile({
path: "/tmp/example.txt"
});
const text = Buffer.from(file.contentBase64, "base64").toString("utf8");
console.log(text);client.fs.writeFile(params, options?)
Writes a full base64 payload to a file.
await client.fs.writeFile({
path: "/tmp/example.txt",
contentBase64: Buffer.from("hello\n").toString("base64")
});client.fs.createDirectory(params, options?)
Creates a directory.
await client.fs.createDirectory({
path: "/tmp/example-dir",
recursive: true
});client.fs.getMetadata(params, options?)
Reads metadata about a file or directory.
const metadata = await client.fs.getMetadata({
path: "/tmp/example.txt"
});
console.log(metadata.kind);client.fs.readDirectory(params, options?)
Lists a directory's direct children.
const directory = await client.fs.readDirectory({
path: "/tmp"
});
console.log(directory.entries.map((entry) => entry.name));client.fs.remove(params, options?)
Removes a file or directory tree.
await client.fs.remove({
path: "/tmp/example-dir",
recursive: true
});client.fs.copy(params, options?)
Copies a file or directory tree.
await client.fs.copy({
sourcePath: "/tmp/example.txt",
destinationPath: "/tmp/example-copy.txt"
});client.account
Account helpers cover the active auth session, login flows, and rate-limit snapshots.
client.account.read(params?, options?)
Reads the current account state. The helper defaults refreshToken to false.
const account = await client.account.read();
console.log(account.account?.email);If you want to opt into refresh work:
await client.account.read({
refreshToken: true
});client.account.loginStart(params, options?)
Starts a login flow.
const login = await client.account.loginStart({
method: "chatgpt"
});client.account.loginCancel(params, options?)
Cancels a previously started browser login flow.
await client.account.loginCancel({
loginId: "login_123"
});client.account.logout(options?)
Clears the active account session from the server process.
await client.account.logout();client.account.rateLimitsRead(options?)
Reads the current rate-limit snapshot.
const rateLimits = await client.account.rateLimitsRead();
console.log(rateLimits.snapshots.length);Notifications, Requests, And Errors
client.onNotification(listener)
Subscribes to raw RPC notifications without narrowing them to known generated methods.
const stop = client.onNotification((notification) => {
console.log(notification.method);
});
stop();client.onEvent(method, listener)
Subscribes to one typed generated server notification method.
const stopDelta = client.onEvent("item/agentMessage/delta", (event) => {
process.stdout.write(event.params.delta);
});Use this when you want typed event payloads without handling unrelated methods yourself.
client.onRequest(listener)
Subscribes to raw inbound server requests.
const stop = client.onRequest((request) => {
console.log(request.method);
});client.onServerRequest(method, listener)
Subscribes to one typed server request method while leaving response control in your hands.
const stop = client.onServerRequest(
"item/tool/call",
async (request) => {
await request.respond({
contentItems: [
{
type: "inputText",
text: "Handled manually."
}
],
success: true
});
}
);The wrapper prevents multiple responses to the same inbound request.
client.handleRequest(method, handler)
Registers one auto-response handler for a specific typed server request method.
const stop = client.handleRequest("item/tool/call", async () => {
return {
contentItems: [
{
type: "inputText",
text: "Handled automatically."
}
],
success: true
};
});Important behavior:
- only one auto-handler can be active per method at a time
- thrown errors are translated into JSON-RPC internal error responses
- the returned cleanup function unregisters the handler
client.handleApprovals(handlers)
Registers typed handlers for approval-oriented server request methods:
applyPatchApprovalexecCommandApprovalitem/commandExecution/requestApprovalitem/fileChange/requestApprovalitem/permissions/requestApproval
const stopApprovals = client.handleApprovals({
execCommandApproval: () => ({ decision: "denied" }),
"item/fileChange/requestApproval": () => ({ decision: "decline" }),
"item/permissions/requestApproval": () => ({
permissions: {},
scope: "turn"
})
});Like handleRequest(), approval handlers are auto-response handlers and must return the exact protocol response for their method.
client.onError(listener)
Subscribes to session-level errors.
const stop = client.onError((error) => {
console.error(error);
});client.onClose(listener)
Subscribes to session closure.
const stop = client.onClose((error) => {
console.log("client closed", error);
});Related Exported Helper Types
The package also exports client-side helper types and result shapes alongside AppServerClient, including:
AppServerClientRequestOptionsAppServerClientInitializeOptionsAppServerClientTurnRunOptionsAppServerClientTurnRunResultAppServerClientThreadRunParamsAppServerClientThreadRunOptionsAppServerClientThreadRunResultAppServerClientThreadRunErrorAppServerClientApprovalHandlers
These are useful when you are building your own wrappers, orchestration helpers, or strongly typed application code around the client.