flockbay 0.10.15

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 (80) hide show
  1. package/README.md +56 -0
  2. package/bin/flockbay-mcp.mjs +56 -0
  3. package/bin/flockbay.mjs +78 -0
  4. package/dist/codex/flockbayMcpStdioBridge.cjs +383 -0
  5. package/dist/codex/flockbayMcpStdioBridge.d.cts +2 -0
  6. package/dist/codex/flockbayMcpStdioBridge.d.mts +2 -0
  7. package/dist/codex/flockbayMcpStdioBridge.mjs +381 -0
  8. package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +136 -0
  9. package/dist/flockbayScreenshotGate-DkxU24cR.cjs +138 -0
  10. package/dist/index--o4BPz5o.cjs +10311 -0
  11. package/dist/index-CUp3juDS.mjs +10268 -0
  12. package/dist/index.cjs +43 -0
  13. package/dist/index.d.cts +1 -0
  14. package/dist/index.d.mts +1 -0
  15. package/dist/index.mjs +40 -0
  16. package/dist/lib.cjs +33 -0
  17. package/dist/lib.d.cts +957 -0
  18. package/dist/lib.d.mts +957 -0
  19. package/dist/lib.mjs +23 -0
  20. package/dist/runCodex-D3eT-TvB.cjs +3449 -0
  21. package/dist/runCodex-o6PCbHQ7.mjs +3446 -0
  22. package/dist/runGemini-Bt0oEj_g.mjs +3183 -0
  23. package/dist/runGemini-CBxZp6I7.cjs +3185 -0
  24. package/dist/types-C-jnUdn_.cjs +4498 -0
  25. package/dist/types-DGd6ea2Z.mjs +4450 -0
  26. package/kits/kit.open_world/kit.json +59 -0
  27. package/package.json +130 -0
  28. package/scripts/claude_local_launcher.cjs +73 -0
  29. package/scripts/claude_remote_launcher.cjs +16 -0
  30. package/scripts/claude_version_utils.cjs +391 -0
  31. package/scripts/ripgrep_launcher.cjs +33 -0
  32. package/scripts/session_hook_forwarder.cjs +49 -0
  33. package/scripts/test-codex-abort-history.mjs +77 -0
  34. package/scripts/unpack-tools.cjs +222 -0
  35. package/tools/licenses/difftastic-LICENSE +21 -0
  36. package/tools/licenses/ripgrep-LICENSE +3 -0
  37. package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -0
  38. package/tools/unreal-mcp/upstream/Docs/README.md +8 -0
  39. package/tools/unreal-mcp/upstream/Docs/Tools/README.md +7 -0
  40. package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +184 -0
  41. package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -0
  42. package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -0
  43. package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -0
  44. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Config/FilterPlugin.ini +8 -0
  45. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1160 -0
  46. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +924 -0
  47. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +709 -0
  48. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +896 -0
  49. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPProjectCommands.cpp +72 -0
  50. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPUMGCommands.cpp +544 -0
  51. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +321 -0
  52. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +419 -0
  53. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPModule.cpp +21 -0
  54. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +34 -0
  55. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +27 -0
  56. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -0
  57. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +40 -0
  58. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -0
  59. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -0
  60. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/MCPServerRunnable.h +34 -0
  61. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPBridge.h +64 -0
  62. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPModule.h +22 -0
  63. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +78 -0
  64. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/UnrealMCP.uplugin +36 -0
  65. package/tools/unreal-mcp/upstream/Python/README.md +40 -0
  66. package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -0
  67. package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +203 -0
  68. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +497 -0
  69. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +194 -0
  70. package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +267 -0
  71. package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +618 -0
  72. package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +366 -0
  73. package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +390 -0
  74. package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +420 -0
  75. package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -0
  76. package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +430 -0
  77. package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +64 -0
  78. package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +333 -0
  79. package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -0
  80. package/tools/unreal-mcp/upstream/Python/uv.lock +521 -0
