@voxagent/bridge 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.
Files changed (70) hide show
  1. package/README.md +74 -0
  2. package/dist/audio/source.d.ts +23 -0
  3. package/dist/audio/source.d.ts.map +1 -0
  4. package/dist/audio/source.js +69 -0
  5. package/dist/audio/source.js.map +1 -0
  6. package/dist/cli.d.ts +13 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +171 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +62 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +102 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/drivers/claude.d.ts +36 -0
  15. package/dist/drivers/claude.d.ts.map +1 -0
  16. package/dist/drivers/claude.js +445 -0
  17. package/dist/drivers/claude.js.map +1 -0
  18. package/dist/drivers/codex.d.ts +56 -0
  19. package/dist/drivers/codex.d.ts.map +1 -0
  20. package/dist/drivers/codex.js +652 -0
  21. package/dist/drivers/codex.js.map +1 -0
  22. package/dist/drivers/index.d.ts +34 -0
  23. package/dist/drivers/index.d.ts.map +1 -0
  24. package/dist/drivers/index.js +18 -0
  25. package/dist/drivers/index.js.map +1 -0
  26. package/dist/drivers/types.d.ts +81 -0
  27. package/dist/drivers/types.d.ts.map +1 -0
  28. package/dist/drivers/types.js +11 -0
  29. package/dist/drivers/types.js.map +1 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/logging.d.ts +15 -0
  35. package/dist/logging.d.ts.map +1 -0
  36. package/dist/logging.js +36 -0
  37. package/dist/logging.js.map +1 -0
  38. package/dist/pair.d.ts +25 -0
  39. package/dist/pair.d.ts.map +1 -0
  40. package/dist/pair.js +41 -0
  41. package/dist/pair.js.map +1 -0
  42. package/dist/permissions.d.ts +67 -0
  43. package/dist/permissions.d.ts.map +1 -0
  44. package/dist/permissions.js +221 -0
  45. package/dist/permissions.js.map +1 -0
  46. package/dist/protocol.d.ts +181 -0
  47. package/dist/protocol.d.ts.map +1 -0
  48. package/dist/protocol.js +63 -0
  49. package/dist/protocol.js.map +1 -0
  50. package/dist/room.d.ts +52 -0
  51. package/dist/room.d.ts.map +1 -0
  52. package/dist/room.js +401 -0
  53. package/dist/room.js.map +1 -0
  54. package/dist/tts/elevenlabs.d.ts +43 -0
  55. package/dist/tts/elevenlabs.d.ts.map +1 -0
  56. package/dist/tts/elevenlabs.js +268 -0
  57. package/dist/tts/elevenlabs.js.map +1 -0
  58. package/dist/tts/index.d.ts +22 -0
  59. package/dist/tts/index.d.ts.map +1 -0
  60. package/dist/tts/index.js +23 -0
  61. package/dist/tts/index.js.map +1 -0
  62. package/dist/tts/openai.d.ts +35 -0
  63. package/dist/tts/openai.d.ts.map +1 -0
  64. package/dist/tts/openai.js +270 -0
  65. package/dist/tts/openai.js.map +1 -0
  66. package/dist/tts/types.d.ts +28 -0
  67. package/dist/tts/types.d.ts.map +1 -0
  68. package/dist/tts/types.js +5 -0
  69. package/dist/tts/types.js.map +1 -0
  70. package/package.json +68 -0
