@northflare/runner 0.0.1
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/DEBUG_LOGGING.md +60 -0
- package/LICENSE +21 -0
- package/MIGRATION_PLAN.md +52 -0
- package/README.md +220 -0
- package/SDK_IMPLEMENTATION_GUIDE.md +1036 -0
- package/bin/northflare-runner +367 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/lib/index.html +116 -0
- package/coverage/lib/preload-script.js.html +964 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/src/collections/index.html +116 -0
- package/coverage/src/collections/runner-messages.ts.html +312 -0
- package/coverage/src/components/claude-manager.ts.html +1290 -0
- package/coverage/src/components/index.html +146 -0
- package/coverage/src/components/message-handler.ts.html +730 -0
- package/coverage/src/components/repository-manager.ts.html +841 -0
- package/coverage/src/index.html +131 -0
- package/coverage/src/index.ts.html +448 -0
- package/coverage/src/runner.ts.html +1239 -0
- package/coverage/src/utils/config.ts.html +780 -0
- package/coverage/src/utils/console.ts.html +121 -0
- package/coverage/src/utils/index.html +161 -0
- package/coverage/src/utils/logger.ts.html +475 -0
- package/coverage/src/utils/status-line.ts.html +445 -0
- package/dist/collections/runner-messages.d.ts +52 -0
- package/dist/collections/runner-messages.d.ts.map +1 -0
- package/dist/collections/runner-messages.js +161 -0
- package/dist/collections/runner-messages.js.map +1 -0
- package/dist/components/claude-manager.d.ts +39 -0
- package/dist/components/claude-manager.d.ts.map +1 -0
- package/dist/components/claude-manager.js +783 -0
- package/dist/components/claude-manager.js.map +1 -0
- package/dist/components/claude-sdk-manager.d.ts +47 -0
- package/dist/components/claude-sdk-manager.d.ts.map +1 -0
- package/dist/components/claude-sdk-manager.js +1088 -0
- package/dist/components/claude-sdk-manager.js.map +1 -0
- package/dist/components/enhanced-repository-manager.d.ts +134 -0
- package/dist/components/enhanced-repository-manager.d.ts.map +1 -0
- package/dist/components/enhanced-repository-manager.js +602 -0
- package/dist/components/enhanced-repository-manager.js.map +1 -0
- package/dist/components/message-handler-sse.d.ts +46 -0
- package/dist/components/message-handler-sse.d.ts.map +1 -0
- package/dist/components/message-handler-sse.js +734 -0
- package/dist/components/message-handler-sse.js.map +1 -0
- package/dist/components/message-handler.d.ts +35 -0
- package/dist/components/message-handler.d.ts.map +1 -0
- package/dist/components/message-handler.js +689 -0
- package/dist/components/message-handler.js.map +1 -0
- package/dist/components/repository-manager.d.ts +51 -0
- package/dist/components/repository-manager.d.ts.map +1 -0
- package/dist/components/repository-manager.js +295 -0
- package/dist/components/repository-manager.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/runner-sse.d.ts +57 -0
- package/dist/runner-sse.d.ts.map +1 -0
- package/dist/runner-sse.js +698 -0
- package/dist/runner-sse.js.map +1 -0
- package/dist/runner.d.ts +51 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +530 -0
- package/dist/runner.js.map +1 -0
- package/dist/services/RunnerAPIClient.d.ts +30 -0
- package/dist/services/RunnerAPIClient.d.ts.map +1 -0
- package/dist/services/RunnerAPIClient.js +112 -0
- package/dist/services/RunnerAPIClient.js.map +1 -0
- package/dist/services/SSEClient.d.ts +60 -0
- package/dist/services/SSEClient.d.ts.map +1 -0
- package/dist/services/SSEClient.js +204 -0
- package/dist/services/SSEClient.js.map +1 -0
- package/dist/types/claude.d.ts +45 -0
- package/dist/types/claude.d.ts.map +1 -0
- package/dist/types/claude.js +6 -0
- package/dist/types/claude.js.map +1 -0
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +31 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +6 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/runner-interface.d.ts +24 -0
- package/dist/types/runner-interface.d.ts.map +1 -0
- package/dist/types/runner-interface.js +6 -0
- package/dist/types/runner-interface.js.map +1 -0
- package/dist/utils/StateManager.d.ts +52 -0
- package/dist/utils/StateManager.d.ts.map +1 -0
- package/dist/utils/StateManager.js +162 -0
- package/dist/utils/StateManager.js.map +1 -0
- package/dist/utils/config.d.ts +41 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +250 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/console.d.ts +11 -0
- package/dist/utils/console.d.ts.map +1 -0
- package/dist/utils/console.js +15 -0
- package/dist/utils/console.js.map +1 -0
- package/dist/utils/expand-env.d.ts +2 -0
- package/dist/utils/expand-env.d.ts.map +1 -0
- package/dist/utils/expand-env.js +20 -0
- package/dist/utils/expand-env.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +108 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/status-line.d.ts +37 -0
- package/dist/utils/status-line.d.ts.map +1 -0
- package/dist/utils/status-line.js +113 -0
- package/dist/utils/status-line.js.map +1 -0
- package/docs/claude-manager.md +91 -0
- package/exceptions.log +22 -0
- package/lib/preload-script.js +293 -0
- package/package.json +55 -0
- package/rejections.log +63 -0
- package/runner.log +488 -0
- package/src/components/claude-sdk-manager.ts +1354 -0
- package/src/components/enhanced-repository-manager.ts +823 -0
- package/src/components/message-handler-sse.ts +1011 -0
- package/src/components/repository-manager.ts +337 -0
- package/src/index.ts +166 -0
- package/src/runner-sse.ts +847 -0
- package/src/services/RunnerAPIClient.ts +135 -0
- package/src/services/SSEClient.ts +258 -0
- package/src/types/claude.ts +55 -0
- package/src/types/computer-name.d.ts +4 -0
- package/src/types/index.ts +63 -0
- package/src/types/messages.ts +39 -0
- package/src/types/runner-interface.ts +34 -0
- package/src/utils/StateManager.ts +187 -0
- package/src/utils/codex-sdk.js +448 -0
- package/src/utils/config.ts +315 -0
- package/src/utils/console.ts +13 -0
- package/src/utils/expand-env.ts +22 -0
- package/src/utils/logger.ts +131 -0
- package/src/utils/sdk-demo.js +34 -0
- package/src/utils/status-line.ts +121 -0
- package/test-debug.sh +26 -0
- package/tests/retry-strategies.test.ts +410 -0
- package/tests/sdk-integration.test.ts +329 -0
- package/tests/sdk-streaming.test.ts +1180 -0
- package/tests/setup.ts +5 -0
- package/tests/test-claude-manager.ts +120 -0
- package/tsconfig.json +36 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Manager for persisting runner state
|
|
3
|
+
*
|
|
4
|
+
* Manages persistent state including lastProcessedAt timestamp
|
|
5
|
+
* to enable proper catch-up on restart.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { createLogger } from './logger';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger('StateManager');
|
|
13
|
+
|
|
14
|
+
export interface RunnerState {
|
|
15
|
+
runnerId: string;
|
|
16
|
+
runnerUid: string | null;
|
|
17
|
+
lastProcessedAt: string | null; // ISO string
|
|
18
|
+
isActiveRunner: boolean;
|
|
19
|
+
updatedAt: string; // ISO string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class StateManager {
|
|
23
|
+
private statePath: string;
|
|
24
|
+
private state: RunnerState | null = null;
|
|
25
|
+
private saveTimer: NodeJS.Timeout | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(dataDir: string, runnerId: string) {
|
|
28
|
+
// Namespace state file by runnerId (stable external ID) to separate different users
|
|
29
|
+
const stateFile = `runner-state-${runnerId}.json`;
|
|
30
|
+
this.statePath = path.join(dataDir, stateFile);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load state from disk
|
|
35
|
+
*/
|
|
36
|
+
async loadState(): Promise<RunnerState | null> {
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(this.statePath, 'utf-8');
|
|
39
|
+
this.state = JSON.parse(content);
|
|
40
|
+
logger.info('Loaded runner state', {
|
|
41
|
+
runnerId: this.state?.runnerId,
|
|
42
|
+
runnerUid: this.state?.runnerUid,
|
|
43
|
+
lastProcessedAt: this.state?.lastProcessedAt,
|
|
44
|
+
isActiveRunner: this.state?.isActiveRunner,
|
|
45
|
+
});
|
|
46
|
+
return this.state;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if ((error as any).code === 'ENOENT') {
|
|
49
|
+
logger.debug('No state file found, starting fresh');
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
logger.error('Failed to load state file:', error);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save state to disk
|
|
59
|
+
*/
|
|
60
|
+
async saveState(state: RunnerState): Promise<void> {
|
|
61
|
+
this.state = state;
|
|
62
|
+
|
|
63
|
+
// Cancel any pending save
|
|
64
|
+
if (this.saveTimer) {
|
|
65
|
+
clearTimeout(this.saveTimer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Debounce saves to avoid excessive disk writes
|
|
69
|
+
this.saveTimer = setTimeout(async () => {
|
|
70
|
+
try {
|
|
71
|
+
// Ensure directory exists
|
|
72
|
+
const dir = path.dirname(this.statePath);
|
|
73
|
+
await fs.mkdir(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
// Update timestamp
|
|
76
|
+
state.updatedAt = new Date().toISOString();
|
|
77
|
+
|
|
78
|
+
// Write atomically by writing to temp file first
|
|
79
|
+
const tempPath = `${this.statePath}.tmp`;
|
|
80
|
+
await fs.writeFile(tempPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
81
|
+
await fs.rename(tempPath, this.statePath);
|
|
82
|
+
|
|
83
|
+
logger.debug('Saved runner state', {
|
|
84
|
+
runnerId: state.runnerId,
|
|
85
|
+
lastProcessedAt: state.lastProcessedAt,
|
|
86
|
+
isActiveRunner: state.isActiveRunner,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error('Failed to save state file:', error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}, 1000); // 1 second debounce
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update lastProcessedAt timestamp
|
|
97
|
+
*/
|
|
98
|
+
async updateLastProcessedAt(timestamp: Date | string): Promise<void> {
|
|
99
|
+
if (!this.state) {
|
|
100
|
+
throw new Error('State not loaded');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isoString = timestamp instanceof Date ? timestamp.toISOString() : timestamp;
|
|
104
|
+
|
|
105
|
+
const updatedState: RunnerState = {
|
|
106
|
+
...this.state,
|
|
107
|
+
lastProcessedAt: isoString,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await this.saveState(updatedState);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Update runner registration details
|
|
115
|
+
*/
|
|
116
|
+
async updateRunnerRegistration(
|
|
117
|
+
runnerId: string,
|
|
118
|
+
runnerUid: string | null,
|
|
119
|
+
lastProcessedAt: Date | null
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const state: RunnerState = this.state || {
|
|
122
|
+
runnerId: '',
|
|
123
|
+
runnerUid: null,
|
|
124
|
+
lastProcessedAt: null,
|
|
125
|
+
isActiveRunner: false,
|
|
126
|
+
updatedAt: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const updatedState: RunnerState = {
|
|
130
|
+
...state,
|
|
131
|
+
runnerId,
|
|
132
|
+
runnerUid,
|
|
133
|
+
lastProcessedAt: lastProcessedAt ? lastProcessedAt.toISOString() : null,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await this.saveState(updatedState);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Update active runner status
|
|
141
|
+
*/
|
|
142
|
+
async updateActiveStatus(isActive: boolean): Promise<void> {
|
|
143
|
+
if (!this.state) {
|
|
144
|
+
throw new Error('State not loaded');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const updatedState: RunnerState = {
|
|
148
|
+
...this.state,
|
|
149
|
+
isActiveRunner: isActive,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
await this.saveState(updatedState);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get current state
|
|
157
|
+
*/
|
|
158
|
+
getState(): RunnerState | null {
|
|
159
|
+
return this.state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get lastProcessedAt as Date
|
|
164
|
+
*/
|
|
165
|
+
getLastProcessedAt(): Date | null {
|
|
166
|
+
if (!this.state || !this.state.lastProcessedAt) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return new Date(this.state.lastProcessedAt);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear state (for testing or reset)
|
|
174
|
+
*/
|
|
175
|
+
async clearState(): Promise<void> {
|
|
176
|
+
this.state = null;
|
|
177
|
+
try {
|
|
178
|
+
await fs.unlink(this.statePath);
|
|
179
|
+
logger.info('Cleared runner state');
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if ((error as any).code !== 'ENOENT') {
|
|
182
|
+
logger.error('Failed to clear state file:', error);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/* Example usage:
|
|
2
|
+
query({
|
|
3
|
+
prompt: "Hello, how are you?",
|
|
4
|
+
options: {
|
|
5
|
+
pathToCodexExecutable: "./codex",
|
|
6
|
+
cwd: "/Users/joe/Documents/myproject/",
|
|
7
|
+
resume: "0199a99f-28aa-7872-96f6-24ce1e3a05fc", // Optional, resume from a previous session
|
|
8
|
+
configOverrides: {
|
|
9
|
+
model: "gpt-5-codex",
|
|
10
|
+
model_reasoning_effort: "high",
|
|
11
|
+
sandbox_mode: "danger-full-access",
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
Output:
|
|
17
|
+
sandbox_mode: "danger-full-access",
|
|
18
|
+
}
|
|
19
|
+
}})
|
|
20
|
+
|
|
21
|
+
Output:
|
|
22
|
+
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
id: '',
|
|
26
|
+
msg: {
|
|
27
|
+
type: 'session_configured',
|
|
28
|
+
session_id: '0199a9ce-1f38-7332-b46b-9711279899f1',
|
|
29
|
+
model: 'gpt-5-codex',
|
|
30
|
+
reasoning_effort: 'high',
|
|
31
|
+
history_log_id: 1399031533,
|
|
32
|
+
history_entry_count: 475,
|
|
33
|
+
rollout_path: '/Users/toby/.codex/sessions/2025/10/03/rollout-2025-10-03T13-21-21-0199a9ce-1f38-7332-b46b-9711279899f1.jsonl'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'submission-1',
|
|
38
|
+
msg: { type: 'task_started', model_context_window: 272000 }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'submission-1',
|
|
42
|
+
msg: { type: 'token_count', info: null, rate_limits: [Object] }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'submission-1',
|
|
46
|
+
msg: { type: 'agent_reasoning_section_break' }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'submission-1',
|
|
50
|
+
msg: { type: 'agent_reasoning_delta', delta: '**Preparing' }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'submission-1',
|
|
54
|
+
msg: { type: 'agent_reasoning_delta', delta: ' a' }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'submission-1',
|
|
58
|
+
msg: { type: 'agent_reasoning_delta', delta: ' friendly' }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'submission-1',
|
|
62
|
+
msg: { type: 'agent_reasoning_delta', delta: ' response' }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'submission-1',
|
|
66
|
+
msg: { type: 'agent_reasoning_delta', delta: '**' }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'submission-1',
|
|
70
|
+
msg: {
|
|
71
|
+
type: 'agent_reasoning',
|
|
72
|
+
text: '**Preparing a friendly response**'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'submission-1',
|
|
77
|
+
msg: { type: 'agent_message_delta', delta: 'Hey' }
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'submission-1',
|
|
81
|
+
msg: { type: 'agent_message_delta', delta: ' there' }
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'submission-1',
|
|
85
|
+
msg: { type: 'agent_message_delta', delta: '!' }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'submission-1',
|
|
89
|
+
msg: { type: 'agent_message_delta', delta: ' How' }
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'submission-1',
|
|
93
|
+
msg: { type: 'agent_message_delta', delta: ' can' }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'submission-1',
|
|
97
|
+
msg: { type: 'agent_message_delta', delta: ' I' }
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'submission-1',
|
|
101
|
+
msg: { type: 'agent_message_delta', delta: ' help' }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'submission-1',
|
|
105
|
+
msg: { type: 'agent_message_delta', delta: ' today' }
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'submission-1',
|
|
109
|
+
msg: { type: 'agent_message_delta', delta: '?' }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'submission-1',
|
|
113
|
+
msg: {
|
|
114
|
+
type: 'agent_message',
|
|
115
|
+
message: 'Hey there! How can I help today?'
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'submission-1',
|
|
120
|
+
msg: { type: 'token_count', info: [Object], rate_limits: [Object] }
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'submission-1',
|
|
124
|
+
msg: {
|
|
125
|
+
type: 'task_complete',
|
|
126
|
+
last_agent_message: 'Hey there! How can I help today?'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
import { spawn } from "node:child_process";
|
|
133
|
+
import { createInterface } from "node:readline";
|
|
134
|
+
|
|
135
|
+
class ProcessTransport {
|
|
136
|
+
constructor(options = {}) {
|
|
137
|
+
const { command = "codex", args = [], cwd, env, onStderr } = options;
|
|
138
|
+
|
|
139
|
+
this.child = spawn(command, args, {
|
|
140
|
+
cwd,
|
|
141
|
+
env: { ...process.env, ...env },
|
|
142
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!this.child.stdin || !this.child.stdout) {
|
|
146
|
+
throw new Error("Codex CLI requires piped stdin/stdout.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.child.stderr) {
|
|
150
|
+
const stderrHandler = (data) => {
|
|
151
|
+
if (onStderr) {
|
|
152
|
+
onStderr(data.toString());
|
|
153
|
+
} else {
|
|
154
|
+
process.stderr.write(data);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
this.child.stderr.on("data", stderrHandler);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.stdin = this.child.stdin;
|
|
161
|
+
this.stdout = this.child.stdout;
|
|
162
|
+
this.closed = false;
|
|
163
|
+
|
|
164
|
+
this.exitPromise = new Promise((resolve, reject) => {
|
|
165
|
+
this.child.once("exit", (code, signal) => {
|
|
166
|
+
if (code === 0 || signal === "SIGTERM" || signal === null) {
|
|
167
|
+
resolve({ code, signal });
|
|
168
|
+
} else {
|
|
169
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
170
|
+
const error = new Error(
|
|
171
|
+
`codex process terminated with ${exitReason}`
|
|
172
|
+
);
|
|
173
|
+
error.code = code;
|
|
174
|
+
error.signal = signal;
|
|
175
|
+
reject(error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
this.child.once("error", (error) => {
|
|
179
|
+
reject(error);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async *readMessages() {
|
|
185
|
+
const rl = createInterface({ input: this.stdout });
|
|
186
|
+
try {
|
|
187
|
+
for await (const line of rl) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (!trimmed) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(trimmed);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const parseError = new Error(
|
|
197
|
+
`Failed to parse Codex JSON output: ${error.message}\n${trimmed}`
|
|
198
|
+
);
|
|
199
|
+
parseError.cause = error;
|
|
200
|
+
throw parseError;
|
|
201
|
+
}
|
|
202
|
+
yield parsed;
|
|
203
|
+
}
|
|
204
|
+
await this.exitPromise;
|
|
205
|
+
} finally {
|
|
206
|
+
rl.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async write(payload) {
|
|
211
|
+
if (this.closed) {
|
|
212
|
+
throw new Error("Cannot write to closed Codex transport.");
|
|
213
|
+
}
|
|
214
|
+
if (!this.stdin || this.stdin.destroyed) {
|
|
215
|
+
throw new Error("Codex stdin is not available.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const serialized =
|
|
219
|
+
typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
220
|
+
const message = serialized.endsWith("\n") ? serialized : `${serialized}\n`;
|
|
221
|
+
const stream = this.stdin;
|
|
222
|
+
|
|
223
|
+
await new Promise((resolve, reject) => {
|
|
224
|
+
let settled = false;
|
|
225
|
+
const finish = () => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
stream.off("error", handleError);
|
|
229
|
+
resolve();
|
|
230
|
+
};
|
|
231
|
+
const handleError = (error) => {
|
|
232
|
+
if (settled) return;
|
|
233
|
+
settled = true;
|
|
234
|
+
stream.off("drain", finish);
|
|
235
|
+
stream.off("error", handleError);
|
|
236
|
+
reject(error);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
stream.once("error", handleError);
|
|
240
|
+
const ok = stream.write(message, finish);
|
|
241
|
+
if (!ok) {
|
|
242
|
+
stream.once("drain", finish);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async close() {
|
|
248
|
+
if (this.closed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.closed = true;
|
|
252
|
+
|
|
253
|
+
if (this.stdin && !this.stdin.destroyed) {
|
|
254
|
+
this.stdin.end();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this.child && !this.child.killed) {
|
|
258
|
+
this.child.kill("SIGTERM");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await this.exitPromise;
|
|
263
|
+
} catch {
|
|
264
|
+
// Swallow shutdown errors when closing intentionally.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interruptProcess() {
|
|
269
|
+
if (this.child && !this.child.killed) {
|
|
270
|
+
this.child.kill("SIGINT");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
class Query {
|
|
276
|
+
constructor(transport) {
|
|
277
|
+
this.transport = transport;
|
|
278
|
+
this._nextSubmissionId = 0;
|
|
279
|
+
this._sessionId = null;
|
|
280
|
+
this._writeError = null;
|
|
281
|
+
this._finished = false;
|
|
282
|
+
this._messageIterator = this._iterateMessages();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_generateSubmissionId() {
|
|
286
|
+
this._nextSubmissionId += 1;
|
|
287
|
+
return `submission-${this._nextSubmissionId}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_registerWrite(promise) {
|
|
291
|
+
promise.catch((error) => {
|
|
292
|
+
if (!this._writeError) {
|
|
293
|
+
this._writeError = error;
|
|
294
|
+
void this.transport.close();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async *_iterateMessages() {
|
|
300
|
+
try {
|
|
301
|
+
for await (const message of this.transport.readMessages()) {
|
|
302
|
+
if (this._writeError) {
|
|
303
|
+
throw this._writeError;
|
|
304
|
+
}
|
|
305
|
+
const msgType = message?.msg?.type;
|
|
306
|
+
if (!this._sessionId && msgType === "session_configured") {
|
|
307
|
+
this._sessionId = message.msg.session_id ?? null;
|
|
308
|
+
}
|
|
309
|
+
yield message;
|
|
310
|
+
}
|
|
311
|
+
if (this._writeError) {
|
|
312
|
+
throw this._writeError;
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
this._finished = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
[Symbol.asyncIterator]() {
|
|
320
|
+
return this._messageIterator;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
next() {
|
|
324
|
+
if (this._writeError) {
|
|
325
|
+
return Promise.reject(this._writeError);
|
|
326
|
+
}
|
|
327
|
+
return this._messageIterator.next();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
get sessionId() {
|
|
331
|
+
return this._sessionId;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async submit(op, id) {
|
|
335
|
+
if (!op || typeof op !== "object") {
|
|
336
|
+
throw new Error("submit expects a Codex op object.");
|
|
337
|
+
}
|
|
338
|
+
const submissionId = id ?? this._generateSubmissionId();
|
|
339
|
+
const submission = { id: submissionId, op };
|
|
340
|
+
await this.transport.write(submission);
|
|
341
|
+
return submissionId;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async submitRaw(submission) {
|
|
345
|
+
if (!submission || typeof submission !== "object") {
|
|
346
|
+
throw new Error("submitRaw expects a submission object.");
|
|
347
|
+
}
|
|
348
|
+
if (!submission.id || !submission.op) {
|
|
349
|
+
throw new Error("Submission must include `id` and `op` fields.");
|
|
350
|
+
}
|
|
351
|
+
await this.transport.write(submission);
|
|
352
|
+
return submission.id;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async submitUserMessage(text, { items, id } = {}) {
|
|
356
|
+
const messageItems = Array.isArray(items) ? [...items] : [];
|
|
357
|
+
if (typeof text === "string" && text.length > 0) {
|
|
358
|
+
messageItems.push({ type: "text", text });
|
|
359
|
+
}
|
|
360
|
+
if (messageItems.length === 0) {
|
|
361
|
+
throw new Error("submitUserMessage requires text or items.");
|
|
362
|
+
}
|
|
363
|
+
return this.submit({ type: "user_input", items: messageItems }, id);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async interrupt(id) {
|
|
367
|
+
return this.submit({ type: "interrupt" }, id);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async shutdown(id) {
|
|
371
|
+
return this.submit({ type: "shutdown" }, id);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async close() {
|
|
375
|
+
await this.transport.close();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
kill() {
|
|
379
|
+
this.transport.interruptProcess();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeConfigOverrides(overrides) {
|
|
384
|
+
if (!overrides) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(overrides)) {
|
|
388
|
+
return overrides;
|
|
389
|
+
}
|
|
390
|
+
if (typeof overrides === "object") {
|
|
391
|
+
return Object.entries(overrides).map(([key, value]) =>
|
|
392
|
+
value === undefined || value === null ? key : `${key}=${value}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
throw new Error("configOverrides must be an array or object.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildSpawnConfig(options = {}) {
|
|
399
|
+
const {
|
|
400
|
+
pathToCodexExecutable,
|
|
401
|
+
resume,
|
|
402
|
+
configOverrides,
|
|
403
|
+
extraArgs,
|
|
404
|
+
cwd,
|
|
405
|
+
env,
|
|
406
|
+
stderr,
|
|
407
|
+
} = options;
|
|
408
|
+
|
|
409
|
+
const command = pathToCodexExecutable ?? "codex";
|
|
410
|
+
const args = ["proto"];
|
|
411
|
+
|
|
412
|
+
const overrides = normalizeConfigOverrides(configOverrides);
|
|
413
|
+
for (const override of overrides) {
|
|
414
|
+
args.push("-c", override);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (resume) {
|
|
418
|
+
args.push("--resume", resume);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (Array.isArray(extraArgs)) {
|
|
422
|
+
args.push(...extraArgs);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
command,
|
|
427
|
+
args,
|
|
428
|
+
cwd,
|
|
429
|
+
env,
|
|
430
|
+
onStderr: stderr,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function query({ prompt, options = {} } = {}) {
|
|
435
|
+
const spawnConfig = buildSpawnConfig(options);
|
|
436
|
+
const transport = new ProcessTransport(spawnConfig);
|
|
437
|
+
const q = new Query(transport);
|
|
438
|
+
|
|
439
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
440
|
+
q._registerWrite(q.submitUserMessage(prompt.trim()));
|
|
441
|
+
} else if (Array.isArray(prompt) && prompt.length > 0) {
|
|
442
|
+
q._registerWrite(q.submitUserMessage("", { items: prompt }));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return q;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export { Query };
|