codex-executor 0.1.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.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/index.ts +21 -0
- package/package.json +39 -0
- package/src/client.ts +283 -0
- package/src/jsonrpc.ts +78 -0
- package/src/types.ts +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mrsekut
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# codex-executor
|
|
2
|
+
|
|
3
|
+
A TypeScript client library for the [Codex CLI](https://github.com/openai/codex) app-server. Communicates via JSON-RPC 2.0 over stdio with a spawned Codex process.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add codex-executor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- [Bun](https://bun.sh/) runtime
|
|
14
|
+
- [Codex CLI](https://github.com/openai/codex) installed and available in PATH
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createCodexClient } from 'codex-executor';
|
|
20
|
+
|
|
21
|
+
const client = createCodexClient();
|
|
22
|
+
|
|
23
|
+
await client.initialize();
|
|
24
|
+
|
|
25
|
+
const thread = await client.startThread({
|
|
26
|
+
model: 'o4-mini',
|
|
27
|
+
baseInstructions: 'You are a helpful assistant.',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = await client.startTurn(thread.id, [
|
|
31
|
+
{ type: 'text', text: 'Hello!', text_elements: [] },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
console.log(result.message);
|
|
35
|
+
console.log(result.tokenUsage);
|
|
36
|
+
|
|
37
|
+
client.kill();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `createCodexClient(options?): CodexClient`
|
|
43
|
+
|
|
44
|
+
Creates a new Codex client instance.
|
|
45
|
+
|
|
46
|
+
**Options:**
|
|
47
|
+
|
|
48
|
+
| Field | Type | Description |
|
|
49
|
+
| ----------- | -------- | --------------------------------------------- |
|
|
50
|
+
| `codexPath` | `string` | Path to the codex binary (default: `"codex"`) |
|
|
51
|
+
| `cwd` | `string` | Working directory for the codex process |
|
|
52
|
+
|
|
53
|
+
### `CodexClient`
|
|
54
|
+
|
|
55
|
+
| Method | Returns | Description |
|
|
56
|
+
| ---------------------------- | --------------------- | ----------------------------------- |
|
|
57
|
+
| `initialize()` | `Promise<void>` | Initialize the client connection |
|
|
58
|
+
| `startThread(params?)` | `Promise<Thread>` | Start a new conversation thread |
|
|
59
|
+
| `resumeThread(threadId)` | `Promise<Thread>` | Resume an existing thread |
|
|
60
|
+
| `startTurn(threadId, input)` | `Promise<TurnResult>` | Send user input and wait for result |
|
|
61
|
+
| `kill()` | `void` | Kill the codex process |
|
|
62
|
+
|
|
63
|
+
### JSON-RPC Utilities
|
|
64
|
+
|
|
65
|
+
Low-level JSON-RPC 2.0 helpers are also exported:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
rpcRequest,
|
|
70
|
+
rpcNotification,
|
|
71
|
+
rpcResponse,
|
|
72
|
+
parseMessage,
|
|
73
|
+
} from 'codex-executor';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
rpcRequest,
|
|
3
|
+
rpcNotification,
|
|
4
|
+
rpcResponse,
|
|
5
|
+
parseMessage,
|
|
6
|
+
} from './src/jsonrpc';
|
|
7
|
+
|
|
8
|
+
export type { ParsedMessage } from './src/jsonrpc';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
CodexClient,
|
|
12
|
+
CodexClientOptions,
|
|
13
|
+
Thread,
|
|
14
|
+
ThreadStartParams,
|
|
15
|
+
TokenUsage,
|
|
16
|
+
TurnResult,
|
|
17
|
+
UserInput,
|
|
18
|
+
} from './src/types';
|
|
19
|
+
|
|
20
|
+
export { createCodexClient } from './src/client';
|
|
21
|
+
export type { CodexProcess } from './src/client';
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-executor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A TypeScript client library for Codex CLI app-server, communicating via JSON-RPC 2.0 over stdio",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "mrsekut",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mrsekut/codex-executor.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/mrsekut/codex-executor#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mrsekut/codex-executor/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"codex",
|
|
18
|
+
"openai",
|
|
19
|
+
"json-rpc",
|
|
20
|
+
"cli",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./index.ts"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src/*.ts",
|
|
29
|
+
"!src/*.test.ts",
|
|
30
|
+
"index.ts"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"prettier": "^3.8.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex app-server client.
|
|
3
|
+
* Communicates via JSON-RPC 2.0 over stdio with a spawned codex process.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
rpcRequest,
|
|
8
|
+
rpcNotification,
|
|
9
|
+
rpcResponse,
|
|
10
|
+
parseMessage,
|
|
11
|
+
} from './jsonrpc';
|
|
12
|
+
import type {
|
|
13
|
+
CodexClient,
|
|
14
|
+
CodexClientOptions,
|
|
15
|
+
Thread,
|
|
16
|
+
ThreadStartParams,
|
|
17
|
+
TokenUsage,
|
|
18
|
+
TurnResult,
|
|
19
|
+
UserInput,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CODEX_PATH = 'codex';
|
|
23
|
+
|
|
24
|
+
/** @internal Process interface for dependency injection in tests */
|
|
25
|
+
export type CodexProcess = {
|
|
26
|
+
stdin: { write(data: string | Uint8Array): unknown };
|
|
27
|
+
stdout: ReadableStream<Uint8Array>;
|
|
28
|
+
stderr: ReadableStream<Uint8Array>;
|
|
29
|
+
kill(): void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type PendingRequest = {
|
|
33
|
+
resolve: (result: unknown) => void;
|
|
34
|
+
reject: (error: Error) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function createCodexClient(
|
|
38
|
+
options: CodexClientOptions & { _process?: CodexProcess } = {},
|
|
39
|
+
): CodexClient {
|
|
40
|
+
return new CodexClientImpl(options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class CodexClientImpl implements CodexClient {
|
|
44
|
+
private proc: CodexProcess;
|
|
45
|
+
private isExternalProcess: boolean;
|
|
46
|
+
private nextId = 0;
|
|
47
|
+
private pending = new Map<number, PendingRequest>();
|
|
48
|
+
private buffer = '';
|
|
49
|
+
private lastTokenUsage: TokenUsage | null = null;
|
|
50
|
+
private lastAgentMessage: string | null = null;
|
|
51
|
+
private turnResolve: ((result: TurnResult) => void) | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(options: CodexClientOptions & { _process?: CodexProcess } = {}) {
|
|
54
|
+
const codexPath = options.codexPath ?? DEFAULT_CODEX_PATH;
|
|
55
|
+
const cwd = options.cwd;
|
|
56
|
+
this.isExternalProcess = options._process != null;
|
|
57
|
+
|
|
58
|
+
this.proc =
|
|
59
|
+
options._process ??
|
|
60
|
+
Bun.spawn([codexPath, 'app-server'], {
|
|
61
|
+
stdin: 'pipe',
|
|
62
|
+
stdout: 'pipe',
|
|
63
|
+
stderr: 'pipe',
|
|
64
|
+
...(cwd ? { cwd } : {}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.startStderrLoop();
|
|
68
|
+
this.startStdoutLoop();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async initialize(): Promise<void> {
|
|
72
|
+
await this.request('initialize', {
|
|
73
|
+
clientInfo: { name: 'codex-executor', version: '0.1.0' },
|
|
74
|
+
capabilities: { experimentalApi: true },
|
|
75
|
+
});
|
|
76
|
+
this.notify('initialized');
|
|
77
|
+
if (!this.isExternalProcess) {
|
|
78
|
+
await Bun.sleep(500);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async startThread(params?: ThreadStartParams): Promise<Thread> {
|
|
83
|
+
const threadParams: Record<string, unknown> = {};
|
|
84
|
+
if (params?.cwd) threadParams['cwd'] = params.cwd;
|
|
85
|
+
if (params?.model) threadParams['model'] = params.model;
|
|
86
|
+
if (params?.baseInstructions)
|
|
87
|
+
threadParams['baseInstructions'] = params.baseInstructions;
|
|
88
|
+
|
|
89
|
+
const result = (await this.request('thread/start', threadParams)) as Record<
|
|
90
|
+
string,
|
|
91
|
+
unknown
|
|
92
|
+
>;
|
|
93
|
+
const thread = result['thread'] as Record<string, unknown>;
|
|
94
|
+
return { id: thread['id'] as string };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async resumeThread(threadId: string): Promise<Thread> {
|
|
98
|
+
const result = (await this.request('thread/resume', {
|
|
99
|
+
threadId,
|
|
100
|
+
})) as Record<string, unknown>;
|
|
101
|
+
const thread = result['thread'] as Record<string, unknown>;
|
|
102
|
+
return { id: thread['id'] as string };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async startTurn(threadId: string, input: UserInput[]): Promise<TurnResult> {
|
|
106
|
+
this.lastTokenUsage = null;
|
|
107
|
+
this.lastAgentMessage = null;
|
|
108
|
+
|
|
109
|
+
const turnPromise = new Promise<TurnResult>(resolve => {
|
|
110
|
+
this.turnResolve = resolve;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this.request('turn/start', { threadId, input });
|
|
114
|
+
|
|
115
|
+
return turnPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
kill(): void {
|
|
119
|
+
this.proc.kill();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Private methods ---
|
|
123
|
+
|
|
124
|
+
private send(data: string): void {
|
|
125
|
+
this.proc.stdin.write(data + '\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private request(
|
|
129
|
+
method: string,
|
|
130
|
+
params: Record<string, unknown> = {},
|
|
131
|
+
): Promise<unknown> {
|
|
132
|
+
const id = ++this.nextId;
|
|
133
|
+
const payload = rpcRequest(id, method, params);
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
this.pending.set(id, { resolve, reject });
|
|
136
|
+
this.send(payload);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private notify(method: string, params: Record<string, unknown> = {}): void {
|
|
141
|
+
this.send(rpcNotification(method, params));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private handleServerRequest(
|
|
145
|
+
id: number,
|
|
146
|
+
method: string,
|
|
147
|
+
_params: unknown,
|
|
148
|
+
): void {
|
|
149
|
+
if (
|
|
150
|
+
method === 'item/commandExecution/requestApproval' ||
|
|
151
|
+
method === 'execCommandApproval' ||
|
|
152
|
+
method === 'item/fileChange/requestApproval' ||
|
|
153
|
+
method === 'applyPatchApproval'
|
|
154
|
+
) {
|
|
155
|
+
this.send(rpcResponse(id, { decision: 'accept' }));
|
|
156
|
+
} else if (method === 'item/tool/call') {
|
|
157
|
+
this.send(
|
|
158
|
+
rpcResponse(id, { output: 'No handler registered', success: false }),
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
this.send(
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
jsonrpc: '2.0',
|
|
164
|
+
id,
|
|
165
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private handleNotification(method: string, params: unknown): void {
|
|
172
|
+
const p = params as Record<string, unknown> | undefined;
|
|
173
|
+
|
|
174
|
+
if (method === 'turn/completed') {
|
|
175
|
+
const turn = p?.['turn'] as Record<string, unknown> | undefined;
|
|
176
|
+
const status = turn?.['status'] as string | undefined;
|
|
177
|
+
const error = turn?.['error'] as
|
|
178
|
+
| Record<string, unknown>
|
|
179
|
+
| null
|
|
180
|
+
| undefined;
|
|
181
|
+
|
|
182
|
+
if (this.turnResolve) {
|
|
183
|
+
this.turnResolve({
|
|
184
|
+
success: status === 'completed',
|
|
185
|
+
message: this.lastAgentMessage,
|
|
186
|
+
tokenUsage: this.lastTokenUsage,
|
|
187
|
+
error: error
|
|
188
|
+
? ((error['message'] as string) ?? 'Unknown error')
|
|
189
|
+
: null,
|
|
190
|
+
});
|
|
191
|
+
this.turnResolve = null;
|
|
192
|
+
}
|
|
193
|
+
} else if (method === 'thread/tokenUsage/updated') {
|
|
194
|
+
const usage = p?.['tokenUsage'] as Record<string, unknown> | undefined;
|
|
195
|
+
const total = usage?.['total'] as Record<string, unknown> | undefined;
|
|
196
|
+
if (total) {
|
|
197
|
+
this.lastTokenUsage = {
|
|
198
|
+
totalTokens: total['totalTokens'] as number,
|
|
199
|
+
inputTokens: total['inputTokens'] as number,
|
|
200
|
+
outputTokens: total['outputTokens'] as number,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
} else if (method === 'codex/event/task_complete') {
|
|
204
|
+
const msg = p?.['msg'] as Record<string, unknown> | undefined;
|
|
205
|
+
this.lastAgentMessage = (msg?.['last_agent_message'] as string) ?? null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private startStderrLoop(): void {
|
|
210
|
+
(async () => {
|
|
211
|
+
const reader = this.proc.stderr.getReader();
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
try {
|
|
214
|
+
while (true) {
|
|
215
|
+
const { done, value } = await reader.read();
|
|
216
|
+
if (done) break;
|
|
217
|
+
const text = decoder.decode(value, { stream: true });
|
|
218
|
+
if (!text.includes('failed to refresh available models')) {
|
|
219
|
+
process.stderr.write(`[codex] ${text}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// process exited
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private startStdoutLoop(): void {
|
|
229
|
+
(async () => {
|
|
230
|
+
const reader = this.proc.stdout.getReader();
|
|
231
|
+
const decoder = new TextDecoder();
|
|
232
|
+
try {
|
|
233
|
+
while (true) {
|
|
234
|
+
const { done, value } = await reader.read();
|
|
235
|
+
if (done) break;
|
|
236
|
+
this.buffer += decoder.decode(value, { stream: true });
|
|
237
|
+
|
|
238
|
+
let newlineIdx: number;
|
|
239
|
+
while ((newlineIdx = this.buffer.indexOf('\n')) !== -1) {
|
|
240
|
+
const line = this.buffer.slice(0, newlineIdx).trim();
|
|
241
|
+
this.buffer = this.buffer.slice(newlineIdx + 1);
|
|
242
|
+
if (!line) continue;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const raw = JSON.parse(line) as unknown;
|
|
246
|
+
const msg = parseMessage(raw);
|
|
247
|
+
if (!msg) continue;
|
|
248
|
+
|
|
249
|
+
switch (msg.kind) {
|
|
250
|
+
case 'response': {
|
|
251
|
+
const p = this.pending.get(msg.id);
|
|
252
|
+
if (p) {
|
|
253
|
+
p.resolve(msg.result);
|
|
254
|
+
this.pending.delete(msg.id);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'error-response': {
|
|
259
|
+
const p = this.pending.get(msg.id);
|
|
260
|
+
if (p) {
|
|
261
|
+
p.reject(new Error(msg.error.message));
|
|
262
|
+
this.pending.delete(msg.id);
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'server-request':
|
|
267
|
+
this.handleServerRequest(msg.id, msg.method, msg.params);
|
|
268
|
+
break;
|
|
269
|
+
case 'notification':
|
|
270
|
+
this.handleNotification(msg.method, msg.params);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// skip unparseable lines
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// process exited
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
}
|
|
283
|
+
}
|
package/src/jsonrpc.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 protocol utilities for Codex app-server communication.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function rpcRequest(
|
|
6
|
+
id: number,
|
|
7
|
+
method: string,
|
|
8
|
+
params: Record<string, unknown> = {},
|
|
9
|
+
): string {
|
|
10
|
+
return JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rpcNotification(
|
|
14
|
+
method: string,
|
|
15
|
+
params: Record<string, unknown> = {},
|
|
16
|
+
): string {
|
|
17
|
+
return JSON.stringify({ jsonrpc: '2.0', method, params });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rpcResponse(id: number, result: unknown): string {
|
|
21
|
+
return JSON.stringify({ jsonrpc: '2.0', id, result });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- Message parsing ---
|
|
25
|
+
|
|
26
|
+
export type ParsedMessage =
|
|
27
|
+
| { kind: 'response'; id: number; result: unknown }
|
|
28
|
+
| {
|
|
29
|
+
kind: 'error-response';
|
|
30
|
+
id: number;
|
|
31
|
+
error: { code: number; message: string };
|
|
32
|
+
}
|
|
33
|
+
| { kind: 'server-request'; id: number; method: string; params: unknown }
|
|
34
|
+
| { kind: 'notification'; method: string; params: unknown };
|
|
35
|
+
|
|
36
|
+
export function parseMessage(raw: unknown): ParsedMessage | null {
|
|
37
|
+
if (typeof raw !== 'object' || raw === null) return null;
|
|
38
|
+
const msg = raw as Record<string, unknown>;
|
|
39
|
+
|
|
40
|
+
// Response (has id + result)
|
|
41
|
+
if ('id' in msg && 'result' in msg) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'response',
|
|
44
|
+
id: msg['id'] as number,
|
|
45
|
+
result: msg['result'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Error response (has id + error)
|
|
50
|
+
if ('id' in msg && 'error' in msg) {
|
|
51
|
+
return {
|
|
52
|
+
kind: 'error-response',
|
|
53
|
+
id: msg['id'] as number,
|
|
54
|
+
error: msg['error'] as { code: number; message: string },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Server request (has id + method)
|
|
59
|
+
if ('id' in msg && 'method' in msg) {
|
|
60
|
+
return {
|
|
61
|
+
kind: 'server-request',
|
|
62
|
+
id: msg['id'] as number,
|
|
63
|
+
method: msg['method'] as string,
|
|
64
|
+
params: msg['params'],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Notification (has method, no id)
|
|
69
|
+
if ('method' in msg && !('id' in msg)) {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'notification',
|
|
72
|
+
method: msg['method'] as string,
|
|
73
|
+
params: msg['params'],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Codex app-server client.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CodexClientOptions = {
|
|
6
|
+
/** Path to the codex binary (default: "codex") */
|
|
7
|
+
codexPath?: string;
|
|
8
|
+
|
|
9
|
+
/** Working directory for the codex process */
|
|
10
|
+
cwd?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ThreadStartParams = {
|
|
14
|
+
/** Override working directory for this thread */
|
|
15
|
+
cwd?: string;
|
|
16
|
+
|
|
17
|
+
/** Override model */
|
|
18
|
+
model?: string;
|
|
19
|
+
|
|
20
|
+
/** Base instructions (system prompt) */
|
|
21
|
+
baseInstructions?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Thread = {
|
|
25
|
+
id: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type TurnResult = {
|
|
29
|
+
success: boolean;
|
|
30
|
+
message: string | null;
|
|
31
|
+
tokenUsage: TokenUsage | null;
|
|
32
|
+
error: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TokenUsage = {
|
|
36
|
+
totalTokens: number;
|
|
37
|
+
inputTokens: number;
|
|
38
|
+
outputTokens: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type UserInput =
|
|
42
|
+
| { type: 'text'; text: string; text_elements: [] }
|
|
43
|
+
| { type: 'skill'; name: string; path: string };
|
|
44
|
+
|
|
45
|
+
export type CodexClient = {
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
startThread(params?: ThreadStartParams): Promise<Thread>;
|
|
48
|
+
resumeThread(threadId: string): Promise<Thread>;
|
|
49
|
+
startTurn(threadId: string, input: UserInput[]): Promise<TurnResult>;
|
|
50
|
+
kill(): void;
|
|
51
|
+
};
|