@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 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 }