happy-coder 0.10.0-2 → 0.10.0-4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/bin/happy-mcp.mjs +32 -0
- package/dist/codex/happyMcpStdioBridge.cjs +80 -0
- package/dist/codex/happyMcpStdioBridge.d.cts +2 -0
- package/dist/codex/happyMcpStdioBridge.d.mts +2 -0
- package/dist/codex/happyMcpStdioBridge.mjs +78 -0
- package/dist/index-67rskwL7.cjs +6033 -0
- package/dist/index-Dw96QD4T.mjs +6025 -0
- package/dist/index.cjs +33 -6024
- package/dist/index.mjs +33 -6024
- package/dist/lib.cjs +2 -1
- package/dist/lib.d.cts +54 -112
- package/dist/lib.d.mts +54 -112
- package/dist/lib.mjs +2 -1
- package/dist/runCodex-BLNf5zb1.cjs +1155 -0
- package/dist/runCodex-BNH8w4O9.mjs +1153 -0
- package/dist/{types-xfXKJHdM.mjs → types-2wHnX7UW.mjs} +311 -170
- package/dist/{types-WP9wteZE.cjs → types-BcDnTXMg.cjs} +348 -206
- package/package.json +20 -6
- package/scripts/ripgrep_launcher.cjs +2 -26
- package/scripts/unpack-tools.cjs +163 -0
- package/tools/archives/difftastic-LICENSE +21 -0
- package/tools/archives/difftastic-arm64-darwin.tar.gz +0 -0
- package/tools/archives/difftastic-arm64-linux.tar.gz +0 -0
- package/tools/archives/difftastic-x64-darwin.tar.gz +0 -0
- package/tools/archives/difftastic-x64-linux.tar.gz +0 -0
- package/tools/archives/difftastic-x64-win32.tar.gz +0 -0
- package/tools/archives/ripgrep-arm64-darwin.tar.gz +0 -0
- package/tools/archives/ripgrep-arm64-linux.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-darwin.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-linux.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-win32.tar.gz +0 -0
- package/tools/licenses/difftastic-LICENSE +21 -0
- package/tools/licenses/ripgrep-LICENSE +3 -0
- package/tools/unpacked/difft +0 -0
- package/ripgrep/arm64-linux/rg +0 -0
- package/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-darwin/rg +0 -0
- package/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/ripgrep/x64-linux/rg +0 -0
- package/ripgrep/x64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-win32/rg.exe +0 -0
- package/ripgrep/x64-win32/ripgrep.node +0 -0
- /package/{ripgrep/COPYING → tools/archives/ripgrep-LICENSE} +0 -0
- /package/{ripgrep/arm64-darwin → tools/unpacked}/rg +0 -0
- /package/{ripgrep/arm64-darwin → tools/unpacked}/ripgrep.node +0 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ink = require('ink');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var types = require('./types-BcDnTXMg.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 index = require('./index-67rskwL7.cjs');
|
|
12
|
+
var os = require('node:os');
|
|
13
|
+
var node_path = require('node:path');
|
|
14
|
+
var fs = require('node:fs');
|
|
15
|
+
require('axios');
|
|
16
|
+
require('chalk');
|
|
17
|
+
require('fs');
|
|
18
|
+
require('node:fs/promises');
|
|
19
|
+
require('tweetnacl');
|
|
20
|
+
require('node:events');
|
|
21
|
+
require('socket.io-client');
|
|
22
|
+
require('child_process');
|
|
23
|
+
require('util');
|
|
24
|
+
require('fs/promises');
|
|
25
|
+
require('crypto');
|
|
26
|
+
require('path');
|
|
27
|
+
require('url');
|
|
28
|
+
require('os');
|
|
29
|
+
require('expo-server-sdk');
|
|
30
|
+
require('node:child_process');
|
|
31
|
+
require('node:readline');
|
|
32
|
+
require('node:url');
|
|
33
|
+
require('ps-list');
|
|
34
|
+
require('cross-spawn');
|
|
35
|
+
require('qrcode-terminal');
|
|
36
|
+
require('open');
|
|
37
|
+
require('fastify');
|
|
38
|
+
require('fastify-type-provider-zod');
|
|
39
|
+
require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
40
|
+
require('node:http');
|
|
41
|
+
require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
42
|
+
require('http');
|
|
43
|
+
|
|
44
|
+
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
|
|
45
|
+
class CodexMcpClient {
|
|
46
|
+
client;
|
|
47
|
+
transport = null;
|
|
48
|
+
connected = false;
|
|
49
|
+
sessionId = null;
|
|
50
|
+
handler = null;
|
|
51
|
+
permissionHandler = null;
|
|
52
|
+
constructor() {
|
|
53
|
+
this.client = new index_js.Client(
|
|
54
|
+
{ name: "happy-codex-client", version: "1.0.0" },
|
|
55
|
+
{ capabilities: { tools: {}, elicitation: {} } }
|
|
56
|
+
);
|
|
57
|
+
this.client.setNotificationHandler(z.z.object({
|
|
58
|
+
method: z.z.literal("codex/event"),
|
|
59
|
+
params: z.z.object({
|
|
60
|
+
msg: z.z.any()
|
|
61
|
+
})
|
|
62
|
+
}).passthrough(), (data) => {
|
|
63
|
+
if (data.params.msg.type === "session_configured") {
|
|
64
|
+
this.sessionId = data.params.msg.session_id;
|
|
65
|
+
}
|
|
66
|
+
this.handler?.(data.params.msg);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
setHandler(handler) {
|
|
70
|
+
this.handler = handler;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Set the permission handler for tool approval
|
|
74
|
+
*/
|
|
75
|
+
setPermissionHandler(handler) {
|
|
76
|
+
this.permissionHandler = handler;
|
|
77
|
+
}
|
|
78
|
+
async connect() {
|
|
79
|
+
if (this.connected) return;
|
|
80
|
+
types.logger.debug("[CodexMCP] Connecting to Codex MCP server...");
|
|
81
|
+
this.transport = new stdio_js.StdioClientTransport({
|
|
82
|
+
command: "codex",
|
|
83
|
+
args: ["mcp"]
|
|
84
|
+
});
|
|
85
|
+
this.registerPermissionHandlers();
|
|
86
|
+
await this.client.connect(this.transport);
|
|
87
|
+
this.connected = true;
|
|
88
|
+
types.logger.debug("[CodexMCP] Connected to Codex");
|
|
89
|
+
}
|
|
90
|
+
registerPermissionHandlers() {
|
|
91
|
+
this.client.setRequestHandler(
|
|
92
|
+
types_js.ElicitRequestSchema,
|
|
93
|
+
async (request) => {
|
|
94
|
+
console.log("[CodexMCP] Received elicitation request:", request.params);
|
|
95
|
+
const params = request.params;
|
|
96
|
+
const toolName = "CodexBash";
|
|
97
|
+
if (!this.permissionHandler) {
|
|
98
|
+
types.logger.debug("[CodexMCP] No permission handler set, denying by default");
|
|
99
|
+
return {
|
|
100
|
+
decision: "denied"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.permissionHandler.handleToolCall(
|
|
105
|
+
params.codex_call_id,
|
|
106
|
+
toolName,
|
|
107
|
+
{
|
|
108
|
+
command: params.codex_command,
|
|
109
|
+
cwd: params.codex_cwd
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
types.logger.debug("[CodexMCP] Permission result:", result);
|
|
113
|
+
return {
|
|
114
|
+
decision: result.decision
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
types.logger.debug("[CodexMCP] Error handling permission request:", error);
|
|
118
|
+
return {
|
|
119
|
+
decision: "denied",
|
|
120
|
+
reason: error instanceof Error ? error.message : "Permission request failed"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
types.logger.debug("[CodexMCP] Permission handlers registered");
|
|
126
|
+
}
|
|
127
|
+
async startSession(config, options) {
|
|
128
|
+
if (!this.connected) await this.connect();
|
|
129
|
+
types.logger.debug("[CodexMCP] Starting Codex session:", config);
|
|
130
|
+
const response = await this.client.callTool({
|
|
131
|
+
name: "codex",
|
|
132
|
+
arguments: config
|
|
133
|
+
}, void 0, {
|
|
134
|
+
signal: options?.signal,
|
|
135
|
+
timeout: DEFAULT_TIMEOUT
|
|
136
|
+
// maxTotalTimeout: 10000000000
|
|
137
|
+
});
|
|
138
|
+
console.log("[CodexMCP] Response:", response);
|
|
139
|
+
this.extractSessionId(response);
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
async continueSession(prompt, options) {
|
|
143
|
+
if (!this.connected) await this.connect();
|
|
144
|
+
if (!this.sessionId) {
|
|
145
|
+
throw new Error("No active session. Call startSession first.");
|
|
146
|
+
}
|
|
147
|
+
const args = { sessionId: this.sessionId, prompt };
|
|
148
|
+
types.logger.debug("[CodexMCP] Continuing Codex session:", args);
|
|
149
|
+
const response = await this.client.callTool({
|
|
150
|
+
name: "codex-reply",
|
|
151
|
+
arguments: args
|
|
152
|
+
}, void 0, {
|
|
153
|
+
signal: options?.signal,
|
|
154
|
+
timeout: DEFAULT_TIMEOUT
|
|
155
|
+
});
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
extractSessionId(response) {
|
|
159
|
+
if (response?.meta?.sessionId) {
|
|
160
|
+
this.sessionId = response.meta.sessionId;
|
|
161
|
+
types.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
|
|
162
|
+
} else if (response?.sessionId) {
|
|
163
|
+
this.sessionId = response.sessionId;
|
|
164
|
+
types.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
|
|
165
|
+
} else {
|
|
166
|
+
const content = response?.content;
|
|
167
|
+
if (Array.isArray(content)) {
|
|
168
|
+
for (const item of content) {
|
|
169
|
+
if (item?.sessionId) {
|
|
170
|
+
this.sessionId = item.sessionId;
|
|
171
|
+
types.logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
getSessionId() {
|
|
179
|
+
return this.sessionId;
|
|
180
|
+
}
|
|
181
|
+
hasActiveSession() {
|
|
182
|
+
return this.sessionId !== null;
|
|
183
|
+
}
|
|
184
|
+
clearSession() {
|
|
185
|
+
this.sessionId = null;
|
|
186
|
+
types.logger.debug("[CodexMCP] Session cleared");
|
|
187
|
+
}
|
|
188
|
+
async disconnect() {
|
|
189
|
+
if (!this.connected) return;
|
|
190
|
+
const pid = this.transport?.pid ?? null;
|
|
191
|
+
types.logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
|
|
192
|
+
try {
|
|
193
|
+
types.logger.debug("[CodexMCP] client.close begin");
|
|
194
|
+
await this.client.close();
|
|
195
|
+
types.logger.debug("[CodexMCP] client.close done");
|
|
196
|
+
} catch (e) {
|
|
197
|
+
types.logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
|
|
198
|
+
try {
|
|
199
|
+
types.logger.debug("[CodexMCP] transport.close begin");
|
|
200
|
+
await this.transport?.close?.();
|
|
201
|
+
types.logger.debug("[CodexMCP] transport.close done");
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (pid) {
|
|
206
|
+
try {
|
|
207
|
+
process.kill(pid, 0);
|
|
208
|
+
types.logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, "SIGKILL");
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.transport = null;
|
|
217
|
+
this.connected = false;
|
|
218
|
+
this.sessionId = null;
|
|
219
|
+
types.logger.debug("[CodexMCP] Disconnected");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class CodexPermissionHandler {
|
|
224
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
225
|
+
session;
|
|
226
|
+
constructor(session) {
|
|
227
|
+
this.session = session;
|
|
228
|
+
this.setupRpcHandler();
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Handle a tool permission request
|
|
232
|
+
* @param toolCallId - The unique ID of the tool call
|
|
233
|
+
* @param toolName - The name of the tool being called
|
|
234
|
+
* @param input - The input parameters for the tool
|
|
235
|
+
* @returns Promise resolving to permission result
|
|
236
|
+
*/
|
|
237
|
+
async handleToolCall(toolCallId, toolName, input) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
this.pendingRequests.set(toolCallId, {
|
|
240
|
+
resolve,
|
|
241
|
+
reject,
|
|
242
|
+
toolName,
|
|
243
|
+
input
|
|
244
|
+
});
|
|
245
|
+
this.session.updateAgentState((currentState) => ({
|
|
246
|
+
...currentState,
|
|
247
|
+
requests: {
|
|
248
|
+
...currentState.requests,
|
|
249
|
+
[toolCallId]: {
|
|
250
|
+
tool: toolName,
|
|
251
|
+
arguments: input,
|
|
252
|
+
createdAt: Date.now()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}));
|
|
256
|
+
types.logger.debug(`[Codex] Permission request sent for tool: ${toolName} (${toolCallId})`);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Setup RPC handler for permission responses
|
|
261
|
+
*/
|
|
262
|
+
setupRpcHandler() {
|
|
263
|
+
this.session.rpcHandlerManager.registerHandler(
|
|
264
|
+
"permission",
|
|
265
|
+
async (response) => {
|
|
266
|
+
const pending = this.pendingRequests.get(response.id);
|
|
267
|
+
if (!pending) {
|
|
268
|
+
types.logger.debug("[Codex] Permission request not found or already resolved");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.pendingRequests.delete(response.id);
|
|
272
|
+
const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
|
|
273
|
+
pending.resolve(result);
|
|
274
|
+
this.session.updateAgentState((currentState) => {
|
|
275
|
+
const request = currentState.requests?.[response.id];
|
|
276
|
+
if (!request) return currentState;
|
|
277
|
+
const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
|
|
278
|
+
let res = {
|
|
279
|
+
...currentState,
|
|
280
|
+
requests: remainingRequests,
|
|
281
|
+
completedRequests: {
|
|
282
|
+
...currentState.completedRequests,
|
|
283
|
+
[response.id]: {
|
|
284
|
+
...request,
|
|
285
|
+
completedAt: Date.now(),
|
|
286
|
+
status: response.approved ? "approved" : "denied",
|
|
287
|
+
decision: result.decision
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
return res;
|
|
292
|
+
});
|
|
293
|
+
types.logger.debug(`[Codex] Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Reset state for new sessions
|
|
299
|
+
*/
|
|
300
|
+
reset() {
|
|
301
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
302
|
+
pending.reject(new Error("Session reset"));
|
|
303
|
+
}
|
|
304
|
+
this.pendingRequests.clear();
|
|
305
|
+
this.session.updateAgentState((currentState) => {
|
|
306
|
+
const pendingRequests = currentState.requests || {};
|
|
307
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
308
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
309
|
+
completedRequests[id] = {
|
|
310
|
+
...request,
|
|
311
|
+
completedAt: Date.now(),
|
|
312
|
+
status: "canceled",
|
|
313
|
+
reason: "Session reset"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
...currentState,
|
|
318
|
+
requests: {},
|
|
319
|
+
completedRequests
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
types.logger.debug("[Codex] Permission handler reset");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class ReasoningProcessor {
|
|
327
|
+
accumulator = "";
|
|
328
|
+
inTitleCapture = false;
|
|
329
|
+
titleBuffer = "";
|
|
330
|
+
contentBuffer = "";
|
|
331
|
+
hasTitle = false;
|
|
332
|
+
currentCallId = null;
|
|
333
|
+
toolCallStarted = false;
|
|
334
|
+
currentTitle = null;
|
|
335
|
+
onMessage = null;
|
|
336
|
+
constructor(onMessage) {
|
|
337
|
+
this.onMessage = onMessage || null;
|
|
338
|
+
this.reset();
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Set the message callback for sending messages directly
|
|
342
|
+
*/
|
|
343
|
+
setMessageCallback(callback) {
|
|
344
|
+
this.onMessage = callback;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Process a reasoning section break - indicates a new reasoning section is starting
|
|
348
|
+
*/
|
|
349
|
+
handleSectionBreak() {
|
|
350
|
+
this.finishCurrentToolCall("canceled");
|
|
351
|
+
this.resetState();
|
|
352
|
+
types.logger.debug("[ReasoningProcessor] Section break - reset state");
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Process a reasoning delta and accumulate content
|
|
356
|
+
*/
|
|
357
|
+
processDelta(delta) {
|
|
358
|
+
this.accumulator += delta;
|
|
359
|
+
if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
|
|
360
|
+
if (this.accumulator.startsWith("**")) {
|
|
361
|
+
this.inTitleCapture = true;
|
|
362
|
+
this.titleBuffer = this.accumulator.substring(2);
|
|
363
|
+
types.logger.debug("[ReasoningProcessor] Started title capture");
|
|
364
|
+
} else if (this.accumulator.length > 0) {
|
|
365
|
+
this.contentBuffer = this.accumulator;
|
|
366
|
+
}
|
|
367
|
+
} else if (this.inTitleCapture) {
|
|
368
|
+
this.titleBuffer = this.accumulator.substring(2);
|
|
369
|
+
const titleEndIndex = this.titleBuffer.indexOf("**");
|
|
370
|
+
if (titleEndIndex !== -1) {
|
|
371
|
+
const title = this.titleBuffer.substring(0, titleEndIndex);
|
|
372
|
+
const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
|
|
373
|
+
this.hasTitle = true;
|
|
374
|
+
this.inTitleCapture = false;
|
|
375
|
+
this.currentTitle = title;
|
|
376
|
+
this.contentBuffer = afterTitle;
|
|
377
|
+
this.currentCallId = node_crypto.randomUUID();
|
|
378
|
+
types.logger.debug(`[ReasoningProcessor] Title captured: "${title}"`);
|
|
379
|
+
this.sendToolCallStart(title);
|
|
380
|
+
}
|
|
381
|
+
} else if (this.hasTitle) {
|
|
382
|
+
this.contentBuffer = this.accumulator.substring(
|
|
383
|
+
this.accumulator.indexOf("**") + 2 + this.currentTitle.length + 2
|
|
384
|
+
);
|
|
385
|
+
} else {
|
|
386
|
+
this.contentBuffer = this.accumulator;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Send the tool call start message
|
|
391
|
+
*/
|
|
392
|
+
sendToolCallStart(title) {
|
|
393
|
+
if (!this.currentCallId || this.toolCallStarted) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const toolCall = {
|
|
397
|
+
type: "tool-call",
|
|
398
|
+
name: "CodexReasoning",
|
|
399
|
+
callId: this.currentCallId,
|
|
400
|
+
input: {
|
|
401
|
+
title
|
|
402
|
+
},
|
|
403
|
+
id: node_crypto.randomUUID()
|
|
404
|
+
};
|
|
405
|
+
types.logger.debug(`[ReasoningProcessor] Sending tool call start for: "${title}"`);
|
|
406
|
+
this.onMessage?.(toolCall);
|
|
407
|
+
this.toolCallStarted = true;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Complete the reasoning section with final text
|
|
411
|
+
*/
|
|
412
|
+
complete(fullText) {
|
|
413
|
+
let title;
|
|
414
|
+
let content = fullText;
|
|
415
|
+
if (fullText.startsWith("**")) {
|
|
416
|
+
const titleEndIndex = fullText.indexOf("**", 2);
|
|
417
|
+
if (titleEndIndex !== -1) {
|
|
418
|
+
title = fullText.substring(2, titleEndIndex);
|
|
419
|
+
content = fullText.substring(titleEndIndex + 2).trim();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
types.logger.debug(`[ReasoningProcessor] Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
|
|
423
|
+
if (title && !this.toolCallStarted) {
|
|
424
|
+
this.currentCallId = this.currentCallId || node_crypto.randomUUID();
|
|
425
|
+
this.sendToolCallStart(title);
|
|
426
|
+
}
|
|
427
|
+
if (this.toolCallStarted && this.currentCallId) {
|
|
428
|
+
const toolResult = {
|
|
429
|
+
type: "tool-call-result",
|
|
430
|
+
callId: this.currentCallId,
|
|
431
|
+
output: {
|
|
432
|
+
content,
|
|
433
|
+
status: "completed"
|
|
434
|
+
},
|
|
435
|
+
id: node_crypto.randomUUID()
|
|
436
|
+
};
|
|
437
|
+
types.logger.debug("[ReasoningProcessor] Sending tool call result");
|
|
438
|
+
this.onMessage?.(toolResult);
|
|
439
|
+
} else {
|
|
440
|
+
const reasoningMessage = {
|
|
441
|
+
type: "reasoning",
|
|
442
|
+
message: content,
|
|
443
|
+
id: node_crypto.randomUUID()
|
|
444
|
+
};
|
|
445
|
+
types.logger.debug("[ReasoningProcessor] Sending reasoning message");
|
|
446
|
+
this.onMessage?.(reasoningMessage);
|
|
447
|
+
}
|
|
448
|
+
this.resetState();
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Abort the current reasoning section
|
|
452
|
+
*/
|
|
453
|
+
abort() {
|
|
454
|
+
types.logger.debug("[ReasoningProcessor] Abort called");
|
|
455
|
+
this.finishCurrentToolCall("canceled");
|
|
456
|
+
this.resetState();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Reset the processor state
|
|
460
|
+
*/
|
|
461
|
+
reset() {
|
|
462
|
+
this.finishCurrentToolCall("canceled");
|
|
463
|
+
this.resetState();
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Finish current tool call if one is in progress
|
|
467
|
+
*/
|
|
468
|
+
finishCurrentToolCall(status) {
|
|
469
|
+
if (this.toolCallStarted && this.currentCallId) {
|
|
470
|
+
const toolResult = {
|
|
471
|
+
type: "tool-call-result",
|
|
472
|
+
callId: this.currentCallId,
|
|
473
|
+
output: {
|
|
474
|
+
content: this.contentBuffer || "",
|
|
475
|
+
status
|
|
476
|
+
},
|
|
477
|
+
id: node_crypto.randomUUID()
|
|
478
|
+
};
|
|
479
|
+
types.logger.debug(`[ReasoningProcessor] Sending tool call result with status: ${status}`);
|
|
480
|
+
this.onMessage?.(toolResult);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Reset internal state
|
|
485
|
+
*/
|
|
486
|
+
resetState() {
|
|
487
|
+
this.accumulator = "";
|
|
488
|
+
this.inTitleCapture = false;
|
|
489
|
+
this.titleBuffer = "";
|
|
490
|
+
this.contentBuffer = "";
|
|
491
|
+
this.hasTitle = false;
|
|
492
|
+
this.currentCallId = null;
|
|
493
|
+
this.toolCallStarted = false;
|
|
494
|
+
this.currentTitle = null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get the current call ID for tool result matching
|
|
498
|
+
*/
|
|
499
|
+
getCurrentCallId() {
|
|
500
|
+
return this.currentCallId;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check if a tool call has been started
|
|
504
|
+
*/
|
|
505
|
+
hasStartedToolCall() {
|
|
506
|
+
return this.toolCallStarted;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
class DiffProcessor {
|
|
511
|
+
previousDiff = null;
|
|
512
|
+
onMessage = null;
|
|
513
|
+
constructor(onMessage) {
|
|
514
|
+
this.onMessage = onMessage || null;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Process a turn_diff message and check if the unified_diff has changed
|
|
518
|
+
*/
|
|
519
|
+
processDiff(unifiedDiff) {
|
|
520
|
+
if (this.previousDiff !== unifiedDiff) {
|
|
521
|
+
types.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
|
|
522
|
+
const callId = node_crypto.randomUUID();
|
|
523
|
+
const toolCall = {
|
|
524
|
+
type: "tool-call",
|
|
525
|
+
name: "CodexDiff",
|
|
526
|
+
callId,
|
|
527
|
+
input: {
|
|
528
|
+
unified_diff: unifiedDiff
|
|
529
|
+
},
|
|
530
|
+
id: node_crypto.randomUUID()
|
|
531
|
+
};
|
|
532
|
+
this.onMessage?.(toolCall);
|
|
533
|
+
const toolResult = {
|
|
534
|
+
type: "tool-call-result",
|
|
535
|
+
callId,
|
|
536
|
+
output: {
|
|
537
|
+
status: "completed"
|
|
538
|
+
},
|
|
539
|
+
id: node_crypto.randomUUID()
|
|
540
|
+
};
|
|
541
|
+
this.onMessage?.(toolResult);
|
|
542
|
+
}
|
|
543
|
+
this.previousDiff = unifiedDiff;
|
|
544
|
+
types.logger.debug("[DiffProcessor] Updated stored diff");
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Reset the processor state (called on task_complete or turn_aborted)
|
|
548
|
+
*/
|
|
549
|
+
reset() {
|
|
550
|
+
types.logger.debug("[DiffProcessor] Resetting diff state");
|
|
551
|
+
this.previousDiff = null;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Set the message callback for sending messages directly
|
|
555
|
+
*/
|
|
556
|
+
setMessageCallback(callback) {
|
|
557
|
+
this.onMessage = callback;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get the current diff value
|
|
561
|
+
*/
|
|
562
|
+
getCurrentDiff() {
|
|
563
|
+
return this.previousDiff;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
|
|
568
|
+
const [messages, setMessages] = React.useState([]);
|
|
569
|
+
const [confirmationMode, setConfirmationMode] = React.useState(false);
|
|
570
|
+
const [actionInProgress, setActionInProgress] = React.useState(false);
|
|
571
|
+
const confirmationTimeoutRef = React.useRef(null);
|
|
572
|
+
const { stdout } = ink.useStdout();
|
|
573
|
+
const terminalWidth = stdout.columns || 80;
|
|
574
|
+
const terminalHeight = stdout.rows || 24;
|
|
575
|
+
React.useEffect(() => {
|
|
576
|
+
setMessages(messageBuffer.getMessages());
|
|
577
|
+
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
|
|
578
|
+
setMessages(newMessages);
|
|
579
|
+
});
|
|
580
|
+
return () => {
|
|
581
|
+
unsubscribe();
|
|
582
|
+
if (confirmationTimeoutRef.current) {
|
|
583
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}, [messageBuffer]);
|
|
587
|
+
const resetConfirmation = React.useCallback(() => {
|
|
588
|
+
setConfirmationMode(false);
|
|
589
|
+
if (confirmationTimeoutRef.current) {
|
|
590
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
591
|
+
confirmationTimeoutRef.current = null;
|
|
592
|
+
}
|
|
593
|
+
}, []);
|
|
594
|
+
const setConfirmationWithTimeout = React.useCallback(() => {
|
|
595
|
+
setConfirmationMode(true);
|
|
596
|
+
if (confirmationTimeoutRef.current) {
|
|
597
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
598
|
+
}
|
|
599
|
+
confirmationTimeoutRef.current = setTimeout(() => {
|
|
600
|
+
resetConfirmation();
|
|
601
|
+
}, 15e3);
|
|
602
|
+
}, [resetConfirmation]);
|
|
603
|
+
ink.useInput(React.useCallback(async (input, key) => {
|
|
604
|
+
if (actionInProgress) return;
|
|
605
|
+
if (key.ctrl && input === "c") {
|
|
606
|
+
if (confirmationMode) {
|
|
607
|
+
resetConfirmation();
|
|
608
|
+
setActionInProgress(true);
|
|
609
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
610
|
+
onExit?.();
|
|
611
|
+
} else {
|
|
612
|
+
setConfirmationWithTimeout();
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (confirmationMode) {
|
|
617
|
+
resetConfirmation();
|
|
618
|
+
}
|
|
619
|
+
}, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
|
|
620
|
+
const getMessageColor = (type) => {
|
|
621
|
+
switch (type) {
|
|
622
|
+
case "user":
|
|
623
|
+
return "magenta";
|
|
624
|
+
case "assistant":
|
|
625
|
+
return "cyan";
|
|
626
|
+
case "system":
|
|
627
|
+
return "blue";
|
|
628
|
+
case "tool":
|
|
629
|
+
return "yellow";
|
|
630
|
+
case "result":
|
|
631
|
+
return "green";
|
|
632
|
+
case "status":
|
|
633
|
+
return "gray";
|
|
634
|
+
default:
|
|
635
|
+
return "white";
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const formatMessage = (msg) => {
|
|
639
|
+
const lines = msg.content.split("\n");
|
|
640
|
+
const maxLineLength = terminalWidth - 10;
|
|
641
|
+
return lines.map((line) => {
|
|
642
|
+
if (line.length <= maxLineLength) return line;
|
|
643
|
+
const chunks = [];
|
|
644
|
+
for (let i = 0; i < line.length; i += maxLineLength) {
|
|
645
|
+
chunks.push(line.slice(i, i + maxLineLength));
|
|
646
|
+
}
|
|
647
|
+
return chunks.join("\n");
|
|
648
|
+
}).join("\n");
|
|
649
|
+
};
|
|
650
|
+
return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
|
|
651
|
+
ink.Box,
|
|
652
|
+
{
|
|
653
|
+
flexDirection: "column",
|
|
654
|
+
width: terminalWidth,
|
|
655
|
+
height: terminalHeight - 4,
|
|
656
|
+
borderStyle: "round",
|
|
657
|
+
borderColor: "gray",
|
|
658
|
+
paddingX: 1,
|
|
659
|
+
overflow: "hidden"
|
|
660
|
+
},
|
|
661
|
+
/* @__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)))),
|
|
662
|
+
/* @__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...") : (
|
|
663
|
+
// Show only the last messages that fit in the available space
|
|
664
|
+
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))))
|
|
665
|
+
))
|
|
666
|
+
), /* @__PURE__ */ React.createElement(
|
|
667
|
+
ink.Box,
|
|
668
|
+
{
|
|
669
|
+
width: terminalWidth,
|
|
670
|
+
borderStyle: "round",
|
|
671
|
+
borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
|
|
672
|
+
paddingX: 2,
|
|
673
|
+
justifyContent: "center",
|
|
674
|
+
alignItems: "center",
|
|
675
|
+
flexDirection: "column"
|
|
676
|
+
},
|
|
677
|
+
/* @__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))
|
|
678
|
+
));
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
async function runCodex(opts) {
|
|
682
|
+
const sessionTag = node_crypto.randomUUID();
|
|
683
|
+
const api = await types.ApiClient.create(opts.credentials);
|
|
684
|
+
const settings = await types.readSettings();
|
|
685
|
+
let machineId = settings?.machineId;
|
|
686
|
+
if (!machineId) {
|
|
687
|
+
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
types.logger.debug(`Using machineId: ${machineId}`);
|
|
691
|
+
await api.getOrCreateMachine({
|
|
692
|
+
machineId,
|
|
693
|
+
metadata: index.initialMachineMetadata
|
|
694
|
+
});
|
|
695
|
+
let state = {
|
|
696
|
+
controlledByUser: false
|
|
697
|
+
};
|
|
698
|
+
let metadata = {
|
|
699
|
+
path: process.cwd(),
|
|
700
|
+
host: os.hostname(),
|
|
701
|
+
version: types.packageJson.version,
|
|
702
|
+
os: os.platform(),
|
|
703
|
+
machineId,
|
|
704
|
+
homeDir: os.homedir(),
|
|
705
|
+
happyHomeDir: types.configuration.happyHomeDir,
|
|
706
|
+
happyLibDir: types.projectPath(),
|
|
707
|
+
happyToolsDir: node_path.resolve(types.projectPath(), "tools", "unpacked"),
|
|
708
|
+
startedFromDaemon: false,
|
|
709
|
+
hostPid: process.pid,
|
|
710
|
+
startedBy: "terminal",
|
|
711
|
+
// Initialize lifecycle state
|
|
712
|
+
lifecycleState: "running",
|
|
713
|
+
lifecycleStateSince: Date.now(),
|
|
714
|
+
flavor: "codex"
|
|
715
|
+
};
|
|
716
|
+
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
717
|
+
const session = api.sessionSyncClient(response);
|
|
718
|
+
const messageQueue = new index.MessageQueue2((mode) => index.hashObject({
|
|
719
|
+
permissionMode: mode.permissionMode,
|
|
720
|
+
model: mode.model
|
|
721
|
+
}));
|
|
722
|
+
let currentPermissionMode = void 0;
|
|
723
|
+
let currentModel = void 0;
|
|
724
|
+
session.onUserMessage((message) => {
|
|
725
|
+
let messagePermissionMode = currentPermissionMode;
|
|
726
|
+
if (message.meta?.permissionMode) {
|
|
727
|
+
const validModes = ["default", "read-only", "safe-yolo", "yolo"];
|
|
728
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
729
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
730
|
+
currentPermissionMode = messagePermissionMode;
|
|
731
|
+
types.logger.debug(`[Codex] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
732
|
+
} else {
|
|
733
|
+
types.logger.debug(`[Codex] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
types.logger.debug(`[Codex] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
|
|
737
|
+
}
|
|
738
|
+
let messageModel = currentModel;
|
|
739
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
740
|
+
messageModel = message.meta.model || void 0;
|
|
741
|
+
currentModel = messageModel;
|
|
742
|
+
types.logger.debug(`[Codex] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
743
|
+
} else {
|
|
744
|
+
types.logger.debug(`[Codex] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
745
|
+
}
|
|
746
|
+
const enhancedMode = {
|
|
747
|
+
permissionMode: messagePermissionMode || "default",
|
|
748
|
+
model: messageModel
|
|
749
|
+
};
|
|
750
|
+
messageQueue.push(message.content.text, enhancedMode);
|
|
751
|
+
});
|
|
752
|
+
let thinking = false;
|
|
753
|
+
session.keepAlive(thinking, "remote");
|
|
754
|
+
const keepAliveInterval = setInterval(() => {
|
|
755
|
+
session.keepAlive(thinking, "remote");
|
|
756
|
+
}, 2e3);
|
|
757
|
+
function logActiveHandles(tag) {
|
|
758
|
+
if (!process.env.DEBUG) return;
|
|
759
|
+
const anyProc = process;
|
|
760
|
+
const handles = typeof anyProc._getActiveHandles === "function" ? anyProc._getActiveHandles() : [];
|
|
761
|
+
const requests = typeof anyProc._getActiveRequests === "function" ? anyProc._getActiveRequests() : [];
|
|
762
|
+
types.logger.debug(`[codex][handles] ${tag}: handles=${handles.length} requests=${requests.length}`);
|
|
763
|
+
try {
|
|
764
|
+
const kinds = handles.map((h) => h && h.constructor ? h.constructor.name : typeof h);
|
|
765
|
+
types.logger.debug(`[codex][handles] kinds=${JSON.stringify(kinds)}`);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
let abortController = new AbortController();
|
|
770
|
+
let shouldExit = false;
|
|
771
|
+
async function handleAbort() {
|
|
772
|
+
types.logger.debug("[Codex] Abort requested");
|
|
773
|
+
try {
|
|
774
|
+
abortController.abort();
|
|
775
|
+
messageQueue.reset();
|
|
776
|
+
permissionHandler.reset();
|
|
777
|
+
reasoningProcessor.abort();
|
|
778
|
+
diffProcessor.reset();
|
|
779
|
+
types.logger.debug("[Codex] Abort completed");
|
|
780
|
+
} catch (error) {
|
|
781
|
+
types.logger.debug("[Codex] Error during abort:", error);
|
|
782
|
+
} finally {
|
|
783
|
+
abortController = new AbortController();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
session.rpcHandlerManager.registerHandler("abort", handleAbort);
|
|
787
|
+
const messageBuffer = new index.MessageBuffer();
|
|
788
|
+
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
789
|
+
let inkInstance = null;
|
|
790
|
+
if (hasTTY) {
|
|
791
|
+
console.clear();
|
|
792
|
+
inkInstance = ink.render(React.createElement(CodexDisplay, {
|
|
793
|
+
messageBuffer,
|
|
794
|
+
logPath: process.env.DEBUG ? types.logger.getLogPath() : void 0,
|
|
795
|
+
onExit: async () => {
|
|
796
|
+
types.logger.debug("[codex]: Exiting agent via Ctrl-C");
|
|
797
|
+
shouldExit = true;
|
|
798
|
+
await handleAbort();
|
|
799
|
+
}
|
|
800
|
+
}), {
|
|
801
|
+
exitOnCtrlC: false,
|
|
802
|
+
patchConsole: false
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (hasTTY) {
|
|
806
|
+
process.stdin.resume();
|
|
807
|
+
if (process.stdin.isTTY) {
|
|
808
|
+
process.stdin.setRawMode(true);
|
|
809
|
+
}
|
|
810
|
+
process.stdin.setEncoding("utf8");
|
|
811
|
+
}
|
|
812
|
+
const client = new CodexMcpClient();
|
|
813
|
+
function findCodexResumeFile(sessionId) {
|
|
814
|
+
if (!sessionId) return null;
|
|
815
|
+
try {
|
|
816
|
+
let collectFilesRecursive2 = function(dir, acc = []) {
|
|
817
|
+
let entries;
|
|
818
|
+
try {
|
|
819
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
820
|
+
} catch {
|
|
821
|
+
return acc;
|
|
822
|
+
}
|
|
823
|
+
for (const entry of entries) {
|
|
824
|
+
const full = node_path.join(dir, entry.name);
|
|
825
|
+
if (entry.isDirectory()) {
|
|
826
|
+
collectFilesRecursive2(full, acc);
|
|
827
|
+
} else if (entry.isFile()) {
|
|
828
|
+
acc.push(full);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return acc;
|
|
832
|
+
};
|
|
833
|
+
var collectFilesRecursive = collectFilesRecursive2;
|
|
834
|
+
const rootDir = node_path.join(os.homedir(), ".codex", "sessions");
|
|
835
|
+
const candidates = collectFilesRecursive2(rootDir).filter((full) => full.endsWith(`-${sessionId}.jsonl`)).filter((full) => {
|
|
836
|
+
try {
|
|
837
|
+
return fs.statSync(full).isFile();
|
|
838
|
+
} catch {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
}).sort((a, b) => {
|
|
842
|
+
const sa = fs.statSync(a).mtimeMs;
|
|
843
|
+
const sb = fs.statSync(b).mtimeMs;
|
|
844
|
+
return sb - sa;
|
|
845
|
+
});
|
|
846
|
+
return candidates[0] || null;
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const permissionHandler = new CodexPermissionHandler(session);
|
|
852
|
+
const reasoningProcessor = new ReasoningProcessor((message) => {
|
|
853
|
+
session.sendCodexMessage(message);
|
|
854
|
+
});
|
|
855
|
+
const diffProcessor = new DiffProcessor((message) => {
|
|
856
|
+
session.sendCodexMessage(message);
|
|
857
|
+
});
|
|
858
|
+
client.setPermissionHandler(permissionHandler);
|
|
859
|
+
client.setHandler((msg) => {
|
|
860
|
+
types.logger.debug(`[Codex] MCP message: ${JSON.stringify(msg)}`);
|
|
861
|
+
if (msg.type === "agent_message") {
|
|
862
|
+
messageBuffer.addMessage(msg.message, "assistant");
|
|
863
|
+
} else if (msg.type === "agent_reasoning_delta") ; else if (msg.type === "agent_reasoning") {
|
|
864
|
+
messageBuffer.addMessage(`[Thinking] ${msg.text.substring(0, 100)}...`, "system");
|
|
865
|
+
} else if (msg.type === "exec_command_begin") {
|
|
866
|
+
messageBuffer.addMessage(`Executing: ${msg.command}`, "tool");
|
|
867
|
+
} else if (msg.type === "exec_command_end") {
|
|
868
|
+
const output = msg.output || msg.error || "Command completed";
|
|
869
|
+
const truncatedOutput = output.substring(0, 200);
|
|
870
|
+
messageBuffer.addMessage(
|
|
871
|
+
`Result: ${truncatedOutput}${output.length > 200 ? "..." : ""}`,
|
|
872
|
+
"result"
|
|
873
|
+
);
|
|
874
|
+
} else if (msg.type === "task_started") {
|
|
875
|
+
messageBuffer.addMessage("Starting task...", "status");
|
|
876
|
+
} else if (msg.type === "task_complete") {
|
|
877
|
+
messageBuffer.addMessage("Task completed", "status");
|
|
878
|
+
} else if (msg.type === "turn_aborted") {
|
|
879
|
+
messageBuffer.addMessage("Turn aborted", "status");
|
|
880
|
+
}
|
|
881
|
+
if (msg.type === "task_started") {
|
|
882
|
+
if (!thinking) {
|
|
883
|
+
types.logger.debug("thinking started");
|
|
884
|
+
thinking = true;
|
|
885
|
+
session.keepAlive(thinking, "remote");
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (msg.type === "task_complete" || msg.type === "turn_aborted") {
|
|
889
|
+
if (thinking) {
|
|
890
|
+
types.logger.debug("thinking completed");
|
|
891
|
+
thinking = false;
|
|
892
|
+
session.keepAlive(thinking, "remote");
|
|
893
|
+
}
|
|
894
|
+
diffProcessor.reset();
|
|
895
|
+
}
|
|
896
|
+
if (msg.type === "agent_reasoning_section_break") {
|
|
897
|
+
reasoningProcessor.handleSectionBreak();
|
|
898
|
+
}
|
|
899
|
+
if (msg.type === "agent_reasoning_delta") {
|
|
900
|
+
reasoningProcessor.processDelta(msg.delta);
|
|
901
|
+
}
|
|
902
|
+
if (msg.type === "agent_reasoning") {
|
|
903
|
+
reasoningProcessor.complete(msg.text);
|
|
904
|
+
}
|
|
905
|
+
if (msg.type === "agent_message") {
|
|
906
|
+
session.sendCodexMessage({
|
|
907
|
+
type: "message",
|
|
908
|
+
message: msg.message,
|
|
909
|
+
id: node_crypto.randomUUID()
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
|
|
913
|
+
let { call_id, type, ...inputs } = msg;
|
|
914
|
+
session.sendCodexMessage({
|
|
915
|
+
type: "tool-call",
|
|
916
|
+
name: "CodexBash",
|
|
917
|
+
callId: call_id,
|
|
918
|
+
input: inputs,
|
|
919
|
+
id: node_crypto.randomUUID()
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (msg.type === "exec_command_end") {
|
|
923
|
+
let { call_id, type, ...output } = msg;
|
|
924
|
+
session.sendCodexMessage({
|
|
925
|
+
type: "tool-call-result",
|
|
926
|
+
callId: call_id,
|
|
927
|
+
output,
|
|
928
|
+
id: node_crypto.randomUUID()
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (msg.type === "token_count") {
|
|
932
|
+
session.sendCodexMessage({
|
|
933
|
+
...msg,
|
|
934
|
+
id: node_crypto.randomUUID()
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (msg.type === "patch_apply_begin") {
|
|
938
|
+
let { call_id, auto_approved, changes } = msg;
|
|
939
|
+
const changeCount = Object.keys(changes).length;
|
|
940
|
+
const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
|
|
941
|
+
messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
|
|
942
|
+
session.sendCodexMessage({
|
|
943
|
+
type: "tool-call",
|
|
944
|
+
name: "CodexPatch",
|
|
945
|
+
callId: call_id,
|
|
946
|
+
input: {
|
|
947
|
+
auto_approved,
|
|
948
|
+
changes
|
|
949
|
+
},
|
|
950
|
+
id: node_crypto.randomUUID()
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
if (msg.type === "patch_apply_end") {
|
|
954
|
+
let { call_id, stdout, stderr, success } = msg;
|
|
955
|
+
if (success) {
|
|
956
|
+
const message = stdout || "Files modified successfully";
|
|
957
|
+
messageBuffer.addMessage(message.substring(0, 200), "result");
|
|
958
|
+
} else {
|
|
959
|
+
const errorMsg = stderr || "Failed to modify files";
|
|
960
|
+
messageBuffer.addMessage(`Error: ${errorMsg.substring(0, 200)}`, "result");
|
|
961
|
+
}
|
|
962
|
+
session.sendCodexMessage({
|
|
963
|
+
type: "tool-call-result",
|
|
964
|
+
callId: call_id,
|
|
965
|
+
output: {
|
|
966
|
+
stdout,
|
|
967
|
+
stderr,
|
|
968
|
+
success
|
|
969
|
+
},
|
|
970
|
+
id: node_crypto.randomUUID()
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (msg.type === "turn_diff") {
|
|
974
|
+
if (msg.unified_diff) {
|
|
975
|
+
diffProcessor.processDiff(msg.unified_diff);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
const happyServer = await index.startHappyServer(session);
|
|
980
|
+
const bridgeCommand = node_path.join(types.projectPath(), "bin", "happy-mcp.mjs");
|
|
981
|
+
const mcpServers = {
|
|
982
|
+
happy: {
|
|
983
|
+
command: bridgeCommand,
|
|
984
|
+
args: ["--url", happyServer.url]
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
let first = true;
|
|
988
|
+
try {
|
|
989
|
+
types.logger.debug("[codex]: client.connect begin");
|
|
990
|
+
await client.connect();
|
|
991
|
+
types.logger.debug("[codex]: client.connect done");
|
|
992
|
+
let wasCreated = false;
|
|
993
|
+
let currentModeHash = null;
|
|
994
|
+
let pending = null;
|
|
995
|
+
let nextExperimentalResume = null;
|
|
996
|
+
while (!shouldExit) {
|
|
997
|
+
logActiveHandles("loop-top");
|
|
998
|
+
let message = pending;
|
|
999
|
+
pending = null;
|
|
1000
|
+
if (!message) {
|
|
1001
|
+
const batch = await messageQueue.waitForMessagesAndGetAsString(abortController.signal);
|
|
1002
|
+
if (!batch || shouldExit) {
|
|
1003
|
+
types.logger.debug(`[codex]: batch=${!!batch}, shouldExit=${shouldExit}`);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
message = batch;
|
|
1007
|
+
}
|
|
1008
|
+
if (!message) {
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
if (wasCreated && currentModeHash && message.hash !== currentModeHash) {
|
|
1012
|
+
types.logger.debug("[Codex] Mode changed \u2013 restarting Codex session");
|
|
1013
|
+
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
1014
|
+
messageBuffer.addMessage("Starting new Codex session (mode changed)...", "status");
|
|
1015
|
+
try {
|
|
1016
|
+
const prevSessionId = client.getSessionId();
|
|
1017
|
+
nextExperimentalResume = findCodexResumeFile(prevSessionId);
|
|
1018
|
+
if (nextExperimentalResume) {
|
|
1019
|
+
types.logger.debug(`[Codex] Found resume file for session ${prevSessionId}: ${nextExperimentalResume}`);
|
|
1020
|
+
messageBuffer.addMessage("Resuming previous context\u2026", "status");
|
|
1021
|
+
} else {
|
|
1022
|
+
types.logger.debug("[Codex] No resume file found for previous session");
|
|
1023
|
+
}
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
types.logger.debug("[Codex] Error while searching resume file", e);
|
|
1026
|
+
}
|
|
1027
|
+
client.clearSession();
|
|
1028
|
+
wasCreated = false;
|
|
1029
|
+
currentModeHash = null;
|
|
1030
|
+
pending = message;
|
|
1031
|
+
permissionHandler.reset();
|
|
1032
|
+
reasoningProcessor.abort();
|
|
1033
|
+
diffProcessor.reset();
|
|
1034
|
+
thinking = false;
|
|
1035
|
+
session.keepAlive(thinking, "remote");
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
messageBuffer.addMessage(message.message, "user");
|
|
1039
|
+
currentModeHash = message.hash;
|
|
1040
|
+
try {
|
|
1041
|
+
const approvalPolicy = (() => {
|
|
1042
|
+
switch (message.mode.permissionMode) {
|
|
1043
|
+
case "default":
|
|
1044
|
+
return "untrusted";
|
|
1045
|
+
case "read-only":
|
|
1046
|
+
return "never";
|
|
1047
|
+
case "safe-yolo":
|
|
1048
|
+
return "on-failure";
|
|
1049
|
+
case "yolo":
|
|
1050
|
+
return "on-failure";
|
|
1051
|
+
}
|
|
1052
|
+
})();
|
|
1053
|
+
const sandbox = (() => {
|
|
1054
|
+
switch (message.mode.permissionMode) {
|
|
1055
|
+
case "default":
|
|
1056
|
+
return "workspace-write";
|
|
1057
|
+
case "read-only":
|
|
1058
|
+
return "read-only";
|
|
1059
|
+
case "safe-yolo":
|
|
1060
|
+
return "workspace-write";
|
|
1061
|
+
case "yolo":
|
|
1062
|
+
return "danger-full-access";
|
|
1063
|
+
}
|
|
1064
|
+
})();
|
|
1065
|
+
if (!wasCreated) {
|
|
1066
|
+
const startConfig = {
|
|
1067
|
+
prompt: first ? message.message + "\n\n" + index.trimIdent(`Based on this message, call functions.happy__change_title to change chat session title that would represent the current task. If chat idea would change dramatically - call this function again to update the title.`) : message.message,
|
|
1068
|
+
sandbox,
|
|
1069
|
+
"approval-policy": approvalPolicy,
|
|
1070
|
+
config: { mcp_servers: mcpServers }
|
|
1071
|
+
};
|
|
1072
|
+
if (message.mode.model) {
|
|
1073
|
+
startConfig.model = message.mode.model;
|
|
1074
|
+
}
|
|
1075
|
+
if (nextExperimentalResume) {
|
|
1076
|
+
startConfig.config.experimental_resume = nextExperimentalResume;
|
|
1077
|
+
nextExperimentalResume = null;
|
|
1078
|
+
}
|
|
1079
|
+
await client.startSession(
|
|
1080
|
+
startConfig,
|
|
1081
|
+
{ signal: abortController.signal }
|
|
1082
|
+
);
|
|
1083
|
+
wasCreated = true;
|
|
1084
|
+
first = false;
|
|
1085
|
+
} else {
|
|
1086
|
+
await client.continueSession(
|
|
1087
|
+
message.message,
|
|
1088
|
+
{ signal: abortController.signal }
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
types.logger.warn("Error in codex session:", error);
|
|
1093
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1094
|
+
messageBuffer.addMessage("Aborted by user", "status");
|
|
1095
|
+
session.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
1096
|
+
} else {
|
|
1097
|
+
messageBuffer.addMessage("Process exited unexpectedly", "status");
|
|
1098
|
+
session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
1099
|
+
}
|
|
1100
|
+
} finally {
|
|
1101
|
+
permissionHandler.reset();
|
|
1102
|
+
reasoningProcessor.abort();
|
|
1103
|
+
diffProcessor.reset();
|
|
1104
|
+
thinking = false;
|
|
1105
|
+
session.keepAlive(thinking, "remote");
|
|
1106
|
+
logActiveHandles("after-turn");
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} finally {
|
|
1110
|
+
types.logger.debug("[codex]: Cleanup start");
|
|
1111
|
+
logActiveHandles("cleanup-start");
|
|
1112
|
+
try {
|
|
1113
|
+
types.logger.debug("[codex]: sendSessionDeath");
|
|
1114
|
+
session.sendSessionDeath();
|
|
1115
|
+
types.logger.debug("[codex]: flush begin");
|
|
1116
|
+
await session.flush();
|
|
1117
|
+
types.logger.debug("[codex]: flush done");
|
|
1118
|
+
types.logger.debug("[codex]: session.close begin");
|
|
1119
|
+
await session.close();
|
|
1120
|
+
types.logger.debug("[codex]: session.close done");
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
types.logger.debug("[codex]: Error while closing session", e);
|
|
1123
|
+
}
|
|
1124
|
+
types.logger.debug("[codex]: client.disconnect begin");
|
|
1125
|
+
await client.disconnect();
|
|
1126
|
+
types.logger.debug("[codex]: client.disconnect done");
|
|
1127
|
+
types.logger.debug("[codex]: happyServer.stop");
|
|
1128
|
+
happyServer.stop();
|
|
1129
|
+
if (process.stdin.isTTY) {
|
|
1130
|
+
types.logger.debug("[codex]: setRawMode(false)");
|
|
1131
|
+
try {
|
|
1132
|
+
process.stdin.setRawMode(false);
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (hasTTY) {
|
|
1137
|
+
types.logger.debug("[codex]: stdin.pause()");
|
|
1138
|
+
try {
|
|
1139
|
+
process.stdin.pause();
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
types.logger.debug("[codex]: clearInterval(keepAlive)");
|
|
1144
|
+
clearInterval(keepAliveInterval);
|
|
1145
|
+
if (inkInstance) {
|
|
1146
|
+
types.logger.debug("[codex]: inkInstance.unmount()");
|
|
1147
|
+
inkInstance.unmount();
|
|
1148
|
+
}
|
|
1149
|
+
messageBuffer.clear();
|
|
1150
|
+
logActiveHandles("cleanup-end");
|
|
1151
|
+
types.logger.debug("[codex]: Cleanup completed");
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
exports.runCodex = runCodex;
|