@muyichengshayu/promptx 0.1.0
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/README.md +80 -0
- package/apps/server/src/appPaths.js +102 -0
- package/apps/server/src/codex.js +585 -0
- package/apps/server/src/codexRunRuntime.js +212 -0
- package/apps/server/src/codexRuns.js +525 -0
- package/apps/server/src/codexSessions.js +149 -0
- package/apps/server/src/db.js +389 -0
- package/apps/server/src/gitDiff.js +1425 -0
- package/apps/server/src/index.js +909 -0
- package/apps/server/src/pdf.js +383 -0
- package/apps/server/src/repository.js +484 -0
- package/apps/server/src/sseHub.js +69 -0
- package/apps/server/src/upload.js +22 -0
- package/apps/server/src/workspaceFiles.js +662 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-c35LrKjV.js +6 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-BYcla0q4.js +12 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C0uQRStP.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-Cp_qHNdX.js +216 -0
- package/apps/web/dist/assets/index-D9ui_gwj.js +25 -0
- package/apps/web/dist/assets/index-DDNrspNi.css +1 -0
- package/apps/web/dist/index.html +16 -0
- package/bin/promptx.js +60 -0
- package/package.json +58 -0
- package/packages/shared/src/index.js +121 -0
- package/scripts/doctor.mjs +251 -0
- package/scripts/service.mjs +308 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
6
|
+
import iconv from 'iconv-lite'
|
|
7
|
+
import initSqlJs from 'sql.js'
|
|
8
|
+
|
|
9
|
+
const CODEX_BIN = process.env.CODEX_BIN || 'codex'
|
|
10
|
+
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
|
|
11
|
+
const STATE_DB_PATH = path.join(CODEX_HOME, 'state_5.sqlite')
|
|
12
|
+
const TMP_DIR = path.join(CODEX_HOME, 'tmp')
|
|
13
|
+
const MAX_THREAD_COUNT = 120
|
|
14
|
+
const MAX_OUTPUT_TAIL_LENGTH = 64 * 1024
|
|
15
|
+
const CODEX_DEFAULT_ARGS = ['--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check']
|
|
16
|
+
const RESOLVED_CODEX_BIN = resolveCodexBinary()
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
const sqlWasmPath = require.resolve('sql.js/dist/sql-wasm.wasm')
|
|
19
|
+
const SQL = await initSqlJs({
|
|
20
|
+
locateFile: () => sqlWasmPath,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function ensureCodexHome() {
|
|
24
|
+
fs.mkdirSync(TMP_DIR, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCodexBinary() {
|
|
28
|
+
if (process.platform !== 'win32') {
|
|
29
|
+
return CODEX_BIN
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (path.extname(CODEX_BIN)) {
|
|
33
|
+
return CODEX_BIN
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(`${CODEX_BIN}.cmd`)) {
|
|
37
|
+
return `${CODEX_BIN}.cmd`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(`${CODEX_BIN}.bat`)) {
|
|
41
|
+
return `${CODEX_BIN}.bat`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(CODEX_BIN)) {
|
|
45
|
+
return CODEX_BIN
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync('where.exe', [CODEX_BIN], {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
|
+
}).trim()
|
|
53
|
+
|
|
54
|
+
if (!output) {
|
|
55
|
+
return CODEX_BIN
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const candidates = output
|
|
59
|
+
.split(/\r?\n/g)
|
|
60
|
+
.map((line) => line.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
|
|
63
|
+
return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
|
|
64
|
+
|| candidates.find((item) => /\.(exe|com)$/i.test(item))
|
|
65
|
+
|| candidates[0]
|
|
66
|
+
|| CODEX_BIN
|
|
67
|
+
} catch {
|
|
68
|
+
return CODEX_BIN
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createCodexSpawn(commandArgs = [], cwd = '') {
|
|
73
|
+
const options = {
|
|
74
|
+
env: process.env,
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
}
|
|
77
|
+
const normalizedCwd = String(cwd || '').trim()
|
|
78
|
+
|
|
79
|
+
if (normalizedCwd) {
|
|
80
|
+
options.cwd = normalizedCwd
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_CODEX_BIN)) {
|
|
84
|
+
return spawn(
|
|
85
|
+
process.env.ComSpec || 'cmd.exe',
|
|
86
|
+
['/d', '/s', '/c', RESOLVED_CODEX_BIN, ...commandArgs],
|
|
87
|
+
options
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return spawn(RESOLVED_CODEX_BIN, commandArgs, options)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSpawnError(error) {
|
|
95
|
+
if (error?.code === 'ENOENT') {
|
|
96
|
+
const attempted = RESOLVED_CODEX_BIN === CODEX_BIN
|
|
97
|
+
? CODEX_BIN
|
|
98
|
+
: `${CODEX_BIN} -> ${RESOLVED_CODEX_BIN}`
|
|
99
|
+
return new Error(
|
|
100
|
+
`找不到 Codex CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`codex --version\`,或设置环境变量 \`CODEX_BIN\` 指向可执行文件。Windows 常见路径是 \`%APPDATA%\\npm\\codex.cmd\`。`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return error
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function trimOutput(value = '', maxLength = 12000) {
|
|
108
|
+
const text = String(value || '').trim()
|
|
109
|
+
if (text.length <= maxLength) {
|
|
110
|
+
return text
|
|
111
|
+
}
|
|
112
|
+
return text.slice(text.length - maxLength)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function appendOutputTail(current = '', chunk = '', maxLength = MAX_OUTPUT_TAIL_LENGTH) {
|
|
116
|
+
const text = `${String(current || '')}${String(chunk || '')}`
|
|
117
|
+
if (text.length <= maxLength) {
|
|
118
|
+
return text
|
|
119
|
+
}
|
|
120
|
+
return text.slice(text.length - maxLength)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function countSuspiciousMojibakeChars(value = '') {
|
|
124
|
+
return (String(value || '').match(/[鑾彇娴嬭瘯鏁嵁璺緞璇诲彇鍒嗛鍚庡墠杩欎釜閫夋嫨鎻掑叆鍖哄伐浣滃櫒]/g) || []).length
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function countReadableCjkChars(value = '') {
|
|
128
|
+
return (String(value || '').match(/[\u4e00-\u9fff]/g) || []).length
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function repairPossibleMojibake(value = '') {
|
|
132
|
+
const text = String(value || '')
|
|
133
|
+
if (!text) {
|
|
134
|
+
return text
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const suspiciousCount = countSuspiciousMojibakeChars(text)
|
|
138
|
+
if (suspiciousCount < 2) {
|
|
139
|
+
return text
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let repaired = text
|
|
143
|
+
try {
|
|
144
|
+
repaired = iconv.decode(iconv.encode(text, 'gb18030'), 'utf8')
|
|
145
|
+
} catch {
|
|
146
|
+
return text
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!repaired || repaired === text) {
|
|
150
|
+
return text
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const repairedSuspiciousCount = countSuspiciousMojibakeChars(repaired)
|
|
154
|
+
|
|
155
|
+
if (repairedSuspiciousCount < suspiciousCount && countReadableCjkChars(repaired) > 0) {
|
|
156
|
+
return repaired
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return text
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sanitizeCodexPayload(value) {
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
return repairPossibleMojibake(value)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
return value.map((item) => sanitizeCodexPayload(item))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!value || typeof value !== 'object') {
|
|
172
|
+
return value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Object.fromEntries(
|
|
176
|
+
Object.entries(value).map(([key, item]) => [key, sanitizeCodexPayload(item)])
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseJsonLine(line = '') {
|
|
181
|
+
const text = String(line || '').trim()
|
|
182
|
+
if (!text) {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(text)
|
|
188
|
+
} catch {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function splitBufferedLines(buffer = '') {
|
|
194
|
+
const text = String(buffer || '')
|
|
195
|
+
if (!text) {
|
|
196
|
+
return { lines: [], rest: '' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
200
|
+
const parts = normalized.split('\n')
|
|
201
|
+
const rest = parts.pop() || ''
|
|
202
|
+
const lines = parts.map((line) => line.trim()).filter(Boolean)
|
|
203
|
+
return { lines, rest }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function flushBufferedText(buffer = '') {
|
|
207
|
+
const { lines, rest } = splitBufferedLines(buffer)
|
|
208
|
+
const tail = String(rest || '').trim()
|
|
209
|
+
return tail ? [...lines, tail] : lines
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractTextFromUnknownError(input, depth = 0) {
|
|
213
|
+
if (!input || depth > 4) {
|
|
214
|
+
return ''
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof input === 'string') {
|
|
218
|
+
return input.trim()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof input !== 'object') {
|
|
222
|
+
return ''
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const priorityKeys = [
|
|
226
|
+
'message',
|
|
227
|
+
'detail',
|
|
228
|
+
'error',
|
|
229
|
+
'last_error',
|
|
230
|
+
'cause',
|
|
231
|
+
'reason',
|
|
232
|
+
'stderr',
|
|
233
|
+
'text',
|
|
234
|
+
'summary',
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
for (const key of priorityKeys) {
|
|
238
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const text = extractTextFromUnknownError(input[key], depth + 1)
|
|
243
|
+
if (text) {
|
|
244
|
+
return text
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const value of Object.values(input)) {
|
|
249
|
+
const text = extractTextFromUnknownError(value, depth + 1)
|
|
250
|
+
if (text) {
|
|
251
|
+
return text
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return ''
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function extractLatestCodexEventError(stdout = '') {
|
|
259
|
+
const lines = flushBufferedText(stdout)
|
|
260
|
+
let latestError = ''
|
|
261
|
+
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
const event = parseJsonLine(line)
|
|
264
|
+
if (!event || (event.type !== 'error' && event.type !== 'turn.failed')) {
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const text = extractTextFromUnknownError(sanitizeCodexPayload(event))
|
|
269
|
+
if (text) {
|
|
270
|
+
latestError = text
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return latestError
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeManagedSession(sessionInput) {
|
|
278
|
+
if (!sessionInput || typeof sessionInput !== 'object') {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const id = String(sessionInput.id || '').trim()
|
|
283
|
+
const cwd = String(sessionInput.cwd || '').trim()
|
|
284
|
+
if (!id || !cwd) {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
id,
|
|
290
|
+
title: String(sessionInput.title || '').trim(),
|
|
291
|
+
cwd,
|
|
292
|
+
codexThreadId: String(sessionInput.codexThreadId || '').trim(),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createExecArgs(session) {
|
|
297
|
+
const baseArgs = ['exec', ...CODEX_DEFAULT_ARGS, ...(session.cwd ? ['-C', session.cwd] : [])]
|
|
298
|
+
|
|
299
|
+
if (session.codexThreadId) {
|
|
300
|
+
return [...baseArgs, 'resume', session.codexThreadId, '-', '--json']
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return [...baseArgs, '-', '--json']
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function extractCodexError(stderr = '', stdout = '') {
|
|
307
|
+
const eventError = extractLatestCodexEventError(stdout)
|
|
308
|
+
if (eventError) {
|
|
309
|
+
return eventError
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const stderrText = trimOutput(stderr)
|
|
313
|
+
if (stderrText) {
|
|
314
|
+
const lines = stderrText.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
315
|
+
return lines[lines.length - 1] || stderrText
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const stdoutText = trimOutput(stdout)
|
|
319
|
+
if (!stdoutText) {
|
|
320
|
+
return 'Codex 执行失败。'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const lines = stdoutText.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
324
|
+
return lines[lines.length - 1] || stdoutText
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function trackThreadId(event, setThreadId) {
|
|
328
|
+
if (event?.type === 'thread.started' && event.thread_id) {
|
|
329
|
+
setThreadId(String(event.thread_id))
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseThreadIdFromStdout(stdout = '') {
|
|
334
|
+
const lines = String(stdout || '')
|
|
335
|
+
.replace(/\r\n/g, '\n')
|
|
336
|
+
.split('\n')
|
|
337
|
+
.map((line) => line.trim())
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
const event = parseJsonLine(line)
|
|
342
|
+
if (event?.type === 'thread.started' && event.thread_id) {
|
|
343
|
+
return String(event.thread_id)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return ''
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function loadCodexThreads(limit = MAX_THREAD_COUNT) {
|
|
351
|
+
if (!fs.existsSync(STATE_DB_PATH)) {
|
|
352
|
+
return []
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const sql = `select id, cwd, title, updated_at from threads order by updated_at desc limit ${Math.max(1, Number(limit) || MAX_THREAD_COUNT)};`
|
|
357
|
+
const db = new SQL.Database(new Uint8Array(fs.readFileSync(STATE_DB_PATH)))
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const statement = db.prepare(sql)
|
|
361
|
+
const rows = []
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
while (statement.step()) {
|
|
365
|
+
rows.push(statement.getAsObject())
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
statement.free()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return rows
|
|
372
|
+
} finally {
|
|
373
|
+
db.close()
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
return []
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function listKnownCodexWorkspaces(limit = MAX_THREAD_COUNT) {
|
|
381
|
+
const seen = new Set()
|
|
382
|
+
const items = []
|
|
383
|
+
|
|
384
|
+
loadCodexThreads(limit).forEach((thread) => {
|
|
385
|
+
const cwd = String(thread.cwd || '').trim()
|
|
386
|
+
if (!cwd || seen.has(cwd)) {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
seen.add(cwd)
|
|
390
|
+
items.push(cwd)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
return items
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {}) {
|
|
397
|
+
const session = normalizeManagedSession(sessionInput)
|
|
398
|
+
const normalizedPrompt = String(prompt || '').trim()
|
|
399
|
+
|
|
400
|
+
if (!session) {
|
|
401
|
+
throw new Error('缺少 PromptX 项目。')
|
|
402
|
+
}
|
|
403
|
+
if (!normalizedPrompt) {
|
|
404
|
+
throw new Error('没有可发送的提示词。')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ensureCodexHome()
|
|
408
|
+
|
|
409
|
+
const outputFile = path.join(TMP_DIR, `promptx-codex-${Date.now()}-${process.pid}.txt`)
|
|
410
|
+
const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
|
|
411
|
+
const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
|
|
412
|
+
|
|
413
|
+
const child = createCodexSpawn(
|
|
414
|
+
[
|
|
415
|
+
...createExecArgs(session),
|
|
416
|
+
'--output-last-message',
|
|
417
|
+
outputFile,
|
|
418
|
+
],
|
|
419
|
+
session.cwd
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
let stdoutBuffer = ''
|
|
423
|
+
let stderrBuffer = ''
|
|
424
|
+
let stdoutRaw = ''
|
|
425
|
+
let stderrRaw = ''
|
|
426
|
+
let finalMessage = ''
|
|
427
|
+
let finalThreadId = session.codexThreadId || ''
|
|
428
|
+
|
|
429
|
+
const emit = (event) => {
|
|
430
|
+
try {
|
|
431
|
+
onEvent(event)
|
|
432
|
+
} catch {
|
|
433
|
+
// Ignore observer failures to avoid breaking the process lifecycle.
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const rememberThreadId = (threadId) => {
|
|
438
|
+
const value = String(threadId || '').trim()
|
|
439
|
+
if (!value || value === finalThreadId) {
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
finalThreadId = value
|
|
443
|
+
try {
|
|
444
|
+
onThreadStarted(value)
|
|
445
|
+
} catch {
|
|
446
|
+
// Ignore observer failures to avoid breaking the process lifecycle.
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
emit({
|
|
451
|
+
type: 'status',
|
|
452
|
+
stage: session.codexThreadId ? 'resuming' : 'starting',
|
|
453
|
+
message: session.codexThreadId
|
|
454
|
+
? '已连接 PromptX 项目,正在继续这轮执行。'
|
|
455
|
+
: '已创建 PromptX 项目,正在启动第一轮执行。',
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
child.stdout.on('data', (chunk) => {
|
|
459
|
+
const text = chunk.toString()
|
|
460
|
+
stdoutRaw = appendOutputTail(stdoutRaw, text)
|
|
461
|
+
stdoutBuffer += text
|
|
462
|
+
const { lines, rest } = splitBufferedLines(stdoutBuffer)
|
|
463
|
+
stdoutBuffer = rest
|
|
464
|
+
|
|
465
|
+
for (const line of lines) {
|
|
466
|
+
const event = parseJsonLine(line)
|
|
467
|
+
if (event) {
|
|
468
|
+
trackThreadId(event, rememberThreadId)
|
|
469
|
+
emit({
|
|
470
|
+
type: 'codex',
|
|
471
|
+
event: sanitizeCodexPayload(event),
|
|
472
|
+
})
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
emit({
|
|
477
|
+
type: 'stdout',
|
|
478
|
+
text: repairPossibleMojibake(line),
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
child.stderr.on('data', (chunk) => {
|
|
484
|
+
const text = chunk.toString()
|
|
485
|
+
stderrRaw = appendOutputTail(stderrRaw, text)
|
|
486
|
+
stderrBuffer += text
|
|
487
|
+
const { lines, rest } = splitBufferedLines(stderrBuffer)
|
|
488
|
+
stderrBuffer = rest
|
|
489
|
+
|
|
490
|
+
for (const line of lines) {
|
|
491
|
+
emit({
|
|
492
|
+
type: 'stderr',
|
|
493
|
+
text: repairPossibleMojibake(line),
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
child.stdin.write(normalizedPrompt)
|
|
499
|
+
child.stdin.end()
|
|
500
|
+
|
|
501
|
+
const result = new Promise((resolve, reject) => {
|
|
502
|
+
child.on('error', (error) => {
|
|
503
|
+
reject(normalizeSpawnError(error))
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
child.on('close', (code) => {
|
|
507
|
+
const stdoutTail = flushBufferedText(stdoutBuffer)
|
|
508
|
+
const stderrTail = flushBufferedText(stderrBuffer)
|
|
509
|
+
|
|
510
|
+
stdoutTail.forEach((line) => {
|
|
511
|
+
const event = parseJsonLine(line)
|
|
512
|
+
if (event) {
|
|
513
|
+
trackThreadId(event, rememberThreadId)
|
|
514
|
+
emit({
|
|
515
|
+
type: 'codex',
|
|
516
|
+
event: sanitizeCodexPayload(event),
|
|
517
|
+
})
|
|
518
|
+
} else {
|
|
519
|
+
emit({
|
|
520
|
+
type: 'stdout',
|
|
521
|
+
text: repairPossibleMojibake(line),
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
stderrTail.forEach((line) => {
|
|
527
|
+
emit({
|
|
528
|
+
type: 'stderr',
|
|
529
|
+
text: repairPossibleMojibake(line),
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
if (fs.existsSync(outputFile)) {
|
|
534
|
+
finalMessage = repairPossibleMojibake(fs.readFileSync(outputFile, 'utf8').trim())
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!finalThreadId) {
|
|
538
|
+
finalThreadId = parseThreadIdFromStdout(stdoutRaw)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (code !== 0) {
|
|
542
|
+
reject(new Error(repairPossibleMojibake(extractCodexError(stderrRaw, stdoutRaw))))
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
emit({
|
|
547
|
+
type: 'completed',
|
|
548
|
+
message: finalMessage,
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
resolve({
|
|
552
|
+
sessionId: session.id,
|
|
553
|
+
message: finalMessage,
|
|
554
|
+
threadId: finalThreadId,
|
|
555
|
+
})
|
|
556
|
+
})
|
|
557
|
+
}).finally(() => {
|
|
558
|
+
fs.rmSync(outputFile, { force: true })
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
child,
|
|
563
|
+
result,
|
|
564
|
+
cancel() {
|
|
565
|
+
if (child.killed) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (process.platform === 'win32' && child.pid) {
|
|
570
|
+
try {
|
|
571
|
+
execFileSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
|
572
|
+
stdio: 'ignore',
|
|
573
|
+
})
|
|
574
|
+
return
|
|
575
|
+
} catch {
|
|
576
|
+
// Fall through to the default child kill when taskkill is unavailable.
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!child.killed) {
|
|
581
|
+
child.kill('SIGTERM')
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
}
|
|
585
|
+
}
|