ever-terminal 1.0.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.
@@ -0,0 +1,1091 @@
1
+ import { existsSync } from "node:fs";
2
+ import { debugLog } from "../debug.js";
3
+ import { mapCodexItemTypeToToolName, mapCodexRequestMethodToToolName, summarizeCodexItem, summarizeCodexRequest, } from "./summarize.js";
4
+ import { appendThreadMessage, recordThreadMeta } from "./memory.js";
5
+ function defaultCodexCwd() {
6
+ return process.env.PROJECT_DIR || process.cwd();
7
+ }
8
+ export class CodexSession {
9
+ client;
10
+ emit;
11
+ activeThreadId;
12
+ activeCwd;
13
+ _busy = false;
14
+ currentTurnId;
15
+ turnStartMs = 0;
16
+ runningInputTokens = 0;
17
+ runningOutputTokens = 0;
18
+ statsTimer = null;
19
+ pendingPermissions = [];
20
+ externallyResolvedPermissions = new Map();
21
+ pendingQuestions = [];
22
+ permissionTimeoutMs = 60000;
23
+ questionTimeoutMs = 120000;
24
+ assistantText = "";
25
+ turnStartedAt = 0;
26
+ emittedToolStarts = new Set();
27
+ emittedToolEnds = new Set();
28
+ idResolve = null;
29
+ idPromise = null;
30
+ idReadyCallbacks = [];
31
+ constructor(emit, client, opts) {
32
+ this.emit = emit;
33
+ this.client = client;
34
+ if (opts?.permissionTimeoutMs)
35
+ this.permissionTimeoutMs = opts.permissionTimeoutMs;
36
+ if (opts?.questionTimeoutMs)
37
+ this.questionTimeoutMs = opts.questionTimeoutMs;
38
+ }
39
+ get id() {
40
+ return this.activeThreadId;
41
+ }
42
+ get cwd() {
43
+ return this.activeCwd;
44
+ }
45
+ get busy() {
46
+ return this._busy;
47
+ }
48
+ get alive() {
49
+ return true;
50
+ }
51
+ /** Tracked session status: 'awaiting' if there's an unanswered permission
52
+ * request or user question, otherwise 'busy'/'idle' based on _busy. */
53
+ get status() {
54
+ if (this.pendingPermissions.length > 0 || this.pendingQuestions.length > 0) {
55
+ return "awaiting";
56
+ }
57
+ return this._busy ? "busy" : "idle";
58
+ }
59
+ get runningStats() {
60
+ return {
61
+ durationMs: this.turnStartMs ? Date.now() - this.turnStartMs : 0,
62
+ inputTokens: this.runningInputTokens,
63
+ outputTokens: this.runningOutputTokens,
64
+ };
65
+ }
66
+ waitForId(timeoutMs = 10000) {
67
+ if (this.activeThreadId)
68
+ return Promise.resolve(this.activeThreadId);
69
+ if (!this.idPromise) {
70
+ this.idPromise = new Promise((resolve) => {
71
+ this.idResolve = resolve;
72
+ });
73
+ }
74
+ return Promise.race([
75
+ this.idPromise,
76
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out waiting for session ID")), timeoutMs)),
77
+ ]);
78
+ }
79
+ onIdReady(cb) {
80
+ if (this.activeThreadId) {
81
+ cb(this.activeThreadId);
82
+ }
83
+ else {
84
+ this.idReadyCallbacks.push(cb);
85
+ }
86
+ }
87
+ setThreadId(id) {
88
+ if (this.activeThreadId === id)
89
+ return;
90
+ this.activeThreadId = id;
91
+ if (this.idResolve) {
92
+ this.idResolve(id);
93
+ this.idResolve = null;
94
+ }
95
+ for (const cb of this.idReadyCallbacks)
96
+ cb(id);
97
+ this.idReadyCallbacks = [];
98
+ }
99
+ send(msg) {
100
+ this.emit(this.activeThreadId ?? "", msg);
101
+ }
102
+ async start(sessionId, cwd) {
103
+ this.activeThreadId = sessionId;
104
+ const fallbackCwd = defaultCodexCwd();
105
+ let requestedCwd = cwd;
106
+ if (!requestedCwd && sessionId) {
107
+ try {
108
+ const thread = await this.client.threadRead(sessionId, false);
109
+ if (typeof thread?.cwd === "string" && thread.cwd)
110
+ requestedCwd = thread.cwd;
111
+ }
112
+ catch { }
113
+ }
114
+ requestedCwd ??= fallbackCwd;
115
+ this.activeCwd = existsSync(requestedCwd) ? requestedCwd : fallbackCwd;
116
+ if (this.activeThreadId) {
117
+ this.setThreadId(this.activeThreadId);
118
+ recordThreadMeta(this.activeThreadId, this.activeCwd);
119
+ }
120
+ console.log(`[codex-session] Configured: resume=${sessionId ?? "new"}, cwd=${this.activeCwd}`);
121
+ }
122
+ async run(prompt) {
123
+ if (this._busy) {
124
+ this.send({ type: "error", message: "Codex turn already running" });
125
+ return;
126
+ }
127
+ if (!this.activeCwd)
128
+ await this.start(undefined, defaultCodexCwd());
129
+ const cwd = this.activeCwd ?? defaultCodexCwd();
130
+ if (!this.activeThreadId) {
131
+ const thread = await this.client.threadStart({ cwd });
132
+ const newId = String(thread?.id ?? "");
133
+ if (!newId)
134
+ throw new Error("Failed to create Codex thread");
135
+ this.setThreadId(newId);
136
+ }
137
+ else {
138
+ await this.client.threadResume({
139
+ threadId: this.activeThreadId,
140
+ cwd,
141
+ });
142
+ }
143
+ const threadId = this.activeThreadId;
144
+ recordThreadMeta(threadId, cwd, prompt);
145
+ appendThreadMessage(threadId, "user", prompt);
146
+ this._busy = true;
147
+ this.turnStartMs = Date.now();
148
+ this.runningInputTokens = 0;
149
+ this.runningOutputTokens = 0;
150
+ this.stopStatsTimer();
151
+ this.statsTimer = setInterval(() => this.emitRunningStats(), 10000);
152
+ this.send({ type: "status", state: "busy", sessionId: threadId, provider: "codex" });
153
+ this.assistantText = "";
154
+ this.turnStartedAt = Date.now();
155
+ try {
156
+ const turn = await this.client.turnStart({
157
+ threadId,
158
+ cwd,
159
+ summary: "detailed",
160
+ input: [{ type: "text", text: prompt }],
161
+ });
162
+ this.currentTurnId = String(turn?.id ?? "");
163
+ if (!this.currentTurnId) {
164
+ this.finishTurn(false, "turn/start returned no turn ID");
165
+ return;
166
+ }
167
+ }
168
+ catch (err) {
169
+ this.finishTurn(false, String(err?.message ?? err ?? "turn/start failed"));
170
+ }
171
+ }
172
+ interrupt() {
173
+ const threadId = this.activeThreadId;
174
+ const turnId = this.currentTurnId;
175
+ if (threadId && turnId) {
176
+ this.client.turnInterrupt(threadId, turnId).catch(() => { });
177
+ }
178
+ }
179
+ async close() {
180
+ this.interrupt();
181
+ this.denyAllPendingPermissions();
182
+ this.clearPendingQuestions("skip");
183
+ }
184
+ async reset(cwd) {
185
+ await this.close();
186
+ this.activeThreadId = undefined;
187
+ this.activeCwd = cwd;
188
+ }
189
+ respondPermission(_decision) {
190
+ const pending = this.pendingPermissions.shift();
191
+ if (!pending)
192
+ return;
193
+ if (pending.timer)
194
+ clearTimeout(pending.timer);
195
+ const { requestId, method, params } = pending;
196
+ const requestedDecision = _decision === "allowAlways"
197
+ ? "allowAlways"
198
+ : _decision === "allow"
199
+ ? "allow"
200
+ : "deny";
201
+ const decision = normalizeCodexPermissionDecision(method, params, requestedDecision);
202
+ const payload = approvalPayload(method, params, decision);
203
+ this.respondPermissionPayload(requestId, method, decision, payload);
204
+ this.emitPermissionResult(method, params, decision);
205
+ if (decision === "deny") {
206
+ this.dropPendingPermissions();
207
+ }
208
+ else {
209
+ this.emitNextPermissionRequest();
210
+ }
211
+ }
212
+ respondQuestion(_answer) {
213
+ const pending = this.pendingQuestions.shift();
214
+ if (!pending)
215
+ return;
216
+ clearTimeout(pending.timer);
217
+ const { requestId, method, params } = pending;
218
+ const answer = (_answer || "skip").trim();
219
+ this.respondQuestionPayload(requestId, method, params, answer);
220
+ }
221
+ stopStatsTimer() {
222
+ if (this.statsTimer) {
223
+ clearInterval(this.statsTimer);
224
+ this.statsTimer = null;
225
+ }
226
+ }
227
+ emitRunningStats() {
228
+ if (!this._busy) {
229
+ this.stopStatsTimer();
230
+ return;
231
+ }
232
+ const stats = this.runningStats;
233
+ this.send({
234
+ type: "running_stats",
235
+ durationMs: stats.durationMs,
236
+ inputTokens: stats.inputTokens,
237
+ outputTokens: stats.outputTokens,
238
+ });
239
+ }
240
+ handleNotification(method, params) {
241
+ const p = params ?? {};
242
+ // turn/started — track turn ID
243
+ if (method === "turn/started") {
244
+ const turn = p.turn ?? {};
245
+ const turnId = String(turn.id ?? "");
246
+ if (turnId)
247
+ this.currentTurnId = turnId;
248
+ this.emittedToolStarts.clear();
249
+ this.emittedToolEnds.clear();
250
+ return;
251
+ }
252
+ if (method === "item/agentMessage/delta") {
253
+ const delta = String(p.delta ?? "");
254
+ if (delta) {
255
+ this.assistantText += delta;
256
+ this.send({ type: "text_delta", text: delta });
257
+ }
258
+ return;
259
+ }
260
+ if (method === "item/started") {
261
+ const item = p.item ?? {};
262
+ if (item.type === "userMessage") {
263
+ const content = Array.isArray(item.content) ? item.content : [];
264
+ const text = content.map((c) => c.text ?? "").join("").trim();
265
+ if (text)
266
+ this.send({ type: "user_prompt", text });
267
+ if (!this._busy) {
268
+ this._busy = true;
269
+ this.turnStartMs = Date.now();
270
+ this.runningInputTokens = 0;
271
+ this.runningOutputTokens = 0;
272
+ this.stopStatsTimer();
273
+ this.statsTimer = setInterval(() => this.emitRunningStats(), 10000);
274
+ this.assistantText = "";
275
+ this.turnStartedAt = Date.now();
276
+ this.send({
277
+ type: "status",
278
+ state: "busy",
279
+ sessionId: this.activeThreadId,
280
+ provider: "codex",
281
+ });
282
+ }
283
+ return;
284
+ }
285
+ if (item.type === "reasoning") {
286
+ this.send({ type: "status", state: "think_start", sessionId: this.activeThreadId, provider: "codex" });
287
+ }
288
+ else if (item.type === "agentMessage") {
289
+ this.send({ type: "status", state: "text_start", sessionId: this.activeThreadId, provider: "codex" });
290
+ }
291
+ else if (isToolLikeItem(item.type)) {
292
+ this.emitToolStart(item);
293
+ }
294
+ return;
295
+ }
296
+ if (method === "item/completed") {
297
+ const item = p.item ?? {};
298
+ this.emitExternalPermissionResult(item);
299
+ if (item.type === "reasoning") {
300
+ this.send({ type: "status", state: "think_end", sessionId: this.activeThreadId, provider: "codex" });
301
+ }
302
+ else if (item.type === "agentMessage") {
303
+ this.send({ type: "status", state: "text_end", sessionId: this.activeThreadId, provider: "codex" });
304
+ }
305
+ else if (isToolLikeItem(item.type)) {
306
+ this.emitToolEnd(item);
307
+ }
308
+ return;
309
+ }
310
+ if (method === "serverRequest/resolved") {
311
+ this.handleServerRequestResolved(p.requestId);
312
+ return;
313
+ }
314
+ if (method === "error") {
315
+ const err = p.error?.message ?? p.error ?? "";
316
+ if (err)
317
+ this.send({ type: "error", message: String(err) });
318
+ return;
319
+ }
320
+ if (method === "turn/completed") {
321
+ const turn = p.turn ?? {};
322
+ if (this.currentTurnId && String(turn.id ?? "") !== this.currentTurnId)
323
+ return;
324
+ const status = String(turn.status ?? "");
325
+ const turnErr = String(turn.error?.message ?? "");
326
+ const success = status === "completed";
327
+ const text = status === "interrupted"
328
+ ? "Interrupted by user"
329
+ : this.assistantText || turnErr || (success ? "" : "Turn failed");
330
+ const usage = turn.usage ?? {};
331
+ this.runningInputTokens = usage.inputTokens ?? usage.input_tokens ?? 0;
332
+ this.runningOutputTokens = usage.outputTokens ?? usage.output_tokens ?? 0;
333
+ this.finishTurn(success, text);
334
+ }
335
+ }
336
+ handleServerRequest(requestId, method, params) {
337
+ if (method === "item/commandExecution/requestApproval" ||
338
+ method === "item/fileChange/requestApproval" ||
339
+ method === "item/permissions/requestApproval" ||
340
+ method === "execCommandApproval" ||
341
+ method === "applyPatchApproval") {
342
+ this.pendingPermissions.push({
343
+ requestId,
344
+ method,
345
+ params,
346
+ timer: null,
347
+ visible: false,
348
+ });
349
+ const toolName = mapCodexRequestMethodToToolName(method);
350
+ console.log(`[codex-session] Permission request: requestId=${String(requestId)} method=${method} tool=${toolName} summary="${summarizeCodexRequest(params, method)}"`);
351
+ debugLog("codex-session", `permission request ${method}`, toOneLineJson(params ?? {}));
352
+ this.emitNextPermissionRequest();
353
+ return;
354
+ }
355
+ if (method === "item/tool/requestUserInput" ||
356
+ method === "mcpServer/elicitation/request") {
357
+ const requestKey = String(requestId);
358
+ const timer = setTimeout(() => {
359
+ this.autoRespondQuestion(requestKey, "skip");
360
+ }, this.questionTimeoutMs);
361
+ this.pendingQuestions.push({ requestId, method, params, timer });
362
+ const questions = normalizeQuestions(method, params);
363
+ console.log(`[codex-session] User question request: requestId=${requestKey} method=${method} questions=${questions.length}`);
364
+ debugLog("codex-session", `question request ${method}`, toOneLineJson(questions));
365
+ this.send({
366
+ type: "user_question",
367
+ questions,
368
+ toolUseId: String(requestId),
369
+ });
370
+ return;
371
+ }
372
+ }
373
+ handleClientClose(error) {
374
+ this.denyAllPendingPermissions();
375
+ this.clearPendingQuestions("skip");
376
+ if (this._busy) {
377
+ this.finishTurn(false, this.assistantText || error.message || "Codex process exited");
378
+ }
379
+ }
380
+ finishTurn(success, text) {
381
+ const threadId = this.activeThreadId;
382
+ if (!threadId || !this._busy)
383
+ return;
384
+ if (this.assistantText.trim()) {
385
+ appendThreadMessage(threadId, "assistant", this.assistantText.trim());
386
+ }
387
+ const durationMs = this.turnStartedAt ? Date.now() - this.turnStartedAt : 0;
388
+ this.currentTurnId = undefined;
389
+ this._busy = false;
390
+ this.stopStatsTimer();
391
+ this.turnStartedAt = 0;
392
+ this.send({
393
+ type: "result",
394
+ success,
395
+ text,
396
+ sessionId: threadId,
397
+ costUsd: 0,
398
+ turns: 1,
399
+ durationMs,
400
+ inputTokens: this.runningInputTokens,
401
+ outputTokens: this.runningOutputTokens,
402
+ provider: "codex",
403
+ });
404
+ this.send({ type: "status", state: "idle", sessionId: threadId, provider: "codex" });
405
+ }
406
+ denyAllPendingPermissions() {
407
+ while (this.pendingPermissions.length > 0) {
408
+ const pending = this.pendingPermissions.shift();
409
+ if (pending.timer)
410
+ clearTimeout(pending.timer);
411
+ const payload = approvalPayload(pending.method, pending.params, "deny");
412
+ this.respondPermissionPayload(pending.requestId, pending.method, "deny", payload);
413
+ }
414
+ }
415
+ clearPendingQuestions(answer) {
416
+ while (this.pendingQuestions.length > 0) {
417
+ const pending = this.pendingQuestions.shift();
418
+ clearTimeout(pending.timer);
419
+ this.respondQuestionPayload(pending.requestId, pending.method, pending.params, answer);
420
+ }
421
+ }
422
+ respondQuestionPayload(requestId, method, params, answer) {
423
+ if (method === "item/tool/requestUserInput") {
424
+ const questions = Array.isArray(params?.questions) ? params.questions : [];
425
+ const parsed = parseQuestionAnswers(questions, answer);
426
+ this.client.respondToServerRequest(requestId, { answers: parsed.rpcAnswers });
427
+ this.send({ type: "question_answer", answers: parsed.displayAnswers });
428
+ return;
429
+ }
430
+ if (method === "mcpServer/elicitation/request") {
431
+ this.client.respondToServerRequest(requestId, {
432
+ action: answer === "skip" ? "decline" : "accept",
433
+ content: null,
434
+ });
435
+ return;
436
+ }
437
+ this.client.respondToServerRequest(requestId, { action: "decline" });
438
+ }
439
+ autoRespondPermission(toolUseId, decision) {
440
+ const idx = this.pendingPermissions.findIndex((pending) => String(pending.requestId) === toolUseId);
441
+ if (idx === -1)
442
+ return;
443
+ const pending = this.pendingPermissions.splice(idx, 1)[0];
444
+ if (pending.timer)
445
+ clearTimeout(pending.timer);
446
+ const payload = approvalPayload(pending.method, pending.params, decision);
447
+ this.respondPermissionPayload(pending.requestId, pending.method, decision, payload);
448
+ this.emitPermissionResult(pending.method, pending.params, decision);
449
+ if (decision === "deny") {
450
+ this.dropPendingPermissions();
451
+ }
452
+ else {
453
+ this.emitNextPermissionRequest();
454
+ }
455
+ }
456
+ dropPendingPermissions() {
457
+ for (const p of this.pendingPermissions) {
458
+ if (p.timer)
459
+ clearTimeout(p.timer);
460
+ }
461
+ this.pendingPermissions.length = 0;
462
+ }
463
+ respondPermissionPayload(requestId, method, decision, payload) {
464
+ const log = toOneLineJson({ requestId, method, decision, payload });
465
+ console.log(`[codex-session] Permission response to app-server: ${log}`);
466
+ debugLog("codex-session", "permission response payload", log);
467
+ this.client.respondToServerRequest(requestId, payload);
468
+ }
469
+ handleServerRequestResolved(requestId) {
470
+ const idx = this.pendingPermissions.findIndex((pending) => String(pending.requestId) === String(requestId));
471
+ if (idx === -1)
472
+ return;
473
+ const pending = this.pendingPermissions.splice(idx, 1)[0];
474
+ if (pending.timer)
475
+ clearTimeout(pending.timer);
476
+ const itemId = typeof pending.params?.itemId === "string" ? pending.params.itemId : "";
477
+ if (itemId) {
478
+ this.externallyResolvedPermissions.set(itemId, {
479
+ method: pending.method,
480
+ params: pending.params,
481
+ });
482
+ return;
483
+ }
484
+ this.emitNextPermissionRequest();
485
+ }
486
+ emitExternalPermissionResult(item) {
487
+ const itemId = typeof item?.id === "string" ? item.id : "";
488
+ if (!itemId)
489
+ return;
490
+ const resolved = this.externallyResolvedPermissions.get(itemId);
491
+ if (!resolved)
492
+ return;
493
+ this.externallyResolvedPermissions.delete(itemId);
494
+ this.send({
495
+ type: "permission_result",
496
+ toolName: mapCodexRequestMethodToToolName(resolved.method),
497
+ summary: summarizeCodexRequest(resolved.params, resolved.method),
498
+ decision: item?.status === "declined" ? "denied" : "allowed",
499
+ });
500
+ this.emitNextPermissionRequest();
501
+ }
502
+ emitNextPermissionRequest() {
503
+ const pending = this.pendingPermissions[0];
504
+ if (!pending || pending.visible)
505
+ return;
506
+ pending.visible = true;
507
+ pending.timer = setTimeout(() => {
508
+ this.autoRespondPermission(String(pending.requestId), "deny");
509
+ }, this.permissionTimeoutMs);
510
+ const description = summarizeCodexRequest(pending.params, pending.method);
511
+ this.send({
512
+ type: "permission_request",
513
+ toolName: mapCodexRequestMethodToToolName(pending.method),
514
+ description,
515
+ detail: extractRequestDetail(pending.params),
516
+ toolUseId: String(pending.requestId),
517
+ options: buildCodexPermissionOptions(pending.method, pending.params, description),
518
+ suggestions: buildCodexPermissionSuggestions(pending.method, pending.params),
519
+ });
520
+ }
521
+ emitToolStart(item) {
522
+ const toolId = String(item?.id ?? "");
523
+ if (toolId && this.emittedToolStarts.has(toolId))
524
+ return;
525
+ if (toolId)
526
+ this.emittedToolStarts.add(toolId);
527
+ this.send({
528
+ type: "tool_start",
529
+ name: mapCodexItemTypeToToolName(item.type),
530
+ toolId,
531
+ });
532
+ }
533
+ emitToolEnd(item) {
534
+ const toolId = String(item?.id ?? "");
535
+ if (toolId && this.emittedToolEnds.has(toolId))
536
+ return;
537
+ if (toolId)
538
+ this.emittedToolEnds.add(toolId);
539
+ this.send({
540
+ type: "tool_end",
541
+ name: mapCodexItemTypeToToolName(item.type),
542
+ toolId,
543
+ summary: summarizeCodexItem(item),
544
+ detail: extractItemDetail(item),
545
+ });
546
+ }
547
+ emitPermissionResult(method, params, decision) {
548
+ this.send({
549
+ type: "permission_result",
550
+ toolName: mapCodexRequestMethodToToolName(method),
551
+ summary: summarizeCodexRequest(params, method),
552
+ decision: decision === "allowAlways"
553
+ ? "always"
554
+ : decision === "allow"
555
+ ? "allowed"
556
+ : "denied",
557
+ });
558
+ }
559
+ autoRespondQuestion(toolUseId, answer) {
560
+ const idx = this.pendingQuestions.findIndex((pending) => String(pending.requestId) === toolUseId);
561
+ if (idx === -1)
562
+ return;
563
+ const pending = this.pendingQuestions.splice(idx, 1)[0];
564
+ clearTimeout(pending.timer);
565
+ this.respondQuestionPayload(pending.requestId, pending.method, pending.params, answer);
566
+ }
567
+ }
568
+ // ── Helpers ──────────────────────────────────────────────
569
+ function toOneLineJson(value, maxLen = 1500) {
570
+ try {
571
+ const text = JSON.stringify(value);
572
+ if (!text)
573
+ return String(value);
574
+ return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
575
+ }
576
+ catch {
577
+ return String(value);
578
+ }
579
+ }
580
+ function isToolLikeItem(type) {
581
+ if (typeof type !== "string")
582
+ return false;
583
+ return (type === "commandExecution" ||
584
+ type === "fileChange" ||
585
+ type === "mcpToolCall" ||
586
+ type === "dynamicToolCall" ||
587
+ type === "webSearch" ||
588
+ type === "imageView" ||
589
+ type === "imageGeneration" ||
590
+ type === "collabAgentToolCall");
591
+ }
592
+ function extractItemDetail(item) {
593
+ const type = String(item?.type ?? "");
594
+ if (type === "commandExecution") {
595
+ const input = {};
596
+ if (item.command)
597
+ input.command = item.command;
598
+ if (item.cwd)
599
+ input.cwd = item.cwd;
600
+ const parts = [];
601
+ if (item.aggregatedOutput)
602
+ parts.push(String(item.aggregatedOutput));
603
+ if (item.exitCode !== undefined)
604
+ parts.push(`exit code: ${item.exitCode}`);
605
+ return { input, output: parts.join("\n") || undefined };
606
+ }
607
+ if (type === "fileChange") {
608
+ const changes = Array.isArray(item.changes) ? item.changes : [];
609
+ const input = {
610
+ files: changes.map((c) => ({ path: c.path, kind: c.kind })),
611
+ };
612
+ const diffs = changes.map((c) => c.diff).filter(Boolean);
613
+ return { input, output: diffs.join("\n") || undefined };
614
+ }
615
+ if (type === "mcpToolCall") {
616
+ const input = {};
617
+ if (item.server)
618
+ input.server = item.server;
619
+ if (item.tool)
620
+ input.tool = item.tool;
621
+ if (item.arguments)
622
+ input.arguments = item.arguments;
623
+ return { input, output: item.result ? String(item.result) : undefined };
624
+ }
625
+ return undefined;
626
+ }
627
+ export function buildCodexPermissionOptions(method, params, description) {
628
+ if (method === "item/commandExecution/requestApproval") {
629
+ return buildCodexCommandPermissionOptions(params);
630
+ }
631
+ const options = [{ text: "Yes", key: "allow" }];
632
+ if (canCodexAllowAlways(method, params)) {
633
+ options.push({
634
+ text: describeCodexAlwaysOption(method, params, description),
635
+ key: "allowAlways",
636
+ });
637
+ }
638
+ options.push({ text: "No", key: "deny" });
639
+ return options;
640
+ }
641
+ function buildCodexCommandPermissionOptions(params) {
642
+ const networkApprovalContext = params?.networkApprovalContext ?? params?.network_approval_context;
643
+ const additionalPermissions = params?.additionalPermissions ?? params?.additional_permissions;
644
+ const availableDecisions = effectiveCodexCommandDecisions(params);
645
+ const options = [];
646
+ for (const decision of availableDecisions) {
647
+ if (decision === "accept") {
648
+ pushCodexPermissionOption(options, {
649
+ text: networkApprovalContext ? "Yes, just this once" : "Yes, proceed",
650
+ key: "allow",
651
+ });
652
+ continue;
653
+ }
654
+ if (decision === "acceptForSession") {
655
+ pushCodexPermissionOption(options, {
656
+ text: networkApprovalContext
657
+ ? "Yes, and allow this host for this conversation"
658
+ : additionalPermissions
659
+ ? "Yes, and allow these permissions for this session"
660
+ : "Yes, and don't ask again for this command in this session",
661
+ key: "allowAlways",
662
+ });
663
+ continue;
664
+ }
665
+ const execpolicyAmendment = decision?.acceptWithExecpolicyAmendment?.execpolicy_amendment;
666
+ if (execpolicyAmendment) {
667
+ const renderedPrefix = renderCodexCommandPrefix(execpolicyAmendment);
668
+ if (!renderedPrefix.includes("\n") && !renderedPrefix.includes("\r")) {
669
+ pushCodexPermissionOption(options, {
670
+ text: `Yes, and don't ask again for commands that start with ${quoteInline(renderedPrefix)}`,
671
+ key: "allowAlways",
672
+ });
673
+ }
674
+ continue;
675
+ }
676
+ const networkAmendment = decision?.applyNetworkPolicyAmendment?.network_policy_amendment;
677
+ if (networkAmendment) {
678
+ if (networkAmendment?.action === "deny") {
679
+ pushCodexPermissionOption(options, {
680
+ text: "No, and block this host in the future",
681
+ key: "deny",
682
+ });
683
+ }
684
+ else {
685
+ pushCodexPermissionOption(options, {
686
+ text: "Yes, and allow this host in the future",
687
+ key: "allowAlways",
688
+ });
689
+ }
690
+ continue;
691
+ }
692
+ if (decision === "decline") {
693
+ pushCodexPermissionOption(options, {
694
+ text: "No, continue without running it",
695
+ key: "deny",
696
+ });
697
+ continue;
698
+ }
699
+ if (decision === "cancel") {
700
+ pushCodexPermissionOption(options, {
701
+ text: "No, and tell Codex what to do differently",
702
+ key: "deny",
703
+ });
704
+ }
705
+ }
706
+ return options.length > 0
707
+ ? options
708
+ : [
709
+ { text: "Yes, proceed", key: "allow" },
710
+ { text: "No, and tell Codex what to do differently", key: "deny" },
711
+ ];
712
+ }
713
+ function pushCodexPermissionOption(options, option) {
714
+ if (!options.some((existing) => existing.key === option.key)) {
715
+ options.push(option);
716
+ }
717
+ }
718
+ function buildCodexPermissionSuggestions(method, params) {
719
+ if (method === "item/commandExecution/requestApproval") {
720
+ const suggestions = [];
721
+ if (Array.isArray(params?.availableDecisions)) {
722
+ suggestions.push({ type: "availableDecisions", decisions: params.availableDecisions });
723
+ }
724
+ const execpolicyAmendment = params?.proposedExecPolicyAmendment ?? params?.proposedExecpolicyAmendment;
725
+ if (execpolicyAmendment) {
726
+ suggestions.push({ type: "proposedExecpolicyAmendment", amendment: execpolicyAmendment });
727
+ }
728
+ if (Array.isArray(params?.proposedNetworkPolicyAmendments)) {
729
+ suggestions.push({
730
+ type: "proposedNetworkPolicyAmendments",
731
+ amendments: params.proposedNetworkPolicyAmendments,
732
+ });
733
+ }
734
+ return suggestions.length > 0 ? suggestions : null;
735
+ }
736
+ if (method === "item/fileChange/requestApproval") {
737
+ return params?.grantRoot ? [{ type: "grantRoot", root: params.grantRoot }] : null;
738
+ }
739
+ if (method === "item/permissions/requestApproval") {
740
+ return [{ type: "permissions", permissions: params?.permissions ?? {} }];
741
+ }
742
+ return null;
743
+ }
744
+ function canCodexAllowAlways(method, params) {
745
+ if (method === "item/permissions/requestApproval")
746
+ return true;
747
+ if (method === "item/commandExecution/requestApproval") {
748
+ return effectiveCodexCommandDecisions(params).some((decision) => decision === "acceptForSession" ||
749
+ Boolean(decision?.acceptWithExecpolicyAmendment) ||
750
+ Boolean(decision?.applyNetworkPolicyAmendment));
751
+ }
752
+ if (method === "item/fileChange/requestApproval")
753
+ return true;
754
+ return method === "execCommandApproval" || method === "applyPatchApproval";
755
+ }
756
+ function describeCodexAlwaysOption(method, params, placeholderText) {
757
+ if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
758
+ const execpolicyAmendment = extractCodexExecpolicyAmendment(params);
759
+ if (execpolicyAmendment) {
760
+ return `Yes, and don't ask again for command rule ${quoteInline(formatCodexRule(execpolicyAmendment))}`;
761
+ }
762
+ const networkAmendment = extractCodexNetworkPolicyAmendment(params);
763
+ if (networkAmendment) {
764
+ return `Yes, and remember network access ${quoteInline(formatCodexNetworkAmendment(networkAmendment))}`;
765
+ }
766
+ return "Yes, and don't ask again for similar commands this session";
767
+ }
768
+ if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
769
+ return params?.grantRoot
770
+ ? `Yes, and allow file changes under ${quoteInline(params.grantRoot)} this session`
771
+ : "Yes, and allow file changes for this session";
772
+ }
773
+ if (method === "item/permissions/requestApproval") {
774
+ return `Yes, and allow ${describeCodexPermissions(params?.permissions)} for this session`;
775
+ }
776
+ return placeholderText;
777
+ }
778
+ function extractCodexExecpolicyAmendment(params) {
779
+ const available = Array.isArray(params?.availableDecisions) ? params.availableDecisions : [];
780
+ for (const decision of available) {
781
+ const amendment = decision?.acceptWithExecpolicyAmendment?.execpolicy_amendment;
782
+ if (amendment)
783
+ return amendment;
784
+ }
785
+ return params?.proposedExecPolicyAmendment ?? params?.proposedExecpolicyAmendment;
786
+ }
787
+ function extractCodexNetworkPolicyAmendment(params) {
788
+ const available = Array.isArray(params?.availableDecisions) ? params.availableDecisions : [];
789
+ for (const decision of available) {
790
+ const amendment = decision?.applyNetworkPolicyAmendment?.network_policy_amendment;
791
+ if (amendment)
792
+ return amendment;
793
+ }
794
+ const amendments = Array.isArray(params?.proposedNetworkPolicyAmendments)
795
+ ? params.proposedNetworkPolicyAmendments
796
+ : [];
797
+ return amendments[0];
798
+ }
799
+ function formatCodexRule(rule) {
800
+ return renderCodexCommandPrefix(rule);
801
+ }
802
+ function renderCodexCommandPrefix(command) {
803
+ const parts = Array.isArray(command) ? command.map((part) => String(part)) : [String(command)];
804
+ const shellScript = extractCodexShellScript(parts);
805
+ return shellScript ?? shellEscapeCommand(parts);
806
+ }
807
+ function extractCodexShellScript(command) {
808
+ if (command.length < 3)
809
+ return null;
810
+ const shell = command[0]?.split(/[\\/]/g).pop();
811
+ if (shell !== "bash" && shell !== "zsh")
812
+ return null;
813
+ const scriptIndex = command.findIndex((part, idx) => idx > 0 && (part === "-c" || part === "-lc"));
814
+ if (scriptIndex === -1 || scriptIndex + 1 >= command.length)
815
+ return null;
816
+ return command[scriptIndex + 1] ?? null;
817
+ }
818
+ function shellEscapeCommand(command) {
819
+ return command.map(shellEscapeArg).join(" ");
820
+ }
821
+ function shellEscapeArg(arg) {
822
+ if (arg === "")
823
+ return "''";
824
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg))
825
+ return arg;
826
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
827
+ }
828
+ function formatCodexNetworkAmendment(amendment) {
829
+ if (typeof amendment === "string")
830
+ return amendment;
831
+ if (amendment && typeof amendment === "object") {
832
+ const host = String(amendment.host ?? amendment.domain ?? amendment.url ?? "network");
833
+ const action = amendment.action ? ` ${String(amendment.action)}` : "";
834
+ return `${host}${action}`;
835
+ }
836
+ return String(amendment);
837
+ }
838
+ function describeCodexPermissions(permissions) {
839
+ const parts = [];
840
+ const fileSystem = permissions?.fileSystem ?? permissions?.filesystem;
841
+ const write = fileSystem?.write;
842
+ if (Array.isArray(write) && write.length > 0) {
843
+ parts.push(`write access to ${write.map((path) => quoteInline(path)).join(", ")}`);
844
+ }
845
+ const read = fileSystem?.read;
846
+ if (Array.isArray(read) && read.length > 0) {
847
+ parts.push(`read access to ${read.map((path) => quoteInline(path)).join(", ")}`);
848
+ }
849
+ if (permissions?.network?.enabled === true) {
850
+ parts.push("network access");
851
+ }
852
+ return parts.length > 0 ? parts.join(" and ") : "requested permissions";
853
+ }
854
+ function quoteInline(value) {
855
+ return `\`${String(value)}\``;
856
+ }
857
+ function normalizeCodexPermissionDecision(method, params, requested) {
858
+ const offered = new Set(buildCodexPermissionOptions(method, params, "").map((option) => option.key));
859
+ if (offered.has(requested))
860
+ return requested;
861
+ if (requested === "allowAlways" && offered.has("allow"))
862
+ return "allow";
863
+ if (offered.has("deny"))
864
+ return "deny";
865
+ return offered.has("allow") ? "allow" : "deny";
866
+ }
867
+ export function approvalPayload(method, params, decision) {
868
+ const requested = decision === "allowAlways" ? "allowAlways" : decision === "allow" ? "allow" : "deny";
869
+ const normalized = normalizeCodexPermissionDecision(method, params, requested);
870
+ if (method === "item/commandExecution/requestApproval" ||
871
+ method === "item/fileChange/requestApproval") {
872
+ const sessionDecision = method === "item/commandExecution/requestApproval"
873
+ ? commandAllowAlwaysDecision(params)
874
+ : "acceptForSession";
875
+ return {
876
+ decision: normalized === "allowAlways"
877
+ ? sessionDecision
878
+ : normalized === "allow"
879
+ ? "accept"
880
+ : method === "item/commandExecution/requestApproval"
881
+ ? commandDenyDecision(params)
882
+ : "cancel",
883
+ };
884
+ }
885
+ if (method === "item/permissions/requestApproval") {
886
+ return normalized === "deny"
887
+ ? { permissions: {} }
888
+ : {
889
+ permissions: params?.permissions ?? {},
890
+ scope: normalized === "allowAlways" ? "session" : "turn",
891
+ };
892
+ }
893
+ if (method === "execCommandApproval" || method === "applyPatchApproval") {
894
+ return {
895
+ decision: normalized === "allowAlways"
896
+ ? "approved_for_session"
897
+ : normalized === "allow"
898
+ ? "approved"
899
+ : "denied",
900
+ };
901
+ }
902
+ return { decision: normalized === "deny" ? "decline" : "accept" };
903
+ }
904
+ function commandAllowAlwaysDecision(params) {
905
+ const available = effectiveCodexCommandDecisions(params);
906
+ for (const decision of available) {
907
+ if (decision === "acceptForSession")
908
+ return "acceptForSession";
909
+ if (decision?.acceptWithExecpolicyAmendment) {
910
+ const amendment = decision.acceptWithExecpolicyAmendment.execpolicy_amendment;
911
+ const renderedPrefix = renderCodexCommandPrefix(amendment);
912
+ if (!renderedPrefix.includes("\n") && !renderedPrefix.includes("\r"))
913
+ return decision;
914
+ }
915
+ const networkAmendment = decision?.applyNetworkPolicyAmendment?.network_policy_amendment;
916
+ if (networkAmendment?.action !== "deny" &&
917
+ decision?.applyNetworkPolicyAmendment)
918
+ return decision;
919
+ }
920
+ const execpolicyAmendment = params?.proposedExecPolicyAmendment ?? params?.proposedExecpolicyAmendment;
921
+ if (execpolicyAmendment) {
922
+ return {
923
+ acceptWithExecpolicyAmendment: {
924
+ execpolicy_amendment: execpolicyAmendment,
925
+ },
926
+ };
927
+ }
928
+ const networkPolicyAmendments = Array.isArray(params?.proposedNetworkPolicyAmendments)
929
+ ? params.proposedNetworkPolicyAmendments
930
+ : [];
931
+ if (networkPolicyAmendments.length === 1) {
932
+ return {
933
+ applyNetworkPolicyAmendment: {
934
+ network_policy_amendment: networkPolicyAmendments[0],
935
+ },
936
+ };
937
+ }
938
+ return "acceptForSession";
939
+ }
940
+ function commandDenyDecision(params) {
941
+ const available = effectiveCodexCommandDecisions(params);
942
+ const networkDeny = available.find((decision) => decision?.applyNetworkPolicyAmendment?.network_policy_amendment?.action === "deny");
943
+ if (networkDeny)
944
+ return networkDeny;
945
+ if (available.includes("decline"))
946
+ return "decline";
947
+ if (available.includes("cancel"))
948
+ return "cancel";
949
+ return "decline";
950
+ }
951
+ function effectiveCodexCommandDecisions(params) {
952
+ if (Array.isArray(params?.availableDecisions))
953
+ return params.availableDecisions;
954
+ const networkApprovalContext = params?.networkApprovalContext ?? params?.network_approval_context;
955
+ const additionalPermissions = params?.additionalPermissions ?? params?.additional_permissions;
956
+ const proposedExecpolicyAmendment = params?.proposedExecPolicyAmendment ?? params?.proposedExecpolicyAmendment;
957
+ const proposedNetworkPolicyAmendments = Array.isArray(params?.proposedNetworkPolicyAmendments)
958
+ ? params.proposedNetworkPolicyAmendments
959
+ : [];
960
+ if (networkApprovalContext) {
961
+ const decisions = ["accept", "acceptForSession"];
962
+ for (const amendment of proposedNetworkPolicyAmendments) {
963
+ if (amendment?.action === "allow") {
964
+ decisions.push({
965
+ applyNetworkPolicyAmendment: { network_policy_amendment: amendment },
966
+ });
967
+ break;
968
+ }
969
+ }
970
+ decisions.push("cancel");
971
+ return decisions;
972
+ }
973
+ if (additionalPermissions)
974
+ return ["accept", "cancel"];
975
+ const decisions = ["accept"];
976
+ if (proposedExecpolicyAmendment) {
977
+ decisions.push({
978
+ acceptWithExecpolicyAmendment: {
979
+ execpolicy_amendment: proposedExecpolicyAmendment,
980
+ },
981
+ });
982
+ }
983
+ decisions.push("cancel");
984
+ return decisions;
985
+ }
986
+ function extractRequestDetail(params) {
987
+ const cmd = typeof params?.command === "string"
988
+ ? params.command
989
+ : Array.isArray(params?.command)
990
+ ? params.command.join(" ")
991
+ : "";
992
+ if (cmd)
993
+ return cmd.slice(0, 200);
994
+ if (typeof params?.reason === "string" && params.reason.trim()) {
995
+ return params.reason.slice(0, 200);
996
+ }
997
+ if (typeof params?.cwd === "string" && params.cwd.trim()) {
998
+ return params.cwd.slice(0, 200);
999
+ }
1000
+ return "";
1001
+ }
1002
+ function normalizeQuestions(method, params) {
1003
+ if (method === "item/tool/requestUserInput") {
1004
+ const qs = Array.isArray(params?.questions) ? params.questions : [];
1005
+ return qs.map((q) => ({
1006
+ question: String(q?.question ?? ""),
1007
+ header: String(q?.header ?? ""),
1008
+ options: Array.isArray(q?.options)
1009
+ ? q.options.map((o) => ({
1010
+ label: String(o?.label ?? ""),
1011
+ description: String(o?.description ?? ""),
1012
+ preview: "",
1013
+ }))
1014
+ : [],
1015
+ }));
1016
+ }
1017
+ if (method === "mcpServer/elicitation/request") {
1018
+ return [
1019
+ {
1020
+ question: String(params?.message ?? "MCP server asks for input"),
1021
+ header: "MCP Input",
1022
+ options: [
1023
+ { label: "accept", description: "Provide acceptance", preview: "" },
1024
+ { label: "decline", description: "Decline request", preview: "" },
1025
+ ],
1026
+ },
1027
+ ];
1028
+ }
1029
+ return [];
1030
+ }
1031
+ function parseQuestionAnswers(questions, answer) {
1032
+ const rpcAnswers = {};
1033
+ const displayAnswers = {};
1034
+ let parsedAnswer = null;
1035
+ if (answer && answer !== "skip") {
1036
+ try {
1037
+ const parsed = JSON.parse(answer);
1038
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1039
+ parsedAnswer = parsed;
1040
+ }
1041
+ }
1042
+ catch {
1043
+ parsedAnswer = null;
1044
+ }
1045
+ }
1046
+ const split = answer
1047
+ .split(",")
1048
+ .map((s) => s.trim())
1049
+ .filter(Boolean);
1050
+ for (let i = 0; i < questions.length; i++) {
1051
+ const question = questions[i] ?? {};
1052
+ const qid = String(question.id ?? `q${i + 1}`);
1053
+ const displayKey = String(question.question ?? question.header ?? qid);
1054
+ const value = extractAnswerValue(parsedAnswer, question, qid, displayKey);
1055
+ const normalized = value ?? split[i] ?? split[0] ?? "skip";
1056
+ rpcAnswers[qid] = { answers: [normalized] };
1057
+ displayAnswers[displayKey] = normalized;
1058
+ }
1059
+ return { rpcAnswers, displayAnswers };
1060
+ }
1061
+ function extractAnswerValue(parsedAnswer, question, qid, displayKey) {
1062
+ if (!parsedAnswer)
1063
+ return null;
1064
+ const direct = parsedAnswer[qid] ??
1065
+ parsedAnswer[displayKey] ??
1066
+ parsedAnswer[String(question?.header ?? "")];
1067
+ return normalizeAnswerValue(direct);
1068
+ }
1069
+ function normalizeAnswerValue(value) {
1070
+ if (typeof value === "string") {
1071
+ const trimmed = value.trim();
1072
+ return trimmed || null;
1073
+ }
1074
+ if (Array.isArray(value)) {
1075
+ for (const item of value) {
1076
+ const normalized = normalizeAnswerValue(item);
1077
+ if (normalized)
1078
+ return normalized;
1079
+ }
1080
+ return null;
1081
+ }
1082
+ if (value && typeof value === "object") {
1083
+ const maybeAnswers = value.answers;
1084
+ if (Array.isArray(maybeAnswers) && maybeAnswers.length > 0) {
1085
+ return normalizeAnswerValue(maybeAnswers[0]);
1086
+ }
1087
+ const maybeAnswer = value.answer;
1088
+ return normalizeAnswerValue(maybeAnswer);
1089
+ }
1090
+ return null;
1091
+ }