clay-server 2.31.0 → 2.32.0-beta.2

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 (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +97 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -0,0 +1,968 @@
1
+ // YOKE Codex Adapter
2
+ // -------------------
3
+ // Implements the YOKE interface using codex app-server protocol.
4
+ // Bidirectional JSON-RPC over stdin/stdout enables interactive approval flows.
5
+
6
+ var { CodexAppServer } = require("../codex-app-server");
7
+
8
+ // --- Event flattening ---
9
+ // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
10
+ //
11
+ // App-server events use slash notation (item/started) and camelCase item types.
12
+ // We normalize to the same YOKE event format used by the rest of the system.
13
+ //
14
+ // Server -> Client notifications:
15
+ // thread/started -> { params: { thread } }
16
+ // turn/started -> { params: {} }
17
+ // turn/completed -> { params: { usage } }
18
+ // turn/failed -> { params: { error } }
19
+ // item/started -> { params: { item } }
20
+ // item/updated -> { params: { item } }
21
+ // item/completed -> { params: { item } }
22
+ // item/agentMessage/delta -> { params: { itemId, delta } }
23
+ //
24
+ // Item types (camelCase in app-server):
25
+ // agentMessage -> text response
26
+ // reasoning -> thinking
27
+ // commandExecution -> bash/shell
28
+ // fileChange -> file edits
29
+ // mcpToolCall -> MCP tool
30
+ // webSearch -> web search
31
+ // error -> error
32
+
33
+ var _uuidCounter = 0;
34
+ function generateUuid() {
35
+ var ts = Date.now().toString(36);
36
+ var cnt = (++_uuidCounter).toString(36);
37
+ var rnd = Math.random().toString(36).substring(2, 8);
38
+ return "codex-" + ts + "-" + cnt + "-" + rnd;
39
+ }
40
+
41
+ function flattenEvent(notification, state) {
42
+ var events = [];
43
+ var method = notification.method;
44
+ var params = notification.params || {};
45
+
46
+
47
+ if (method === "thread/started") {
48
+ state.threadId = params.thread ? params.thread.id : (params.threadId || null);
49
+ return events;
50
+ }
51
+
52
+ if (method === "turn/started") {
53
+ state.turnStarted = true;
54
+ var userUuid = generateUuid();
55
+ events.push({ yokeType: "turn_start", uuid: userUuid, messageType: "user" });
56
+ return events;
57
+ }
58
+
59
+ if (method === "turn/completed") {
60
+ var usage = params.usage || null;
61
+ var turnStatus = params.status || (params.turn && params.turn.status) || null;
62
+ state.lastUsage = usage;
63
+ // Emit interrupted status so UI shows "stopped" message
64
+ if (turnStatus === "interrupted" || state.aborted) {
65
+ events.push({ yokeType: "interrupted" });
66
+ }
67
+ var inputTokens = state.lastInputTokens || (usage ? (usage.input_tokens || 0) + (usage.cached_input_tokens || 0) : 0);
68
+ var outputTokens = usage ? (usage.output_tokens || 0) : 0;
69
+ var cachedTokens = usage ? (usage.cached_input_tokens || 0) : 0;
70
+ var hasTokenData = inputTokens > 0 || outputTokens > 0;
71
+ var resultModelUsage = {};
72
+ resultModelUsage[state.model] = { contextWindow: null };
73
+ var assistantUuid = generateUuid();
74
+ events.push({
75
+ yokeType: "result",
76
+ uuid: assistantUuid,
77
+ messageType: "assistant",
78
+ cost: null,
79
+ duration: null,
80
+ usage: hasTokenData ? {
81
+ input_tokens: inputTokens,
82
+ output_tokens: outputTokens,
83
+ cache_read_input_tokens: cachedTokens,
84
+ cache_creation_input_tokens: 0,
85
+ } : null,
86
+ modelUsage: resultModelUsage,
87
+ sessionId: state.threadId || null,
88
+ lastStreamInputTokens: state.lastInputTokens || null,
89
+ });
90
+ state.lastInputTokens = null;
91
+ return events;
92
+ }
93
+
94
+ if (method === "turn/failed") {
95
+ events.push({
96
+ yokeType: "error",
97
+ text: params.error ? params.error.message : "Turn failed",
98
+ });
99
+ return events;
100
+ }
101
+
102
+ // Rate limits from Codex account
103
+ if (method === "account/rateLimits/updated") {
104
+ var rl = params.rateLimits;
105
+ if (rl) {
106
+ var windows = [
107
+ { key: "primary", type: "five_hour" },
108
+ { key: "secondary", type: "seven_day" },
109
+ ];
110
+ for (var wi = 0; wi < windows.length; wi++) {
111
+ var w = rl[windows[wi].key];
112
+ if (!w) continue;
113
+ var utilization = (w.usedPercent || 0) / 100;
114
+ var status = "allowed";
115
+ if (w.usedPercent >= 100) status = "rejected";
116
+ else if (w.usedPercent >= 80) status = "allowed_warning";
117
+ events.push({
118
+ yokeType: "rate_limit",
119
+ rateLimitInfo: {
120
+ status: status,
121
+ resetsAt: w.resetsAt || null,
122
+ rateLimitType: windows[wi].type,
123
+ utilization: utilization,
124
+ isUsingOverage: false,
125
+ },
126
+ });
127
+ }
128
+ }
129
+ return events;
130
+ }
131
+
132
+ // Streaming text delta (app-server specific, not present in exec mode)
133
+ if (method === "item/agentMessage/delta") {
134
+ var deltaItemId = params.itemId || params.id;
135
+ if (deltaItemId && !state.textBlocks[deltaItemId]) {
136
+ state.textBlocks[deltaItemId] = true;
137
+ state.blockCounter++;
138
+ events.push({ yokeType: "text_start", blockId: "blk_" + state.blockCounter });
139
+ }
140
+ if (params.delta) {
141
+ events.push({
142
+ yokeType: "text_delta",
143
+ blockId: "blk_" + state.blockCounter,
144
+ text: params.delta,
145
+ });
146
+ // Track cumulative streamed length so item/completed doesn't re-send the full text
147
+ if (deltaItemId) {
148
+ state.textLengths[deltaItemId] = (state.textLengths[deltaItemId] || 0) + params.delta.length;
149
+ }
150
+ }
151
+ return events;
152
+ }
153
+
154
+ // serverRequest/resolved - confirmation that an approval was processed
155
+ if (method === "serverRequest/resolved") {
156
+ return events; // no-op, approval already handled
157
+ }
158
+
159
+ // Item events
160
+ if (method === "item/started" || method === "item/updated" || method === "item/completed") {
161
+ var item = params.item;
162
+ if (!item) return events;
163
+
164
+ var evtPhase = method.split("/")[1]; // "started", "updated", "completed"
165
+
166
+ // Agent message (text response)
167
+ if (item.type === "agentMessage" || item.type === "agent_message") {
168
+ if (!state.textBlocks[item.id]) {
169
+ state.textBlocks[item.id] = true;
170
+ state.blockCounter++;
171
+ events.push({ yokeType: "text_start", blockId: "blk_" + state.blockCounter });
172
+ }
173
+ if (item.text) {
174
+ var prevLen = state.textLengths[item.id] || 0;
175
+ if (item.text.length > prevLen) {
176
+ events.push({
177
+ yokeType: "text_delta",
178
+ blockId: "blk_" + state.blockCounter,
179
+ text: item.text.substring(prevLen),
180
+ });
181
+ state.textLengths[item.id] = item.text.length;
182
+ }
183
+ }
184
+ return events;
185
+ }
186
+
187
+ // Reasoning (thinking)
188
+ if (item.type === "reasoning") {
189
+ if (!state.thinkingBlocks[item.id]) {
190
+ state.blockCounter++;
191
+ state.thinkingBlocks[item.id] = "blk_" + state.blockCounter;
192
+ events.push({ yokeType: "thinking_start", blockId: "blk_" + state.blockCounter });
193
+ }
194
+ if (item.text) {
195
+ var thinkBlockId = state.thinkingBlocks[item.id];
196
+ var prevThinkLen = state.thinkingLengths[item.id] || 0;
197
+ if (item.text.length > prevThinkLen) {
198
+ events.push({
199
+ yokeType: "thinking_delta",
200
+ blockId: thinkBlockId,
201
+ text: item.text.substring(prevThinkLen),
202
+ });
203
+ state.thinkingLengths[item.id] = item.text.length;
204
+ }
205
+ }
206
+ if (evtPhase === "completed") {
207
+ events.push({ yokeType: "thinking_stop", blockId: state.thinkingBlocks[item.id] });
208
+ }
209
+ return events;
210
+ }
211
+
212
+ // Command execution (bash/shell)
213
+ if (item.type === "commandExecution" || item.type === "command_execution") {
214
+ if (!state.toolBlocks[item.id]) {
215
+ state.blockCounter++;
216
+ state.toolBlocks[item.id] = "blk_" + state.blockCounter;
217
+ var toolBlockId = state.toolBlocks[item.id];
218
+ events.push({
219
+ yokeType: "tool_start",
220
+ blockId: toolBlockId,
221
+ toolId: item.id,
222
+ toolName: "Bash",
223
+ });
224
+ events.push({
225
+ yokeType: "tool_executing",
226
+ blockId: toolBlockId,
227
+ toolId: item.id,
228
+ toolName: "Bash",
229
+ input: { command: item.command },
230
+ });
231
+ }
232
+ if (evtPhase === "completed") {
233
+ events.push({
234
+ yokeType: "tool_result",
235
+ toolId: item.id,
236
+ blockId: state.toolBlocks[item.id],
237
+ content: item.aggregated_output || item.output || "",
238
+ isError: item.status === "failed",
239
+ });
240
+ }
241
+ return events;
242
+ }
243
+
244
+ // File change
245
+ if (item.type === "fileChange" || item.type === "file_change") {
246
+ if (!state.toolBlocks[item.id]) {
247
+ state.blockCounter++;
248
+ state.toolBlocks[item.id] = "blk_" + state.blockCounter;
249
+ var fcBlockId = state.toolBlocks[item.id];
250
+ events.push({
251
+ yokeType: "tool_start",
252
+ blockId: fcBlockId,
253
+ toolId: item.id,
254
+ toolName: "Edit",
255
+ });
256
+ var changeDesc = (item.changes || []).map(function(c) { return c.kind + " " + c.path; }).join(", ");
257
+ events.push({
258
+ yokeType: "tool_executing",
259
+ blockId: fcBlockId,
260
+ toolId: item.id,
261
+ toolName: "Edit",
262
+ input: { changes: changeDesc },
263
+ });
264
+ }
265
+ if (evtPhase === "completed") {
266
+ events.push({
267
+ yokeType: "tool_result",
268
+ toolId: item.id,
269
+ blockId: state.toolBlocks[item.id],
270
+ content: item.status === "completed" ? "Changes applied" : "Changes failed",
271
+ isError: item.status === "failed",
272
+ });
273
+ }
274
+ return events;
275
+ }
276
+
277
+ // MCP tool call
278
+ if (item.type === "mcpToolCall" || item.type === "mcp_tool_call") {
279
+ console.log("[yoke/codex] MCP event:", method, "tool=" + (item.tool || "?"), "status=" + (item.status || "?"), "error=" + (item.error ? JSON.stringify(item.error) : "none"));
280
+ if (!state.toolBlocks[item.id]) {
281
+ state.blockCounter++;
282
+ state.toolBlocks[item.id] = "blk_" + state.blockCounter;
283
+ var mcpBlockId = state.toolBlocks[item.id];
284
+ events.push({
285
+ yokeType: "tool_start",
286
+ blockId: mcpBlockId,
287
+ toolId: item.id,
288
+ toolName: item.tool || "mcp_tool",
289
+ });
290
+ events.push({
291
+ yokeType: "tool_executing",
292
+ blockId: mcpBlockId,
293
+ toolId: item.id,
294
+ toolName: item.tool || "mcp_tool",
295
+ input: item.arguments || {},
296
+ });
297
+ }
298
+ if (evtPhase === "completed") {
299
+ var resultText = "";
300
+ if (item.result && item.result.content) {
301
+ resultText = item.result.content.map(function(c) { return c.text || ""; }).join("\n");
302
+ }
303
+ if (item.error) resultText = item.error.message;
304
+ events.push({
305
+ yokeType: "tool_result",
306
+ toolId: item.id,
307
+ blockId: state.toolBlocks[item.id],
308
+ content: resultText,
309
+ isError: !!item.error,
310
+ });
311
+ }
312
+ return events;
313
+ }
314
+
315
+ // Web search
316
+ if (item.type === "webSearch" || item.type === "web_search") {
317
+ if (!state.toolBlocks[item.id]) {
318
+ state.blockCounter++;
319
+ state.toolBlocks[item.id] = "blk_" + state.blockCounter;
320
+ events.push({
321
+ yokeType: "tool_start",
322
+ blockId: state.toolBlocks[item.id],
323
+ toolId: item.id,
324
+ toolName: "WebSearch",
325
+ });
326
+ }
327
+ return events;
328
+ }
329
+
330
+ // Error item
331
+ if (item.type === "error") {
332
+ events.push({
333
+ yokeType: "error",
334
+ text: item.message || "Unknown error",
335
+ });
336
+ return events;
337
+ }
338
+ }
339
+
340
+ // Token usage update - track input tokens for context bar
341
+ if (method === "thread/tokenUsage/updated") {
342
+ var tu = params.tokenUsage;
343
+ if (tu && tu.total) {
344
+ state.lastInputTokens = tu.total.inputTokens || 0;
345
+ }
346
+ return events;
347
+ }
348
+
349
+ // Unknown event type - pass through
350
+ console.log("[yoke/codex] UNHANDLED event:", method, JSON.stringify(params).substring(0, 200));
351
+ events.push({
352
+ yokeType: "runtime_specific",
353
+ vendor: "codex",
354
+ eventType: method,
355
+ raw: params,
356
+ });
357
+
358
+ return events;
359
+ }
360
+
361
+ // --- QueryHandle ---
362
+
363
+ function createCodexQueryHandle(appServer, queryOpts) {
364
+ var abortController = queryOpts.abortController;
365
+ var systemPrompt = queryOpts.systemPrompt || "";
366
+ var canUseTool = queryOpts.canUseTool || null;
367
+
368
+ // Check if the query was cancelled (either via handle.abort() or direct signal abort)
369
+ function isCancelled() {
370
+ return state.aborted || (abortController && abortController.signal && abortController.signal.aborted);
371
+ }
372
+
373
+ var state = {
374
+ blockCounter: 0,
375
+ threadId: null,
376
+ turnStarted: false,
377
+ lastUsage: null,
378
+ lastInputTokens: null, // from thread/tokenUsage/updated
379
+ done: false,
380
+ aborted: false,
381
+ loopStarted: false,
382
+ model: queryOpts.model || "gpt-5.4",
383
+ // Track incremental text deltas
384
+ textBlocks: {}, // itemId -> true (text_start sent)
385
+ textLengths: {}, // itemId -> last sent length
386
+ thinkingBlocks: {}, // itemId -> blockId
387
+ thinkingLengths: {}, // itemId -> last sent length
388
+ toolBlocks: {}, // itemId -> blockId (for tool_start dedup)
389
+ };
390
+
391
+ // Internal event buffer for async iterator
392
+ var eventBuffer = [];
393
+ var eventWaiting = null;
394
+ var iteratorDone = false;
395
+
396
+ function pushEvent(evt) {
397
+ if (iteratorDone) return;
398
+ if (eventWaiting) {
399
+ var resolve = eventWaiting;
400
+ eventWaiting = null;
401
+ resolve({ value: evt, done: false });
402
+ } else {
403
+ eventBuffer.push(evt);
404
+ }
405
+ }
406
+
407
+ function endIterator() {
408
+ iteratorDone = true;
409
+ if (eventWaiting) {
410
+ var resolve = eventWaiting;
411
+ eventWaiting = null;
412
+ resolve({ value: undefined, done: true });
413
+ }
414
+ }
415
+
416
+ // Message queue for multi-turn
417
+ var messageQueue = [];
418
+ var messageWaiting = null;
419
+ var messageQueueEnded = false;
420
+
421
+ function pushMessageToQueue(msg) {
422
+ if (messageQueueEnded) return;
423
+ if (messageWaiting) {
424
+ var resolve = messageWaiting;
425
+ messageWaiting = null;
426
+ resolve(msg);
427
+ } else {
428
+ messageQueue.push(msg);
429
+ }
430
+ }
431
+
432
+ function waitForMessage() {
433
+ if (messageQueue.length > 0) return Promise.resolve(messageQueue.shift());
434
+ if (messageQueueEnded) return Promise.resolve(null);
435
+ return new Promise(function(resolve) { messageWaiting = resolve; });
436
+ }
437
+
438
+ // Track whether this turn is still active (waiting for turn/completed or turn/failed)
439
+ var turnResolve = null;
440
+
441
+ // --- App-server event handler ---
442
+ function handleServerEvent(msg) {
443
+ var method = msg.method;
444
+ var params = msg.params || {};
445
+
446
+ // Ignore events from other threads (app-server is shared across sessions)
447
+ if (params.threadId && state.threadId && params.threadId !== state.threadId) return;
448
+
449
+ // After abort, only let turn-ending events through
450
+ if (isCancelled() && method !== "turn/completed" && method !== "turn/failed" && method !== "serverRequest/resolved" && method !== "thread/status/changed") return;
451
+
452
+ // --- Approval helper ---
453
+ // canUseTool returns { behavior: "allow"|"deny", updatedInput } or truthy/falsy
454
+ function isApproved(decision) {
455
+ if (!decision) return false;
456
+ if (decision === true) return true;
457
+ if (decision.behavior === "allow") return true;
458
+ return false;
459
+ }
460
+
461
+ // Command approval request
462
+ if (method === "item/commandExecution/requestApproval") {
463
+ var cmdParams = msg.params || {};
464
+ if (canUseTool) {
465
+ canUseTool("Bash", { command: cmdParams.command }, {}).then(function(decision) {
466
+ appServer.respond(msg.id, isApproved(decision) ? "accept" : "decline");
467
+ }).catch(function(err) {
468
+ console.error("[yoke/codex] canUseTool error:", err.message);
469
+ appServer.respond(msg.id, "decline");
470
+ });
471
+ } else {
472
+ appServer.respond(msg.id, "accept");
473
+ }
474
+ return;
475
+ }
476
+
477
+ // File change approval request
478
+ if (method === "item/fileChange/requestApproval") {
479
+ var fcParams = msg.params || {};
480
+ if (canUseTool) {
481
+ var changeInfo = (fcParams.changes || []).map(function(c) { return c.kind + " " + c.path; }).join(", ");
482
+ canUseTool("Edit", { changes: changeInfo, path: fcParams.path }, {}).then(function(decision) {
483
+ appServer.respond(msg.id, isApproved(decision) ? "accept" : "decline");
484
+ }).catch(function(err) {
485
+ console.error("[yoke/codex] canUseTool error:", err.message);
486
+ appServer.respond(msg.id, "decline");
487
+ });
488
+ } else {
489
+ appServer.respond(msg.id, "accept");
490
+ }
491
+ return;
492
+ }
493
+
494
+ // MCP tool approval / elicitation (app-server uses mcpServer/elicitation/request)
495
+ if (method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request") {
496
+ var mcpParams = msg.params || {};
497
+ var mcpMeta = mcpParams._meta || {};
498
+ console.log("[yoke/codex] MCP approval request:", (mcpMeta.tool || "?"), "server=" + (mcpParams.serverName || "?"));
499
+ if (canUseTool) {
500
+ canUseTool("mcp__" + (mcpParams.serverName || "unknown") + "__" + (mcpMeta.tool || "call"), mcpParams, {}).then(function(decision) {
501
+ appServer.respond(msg.id, { action: isApproved(decision) ? "accept" : "decline" });
502
+ }).catch(function(err) {
503
+ console.error("[yoke/codex] MCP canUseTool error:", err.message);
504
+ appServer.respond(msg.id, { action: "decline" });
505
+ });
506
+ } else {
507
+ appServer.respond(msg.id, { action: "accept" });
508
+ }
509
+ return;
510
+ }
511
+
512
+ // Regular events: flatten and push to iterator
513
+ var yokeEvents = flattenEvent(msg, state);
514
+ for (var i = 0; i < yokeEvents.length; i++) {
515
+ pushEvent(yokeEvents[i]);
516
+ }
517
+
518
+ // Resolve turn promise when turn ends
519
+ if (method === "turn/completed" || method === "turn/failed") {
520
+ if (turnResolve) {
521
+ var resolve = turnResolve;
522
+ turnResolve = null;
523
+ resolve();
524
+ }
525
+ }
526
+ }
527
+
528
+ // --- Main query loop ---
529
+ async function runQueryLoop(initialMessage) {
530
+ // Prepend system prompt (project instructions from YOKE layer) to first message
531
+ var currentMessage = systemPrompt
532
+ ? systemPrompt + "\n\n" + initialMessage
533
+ : initialMessage;
534
+
535
+ try {
536
+ // Set event handler on app-server
537
+ appServer.eventHandler = handleServerEvent;
538
+
539
+ // Start or resume thread
540
+ var threadParams = {
541
+ model: queryOpts.model || "gpt-5.4",
542
+ sandboxMode: queryOpts.sandboxMode || "workspace-write",
543
+ approvalPolicy: queryOpts.approvalPolicy || "on-failure",
544
+ workingDirectory: queryOpts.cwd,
545
+ skipGitRepoCheck: true,
546
+ };
547
+ if (queryOpts.modelReasoningEffort) {
548
+ threadParams.modelReasoningEffort = queryOpts.modelReasoningEffort;
549
+ }
550
+ if (queryOpts.webSearchMode) {
551
+ threadParams.webSearchMode = queryOpts.webSearchMode;
552
+ }
553
+
554
+ var threadResult;
555
+ if (queryOpts.resumeSessionId) {
556
+ threadResult = await appServer.send("thread/resume", {
557
+ threadId: queryOpts.resumeSessionId,
558
+ model: threadParams.model,
559
+ }, 60000);
560
+ } else {
561
+ threadResult = await appServer.send("thread/start", threadParams, 60000);
562
+ }
563
+
564
+ if (threadResult && threadResult.thread) {
565
+ state.threadId = threadResult.thread.id;
566
+ }
567
+
568
+ while (!isCancelled()) {
569
+ // Reset per-turn state
570
+ state.turnStarted = false;
571
+ state.textBlocks = {};
572
+ state.textLengths = {};
573
+ state.thinkingBlocks = {};
574
+ state.thinkingLengths = {};
575
+ state.toolBlocks = {};
576
+
577
+ // Start turn
578
+ var turnPromise = new Promise(function(resolve) { turnResolve = resolve; });
579
+
580
+ var input;
581
+ if (typeof currentMessage === "string") {
582
+ input = [{ type: "text", text: currentMessage }];
583
+ } else {
584
+ input = currentMessage;
585
+ }
586
+
587
+ await appServer.send("turn/start", {
588
+ threadId: state.threadId,
589
+ input: input,
590
+ }, 60000);
591
+
592
+ // Wait for turn to complete
593
+ await turnPromise;
594
+
595
+ if (isCancelled()) break;
596
+
597
+ // Wait for next message (multi-turn)
598
+ var nextMsg = await waitForMessage();
599
+ if (nextMsg === null) break;
600
+ currentMessage = nextMsg;
601
+ }
602
+ } catch (e) {
603
+ // Suppress AbortError when the user stopped the query.
604
+ if (!isCancelled() && e.name !== "AbortError") {
605
+ console.error("[yoke/codex] runQueryLoop error:", e.message || e);
606
+ console.error("[yoke/codex] stack:", e.stack || "(no stack)");
607
+ pushEvent({
608
+ yokeType: "error",
609
+ text: e.message || String(e),
610
+ });
611
+ }
612
+ }
613
+
614
+ state.done = true;
615
+ endIterator();
616
+ }
617
+
618
+ var handle = {
619
+ [Symbol.asyncIterator]: function() {
620
+ return {
621
+ next: function() {
622
+ if (eventBuffer.length > 0) {
623
+ return Promise.resolve({ value: eventBuffer.shift(), done: false });
624
+ }
625
+ if (iteratorDone) {
626
+ return Promise.resolve({ value: undefined, done: true });
627
+ }
628
+ return new Promise(function(resolve) { eventWaiting = resolve; });
629
+ },
630
+ };
631
+ },
632
+
633
+ pushMessage: function(text, images) {
634
+ var input;
635
+ if (images && images.length > 0) {
636
+ input = [];
637
+ for (var i = 0; i < images.length; i++) {
638
+ // Codex supports local_image with path, not base64
639
+ // For now, text-only
640
+ }
641
+ input.push({ type: "text", text: text || "" });
642
+ } else {
643
+ input = text || "";
644
+ }
645
+
646
+ if (!state.loopStarted) {
647
+ state.loopStarted = true;
648
+ runQueryLoop(input);
649
+ } else {
650
+ pushMessageToQueue(input);
651
+ }
652
+ },
653
+
654
+ setModel: function(model) {
655
+ // Model is set at thread creation. Cannot change mid-thread.
656
+ return Promise.resolve();
657
+ },
658
+
659
+ setEffort: function(effort) {
660
+ // Stored for next thread
661
+ return Promise.resolve();
662
+ },
663
+
664
+ setToolPolicy: function(policy) {
665
+ // Codex uses approvalPolicy at thread creation
666
+ return Promise.resolve();
667
+ },
668
+
669
+ stopTask: function(taskId) {
670
+ // Codex doesn't expose sub-task stopping
671
+ return Promise.resolve();
672
+ },
673
+
674
+ getContextUsage: function() {
675
+ return Promise.resolve(state.lastUsage);
676
+ },
677
+
678
+ abort: function() {
679
+ console.log("[yoke/codex] handle.abort() called, threadId=" + state.threadId + " already aborted=" + state.aborted);
680
+ state.aborted = true;
681
+ // Send turn/interrupt to stop the server-side turn
682
+ if (state.threadId && appServer.started) {
683
+ appServer.send("turn/interrupt", { threadId: state.threadId }, 5000).catch(function() {});
684
+ }
685
+ // End iterator immediately. sdk-bridge's post-loop code checks
686
+ // session.taskStopRequested and sends the interrupted message + done.
687
+ // This matches Claude's abort pattern.
688
+ if (turnResolve) {
689
+ var resolve = turnResolve;
690
+ turnResolve = null;
691
+ resolve();
692
+ }
693
+ endIterator();
694
+ },
695
+
696
+ close: function() {
697
+ messageQueueEnded = true;
698
+ if (messageWaiting) {
699
+ var resolve = messageWaiting;
700
+ messageWaiting = null;
701
+ resolve(null);
702
+ }
703
+ endIterator();
704
+ },
705
+
706
+ endInput: function() {
707
+ messageQueueEnded = true;
708
+ if (messageWaiting) {
709
+ var resolve = messageWaiting;
710
+ messageWaiting = null;
711
+ resolve(null);
712
+ }
713
+ },
714
+ };
715
+
716
+ // Listen for external abort (sdk-bridge's stopTask calls session.abortController.abort())
717
+ if (abortController && abortController.signal) {
718
+ abortController.signal.addEventListener("abort", function() {
719
+ if (!state.aborted) handle.abort();
720
+ }, { once: true });
721
+ }
722
+
723
+ return handle;
724
+ }
725
+
726
+ // --- Adapter factory ---
727
+
728
+ function createCodexAdapter(opts) {
729
+ var _cwd = (opts && opts.cwd) || process.cwd();
730
+ var _cachedModels = [];
731
+ var _appServer = null;
732
+ var _initPromise = null;
733
+ var _initOpts = null; // stored for query-time access
734
+
735
+ var adapter = {
736
+ vendor: "codex",
737
+
738
+ init: function(initOpts) {
739
+ // Already initialized - return cached result
740
+ if (_appServer && _appServer.started && _cachedModels.length > 0) {
741
+ return Promise.resolve({
742
+ models: _cachedModels,
743
+ defaultModel: "gpt-5.4",
744
+ skills: [],
745
+ slashCommands: [],
746
+ fastModeState: null,
747
+ capabilities: {
748
+ thinking: true,
749
+ betas: false,
750
+ rewind: false,
751
+ sessionResume: true,
752
+ promptSuggestions: false,
753
+ elicitation: true,
754
+ fileCheckpointing: false,
755
+ contextCompacting: false,
756
+ toolPolicy: ["ask", "allow-all"],
757
+ },
758
+ });
759
+ }
760
+
761
+ // Deduplicate concurrent init calls
762
+ if (_initPromise) return _initPromise;
763
+
764
+ _initPromise = (async function() {
765
+ _initOpts = initOpts;
766
+
767
+ var serverOpts = { cwd: _cwd };
768
+
769
+ // Extract adapter options
770
+ if (initOpts && initOpts.adapterOptions && initOpts.adapterOptions.CODEX) {
771
+ var co = initOpts.adapterOptions.CODEX;
772
+ if (co.apiKey) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_API_KEY: co.apiKey });
773
+ if (co.baseUrl) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_BASE_URL: co.baseUrl });
774
+ if (co.config) serverOpts.config = co.config;
775
+ }
776
+
777
+ // Track 1: Read local MCP server definitions from ~/.clay/mcp.json
778
+ // and inject into Codex config so Codex manages them natively.
779
+ var mcpServerConfig = {};
780
+ try {
781
+ var mcpLocal = require("../../mcp-local");
782
+ var localMcpServers = mcpLocal.readMergedServers();
783
+ var mcpNames = Object.keys(localMcpServers);
784
+ for (var mi = 0; mi < mcpNames.length; mi++) {
785
+ var ms = localMcpServers[mcpNames[mi]];
786
+ if (ms.command) {
787
+ mcpServerConfig[mcpNames[mi]] = { command: ms.command, args: ms.args || [] };
788
+ if (ms.env && Object.keys(ms.env).length > 0) {
789
+ mcpServerConfig[mcpNames[mi]].env = ms.env;
790
+ }
791
+ }
792
+ }
793
+ } catch (e) {
794
+ console.error("[codex] Failed to read local MCP config:", e.message);
795
+ }
796
+
797
+ // Track 2: Add clay-tools bridge server for in-app + remote MCP tools.
798
+ var bridgePath = require("path").join(__dirname, "..", "mcp-bridge-server.js");
799
+ var clayPort = (initOpts && initOpts.clayPort) || process.env.CLAY_PORT || 2633;
800
+ var clayTls = (initOpts && initOpts.clayTls) || false;
801
+ var clayAuthToken = (initOpts && initOpts.clayAuthToken) || "";
802
+ var claySlug = (initOpts && initOpts.slug) || "";
803
+ try {
804
+ if (require("fs").existsSync(bridgePath)) {
805
+ var bridgeArgs = [bridgePath, "--port", String(clayPort), "--slug", claySlug];
806
+ if (clayTls) bridgeArgs.push("--tls");
807
+ var bridgeEnv = {};
808
+ if (clayAuthToken) bridgeEnv.CLAY_AUTH_TOKEN = clayAuthToken;
809
+ mcpServerConfig["clay-tools"] = {
810
+ command: process.execPath,
811
+ args: bridgeArgs,
812
+ env: Object.keys(bridgeEnv).length > 0 ? bridgeEnv : undefined,
813
+ };
814
+ }
815
+ } catch (e) {
816
+ console.error("[codex] Failed to configure clay-tools bridge:", e.message);
817
+ }
818
+
819
+ if (Object.keys(mcpServerConfig).length > 0) {
820
+ serverOpts.config = Object.assign({}, serverOpts.config || {}, {
821
+ mcp_servers: mcpServerConfig,
822
+ });
823
+ console.log("[codex] MCP servers configured:", Object.keys(mcpServerConfig).join(", "));
824
+ try {
825
+ var names = Object.keys(mcpServerConfig);
826
+ for (var di = 0; di < names.length; di++) {
827
+ var sc = mcpServerConfig[names[di]];
828
+ console.log("[codex] MCP server '" + names[di] + "': command=" + sc.command + " args=" + JSON.stringify(sc.args));
829
+ }
830
+ } catch (e) {}
831
+ }
832
+
833
+ // Spawn and initialize app-server
834
+ _appServer = new CodexAppServer(null, serverOpts);
835
+ await _appServer.start();
836
+
837
+ await _appServer.send("initialize", {
838
+ clientInfo: { name: "clay", title: "Clay", version: "1.0.0" },
839
+ capabilities: { experimentalApi: true },
840
+ });
841
+ _appServer.notify("initialized", {});
842
+
843
+ console.log("[codex] App-server initialized, models: gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2");
844
+
845
+ _cachedModels = [
846
+ "gpt-5.4",
847
+ "gpt-5.4-mini",
848
+ "gpt-5.3-codex",
849
+ "gpt-5.3-codex-spark",
850
+ "gpt-5.2",
851
+ ];
852
+
853
+ _initPromise = null;
854
+
855
+ return {
856
+ models: _cachedModels,
857
+ defaultModel: "gpt-5.4",
858
+ skills: [],
859
+ slashCommands: [],
860
+ fastModeState: null,
861
+ capabilities: {
862
+ thinking: true,
863
+ betas: false,
864
+ rewind: false,
865
+ sessionResume: true,
866
+ promptSuggestions: false,
867
+ elicitation: true,
868
+ fileCheckpointing: false,
869
+ contextCompacting: false,
870
+ toolPolicy: ["ask", "allow-all"],
871
+ },
872
+ };
873
+ })();
874
+
875
+ return _initPromise;
876
+ },
877
+
878
+ supportedModels: function() {
879
+ return Promise.resolve(_cachedModels.slice());
880
+ },
881
+
882
+ createToolServer: function(def) {
883
+ // Codex handles tools internally (file ops, bash, etc.)
884
+ // MCP tools are configured via Codex config, not SDK.
885
+ console.log("[yoke/codex] createToolServer skipped: Codex handles tools internally");
886
+ return null;
887
+ },
888
+
889
+ createQuery: async function(queryOpts) {
890
+ if (!_appServer || !_appServer.started) {
891
+ throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
892
+ }
893
+
894
+ var model = queryOpts.model || "gpt-5.4";
895
+ var ac = queryOpts.abortController || new AbortController();
896
+
897
+ // Map YOKE options to Codex thread options
898
+ var codexOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.CODEX) || {};
899
+
900
+ var handleOpts = {
901
+ model: model,
902
+ cwd: queryOpts.cwd || _cwd,
903
+ systemPrompt: queryOpts.systemPrompt || "",
904
+ abortController: ac,
905
+ canUseTool: queryOpts.canUseTool || null,
906
+ resumeSessionId: queryOpts.resumeSessionId || null,
907
+ };
908
+
909
+ // Reasoning effort
910
+ if (queryOpts.effort || codexOpts.modelReasoningEffort) {
911
+ handleOpts.modelReasoningEffort = codexOpts.modelReasoningEffort || queryOpts.effort || "medium";
912
+ }
913
+
914
+ // Tool policy -> approval mode
915
+ if (queryOpts.toolPolicy === "allow-all") {
916
+ handleOpts.approvalPolicy = "never";
917
+ } else {
918
+ handleOpts.approvalPolicy = codexOpts.approvalPolicy || "on-failure";
919
+ }
920
+
921
+ // Sandbox mode
922
+ handleOpts.sandboxMode = codexOpts.sandboxMode || "workspace-write";
923
+
924
+ // Web search
925
+ if (codexOpts.webSearchMode && codexOpts.webSearchMode !== "disabled") {
926
+ handleOpts.webSearchMode = codexOpts.webSearchMode;
927
+ }
928
+
929
+ console.log("[yoke/codex] createQuery: model=" + model + " approval=" + handleOpts.approvalPolicy + " sandbox=" + handleOpts.sandboxMode);
930
+
931
+ var handle = createCodexQueryHandle(_appServer, handleOpts);
932
+ return handle;
933
+ },
934
+
935
+ // Codex has session persistence via thread IDs
936
+ getSessionInfo: function(sessionId) {
937
+ return Promise.resolve(null);
938
+ },
939
+ listSessions: function() { return Promise.resolve([]); },
940
+ renameSession: function() { return Promise.resolve(); },
941
+ forkSession: function(threadId, opts) {
942
+ if (!_appServer || !_appServer.started) return Promise.resolve(null);
943
+ return _appServer.send("thread/fork", { threadId: threadId }, 30000).then(function(result) {
944
+ var newThreadId = (result && result.thread) ? result.thread.id : null;
945
+ if (!newThreadId) throw new Error("thread/fork did not return a new thread id");
946
+ return { sessionId: newThreadId };
947
+ });
948
+ },
949
+ rollbackThread: function(threadId, numTurns) {
950
+ if (!_appServer || !_appServer.started) return Promise.resolve(null);
951
+ return _appServer.send("thread/rollback", { threadId: threadId, numTurns: numTurns }, 30000);
952
+ },
953
+
954
+ // Shutdown the app-server process
955
+ shutdown: function() {
956
+ if (_appServer) {
957
+ _appServer.stop();
958
+ _appServer = null;
959
+ }
960
+ },
961
+ };
962
+
963
+ return adapter;
964
+ }
965
+
966
+ module.exports = {
967
+ createCodexAdapter: createCodexAdapter,
968
+ };