@fixy/codex-adapter 0.0.2 → 0.0.4
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/.omc/state/last-tool-error.json +7 -0
- package/dist/__tests__/adapter.test.d.ts +2 -0
- package/dist/__tests__/adapter.test.d.ts.map +1 -0
- package/dist/__tests__/adapter.test.js +165 -0
- package/dist/__tests__/adapter.test.js.map +1 -0
- package/dist/__tests__/parse.test.d.ts +2 -0
- package/dist/__tests__/parse.test.d.ts.map +1 -0
- package/dist/__tests__/parse.test.js +78 -0
- package/dist/__tests__/parse.test.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -1
- package/dist/index.js.map +1 -1
- package/dist/parse.d.ts +6 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +56 -0
- package/dist/parse.js.map +1 -0
- package/package.json +6 -2
- package/src/__tests__/adapter.test.ts +195 -0
- package/src/__tests__/parse.test.ts +94 -0
- package/src/index.ts +201 -1
- package/src/parse.ts +65 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tool_name": "Bash",
|
|
3
|
+
"tool_input_preview": "{\"command\":\"pnpm tsc --noEmit 2>&1\",\"description\":\"Typecheck codex-adapter directly\"}",
|
|
4
|
+
"error": "Exit code 2\nsrc/index.ts(51,14): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.\n[rerun: b14]",
|
|
5
|
+
"timestamp": "2026-04-12T11:48:20.179Z",
|
|
6
|
+
"retry_count": 1
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createCodexAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let originalPath;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-codex-test-'));
|
|
10
|
+
originalPath = process.env.PATH ?? '';
|
|
11
|
+
const mockCodex = path.join(tmpDir, 'codex');
|
|
12
|
+
fs.writeFileSync(mockCodex, `#!/bin/bash
|
|
13
|
+
if [[ "$1" == "--version" ]]; then
|
|
14
|
+
echo "codex-cli 0.112.0"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [[ "$1" == "exec" ]]; then
|
|
19
|
+
# Check for resume subcommand
|
|
20
|
+
THREAD_ID="mock-thread-001"
|
|
21
|
+
if [[ "$2" == "resume" ]]; then
|
|
22
|
+
THREAD_ID="$3"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Emit noise to stderr (should be filtered)
|
|
26
|
+
echo "2026-04-12T11:46:37.355150Z ERROR codex_core::skills::loader: failed to stat skills entry" >&2
|
|
27
|
+
|
|
28
|
+
echo '{"type":"thread.started","thread_id":"'"\$THREAD_ID"'"}'
|
|
29
|
+
echo '{"type":"turn.started"}'
|
|
30
|
+
echo '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Mock codex response."}}'
|
|
31
|
+
echo '{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":10}}'
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "Unknown command" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
`, { mode: 0o755 });
|
|
38
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
39
|
+
});
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
process.env.PATH = originalPath;
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
function makeCtx(overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
runId: 'test-run-001',
|
|
47
|
+
agent: { id: 'codex', name: 'Codex CLI' },
|
|
48
|
+
threadContext: {
|
|
49
|
+
threadId: 'thread-001',
|
|
50
|
+
projectRoot: tmpDir,
|
|
51
|
+
worktreePath: tmpDir,
|
|
52
|
+
repoRef: null,
|
|
53
|
+
},
|
|
54
|
+
messages: [],
|
|
55
|
+
prompt: 'Hello Codex',
|
|
56
|
+
session: null,
|
|
57
|
+
onLog: () => { },
|
|
58
|
+
onMeta: () => { },
|
|
59
|
+
onSpawn: () => { },
|
|
60
|
+
signal: AbortSignal.timeout(30_000),
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
describe('CodexAdapter.probe()', () => {
|
|
65
|
+
it('finds the mock codex binary and reports available=true', async () => {
|
|
66
|
+
const adapter = createCodexAdapter();
|
|
67
|
+
const result = await adapter.probe();
|
|
68
|
+
expect(result.available).toBe(true);
|
|
69
|
+
expect(result.version).toContain('0.112.0');
|
|
70
|
+
});
|
|
71
|
+
it('reports available=false when codex is not on PATH', async () => {
|
|
72
|
+
const adapter = createCodexAdapter();
|
|
73
|
+
const savedPath = process.env.PATH;
|
|
74
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
75
|
+
try {
|
|
76
|
+
const result = await adapter.probe();
|
|
77
|
+
expect(result.available).toBe(false);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
process.env.PATH = savedPath;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('CodexAdapter.execute()', () => {
|
|
85
|
+
it('executes with mock codex and returns expected result', async () => {
|
|
86
|
+
const adapter = createCodexAdapter();
|
|
87
|
+
let metaCalled = false;
|
|
88
|
+
let spawnPid = null;
|
|
89
|
+
const ctx = makeCtx({
|
|
90
|
+
onMeta: (_meta) => {
|
|
91
|
+
metaCalled = true;
|
|
92
|
+
},
|
|
93
|
+
onSpawn: (pid) => {
|
|
94
|
+
spawnPid = pid;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const result = await adapter.execute(ctx);
|
|
98
|
+
expect(result.summary).toBe('Mock codex response.');
|
|
99
|
+
expect(result.session).not.toBeNull();
|
|
100
|
+
expect(result.session?.sessionId).toBe('mock-thread-001');
|
|
101
|
+
expect(result.exitCode).toBe(0);
|
|
102
|
+
expect(result.timedOut).toBe(false);
|
|
103
|
+
expect(result.patches).toEqual([]);
|
|
104
|
+
expect(metaCalled).toBe(true);
|
|
105
|
+
expect(typeof spawnPid).toBe('number');
|
|
106
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
it('resumes a session by passing the thread id to codex exec resume', async () => {
|
|
109
|
+
const adapter = createCodexAdapter();
|
|
110
|
+
const ctx = makeCtx({
|
|
111
|
+
session: { sessionId: 'resume-thread-42', params: {} },
|
|
112
|
+
});
|
|
113
|
+
const result = await adapter.execute(ctx);
|
|
114
|
+
expect(result.session).not.toBeNull();
|
|
115
|
+
expect(result.session?.sessionId).toBe('resume-thread-42');
|
|
116
|
+
});
|
|
117
|
+
it('filters codex startup noise from stderr', async () => {
|
|
118
|
+
const adapter = createCodexAdapter();
|
|
119
|
+
const stderrChunks = [];
|
|
120
|
+
const ctx = makeCtx({
|
|
121
|
+
onLog: (stream, chunk) => {
|
|
122
|
+
if (stream === 'stderr')
|
|
123
|
+
stderrChunks.push(chunk);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const result = await adapter.execute(ctx);
|
|
127
|
+
// The mock writes an ERROR codex_core line to stderr — it should be filtered
|
|
128
|
+
expect(stderrChunks.join('')).not.toContain('codex_core::skills::loader');
|
|
129
|
+
expect(result.warnings.length).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
it('forwards only agent_message text to stdout onLog, not raw JSON', async () => {
|
|
132
|
+
const adapter = createCodexAdapter();
|
|
133
|
+
const stdoutChunks = [];
|
|
134
|
+
const ctx = makeCtx({
|
|
135
|
+
onLog: (stream, chunk) => {
|
|
136
|
+
if (stream === 'stdout')
|
|
137
|
+
stdoutChunks.push(chunk);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
await adapter.execute(ctx);
|
|
141
|
+
const output = stdoutChunks.join('');
|
|
142
|
+
expect(output).toContain('Mock codex response.');
|
|
143
|
+
expect(output).not.toContain('thread.started');
|
|
144
|
+
expect(output).not.toContain('turn.started');
|
|
145
|
+
expect(output).not.toContain('turn.completed');
|
|
146
|
+
});
|
|
147
|
+
it('inherits CODEX_HOME from process.env', async () => {
|
|
148
|
+
const adapter = createCodexAdapter();
|
|
149
|
+
let metaEnv = {};
|
|
150
|
+
process.env['CODEX_HOME'] = '/custom/codex/home';
|
|
151
|
+
const ctx = makeCtx({
|
|
152
|
+
onMeta: (meta) => {
|
|
153
|
+
metaEnv = meta.env;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await adapter.execute(ctx);
|
|
158
|
+
expect(metaEnv['CODEX_HOME']).toBe('/custom/codex/home');
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
delete process.env['CODEX_HOME'];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.js","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,IAAI,MAAc,CAAC;AACnB,IAAI,YAAoB,CAAC;AAEzB,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACpE,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAEtC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,EAAE,CAAC,aAAa,CACd,SAAS,EACT;;;;;;;;;;;;;;;;;;;;;;;;;CAyBH,EACG,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,GAAG,GAAG,YAAY,CAAC;AACjD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,GAAG,EAAE;IACZ,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,YAAY,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,SAAS,OAAO,CAAC,YAA2C,EAAE;IAC5D,OAAO;QACL,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE;QACzC,aAAa,EAAE;YACb,QAAQ,EAAE,YAAY;YACtB,WAAW,EAAE,MAAM;YACnB,YAAY,EAAE,MAAM;YACpB,OAAO,EAAE,IAAI;SACd;QACD,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;QACf,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC;QACjB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,GAAG,SAAS;KACsB,CAAC;AACvC,CAAC;AAED,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAErC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,4BAA4B,CAAC;QAEhD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACrC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,IAAI,QAAQ,GAAkB,IAAI,CAAC;QAEnC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,MAAM,EAAE,CAAC,KAAyB,EAAE,EAAE;gBACpC,UAAU,GAAG,IAAI,CAAC;YACpB,CAAC;YACD,OAAO,EAAE,CAAC,GAAW,EAAE,EAAE;gBACvB,QAAQ,GAAG,GAAG,CAAC;YACjB,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACnC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QAErC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,OAAO,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,EAAE;SACvD,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;gBACvB,IAAI,MAAM,KAAK,QAAQ;oBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpD,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,6EAA6E;QAC7E,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;gBACvB,IAAI,MAAM,KAAK,QAAQ;oBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpD,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE3B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,IAAI,OAAO,GAA2B,EAAE,CAAC;QAEzC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAAC;QAEjD,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,MAAM,EAAE,CAAC,IAAwB,EAAE,EAAE;gBACnC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;YACrB,CAAC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAC3D,CAAC;gBAAS,CAAC;YACT,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexStreamJson } from '../parse.js';
|
|
3
|
+
describe('parseCodexStreamJson', () => {
|
|
4
|
+
it('extracts sessionId from thread.started and text from item.completed', () => {
|
|
5
|
+
const input = [
|
|
6
|
+
JSON.stringify({ type: 'thread.started', thread_id: '019d8183-e8b0-7e33-a2f8-29d950432756' }),
|
|
7
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
8
|
+
JSON.stringify({
|
|
9
|
+
type: 'item.completed',
|
|
10
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Hello.' },
|
|
11
|
+
}),
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
type: 'turn.completed',
|
|
14
|
+
usage: { input_tokens: 100, cached_input_tokens: 50, output_tokens: 10 },
|
|
15
|
+
}),
|
|
16
|
+
].join('\n');
|
|
17
|
+
const result = parseCodexStreamJson(input);
|
|
18
|
+
expect(result.sessionId).toBe('019d8183-e8b0-7e33-a2f8-29d950432756');
|
|
19
|
+
expect(result.summary).toBe('Hello.');
|
|
20
|
+
});
|
|
21
|
+
it('concatenates multiple agent_message items', () => {
|
|
22
|
+
const input = [
|
|
23
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-multi' }),
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
type: 'item.completed',
|
|
26
|
+
item: { id: 'item_0', type: 'agent_message', text: 'First part.' },
|
|
27
|
+
}),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: 'item.completed',
|
|
30
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Second part.' },
|
|
31
|
+
}),
|
|
32
|
+
].join('\n');
|
|
33
|
+
const result = parseCodexStreamJson(input);
|
|
34
|
+
expect(result.sessionId).toBe('thread-multi');
|
|
35
|
+
expect(result.summary).toContain('First part.');
|
|
36
|
+
expect(result.summary).toContain('Second part.');
|
|
37
|
+
});
|
|
38
|
+
it('ignores non-agent_message items', () => {
|
|
39
|
+
const input = [
|
|
40
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-filter' }),
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
type: 'item.completed',
|
|
43
|
+
item: { id: 'item_0', type: 'tool_call', text: 'should be ignored' },
|
|
44
|
+
}),
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
type: 'item.completed',
|
|
47
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Visible text.' },
|
|
48
|
+
}),
|
|
49
|
+
].join('\n');
|
|
50
|
+
const result = parseCodexStreamJson(input);
|
|
51
|
+
expect(result.summary).toBe('Visible text.');
|
|
52
|
+
expect(result.summary).not.toContain('should be ignored');
|
|
53
|
+
});
|
|
54
|
+
it('falls back to raw stdout for non-JSON output', () => {
|
|
55
|
+
const input = 'This is plain text output\nNot JSON\n';
|
|
56
|
+
const result = parseCodexStreamJson(input);
|
|
57
|
+
expect(result.sessionId).toBeNull();
|
|
58
|
+
expect(result.summary).toBe(input);
|
|
59
|
+
});
|
|
60
|
+
it('returns null sessionId and empty summary for empty string', () => {
|
|
61
|
+
const result = parseCodexStreamJson('');
|
|
62
|
+
expect(result.sessionId).toBeNull();
|
|
63
|
+
expect(result.summary).toBe('');
|
|
64
|
+
});
|
|
65
|
+
it('handles missing thread_id in thread.started gracefully', () => {
|
|
66
|
+
const input = [
|
|
67
|
+
JSON.stringify({ type: 'thread.started' }),
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
type: 'item.completed',
|
|
70
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Response without session.' },
|
|
71
|
+
}),
|
|
72
|
+
].join('\n');
|
|
73
|
+
const result = parseCodexStreamJson(input);
|
|
74
|
+
expect(result.sessionId).toBeNull();
|
|
75
|
+
expect(result.summary).toBe('Response without session.');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=parse.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.js","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAIZ,MAAM,YAAY,CAAC;AA6LpB,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAED,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,172 @@
|
|
|
1
|
-
|
|
1
|
+
// packages/codex-adapter/src/index.ts
|
|
2
|
+
import { buildInheritedEnv, redactEnvForLogs, resolveCommand, runChildProcess, } from '@fixy/adapter-utils';
|
|
3
|
+
import { parseCodexStreamJson } from './parse.js';
|
|
4
|
+
// Codex CLI writes skill-loader errors to stderr on startup — suppress them.
|
|
5
|
+
const CODEX_NOISE_RE = /^[0-9TZ:.+-]+ (ERROR|WARN) codex_/;
|
|
6
|
+
function filterCodexNoise(stderr) {
|
|
7
|
+
return stderr
|
|
8
|
+
.split('\n')
|
|
9
|
+
.filter((line) => !CODEX_NOISE_RE.test(line))
|
|
10
|
+
.join('\n')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
class CodexAdapter {
|
|
14
|
+
id = 'codex';
|
|
15
|
+
name = 'Codex CLI';
|
|
16
|
+
async probe() {
|
|
17
|
+
let resolvedCommand;
|
|
18
|
+
try {
|
|
19
|
+
resolvedCommand = await resolveCommand('codex');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {
|
|
23
|
+
available: false,
|
|
24
|
+
version: null,
|
|
25
|
+
authStatus: 'unknown',
|
|
26
|
+
detail: 'codex CLI not found in PATH',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await runChildProcess({
|
|
31
|
+
command: resolvedCommand,
|
|
32
|
+
args: ['--version'],
|
|
33
|
+
cwd: process.cwd(),
|
|
34
|
+
env: buildInheritedEnv(),
|
|
35
|
+
timeoutMs: 10_000,
|
|
36
|
+
});
|
|
37
|
+
const version = result.stdout.trim() || null;
|
|
38
|
+
return {
|
|
39
|
+
available: true,
|
|
40
|
+
version,
|
|
41
|
+
authStatus: 'unknown',
|
|
42
|
+
detail: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
version: null,
|
|
50
|
+
authStatus: 'unknown',
|
|
51
|
+
detail,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async execute(ctx) {
|
|
56
|
+
const resolvedCommand = await resolveCommand('codex');
|
|
57
|
+
const env = buildInheritedEnv({});
|
|
58
|
+
let args;
|
|
59
|
+
if (ctx.session) {
|
|
60
|
+
// Resume existing session: codex exec resume <thread_id> --json ...
|
|
61
|
+
args = [
|
|
62
|
+
'exec',
|
|
63
|
+
'resume',
|
|
64
|
+
ctx.session.sessionId,
|
|
65
|
+
'--json',
|
|
66
|
+
'--skip-git-repo-check',
|
|
67
|
+
'--full-auto',
|
|
68
|
+
ctx.prompt,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// New session: codex exec --json --skip-git-repo-check --full-auto <prompt>
|
|
73
|
+
args = [
|
|
74
|
+
'exec',
|
|
75
|
+
'--json',
|
|
76
|
+
'--skip-git-repo-check',
|
|
77
|
+
'--full-auto',
|
|
78
|
+
ctx.prompt,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
ctx.onMeta({
|
|
82
|
+
resolvedCommand,
|
|
83
|
+
args,
|
|
84
|
+
cwd: ctx.threadContext.worktreePath,
|
|
85
|
+
env: redactEnvForLogs(env),
|
|
86
|
+
});
|
|
87
|
+
// Buffer partial stdout lines so we can parse JSONL events and forward
|
|
88
|
+
// only the agent text instead of raw JSON to the terminal.
|
|
89
|
+
let stdoutLineBuffer = '';
|
|
90
|
+
const forwardJsonLine = (line) => {
|
|
91
|
+
try {
|
|
92
|
+
const obj = JSON.parse(line);
|
|
93
|
+
if (typeof obj === 'object' &&
|
|
94
|
+
obj !== null &&
|
|
95
|
+
obj['type'] === 'item.completed') {
|
|
96
|
+
const item = obj['item'];
|
|
97
|
+
if (typeof item === 'object' &&
|
|
98
|
+
item !== null &&
|
|
99
|
+
item['type'] === 'agent_message') {
|
|
100
|
+
const text = item['text'];
|
|
101
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
102
|
+
ctx.onLog('stdout', text + '\n');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Not JSON — forward as-is (shouldn't happen with --json flag)
|
|
109
|
+
if (line.length > 0)
|
|
110
|
+
ctx.onLog('stdout', line + '\n');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const result = await runChildProcess({
|
|
114
|
+
command: resolvedCommand,
|
|
115
|
+
args,
|
|
116
|
+
cwd: ctx.threadContext.worktreePath,
|
|
117
|
+
env,
|
|
118
|
+
signal: ctx.signal,
|
|
119
|
+
onLog: (stream, chunk) => {
|
|
120
|
+
if (stream === 'stderr') {
|
|
121
|
+
const filtered = filterCodexNoise(chunk);
|
|
122
|
+
if (filtered.length > 0)
|
|
123
|
+
ctx.onLog('stderr', filtered);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// stdout: buffer and parse JSONL line-by-line
|
|
127
|
+
stdoutLineBuffer += chunk;
|
|
128
|
+
const lines = stdoutLineBuffer.split('\n');
|
|
129
|
+
stdoutLineBuffer = lines.pop() ?? '';
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const trimmed = line.trim();
|
|
132
|
+
if (trimmed.length > 0)
|
|
133
|
+
forwardJsonLine(trimmed);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
onSpawn: ctx.onSpawn,
|
|
137
|
+
});
|
|
138
|
+
// Flush any remaining buffered line
|
|
139
|
+
if (stdoutLineBuffer.trim().length > 0) {
|
|
140
|
+
forwardJsonLine(stdoutLineBuffer.trim());
|
|
141
|
+
}
|
|
142
|
+
const parsed = parseCodexStreamJson(result.stdout);
|
|
143
|
+
const warnings = [];
|
|
144
|
+
const filteredStderr = filterCodexNoise(result.stderr ?? '');
|
|
145
|
+
if (filteredStderr.length > 0) {
|
|
146
|
+
warnings.push(filteredStderr);
|
|
147
|
+
}
|
|
148
|
+
let errorMessage = null;
|
|
149
|
+
if (result.exitCode !== 0 && !result.timedOut) {
|
|
150
|
+
const stderrMsg = filteredStderr;
|
|
151
|
+
errorMessage =
|
|
152
|
+
stderrMsg.length > 0
|
|
153
|
+
? `codex exited with code ${result.exitCode}: ${stderrMsg}`
|
|
154
|
+
: `codex exited with code ${result.exitCode}`;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
exitCode: result.exitCode,
|
|
158
|
+
signal: result.signal,
|
|
159
|
+
timedOut: result.timedOut,
|
|
160
|
+
summary: parsed.summary,
|
|
161
|
+
session: parsed.sessionId ? { sessionId: parsed.sessionId, params: {} } : null,
|
|
162
|
+
patches: [],
|
|
163
|
+
warnings,
|
|
164
|
+
errorMessage,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function createCodexAdapter() {
|
|
169
|
+
return new CodexAdapter();
|
|
170
|
+
}
|
|
171
|
+
export { parseCodexStreamJson } from './parse.js';
|
|
2
172
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAkBD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CA2CtE"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function tryParseJson(line) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = JSON.parse(line);
|
|
4
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
5
|
+
return parsed;
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function asString(value, fallback) {
|
|
14
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
15
|
+
}
|
|
16
|
+
export function parseCodexStreamJson(stdout) {
|
|
17
|
+
if (stdout.length === 0) {
|
|
18
|
+
return { sessionId: null, summary: '' };
|
|
19
|
+
}
|
|
20
|
+
let sessionId = null;
|
|
21
|
+
const agentTexts = [];
|
|
22
|
+
const lines = stdout.split('\n');
|
|
23
|
+
for (const rawLine of lines) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (line.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
const obj = tryParseJson(line);
|
|
28
|
+
if (obj === null)
|
|
29
|
+
continue;
|
|
30
|
+
const type = obj['type'];
|
|
31
|
+
if (type === 'thread.started') {
|
|
32
|
+
// {"type":"thread.started","thread_id":"<uuid>"}
|
|
33
|
+
const tid = asString(obj['thread_id'], '');
|
|
34
|
+
if (tid !== '')
|
|
35
|
+
sessionId = tid;
|
|
36
|
+
}
|
|
37
|
+
else if (type === 'item.completed') {
|
|
38
|
+
// {"type":"item.completed","item":{"id":"...","type":"agent_message","text":"..."}}
|
|
39
|
+
const item = obj['item'];
|
|
40
|
+
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
|
41
|
+
const it = item;
|
|
42
|
+
if (it['type'] === 'agent_message') {
|
|
43
|
+
const text = asString(it['text'], '');
|
|
44
|
+
if (text !== '')
|
|
45
|
+
agentTexts.push(text);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const summary = agentTexts.join('\n\n').trim();
|
|
51
|
+
if (summary === '' && sessionId === null) {
|
|
52
|
+
return { sessionId: null, summary: stdout };
|
|
53
|
+
}
|
|
54
|
+
return { sessionId, summary };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAKA,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5E,OAAO,MAAiC,CAAC;QAC3C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,QAAgB;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC1C,CAAC;IAED,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEhC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,GAAG,KAAK,IAAI;YAAE,SAAS;QAE3B,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAEzB,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YAC9B,iDAAiD;YACjD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3C,IAAI,GAAG,KAAK,EAAE;gBAAE,SAAS,GAAG,GAAG,CAAC;QAClC,CAAC;aAAM,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YACrC,oFAAoF;YACpF,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;YACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtE,MAAM,EAAE,GAAG,IAA+B,CAAC;gBAC3C,IAAI,EAAE,CAAC,MAAM,CAAC,KAAK,eAAe,EAAE,CAAC;oBACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,IAAI,KAAK,EAAE;wBAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAE/C,IAAI,OAAO,KAAK,EAAE,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACzC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC9C,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAChC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixy/codex-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@fixy/adapter-utils": "0.0.
|
|
14
|
+
"@fixy/adapter-utils": "0.0.4",
|
|
15
|
+
"@fixy/core": "0.0.4"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.6.0"
|
|
15
19
|
},
|
|
16
20
|
"license": "MIT",
|
|
17
21
|
"scripts": {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createCodexAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import type { FixyExecutionContext, FixyInvocationMeta } from '@fixy/core';
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let originalPath: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-codex-test-'));
|
|
13
|
+
originalPath = process.env.PATH ?? '';
|
|
14
|
+
|
|
15
|
+
const mockCodex = path.join(tmpDir, 'codex');
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
mockCodex,
|
|
18
|
+
`#!/bin/bash
|
|
19
|
+
if [[ "$1" == "--version" ]]; then
|
|
20
|
+
echo "codex-cli 0.112.0"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ "$1" == "exec" ]]; then
|
|
25
|
+
# Check for resume subcommand
|
|
26
|
+
THREAD_ID="mock-thread-001"
|
|
27
|
+
if [[ "$2" == "resume" ]]; then
|
|
28
|
+
THREAD_ID="$3"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Emit noise to stderr (should be filtered)
|
|
32
|
+
echo "2026-04-12T11:46:37.355150Z ERROR codex_core::skills::loader: failed to stat skills entry" >&2
|
|
33
|
+
|
|
34
|
+
echo '{"type":"thread.started","thread_id":"'"\$THREAD_ID"'"}'
|
|
35
|
+
echo '{"type":"turn.started"}'
|
|
36
|
+
echo '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Mock codex response."}}'
|
|
37
|
+
echo '{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":10}}'
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "Unknown command" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
`,
|
|
44
|
+
{ mode: 0o755 },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
process.env.PATH = originalPath;
|
|
52
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeCtx(overrides: Partial<FixyExecutionContext> = {}): FixyExecutionContext {
|
|
56
|
+
return {
|
|
57
|
+
runId: 'test-run-001',
|
|
58
|
+
agent: { id: 'codex', name: 'Codex CLI' },
|
|
59
|
+
threadContext: {
|
|
60
|
+
threadId: 'thread-001',
|
|
61
|
+
projectRoot: tmpDir,
|
|
62
|
+
worktreePath: tmpDir,
|
|
63
|
+
repoRef: null,
|
|
64
|
+
},
|
|
65
|
+
messages: [],
|
|
66
|
+
prompt: 'Hello Codex',
|
|
67
|
+
session: null,
|
|
68
|
+
onLog: () => {},
|
|
69
|
+
onMeta: () => {},
|
|
70
|
+
onSpawn: () => {},
|
|
71
|
+
signal: AbortSignal.timeout(30_000),
|
|
72
|
+
...overrides,
|
|
73
|
+
} as unknown as FixyExecutionContext;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('CodexAdapter.probe()', () => {
|
|
77
|
+
it('finds the mock codex binary and reports available=true', async () => {
|
|
78
|
+
const adapter = createCodexAdapter();
|
|
79
|
+
const result = await adapter.probe();
|
|
80
|
+
|
|
81
|
+
expect(result.available).toBe(true);
|
|
82
|
+
expect(result.version).toContain('0.112.0');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reports available=false when codex is not on PATH', async () => {
|
|
86
|
+
const adapter = createCodexAdapter();
|
|
87
|
+
const savedPath = process.env.PATH;
|
|
88
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await adapter.probe();
|
|
92
|
+
expect(result.available).toBe(false);
|
|
93
|
+
} finally {
|
|
94
|
+
process.env.PATH = savedPath;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('CodexAdapter.execute()', () => {
|
|
100
|
+
it('executes with mock codex and returns expected result', async () => {
|
|
101
|
+
const adapter = createCodexAdapter();
|
|
102
|
+
let metaCalled = false;
|
|
103
|
+
let spawnPid: number | null = null;
|
|
104
|
+
|
|
105
|
+
const ctx = makeCtx({
|
|
106
|
+
onMeta: (_meta: FixyInvocationMeta) => {
|
|
107
|
+
metaCalled = true;
|
|
108
|
+
},
|
|
109
|
+
onSpawn: (pid: number) => {
|
|
110
|
+
spawnPid = pid;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await adapter.execute(ctx);
|
|
115
|
+
|
|
116
|
+
expect(result.summary).toBe('Mock codex response.');
|
|
117
|
+
expect(result.session).not.toBeNull();
|
|
118
|
+
expect(result.session?.sessionId).toBe('mock-thread-001');
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
expect(result.timedOut).toBe(false);
|
|
121
|
+
expect(result.patches).toEqual([]);
|
|
122
|
+
expect(metaCalled).toBe(true);
|
|
123
|
+
expect(typeof spawnPid).toBe('number');
|
|
124
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('resumes a session by passing the thread id to codex exec resume', async () => {
|
|
128
|
+
const adapter = createCodexAdapter();
|
|
129
|
+
|
|
130
|
+
const ctx = makeCtx({
|
|
131
|
+
session: { sessionId: 'resume-thread-42', params: {} },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await adapter.execute(ctx);
|
|
135
|
+
|
|
136
|
+
expect(result.session).not.toBeNull();
|
|
137
|
+
expect(result.session?.sessionId).toBe('resume-thread-42');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('filters codex startup noise from stderr', async () => {
|
|
141
|
+
const adapter = createCodexAdapter();
|
|
142
|
+
const stderrChunks: string[] = [];
|
|
143
|
+
|
|
144
|
+
const ctx = makeCtx({
|
|
145
|
+
onLog: (stream, chunk) => {
|
|
146
|
+
if (stream === 'stderr') stderrChunks.push(chunk);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await adapter.execute(ctx);
|
|
151
|
+
|
|
152
|
+
// The mock writes an ERROR codex_core line to stderr — it should be filtered
|
|
153
|
+
expect(stderrChunks.join('')).not.toContain('codex_core::skills::loader');
|
|
154
|
+
expect(result.warnings.length).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('forwards only agent_message text to stdout onLog, not raw JSON', async () => {
|
|
158
|
+
const adapter = createCodexAdapter();
|
|
159
|
+
const stdoutChunks: string[] = [];
|
|
160
|
+
|
|
161
|
+
const ctx = makeCtx({
|
|
162
|
+
onLog: (stream, chunk) => {
|
|
163
|
+
if (stream === 'stdout') stdoutChunks.push(chunk);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await adapter.execute(ctx);
|
|
168
|
+
|
|
169
|
+
const output = stdoutChunks.join('');
|
|
170
|
+
expect(output).toContain('Mock codex response.');
|
|
171
|
+
expect(output).not.toContain('thread.started');
|
|
172
|
+
expect(output).not.toContain('turn.started');
|
|
173
|
+
expect(output).not.toContain('turn.completed');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('inherits CODEX_HOME from process.env', async () => {
|
|
177
|
+
const adapter = createCodexAdapter();
|
|
178
|
+
let metaEnv: Record<string, string> = {};
|
|
179
|
+
|
|
180
|
+
process.env['CODEX_HOME'] = '/custom/codex/home';
|
|
181
|
+
|
|
182
|
+
const ctx = makeCtx({
|
|
183
|
+
onMeta: (meta: FixyInvocationMeta) => {
|
|
184
|
+
metaEnv = meta.env;
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await adapter.execute(ctx);
|
|
190
|
+
expect(metaEnv['CODEX_HOME']).toBe('/custom/codex/home');
|
|
191
|
+
} finally {
|
|
192
|
+
delete process.env['CODEX_HOME'];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexStreamJson } from '../parse.js';
|
|
3
|
+
|
|
4
|
+
describe('parseCodexStreamJson', () => {
|
|
5
|
+
it('extracts sessionId from thread.started and text from item.completed', () => {
|
|
6
|
+
const input = [
|
|
7
|
+
JSON.stringify({ type: 'thread.started', thread_id: '019d8183-e8b0-7e33-a2f8-29d950432756' }),
|
|
8
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
9
|
+
JSON.stringify({
|
|
10
|
+
type: 'item.completed',
|
|
11
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Hello.' },
|
|
12
|
+
}),
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
type: 'turn.completed',
|
|
15
|
+
usage: { input_tokens: 100, cached_input_tokens: 50, output_tokens: 10 },
|
|
16
|
+
}),
|
|
17
|
+
].join('\n');
|
|
18
|
+
|
|
19
|
+
const result = parseCodexStreamJson(input);
|
|
20
|
+
|
|
21
|
+
expect(result.sessionId).toBe('019d8183-e8b0-7e33-a2f8-29d950432756');
|
|
22
|
+
expect(result.summary).toBe('Hello.');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('concatenates multiple agent_message items', () => {
|
|
26
|
+
const input = [
|
|
27
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-multi' }),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: 'item.completed',
|
|
30
|
+
item: { id: 'item_0', type: 'agent_message', text: 'First part.' },
|
|
31
|
+
}),
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
type: 'item.completed',
|
|
34
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Second part.' },
|
|
35
|
+
}),
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
const result = parseCodexStreamJson(input);
|
|
39
|
+
|
|
40
|
+
expect(result.sessionId).toBe('thread-multi');
|
|
41
|
+
expect(result.summary).toContain('First part.');
|
|
42
|
+
expect(result.summary).toContain('Second part.');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('ignores non-agent_message items', () => {
|
|
46
|
+
const input = [
|
|
47
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-filter' }),
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
type: 'item.completed',
|
|
50
|
+
item: { id: 'item_0', type: 'tool_call', text: 'should be ignored' },
|
|
51
|
+
}),
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
type: 'item.completed',
|
|
54
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Visible text.' },
|
|
55
|
+
}),
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const result = parseCodexStreamJson(input);
|
|
59
|
+
|
|
60
|
+
expect(result.summary).toBe('Visible text.');
|
|
61
|
+
expect(result.summary).not.toContain('should be ignored');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to raw stdout for non-JSON output', () => {
|
|
65
|
+
const input = 'This is plain text output\nNot JSON\n';
|
|
66
|
+
|
|
67
|
+
const result = parseCodexStreamJson(input);
|
|
68
|
+
|
|
69
|
+
expect(result.sessionId).toBeNull();
|
|
70
|
+
expect(result.summary).toBe(input);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null sessionId and empty summary for empty string', () => {
|
|
74
|
+
const result = parseCodexStreamJson('');
|
|
75
|
+
|
|
76
|
+
expect(result.sessionId).toBeNull();
|
|
77
|
+
expect(result.summary).toBe('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles missing thread_id in thread.started gracefully', () => {
|
|
81
|
+
const input = [
|
|
82
|
+
JSON.stringify({ type: 'thread.started' }),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
type: 'item.completed',
|
|
85
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Response without session.' },
|
|
86
|
+
}),
|
|
87
|
+
].join('\n');
|
|
88
|
+
|
|
89
|
+
const result = parseCodexStreamJson(input);
|
|
90
|
+
|
|
91
|
+
expect(result.sessionId).toBeNull();
|
|
92
|
+
expect(result.summary).toBe('Response without session.');
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1 +1,201 @@
|
|
|
1
|
-
|
|
1
|
+
// packages/codex-adapter/src/index.ts
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FixyAdapter,
|
|
5
|
+
FixyProbeResult,
|
|
6
|
+
FixyExecutionContext,
|
|
7
|
+
FixyExecutionResult,
|
|
8
|
+
} from '@fixy/core';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
buildInheritedEnv,
|
|
12
|
+
redactEnvForLogs,
|
|
13
|
+
resolveCommand,
|
|
14
|
+
runChildProcess,
|
|
15
|
+
} from '@fixy/adapter-utils';
|
|
16
|
+
|
|
17
|
+
import { parseCodexStreamJson } from './parse.js';
|
|
18
|
+
|
|
19
|
+
// Codex CLI writes skill-loader errors to stderr on startup — suppress them.
|
|
20
|
+
const CODEX_NOISE_RE = /^[0-9TZ:.+-]+ (ERROR|WARN) codex_/;
|
|
21
|
+
|
|
22
|
+
function filterCodexNoise(stderr: string): string {
|
|
23
|
+
return stderr
|
|
24
|
+
.split('\n')
|
|
25
|
+
.filter((line) => !CODEX_NOISE_RE.test(line))
|
|
26
|
+
.join('\n')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CodexAdapter implements FixyAdapter {
|
|
31
|
+
readonly id = 'codex';
|
|
32
|
+
readonly name = 'Codex CLI';
|
|
33
|
+
|
|
34
|
+
async probe(): Promise<FixyProbeResult> {
|
|
35
|
+
let resolvedCommand: string;
|
|
36
|
+
try {
|
|
37
|
+
resolvedCommand = await resolveCommand('codex');
|
|
38
|
+
} catch {
|
|
39
|
+
return {
|
|
40
|
+
available: false,
|
|
41
|
+
version: null,
|
|
42
|
+
authStatus: 'unknown',
|
|
43
|
+
detail: 'codex CLI not found in PATH',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await runChildProcess({
|
|
49
|
+
command: resolvedCommand,
|
|
50
|
+
args: ['--version'],
|
|
51
|
+
cwd: process.cwd(),
|
|
52
|
+
env: buildInheritedEnv(),
|
|
53
|
+
timeoutMs: 10_000,
|
|
54
|
+
});
|
|
55
|
+
const version = result.stdout.trim() || null;
|
|
56
|
+
return {
|
|
57
|
+
available: true,
|
|
58
|
+
version,
|
|
59
|
+
authStatus: 'unknown',
|
|
60
|
+
detail: null,
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return {
|
|
65
|
+
available: false,
|
|
66
|
+
version: null,
|
|
67
|
+
authStatus: 'unknown',
|
|
68
|
+
detail,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async execute(ctx: FixyExecutionContext): Promise<FixyExecutionResult> {
|
|
74
|
+
const resolvedCommand = await resolveCommand('codex');
|
|
75
|
+
const env = buildInheritedEnv({});
|
|
76
|
+
|
|
77
|
+
let args: string[];
|
|
78
|
+
|
|
79
|
+
if (ctx.session) {
|
|
80
|
+
// Resume existing session: codex exec resume <thread_id> --json ...
|
|
81
|
+
args = [
|
|
82
|
+
'exec',
|
|
83
|
+
'resume',
|
|
84
|
+
ctx.session.sessionId,
|
|
85
|
+
'--json',
|
|
86
|
+
'--skip-git-repo-check',
|
|
87
|
+
'--full-auto',
|
|
88
|
+
ctx.prompt,
|
|
89
|
+
];
|
|
90
|
+
} else {
|
|
91
|
+
// New session: codex exec --json --skip-git-repo-check --full-auto <prompt>
|
|
92
|
+
args = [
|
|
93
|
+
'exec',
|
|
94
|
+
'--json',
|
|
95
|
+
'--skip-git-repo-check',
|
|
96
|
+
'--full-auto',
|
|
97
|
+
ctx.prompt,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ctx.onMeta({
|
|
102
|
+
resolvedCommand,
|
|
103
|
+
args,
|
|
104
|
+
cwd: ctx.threadContext.worktreePath,
|
|
105
|
+
env: redactEnvForLogs(env),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Buffer partial stdout lines so we can parse JSONL events and forward
|
|
109
|
+
// only the agent text instead of raw JSON to the terminal.
|
|
110
|
+
let stdoutLineBuffer = '';
|
|
111
|
+
|
|
112
|
+
const forwardJsonLine = (line: string): void => {
|
|
113
|
+
try {
|
|
114
|
+
const obj = JSON.parse(line);
|
|
115
|
+
if (
|
|
116
|
+
typeof obj === 'object' &&
|
|
117
|
+
obj !== null &&
|
|
118
|
+
obj['type'] === 'item.completed'
|
|
119
|
+
) {
|
|
120
|
+
const item = obj['item'];
|
|
121
|
+
if (
|
|
122
|
+
typeof item === 'object' &&
|
|
123
|
+
item !== null &&
|
|
124
|
+
item['type'] === 'agent_message'
|
|
125
|
+
) {
|
|
126
|
+
const text = item['text'];
|
|
127
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
128
|
+
ctx.onLog('stdout', text + '\n');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Not JSON — forward as-is (shouldn't happen with --json flag)
|
|
134
|
+
if (line.length > 0) ctx.onLog('stdout', line + '\n');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = await runChildProcess({
|
|
139
|
+
command: resolvedCommand,
|
|
140
|
+
args,
|
|
141
|
+
cwd: ctx.threadContext.worktreePath,
|
|
142
|
+
env,
|
|
143
|
+
signal: ctx.signal,
|
|
144
|
+
onLog: (stream, chunk) => {
|
|
145
|
+
if (stream === 'stderr') {
|
|
146
|
+
const filtered = filterCodexNoise(chunk);
|
|
147
|
+
if (filtered.length > 0) ctx.onLog('stderr', filtered);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// stdout: buffer and parse JSONL line-by-line
|
|
151
|
+
stdoutLineBuffer += chunk;
|
|
152
|
+
const lines = stdoutLineBuffer.split('\n');
|
|
153
|
+
stdoutLineBuffer = lines.pop() ?? '';
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (trimmed.length > 0) forwardJsonLine(trimmed);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
onSpawn: ctx.onSpawn,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Flush any remaining buffered line
|
|
163
|
+
if (stdoutLineBuffer.trim().length > 0) {
|
|
164
|
+
forwardJsonLine(stdoutLineBuffer.trim());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parsed = parseCodexStreamJson(result.stdout);
|
|
168
|
+
|
|
169
|
+
const warnings: string[] = [];
|
|
170
|
+
const filteredStderr = filterCodexNoise(result.stderr ?? '');
|
|
171
|
+
if (filteredStderr.length > 0) {
|
|
172
|
+
warnings.push(filteredStderr);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let errorMessage: string | null = null;
|
|
176
|
+
if (result.exitCode !== 0 && !result.timedOut) {
|
|
177
|
+
const stderrMsg = filteredStderr;
|
|
178
|
+
errorMessage =
|
|
179
|
+
stderrMsg.length > 0
|
|
180
|
+
? `codex exited with code ${result.exitCode}: ${stderrMsg}`
|
|
181
|
+
: `codex exited with code ${result.exitCode}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
exitCode: result.exitCode,
|
|
186
|
+
signal: result.signal,
|
|
187
|
+
timedOut: result.timedOut,
|
|
188
|
+
summary: parsed.summary,
|
|
189
|
+
session: parsed.sessionId ? { sessionId: parsed.sessionId, params: {} } : null,
|
|
190
|
+
patches: [],
|
|
191
|
+
warnings,
|
|
192
|
+
errorMessage,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function createCodexAdapter(): FixyAdapter {
|
|
198
|
+
return new CodexAdapter();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { parseCodexStreamJson } from './parse.js';
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface CodexStreamResult {
|
|
2
|
+
sessionId: string | null;
|
|
3
|
+
summary: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function tryParseJson(line: string): Record<string, unknown> | null {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(line);
|
|
9
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
10
|
+
return parsed as Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asString(value: unknown, fallback: string): string {
|
|
19
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseCodexStreamJson(stdout: string): CodexStreamResult {
|
|
23
|
+
if (stdout.length === 0) {
|
|
24
|
+
return { sessionId: null, summary: '' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let sessionId: string | null = null;
|
|
28
|
+
const agentTexts: string[] = [];
|
|
29
|
+
|
|
30
|
+
const lines = stdout.split('\n');
|
|
31
|
+
|
|
32
|
+
for (const rawLine of lines) {
|
|
33
|
+
const line = rawLine.trim();
|
|
34
|
+
if (line.length === 0) continue;
|
|
35
|
+
|
|
36
|
+
const obj = tryParseJson(line);
|
|
37
|
+
if (obj === null) continue;
|
|
38
|
+
|
|
39
|
+
const type = obj['type'];
|
|
40
|
+
|
|
41
|
+
if (type === 'thread.started') {
|
|
42
|
+
// {"type":"thread.started","thread_id":"<uuid>"}
|
|
43
|
+
const tid = asString(obj['thread_id'], '');
|
|
44
|
+
if (tid !== '') sessionId = tid;
|
|
45
|
+
} else if (type === 'item.completed') {
|
|
46
|
+
// {"type":"item.completed","item":{"id":"...","type":"agent_message","text":"..."}}
|
|
47
|
+
const item = obj['item'];
|
|
48
|
+
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
|
49
|
+
const it = item as Record<string, unknown>;
|
|
50
|
+
if (it['type'] === 'agent_message') {
|
|
51
|
+
const text = asString(it['text'], '');
|
|
52
|
+
if (text !== '') agentTexts.push(text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const summary = agentTexts.join('\n\n').trim();
|
|
59
|
+
|
|
60
|
+
if (summary === '' && sessionId === null) {
|
|
61
|
+
return { sessionId: null, summary: stdout };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { sessionId, summary };
|
|
65
|
+
}
|