@lythos/agent-adapter-codex 0.9.49
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/package.json +27 -0
- package/src/index.test.ts +65 -0
- package/src/index.ts +126 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lythos/agent-adapter-codex",
|
|
3
|
+
"version": "0.9.49",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "bun test src/ --pass-with-no-tests",
|
|
7
|
+
"test:coverage": "bun test src/ --coverage --coverage-reporter=lcov --coverage-dir=coverage --pass-with-no-tests",
|
|
8
|
+
"test:watch": "bun test src/ --watch"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"codex",
|
|
20
|
+
"openai",
|
|
21
|
+
"agent-adapter",
|
|
22
|
+
"codex-cli"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@lythos/agent-adapter": "^0.9.48"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { buildCodexCommand, parseCodexJsonl } from './index'
|
|
3
|
+
|
|
4
|
+
describe('buildCodexCommand', () => {
|
|
5
|
+
it('builds codex exec args without shell wrapper', () => {
|
|
6
|
+
const cmd = buildCodexCommand()
|
|
7
|
+
expect(cmd[0]).toBe('codex')
|
|
8
|
+
expect(cmd[1]).toBe('exec')
|
|
9
|
+
expect(cmd).toContain('--json')
|
|
10
|
+
expect(cmd).toContain('--ephemeral')
|
|
11
|
+
expect(cmd).toContain('--skip-git-repo-check')
|
|
12
|
+
expect(cmd).toContain('--dangerously-bypass-approvals-and-sandbox')
|
|
13
|
+
expect(cmd).toContain('-')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not use shell (injection-safe)', () => {
|
|
17
|
+
const cmd = buildCodexCommand()
|
|
18
|
+
expect(cmd).not.toContain('sh')
|
|
19
|
+
expect(cmd).not.toContain('-c')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('parseCodexJsonl', () => {
|
|
24
|
+
it('extracts text from item.completed agent_message', () => {
|
|
25
|
+
const raw = [
|
|
26
|
+
'{"type":"item.completed","item":{"type":"agent_message","content":[{"type":"output_text","text":"Hello Codex"}]}}',
|
|
27
|
+
].join('\n')
|
|
28
|
+
expect(parseCodexJsonl(raw)).toBe('Hello Codex')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('extracts text from thread.turn.completed', () => {
|
|
32
|
+
const raw = [
|
|
33
|
+
'{"type":"thread.turn.completed","turn":{"final_message":"Task done"}}',
|
|
34
|
+
].join('\n')
|
|
35
|
+
expect(parseCodexJsonl(raw)).toBe('Task done')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('concatenates multiple messages', () => {
|
|
39
|
+
const raw = [
|
|
40
|
+
'{"type":"item.completed","item":{"type":"agent_message","content":[{"type":"output_text","text":"A"}]}}',
|
|
41
|
+
'{"type":"item.completed","item":{"type":"agent_message","content":[{"type":"output_text","text":"B"}]}}',
|
|
42
|
+
].join('\n')
|
|
43
|
+
expect(parseCodexJsonl(raw)).toBe('A\nB')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('skips non-message events', () => {
|
|
47
|
+
const raw = [
|
|
48
|
+
'{"type":"thread.started"}',
|
|
49
|
+
'{"type":"item.completed","item":{"type":"agent_message","content":[{"type":"output_text","text":"valid"}]}}',
|
|
50
|
+
].join('\n')
|
|
51
|
+
expect(parseCodexJsonl(raw)).toBe('valid')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('skips malformed JSON lines', () => {
|
|
55
|
+
const raw = [
|
|
56
|
+
'garbage',
|
|
57
|
+
'{"type":"item.completed","item":{"type":"agent_message","content":[{"type":"output_text","text":"ok"}]}}',
|
|
58
|
+
].join('\n')
|
|
59
|
+
expect(parseCodexJsonl(raw)).toBe('ok')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns empty for empty input', () => {
|
|
63
|
+
expect(parseCodexJsonl('')).toBe('')
|
|
64
|
+
})
|
|
65
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex adapter — spawns `codex exec` headless CLI.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same CLI-wrapper pattern as the kimi adapter:
|
|
5
|
+
* - Prompt via temp file + Bun.file(stdin) — no shell, no injection
|
|
6
|
+
* - Array args — no string interpolation
|
|
7
|
+
* - JSONL stdout parsing
|
|
8
|
+
*
|
|
9
|
+
* Reference: codex-research-2026-05-10
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFileSync, unlinkSync, existsSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import type { AgentAdapter, AgentRunResult } from '@lythos/agent-adapter'
|
|
16
|
+
import { registerAgent } from '@lythos/agent-adapter'
|
|
17
|
+
|
|
18
|
+
// ── Pure functions (testable without CLI) ────────────────────
|
|
19
|
+
|
|
20
|
+
/** Build codex exec args — no shell wrapper, injection-safe. */
|
|
21
|
+
export function buildCodexCommand(): string[] {
|
|
22
|
+
return [
|
|
23
|
+
'codex', 'exec',
|
|
24
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
25
|
+
'--json',
|
|
26
|
+
'--ephemeral',
|
|
27
|
+
'--skip-git-repo-check',
|
|
28
|
+
'-', // read prompt from stdin
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse codex JSONL output into plain text.
|
|
34
|
+
* Extracts text from item.completed events and final thread.turn.completed message.
|
|
35
|
+
*/
|
|
36
|
+
export function parseCodexJsonl(raw: string): string {
|
|
37
|
+
const lines: string[] = []
|
|
38
|
+
for (const line of raw.split('\n')) {
|
|
39
|
+
if (!line.trim()) continue
|
|
40
|
+
try {
|
|
41
|
+
const event = JSON.parse(line)
|
|
42
|
+
// item.completed: agent finished a task item
|
|
43
|
+
if (event.type === 'item.completed' && event.item?.type === 'agent_message') {
|
|
44
|
+
for (const block of event.item.content || []) {
|
|
45
|
+
if (block.type === 'output_text' && block.text) {
|
|
46
|
+
lines.push(block.text)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// thread.turn.completed: final turn result
|
|
51
|
+
if (event.type === 'thread.turn.completed') {
|
|
52
|
+
const msg = event.turn?.final_message
|
|
53
|
+
if (typeof msg === 'string') lines.push(msg)
|
|
54
|
+
}
|
|
55
|
+
// Fallback: any event with a text message
|
|
56
|
+
if (event.message && typeof event.message === 'string') {
|
|
57
|
+
lines.push(event.message)
|
|
58
|
+
}
|
|
59
|
+
} catch { /* skip malformed lines */ }
|
|
60
|
+
}
|
|
61
|
+
return lines.join('\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Spawn wrapper (IO, tested via arena integration) ─────────
|
|
65
|
+
|
|
66
|
+
async function spawnCodex(opts: {
|
|
67
|
+
brief: string
|
|
68
|
+
cwd: string
|
|
69
|
+
timeoutMs?: number
|
|
70
|
+
}): Promise<AgentRunResult> {
|
|
71
|
+
const promptFile = join(tmpdir(), `codex-prompt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`)
|
|
72
|
+
writeFileSync(promptFile, opts.brief, 'utf-8')
|
|
73
|
+
|
|
74
|
+
const start = Date.now()
|
|
75
|
+
|
|
76
|
+
const proc = Bun.spawn(buildCodexCommand(), {
|
|
77
|
+
cwd: opts.cwd,
|
|
78
|
+
stdin: Bun.file(promptFile),
|
|
79
|
+
stdout: 'pipe',
|
|
80
|
+
stderr: 'pipe',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const timeout = setTimeout(() => proc.kill(), opts.timeoutMs ?? 120_000)
|
|
84
|
+
await proc.exited
|
|
85
|
+
clearTimeout(timeout)
|
|
86
|
+
|
|
87
|
+
try { unlinkSync(promptFile) } catch {}
|
|
88
|
+
|
|
89
|
+
const durationMs = Date.now() - start
|
|
90
|
+
const rawStdout = await new Response(proc.stdout).text()
|
|
91
|
+
const stderr = await new Response(proc.stderr).text()
|
|
92
|
+
const code = proc.exitCode ?? 1
|
|
93
|
+
|
|
94
|
+
let stdout: string
|
|
95
|
+
try {
|
|
96
|
+
stdout = parseCodexJsonl(rawStdout)
|
|
97
|
+
} catch {
|
|
98
|
+
stdout = rawStdout
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { stdout: stdout.trim(), stderr, code, durationMs, checkpoints: [] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const codexAdapter: AgentAdapter = {
|
|
105
|
+
name: 'codex',
|
|
106
|
+
|
|
107
|
+
async spawn(opts): Promise<AgentRunResult> {
|
|
108
|
+
if (!Bun.which('codex')) {
|
|
109
|
+
throw new Error('codex not found in PATH. Install: npm i -g @openai/codex')
|
|
110
|
+
}
|
|
111
|
+
// Verify auth exists (OAuth or API key)
|
|
112
|
+
const home = process.env.HOME || process.env.USERPROFILE || '~'
|
|
113
|
+
const authFile = join(home, '.codex', 'auth.json')
|
|
114
|
+
if (!existsSync(authFile) && !process.env.OPENAI_API_KEY) {
|
|
115
|
+
console.warn('⚠️ No ~/.codex/auth.json or OPENAI_API_KEY found. Run "codex login" first.')
|
|
116
|
+
}
|
|
117
|
+
return spawnCodex(opts)
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async invokeTool(_opts): Promise<unknown> {
|
|
121
|
+
throw new Error('invokeTool not implemented for codex adapter')
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
registerAgent('codex', codexAdapter)
|
|
126
|
+
export { codexAdapter }
|