@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.
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.test.d.ts.map
@@ -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":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parse.test.d.ts.map
@@ -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
@@ -1,2 +1,4 @@
1
- export {};
1
+ import type { FixyAdapter } from '@fixy/core';
2
+ export declare function createCodexAdapter(): FixyAdapter;
3
+ export { parseCodexStreamJson } from './parse.js';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
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
- export {};
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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"}
@@ -0,0 +1,6 @@
1
+ export interface CodexStreamResult {
2
+ sessionId: string | null;
3
+ summary: string;
4
+ }
5
+ export declare function parseCodexStreamJson(stdout: string): CodexStreamResult;
6
+ //# sourceMappingURL=parse.d.ts.map
@@ -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.2",
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.2"
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
- export {};
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
+ }
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "dist",
5
- "rootDir": "src"
5
+ "rootDir": "src",
6
+ "types": ["node"]
6
7
  },
7
8
  "include": ["src"]
8
9
  }