@@ -0,0 +1,652 @@
1
+ /**
2
+ * @file Codex driver — drives `codex app-server` over line-delimited JSON-RPC 2.0.
3
+ *
4
+ * Why app-server instead of `codex exec --json`:
5
+ * `exec --json` does not support pause/resume approvals — when an action
6
+ * would normally trigger a prompt, exec mode treats it as a hard failure.
7
+ * The app-server transport (`codex app-server`, default stdio) is a proper
8
+ * JSON-RPC 2.0 channel where Codex sends server-initiated approval
9
+ * requests (`item/commandExecution/requestApproval`,
10
+ * `item/fileChange/requestApproval`) that we respond to via the same
11
+ * JSON-RPC envelope. That lets us route approvals through the bridge's
12
+ * PermissionBroker to a meeting-side Allow / Deny modal.
13
+ *
14
+ * Lifecycle:
15
+ * - One long-lived `codex app-server` subprocess per CodexDriver instance.
16
+ * Spawned lazily on the first run() so capabilities() stays cheap.
17
+ * - `initialize` request once, `thread/start` on the first run, then
18
+ * `turn/start` for each subsequent run() call (threadId reused so Codex
19
+ * keeps conversation context across turns).
20
+ * - On run abort: `turn/interrupt` for the active turn.
21
+ * - On close(): SIGTERM the process and reject pending requests.
22
+ *
23
+ * The streaming JSON-RPC types and notification methods are taken from
24
+ * `codex app-server generate-ts` output (see /tmp/codex-protocol).
25
+ */
26
+ import { spawn } from 'node:child_process';
27
+ import { homedir } from 'node:os';
28
+ import { readFile } from 'node:fs/promises';
29
+ import { join } from 'node:path';
30
+ import { randomUUID } from 'node:crypto';
31
+ import { log } from '../logging.js';
32
+ const BRIDGE_VERSION = '0.1.0';
33
+ export class CodexDriver {
34
+ options;
35
+ backend = 'codex';
36
+ proc = null;
37
+ threadId = null;
38
+ nextRequestId = 1;
39
+ pending = new Map();
40
+ activeTurn = null;
41
+ rxBuffer = '';
42
+ serverStartFailed = false;
43
+ constructor(options = {}) {
44
+ this.options = options;
45
+ }
46
+ async capabilities(cwd) {
47
+ const mcpServers = await this.discoverMcpServers();
48
+ return {
49
+ backend: 'codex',
50
+ skills: [],
51
+ plugins: [],
52
+ mcpServers,
53
+ cwd,
54
+ bridgeVersion: BRIDGE_VERSION,
55
+ };
56
+ }
57
+ async *run(input, signal) {
58
+ const runId = randomUUID();
59
+ const startedAtMs = Date.now();
60
+ yield { type: 'run_start', runId, startedAtMs };
61
+ try {
62
+ await this.ensureServer();
63
+ }
64
+ catch (error) {
65
+ yield {
66
+ type: 'run_end',
67
+ runId,
68
+ reason: 'error',
69
+ message: error instanceof Error ? error.message : String(error),
70
+ totalLatencyMs: Date.now() - startedAtMs,
71
+ };
72
+ return;
73
+ }
74
+ if (!this.threadId) {
75
+ try {
76
+ // Codex 0.104's CLI-side default leaks `gpt-5.5` into thread/start,
77
+ // which the live server rejects. The codex-suffixed flagships
78
+ // (gpt-5.2-codex etc.) require API-key auth and fail under
79
+ // ChatGPT-account auth. Default to `gpt-5.4` which the server
80
+ // advertises in `model/list` and accepts under both auth modes.
81
+ // Users with API keys can override via --llm-model.
82
+ const model = this.options.model ?? 'gpt-5.4';
83
+ const result = (await this.sendRequest('thread/start', {
84
+ cwd: input.cwd,
85
+ approvalPolicy: this.options.fullAccess ? 'never' : 'on-request',
86
+ sandbox: this.options.fullAccess ? 'danger-full-access' : 'workspace-write',
87
+ model,
88
+ experimentalRawEvents: false,
89
+ persistExtendedHistory: false,
90
+ }));
91
+ this.threadId = result.thread?.id ?? null;
92
+ }
93
+ catch (error) {
94
+ yield {
95
+ type: 'run_end',
96
+ runId,
97
+ reason: 'error',
98
+ message: `thread/start failed: ${error instanceof Error ? error.message : String(error)}`,
99
+ totalLatencyMs: Date.now() - startedAtMs,
100
+ };
101
+ return;
102
+ }
103
+ }
104
+ if (!this.threadId) {
105
+ yield {
106
+ type: 'run_end',
107
+ runId,
108
+ reason: 'error',
109
+ message: 'Codex did not return a thread ID.',
110
+ totalLatencyMs: Date.now() - startedAtMs,
111
+ };
112
+ return;
113
+ }
114
+ // Build a queue + iterator pair driven by handleNotification + handleServerRequest.
115
+ const queue = [];
116
+ const waiters = [];
117
+ let ended = false;
118
+ const push = (event) => {
119
+ if (ended)
120
+ return;
121
+ const waiter = waiters.shift();
122
+ if (waiter)
123
+ waiter({ value: event, done: false });
124
+ else
125
+ queue.push(event);
126
+ };
127
+ const end = (reason, message) => {
128
+ if (ended)
129
+ return;
130
+ ended = true;
131
+ const totalLatencyMs = Date.now() - startedAtMs;
132
+ const evt = message
133
+ ? { type: 'run_end', runId, reason, message, totalLatencyMs }
134
+ : { type: 'run_end', runId, reason, totalLatencyMs };
135
+ const waiter = waiters.shift();
136
+ if (waiter)
137
+ waiter({ value: evt, done: false });
138
+ else
139
+ queue.push(evt);
140
+ // Then signal exhaustion.
141
+ const final = waiters.shift();
142
+ if (final)
143
+ final({ value: undefined, done: true });
144
+ };
145
+ const turn = {
146
+ turnId: null,
147
+ push,
148
+ end,
149
+ startedAtMs,
150
+ itemStarts: new Map(),
151
+ runId,
152
+ signal,
153
+ finalText: '',
154
+ };
155
+ this.activeTurn = turn;
156
+ let promptText = input.prompt;
157
+ if (input.systemPromptAddendum) {
158
+ promptText = `${input.systemPromptAddendum}\n\n---\n\n${promptText}`;
159
+ }
160
+ const onAbort = () => {
161
+ if (!turn.turnId)
162
+ return;
163
+ void this.sendRequest('turn/interrupt', { threadId: this.threadId, turnId: turn.turnId }).catch(() => undefined);
164
+ };
165
+ if (signal.aborted)
166
+ onAbort();
167
+ else
168
+ signal.addEventListener('abort', onAbort, { once: true });
169
+ // Kick off the turn — don't await, let notifications stream while we wait.
170
+ void this.sendRequest('turn/start', {
171
+ threadId: this.threadId,
172
+ input: [{ type: 'text', text: promptText }],
173
+ })
174
+ .then((result) => {
175
+ const r = result;
176
+ if (r.turn?.id)
177
+ turn.turnId = r.turn.id;
178
+ })
179
+ .catch((error) => {
180
+ end('error', `turn/start failed: ${error instanceof Error ? error.message : String(error)}`);
181
+ });
182
+ try {
183
+ while (true) {
184
+ if (queue.length > 0) {
185
+ const evt = queue.shift();
186
+ yield evt;
187
+ if (evt.type === 'run_end')
188
+ return;
189
+ continue;
190
+ }
191
+ if (ended && queue.length === 0)
192
+ return;
193
+ const next = await new Promise((resolve) => {
194
+ waiters.push(resolve);
195
+ });
196
+ if (next.done)
197
+ return;
198
+ yield next.value;
199
+ if (next.value.type === 'run_end')
200
+ return;
201
+ }
202
+ }
203
+ finally {
204
+ signal.removeEventListener('abort', onAbort);
205
+ if (this.activeTurn === turn)
206
+ this.activeTurn = null;
207
+ }
208
+ }
209
+ async close() {
210
+ if (this.activeTurn) {
211
+ this.activeTurn.end('interrupted');
212
+ this.activeTurn = null;
213
+ }
214
+ for (const [, entry] of this.pending) {
215
+ entry.reject(new Error('Codex driver closed.'));
216
+ }
217
+ this.pending.clear();
218
+ if (this.proc && !this.proc.killed) {
219
+ this.proc.kill('SIGTERM');
220
+ }
221
+ this.proc = null;
222
+ this.threadId = null;
223
+ }
224
+ async ensureServer() {
225
+ if (this.proc)
226
+ return;
227
+ if (this.serverStartFailed) {
228
+ throw new Error('codex app-server previously failed to start.');
229
+ }
230
+ const child = spawn('codex', ['app-server'], {
231
+ stdio: ['pipe', 'pipe', 'pipe'],
232
+ env: { ...process.env },
233
+ });
234
+ this.proc = child;
235
+ child.stdout.setEncoding('utf-8');
236
+ child.stdout.on('data', (chunk) => this.onStdout(chunk));
237
+ child.stderr.setEncoding('utf-8');
238
+ child.stderr.on('data', (chunk) => {
239
+ log.debug('codex_stderr', { chunk: chunk.slice(0, 240) });
240
+ });
241
+ child.once('exit', (code, signal) => {
242
+ log.info('codex_app_server_exited', { code, signal });
243
+ const wasActive = this.activeTurn;
244
+ this.proc = null;
245
+ for (const [, entry] of this.pending) {
246
+ entry.reject(new Error('codex app-server exited.'));
247
+ }
248
+ this.pending.clear();
249
+ if (wasActive) {
250
+ wasActive.end('error', 'codex app-server exited unexpectedly.');
251
+ this.activeTurn = null;
252
+ }
253
+ });
254
+ try {
255
+ await this.sendRequest('initialize', {
256
+ clientInfo: { name: 'voxagent-bridge', title: 'VoxAgent Bridge', version: BRIDGE_VERSION },
257
+ capabilities: { experimentalApi: false },
258
+ });
259
+ }
260
+ catch (error) {
261
+ this.serverStartFailed = true;
262
+ child.kill('SIGTERM');
263
+ this.proc = null;
264
+ throw new Error(`codex initialize failed: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ }
267
+ onStdout(chunk) {
268
+ this.rxBuffer += chunk;
269
+ let idx;
270
+ while ((idx = this.rxBuffer.indexOf('\n')) !== -1) {
271
+ const line = this.rxBuffer.slice(0, idx).trim();
272
+ this.rxBuffer = this.rxBuffer.slice(idx + 1);
273
+ if (line.length === 0)
274
+ continue;
275
+ let parsed;
276
+ try {
277
+ parsed = JSON.parse(line);
278
+ }
279
+ catch {
280
+ log.warn('codex_unparseable_line', { line: line.slice(0, 200) });
281
+ continue;
282
+ }
283
+ this.dispatch(parsed);
284
+ }
285
+ }
286
+ dispatch(msg) {
287
+ if ('id' in msg && msg.result !== undefined) {
288
+ this.handleResponse(msg);
289
+ return;
290
+ }
291
+ if ('id' in msg && msg.error !== undefined) {
292
+ this.handleResponse(msg);
293
+ return;
294
+ }
295
+ if ('id' in msg && 'method' in msg) {
296
+ this.handleServerRequest(msg);
297
+ return;
298
+ }
299
+ if ('method' in msg) {
300
+ this.handleNotification(msg);
301
+ return;
302
+ }
303
+ }
304
+ handleResponse(resp) {
305
+ if (typeof resp.id !== 'number')
306
+ return;
307
+ const entry = this.pending.get(resp.id);
308
+ if (!entry)
309
+ return;
310
+ this.pending.delete(resp.id);
311
+ if (resp.error) {
312
+ entry.reject(new Error(`${resp.error.code}: ${resp.error.message}`));
313
+ }
314
+ else {
315
+ entry.resolve(resp.result ?? null);
316
+ }
317
+ }
318
+ handleServerRequest(req) {
319
+ const params = (req.params ?? {});
320
+ if (req.method === 'item/commandExecution/requestApproval') {
321
+ void this.handleApproval('Bash', req.id, params);
322
+ return;
323
+ }
324
+ if (req.method === 'item/fileChange/requestApproval') {
325
+ void this.handleApproval('Edit', req.id, params);
326
+ return;
327
+ }
328
+ // For server requests we don't recognize, decline politely so Codex doesn't hang.
329
+ log.debug('codex_unknown_server_request', { method: req.method });
330
+ this.sendResponse(req.id, { error: { code: -32601, message: 'Not implemented in bridge.' } });
331
+ }
332
+ async handleApproval(surface, requestId, params) {
333
+ const resolver = this.options.permissionResolver;
334
+ if (this.options.fullAccess || !resolver) {
335
+ this.sendResponse(requestId, { result: { decision: 'accept' } });
336
+ return;
337
+ }
338
+ try {
339
+ const result = await resolver.decide({
340
+ runId: this.activeTurn?.runId ?? 'unknown',
341
+ toolName: surface,
342
+ toolInput: params,
343
+ });
344
+ this.sendResponse(requestId, {
345
+ result: { decision: result.decision === 'allow' ? 'accept' : 'decline' },
346
+ });
347
+ }
348
+ catch (error) {
349
+ log.warn('codex_approval_resolver_failed', {
350
+ message: error instanceof Error ? error.message : String(error),
351
+ });
352
+ this.sendResponse(requestId, { result: { decision: 'decline' } });
353
+ }
354
+ }
355
+ handleNotification(notif) {
356
+ const turn = this.activeTurn;
357
+ const params = (notif.params ?? {});
358
+ switch (notif.method) {
359
+ case 'turn/started': {
360
+ if (!turn)
361
+ return;
362
+ const p = params;
363
+ turn.turnId = p.turn.id;
364
+ return;
365
+ }
366
+ case 'turn/completed': {
367
+ if (!turn)
368
+ return;
369
+ const p = params;
370
+ if (p.turn?.id && turn.turnId && p.turn.id !== turn.turnId)
371
+ return;
372
+ if (turn.finalText) {
373
+ turn.push({ type: 'text_done', runId: turn.runId, text: turn.finalText });
374
+ }
375
+ const failed = p.turn?.status === 'failed' || p.turn?.status === 'error';
376
+ const reason = turn.signal.aborted
377
+ ? 'interrupted'
378
+ : failed
379
+ ? 'error'
380
+ : 'completed';
381
+ const errorMessage = parseTurnErrorMessage(p.turn?.error?.message);
382
+ if (failed && errorMessage) {
383
+ // Surface the failure to the room so the user sees why the agent
384
+ // went idle rather than just watching status drop silently.
385
+ turn.push({
386
+ type: 'text_done',
387
+ runId: turn.runId,
388
+ text: `Codex couldn't complete the turn: ${errorMessage}`,
389
+ });
390
+ }
391
+ turn.end(reason, errorMessage ?? undefined);
392
+ return;
393
+ }
394
+ case 'item/started': {
395
+ if (!turn)
396
+ return;
397
+ const p = params;
398
+ this.handleItemStarted(turn, p);
399
+ return;
400
+ }
401
+ case 'item/completed': {
402
+ if (!turn)
403
+ return;
404
+ const p = params;
405
+ this.handleItemCompleted(turn, p);
406
+ return;
407
+ }
408
+ case 'item/agentMessage/delta': {
409
+ if (!turn)
410
+ return;
411
+ const p = params;
412
+ if (p.delta && p.delta.length > 0) {
413
+ turn.finalText += p.delta;
414
+ turn.push({ type: 'text_delta', runId: turn.runId, delta: p.delta });
415
+ }
416
+ return;
417
+ }
418
+ case 'item/commandExecution/outputDelta': {
419
+ if (!turn)
420
+ return;
421
+ const p = params;
422
+ const text = p.text ?? p.chunk ?? '';
423
+ if (text.length > 0) {
424
+ turn.push({
425
+ type: 'tool_use_progress',
426
+ runId: turn.runId,
427
+ toolUseId: p.itemId,
428
+ chunk: text,
429
+ });
430
+ }
431
+ return;
432
+ }
433
+ case 'item/reasoning/textDelta':
434
+ case 'item/reasoning/summaryTextDelta': {
435
+ if (!turn)
436
+ return;
437
+ const p = params;
438
+ if (p.delta) {
439
+ turn.push({ type: 'thinking_delta', runId: turn.runId, delta: p.delta });
440
+ }
441
+ return;
442
+ }
443
+ case 'error': {
444
+ if (!turn)
445
+ return;
446
+ const errMessage = typeof params.error?.message === 'string'
447
+ ? parseTurnErrorMessage(params.error.message)
448
+ : null;
449
+ const message = errMessage ?? (typeof params.message === 'string' ? params.message : 'Codex error.');
450
+ turn.push({
451
+ type: 'text_done',
452
+ runId: turn.runId,
453
+ text: `Codex error: ${message}`,
454
+ });
455
+ turn.end('error', message);
456
+ return;
457
+ }
458
+ default:
459
+ return;
460
+ }
461
+ }
462
+ handleItemStarted(turn, params) {
463
+ const item = params.item;
464
+ if (item.type === 'commandExecution') {
465
+ const ce = item;
466
+ turn.itemStarts.set(item.id, Date.now());
467
+ turn.push({
468
+ type: 'tool_use_start',
469
+ runId: turn.runId,
470
+ toolUseId: item.id,
471
+ name: 'Bash',
472
+ input: { command: ce.command, cwd: ce.cwd },
473
+ });
474
+ return;
475
+ }
476
+ if (item.type === 'fileChange') {
477
+ const fc = item;
478
+ turn.itemStarts.set(item.id, Date.now());
479
+ turn.push({
480
+ type: 'tool_use_start',
481
+ runId: turn.runId,
482
+ toolUseId: item.id,
483
+ name: 'Edit',
484
+ input: { changes: fc.changes },
485
+ });
486
+ return;
487
+ }
488
+ if (item.type === 'mcpToolCall') {
489
+ const mc = item;
490
+ turn.itemStarts.set(item.id, Date.now());
491
+ turn.push({
492
+ type: 'tool_use_start',
493
+ runId: turn.runId,
494
+ toolUseId: item.id,
495
+ name: `${mc.server}:${mc.tool}`,
496
+ input: mc.arguments,
497
+ });
498
+ return;
499
+ }
500
+ if (item.type === 'webSearch') {
501
+ const ws = item;
502
+ turn.itemStarts.set(item.id, Date.now());
503
+ turn.push({
504
+ type: 'tool_use_start',
505
+ runId: turn.runId,
506
+ toolUseId: item.id,
507
+ name: 'WebSearch',
508
+ input: { query: ws.query },
509
+ });
510
+ }
511
+ }
512
+ handleItemCompleted(turn, params) {
513
+ const item = params.item;
514
+ const startedAt = turn.itemStarts.get(item.id);
515
+ const latencyMs = startedAt ? Date.now() - startedAt : undefined;
516
+ if (item.type === 'agentMessage') {
517
+ const am = item;
518
+ // Final assistant text — prefer the streamed accumulator, fall back to am.text.
519
+ if (!turn.finalText && am.text)
520
+ turn.finalText = am.text;
521
+ return;
522
+ }
523
+ if (item.type === 'commandExecution') {
524
+ const ce = item;
525
+ const status = ce.exitCode === 0 ? 'success' : 'error';
526
+ turn.push({
527
+ type: 'tool_use_end',
528
+ runId: turn.runId,
529
+ toolUseId: item.id,
530
+ status,
531
+ output: {
532
+ command: ce.command,
533
+ aggregatedOutput: ce.aggregatedOutput,
534
+ exitCode: ce.exitCode,
535
+ durationMs: ce.durationMs,
536
+ },
537
+ ...(latencyMs !== undefined ? { latencyMs } : {}),
538
+ });
539
+ turn.itemStarts.delete(item.id);
540
+ return;
541
+ }
542
+ if (item.type === 'fileChange') {
543
+ const fc = item;
544
+ turn.push({
545
+ type: 'tool_use_end',
546
+ runId: turn.runId,
547
+ toolUseId: item.id,
548
+ status: fc.status === 'success' || fc.status === 'completed' ? 'success' : 'error',
549
+ output: { changes: fc.changes, status: fc.status },
550
+ ...(latencyMs !== undefined ? { latencyMs } : {}),
551
+ });
552
+ turn.itemStarts.delete(item.id);
553
+ return;
554
+ }
555
+ if (item.type === 'mcpToolCall') {
556
+ const mc = item;
557
+ turn.push({
558
+ type: 'tool_use_end',
559
+ runId: turn.runId,
560
+ toolUseId: item.id,
561
+ status: mc.error ? 'error' : 'success',
562
+ output: { result: mc.result, error: mc.error },
563
+ ...(latencyMs !== undefined ? { latencyMs } : {}),
564
+ });
565
+ turn.itemStarts.delete(item.id);
566
+ return;
567
+ }
568
+ if (item.type === 'webSearch') {
569
+ turn.push({
570
+ type: 'tool_use_end',
571
+ runId: turn.runId,
572
+ toolUseId: item.id,
573
+ status: 'success',
574
+ output: item,
575
+ ...(latencyMs !== undefined ? { latencyMs } : {}),
576
+ });
577
+ turn.itemStarts.delete(item.id);
578
+ }
579
+ }
580
+ sendRequest(method, params) {
581
+ if (!this.proc) {
582
+ return Promise.reject(new Error('codex app-server is not running.'));
583
+ }
584
+ const id = this.nextRequestId++;
585
+ const message = { jsonrpc: '2.0', id, method, params };
586
+ return new Promise((resolve, reject) => {
587
+ this.pending.set(id, { resolve, reject });
588
+ try {
589
+ this.proc.stdin.write(`${JSON.stringify(message)}\n`);
590
+ }
591
+ catch (error) {
592
+ this.pending.delete(id);
593
+ reject(error instanceof Error ? error : new Error(String(error)));
594
+ }
595
+ });
596
+ }
597
+ sendResponse(id, body) {
598
+ if (!this.proc)
599
+ return;
600
+ const envelope = { jsonrpc: '2.0', id, ...body };
601
+ try {
602
+ this.proc.stdin.write(`${JSON.stringify(envelope)}\n`);
603
+ }
604
+ catch (error) {
605
+ log.warn('codex_send_response_failed', {
606
+ message: error instanceof Error ? error.message : String(error),
607
+ });
608
+ }
609
+ }
610
+ async discoverMcpServers() {
611
+ const configPath = join(homedir(), '.codex', 'config.toml');
612
+ try {
613
+ const text = await readFile(configPath, 'utf-8');
614
+ const found = new Set();
615
+ const sectionRegex = /^\s*\[mcp_servers\.([^\]\s]+)\]/gm;
616
+ let match;
617
+ while ((match = sectionRegex.exec(text)) !== null) {
618
+ if (match[1])
619
+ found.add(match[1]);
620
+ }
621
+ return [...found].sort();
622
+ }
623
+ catch {
624
+ return [];
625
+ }
626
+ }
627
+ }
628
+ /**
629
+ * Codex sometimes wraps its server-side errors as a JSON string inside
630
+ * `turn.error.message` — e.g. `'{"detail":"…"}'`. Pull out the human-readable
631
+ * sentence so we don't dump raw JSON into the meeting UI.
632
+ */
633
+ function parseTurnErrorMessage(raw) {
634
+ if (typeof raw !== 'string')
635
+ return null;
636
+ const trimmed = raw.trim();
637
+ if (!trimmed)
638
+ return null;
639
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
640
+ try {
641
+ const parsed = JSON.parse(trimmed);
642
+ const detail = parsed.detail ?? parsed.message;
643
+ if (typeof detail === 'string' && detail.length > 0)
644
+ return detail;
645
+ }
646
+ catch {
647
+ // fall through to raw string
648
+ }
649
+ }
650
+ return trimmed;
651
+ }
652
+ //# sourceMappingURL=codex.js.map