clay-server 2.31.0 → 2.32.0-beta.10

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