@@ -0,0 +1,3449 @@
1
+ 'use strict';
2
+
3
+ var ink = require('ink');
4
+ var React = require('react');
5
+ var types = require('./types-C-jnUdn_.cjs');
6
+ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
7
+ var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
+ var z = require('zod');
9
+ var types_js = require('@modelcontextprotocol/sdk/types.js');
10
+ var node_crypto = require('node:crypto');
11
+ var fs = require('node:fs');
12
+ var os = require('node:os');
13
+ var path = require('node:path');
14
+ var node_child_process = require('node:child_process');
15
+ var index = require('./index--o4BPz5o.cjs');
16
+ var flockbayScreenshotGate = require('./flockbayScreenshotGate-DkxU24cR.cjs');
17
+ require('axios');
18
+ require('chalk');
19
+ require('fs');
20
+ require('node:fs/promises');
21
+ require('tweetnacl');
22
+ require('node:events');
23
+ require('socket.io-client');
24
+ require('child_process');
25
+ require('fs/promises');
26
+ require('crypto');
27
+ require('path');
28
+ require('url');
29
+ require('node:process');
30
+ require('os');
31
+ require('node:net');
32
+ require('expo-server-sdk');
33
+ require('node:readline');
34
+ require('node:url');
35
+ require('ps-list');
36
+ require('cross-spawn');
37
+ require('tmp');
38
+ require('qrcode-terminal');
39
+ require('open');
40
+ require('fastify');
41
+ require('fastify-type-provider-zod');
42
+ require('@modelcontextprotocol/sdk/server/mcp.js');
43
+ require('node:http');
44
+ require('@modelcontextprotocol/sdk/server/streamableHttp.js');
45
+ require('http');
46
+ require('util');
47
+
48
+ function normalizeRequestedSchema(value) {
49
+ const unwrap = (obj) => {
50
+ const candidates = [
51
+ obj?.schema,
52
+ obj?.jsonSchema,
53
+ obj?.json_schema,
54
+ obj?.requestedSchema,
55
+ obj?.requested_schema
56
+ ];
57
+ for (const c of candidates) {
58
+ if (c !== void 0) return c;
59
+ }
60
+ return obj;
61
+ };
62
+ const parseJsonIfString = (v) => {
63
+ if (typeof v !== "string") return v;
64
+ try {
65
+ return JSON.parse(v);
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+ const resolveRef = (ref, root2) => {
71
+ if (!ref.startsWith("#/")) return null;
72
+ const parts = ref.slice(2).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
73
+ let cur = root2;
74
+ for (const p of parts) {
75
+ if (!cur || typeof cur !== "object") return null;
76
+ cur = cur[p];
77
+ }
78
+ return cur ?? null;
79
+ };
80
+ const isObjectSchemaWithProperties = (s2) => {
81
+ return s2 && typeof s2 === "object" && s2.type === "object" && s2.properties && typeof s2.properties === "object" && Object.keys(s2.properties).length > 0;
82
+ };
83
+ const unionStrings = (a, b) => {
84
+ const out = /* @__PURE__ */ new Set();
85
+ const add = (v) => {
86
+ if (!Array.isArray(v)) return;
87
+ for (const x of v) if (typeof x === "string") out.add(x);
88
+ };
89
+ add(a);
90
+ add(b);
91
+ return Array.from(out);
92
+ };
93
+ const extract = (schema, root2, depth) => {
94
+ if (depth > 6) return null;
95
+ let s2 = parseJsonIfString(schema);
96
+ if (!s2 || typeof s2 !== "object") return null;
97
+ s2 = unwrap(s2);
98
+ if (typeof s2?.$ref === "string") {
99
+ const resolved = resolveRef(s2.$ref, root2);
100
+ if (!resolved) return null;
101
+ return extract(resolved, root2, depth + 1);
102
+ }
103
+ if (isObjectSchemaWithProperties(s2)) return s2;
104
+ const oneOf = Array.isArray(s2.oneOf) ? s2.oneOf : [];
105
+ const anyOf = Array.isArray(s2.anyOf) ? s2.anyOf : [];
106
+ const allOf = Array.isArray(s2.allOf) ? s2.allOf : [];
107
+ for (const child of oneOf) {
108
+ const found = extract(child, root2, depth + 1);
109
+ if (found) return found;
110
+ }
111
+ for (const child of anyOf) {
112
+ const found = extract(child, root2, depth + 1);
113
+ if (found) return found;
114
+ }
115
+ if (allOf.length > 0) {
116
+ const merged = { type: "object", properties: {}, required: [] };
117
+ let saw = false;
118
+ for (const child of allOf) {
119
+ const found = extract(child, root2, depth + 1);
120
+ if (!found || typeof found !== "object") continue;
121
+ if (found.type !== "object" || !found.properties || typeof found.properties !== "object") continue;
122
+ Object.assign(merged.properties, found.properties);
123
+ merged.required = unionStrings(merged.required, found.required);
124
+ saw = saw || Object.keys(found.properties).length > 0;
125
+ }
126
+ if (saw && Object.keys(merged.properties).length > 0) return merged;
127
+ }
128
+ if (s2?.type === "object" && s2.properties && typeof s2.properties === "object") return s2;
129
+ return null;
130
+ };
131
+ let root = parseJsonIfString(value);
132
+ if (!root || typeof root !== "object") return null;
133
+ root = unwrap(root);
134
+ const s = extract(root, root, 0);
135
+ if (!s || typeof s !== "object") return null;
136
+ if (s.type !== "object") return null;
137
+ if (!s.properties || typeof s.properties !== "object") return null;
138
+ const required = Array.isArray(s.required) ? s.required.filter((k) => typeof k === "string") : void 0;
139
+ return { type: "object", properties: s.properties, required };
140
+ }
141
+ function summarizeRequestedSchemaForLogs(requestedSchemaRaw) {
142
+ const schema = normalizeRequestedSchema(requestedSchemaRaw);
143
+ if (!schema) return { ok: false, propertyCount: 0, properties: [], required: [] };
144
+ const properties = Object.keys(schema.properties || {});
145
+ const required = Array.isArray(schema.required) ? schema.required : [];
146
+ return { ok: true, propertyCount: properties.length, properties, required };
147
+ }
148
+ function buildAskUserQuestionPromptFromRequestedSchema(requestedSchemaRaw, message) {
149
+ const schema = normalizeRequestedSchema(requestedSchemaRaw);
150
+ if (!schema) return null;
151
+ const questions = [];
152
+ const keys = Object.keys(schema.properties || {});
153
+ if (keys.length === 0) return null;
154
+ for (const key of keys) {
155
+ const prop = schema.properties[key];
156
+ if (!prop || typeof prop !== "object") return null;
157
+ if (prop.type === "string") {
158
+ const options = Array.isArray(prop.enum) ? prop.enum.filter((v) => typeof v === "string" && v.length > 0) : [];
159
+ if (options.length === 0) return null;
160
+ const enumDescriptions = Array.isArray(prop.enumDescriptions) ? prop.enumDescriptions.filter((v) => typeof v === "string") : Array.isArray(prop["x-enumDescriptions"]) ? prop["x-enumDescriptions"].filter((v) => typeof v === "string") : [];
161
+ questions.push({
162
+ key,
163
+ header: typeof prop.title === "string" && prop.title.trim().length > 0 ? String(prop.title).trim() : key,
164
+ question: typeof prop.description === "string" && prop.description.trim().length > 0 ? String(prop.description).trim() : typeof message === "string" && message.trim().length > 0 ? message.trim() : key,
165
+ options: options.map((label, idx) => ({
166
+ label,
167
+ description: typeof enumDescriptions[idx] === "string" ? String(enumDescriptions[idx]) : ""
168
+ })),
169
+ multiSelect: false
170
+ });
171
+ continue;
172
+ }
173
+ if (prop.type === "array") {
174
+ const items = prop.items;
175
+ if (!items || typeof items !== "object" || items.type !== "string") return null;
176
+ const options = Array.isArray(items.enum) ? items.enum.filter((v) => typeof v === "string" && v.length > 0) : [];
177
+ if (options.length === 0) return null;
178
+ const enumDescriptions = Array.isArray(items.enumDescriptions) ? items.enumDescriptions.filter((v) => typeof v === "string") : Array.isArray(items["x-enumDescriptions"]) ? items["x-enumDescriptions"].filter((v) => typeof v === "string") : [];
179
+ questions.push({
180
+ key,
181
+ header: typeof prop.title === "string" && prop.title.trim().length > 0 ? String(prop.title).trim() : key,
182
+ question: typeof prop.description === "string" && prop.description.trim().length > 0 ? String(prop.description).trim() : typeof message === "string" && message.trim().length > 0 ? message.trim() : key,
183
+ options: options.map((label, idx) => ({
184
+ label,
185
+ description: typeof enumDescriptions[idx] === "string" ? String(enumDescriptions[idx]) : ""
186
+ })),
187
+ multiSelect: true
188
+ });
189
+ continue;
190
+ }
191
+ return null;
192
+ }
193
+ if (questions.length === 0) return null;
194
+ return { questions };
195
+ }
196
+ function pickEnumValue(options, preferred) {
197
+ if (!options || options.length === 0) return void 0;
198
+ for (const want of preferred) {
199
+ const found = options.find((v) => typeof v === "string" && v.toLowerCase() === want.toLowerCase());
200
+ if (found) return found;
201
+ }
202
+ return options[0];
203
+ }
204
+ function looksPositive(value) {
205
+ const v = value.trim().toLowerCase();
206
+ return v === "1" || v === "true" || v === "yes" || v === "y" || v === "allow" || v === "accept" || v === "approved" || v === "approve" || v === "ok";
207
+ }
208
+ function looksNegative(value) {
209
+ const v = value.trim().toLowerCase();
210
+ return v === "0" || v === "false" || v === "no" || v === "n" || v === "deny" || v === "denied" || v === "decline" || v === "reject" || v === "abort" || v === "cancel";
211
+ }
212
+ function guessValue(key, schema, allow) {
213
+ const k = key.toLowerCase();
214
+ switch (schema.type) {
215
+ case "boolean":
216
+ return allow;
217
+ case "number":
218
+ return allow ? 1 : 0;
219
+ case "array":
220
+ return [];
221
+ case "string": {
222
+ const enumVal = pickEnumValue(
223
+ schema.enum,
224
+ allow ? ["allow", "approve", "approved", "accept", "yes", "true", "y"] : ["deny", "denied", "decline", "no", "false", "n"]
225
+ );
226
+ if (enumVal) return enumVal;
227
+ if (schema.enum && schema.enum.length > 0) {
228
+ const values = schema.enum.filter((v) => typeof v === "string");
229
+ if (allow) {
230
+ const positive = values.find(looksPositive);
231
+ if (positive) return positive;
232
+ const nonNegative = values.find((v) => !looksNegative(v));
233
+ if (nonNegative) return nonNegative;
234
+ } else {
235
+ const negative = values.find(looksNegative);
236
+ if (negative) return negative;
237
+ const nonPositive = values.find((v) => !looksPositive(v));
238
+ if (nonPositive) return nonPositive;
239
+ }
240
+ return values[0] ?? (allow ? "yes" : "no");
241
+ }
242
+ if (k.includes("decision")) return allow ? "approved" : "denied";
243
+ if (k.includes("action")) return allow ? "accept" : "decline";
244
+ if (k.includes("allow") || k.includes("approve") || k.includes("confirm")) return allow ? "true" : "false";
245
+ return allow ? "yes" : "no";
246
+ }
247
+ }
248
+ }
249
+ function buildMcpElicitationResult(decision, requestedSchemaRaw, options) {
250
+ const allow = decision === "approved" || decision === "approved_for_session";
251
+ const action = "accept";
252
+ const codexDecision = allow ? decision === "approved_for_session" ? "approved_for_session" : "approved" : "denied";
253
+ const formatDeniedReasonForModel = (value) => {
254
+ const raw = value.trim();
255
+ if (!raw) return "";
256
+ if (/^Blocked\b/.test(raw)) return raw;
257
+ if (raw === "ledger_read_required") {
258
+ return "Blocked by policy (automatic; not the user): read the ledger before making file edits. Next: call mcp__flockbay__ledger_read, then retry the edit.";
259
+ }
260
+ if (raw === "docs_index_read_required") {
261
+ return "Blocked by policy (automatic; not the user): read the game Documentation index before making edits. Next: call mcp__flockbay__docs_index_read, then retry the edit.";
262
+ }
263
+ if (raw.startsWith("file_claim_required:")) {
264
+ const withoutPrefix = raw.slice("file_claim_required:".length);
265
+ const file = withoutPrefix.split("(")[0]?.trim() || "the file";
266
+ return `Blocked by policy (automatic; not the user): claim ${file} before editing it. Next: claim the file via mcp__flockbay__ledger_claim (or mcp__flockbay__coordination_claim_files), then retry the edit.`;
267
+ }
268
+ if (raw === "read_only_mode") {
269
+ return "Blocked by policy (automatic; not the user): this session is in read-only mode. Next: switch permission mode to allow edits, then retry.";
270
+ }
271
+ return `Blocked: ${raw}`;
272
+ };
273
+ const deniedReasonRaw = typeof options?.deniedReason === "string" ? options.deniedReason : "";
274
+ const deniedReason = !allow ? formatDeniedReasonForModel(deniedReasonRaw) : "";
275
+ const requestedSchema = normalizeRequestedSchema(requestedSchemaRaw);
276
+ if (!requestedSchema) {
277
+ if (!allow && deniedReason) {
278
+ return { action, decision: codexDecision, content: { reason: deniedReason } };
279
+ }
280
+ return { action, decision: codexDecision };
281
+ }
282
+ const keys = Object.keys(requestedSchema.properties || {});
283
+ const required = Array.isArray(requestedSchema.required) ? requestedSchema.required : [];
284
+ const content = {};
285
+ const provided = options?.content && typeof options.content === "object" ? options.content : null;
286
+ if (provided) {
287
+ for (const [k, v] of Object.entries(provided)) {
288
+ content[k] = v;
289
+ }
290
+ }
291
+ for (const key of required) {
292
+ if (key in content) continue;
293
+ const schema = requestedSchema.properties?.[key];
294
+ if (!schema) continue;
295
+ content[key] = guessValue(key, schema, allow);
296
+ }
297
+ for (const key of keys) {
298
+ if (key in content) continue;
299
+ const schema = requestedSchema.properties?.[key];
300
+ if (!schema) continue;
301
+ content[key] = guessValue(key, schema, allow);
302
+ }
303
+ if (!allow && deniedReason) {
304
+ const isFreeformString = (schema) => {
305
+ return Boolean(schema && schema.type === "string" && (!("enum" in schema) || !schema.enum || schema.enum.length === 0));
306
+ };
307
+ let didSet = false;
308
+ for (const key of keys) {
309
+ const schema = requestedSchema.properties?.[key];
310
+ if (!isFreeformString(schema)) continue;
311
+ const lower = key.toLowerCase();
312
+ if (lower.includes("reason") || lower.includes("error") || lower.includes("message") || lower.includes("detail") || lower.includes("explain") || lower.includes("why")) {
313
+ content[key] = deniedReason;
314
+ didSet = true;
315
+ }
316
+ }
317
+ if (!didSet) {
318
+ const fallbackKey = keys.find((k) => isFreeformString(requestedSchema.properties?.[k]));
319
+ if (fallbackKey) {
320
+ content[fallbackKey] = deniedReason;
321
+ didSet = true;
322
+ }
323
+ }
324
+ if (!didSet) {
325
+ content.reason = deniedReason;
326
+ }
327
+ }
328
+ if (decision === "approved_for_session") {
329
+ for (const key of keys) {
330
+ const schema = requestedSchema.properties?.[key];
331
+ if (!schema || schema.type !== "boolean") continue;
332
+ const lower = key.toLowerCase();
333
+ if (lower.includes("remember") || lower.includes("persist") || lower.includes("session") || lower.includes("always") || lower.includes("dontask") || lower.includes("dont_ask") || lower.includes("skip")) {
334
+ content[key] = true;
335
+ }
336
+ }
337
+ }
338
+ if (Object.keys(content).length === 0) return { action, decision: codexDecision };
339
+ return { action, content, decision: codexDecision };
340
+ }
341
+
342
+ const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
343
+ function getCodexMcpCommand() {
344
+ try {
345
+ const codexBin = resolveCodexBin();
346
+ const versionRes = node_child_process.spawnSync(codexBin, ["--version"], {
347
+ encoding: "utf8",
348
+ env: buildCodexSpawnEnv(codexBin),
349
+ timeout: 4e3
350
+ });
351
+ const version = String(versionRes.stdout || "").trim();
352
+ if (versionRes.status !== 0 || !version) {
353
+ return "mcp-server";
354
+ }
355
+ const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
356
+ if (!match) return "mcp-server";
357
+ const versionStr = match[1];
358
+ const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
359
+ if (major > 0 || minor > 43) return "mcp-server";
360
+ if (minor === 43 && patch === 0) {
361
+ if (versionStr.includes("-alpha.")) {
362
+ const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
363
+ return alphaNum >= 5 ? "mcp-server" : "mcp";
364
+ }
365
+ return "mcp-server";
366
+ }
367
+ return "mcp";
368
+ } catch (error) {
369
+ types.logger.debug("[CodexMCP] Error detecting codex version, defaulting to mcp-server:", error);
370
+ return "mcp-server";
371
+ }
372
+ }
373
+ function buildCodexSpawnEnv(codexBin) {
374
+ const env = Object.keys(process.env).reduce((acc, key) => {
375
+ const value = process.env[key];
376
+ if (typeof value === "string") acc[key] = value;
377
+ return acc;
378
+ }, {});
379
+ const existingPath = env.PATH || "";
380
+ const existingParts = existingPath.split(":").filter(Boolean);
381
+ const prepend = [];
382
+ const add = (p) => {
383
+ if (!p) return;
384
+ if (!prepend.includes(p) && !existingParts.includes(p)) prepend.push(p);
385
+ };
386
+ if (codexBin && codexBin.includes("/")) {
387
+ add(path.dirname(codexBin));
388
+ }
389
+ add(path.dirname(process.execPath));
390
+ add("/opt/homebrew/bin");
391
+ add("/usr/local/bin");
392
+ add("/usr/bin");
393
+ add("/bin");
394
+ add("/usr/sbin");
395
+ add("/sbin");
396
+ env.PATH = [...prepend, ...existingParts].join(":");
397
+ return env;
398
+ }
399
+ function resolveCodexBin() {
400
+ const explicit = String(process.env.FLOCKBAY_CODEX_BIN || "").trim();
401
+ if (explicit) return explicit;
402
+ const existsExecutable = (p) => {
403
+ try {
404
+ fs.accessSync(p, fs.constants.X_OK);
405
+ return true;
406
+ } catch {
407
+ return false;
408
+ }
409
+ };
410
+ const candidates = [
411
+ path.join(os.homedir(), ".nvm", "versions", "node", "current", "bin", "codex"),
412
+ path.join(os.homedir(), ".local", "bin", "codex"),
413
+ "/opt/homebrew/bin/codex",
414
+ "/usr/local/bin/codex"
415
+ ];
416
+ for (const c of candidates) {
417
+ if (c && existsExecutable(c)) return c;
418
+ }
419
+ try {
420
+ const nvmRoot = path.join(os.homedir(), ".nvm", "versions", "node");
421
+ const versions = fs.readdirSync(nvmRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((name) => name.startsWith("v"));
422
+ const parse = (v) => {
423
+ const m = v.match(/^v(\d+)\.(\d+)\.(\d+)/);
424
+ if (!m) return null;
425
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
426
+ };
427
+ versions.sort((a, b) => {
428
+ const pa = parse(a);
429
+ const pb = parse(b);
430
+ if (!pa || !pb) return a.localeCompare(b);
431
+ if (pa[0] !== pb[0]) return pb[0] - pa[0];
432
+ if (pa[1] !== pb[1]) return pb[1] - pa[1];
433
+ return pb[2] - pa[2];
434
+ });
435
+ for (const v of versions) {
436
+ const p = path.join(nvmRoot, v, "bin", "codex");
437
+ if (existsExecutable(p)) return p;
438
+ }
439
+ } catch {
440
+ }
441
+ const tryShell = (shellPath, args) => {
442
+ try {
443
+ const res = node_child_process.spawnSync(shellPath, args, {
444
+ encoding: "utf8",
445
+ env: process.env,
446
+ timeout: 4e3
447
+ });
448
+ const out = String(res.stdout || "").trim();
449
+ if (res.status === 0 && out) return out.split("\n")[0].trim();
450
+ } catch {
451
+ }
452
+ return null;
453
+ };
454
+ const fromZsh = tryShell("/bin/zsh", ["-lc", "command -v codex"]);
455
+ if (fromZsh) return fromZsh;
456
+ const fromBash = tryShell("/bin/bash", ["-lc", "command -v codex"]);
457
+ if (fromBash) return fromBash;
458
+ return "codex";
459
+ }
460
+ function normalizeRelativePath(input) {
461
+ const raw = typeof input === "string" ? input.trim() : String(input ?? "").trim();
462
+ if (!raw) return null;
463
+ let candidate = raw.replace(/^['"`]+/, "").replace(/['"`]+$/, "");
464
+ if (!candidate) return null;
465
+ candidate = candidate.replace(/\\/g, "/");
466
+ try {
467
+ if (path.isAbsolute(candidate)) {
468
+ const rel = path.relative(process.cwd(), candidate).replace(/\\/g, "/");
469
+ candidate = rel;
470
+ }
471
+ } catch {
472
+ }
473
+ candidate = candidate.replace(/^\.\/+/, "").replace(/^\/+/, "");
474
+ if (!candidate) return null;
475
+ if (candidate.includes("..")) return null;
476
+ return candidate;
477
+ }
478
+ function extractLikelyFilePathsFromMessage(message) {
479
+ const out = [];
480
+ const seen = /* @__PURE__ */ new Set();
481
+ const push = (v) => {
482
+ const norm = normalizeRelativePath(v);
483
+ if (!norm) return;
484
+ if (!norm.includes("/")) return;
485
+ if (norm.includes(" ")) return;
486
+ if (norm.startsWith("http://") || norm.startsWith("https://")) return;
487
+ if (seen.has(norm)) return;
488
+ seen.add(norm);
489
+ out.push(norm);
490
+ };
491
+ for (const m of message.matchAll(/`([^`]+)`/g)) {
492
+ push(m[1]);
493
+ }
494
+ for (const m of message.matchAll(/(?:^|\s)([./~A-Za-z0-9_-][A-Za-z0-9_./~ -]*\/[A-Za-z0-9_./~-]+)(?=\s|$)/g)) {
495
+ push(m[1]);
496
+ }
497
+ return out;
498
+ }
499
+ function inferElicitationToolName(params, requestedSchemaRaw) {
500
+ const extractCommandFromMessage = (message2) => {
501
+ const text = String(message2 || "");
502
+ if (!text.trim()) return [];
503
+ const firstNonEmptyLine = (value) => {
504
+ for (const raw of value.split(/\r?\n/)) {
505
+ const line = raw.trimEnd();
506
+ if (line.trim()) return line;
507
+ }
508
+ return null;
509
+ };
510
+ const fence = text.match(/```(?:bash|sh|zsh)?\s*\n([\s\S]*?)```/i);
511
+ if (fence?.[1]) {
512
+ const line = firstNonEmptyLine(fence[1]);
513
+ if (line) return [line.replace(/^\$\s+/, "").trim()];
514
+ }
515
+ const tick = text.match(/`([^`]+)`/);
516
+ if (tick?.[1]) {
517
+ const cmd = tick[1].trim();
518
+ if (cmd) return [cmd];
519
+ }
520
+ const dollar = text.match(/(?:^|\n)\$\s+([^\n]+)/);
521
+ if (dollar?.[1]) {
522
+ const cmd = dollar[1].trim();
523
+ if (cmd) return [cmd];
524
+ }
525
+ const section = text.match(/(?:^|\n)Command:\s*\n+([^\n]+)/i);
526
+ if (section?.[1]) {
527
+ const cmd = section[1].trim();
528
+ if (cmd) return [cmd];
529
+ }
530
+ return [];
531
+ };
532
+ const asStringArray = (value) => {
533
+ if (Array.isArray(value)) {
534
+ const out = value.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
535
+ return out.length > 0 ? out : void 0;
536
+ }
537
+ if (typeof value === "string") {
538
+ const trimmed = value.trim();
539
+ return trimmed.length > 0 ? [trimmed] : void 0;
540
+ }
541
+ return void 0;
542
+ };
543
+ const extractCommandFromParams = (root) => {
544
+ const candidates = [
545
+ root?.codex_command,
546
+ root?.command,
547
+ root?.cmd,
548
+ root?.args,
549
+ root?.argv,
550
+ root?.input?.codex_command,
551
+ root?.input?.command,
552
+ root?.input?.cmd,
553
+ root?.input?.args,
554
+ root?.input?.argv,
555
+ root?.arguments?.codex_command,
556
+ root?.arguments?.command,
557
+ root?.arguments?.cmd,
558
+ root?.arguments?.args,
559
+ root?.arguments?.argv,
560
+ root?.tool?.command,
561
+ root?.tool?.cmd,
562
+ root?.tool?.args,
563
+ root?.tool?.input?.command,
564
+ root?.tool?.input?.cmd,
565
+ root?.tool?.input?.args,
566
+ root?.request?.command,
567
+ root?.request?.cmd,
568
+ root?.request?.args,
569
+ root?.command?.command,
570
+ root?.command?.cmd,
571
+ root?.command?.args,
572
+ root?.exec?.command,
573
+ root?.exec?.cmd,
574
+ root?.exec?.args
575
+ ];
576
+ for (const candidate of candidates) {
577
+ const arr = asStringArray(candidate);
578
+ if (arr) return arr;
579
+ }
580
+ const seen = /* @__PURE__ */ new Set();
581
+ const visit = (value, depth) => {
582
+ if (depth > 5) return null;
583
+ if (!value || typeof value !== "object") return null;
584
+ if (seen.has(value)) return null;
585
+ seen.add(value);
586
+ if (Array.isArray(value)) {
587
+ for (const v of value) {
588
+ const found = visit(v, depth + 1);
589
+ if (found?.length) return found;
590
+ }
591
+ return null;
592
+ }
593
+ const obj = value;
594
+ for (const key of Object.keys(obj)) {
595
+ const lower = key.toLowerCase();
596
+ if (lower === "command" || lower === "cmd" || lower === "args" || lower === "argv" || lower === "codex_command") {
597
+ const arr = asStringArray(obj[key]);
598
+ if (arr) return arr;
599
+ }
600
+ }
601
+ for (const key of Object.keys(obj)) {
602
+ const found = visit(obj[key], depth + 1);
603
+ if (found?.length) return found;
604
+ }
605
+ return null;
606
+ };
607
+ return visit(root, 0) ?? [];
608
+ };
609
+ const extractedCmd = extractCommandFromParams(params);
610
+ if (extractedCmd.length > 0) return "CodexBash";
611
+ const message = typeof params?.message === "string" ? params.message : "";
612
+ const cmdFromMessage = message ? extractCommandFromMessage(message) : [];
613
+ if (cmdFromMessage.length > 0) return "CodexBash";
614
+ if (Array.isArray(params?.files) || params?.changes || params?.patch || params?.diff) return "CodexPatch";
615
+ const schemaSummary = summarizeRequestedSchemaForLogs(requestedSchemaRaw);
616
+ const schemaKeys = schemaSummary.ok ? schemaSummary.properties.map((k) => k.toLowerCase()) : [];
617
+ const patchySchemaKeys = /* @__PURE__ */ new Set([
618
+ "changes",
619
+ "files",
620
+ "patch",
621
+ "diff",
622
+ "unified_diff"
623
+ ]);
624
+ if (schemaKeys.some((k) => patchySchemaKeys.has(k))) return "CodexPatch";
625
+ return "CodexBash";
626
+ }
627
+ class CodexMcpClient {
628
+ client;
629
+ transport = null;
630
+ connected = false;
631
+ sessionId = null;
632
+ conversationId = null;
633
+ handler = null;
634
+ permissionHandler = null;
635
+ authSnapshot = null;
636
+ reconnectPromise = null;
637
+ latestUnifiedDiff = null;
638
+ latestPatchApplyBegin = null;
639
+ latestExecApproval = null;
640
+ constructor() {
641
+ this.client = this.createClient();
642
+ }
643
+ createClient() {
644
+ const client = new index_js.Client(
645
+ { name: "flockbay-codex-client", version: "1.0.0" },
646
+ { capabilities: { elicitation: {} } }
647
+ );
648
+ client.setNotificationHandler(z.z.object({
649
+ method: z.z.literal("codex/event"),
650
+ params: z.z.object({
651
+ msg: z.z.any()
652
+ })
653
+ }).passthrough(), (data) => {
654
+ const msg = data.params.msg;
655
+ this.updateIdentifiersFromEvent(msg);
656
+ this.captureLatestDiff(msg);
657
+ this.handler?.(msg);
658
+ });
659
+ return client;
660
+ }
661
+ captureLatestDiff(event) {
662
+ try {
663
+ if (!event || typeof event !== "object") return;
664
+ const type = typeof event.type === "string" ? event.type : "";
665
+ const now = Date.now();
666
+ if (type === "turn_diff") {
667
+ const diff = typeof event.unified_diff === "string" ? String(event.unified_diff) : "";
668
+ if (!diff.trim()) return;
669
+ this.latestUnifiedDiff = { diff, updatedAt: now };
670
+ return;
671
+ }
672
+ if (type === "patch_apply_begin") {
673
+ const changes = event.changes;
674
+ if (!changes || typeof changes !== "object") return;
675
+ this.latestPatchApplyBegin = { changes, updatedAt: now };
676
+ return;
677
+ }
678
+ if (type === "exec_approval_request" || type === "exec_command_begin") {
679
+ const cmd = typeof event.command === "string" ? String(event.command).trim() : "";
680
+ if (!cmd) return;
681
+ const cwd = typeof event.cwd === "string" ? String(event.cwd).trim() : void 0;
682
+ this.latestExecApproval = { command: cmd, cwd, updatedAt: now };
683
+ return;
684
+ }
685
+ } catch {
686
+ }
687
+ }
688
+ getCodexAuthSnapshot() {
689
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
690
+ const authFilePath = path.join(codexHome, "auth.json");
691
+ let mtimeMs = null;
692
+ let size = null;
693
+ let accountId = null;
694
+ try {
695
+ const stat = fs.statSync(authFilePath);
696
+ mtimeMs = Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : null;
697
+ size = Number.isFinite(stat.size) ? stat.size : null;
698
+ } catch {
699
+ return { authFilePath, mtimeMs, size, accountId };
700
+ }
701
+ try {
702
+ const raw = fs.readFileSync(authFilePath, "utf8");
703
+ const parsed = JSON.parse(raw);
704
+ const candidate = parsed?.tokens?.account_id ?? parsed?.tokens?.accountId ?? parsed?.account_id ?? parsed?.accountId;
705
+ accountId = typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null;
706
+ } catch {
707
+ }
708
+ return { authFilePath, mtimeMs, size, accountId };
709
+ }
710
+ didAuthChange(prev, next) {
711
+ if (!prev) return false;
712
+ if (prev.authFilePath !== next.authFilePath) return true;
713
+ if (prev.mtimeMs !== next.mtimeMs) return true;
714
+ if (prev.size !== next.size) return true;
715
+ if (prev.accountId && next.accountId && prev.accountId !== next.accountId) return true;
716
+ return false;
717
+ }
718
+ async reconnectForAuthChange(context) {
719
+ if (this.reconnectPromise) {
720
+ await this.reconnectPromise;
721
+ return;
722
+ }
723
+ this.reconnectPromise = (async () => {
724
+ types.logger.debug("[CodexMCP] Reconnecting to pick up Codex auth changes", {
725
+ context,
726
+ authFilePath: this.authSnapshot?.authFilePath,
727
+ prevAccountId: this.authSnapshot?.accountId
728
+ });
729
+ await this.disconnect();
730
+ this.client = this.createClient();
731
+ await this.connect();
732
+ })();
733
+ try {
734
+ await this.reconnectPromise;
735
+ } finally {
736
+ this.reconnectPromise = null;
737
+ }
738
+ }
739
+ getToolErrorMessage(response) {
740
+ if (!response || typeof response !== "object") {
741
+ return "Invalid tool response";
742
+ }
743
+ const asAny = response;
744
+ if (typeof asAny.error === "string" && asAny.error.trim().length > 0) {
745
+ try {
746
+ const parsed = JSON.parse(asAny.error);
747
+ if (parsed && typeof parsed === "object" && typeof parsed.detail === "string" && parsed.detail.trim().length > 0) {
748
+ return parsed.detail;
749
+ }
750
+ } catch {
751
+ }
752
+ return asAny.error;
753
+ }
754
+ if (asAny.isError === true) {
755
+ const content = Array.isArray(asAny.content) ? asAny.content : [];
756
+ const text = content.map((item) => item?.type === "text" ? item?.text : void 0).filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
757
+ return text || "Tool returned an error";
758
+ }
759
+ return null;
760
+ }
761
+ assertToolSuccess(response, context) {
762
+ const errorMessage = this.getToolErrorMessage(response);
763
+ if (!errorMessage) {
764
+ return;
765
+ }
766
+ const withHint = this.addUsageLimitHint(errorMessage);
767
+ throw new Error(`${context}: ${withHint}`);
768
+ }
769
+ addUsageLimitHint(message) {
770
+ if (!/hit your usage limit/i.test(message)) {
771
+ return message;
772
+ }
773
+ return [
774
+ message.trimEnd(),
775
+ "",
776
+ "Tip: If you have another ChatGPT account with Codex usage, re-run `flockbay connect codex` and sign into that account."
777
+ ].join("\n");
778
+ }
779
+ setHandler(handler) {
780
+ this.handler = handler;
781
+ }
782
+ /**
783
+ * Set the permission handler for tool approval
784
+ */
785
+ setPermissionHandler(handler) {
786
+ this.permissionHandler = handler;
787
+ }
788
+ async connect() {
789
+ if (this.connected) return;
790
+ const mcpCommand = getCodexMcpCommand();
791
+ const codexBin = resolveCodexBin();
792
+ types.logger.debug(`[CodexMCP] Connecting to Codex MCP server using command: ${codexBin} ${mcpCommand}`);
793
+ const env = buildCodexSpawnEnv(codexBin);
794
+ this.transport = new stdio_js.StdioClientTransport({
795
+ command: codexBin,
796
+ args: [mcpCommand],
797
+ env
798
+ });
799
+ this.registerPermissionHandlers();
800
+ const connectTimeoutMsRaw = String(process.env.FLOCKBAY_CODEX_CONNECT_TIMEOUT_MS || "").trim();
801
+ const connectTimeoutMs = Math.max(
802
+ 1e3,
803
+ Number.isFinite(Number(connectTimeoutMsRaw)) && Number(connectTimeoutMsRaw) > 0 ? Number(connectTimeoutMsRaw) : 2e4
804
+ );
805
+ try {
806
+ const connectPromise = this.client.connect(this.transport);
807
+ await new Promise((resolve, reject) => {
808
+ const timer = setTimeout(() => {
809
+ reject(
810
+ new Error(
811
+ `Timed out connecting to Codex MCP server after ${connectTimeoutMs}ms. This usually means the \`codex\` process failed to start (PATH) or is waiting for login/auth.`
812
+ )
813
+ );
814
+ }, connectTimeoutMs);
815
+ timer.unref?.();
816
+ connectPromise.then(() => resolve()).catch(reject).finally(() => clearTimeout(timer));
817
+ });
818
+ } catch (error) {
819
+ try {
820
+ await this.transport?.close?.();
821
+ } catch {
822
+ }
823
+ this.transport = null;
824
+ throw error;
825
+ }
826
+ this.connected = true;
827
+ this.authSnapshot = this.getCodexAuthSnapshot();
828
+ types.logger.debug("[CodexMCP] Connected to Codex");
829
+ }
830
+ registerPermissionHandlers() {
831
+ this.client.setRequestHandler(
832
+ types_js.ElicitRequestSchema,
833
+ async (request) => {
834
+ const params = request.params || {};
835
+ const requestedSchemaRaw = params?.requestedSchema ?? request.params?.requestedSchema;
836
+ types.logger.debug("[CodexMCP] Received elicitation request", {
837
+ paramKeys: Object.keys(params || {}),
838
+ hasRequestedSchema: !!params?.requestedSchema,
839
+ messagePreview: typeof params?.message === "string" ? params.message.slice(0, 160) : void 0,
840
+ requestedSchema: summarizeRequestedSchemaForLogs(requestedSchemaRaw)
841
+ });
842
+ const asNonEmptyString = (value) => {
843
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
844
+ };
845
+ const asStringArray = (value) => {
846
+ if (Array.isArray(value)) {
847
+ const out = value.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
848
+ return out.length > 0 ? out : void 0;
849
+ }
850
+ if (typeof value === "string" && value.trim().length > 0) return [value.trim()];
851
+ return void 0;
852
+ };
853
+ const uniq = (values) => {
854
+ const out = [];
855
+ const seen = /* @__PURE__ */ new Set();
856
+ for (const v of values) {
857
+ const s = asNonEmptyString(v);
858
+ if (!s) continue;
859
+ if (seen.has(s)) continue;
860
+ seen.add(s);
861
+ out.push(s);
862
+ }
863
+ return out;
864
+ };
865
+ if (!this.permissionHandler) {
866
+ types.logger.debug("[CodexMCP] No permission handler set, denying by default");
867
+ return {
868
+ action: "decline"
869
+ };
870
+ }
871
+ try {
872
+ const extractPathsFromUnifiedDiff = (diffText) => {
873
+ const out = [];
874
+ const seen = /* @__PURE__ */ new Set();
875
+ const push = (raw) => {
876
+ if (typeof raw !== "string") return;
877
+ let s = raw.trim();
878
+ if (!s) return;
879
+ s = s.replace(/^a\//, "").replace(/^b\//, "");
880
+ if (s === "/dev/null") return;
881
+ if (s.startsWith("/")) return;
882
+ if (seen.has(s)) return;
883
+ seen.add(s);
884
+ out.push(s);
885
+ };
886
+ for (const m of diffText.matchAll(/^diff --git a\/(.+?) b\/(.+?)$/gm)) {
887
+ push(m[1]);
888
+ push(m[2]);
889
+ }
890
+ for (const m of diffText.matchAll(/^\+\+\+\s+b\/(.+)$/gm)) push(m[1]);
891
+ for (const m of diffText.matchAll(/^---\s+a\/(.+)$/gm)) push(m[1]);
892
+ return out;
893
+ };
894
+ const candidateIds = uniq([
895
+ params.codex_mcp_tool_call_id,
896
+ params.codexMcpToolCallId,
897
+ params.mcp_tool_call_id,
898
+ params.mcpToolCallId,
899
+ params.tool_call_id,
900
+ params.toolCallId,
901
+ params.codex_call_id,
902
+ params.codexCallId,
903
+ params.call_id,
904
+ params.callId,
905
+ params.id,
906
+ params.requestId,
907
+ params.codex_event_id,
908
+ params.codexEventId,
909
+ params.event_id,
910
+ params.eventId,
911
+ request?.id
912
+ ]);
913
+ const primaryId = candidateIds[0] ?? node_crypto.randomUUID();
914
+ const aliases = candidateIds.filter((id) => id !== primaryId);
915
+ const extractCommandFromMessage = (message2) => {
916
+ const text = String(message2 || "");
917
+ if (!text.trim()) return [];
918
+ const firstNonEmptyLine = (value) => {
919
+ for (const raw of value.split(/\r?\n/)) {
920
+ const line = raw.trimEnd();
921
+ if (line.trim()) return line;
922
+ }
923
+ return null;
924
+ };
925
+ const fence = text.match(/```(?:bash|sh|zsh)?\s*\n([\s\S]*?)```/i);
926
+ if (fence?.[1]) {
927
+ const line = firstNonEmptyLine(fence[1]);
928
+ if (line) return [line.replace(/^\$\s+/, "").trim()];
929
+ }
930
+ const tick = text.match(/`([^`]+)`/);
931
+ if (tick?.[1]) {
932
+ const cmd = tick[1].trim();
933
+ if (cmd) return [cmd];
934
+ }
935
+ const dollar = text.match(/(?:^|\n)\$\s+([^\n]+)/);
936
+ if (dollar?.[1]) {
937
+ const cmd = dollar[1].trim();
938
+ if (cmd) return [cmd];
939
+ }
940
+ const section = text.match(/(?:^|\n)Command:\s*\n+([^\n]+)/i);
941
+ if (section?.[1]) {
942
+ const cmd = section[1].trim();
943
+ if (cmd) return [cmd];
944
+ }
945
+ return [];
946
+ };
947
+ const extractCommandFromParams = (root) => {
948
+ const candidates = [
949
+ { source: "params.codex_command", value: root?.codex_command },
950
+ { source: "params.command", value: root?.command },
951
+ { source: "params.cmd", value: root?.cmd },
952
+ { source: "params.args", value: root?.args },
953
+ { source: "params.argv", value: root?.argv },
954
+ { source: "params.input.command", value: root?.input?.command },
955
+ { source: "params.input.cmd", value: root?.input?.cmd },
956
+ { source: "params.input.args", value: root?.input?.args },
957
+ { source: "params.input.argv", value: root?.input?.argv },
958
+ { source: "params.arguments.command", value: root?.arguments?.command },
959
+ { source: "params.arguments.cmd", value: root?.arguments?.cmd },
960
+ { source: "params.arguments.args", value: root?.arguments?.args },
961
+ { source: "params.arguments.argv", value: root?.arguments?.argv },
962
+ { source: "params.tool.input.command", value: root?.tool?.input?.command },
963
+ { source: "params.tool.input.cmd", value: root?.tool?.input?.cmd },
964
+ { source: "params.tool.input.args", value: root?.tool?.input?.args },
965
+ { source: "params.request.command", value: root?.request?.command },
966
+ { source: "params.request.cmd", value: root?.request?.cmd },
967
+ { source: "params.request.args", value: root?.request?.args },
968
+ { source: "params.command.command", value: root?.command?.command },
969
+ { source: "params.command.cmd", value: root?.command?.cmd },
970
+ { source: "params.command.args", value: root?.command?.args },
971
+ { source: "params.exec.command", value: root?.exec?.command },
972
+ { source: "params.exec.cmd", value: root?.exec?.cmd },
973
+ { source: "params.exec.args", value: root?.exec?.args }
974
+ ];
975
+ for (const { source, value } of candidates) {
976
+ const cmd = asStringArray(value);
977
+ if (cmd) return { command: cmd, source };
978
+ }
979
+ const seen = /* @__PURE__ */ new Set();
980
+ const visit = (value, depth) => {
981
+ if (depth > 5) return null;
982
+ if (!value || typeof value !== "object") return null;
983
+ if (seen.has(value)) return null;
984
+ seen.add(value);
985
+ if (Array.isArray(value)) {
986
+ for (const v of value) {
987
+ const found = visit(v, depth + 1);
988
+ if (found) return found;
989
+ }
990
+ return null;
991
+ }
992
+ const obj = value;
993
+ for (const key of Object.keys(obj)) {
994
+ const lower = key.toLowerCase();
995
+ if (lower === "command" || lower === "cmd" || lower === "args" || lower === "argv" || lower === "codex_command") {
996
+ const cmd = asStringArray(obj[key]);
997
+ if (cmd) return { command: cmd, source: `deep:${key}` };
998
+ }
999
+ }
1000
+ for (const key of Object.keys(obj)) {
1001
+ const found = visit(obj[key], depth + 1);
1002
+ if (found) return found;
1003
+ }
1004
+ return null;
1005
+ };
1006
+ return visit(root, 0) ?? { command: [], source: "none" };
1007
+ };
1008
+ const extracted = extractCommandFromParams(params);
1009
+ const cwd = asNonEmptyString(params.codex_cwd ?? params.cwd);
1010
+ const message = typeof params?.message === "string" ? params.message : void 0;
1011
+ let command = extracted.command.length > 0 ? extracted.command : message ? extractCommandFromMessage(message) : [];
1012
+ const askUserQuestion = buildAskUserQuestionPromptFromRequestedSchema(requestedSchemaRaw, message);
1013
+ const isApprovalish = (() => {
1014
+ if (!askUserQuestion) return false;
1015
+ const keys = askUserQuestion.questions.map((q) => String(q.key || "")).filter(Boolean);
1016
+ if (keys.some((k) => /(^|_)ok$|^ok(_|$)|patch_ok|exec_ok|approve_ok|permission_ok/i.test(k))) return true;
1017
+ const flat = [
1018
+ message || "",
1019
+ ...askUserQuestion.questions.flatMap((q) => [
1020
+ q.key,
1021
+ q.header,
1022
+ q.question,
1023
+ ...Array.isArray(q.options) ? q.options.flatMap((o) => [o.label, o.description]) : []
1024
+ ])
1025
+ ].filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
1026
+ return /apply patch|code patch|exec(ution)?|permission|policy|approve|approved|allow|deny|confirm|click approve/i.test(flat);
1027
+ })();
1028
+ const hasRichOptions = askUserQuestion && (askUserQuestion.questions.length > 1 || askUserQuestion.questions.some((q) => q.options.length >= 3));
1029
+ if (askUserQuestion && hasRichOptions && !isApprovalish) {
1030
+ types.logger.debug("[CodexMCP] Elicitation looks like user question; emitting AskUserQuestion card", {
1031
+ primaryId,
1032
+ questionCount: askUserQuestion.questions.length,
1033
+ keys: askUserQuestion.questions.map((q) => q.key).slice(0, 20)
1034
+ });
1035
+ await this.permissionHandler.handleElicitation(primaryId, askUserQuestion, {
1036
+ decision: "approved"
1037
+ });
1038
+ return buildMcpElicitationResult("denied", requestedSchemaRaw, {
1039
+ deniedReason: "awaiting_user_message"
1040
+ });
1041
+ }
1042
+ let toolName = inferElicitationToolName(params, requestedSchemaRaw);
1043
+ if (toolName === "CodexBash" && command.length === 0 && this.latestExecApproval) {
1044
+ const ageMs = Date.now() - this.latestExecApproval.updatedAt;
1045
+ if (ageMs >= 0 && ageMs < 2 * 6e4) {
1046
+ command = [this.latestExecApproval.command];
1047
+ }
1048
+ }
1049
+ if (toolName === "CodexBash" && command.length === 0) {
1050
+ const diffAgeMs2 = this.latestUnifiedDiff ? Date.now() - this.latestUnifiedDiff.updatedAt : null;
1051
+ const patchAgeMs = this.latestPatchApplyBegin ? Date.now() - this.latestPatchApplyBegin.updatedAt : null;
1052
+ const hasRecentPatchContext = diffAgeMs2 != null && diffAgeMs2 >= 0 && diffAgeMs2 < 5 * 6e4 || patchAgeMs != null && patchAgeMs >= 0 && patchAgeMs < 5 * 6e4;
1053
+ if (hasRecentPatchContext) {
1054
+ toolName = "CodexPatch";
1055
+ }
1056
+ }
1057
+ const patchFilesFromChanges = toolName === "CodexPatch" && params?.changes && typeof params.changes === "object" ? Object.keys(params.changes) : [];
1058
+ const patchFiles = toolName === "CodexPatch" ? [
1059
+ ...Array.isArray(params?.files) ? params.files : [],
1060
+ ...patchFilesFromChanges,
1061
+ ...typeof message === "string" ? extractLikelyFilePathsFromMessage(message) : []
1062
+ ].map(normalizeRelativePath).filter((v) => Boolean(v)) : [];
1063
+ let diffFiles = [];
1064
+ let diffAgeMs = null;
1065
+ if (toolName === "CodexPatch" && patchFiles.length === 0 && this.latestUnifiedDiff?.diff) {
1066
+ diffAgeMs = Date.now() - this.latestUnifiedDiff.updatedAt;
1067
+ if (diffAgeMs >= 0 && diffAgeMs < 5 * 6e4) {
1068
+ diffFiles = extractPathsFromUnifiedDiff(this.latestUnifiedDiff.diff).map(normalizeRelativePath).filter((v) => Boolean(v));
1069
+ }
1070
+ }
1071
+ let patchBeginFiles = [];
1072
+ let patchBeginAgeMs = null;
1073
+ if (toolName === "CodexPatch" && patchFiles.length === 0 && diffFiles.length === 0 && this.latestPatchApplyBegin?.changes) {
1074
+ patchBeginAgeMs = Date.now() - this.latestPatchApplyBegin.updatedAt;
1075
+ if (patchBeginAgeMs >= 0 && patchBeginAgeMs < 5 * 6e4) {
1076
+ patchBeginFiles = Object.keys(this.latestPatchApplyBegin.changes).map(normalizeRelativePath).filter((v) => Boolean(v));
1077
+ }
1078
+ }
1079
+ const patchFilesFinal = toolName === "CodexPatch" && patchFiles.length === 0 ? diffFiles.length > 0 ? diffFiles : patchBeginFiles : patchFiles;
1080
+ const patchChanges = toolName === "CodexPatch" && patchFilesFinal.length > 0 ? Object.fromEntries(patchFilesFinal.map((p) => [p, { op: "update" }])) : void 0;
1081
+ const result = await this.permissionHandler.handleToolCall(
1082
+ primaryId,
1083
+ toolName,
1084
+ {
1085
+ command,
1086
+ cwd,
1087
+ message,
1088
+ ...patchChanges ? { changes: patchChanges } : null,
1089
+ ...patchFilesFinal.length > 0 ? { files: patchFilesFinal } : null,
1090
+ ...toolName === "CodexPatch" && this.latestUnifiedDiff?.diff && diffFiles.length > 0 ? { unified_diff: this.latestUnifiedDiff.diff } : null,
1091
+ _permissionDebug: {
1092
+ primaryId,
1093
+ aliases,
1094
+ paramsKeys: Object.keys(params || {}),
1095
+ hasCommand: command.length > 0,
1096
+ commandSource: extracted.source,
1097
+ inferredTool: toolName,
1098
+ patchFiles: patchFilesFinal.slice(0, 25),
1099
+ diffPreviewUsed: diffFiles.length > 0,
1100
+ diffFileCount: diffFiles.length,
1101
+ diffAgeMs,
1102
+ patchApplyBeginUsed: patchBeginFiles.length > 0,
1103
+ patchApplyBeginFileCount: patchBeginFiles.length,
1104
+ patchApplyBeginAgeMs: patchBeginAgeMs,
1105
+ latestExecApprovalAgeMs: toolName === "CodexBash" && this.latestExecApproval ? Date.now() - this.latestExecApproval.updatedAt : null,
1106
+ conversationId: this.conversationId,
1107
+ sessionId: this.sessionId,
1108
+ requestedSchema: summarizeRequestedSchemaForLogs(requestedSchemaRaw)
1109
+ }
1110
+ },
1111
+ aliases
1112
+ );
1113
+ types.logger.debug("[CodexMCP] Permission result:", result);
1114
+ const elicitationResult = buildMcpElicitationResult(result.decision, requestedSchemaRaw, {
1115
+ deniedReason: result.reason
1116
+ });
1117
+ types.logger.debug("[CodexMCP] Elicitation response:", {
1118
+ decision: result.decision,
1119
+ action: elicitationResult.action,
1120
+ codexDecision: elicitationResult.decision,
1121
+ contentKeys: elicitationResult.content ? Object.keys(elicitationResult.content) : [],
1122
+ content: elicitationResult.content
1123
+ });
1124
+ return elicitationResult;
1125
+ } catch (error) {
1126
+ const message = error instanceof Error ? error.message : String(error);
1127
+ const stack = error instanceof Error ? error.stack : void 0;
1128
+ types.logger.debug("[CodexMCP] Error handling permission request:", { message, stack });
1129
+ return {
1130
+ action: "decline"
1131
+ };
1132
+ }
1133
+ }
1134
+ );
1135
+ types.logger.debug("[CodexMCP] Permission handlers registered");
1136
+ }
1137
+ async startSession(config, options) {
1138
+ if (!this.connected) await this.connect();
1139
+ if (this.sessionId) {
1140
+ types.logger.debug("[CodexMCP] startSession called with active session; continuing instead", {
1141
+ sessionId: this.sessionId,
1142
+ hasConversationId: !!this.conversationId
1143
+ });
1144
+ return this.continueSession(config.prompt, options);
1145
+ }
1146
+ if (this.connected && !this.sessionId) {
1147
+ const nextAuth = this.getCodexAuthSnapshot();
1148
+ if (this.didAuthChange(this.authSnapshot, nextAuth)) {
1149
+ await this.reconnectForAuthChange("startSession:auth-changed");
1150
+ } else if (!this.authSnapshot) {
1151
+ this.authSnapshot = nextAuth;
1152
+ }
1153
+ }
1154
+ types.logger.debug("[CodexMCP] Starting Codex session:", config);
1155
+ const response = await this.client.callTool({
1156
+ name: "codex",
1157
+ arguments: config
1158
+ }, void 0, {
1159
+ signal: options?.signal,
1160
+ timeout: DEFAULT_TIMEOUT
1161
+ // maxTotalTimeout: 10000000000
1162
+ });
1163
+ types.logger.debug("[CodexMCP] startSession response:", response);
1164
+ try {
1165
+ this.assertToolSuccess(response, "[CodexMCP] startSession failed");
1166
+ } catch (error) {
1167
+ const message = error instanceof Error ? error.message : String(error);
1168
+ if (/hit your usage limit/i.test(message)) {
1169
+ const nextAuth = this.getCodexAuthSnapshot();
1170
+ if (this.didAuthChange(this.authSnapshot, nextAuth)) {
1171
+ await this.reconnectForAuthChange("startSession:usage-limit:auth-changed");
1172
+ const retry = await this.client.callTool({
1173
+ name: "codex",
1174
+ arguments: config
1175
+ }, void 0, {
1176
+ signal: options?.signal,
1177
+ timeout: DEFAULT_TIMEOUT
1178
+ });
1179
+ types.logger.debug("[CodexMCP] startSession retry response:", retry);
1180
+ this.assertToolSuccess(retry, "[CodexMCP] startSession failed");
1181
+ this.extractIdentifiers(retry);
1182
+ return retry;
1183
+ }
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ this.extractIdentifiers(response);
1188
+ return response;
1189
+ }
1190
+ async continueSession(prompt, options) {
1191
+ if (!this.connected) await this.connect();
1192
+ if (!this.sessionId) {
1193
+ throw new Error("No active session. Call startSession first.");
1194
+ }
1195
+ if (!this.conversationId) {
1196
+ this.conversationId = this.sessionId;
1197
+ types.logger.debug("[CodexMCP] conversationId missing, defaulting to sessionId:", this.conversationId);
1198
+ }
1199
+ const args = { sessionId: this.sessionId, conversationId: this.conversationId, prompt };
1200
+ types.logger.debug("[CodexMCP] Continuing Codex session:", args);
1201
+ const response = await this.client.callTool({
1202
+ name: "codex-reply",
1203
+ arguments: args
1204
+ }, void 0, {
1205
+ signal: options?.signal,
1206
+ timeout: DEFAULT_TIMEOUT
1207
+ });
1208
+ types.logger.debug("[CodexMCP] continueSession response:", response);
1209
+ this.assertToolSuccess(response, "[CodexMCP] continueSession failed");
1210
+ this.extractIdentifiers(response);
1211
+ return response;
1212
+ }
1213
+ updateIdentifiersFromEvent(event) {
1214
+ if (!event || typeof event !== "object") {
1215
+ return;
1216
+ }
1217
+ const candidates = [event];
1218
+ if (event.data && typeof event.data === "object") {
1219
+ candidates.push(event.data);
1220
+ }
1221
+ for (const candidate of candidates) {
1222
+ const sessionId = candidate.session_id ?? candidate.sessionId;
1223
+ if (sessionId) {
1224
+ this.sessionId = sessionId;
1225
+ types.logger.debug("[CodexMCP] Session ID extracted from event:", this.sessionId);
1226
+ }
1227
+ const conversationId = candidate.conversation_id ?? candidate.conversationId;
1228
+ if (conversationId) {
1229
+ this.conversationId = conversationId;
1230
+ types.logger.debug("[CodexMCP] Conversation ID extracted from event:", this.conversationId);
1231
+ }
1232
+ }
1233
+ }
1234
+ extractIdentifiers(response) {
1235
+ const meta = response?.meta || {};
1236
+ const sessionId = meta.session_id ?? meta.sessionId ?? response?.session_id ?? response?.sessionId;
1237
+ if (sessionId) {
1238
+ this.sessionId = sessionId;
1239
+ types.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
1240
+ }
1241
+ const conversationId = meta.conversation_id ?? meta.conversationId ?? response?.conversation_id ?? response?.conversationId;
1242
+ if (conversationId) {
1243
+ this.conversationId = conversationId;
1244
+ types.logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
1245
+ }
1246
+ const content = response?.content;
1247
+ if (Array.isArray(content)) {
1248
+ for (const item of content) {
1249
+ const contentSessionId = item?.session_id ?? item?.sessionId;
1250
+ if (!this.sessionId && contentSessionId) {
1251
+ this.sessionId = contentSessionId;
1252
+ types.logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
1253
+ }
1254
+ const contentConversationId = item?.conversation_id ?? item?.conversationId;
1255
+ if (!this.conversationId && contentConversationId) {
1256
+ this.conversationId = contentConversationId;
1257
+ types.logger.debug("[CodexMCP] Conversation ID extracted from content:", this.conversationId);
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+ getSessionId() {
1263
+ return this.sessionId;
1264
+ }
1265
+ getConversationId() {
1266
+ return this.conversationId;
1267
+ }
1268
+ hasActiveSession() {
1269
+ return this.sessionId !== null;
1270
+ }
1271
+ clearSession() {
1272
+ const previousSessionId = this.sessionId;
1273
+ this.sessionId = null;
1274
+ this.conversationId = null;
1275
+ types.logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
1276
+ }
1277
+ /**
1278
+ * Store the current session ID without clearing it, useful for abort handling
1279
+ */
1280
+ storeSessionForResume() {
1281
+ types.logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
1282
+ return this.sessionId;
1283
+ }
1284
+ /**
1285
+ * Store the current conversation ID, useful for resuming from the local Codex transcript.
1286
+ */
1287
+ storeConversationForResume() {
1288
+ types.logger.debug("[CodexMCP] Storing conversation for potential resume:", this.conversationId);
1289
+ return this.conversationId;
1290
+ }
1291
+ async disconnect() {
1292
+ if (!this.connected) return;
1293
+ const pid = this.transport?.pid ?? null;
1294
+ types.logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
1295
+ try {
1296
+ types.logger.debug("[CodexMCP] client.close begin");
1297
+ await this.client.close();
1298
+ types.logger.debug("[CodexMCP] client.close done");
1299
+ } catch (e) {
1300
+ types.logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
1301
+ try {
1302
+ types.logger.debug("[CodexMCP] transport.close begin");
1303
+ await this.transport?.close?.();
1304
+ types.logger.debug("[CodexMCP] transport.close done");
1305
+ } catch (error) {
1306
+ console.error("[CodexMCP] Error closing transport:", error);
1307
+ types.logger.debug("[CodexMCP] Error closing transport:", error);
1308
+ }
1309
+ }
1310
+ if (pid) {
1311
+ try {
1312
+ process.kill(pid, 0);
1313
+ types.logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
1314
+ try {
1315
+ process.kill(pid, "SIGKILL");
1316
+ } catch (error) {
1317
+ const e = error;
1318
+ if (e?.code !== "ESRCH") {
1319
+ console.error("[CodexMCP] Failed to SIGKILL child process:", pid, error);
1320
+ types.logger.debug("[CodexMCP] Failed to SIGKILL child process:", pid, error);
1321
+ }
1322
+ }
1323
+ } catch (error) {
1324
+ const e = error;
1325
+ if (e?.code !== "ESRCH") {
1326
+ console.error("[CodexMCP] Failed to check child process status:", pid, error);
1327
+ types.logger.debug("[CodexMCP] Failed to check child process status:", pid, error);
1328
+ }
1329
+ }
1330
+ }
1331
+ this.transport = null;
1332
+ this.connected = false;
1333
+ this.sessionId = null;
1334
+ this.conversationId = null;
1335
+ this.authSnapshot = null;
1336
+ types.logger.debug("[CodexMCP] Disconnected");
1337
+ }
1338
+ }
1339
+
1340
+ class CodexPermissionHandler {
1341
+ pendingRequests = /* @__PURE__ */ new Map();
1342
+ aliasToPrimary = /* @__PURE__ */ new Map();
1343
+ session;
1344
+ permissionMode = "default";
1345
+ elicitationHub = null;
1346
+ constructor(session, options) {
1347
+ this.session = session;
1348
+ this.elicitationHub = options?.elicitationHub ?? null;
1349
+ this.setupRpcHandler();
1350
+ }
1351
+ setPermissionMode(mode) {
1352
+ this.permissionMode = mode;
1353
+ }
1354
+ /**
1355
+ * Handle a tool permission request
1356
+ * @param toolCallId - The unique ID of the tool call
1357
+ * @param toolName - The name of the tool being called
1358
+ * @param input - The input parameters for the tool
1359
+ * @param aliases - Additional IDs that might be used by clients when responding
1360
+ * @returns Promise resolving to permission result
1361
+ */
1362
+ async handleToolCall(toolCallId, toolName, input, aliases = []) {
1363
+ const enforced = this.enforceCoordinationGate(toolName, input);
1364
+ if (enforced) {
1365
+ this.emitPolicyCard(toolCallId, toolName, input, enforced);
1366
+ this.recordCompletedDecision(toolCallId, toolName, input, enforced);
1367
+ return enforced;
1368
+ }
1369
+ const auto = this.autoDecisionForToolCall(toolName, input);
1370
+ if (auto) {
1371
+ if (typeof auto?.then === "function") {
1372
+ const res2 = await auto;
1373
+ this.emitPolicyCard(toolCallId, toolName, input, res2);
1374
+ if (res2.decision !== "approved" && res2.decision !== "approved_for_session") {
1375
+ this.recordCompletedDecision(toolCallId, toolName, input, res2);
1376
+ }
1377
+ return res2;
1378
+ }
1379
+ const res = auto;
1380
+ this.emitPolicyCard(toolCallId, toolName, input, res);
1381
+ if (res.decision !== "approved" && res.decision !== "approved_for_session") {
1382
+ this.recordCompletedDecision(toolCallId, toolName, input, res);
1383
+ }
1384
+ return res;
1385
+ }
1386
+ this.emitPolicyCard(toolCallId, toolName, input, {
1387
+ decision: "abort",
1388
+ reason: "permission_prompt_required"
1389
+ });
1390
+ return new Promise((resolve, reject) => {
1391
+ const normalizedAliases = aliases.filter((id) => typeof id === "string" && id.length > 0 && id !== toolCallId);
1392
+ const allIds = [toolCallId, ...normalizedAliases];
1393
+ const pending = {
1394
+ resolve,
1395
+ reject,
1396
+ toolName,
1397
+ input,
1398
+ ids: allIds
1399
+ };
1400
+ for (const id of allIds) {
1401
+ this.pendingRequests.set(id, pending);
1402
+ this.aliasToPrimary.set(id, toolCallId);
1403
+ }
1404
+ this.session.updateAgentState((currentState) => ({
1405
+ ...currentState,
1406
+ requests: {
1407
+ ...currentState.requests,
1408
+ [toolCallId]: {
1409
+ tool: toolName,
1410
+ arguments: input,
1411
+ createdAt: Date.now()
1412
+ }
1413
+ }
1414
+ }));
1415
+ types.logger.debug(`[Codex] Permission request sent for tool: ${toolName} (${toolCallId})`);
1416
+ });
1417
+ }
1418
+ /**
1419
+ * Handle a structured user-input elicitation request (e.g. AskUserQuestion).
1420
+ * Returns an MCP-compatible elicitation result to send back to codex-cli.
1421
+ */
1422
+ async handleElicitation(toolCallId, input, options) {
1423
+ const callId = toolCallId || node_crypto.randomUUID();
1424
+ if (!this.elicitationHub) {
1425
+ throw new Error("Elicitation hub not configured for this session");
1426
+ }
1427
+ this.session.sendCodexMessage({
1428
+ type: "tool-call",
1429
+ name: "AskUserQuestion",
1430
+ callId,
1431
+ input: input ?? {},
1432
+ id: node_crypto.randomUUID()
1433
+ });
1434
+ this.session.sendCodexMessage({
1435
+ type: "tool-call-result",
1436
+ callId,
1437
+ output: {
1438
+ status: "awaiting_user_message",
1439
+ hint: "User will answer via a new chat message when ready."
1440
+ },
1441
+ is_error: false,
1442
+ id: node_crypto.randomUUID()
1443
+ });
1444
+ return { action: "accept", decision: "denied" };
1445
+ }
1446
+ autoDecisionForToolCall(toolName, input) {
1447
+ const mode = this.permissionMode;
1448
+ if (mode === "always-ask") {
1449
+ return null;
1450
+ }
1451
+ if (mode === "default") {
1452
+ return null;
1453
+ }
1454
+ if (mode === "read-only") {
1455
+ return { decision: "denied", reason: "read_only_mode" };
1456
+ }
1457
+ if (mode === "safe-yolo" || mode === "yolo") {
1458
+ return this.autoApproveWithQuota(toolName, input);
1459
+ }
1460
+ return null;
1461
+ }
1462
+ async autoApproveWithQuota(toolName, _input) {
1463
+ const decision = "approved_for_session";
1464
+ if (index.shouldCountToolCall(toolName)) {
1465
+ try {
1466
+ const quota = await index.consumeToolQuota({ token: this.session.getAuthToken(), toolName });
1467
+ if (quota.allowed !== true) {
1468
+ return { decision: "denied", reason: index.formatQuotaDeniedReason(quota) };
1469
+ }
1470
+ } catch (err) {
1471
+ return { decision: "denied", reason: err?.message || "Tool quota check failed" };
1472
+ }
1473
+ }
1474
+ return { decision };
1475
+ }
1476
+ /**
1477
+ * Setup RPC handler for permission responses
1478
+ */
1479
+ setupRpcHandler() {
1480
+ this.session.rpcHandlerManager.registerHandler(
1481
+ "permission",
1482
+ async (response) => {
1483
+ const responseId = response?.id;
1484
+ const primaryId = (responseId && this.aliasToPrimary.get(responseId)) ?? responseId;
1485
+ const pending = (primaryId && this.pendingRequests.get(primaryId)) ?? (responseId && this.pendingRequests.get(responseId));
1486
+ if (!pending) {
1487
+ types.logger.debug("[Codex] Permission request not found or already resolved");
1488
+ return { ok: true };
1489
+ }
1490
+ for (const id of pending.ids) {
1491
+ this.pendingRequests.delete(id);
1492
+ this.aliasToPrimary.delete(id);
1493
+ }
1494
+ let approved = response.approved;
1495
+ let reason;
1496
+ let deniedByQuota = false;
1497
+ if (approved && index.shouldCountToolCall(pending.toolName)) {
1498
+ try {
1499
+ const quota = await index.consumeToolQuota({ token: this.session.getAuthToken(), toolName: pending.toolName });
1500
+ if (quota.allowed !== true) {
1501
+ approved = false;
1502
+ reason = index.formatQuotaDeniedReason(quota);
1503
+ deniedByQuota = true;
1504
+ }
1505
+ } catch (err) {
1506
+ approved = false;
1507
+ reason = err?.message || "Tool quota check failed";
1508
+ deniedByQuota = true;
1509
+ }
1510
+ }
1511
+ const decision = approved ? response.decision === "approved_for_session" ? "approved_for_session" : "approved" : deniedByQuota ? "denied" : response.decision === "denied" ? "denied" : "abort";
1512
+ const result = { decision, reason };
1513
+ this.emitPolicyCard(primaryId ?? responseId ?? pending.ids[0] ?? node_crypto.randomUUID(), pending.toolName, pending.input, result);
1514
+ pending.resolve(result);
1515
+ this.session.updateAgentState((currentState) => {
1516
+ const requestKey = primaryId ?? responseId;
1517
+ if (!requestKey) return currentState;
1518
+ const request = currentState.requests?.[requestKey];
1519
+ if (!request) return currentState;
1520
+ const remainingRequests = { ...currentState.requests || {} };
1521
+ delete remainingRequests[requestKey];
1522
+ let res = {
1523
+ ...currentState,
1524
+ requests: remainingRequests,
1525
+ completedRequests: {
1526
+ ...currentState.completedRequests,
1527
+ [requestKey]: {
1528
+ ...request,
1529
+ completedAt: Date.now(),
1530
+ status: approved ? "approved" : "denied",
1531
+ decision: result.decision,
1532
+ reason
1533
+ }
1534
+ }
1535
+ };
1536
+ return res;
1537
+ });
1538
+ types.logger.debug(`[Codex] Permission ${approved ? "approved" : "denied"} for ${pending.toolName}`);
1539
+ return { ok: true };
1540
+ }
1541
+ );
1542
+ }
1543
+ /**
1544
+ * Reset state for new sessions
1545
+ */
1546
+ reset() {
1547
+ for (const [id, pending] of this.pendingRequests.entries()) {
1548
+ pending.reject(new Error("Session reset"));
1549
+ }
1550
+ this.pendingRequests.clear();
1551
+ this.aliasToPrimary.clear();
1552
+ this.session.updateAgentState((currentState) => {
1553
+ const pendingRequests = currentState.requests || {};
1554
+ const completedRequests = { ...currentState.completedRequests };
1555
+ for (const [id, request] of Object.entries(pendingRequests)) {
1556
+ completedRequests[id] = {
1557
+ ...request,
1558
+ completedAt: Date.now(),
1559
+ status: "canceled",
1560
+ reason: "Session reset"
1561
+ };
1562
+ }
1563
+ return {
1564
+ ...currentState,
1565
+ requests: {},
1566
+ completedRequests
1567
+ };
1568
+ });
1569
+ types.logger.debug("[Codex] Permission handler reset");
1570
+ }
1571
+ enforceCoordinationGate(toolName, input) {
1572
+ const hasContext = typeof this.session?.hasCoordinationContext === "function" ? this.session.hasCoordinationContext() : false;
1573
+ if (!hasContext) return null;
1574
+ if (toolName !== "CodexPatch") {
1575
+ return null;
1576
+ }
1577
+ const didReadDocsIndex = typeof this.session?.didReadDocsIndexWithin === "function" ? this.session.didReadDocsIndexWithin(365 * 24 * 60 * 6e4) : false;
1578
+ if (!didReadDocsIndex) {
1579
+ return {
1580
+ decision: "denied",
1581
+ reason: "docs_index_read_required"
1582
+ };
1583
+ }
1584
+ const maxAgeMs = 15 * 6e4;
1585
+ const didReadLedger = typeof this.session?.didReadCoordinationLedgerWithin === "function" ? this.session.didReadCoordinationLedgerWithin(maxAgeMs) : false;
1586
+ if (!didReadLedger) {
1587
+ return {
1588
+ decision: "denied",
1589
+ reason: "ledger_read_required"
1590
+ };
1591
+ }
1592
+ const changes = input?.changes ?? input?.input?.changes;
1593
+ const filePaths = changes && typeof changes === "object" ? Object.keys(changes).map(normalizeToolFilePath).filter((v) => Boolean(v)) : [];
1594
+ if (filePaths.length === 0) return null;
1595
+ const guard = this.session?.coordinationLeaseGuard;
1596
+ const hasClaim = (filePath) => {
1597
+ try {
1598
+ return Boolean(guard && typeof guard.has === "function" && guard.has(filePath));
1599
+ } catch (error) {
1600
+ console.error("[permissionHandler] coordinationLeaseGuard.has threw:", error);
1601
+ types.logger.debug("[permissionHandler] coordinationLeaseGuard.has threw:", error);
1602
+ return false;
1603
+ }
1604
+ };
1605
+ const missing = filePaths.filter((p) => !hasClaim(p));
1606
+ if (missing.length > 0) {
1607
+ let debugClaims = "";
1608
+ try {
1609
+ const snapshot = guard && typeof guard.snapshot === "function" ? guard.snapshot() : [];
1610
+ const sample = Array.isArray(snapshot) ? snapshot.slice(0, 12).map((v) => String(v?.file ?? v)).filter(Boolean) : [];
1611
+ if (sample.length > 0) debugClaims = ` (have=${sample.join(", ")})`;
1612
+ } catch (error) {
1613
+ console.error("[permissionHandler] Failed to snapshot coordination lease guard:", error);
1614
+ types.logger.debug("[permissionHandler] Failed to snapshot coordination lease guard:", error);
1615
+ }
1616
+ return {
1617
+ decision: "denied",
1618
+ reason: `file_claim_required:${missing[0]}${debugClaims}`
1619
+ };
1620
+ }
1621
+ return null;
1622
+ }
1623
+ recordCompletedDecision(toolCallId, toolName, input, result) {
1624
+ const status = result.decision === "approved" || result.decision === "approved_for_session" ? "approved" : "denied";
1625
+ const completedAt = Date.now();
1626
+ const reason = result.reason;
1627
+ const decision = result.decision;
1628
+ this.session.updateAgentState((currentState) => ({
1629
+ ...currentState,
1630
+ completedRequests: {
1631
+ ...currentState.completedRequests || {},
1632
+ [toolCallId]: {
1633
+ tool: toolName,
1634
+ arguments: input,
1635
+ createdAt: completedAt,
1636
+ completedAt,
1637
+ status,
1638
+ decision,
1639
+ reason
1640
+ }
1641
+ }
1642
+ }));
1643
+ }
1644
+ emitPolicyCard(toolCallId, toolName, input, result) {
1645
+ const decision = result.decision;
1646
+ const hasContext = typeof this.session?.hasCoordinationContext === "function" ? this.session.hasCoordinationContext() : false;
1647
+ const readAt = typeof this.session?.getCoordinationLedgerReadAt === "function" ? this.session.getCoordinationLedgerReadAt() : 0;
1648
+ const ageMs = readAt > 0 ? Date.now() - readAt : null;
1649
+ let claims = [];
1650
+ try {
1651
+ const guard = this.session?.coordinationLeaseGuard;
1652
+ claims = guard && typeof guard.snapshot === "function" ? guard.snapshot().slice(0, 50) : [];
1653
+ } catch {
1654
+ claims = [];
1655
+ }
1656
+ const gate = describeCoordinationGate({
1657
+ hasCoordinationContext: hasContext,
1658
+ toolName,
1659
+ input,
1660
+ ledgerReadAt: readAt || null,
1661
+ claimSnapshot: claims
1662
+ });
1663
+ const hint = buildPolicyHint(result.reason, decision, gate);
1664
+ const callId = `policy:${toolCallId}:${node_crypto.randomUUID().slice(0, 8)}`;
1665
+ const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && result.reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
1666
+ const payload = {
1667
+ kind,
1668
+ blockedTool: toolName,
1669
+ summary: hint.summary,
1670
+ nextSteps: hint.nextSteps,
1671
+ toolName,
1672
+ decision,
1673
+ reason: result.reason,
1674
+ permissionMode: this.permissionMode,
1675
+ hasCoordinationContext: hasContext,
1676
+ ledgerReadAt: readAt || null,
1677
+ ledgerReadAgeMs: ageMs,
1678
+ claimSnapshot: claims,
1679
+ claimedFiles: formatClaimSnapshotFiles(claims),
1680
+ coordinationGate: gate,
1681
+ inputPreviewKeys: input && typeof input === "object" ? Object.keys(input).slice(0, 20) : null
1682
+ };
1683
+ try {
1684
+ this.session.sendCodexMessage({
1685
+ type: "tool-call",
1686
+ name: "FlockbayPolicy",
1687
+ callId,
1688
+ input: payload,
1689
+ id: node_crypto.randomUUID()
1690
+ });
1691
+ this.session.sendCodexMessage({
1692
+ type: "tool-call-result",
1693
+ callId,
1694
+ output: payload,
1695
+ is_error: kind === "policy_block",
1696
+ id: node_crypto.randomUUID()
1697
+ });
1698
+ } catch (err) {
1699
+ types.logger.debug("[Codex] Failed to emit inline policy card", err);
1700
+ }
1701
+ }
1702
+ }
1703
+ function normalizeToolFilePath(input) {
1704
+ const raw = typeof input === "string" ? input.trim() : String(input ?? "").trim();
1705
+ if (!raw) return null;
1706
+ let candidate = raw.replace(/\\/g, "/");
1707
+ candidate = candidate.replace(/^\.\/+/, "").replace(/^\/+/, "");
1708
+ if (!candidate) return null;
1709
+ if (candidate.includes("..")) return null;
1710
+ return candidate;
1711
+ }
1712
+ function buildPolicyHint(reason, decision, gate) {
1713
+ const raw = typeof reason === "string" ? reason.trim() : "";
1714
+ if (!raw) {
1715
+ if (decision === "approved" || decision === "approved_for_session") {
1716
+ return {
1717
+ summary: "Allowed.",
1718
+ nextSteps: []
1719
+ };
1720
+ }
1721
+ return {
1722
+ summary: "Blocked by policy.",
1723
+ nextSteps: []
1724
+ };
1725
+ }
1726
+ if (raw === "permission_prompt_required") {
1727
+ return {
1728
+ summary: "Waiting for permission to run this tool.",
1729
+ nextSteps: []
1730
+ };
1731
+ }
1732
+ if (raw === "ledger_read_required") {
1733
+ return {
1734
+ summary: "Blocked by policy (automatic; not the user): read the ledger before making file edits.",
1735
+ nextSteps: [
1736
+ "Call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`).",
1737
+ "Then retry the file edit."
1738
+ ]
1739
+ };
1740
+ }
1741
+ if (raw === "docs_index_read_required") {
1742
+ return {
1743
+ summary: "Blocked by policy (automatic; not the user): read the game Documentation index before making edits.",
1744
+ nextSteps: [
1745
+ "Call `mcp__flockbay__docs_index_read`.",
1746
+ "Then retry the edit."
1747
+ ]
1748
+ };
1749
+ }
1750
+ if (raw.startsWith("file_claim_required:")) {
1751
+ const withoutPrefix = raw.slice("file_claim_required:".length);
1752
+ const file = withoutPrefix.split("(")[0]?.trim() || "the file";
1753
+ return {
1754
+ summary: `Blocked by policy (automatic; not the user): claim \`${file}\` before editing it.`,
1755
+ nextSteps: [
1756
+ `Claim the file via \`mcp__flockbay__ledger_claim\` or \`mcp__flockbay__coordination_claim_files\` (files: ["${file}"]).`,
1757
+ "Then retry the file edit."
1758
+ ]
1759
+ };
1760
+ }
1761
+ if (raw === "read_only_mode") {
1762
+ return {
1763
+ summary: "Blocked by policy (automatic; not the user): this session is in read-only mode.",
1764
+ nextSteps: [
1765
+ "Switch permission mode to allow edits (disable read-only).",
1766
+ "Then retry the action."
1767
+ ]
1768
+ };
1769
+ }
1770
+ if (raw.toLowerCase().includes("quota")) {
1771
+ return {
1772
+ summary: `Blocked: tool quota denied (${raw}).`,
1773
+ nextSteps: [
1774
+ "Wait and retry later, or switch to a different model/provider."
1775
+ ]
1776
+ };
1777
+ }
1778
+ if (decision === "approved" || decision === "approved_for_session") {
1779
+ if (gate.hasCoordinationContext && gate.toolName === "CodexPatch") {
1780
+ const files = gate.requestedFiles.length > 0 ? ` (${gate.requestedFiles.join(", ")})` : "";
1781
+ return {
1782
+ summary: `Allowed: file edit passed ledger + claim checks${files}.`,
1783
+ nextSteps: []
1784
+ };
1785
+ }
1786
+ return { summary: "Allowed.", nextSteps: [] };
1787
+ }
1788
+ return {
1789
+ summary: `Blocked: ${raw}`,
1790
+ nextSteps: []
1791
+ };
1792
+ }
1793
+ function formatClaimSnapshotFiles(snapshot) {
1794
+ if (!Array.isArray(snapshot)) return [];
1795
+ const files = [];
1796
+ for (const entry of snapshot) {
1797
+ if (!entry) continue;
1798
+ if (typeof entry === "string") {
1799
+ files.push(entry);
1800
+ continue;
1801
+ }
1802
+ const file = entry?.file;
1803
+ if (typeof file === "string" && file.trim()) files.push(file.trim());
1804
+ }
1805
+ return files.slice(0, 50);
1806
+ }
1807
+ function describeCoordinationGate(args) {
1808
+ const ledgerReadAt = typeof args.ledgerReadAt === "number" && args.ledgerReadAt > 0 ? args.ledgerReadAt : null;
1809
+ const ledgerReadAgeMs = ledgerReadAt ? Date.now() - ledgerReadAt : null;
1810
+ const changes = args.input?.changes ?? args.input?.input?.changes;
1811
+ const requestedFiles = changes && typeof changes === "object" ? Object.keys(changes).map(normalizeToolFilePath).filter((v) => Boolean(v)) : [];
1812
+ const claimedFilesAll = formatClaimSnapshotFiles(args.claimSnapshot);
1813
+ const claimedSet = new Set(claimedFilesAll);
1814
+ const missingClaims = requestedFiles.filter((p) => !claimedSet.has(p));
1815
+ const claimedFilesSample = claimedFilesAll.slice(0, 12);
1816
+ return {
1817
+ hasCoordinationContext: args.hasCoordinationContext,
1818
+ toolName: args.toolName,
1819
+ ledgerReadAt,
1820
+ ledgerReadAgeMs,
1821
+ requestedFiles,
1822
+ missingClaims,
1823
+ claimedFilesSample
1824
+ };
1825
+ }
1826
+
1827
+ class ReasoningProcessor {
1828
+ accumulator = "";
1829
+ inTitleCapture = false;
1830
+ titleBuffer = "";
1831
+ contentBuffer = "";
1832
+ hasTitle = false;
1833
+ currentCallId = null;
1834
+ toolCallStarted = false;
1835
+ currentTitle = null;
1836
+ onMessage = null;
1837
+ constructor(onMessage) {
1838
+ this.onMessage = onMessage || null;
1839
+ this.reset();
1840
+ }
1841
+ /**
1842
+ * Set the message callback for sending messages directly
1843
+ */
1844
+ setMessageCallback(callback) {
1845
+ this.onMessage = callback;
1846
+ }
1847
+ /**
1848
+ * Process a reasoning section break - indicates a new reasoning section is starting
1849
+ */
1850
+ handleSectionBreak() {
1851
+ this.finishCurrentToolCall("canceled");
1852
+ this.resetState();
1853
+ types.logger.debug("[ReasoningProcessor] Section break - reset state");
1854
+ }
1855
+ /**
1856
+ * Process a reasoning delta and accumulate content
1857
+ */
1858
+ processDelta(delta) {
1859
+ this.accumulator += delta;
1860
+ if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
1861
+ if (this.accumulator.startsWith("**")) {
1862
+ this.inTitleCapture = true;
1863
+ this.titleBuffer = this.accumulator.substring(2);
1864
+ types.logger.debug("[ReasoningProcessor] Started title capture");
1865
+ } else if (this.accumulator.length > 0) {
1866
+ this.contentBuffer = this.accumulator;
1867
+ }
1868
+ } else if (this.inTitleCapture) {
1869
+ this.titleBuffer = this.accumulator.substring(2);
1870
+ const titleEndIndex = this.titleBuffer.indexOf("**");
1871
+ if (titleEndIndex !== -1) {
1872
+ const title = this.titleBuffer.substring(0, titleEndIndex);
1873
+ const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
1874
+ this.hasTitle = true;
1875
+ this.inTitleCapture = false;
1876
+ this.currentTitle = title;
1877
+ this.contentBuffer = afterTitle;
1878
+ this.currentCallId = node_crypto.randomUUID();
1879
+ types.logger.debug(`[ReasoningProcessor] Title captured: "${title}"`);
1880
+ this.sendToolCallStart(title);
1881
+ }
1882
+ } else if (this.hasTitle) {
1883
+ this.contentBuffer = this.accumulator.substring(
1884
+ this.accumulator.indexOf("**") + 2 + this.currentTitle.length + 2
1885
+ );
1886
+ } else {
1887
+ this.contentBuffer = this.accumulator;
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Send the tool call start message
1892
+ */
1893
+ sendToolCallStart(title) {
1894
+ if (!this.currentCallId || this.toolCallStarted) {
1895
+ return;
1896
+ }
1897
+ const toolCall = {
1898
+ type: "tool-call",
1899
+ name: "CodexReasoning",
1900
+ callId: this.currentCallId,
1901
+ input: {
1902
+ title
1903
+ },
1904
+ id: node_crypto.randomUUID()
1905
+ };
1906
+ types.logger.debug(`[ReasoningProcessor] Sending tool call start for: "${title}"`);
1907
+ this.onMessage?.(toolCall);
1908
+ this.toolCallStarted = true;
1909
+ }
1910
+ /**
1911
+ * Complete the reasoning section with final text
1912
+ */
1913
+ complete(fullText) {
1914
+ let title;
1915
+ let content = fullText;
1916
+ if (fullText.startsWith("**")) {
1917
+ const titleEndIndex = fullText.indexOf("**", 2);
1918
+ if (titleEndIndex !== -1) {
1919
+ title = fullText.substring(2, titleEndIndex);
1920
+ content = fullText.substring(titleEndIndex + 2).trim();
1921
+ }
1922
+ }
1923
+ types.logger.debug(`[ReasoningProcessor] Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
1924
+ if (title && !this.toolCallStarted) {
1925
+ this.currentCallId = this.currentCallId || node_crypto.randomUUID();
1926
+ this.sendToolCallStart(title);
1927
+ }
1928
+ if (this.toolCallStarted && this.currentCallId) {
1929
+ const toolResult = {
1930
+ type: "tool-call-result",
1931
+ callId: this.currentCallId,
1932
+ output: {
1933
+ content,
1934
+ status: "completed"
1935
+ },
1936
+ id: node_crypto.randomUUID()
1937
+ };
1938
+ types.logger.debug("[ReasoningProcessor] Sending tool call result");
1939
+ this.onMessage?.(toolResult);
1940
+ } else {
1941
+ const reasoningMessage = {
1942
+ type: "reasoning",
1943
+ message: content,
1944
+ id: node_crypto.randomUUID()
1945
+ };
1946
+ types.logger.debug("[ReasoningProcessor] Sending reasoning message");
1947
+ this.onMessage?.(reasoningMessage);
1948
+ }
1949
+ this.resetState();
1950
+ }
1951
+ /**
1952
+ * Abort the current reasoning section
1953
+ */
1954
+ abort() {
1955
+ types.logger.debug("[ReasoningProcessor] Abort called");
1956
+ this.finishCurrentToolCall("canceled");
1957
+ this.resetState();
1958
+ }
1959
+ /**
1960
+ * Reset the processor state
1961
+ */
1962
+ reset() {
1963
+ this.finishCurrentToolCall("canceled");
1964
+ this.resetState();
1965
+ }
1966
+ /**
1967
+ * Finish current tool call if one is in progress
1968
+ */
1969
+ finishCurrentToolCall(status) {
1970
+ if (this.toolCallStarted && this.currentCallId) {
1971
+ const toolResult = {
1972
+ type: "tool-call-result",
1973
+ callId: this.currentCallId,
1974
+ output: {
1975
+ content: this.contentBuffer || "",
1976
+ status
1977
+ },
1978
+ id: node_crypto.randomUUID()
1979
+ };
1980
+ types.logger.debug(`[ReasoningProcessor] Sending tool call result with status: ${status}`);
1981
+ this.onMessage?.(toolResult);
1982
+ }
1983
+ }
1984
+ /**
1985
+ * Reset internal state
1986
+ */
1987
+ resetState() {
1988
+ this.accumulator = "";
1989
+ this.inTitleCapture = false;
1990
+ this.titleBuffer = "";
1991
+ this.contentBuffer = "";
1992
+ this.hasTitle = false;
1993
+ this.currentCallId = null;
1994
+ this.toolCallStarted = false;
1995
+ this.currentTitle = null;
1996
+ }
1997
+ /**
1998
+ * Get the current call ID for tool result matching
1999
+ */
2000
+ getCurrentCallId() {
2001
+ return this.currentCallId;
2002
+ }
2003
+ /**
2004
+ * Check if a tool call has been started
2005
+ */
2006
+ hasStartedToolCall() {
2007
+ return this.toolCallStarted;
2008
+ }
2009
+ }
2010
+
2011
+ class DiffProcessor {
2012
+ previousDiff = null;
2013
+ onMessage = null;
2014
+ constructor(onMessage) {
2015
+ this.onMessage = onMessage || null;
2016
+ }
2017
+ /**
2018
+ * Process a turn_diff message and check if the unified_diff has changed
2019
+ */
2020
+ processDiff(unifiedDiff) {
2021
+ if (this.previousDiff !== unifiedDiff) {
2022
+ types.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
2023
+ const callId = node_crypto.randomUUID();
2024
+ const toolCall = {
2025
+ type: "tool-call",
2026
+ name: "CodexDiff",
2027
+ callId,
2028
+ input: {
2029
+ unified_diff: unifiedDiff
2030
+ },
2031
+ id: node_crypto.randomUUID()
2032
+ };
2033
+ this.onMessage?.(toolCall);
2034
+ const toolResult = {
2035
+ type: "tool-call-result",
2036
+ callId,
2037
+ output: {
2038
+ status: "completed"
2039
+ },
2040
+ id: node_crypto.randomUUID()
2041
+ };
2042
+ this.onMessage?.(toolResult);
2043
+ }
2044
+ this.previousDiff = unifiedDiff;
2045
+ types.logger.debug("[DiffProcessor] Updated stored diff");
2046
+ }
2047
+ /**
2048
+ * Reset the processor state (called on task_complete or turn_aborted)
2049
+ */
2050
+ reset() {
2051
+ types.logger.debug("[DiffProcessor] Resetting diff state");
2052
+ this.previousDiff = null;
2053
+ }
2054
+ /**
2055
+ * Set the message callback for sending messages directly
2056
+ */
2057
+ setMessageCallback(callback) {
2058
+ this.onMessage = callback;
2059
+ }
2060
+ /**
2061
+ * Get the current diff value
2062
+ */
2063
+ getCurrentDiff() {
2064
+ return this.previousDiff;
2065
+ }
2066
+ }
2067
+
2068
+ const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
2069
+ const [messages, setMessages] = React.useState([]);
2070
+ const [confirmationMode, setConfirmationMode] = React.useState(false);
2071
+ const [actionInProgress, setActionInProgress] = React.useState(false);
2072
+ const confirmationTimeoutRef = React.useRef(null);
2073
+ const { stdout } = ink.useStdout();
2074
+ const terminalWidth = stdout.columns || 80;
2075
+ const terminalHeight = stdout.rows || 24;
2076
+ React.useEffect(() => {
2077
+ setMessages(messageBuffer.getMessages());
2078
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
2079
+ setMessages(newMessages);
2080
+ });
2081
+ return () => {
2082
+ unsubscribe();
2083
+ if (confirmationTimeoutRef.current) {
2084
+ clearTimeout(confirmationTimeoutRef.current);
2085
+ }
2086
+ };
2087
+ }, [messageBuffer]);
2088
+ const resetConfirmation = React.useCallback(() => {
2089
+ setConfirmationMode(false);
2090
+ if (confirmationTimeoutRef.current) {
2091
+ clearTimeout(confirmationTimeoutRef.current);
2092
+ confirmationTimeoutRef.current = null;
2093
+ }
2094
+ }, []);
2095
+ const setConfirmationWithTimeout = React.useCallback(() => {
2096
+ setConfirmationMode(true);
2097
+ if (confirmationTimeoutRef.current) {
2098
+ clearTimeout(confirmationTimeoutRef.current);
2099
+ }
2100
+ confirmationTimeoutRef.current = setTimeout(() => {
2101
+ resetConfirmation();
2102
+ }, 15e3);
2103
+ }, [resetConfirmation]);
2104
+ ink.useInput(React.useCallback(async (input, key) => {
2105
+ if (actionInProgress) return;
2106
+ if (key.ctrl && input === "c") {
2107
+ if (confirmationMode) {
2108
+ resetConfirmation();
2109
+ setActionInProgress(true);
2110
+ await new Promise((resolve) => setTimeout(resolve, 100));
2111
+ onExit?.();
2112
+ } else {
2113
+ setConfirmationWithTimeout();
2114
+ }
2115
+ return;
2116
+ }
2117
+ if (confirmationMode) {
2118
+ resetConfirmation();
2119
+ }
2120
+ }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
2121
+ const getMessageColor = (type) => {
2122
+ switch (type) {
2123
+ case "user":
2124
+ return "magenta";
2125
+ case "assistant":
2126
+ return "cyan";
2127
+ case "system":
2128
+ return "blue";
2129
+ case "tool":
2130
+ return "yellow";
2131
+ case "result":
2132
+ return "green";
2133
+ case "status":
2134
+ return "gray";
2135
+ default:
2136
+ return "white";
2137
+ }
2138
+ };
2139
+ const formatMessage = (msg) => {
2140
+ const lines = msg.content.split("\n");
2141
+ const maxLineLength = terminalWidth - 10;
2142
+ return lines.map((line) => {
2143
+ if (line.length <= maxLineLength) return line;
2144
+ const chunks = [];
2145
+ for (let i = 0; i < line.length; i += maxLineLength) {
2146
+ chunks.push(line.slice(i, i + maxLineLength));
2147
+ }
2148
+ return chunks.join("\n");
2149
+ }).join("\n");
2150
+ };
2151
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
2152
+ ink.Box,
2153
+ {
2154
+ flexDirection: "column",
2155
+ width: terminalWidth,
2156
+ height: terminalHeight - 4,
2157
+ borderStyle: "round",
2158
+ borderColor: "gray",
2159
+ paddingX: 1,
2160
+ overflow: "hidden"
2161
+ },
2162
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
2163
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
2164
+ // Show only the last messages that fit in the available space
2165
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
2166
+ ))
2167
+ ), /* @__PURE__ */ React.createElement(
2168
+ ink.Box,
2169
+ {
2170
+ width: terminalWidth,
2171
+ borderStyle: "round",
2172
+ borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
2173
+ paddingX: 2,
2174
+ justifyContent: "center",
2175
+ alignItems: "center",
2176
+ flexDirection: "column"
2177
+ },
2178
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
2179
+ ));
2180
+ };
2181
+
2182
+ function hashCodexSessionMode(mode) {
2183
+ return index.hashObject({
2184
+ model: mode.model ?? null,
2185
+ appendSystemPrompt: mode.appendSystemPrompt ?? null
2186
+ });
2187
+ }
2188
+ function hashCodexSessionModeFromEnhancedMode(mode) {
2189
+ return hashCodexSessionMode({ model: mode.model, appendSystemPrompt: mode.appendSystemPrompt });
2190
+ }
2191
+
2192
+ function readCodexAuthIsChatGptAccount() {
2193
+ try {
2194
+ const authPath = path.join(os.homedir(), ".codex", "auth.json");
2195
+ const raw = fs.readFileSync(authPath, "utf8");
2196
+ const parsed = JSON.parse(raw);
2197
+ const accessToken = parsed?.tokens?.access_token;
2198
+ if (typeof accessToken !== "string" || !accessToken) return false;
2199
+ return Boolean(parsed?.tokens?.account_id) && parsed?.OPENAI_API_KEY === null;
2200
+ } catch {
2201
+ return false;
2202
+ }
2203
+ }
2204
+ function parseEffortSuffix(model) {
2205
+ const m = String(model || "").trim();
2206
+ if (!m) return null;
2207
+ if (m.endsWith("-low")) return "low";
2208
+ if (m.endsWith("-medium")) return "medium";
2209
+ if (m.endsWith("-high")) return "high";
2210
+ return null;
2211
+ }
2212
+ function resolveCodexModelOverride(rawModel) {
2213
+ const model = typeof rawModel === "string" ? rawModel.trim() : "";
2214
+ if (!model) return {};
2215
+ const isChatGptAuth = readCodexAuthIsChatGptAccount();
2216
+ if (!isChatGptAuth) {
2217
+ return { config: { model } };
2218
+ }
2219
+ const effort = parseEffortSuffix(model);
2220
+ if (model === "gpt-5-minimal" || model.startsWith("gpt-5-codex-") || model.startsWith("gpt-5-")) {
2221
+ const nextEffort = effort ?? "medium";
2222
+ return {
2223
+ config: {
2224
+ model: "gpt-5.2",
2225
+ model_reasoning_effort: nextEffort
2226
+ }
2227
+ };
2228
+ }
2229
+ return { config: { model } };
2230
+ }
2231
+
2232
+ function emitReadyIfIdle({ pending, queueSize, shouldExit, sendReady, notify }) {
2233
+ if (shouldExit) {
2234
+ return false;
2235
+ }
2236
+ if (pending) {
2237
+ return false;
2238
+ }
2239
+ if (queueSize() > 0) {
2240
+ return false;
2241
+ }
2242
+ sendReady();
2243
+ notify?.();
2244
+ if (process.env.FLOCKBAY_AUTO_EXIT_ON_IDLE === "1") {
2245
+ setTimeout(() => process.exit(0), 50).unref();
2246
+ }
2247
+ return true;
2248
+ }
2249
+ const FLOCKBAY_CODEX_BASE_INSTRUCTIONS = index.trimIdent(`
2250
+ You have access to external tools via MCP (e.g. mcp__flockbay__*).
2251
+
2252
+ MCP tool naming rules:
2253
+ - MCP tools are named like \`mcp__flockbay__unreal_latest_screenshots\` (no \`functions.\` prefix).
2254
+ - If the user writes \`functions.mcp__...\`, treat it as a request for the MCP tool \`mcp__...\` (strip \`functions.\`) and call the tool.
2255
+
2256
+ Tool integrity rules:
2257
+ - Do NOT claim you called a tool unless you actually called it.
2258
+ - Do NOT fabricate tool outputs (including JSON payloads like { "views": [...] }).
2259
+ - When the user asks you to call a tool, you must call it instead of "simulating" results.
2260
+ - If you cannot call the requested tool, say so explicitly and stop (do not "approximate" the result).
2261
+ - If a tool output contains huge base64 blobs, do not paste them into chat; rely on the tool result card/UI instead.
2262
+
2263
+ Terminal rules:
2264
+ - If the user says "Do NOT use Terminal", do NOT call \`CodexBash\`. Use MCP tools only.
2265
+ `);
2266
+ function sanitizeCodexInstructions(input) {
2267
+ return input.replace(/\r\n/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
2268
+ }
2269
+ function normalizeUserToolAliases(text) {
2270
+ return String(text).replace(/\bfunctions\.mcp__/g, "mcp__");
2271
+ }
2272
+ async function runCodex(opts) {
2273
+ const sessionTag = node_crypto.randomUUID();
2274
+ const api = await types.ApiClient.create(opts.credentials);
2275
+ types.logger.debug(`[codex] Starting with options: startedBy=${opts.startedBy || "terminal"}`);
2276
+ const settings = await types.readSettings();
2277
+ let machineId = settings?.machineId;
2278
+ if (!machineId) {
2279
+ console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Support: coming soon.`);
2280
+ process.exit(1);
2281
+ }
2282
+ types.logger.debug(`Using machineId: ${machineId}`);
2283
+ try {
2284
+ const existing = await api.getMachine(machineId);
2285
+ await api.getOrCreateMachine({
2286
+ machineId,
2287
+ metadata: { ...existing.metadata || {}, ...index.initialMachineMetadata }
2288
+ });
2289
+ } catch (error) {
2290
+ const status = error?.response?.status;
2291
+ if (status === 404) {
2292
+ await api.getOrCreateMachine({ machineId, metadata: index.initialMachineMetadata });
2293
+ } else {
2294
+ throw error;
2295
+ }
2296
+ }
2297
+ let state = {
2298
+ controlledByUser: false
2299
+ };
2300
+ const coordinationProjectRootPath = process.env.FLOCKBAY_COORDINATION_PROJECT_ROOT_PATH || void 0;
2301
+ const coordinationWorkspaceProjectId = process.env.FLOCKBAY_COORDINATION_WORKSPACE_PROJECT_ID || void 0;
2302
+ const coordinationFeatureId = process.env.FLOCKBAY_COORDINATION_FEATURE_ID || void 0;
2303
+ const coordinationWorkItemId = process.env.FLOCKBAY_COORDINATION_WORK_ITEM_ID || void 0;
2304
+ let metadata = {
2305
+ path: process.cwd(),
2306
+ projectRootPath: coordinationProjectRootPath,
2307
+ host: os.hostname(),
2308
+ version: types.packageJson.version,
2309
+ os: os.platform(),
2310
+ machineId,
2311
+ workspaceProjectId: coordinationWorkspaceProjectId,
2312
+ featureId: coordinationFeatureId,
2313
+ workItemId: coordinationWorkItemId,
2314
+ homeDir: os.homedir(),
2315
+ flockbayHomeDir: types.configuration.flockbayHomeDir,
2316
+ flockbayLibDir: types.projectPath(),
2317
+ flockbayToolsDir: path.resolve(types.projectPath(), "tools", "unpacked"),
2318
+ startedFromDaemon: opts.startedBy === "daemon",
2319
+ hostPid: process.pid,
2320
+ startedBy: opts.startedBy || "terminal",
2321
+ // Initialize lifecycle state
2322
+ lifecycleState: "running",
2323
+ lifecycleStateSince: Date.now(),
2324
+ flavor: "codex"
2325
+ };
2326
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
2327
+ const session = api.sessionSyncClient(response);
2328
+ const elicitationHub = new index.ElicitationHub();
2329
+ const permissionHandler = new CodexPermissionHandler(session, { elicitationHub });
2330
+ try {
2331
+ types.logger.debug(`[START] Reporting session ${response.id} to daemon`);
2332
+ const result = await index.notifyDaemonSessionStarted(response.id, metadata);
2333
+ if (result.error) {
2334
+ types.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
2335
+ } else {
2336
+ types.logger.debug(`[START] Reported session ${response.id} to daemon`);
2337
+ }
2338
+ } catch (error) {
2339
+ types.logger.debug("[START] Failed to report to daemon (may not be running):", error);
2340
+ }
2341
+ const messageQueue = new index.MessageQueue2((mode) => hashCodexSessionModeFromEnhancedMode(mode));
2342
+ const autoPrompt = String(process.env.FLOCKBAY_AUTO_PROMPT || "").trim();
2343
+ if (autoPrompt) {
2344
+ messageQueue.push(autoPrompt, {
2345
+ permissionMode: "default",
2346
+ model: void 0,
2347
+ appendSystemPrompt: index.PLATFORM_SYSTEM_PROMPT
2348
+ });
2349
+ }
2350
+ const bypassUeGates = String(process.env.FLOCKBAY_DEV_BYPASS_UE_GATES || "") === "1";
2351
+ let currentPermissionMode = void 0;
2352
+ let currentModel = void 0;
2353
+ const defaultAppendSystemPrompt = index.PLATFORM_SYSTEM_PROMPT;
2354
+ let currentAppendSystemPrompt = defaultAppendSystemPrompt;
2355
+ session.onUserMessage((message) => {
2356
+ const images = Array.isArray(message.meta?.attachments?.images) ? message.meta.attachments.images : [];
2357
+ if (images.length > 0) {
2358
+ index.setLatestUserImages(images);
2359
+ }
2360
+ let messagePermissionMode = currentPermissionMode;
2361
+ if (message.meta?.permissionMode) {
2362
+ const validModes = ["default", "always-ask", "read-only", "safe-yolo", "yolo"];
2363
+ if (validModes.includes(message.meta.permissionMode)) {
2364
+ messagePermissionMode = message.meta.permissionMode;
2365
+ currentPermissionMode = messagePermissionMode;
2366
+ permissionHandler.setPermissionMode(currentPermissionMode);
2367
+ types.logger.debug(`[Codex] Permission mode updated from user message to: ${currentPermissionMode}`);
2368
+ } else {
2369
+ types.logger.debug(`[Codex] Invalid permission mode received: ${message.meta.permissionMode}`);
2370
+ }
2371
+ } else {
2372
+ types.logger.debug(`[Codex] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
2373
+ }
2374
+ let messageModel = currentModel;
2375
+ if (message.meta?.hasOwnProperty("model")) {
2376
+ messageModel = message.meta.model || void 0;
2377
+ currentModel = messageModel;
2378
+ types.logger.debug(`[Codex] Model updated from user message: ${messageModel || "reset to default"}`);
2379
+ } else {
2380
+ types.logger.debug(`[Codex] User message received with no model override, using current: ${currentModel || "default"}`);
2381
+ }
2382
+ let messageAppendSystemPrompt = currentAppendSystemPrompt;
2383
+ if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
2384
+ const raw = message.meta.appendSystemPrompt;
2385
+ messageAppendSystemPrompt = typeof raw === "string" && raw.trim().length > 0 ? raw : defaultAppendSystemPrompt;
2386
+ currentAppendSystemPrompt = messageAppendSystemPrompt;
2387
+ types.logger.debug(`[Codex] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to default"}`);
2388
+ } else {
2389
+ types.logger.debug(`[Codex] User message received with no appendSystemPrompt override, using current: ${currentAppendSystemPrompt ? "set" : "default"}`);
2390
+ }
2391
+ const enhancedMode = {
2392
+ permissionMode: messagePermissionMode || "default",
2393
+ model: messageModel,
2394
+ appendSystemPrompt: messageAppendSystemPrompt
2395
+ };
2396
+ const rawText = normalizeUserToolAliases(message.content.text);
2397
+ const promptText = images.length > 0 ? index.withUserImagesMarker(
2398
+ [
2399
+ `User attached ${images.length} image${images.length === 1 ? "" : "s"}.`,
2400
+ `You MUST call \`mcp__flockbay__latest_user_images\` with {"limit": ${images.length}} BEFORE replying.`,
2401
+ `If the tool call fails or returns no images, reply exactly: image-attachment-missing`,
2402
+ `Then answer the user's message:`,
2403
+ rawText
2404
+ ].join("\n\n"),
2405
+ images.length
2406
+ ) : rawText;
2407
+ messageQueue.push(promptText, enhancedMode, images.length > 0 ? { isolate: true } : void 0);
2408
+ });
2409
+ let thinking = false;
2410
+ session.keepAlive(thinking, "remote");
2411
+ const keepAliveInterval = setInterval(() => {
2412
+ session.keepAlive(thinking, "remote");
2413
+ }, 2e3);
2414
+ const sendReady = () => {
2415
+ session.sendSessionEvent({ type: "ready" });
2416
+ try {
2417
+ api.push().sendToAllDevices(
2418
+ "It's ready!",
2419
+ "Codex is waiting for your command",
2420
+ { sessionId: session.sessionId }
2421
+ );
2422
+ } catch (pushError) {
2423
+ types.logger.debug("[Codex] Failed to send ready push", pushError);
2424
+ }
2425
+ };
2426
+ function logActiveHandles(tag) {
2427
+ if (!process.env.DEBUG) return;
2428
+ const anyProc = process;
2429
+ const handles = typeof anyProc._getActiveHandles === "function" ? anyProc._getActiveHandles() : [];
2430
+ const requests = typeof anyProc._getActiveRequests === "function" ? anyProc._getActiveRequests() : [];
2431
+ types.logger.debug(`[codex][handles] ${tag}: handles=${handles.length} requests=${requests.length}`);
2432
+ try {
2433
+ const kinds = handles.map((h) => h && h.constructor ? h.constructor.name : typeof h);
2434
+ types.logger.debug(`[codex][handles] kinds=${JSON.stringify(kinds)}`);
2435
+ } catch (error) {
2436
+ console.error("[codex][handles] Failed to serialize active handles:", error);
2437
+ types.logger.debug("[codex][handles] Failed to serialize active handles:", error);
2438
+ }
2439
+ }
2440
+ let abortController = new AbortController();
2441
+ let shouldExit = false;
2442
+ let storedConversationIdForRecovery = null;
2443
+ async function handleAbort() {
2444
+ types.logger.debug("[Codex] Abort requested - stopping current task");
2445
+ try {
2446
+ abortController.abort();
2447
+ messageQueue.reset();
2448
+ permissionHandler.reset();
2449
+ reasoningProcessor.abort();
2450
+ diffProcessor.reset();
2451
+ types.logger.debug("[Codex] Abort completed - session remains active");
2452
+ } catch (error) {
2453
+ types.logger.debug("[Codex] Error during abort:", error);
2454
+ } finally {
2455
+ abortController = new AbortController();
2456
+ }
2457
+ }
2458
+ const handleKillSession = async () => {
2459
+ types.logger.debug("[Codex] Kill session requested - terminating process");
2460
+ await handleAbort();
2461
+ types.logger.debug("[Codex] Abort completed, proceeding with termination");
2462
+ try {
2463
+ if (session) {
2464
+ session.updateMetadata((currentMetadata) => ({
2465
+ ...currentMetadata,
2466
+ lifecycleState: "archived",
2467
+ lifecycleStateSince: Date.now(),
2468
+ archivedBy: "cli",
2469
+ archiveReason: "User terminated"
2470
+ }));
2471
+ session.sendSessionDeath();
2472
+ await session.flush();
2473
+ await session.close();
2474
+ }
2475
+ index.stopCaffeinate();
2476
+ flockbayServer.stop();
2477
+ types.logger.debug("[Codex] Session termination complete, exiting");
2478
+ process.exit(0);
2479
+ } catch (error) {
2480
+ types.logger.debug("[Codex] Error during session termination:", error);
2481
+ process.exit(1);
2482
+ }
2483
+ };
2484
+ session.rpcHandlerManager.registerHandler("abort", handleAbort);
2485
+ index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
2486
+ const messageBuffer = new index.MessageBuffer();
2487
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
2488
+ let inkInstance = null;
2489
+ if (hasTTY) {
2490
+ console.clear();
2491
+ inkInstance = ink.render(React.createElement(CodexDisplay, {
2492
+ messageBuffer,
2493
+ logPath: process.env.DEBUG ? types.logger.getLogPath() : void 0,
2494
+ onExit: async () => {
2495
+ types.logger.debug("[codex]: Exiting agent via Ctrl-C");
2496
+ shouldExit = true;
2497
+ await handleAbort();
2498
+ }
2499
+ }), {
2500
+ exitOnCtrlC: false,
2501
+ patchConsole: false
2502
+ });
2503
+ }
2504
+ if (hasTTY) {
2505
+ process.stdin.resume();
2506
+ if (process.stdin.isTTY) {
2507
+ process.stdin.setRawMode(true);
2508
+ }
2509
+ process.stdin.setEncoding("utf8");
2510
+ }
2511
+ const client = new CodexMcpClient();
2512
+ function findCodexResumeFile(conversationId) {
2513
+ if (!conversationId) return null;
2514
+ try {
2515
+ let collectFilesRecursive2 = function(dir, acc = []) {
2516
+ let entries;
2517
+ try {
2518
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2519
+ } catch {
2520
+ return acc;
2521
+ }
2522
+ for (const entry of entries) {
2523
+ const full = path.join(dir, entry.name);
2524
+ if (entry.isDirectory()) {
2525
+ collectFilesRecursive2(full, acc);
2526
+ } else if (entry.isFile()) {
2527
+ acc.push(full);
2528
+ }
2529
+ }
2530
+ return acc;
2531
+ };
2532
+ var collectFilesRecursive = collectFilesRecursive2;
2533
+ const codexHomeDir = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
2534
+ const rootDir = path.join(codexHomeDir, "sessions");
2535
+ const allJsonl = collectFilesRecursive2(rootDir).filter((full) => full.endsWith(".jsonl")).filter((full) => {
2536
+ try {
2537
+ return fs.statSync(full).isFile();
2538
+ } catch {
2539
+ return false;
2540
+ }
2541
+ });
2542
+ const candidates = allJsonl.filter((full) => full.includes(conversationId)).sort((a, b) => {
2543
+ const sa = fs.statSync(a).mtimeMs;
2544
+ const sb = fs.statSync(b).mtimeMs;
2545
+ return sb - sa;
2546
+ });
2547
+ if (candidates.length === 0 && process.env.DEBUG) {
2548
+ types.logger.debug("[Codex] No resume transcript match", {
2549
+ conversationId,
2550
+ rootDir,
2551
+ jsonlCount: allJsonl.length
2552
+ });
2553
+ }
2554
+ return candidates[0] || null;
2555
+ } catch {
2556
+ return null;
2557
+ }
2558
+ }
2559
+ const reasoningProcessor = new ReasoningProcessor((message) => {
2560
+ session.sendCodexMessage(message);
2561
+ });
2562
+ const diffProcessor = new DiffProcessor((message) => {
2563
+ session.sendCodexMessage(message);
2564
+ });
2565
+ const codexToolCallIds = /* @__PURE__ */ new Map();
2566
+ const isScreenshotToolName = (name) => {
2567
+ const n = typeof name === "string" ? name : "";
2568
+ return /unreal_(latest_screenshots|headless_screenshot|fast_preview)/i.test(n);
2569
+ };
2570
+ const summarizeScreenshotOutput = (output) => {
2571
+ try {
2572
+ const views = Array.isArray(output?.views) ? output.views : null;
2573
+ if (!views || views.length === 0) return { views: 0, firstUrl: null };
2574
+ const firstUrl = typeof views[0]?.url === "string" ? String(views[0].url) : typeof views[0]?.imageId === "string" ? String(views[0].imageId) : null;
2575
+ return { views: views.length, firstUrl };
2576
+ } catch {
2577
+ return null;
2578
+ }
2579
+ };
2580
+ const screenshotGate = {
2581
+ paths: [],
2582
+ seen: /* @__PURE__ */ new Set(),
2583
+ hasImageBlocks: false,
2584
+ inAutoReview: false
2585
+ };
2586
+ const resetScreenshotGateForTurn = () => {
2587
+ screenshotGate.paths = [];
2588
+ screenshotGate.seen.clear();
2589
+ screenshotGate.hasImageBlocks = false;
2590
+ screenshotGate.inAutoReview = false;
2591
+ };
2592
+ const collectScreenshotsForGate = (output) => {
2593
+ if (!output) return;
2594
+ const cwdHint = output?.cwd;
2595
+ const cwd = typeof cwdHint === "string" && cwdHint.trim().length > 0 ? cwdHint.trim() : process.cwd();
2596
+ const detected = flockbayScreenshotGate.detectScreenshotsForGate({ output, cwd });
2597
+ if (detected.paths.length === 0) return;
2598
+ if (detected.hasImageBlocks) screenshotGate.hasImageBlocks = true;
2599
+ for (const p of detected.paths) {
2600
+ const trimmed = String(p || "").trim();
2601
+ if (!trimmed) continue;
2602
+ if (screenshotGate.seen.has(trimmed)) continue;
2603
+ screenshotGate.seen.add(trimmed);
2604
+ screenshotGate.paths.push(trimmed);
2605
+ }
2606
+ };
2607
+ const buildScreenshotAutoReviewPrompt = (paths) => {
2608
+ const unique = Array.from(new Set(paths.map((p) => String(p || "").trim()).filter(Boolean)));
2609
+ const toolArgs = JSON.stringify({
2610
+ paths: unique,
2611
+ limit: unique.length,
2612
+ upload: false,
2613
+ includeToolImages: true
2614
+ }, null, 2);
2615
+ return index.trimIdent(`
2616
+ You just generated ${unique.length} screenshot${unique.length === 1 ? "" : "s"}.
2617
+
2618
+ Before doing anything else, call \`mcp__flockbay__read_images\` with:
2619
+ ${toolArgs}
2620
+
2621
+ Then visually inspect EVERY screenshot and report:
2622
+ - One short bullet per image ("Image N: ...") describing what you see
2623
+ - Whether the screenshots match the user's request
2624
+
2625
+ Do not run any other tools in this step.
2626
+ `);
2627
+ };
2628
+ client.setPermissionHandler(permissionHandler);
2629
+ const execInflight = /* @__PURE__ */ new Set();
2630
+ const execAliasToCallId = /* @__PURE__ */ new Map();
2631
+ client.setHandler((msg) => {
2632
+ try {
2633
+ const preview = typeof msg?.type === "string" ? msg.type : "unknown";
2634
+ types.logger.debug(`[Codex] MCP message: type=${preview}`);
2635
+ } catch {
2636
+ types.logger.debug("[Codex] MCP message received");
2637
+ }
2638
+ const extractCallId = (event) => {
2639
+ if (!event || typeof event !== "object") return null;
2640
+ const asIdString = (value) => {
2641
+ if (typeof value === "string") {
2642
+ const trimmed = value.trim();
2643
+ if (!trimmed || trimmed === "undefined") return null;
2644
+ return trimmed;
2645
+ }
2646
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
2647
+ if (typeof value === "bigint") return String(value);
2648
+ return null;
2649
+ };
2650
+ const nestedCandidates = [
2651
+ event?.command?.call_id,
2652
+ event?.command?.callId,
2653
+ event?.command?.command_id,
2654
+ event?.command?.commandId,
2655
+ event?.command?.exec_id,
2656
+ event?.command?.execId,
2657
+ event?.command?.id,
2658
+ event?.request?.call_id,
2659
+ event?.request?.callId,
2660
+ event?.request?.id,
2661
+ event?.request?.uuid,
2662
+ event?.request_id,
2663
+ event?.requestId,
2664
+ event?.tool_use_id,
2665
+ event?.toolUseId,
2666
+ event?.tool_use_uuid,
2667
+ event?.toolUseUuid,
2668
+ event?.tool_call_id,
2669
+ event?.toolCallId,
2670
+ event?.tool_call_uuid,
2671
+ event?.toolCallUuid,
2672
+ event?.tool?.call_id,
2673
+ event?.tool?.callId,
2674
+ event?.tool?.id,
2675
+ event?.tool?.uuid,
2676
+ event?.params?.call_id,
2677
+ event?.params?.callId,
2678
+ event?.params?.id,
2679
+ event?.params?.uuid,
2680
+ event?.data?.call_id,
2681
+ event?.data?.callId,
2682
+ event?.data?.id,
2683
+ event?.data?.uuid
2684
+ ];
2685
+ const candidates = [
2686
+ event.call_id,
2687
+ event.callId,
2688
+ event.codex_call_id,
2689
+ event.codexCallId,
2690
+ event.tool_call_id,
2691
+ event.toolCallId,
2692
+ event.command_id,
2693
+ event.commandId,
2694
+ event.exec_id,
2695
+ event.execId,
2696
+ event.id,
2697
+ event.uuid,
2698
+ event.event_id,
2699
+ event.eventId,
2700
+ ...nestedCandidates
2701
+ ];
2702
+ for (const value of candidates) {
2703
+ const id = asIdString(value);
2704
+ if (id) return id;
2705
+ }
2706
+ return null;
2707
+ };
2708
+ const getCallIdAliases = (event) => {
2709
+ if (!event || typeof event !== "object") return [];
2710
+ const raw = [
2711
+ event.call_id,
2712
+ event.callId,
2713
+ event.codex_call_id,
2714
+ event.codexCallId,
2715
+ event.tool_call_id,
2716
+ event.toolCallId,
2717
+ event.command_id,
2718
+ event.commandId,
2719
+ event.exec_id,
2720
+ event.execId,
2721
+ event.id,
2722
+ event?.command?.call_id,
2723
+ event?.command?.callId,
2724
+ event?.command?.command_id,
2725
+ event?.command?.commandId,
2726
+ event?.command?.exec_id,
2727
+ event?.command?.execId,
2728
+ event?.command?.id,
2729
+ event?.request?.call_id,
2730
+ event?.request?.callId,
2731
+ event?.request?.id,
2732
+ event?.request_id,
2733
+ event?.requestId,
2734
+ event?.tool_use_id,
2735
+ event?.toolUseId
2736
+ ];
2737
+ const out = [];
2738
+ const seen = /* @__PURE__ */ new Set();
2739
+ for (const v of raw) {
2740
+ if (typeof v !== "string") continue;
2741
+ const s = v.trim();
2742
+ if (!s || s === "undefined") continue;
2743
+ if (seen.has(s)) continue;
2744
+ seen.add(s);
2745
+ out.push(s);
2746
+ }
2747
+ return out;
2748
+ };
2749
+ const rememberExecAliases = (canonicalCallId, event) => {
2750
+ for (const alias of getCallIdAliases(event)) {
2751
+ execAliasToCallId.set(alias, canonicalCallId);
2752
+ }
2753
+ };
2754
+ const resolveExecCallId = (event, allowCreate) => {
2755
+ const aliases = getCallIdAliases(event);
2756
+ for (const a of aliases) {
2757
+ const mapped = execAliasToCallId.get(a);
2758
+ if (mapped) return mapped;
2759
+ }
2760
+ const extracted = extractCallId(event);
2761
+ if (extracted) return extracted;
2762
+ if (!allowCreate) return null;
2763
+ return null;
2764
+ };
2765
+ const markExecStarted = (callId, event) => {
2766
+ rememberExecAliases(callId, event);
2767
+ if (execInflight.has(callId)) return;
2768
+ execInflight.add(callId);
2769
+ };
2770
+ const markExecFinished = (callId) => {
2771
+ execInflight.delete(callId);
2772
+ for (const [alias, mapped] of execAliasToCallId.entries()) {
2773
+ if (mapped === callId) execAliasToCallId.delete(alias);
2774
+ }
2775
+ };
2776
+ const flushInflightExecCalls = (opts2) => {
2777
+ if (execInflight.size === 0) return;
2778
+ for (const callId of Array.from(execInflight)) {
2779
+ session.sendCodexMessage({
2780
+ type: "tool-call-result",
2781
+ callId,
2782
+ output: { ok: !opts2.isError, reason: opts2.reason, missing_exec_command_end: true },
2783
+ is_error: opts2.isError,
2784
+ id: node_crypto.randomUUID()
2785
+ });
2786
+ markExecFinished(callId);
2787
+ }
2788
+ };
2789
+ const normalizeMcpToolName = (serverName, toolName) => {
2790
+ const rawName = toolName.trim();
2791
+ if (!rawName) return "unknown";
2792
+ if (rawName.startsWith("mcp__") || rawName.startsWith("functions.mcp__")) return rawName.replace(/^functions\./, "");
2793
+ const separatorMatch = rawName.match(/^([A-Za-z0-9_-]+)[:.](.+)$/);
2794
+ if (separatorMatch) {
2795
+ const [, server, tool] = separatorMatch;
2796
+ return `mcp__${server}__${tool}`;
2797
+ }
2798
+ if (serverName) {
2799
+ return `mcp__${serverName}__${rawName}`;
2800
+ }
2801
+ return rawName;
2802
+ };
2803
+ const extractMcpServerName = (event) => {
2804
+ const candidates = [
2805
+ event?.server_name,
2806
+ event?.serverName,
2807
+ event?.mcp_server,
2808
+ event?.mcpServer,
2809
+ event?.server,
2810
+ event?.tool?.server_name,
2811
+ event?.tool?.serverName,
2812
+ event?.tool?.server,
2813
+ event?.params?.server_name,
2814
+ event?.params?.serverName,
2815
+ event?.params?.server,
2816
+ event?.data?.server_name,
2817
+ event?.data?.serverName,
2818
+ event?.data?.server
2819
+ ];
2820
+ for (const value of candidates) {
2821
+ if (typeof value === "string" && value.trim().length > 0) return value;
2822
+ }
2823
+ return null;
2824
+ };
2825
+ const extractMcpToolName = (event) => {
2826
+ const candidates = [
2827
+ event?.tool_name,
2828
+ event?.toolName,
2829
+ event?.mcp_tool_name,
2830
+ event?.mcpToolName,
2831
+ event?.name,
2832
+ event?.tool?.name,
2833
+ event?.tool?.tool_name,
2834
+ event?.tool?.toolName,
2835
+ event?.params?.tool_name,
2836
+ event?.params?.toolName,
2837
+ event?.params?.name,
2838
+ event?.data?.tool_name,
2839
+ event?.data?.toolName,
2840
+ event?.data?.name
2841
+ ];
2842
+ for (const value of candidates) {
2843
+ if (typeof value === "string" && value.trim().length > 0) return value;
2844
+ }
2845
+ return null;
2846
+ };
2847
+ const extractToolInput = (event) => {
2848
+ const candidates = [
2849
+ event?.arguments,
2850
+ event?.args,
2851
+ event?.input,
2852
+ event?.params,
2853
+ event?.tool_arguments,
2854
+ event?.toolArguments,
2855
+ event?.tool?.arguments,
2856
+ event?.tool?.args,
2857
+ event?.tool?.input,
2858
+ event?.params?.arguments,
2859
+ event?.params?.args,
2860
+ event?.params?.input,
2861
+ event?.data?.arguments,
2862
+ event?.data?.args,
2863
+ event?.data?.input
2864
+ ];
2865
+ for (const value of candidates) {
2866
+ if (value !== void 0) return value;
2867
+ }
2868
+ return void 0;
2869
+ };
2870
+ const extractToolOutput = (event) => {
2871
+ const candidates = [
2872
+ event?.output,
2873
+ event?.result,
2874
+ event?.data,
2875
+ event?.response,
2876
+ event?.tool_result,
2877
+ event?.toolResult,
2878
+ event?.tool?.output,
2879
+ event?.tool?.result,
2880
+ event?.params?.output,
2881
+ event?.params?.result,
2882
+ event?.params?.response
2883
+ ];
2884
+ for (const value of candidates) {
2885
+ if (value !== void 0) return value;
2886
+ }
2887
+ return void 0;
2888
+ };
2889
+ const shouldConsiderMcpToolEvent = (event) => {
2890
+ if (!event || typeof event !== "object") return false;
2891
+ if (event.type === "exec_command_begin" || event.type === "exec_approval_request" || event.type === "exec_command_end") return false;
2892
+ if (event.type === "patch_apply_begin" || event.type === "patch_apply_end") return false;
2893
+ if (event.type === "token_count") return false;
2894
+ const toolNameRaw = extractMcpToolName(event);
2895
+ if (!toolNameRaw) return false;
2896
+ if (toolNameRaw === "CodexBash" || toolNameRaw === "CodexPatch") return false;
2897
+ const serverName = extractMcpServerName(event);
2898
+ if (serverName) return true;
2899
+ if (toolNameRaw.startsWith("mcp__") || toolNameRaw.startsWith("functions.mcp__")) return true;
2900
+ if (/^([A-Za-z0-9_-]+)[:.].+$/.test(toolNameRaw)) return true;
2901
+ return /unreal_(latest_screenshots|headless_screenshot|fast_preview)|flockbay__/i.test(toolNameRaw);
2902
+ };
2903
+ const isMcpToolResultLike = (event) => {
2904
+ if (!shouldConsiderMcpToolEvent(event)) return false;
2905
+ const hasOutput = extractToolOutput(event) !== void 0 || event && typeof event === "object" && ("content" in event || "stdout" in event || "stderr" in event);
2906
+ const type = typeof event?.type === "string" ? event.type : "";
2907
+ return Boolean(hasOutput) || /(result|end|finished|complete)/i.test(type);
2908
+ };
2909
+ const isMcpToolBeginLike = (event) => {
2910
+ if (!shouldConsiderMcpToolEvent(event)) return false;
2911
+ if (isMcpToolResultLike(event)) return false;
2912
+ const hasInput = extractToolInput(event) !== void 0;
2913
+ const type = typeof event?.type === "string" ? event.type : "";
2914
+ return Boolean(hasInput) || /(begin|start|call)/i.test(type);
2915
+ };
2916
+ const maybeEmitMcpToolCall = (event) => {
2917
+ const callId = extractCallId(event);
2918
+ if (!callId) return;
2919
+ const toolNameRaw = extractMcpToolName(event);
2920
+ if (!toolNameRaw) return;
2921
+ const serverName = extractMcpServerName(event);
2922
+ const toolName = normalizeMcpToolName(serverName, toolNameRaw);
2923
+ const input = extractToolInput(event) ?? {};
2924
+ codexToolCallIds.set(callId, toolName);
2925
+ if (isScreenshotToolName(toolName)) {
2926
+ console.error("[screenshots-diag] emit tool-call", { callId, toolName });
2927
+ }
2928
+ session.sendCodexMessage({
2929
+ type: "tool-call",
2930
+ name: toolName,
2931
+ callId,
2932
+ input,
2933
+ id: node_crypto.randomUUID()
2934
+ });
2935
+ };
2936
+ const maybeEmitMcpToolResult = (event) => {
2937
+ const callId = extractCallId(event);
2938
+ if (!callId) return;
2939
+ const toolNameRaw = extractMcpToolName(event);
2940
+ const serverName = extractMcpServerName(event);
2941
+ const toolName = toolNameRaw ? normalizeMcpToolName(serverName, toolNameRaw) : codexToolCallIds.get(callId) ?? null;
2942
+ if (toolName && !codexToolCallIds.has(callId)) {
2943
+ codexToolCallIds.set(callId, toolName);
2944
+ session.sendCodexMessage({
2945
+ type: "tool-call",
2946
+ name: toolName,
2947
+ callId,
2948
+ input: extractToolInput(event) ?? {},
2949
+ id: node_crypto.randomUUID()
2950
+ });
2951
+ }
2952
+ const output = extractToolOutput(event) ?? event;
2953
+ const is_error = Boolean(event?.is_error ?? event?.isError ?? event?.error);
2954
+ collectScreenshotsForGate(output);
2955
+ try {
2956
+ index.applyCoordinationSideEffectsFromMcpToolResult({
2957
+ session,
2958
+ toolName,
2959
+ output,
2960
+ isError: is_error
2961
+ });
2962
+ } catch (err) {
2963
+ types.logger.debug("[codex] Failed applying coordination side effects from MCP tool result", {
2964
+ toolName,
2965
+ error: err instanceof Error ? err.message : String(err)
2966
+ });
2967
+ }
2968
+ if (isScreenshotToolName(toolName ?? void 0)) {
2969
+ console.error("[screenshots-diag] emit tool-call-result", {
2970
+ callId,
2971
+ toolName,
2972
+ is_error,
2973
+ ...summarizeScreenshotOutput(output)
2974
+ });
2975
+ }
2976
+ session.sendCodexMessage({
2977
+ type: "tool-call-result",
2978
+ callId,
2979
+ output,
2980
+ is_error,
2981
+ id: node_crypto.randomUUID()
2982
+ });
2983
+ };
2984
+ const MAX_TOOL_RESULT_CHARS = 512e3;
2985
+ const tailTruncate = (value) => {
2986
+ if (typeof value !== "string") return { value, truncated: false };
2987
+ if (value.length <= MAX_TOOL_RESULT_CHARS) return { value, truncated: false };
2988
+ return { value: value.slice(-MAX_TOOL_RESULT_CHARS), truncated: true };
2989
+ };
2990
+ const sanitizeExecCommandEndOutput = (output) => {
2991
+ const next = { ...output };
2992
+ const truncatedFields = [];
2993
+ for (const [key, value] of Object.entries(next)) {
2994
+ const { value: clipped, truncated } = tailTruncate(value);
2995
+ if (truncated) {
2996
+ next[key] = clipped;
2997
+ truncatedFields.push(key);
2998
+ }
2999
+ }
3000
+ if (typeof next.output === "string" && typeof next.stdout !== "string" && typeof next.stderr !== "string") {
3001
+ next.stdout = next.output;
3002
+ next.stdoutFromOutput = true;
3003
+ }
3004
+ if (truncatedFields.length) {
3005
+ next.outputTruncated = true;
3006
+ next.truncatedFields = truncatedFields;
3007
+ }
3008
+ return next;
3009
+ };
3010
+ if (msg.type === "agent_message") {
3011
+ messageBuffer.addMessage(msg.message, "assistant");
3012
+ } else if (msg.type === "agent_reasoning_delta") ; else if (msg.type === "agent_reasoning") {
3013
+ messageBuffer.addMessage(`[Thinking] ${msg.text.substring(0, 100)}...`, "system");
3014
+ } else if (msg.type === "exec_command_begin") {
3015
+ messageBuffer.addMessage(`Executing: ${msg.command}`, "tool");
3016
+ } else if (msg.type === "exec_command_end") {
3017
+ const output = msg.output || msg.error || "Command completed";
3018
+ const truncatedOutput = output.substring(0, 200);
3019
+ messageBuffer.addMessage(
3020
+ `Result: ${truncatedOutput}${output.length > 200 ? "..." : ""}`,
3021
+ "result"
3022
+ );
3023
+ } else if (msg.type === "task_started") {
3024
+ messageBuffer.addMessage("Starting task...", "status");
3025
+ } else if (msg.type === "task_complete") {
3026
+ messageBuffer.addMessage("Task completed", "status");
3027
+ flushInflightExecCalls({ isError: false, reason: "task_complete" });
3028
+ sendReady();
3029
+ } else if (msg.type === "turn_aborted") {
3030
+ messageBuffer.addMessage("Turn aborted", "status");
3031
+ flushInflightExecCalls({ isError: true, reason: "turn_aborted" });
3032
+ sendReady();
3033
+ }
3034
+ if (msg.type === "task_started") {
3035
+ if (!thinking) {
3036
+ types.logger.debug("thinking started");
3037
+ thinking = true;
3038
+ session.keepAlive(thinking, "remote");
3039
+ }
3040
+ }
3041
+ if (msg.type === "task_complete" || msg.type === "turn_aborted") {
3042
+ if (thinking) {
3043
+ types.logger.debug("thinking completed");
3044
+ thinking = false;
3045
+ session.keepAlive(thinking, "remote");
3046
+ }
3047
+ diffProcessor.reset();
3048
+ }
3049
+ if (msg.type === "agent_reasoning_section_break") {
3050
+ reasoningProcessor.handleSectionBreak();
3051
+ }
3052
+ if (msg.type === "agent_reasoning_delta") {
3053
+ reasoningProcessor.processDelta(msg.delta);
3054
+ }
3055
+ if (msg.type === "agent_reasoning") {
3056
+ reasoningProcessor.complete(msg.text);
3057
+ }
3058
+ if (msg.type === "agent_message") {
3059
+ session.sendCodexMessage({
3060
+ type: "message",
3061
+ message: msg.message,
3062
+ id: node_crypto.randomUUID()
3063
+ });
3064
+ }
3065
+ if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
3066
+ const callId = resolveExecCallId(msg, true);
3067
+ if (!callId) {
3068
+ console.error("[codexbash-diag] Missing call id for exec begin/approval event", {
3069
+ type: msg.type,
3070
+ keys: Object.keys(msg || {})
3071
+ });
3072
+ throw new Error("Missing call id for exec begin/approval event");
3073
+ }
3074
+ if (msg.type === "exec_command_begin") {
3075
+ try {
3076
+ console.error("[codexbash-diag] exec begin callId", { callId, aliases: getCallIdAliases(msg) });
3077
+ } catch {
3078
+ }
3079
+ }
3080
+ markExecStarted(callId, msg);
3081
+ let { call_id, callId: _callId, type, ...inputs } = msg;
3082
+ session.sendCodexMessage({
3083
+ type: "tool-call",
3084
+ name: "CodexBash",
3085
+ callId,
3086
+ input: inputs,
3087
+ id: node_crypto.randomUUID()
3088
+ });
3089
+ }
3090
+ if (msg.type === "exec_command_end") {
3091
+ const callId = resolveExecCallId(msg, false);
3092
+ if (!callId) {
3093
+ console.error("[codexbash-diag] Missing call id for exec end event", {
3094
+ keys: Object.keys(msg || {})
3095
+ });
3096
+ throw new Error("Missing call id for exec end event");
3097
+ }
3098
+ rememberExecAliases(callId, msg);
3099
+ try {
3100
+ console.error("[codexbash-diag] exec end callId", { callId, aliases: getCallIdAliases(msg) });
3101
+ } catch {
3102
+ }
3103
+ let { call_id, callId: _callId, type, ...output } = msg;
3104
+ const sanitized = sanitizeExecCommandEndOutput(output);
3105
+ collectScreenshotsForGate({ ...sanitized, cwd: output?.cwd ?? sanitized?.cwd ?? msg?.cwd });
3106
+ session.sendCodexMessage({
3107
+ type: "tool-call-result",
3108
+ callId,
3109
+ output: sanitized,
3110
+ is_error: Boolean(sanitized?.error) || sanitized?.success === false,
3111
+ id: node_crypto.randomUUID()
3112
+ });
3113
+ markExecFinished(callId);
3114
+ }
3115
+ if (msg.type === "token_count") {
3116
+ return;
3117
+ }
3118
+ if (msg.type === "patch_apply_begin") {
3119
+ const callId = extractCallId(msg);
3120
+ if (!callId) {
3121
+ types.logger.debug("[Codex] Missing call id for patch apply begin", { keys: Object.keys(msg || {}) });
3122
+ return;
3123
+ }
3124
+ let { call_id, callId: _callId, auto_approved, changes } = msg;
3125
+ const changeCount = Object.keys(changes).length;
3126
+ const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
3127
+ messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
3128
+ session.sendCodexMessage({
3129
+ type: "tool-call",
3130
+ name: "CodexPatch",
3131
+ callId,
3132
+ input: {
3133
+ auto_approved,
3134
+ changes
3135
+ },
3136
+ id: node_crypto.randomUUID()
3137
+ });
3138
+ }
3139
+ if (msg.type === "patch_apply_end") {
3140
+ const callId = extractCallId(msg);
3141
+ if (!callId) {
3142
+ types.logger.debug("[Codex] Missing call id for patch apply end", { keys: Object.keys(msg || {}) });
3143
+ return;
3144
+ }
3145
+ let { call_id, callId: _callId, stdout, stderr, success } = msg;
3146
+ if (success) {
3147
+ const message = stdout || "Files modified successfully";
3148
+ messageBuffer.addMessage(message.substring(0, 200), "result");
3149
+ } else {
3150
+ const errorMsg = stderr || "Failed to modify files";
3151
+ messageBuffer.addMessage(`Error: ${errorMsg.substring(0, 200)}`, "result");
3152
+ }
3153
+ session.sendCodexMessage({
3154
+ type: "tool-call-result",
3155
+ callId,
3156
+ output: {
3157
+ stdout,
3158
+ stderr,
3159
+ success
3160
+ },
3161
+ id: node_crypto.randomUUID()
3162
+ });
3163
+ }
3164
+ if (msg.type === "turn_diff") {
3165
+ if (msg.unified_diff) {
3166
+ diffProcessor.processDiff(msg.unified_diff);
3167
+ }
3168
+ }
3169
+ if (isMcpToolBeginLike(msg)) {
3170
+ maybeEmitMcpToolCall(msg);
3171
+ }
3172
+ if (isMcpToolResultLike(msg)) {
3173
+ maybeEmitMcpToolResult(msg);
3174
+ }
3175
+ });
3176
+ const flockbayServer = await index.startFlockbayServer(session, { elicitationHub });
3177
+ const bridgeCommand = path.join(types.projectPath(), "bin", "flockbay-mcp.mjs");
3178
+ const mcpServers = {
3179
+ flockbay: {
3180
+ command: bridgeCommand,
3181
+ args: ["--url", flockbayServer.url]
3182
+ }
3183
+ };
3184
+ let first = true;
3185
+ try {
3186
+ types.logger.debug("[codex]: client.connect begin");
3187
+ try {
3188
+ await client.connect();
3189
+ } catch (error) {
3190
+ const message = error instanceof Error ? error.message : String(error);
3191
+ const details = error instanceof Error ? error.stack : void 0;
3192
+ types.logger.warn("[codex]: client.connect failed", { message, details });
3193
+ session.sendCodexMessage({
3194
+ type: "message",
3195
+ message: `Codex can\u2019t start on this machine because the \`codex\` CLI isn\u2019t available in the daemon\u2019s environment.
3196
+
3197
+ Fix:
3198
+ - Install Codex CLI (or ensure it\u2019s on PATH for launchd/daemon)
3199
+ - Or set \`FLOCKBAY_CODEX_BIN\` to the full path of the \`codex\` binary
3200
+ - Or switch this session to Claude/Gemini
3201
+
3202
+ Error: ${message}`,
3203
+ id: node_crypto.randomUUID()
3204
+ });
3205
+ sendReady();
3206
+ await session.flush();
3207
+ return;
3208
+ }
3209
+ types.logger.debug("[codex]: client.connect done");
3210
+ let wasCreated = false;
3211
+ let currentModeHash = null;
3212
+ let pending = null;
3213
+ let nextExperimentalResume = null;
3214
+ while (!shouldExit) {
3215
+ logActiveHandles("loop-top");
3216
+ let message = pending;
3217
+ pending = null;
3218
+ if (!message) {
3219
+ const waitSignal = abortController.signal;
3220
+ const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
3221
+ if (!batch) {
3222
+ if (waitSignal.aborted && !shouldExit) {
3223
+ types.logger.debug("[codex]: Wait aborted while idle; ignoring and continuing");
3224
+ continue;
3225
+ }
3226
+ types.logger.debug(`[codex]: batch=${!!batch}, shouldExit=${shouldExit}`);
3227
+ break;
3228
+ }
3229
+ message = batch;
3230
+ }
3231
+ if (!message) {
3232
+ break;
3233
+ }
3234
+ if (wasCreated && currentModeHash && message.hash !== currentModeHash) {
3235
+ types.logger.debug("[Codex] Mode changed \u2013 restarting Codex session");
3236
+ messageBuffer.addMessage("\u2550".repeat(40), "status");
3237
+ messageBuffer.addMessage("Starting new Codex session (mode changed)...", "status");
3238
+ try {
3239
+ const prevConversationId = client.getConversationId() || client.getSessionId();
3240
+ nextExperimentalResume = findCodexResumeFile(prevConversationId);
3241
+ if (nextExperimentalResume) {
3242
+ types.logger.debug(`[Codex] Found resume file for conversation ${prevConversationId}: ${nextExperimentalResume}`);
3243
+ messageBuffer.addMessage("Resuming previous context\u2026", "status");
3244
+ } else {
3245
+ types.logger.debug("[Codex] No resume file found for previous session");
3246
+ }
3247
+ } catch (e) {
3248
+ types.logger.debug("[Codex] Error while searching resume file", e);
3249
+ }
3250
+ client.clearSession();
3251
+ wasCreated = false;
3252
+ currentModeHash = null;
3253
+ pending = message;
3254
+ permissionHandler.reset();
3255
+ reasoningProcessor.abort();
3256
+ diffProcessor.reset();
3257
+ thinking = false;
3258
+ session.keepAlive(thinking, "remote");
3259
+ continue;
3260
+ }
3261
+ messageBuffer.addMessage(message.message, "user");
3262
+ currentModeHash = message.hash;
3263
+ let skipAutoFinalize = false;
3264
+ try {
3265
+ resetScreenshotGateForTurn();
3266
+ const overrides = { approvalPolicy: "untrusted", sandbox: "workspace-write" };
3267
+ if (!bypassUeGates) {
3268
+ const detection = await index.detectUnrealProject(process.cwd());
3269
+ if (!detection.ok) {
3270
+ session.sendCodexMessage({
3271
+ type: "message",
3272
+ message: "Select an Unreal project folder (the folder containing a *.uproject) to continue.",
3273
+ id: node_crypto.randomUUID()
3274
+ });
3275
+ if (wasCreated) {
3276
+ client.clearSession();
3277
+ wasCreated = false;
3278
+ currentModeHash = null;
3279
+ }
3280
+ continue;
3281
+ }
3282
+ }
3283
+ if (!wasCreated) {
3284
+ const projectCapsule = await index.buildProjectCapsule({ startDir: process.cwd() });
3285
+ const modelOverride = resolveCodexModelOverride(message.mode.model);
3286
+ const startConfig = {
3287
+ prompt: message.message,
3288
+ config: { mcp_servers: mcpServers }
3289
+ };
3290
+ startConfig.sandbox = overrides.sandbox;
3291
+ startConfig["approval-policy"] = overrides.approvalPolicy;
3292
+ if (modelOverride.config && typeof startConfig.config === "object" && startConfig.config) {
3293
+ startConfig.config = { ...startConfig.config, ...modelOverride.config };
3294
+ }
3295
+ startConfig["developer-instructions"] = sanitizeCodexInstructions(
3296
+ [
3297
+ FLOCKBAY_CODEX_BASE_INSTRUCTIONS,
3298
+ message.mode.appendSystemPrompt,
3299
+ projectCapsule
3300
+ ].filter((v) => typeof v === "string" && v.trim().length > 0).join("\n\n")
3301
+ );
3302
+ let resumeFile = null;
3303
+ if (nextExperimentalResume) {
3304
+ resumeFile = nextExperimentalResume;
3305
+ nextExperimentalResume = null;
3306
+ types.logger.debug("[Codex] Using resume file from mode change:", resumeFile);
3307
+ } else if (storedConversationIdForRecovery) {
3308
+ const recoveryResumeFile = findCodexResumeFile(storedConversationIdForRecovery);
3309
+ if (recoveryResumeFile) {
3310
+ resumeFile = recoveryResumeFile;
3311
+ types.logger.debug("[Codex] Using resume file from previous session (error recovery):", resumeFile);
3312
+ }
3313
+ storedConversationIdForRecovery = null;
3314
+ }
3315
+ if (resumeFile) {
3316
+ startConfig.config.experimental_resume = resumeFile;
3317
+ }
3318
+ await client.startSession(
3319
+ startConfig,
3320
+ { signal: abortController.signal }
3321
+ );
3322
+ wasCreated = true;
3323
+ first = false;
3324
+ } else {
3325
+ const response2 = await client.continueSession(
3326
+ message.message,
3327
+ { signal: abortController.signal }
3328
+ );
3329
+ types.logger.debug("[Codex] continueSession response:", response2);
3330
+ }
3331
+ if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0 && !screenshotGate.hasImageBlocks) {
3332
+ screenshotGate.inAutoReview = true;
3333
+ messageBuffer.addMessage("Auto-reviewing screenshots\u2026", "status");
3334
+ const reviewPrompt = buildScreenshotAutoReviewPrompt(screenshotGate.paths);
3335
+ await client.continueSession(reviewPrompt, { signal: abortController.signal });
3336
+ screenshotGate.inAutoReview = false;
3337
+ }
3338
+ } catch (error) {
3339
+ types.logger.warn("Error in codex session:", error);
3340
+ const isAbortError = error instanceof Error && error.name === "AbortError";
3341
+ if (isAbortError) {
3342
+ skipAutoFinalize = true;
3343
+ messageBuffer.addMessage("Aborted by user", "status");
3344
+ session.sendSessionEvent({ type: "message", message: "Aborted by user" });
3345
+ if (!client.hasActiveSession()) {
3346
+ wasCreated = false;
3347
+ currentModeHash = null;
3348
+ types.logger.debug("[Codex] Abort occurred with no active session; will restart on next message");
3349
+ } else {
3350
+ wasCreated = true;
3351
+ types.logger.debug("[Codex] Abort completed; keeping active session for next message");
3352
+ }
3353
+ } else {
3354
+ const errorMessage = error instanceof Error ? error.message : String(error);
3355
+ messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
3356
+ session.sendSessionEvent({ type: "message", message: `Error: ${errorMessage}` });
3357
+ if (client.hasActiveSession()) {
3358
+ storedConversationIdForRecovery = client.storeConversationForResume() || client.storeSessionForResume();
3359
+ types.logger.debug("[Codex] Stored conversation after error:", storedConversationIdForRecovery);
3360
+ }
3361
+ client.clearSession();
3362
+ wasCreated = false;
3363
+ currentModeHash = null;
3364
+ }
3365
+ } finally {
3366
+ permissionHandler.reset();
3367
+ reasoningProcessor.abort();
3368
+ diffProcessor.reset();
3369
+ thinking = false;
3370
+ session.keepAlive(thinking, "remote");
3371
+ if (!skipAutoFinalize) {
3372
+ try {
3373
+ const finalizeRes = await index.autoFinalizeCoordinationWorkItem({
3374
+ token: session.getAuthToken(),
3375
+ cwd: process.cwd(),
3376
+ summary: message?.message ?? null
3377
+ });
3378
+ if (!finalizeRes.ok) {
3379
+ const msg = `Auto-finalize failed: ${finalizeRes.error}`;
3380
+ messageBuffer.addMessage(msg, "status");
3381
+ session.sendSessionEvent({ type: "message", message: msg });
3382
+ }
3383
+ } catch (err) {
3384
+ const msg = `Auto-finalize failed: ${err instanceof Error ? err.message : String(err)}`;
3385
+ messageBuffer.addMessage(msg, "status");
3386
+ session.sendSessionEvent({ type: "message", message: msg });
3387
+ }
3388
+ }
3389
+ emitReadyIfIdle({
3390
+ pending,
3391
+ queueSize: () => messageQueue.size(),
3392
+ shouldExit,
3393
+ sendReady
3394
+ });
3395
+ logActiveHandles("after-turn");
3396
+ }
3397
+ }
3398
+ } finally {
3399
+ types.logger.debug("[codex]: Final cleanup start");
3400
+ logActiveHandles("cleanup-start");
3401
+ try {
3402
+ types.logger.debug("[codex]: sendSessionDeath");
3403
+ session.sendSessionDeath();
3404
+ types.logger.debug("[codex]: flush begin");
3405
+ await session.flush();
3406
+ types.logger.debug("[codex]: flush done");
3407
+ types.logger.debug("[codex]: session.close begin");
3408
+ await session.close();
3409
+ types.logger.debug("[codex]: session.close done");
3410
+ } catch (e) {
3411
+ types.logger.debug("[codex]: Error while closing session", e);
3412
+ }
3413
+ types.logger.debug("[codex]: client.disconnect begin");
3414
+ await client.disconnect();
3415
+ types.logger.debug("[codex]: client.disconnect done");
3416
+ types.logger.debug("[codex]: flockbayServer.stop");
3417
+ flockbayServer.stop();
3418
+ if (process.stdin.isTTY) {
3419
+ types.logger.debug("[codex]: setRawMode(false)");
3420
+ try {
3421
+ process.stdin.setRawMode(false);
3422
+ } catch (error) {
3423
+ console.error("[codex] Failed to setRawMode(false) during cleanup:", error);
3424
+ types.logger.debug("[codex] Failed to setRawMode(false) during cleanup:", error);
3425
+ }
3426
+ }
3427
+ if (hasTTY) {
3428
+ types.logger.debug("[codex]: stdin.pause()");
3429
+ try {
3430
+ process.stdin.pause();
3431
+ } catch (error) {
3432
+ console.error("[codex] Failed to pause stdin during cleanup:", error);
3433
+ types.logger.debug("[codex] Failed to pause stdin during cleanup:", error);
3434
+ }
3435
+ }
3436
+ types.logger.debug("[codex]: clearInterval(keepAlive)");
3437
+ clearInterval(keepAliveInterval);
3438
+ if (inkInstance) {
3439
+ types.logger.debug("[codex]: inkInstance.unmount()");
3440
+ inkInstance.unmount();
3441
+ }
3442
+ messageBuffer.clear();
3443
+ logActiveHandles("cleanup-end");
3444
+ types.logger.debug("[codex]: Final cleanup completed");
3445
+ }
3446
+ }
3447
+
3448
+ exports.emitReadyIfIdle = emitReadyIfIdle;
3449
+ exports.runCodex = runCodex;