Skip to content

Commit c4ed6dd

Browse files
ross-rlclaude
andauthored
feat(sdk): codex read/maintenance RPC wrappers (status, usage, mcp, skills, rename) (#146)
## Summary - Adds six public wrappers to `CodexAxonConnection` over already-vendored app-server protocol types, following the existing `readConfig`/`compactThread` pattern: - `getAccountRateLimits()` → `account/rateLimits/read` - `getAccountTokenUsage()` → `account/usage/read` - `readThread(params?)` → `thread/read` (defaults to the active thread) - `setThreadName(name)` → `thread/name/set` - `listMcpServerStatus(params?)` → `mcpServerStatus/list` - `listSkills(params?)` → `skills/list` - Re-exports the corresponding v2 param/response types from `protocol/index.ts` so they're available through the `codex` module entry. No new codegen — all types were already vendored from `@openai/codex` 0.144.1. - Thread-scoped wrappers (`readThread`, `setThreadName`) throw `ConnectionStateError("not_connected")` before a thread exists, matching `compactThread`. - Motivation: these back client-side slash commands (`/status`, `/usage`, `/mcp`, `/skills`, `/rename`) in downstream apps, which previously had no public path to these RPCs since `request()` is private. ## Test plan - New test asserts each wrapper emits the correct JSON-RPC method and params on the wire (threadId injection, param passthrough). - New test asserts thread-scoped wrappers reject with `not_connected` before a thread exists. - Full suite: 579 tests pass; `npm run typecheck` and `biome check` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 941398f commit c4ed6dd

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

sdk/src/codex/connection.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,49 @@ describe("CodexAxonConnection", () => {
177177
expect(conn.threadId).toBeUndefined();
178178
});
179179

180+
it("sends the wire method and params for each read/maintenance wrapper", async () => {
181+
const { ctrl, mock, conn } = setup();
182+
mock.axon.publish.mockImplementation(async (event) => {
183+
const frame = JSON.parse(event.payload);
184+
if (frame.id == null) return;
185+
if (frame.method === "thread/start") {
186+
ctrl.push(
187+
makeAgentEvent("response", { id: frame.id, result: { thread: { id: "thr-1" } } }),
188+
);
189+
} else {
190+
ctrl.push(makeAgentEvent("response", { id: frame.id, result: {} }));
191+
}
192+
});
193+
await conn.connect();
194+
await conn.startThread();
195+
await conn.getAccountRateLimits();
196+
await conn.getAccountTokenUsage();
197+
await conn.readThread({ includeTurns: true });
198+
await conn.setThreadName("renamed");
199+
await conn.listMcpServerStatus();
200+
await conn.listSkills({ forceReload: true });
201+
const frames = mock.axon.publish.mock.calls.map(([event]) => JSON.parse(event.payload));
202+
const byMethod = Object.fromEntries(frames.map((frame) => [frame.method, frame]));
203+
// Option<()> wire params: the field must be absent, not an empty object.
204+
expect(byMethod["account/rateLimits/read"]).not.toHaveProperty("params");
205+
expect(byMethod["account/usage/read"]).not.toHaveProperty("params");
206+
expect(byMethod["thread/read"]).toMatchObject({
207+
params: { threadId: "thr-1", includeTurns: true },
208+
});
209+
expect(byMethod["thread/name/set"]).toMatchObject({
210+
params: { threadId: "thr-1", name: "renamed" },
211+
});
212+
expect(byMethod["mcpServerStatus/list"]).toMatchObject({ params: {} });
213+
expect(byMethod["skills/list"]).toMatchObject({ params: { forceReload: true } });
214+
});
215+
216+
it("rejects thread-scoped wrappers before a thread exists", async () => {
217+
const { conn } = setup();
218+
await conn.connect();
219+
await expect(conn.readThread()).rejects.toMatchObject({ code: "not_connected" });
220+
await expect(conn.setThreadName("nope")).rejects.toMatchObject({ code: "not_connected" });
221+
});
222+
180223
it("correlates responses by JSON-RPC id", async () => {
181224
const { ctrl, mock, conn } = setup();
182225
mock.axon.publish.mockImplementation(async (event) => {

sdk/src/codex/connection.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,21 @@ import type {
2222
CollaborationMode,
2323
ConfigReadParams,
2424
ConfigReadResponse,
25+
GetAccountRateLimitsResponse,
26+
GetAccountTokenUsageResponse,
2527
InitializeParams,
2628
InitializeResponse,
29+
ListMcpServerStatusParams,
30+
ListMcpServerStatusResponse,
2731
ReviewDelivery,
2832
ReviewStartResponse,
2933
ReviewTarget,
3034
ServerRequest,
35+
SkillsListParams,
36+
SkillsListResponse,
37+
ThreadReadParams,
38+
ThreadReadResponse,
39+
ThreadSetNameResponse,
3140
ThreadStartParams,
3241
ThreadStartResponse,
3342
TurnStartParams,
@@ -554,6 +563,58 @@ export class CodexAxonConnection {
554563
async readConfig(params: ConfigReadParams = {}): Promise<ConfigReadResponse> {
555564
return (await this.request("config/read", params)) as ConfigReadResponse;
556565
}
566+
/**
567+
* Reads the account's current rate limits (`account/rateLimits/read`).
568+
* Wire params are `Option<()>`: the params field must be omitted, not `{}`.
569+
*/
570+
async getAccountRateLimits(): Promise<GetAccountRateLimitsResponse> {
571+
return (await this.request(
572+
"account/rateLimits/read",
573+
undefined,
574+
)) as GetAccountRateLimitsResponse;
575+
}
576+
/**
577+
* Reads the account's token-usage summary (`account/usage/read`).
578+
* Wire params are `Option<()>`: the params field must be omitted, not `{}`.
579+
*/
580+
async getAccountTokenUsage(): Promise<GetAccountTokenUsageResponse> {
581+
return (await this.request("account/usage/read", undefined)) as GetAccountTokenUsageResponse;
582+
}
583+
/**
584+
* Reads a thread's metadata — and optionally its turns — from rollout
585+
* history (`thread/read`). Defaults to the active thread.
586+
*/
587+
async readThread(params?: Partial<ThreadReadParams>): Promise<ThreadReadResponse> {
588+
const threadId = params?.threadId ?? this._threadId;
589+
if (!threadId)
590+
throw new ConnectionStateError(
591+
"not_connected",
592+
"No active thread. Call startThread() first.",
593+
);
594+
return (await this.request("thread/read", { ...params, threadId })) as ThreadReadResponse;
595+
}
596+
/** Renames the active thread (`thread/name/set`). */
597+
async setThreadName(name: string): Promise<ThreadSetNameResponse> {
598+
if (!this._threadId)
599+
throw new ConnectionStateError(
600+
"not_connected",
601+
"No active thread. Call startThread() first.",
602+
);
603+
return (await this.request("thread/name/set", {
604+
threadId: this._threadId,
605+
name,
606+
})) as ThreadSetNameResponse;
607+
}
608+
/** Lists configured MCP servers with startup/auth status (`mcpServerStatus/list`). */
609+
async listMcpServerStatus(
610+
params: ListMcpServerStatusParams = {},
611+
): Promise<ListMcpServerStatusResponse> {
612+
return (await this.request("mcpServerStatus/list", params)) as ListMcpServerStatusResponse;
613+
}
614+
/** Lists skills available to the session (`skills/list`). */
615+
async listSkills(params: SkillsListParams = {}): Promise<SkillsListResponse> {
616+
return (await this.request("skills/list", params)) as SkillsListResponse;
617+
}
557618
/** Interrupts the broker adapter's currently tracked turn. */
558619
async interrupt(): Promise<void> {
559620
await this.request("turn/interrupt", {});

sdk/src/codex/protocol/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ export type * from "./generated/index.js";
33
export type {
44
ConfigReadParams,
55
ConfigReadResponse,
6+
GetAccountRateLimitsResponse,
7+
GetAccountTokenUsageResponse,
8+
ListMcpServerStatusParams,
9+
ListMcpServerStatusResponse,
610
ReviewDelivery,
711
ReviewStartResponse,
812
ReviewTarget,
13+
SkillsListParams,
14+
SkillsListResponse,
15+
ThreadReadParams,
16+
ThreadReadResponse,
17+
ThreadSetNameParams,
18+
ThreadSetNameResponse,
919
ThreadStartParams,
1020
ThreadStartResponse,
1121
ToolRequestUserInputAnswer,

0 commit comments

Comments
 (0)