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,1483 @@
1
+ // YOKE Claude Adapter
2
+ // --------------------
3
+ // Implements the YOKE interface using @anthropic-ai/claude-agent-sdk.
4
+ // This is the ONLY file (besides claude-worker.js) that imports the SDK.
5
+ // Also manages worker processes for OS-level user isolation.
6
+
7
+ var path = require("path");
8
+ var fs = require("fs");
9
+ var os = require("os");
10
+ var net = require("net");
11
+ var crypto = require("crypto");
12
+ var { spawn } = require("child_process");
13
+ var { resolveOsUserInfo } = require("../../os-users");
14
+
15
+ // --- SDK loading ---
16
+ // Async loader (ESM dynamic import, same pattern as current project.js getSDK)
17
+ var _sdkPromise = null;
18
+ function loadSDK() {
19
+ if (!_sdkPromise) _sdkPromise = import("@anthropic-ai/claude-agent-sdk");
20
+ return _sdkPromise;
21
+ }
22
+
23
+ // Sync loader (CJS require, for createToolServer which must be synchronous)
24
+ var _sdkSync = null;
25
+ function loadSDKSync() {
26
+ if (!_sdkSync) {
27
+ try { _sdkSync = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
28
+ console.error("[yoke/claude] Failed to load SDK synchronously:", e.message);
29
+ return null;
30
+ }
31
+ }
32
+ return _sdkSync;
33
+ }
34
+
35
+ // --- Internal message queue (async iterable for SDK prompt) ---
36
+ function createMessageQueue() {
37
+ var queue = [];
38
+ var waiting = null;
39
+ var ended = false;
40
+ return {
41
+ push: function(msg) {
42
+ if (ended) return;
43
+ if (waiting) {
44
+ var resolve = waiting;
45
+ waiting = null;
46
+ resolve({ value: msg, done: false });
47
+ } else {
48
+ queue.push(msg);
49
+ }
50
+ },
51
+ end: function() {
52
+ ended = true;
53
+ if (waiting) {
54
+ var resolve = waiting;
55
+ waiting = null;
56
+ resolve({ value: undefined, done: true });
57
+ }
58
+ },
59
+ [Symbol.asyncIterator]: function() {
60
+ return {
61
+ next: function() {
62
+ if (queue.length > 0) return Promise.resolve({ value: queue.shift(), done: false });
63
+ if (ended) return Promise.resolve({ value: undefined, done: true });
64
+ return new Promise(function(resolve) { waiting = resolve; });
65
+ },
66
+ };
67
+ },
68
+ };
69
+ }
70
+
71
+ // --- Event flattening ---
72
+ // Converts raw Claude SDK events into flat objects with a yokeType field.
73
+ // This decouples processSDKMessage from the deeply-nested SDK event shapes.
74
+ function flattenEvent(raw) {
75
+ // session_id and uuid are cross-cutting: attach to any event that has them
76
+ var base = {};
77
+ if (raw.session_id) base.sessionId = raw.session_id;
78
+ if (raw.uuid) {
79
+ base.uuid = raw.uuid;
80
+ base.messageType = raw.type; // "user" or "assistant"
81
+ base.parentToolUseId = raw.parent_tool_use_id || null;
82
+ }
83
+
84
+ // --- stream_event with nested event ---
85
+ if (raw.type === "stream_event" && raw.event) {
86
+ var evt = raw.event;
87
+
88
+ if (evt.type === "message_start") {
89
+ base.yokeType = "turn_start";
90
+ if (evt.message && evt.message.usage) {
91
+ var u = evt.message.usage;
92
+ base.inputTokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0);
93
+ }
94
+ return base;
95
+ }
96
+
97
+ if (evt.type === "content_block_start" && evt.content_block) {
98
+ var block = evt.content_block;
99
+ base.blockIndex = evt.index;
100
+ base.blockId = "blk_" + evt.index;
101
+ if (block.type === "tool_use") {
102
+ base.yokeType = "tool_start";
103
+ base.toolId = block.id;
104
+ base.toolName = block.name;
105
+ } else if (block.type === "thinking") {
106
+ base.yokeType = "thinking_start";
107
+ } else if (block.type === "text") {
108
+ base.yokeType = "text_start";
109
+ } else {
110
+ base.yokeType = "block_start";
111
+ base.blockType = block.type;
112
+ }
113
+ return base;
114
+ }
115
+
116
+ if (evt.type === "content_block_delta" && evt.delta) {
117
+ base.blockIndex = evt.index;
118
+ base.blockId = "blk_" + evt.index;
119
+ if (evt.delta.type === "text_delta") {
120
+ base.yokeType = "text_delta";
121
+ base.text = evt.delta.text;
122
+ } else if (evt.delta.type === "input_json_delta") {
123
+ base.yokeType = "tool_input_delta";
124
+ base.partialJson = evt.delta.partial_json;
125
+ } else if (evt.delta.type === "thinking_delta") {
126
+ base.yokeType = "thinking_delta";
127
+ base.text = evt.delta.thinking;
128
+ } else {
129
+ base.yokeType = "block_delta";
130
+ base.delta = evt.delta;
131
+ }
132
+ return base;
133
+ }
134
+
135
+ if (evt.type === "content_block_stop") {
136
+ base.yokeType = "block_stop";
137
+ base.blockIndex = evt.index;
138
+ base.blockId = "blk_" + evt.index;
139
+ return base;
140
+ }
141
+
142
+ if (evt.type === "message_stop") {
143
+ base.yokeType = "turn_stop";
144
+ return base;
145
+ }
146
+
147
+ // Unrecognized stream_event: pass through
148
+ base.yokeType = "stream_event";
149
+ base.event = evt;
150
+ return base;
151
+ }
152
+
153
+ // --- system events ---
154
+ if (raw.type === "system") {
155
+ if (raw.subtype === "init") {
156
+ base.yokeType = "init";
157
+ base.model = raw.model;
158
+ base.skills = raw.skills;
159
+ base.slashCommands = raw.slash_commands;
160
+ base.fastModeState = raw.fast_mode_state || null;
161
+ return base;
162
+ }
163
+ if (raw.subtype === "status") {
164
+ base.yokeType = "status";
165
+ base.status = raw.status;
166
+ return base;
167
+ }
168
+ if (raw.subtype === "task_started") {
169
+ base.yokeType = "task_started";
170
+ base.parentToolId = raw.tool_use_id;
171
+ base.taskId = raw.task_id;
172
+ base.description = raw.description || "";
173
+ return base;
174
+ }
175
+ if (raw.subtype === "task_progress") {
176
+ base.yokeType = "task_progress";
177
+ base.parentToolId = raw.tool_use_id;
178
+ base.taskId = raw.task_id;
179
+ base.usage = raw.usage || null;
180
+ base.lastToolName = raw.last_tool_name || null;
181
+ base.description = raw.description || "";
182
+ base.summary = raw.summary || null;
183
+ return base;
184
+ }
185
+ // Catch-all system event
186
+ base.yokeType = "system";
187
+ base.subtype = raw.subtype;
188
+ base.error = raw.error;
189
+ base.message = raw.message;
190
+ base.text = raw.text;
191
+ base.content = raw.content;
192
+ return base;
193
+ }
194
+
195
+ // --- result ---
196
+ if (raw.type === "result") {
197
+ base.yokeType = "result";
198
+ base.cost = raw.total_cost_usd;
199
+ base.duration = raw.duration_ms;
200
+ base.usage = raw.usage || null;
201
+ base.modelUsage = raw.modelUsage || null;
202
+ base.subtype = raw.subtype;
203
+ base.errors = raw.errors;
204
+ base.terminalReason = raw.terminal_reason;
205
+ base.fastModeState = raw.fast_mode_state || null;
206
+ return base;
207
+ }
208
+
209
+ // --- assistant/user messages (tool results, subagent messages, fallback text) ---
210
+ if (raw.type === "assistant" || raw.type === "user") {
211
+ if (raw.parent_tool_use_id) {
212
+ base.yokeType = "subagent_message";
213
+ base.parentToolUseId = raw.parent_tool_use_id;
214
+ base.messageRole = raw.type;
215
+ base.content = raw.message ? raw.message.content : null;
216
+ return base;
217
+ }
218
+ base.yokeType = "message";
219
+ base.messageRole = raw.type;
220
+ base.content = raw.message ? raw.message.content : null;
221
+ return base;
222
+ }
223
+
224
+ // --- rate_limit_event ---
225
+ if (raw.type === "rate_limit_event" && raw.rate_limit_info) {
226
+ base.yokeType = "rate_limit";
227
+ base.rateLimitInfo = raw.rate_limit_info;
228
+ return base;
229
+ }
230
+
231
+ // --- prompt_suggestion ---
232
+ if (raw.type === "prompt_suggestion") {
233
+ base.yokeType = "prompt_suggestion";
234
+ base.suggestion = raw.suggestion || "";
235
+ return base;
236
+ }
237
+
238
+ // --- task_notification ---
239
+ if (raw.type === "task_notification") {
240
+ base.yokeType = "task_notification";
241
+ base.parentToolId = raw.parent_tool_use_id;
242
+ base.taskId = raw.task_id;
243
+ base.status = raw.status || "completed";
244
+ base.summary = raw.summary || "";
245
+ base.usage = raw.usage || null;
246
+ return base;
247
+ }
248
+
249
+ // --- tool_progress ---
250
+ if (raw.type === "tool_progress") {
251
+ base.yokeType = "tool_progress";
252
+ base.parentToolId = raw.parent_tool_use_id;
253
+ base.text = raw.content || "";
254
+ return base;
255
+ }
256
+
257
+ // --- _worker_meta passthrough (not a raw SDK event) ---
258
+ if (raw.type === "_worker_meta") {
259
+ return raw;
260
+ }
261
+
262
+ // --- fallback: unknown event type ---
263
+ base.yokeType = "unknown";
264
+ base.rawType = raw.type;
265
+ base.raw = raw;
266
+ return base;
267
+ }
268
+
269
+ // --- QueryHandle ---
270
+ // Wraps a raw SDK query object with the YOKE QueryHandle interface.
271
+ // Events are flattened via flattenEvent before yielding.
272
+ function createQueryHandle(rawQuery, messageQueue, abortController) {
273
+ var handle = {
274
+ // Opaque adapter state (null for in-process queries)
275
+ _adapterState: null,
276
+
277
+ // Async iterable: yields flattened SDK events
278
+ [Symbol.asyncIterator]: function() {
279
+ var rawIter = rawQuery[Symbol.asyncIterator]();
280
+ return {
281
+ next: function() {
282
+ return rawIter.next().then(function(result) {
283
+ if (result.done) return result;
284
+ return { value: flattenEvent(result.value), done: false };
285
+ });
286
+ },
287
+ };
288
+ },
289
+
290
+ pushMessage: function(text, images) {
291
+ var content = [];
292
+ if (images && images.length > 0) {
293
+ for (var i = 0; i < images.length; i++) {
294
+ content.push({
295
+ type: "image",
296
+ source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
297
+ });
298
+ }
299
+ }
300
+ if (text) content.push({ type: "text", text: text });
301
+ messageQueue.push({ type: "user", message: { role: "user", content: content } });
302
+ },
303
+
304
+ setModel: function(model) {
305
+ if (rawQuery && typeof rawQuery.setModel === "function") {
306
+ return rawQuery.setModel(model);
307
+ }
308
+ return Promise.resolve();
309
+ },
310
+
311
+ setEffort: function(effort) {
312
+ // Claude SDK has no setEffort on active query.
313
+ // Stored at Clay level for next query.
314
+ return Promise.resolve();
315
+ },
316
+
317
+ setToolPolicy: function(policy) {
318
+ // Map YOKE policy to Claude permission mode
319
+ if (rawQuery && typeof rawQuery.setPermissionMode === "function") {
320
+ var mode = policy === "allow-all" ? "bypassPermissions" : "default";
321
+ return rawQuery.setPermissionMode(mode);
322
+ }
323
+ return Promise.resolve();
324
+ },
325
+
326
+ // Phase 3 backward compat: direct setPermissionMode with Claude-specific modes
327
+ setPermissionMode: function(mode) {
328
+ if (rawQuery && typeof rawQuery.setPermissionMode === "function") {
329
+ return rawQuery.setPermissionMode(mode);
330
+ }
331
+ return Promise.resolve();
332
+ },
333
+
334
+ stopTask: function(taskId) {
335
+ if (rawQuery && typeof rawQuery.stopTask === "function") {
336
+ return rawQuery.stopTask(taskId);
337
+ }
338
+ return Promise.resolve();
339
+ },
340
+
341
+ getContextUsage: function() {
342
+ if (rawQuery && typeof rawQuery.getContextUsage === "function") {
343
+ return rawQuery.getContextUsage();
344
+ }
345
+ return Promise.resolve(null);
346
+ },
347
+
348
+ supportedModels: function() {
349
+ if (rawQuery && typeof rawQuery.supportedModels === "function") {
350
+ return rawQuery.supportedModels();
351
+ }
352
+ return Promise.resolve([]);
353
+ },
354
+
355
+ abort: function() {
356
+ if (abortController) {
357
+ try { abortController.abort(); } catch (e) {}
358
+ }
359
+ },
360
+
361
+ close: function() {
362
+ try { messageQueue.end(); } catch (e) {}
363
+ if (rawQuery && typeof rawQuery.close === "function") {
364
+ try { rawQuery.close(); } catch (e) {}
365
+ }
366
+ },
367
+
368
+ // End the message queue without closing the raw query
369
+ endInput: function() {
370
+ try { messageQueue.end(); } catch (e) {}
371
+ },
372
+
373
+ // Claude SDK specific: rewind files to a previous state
374
+ rewindFiles: function(uuid, opts) {
375
+ if (rawQuery && typeof rawQuery.rewindFiles === "function") {
376
+ return rawQuery.rewindFiles(uuid, opts);
377
+ }
378
+ return Promise.reject(new Error("rewindFiles not supported"));
379
+ },
380
+ };
381
+
382
+ return handle;
383
+ }
384
+
385
+ // ===================================================================
386
+ // Worker process management (OS-level multi-user)
387
+ // ===================================================================
388
+
389
+ // Ensure the package directory tree is world-readable so OS-level users
390
+ // can access the worker script and its dependencies (the install path
391
+ // may be under /root/.npm/_npx/ which defaults to 700)
392
+ (function ensurePackageReadable() {
393
+ try {
394
+ // Walk up from __dirname to find the package root (where node_modules lives)
395
+ var pkgDir = path.join(__dirname, "..", "..", "..");
396
+ // Open read+execute on each ancestor directory up to and including the
397
+ // npx cache entry so that non-root users can traverse the path
398
+ var dir = pkgDir;
399
+ var dirs = [];
400
+ while (dir !== path.dirname(dir)) {
401
+ dirs.push(dir);
402
+ dir = path.dirname(dir);
403
+ }
404
+ // Open o+rx on each ancestor so non-root users can traverse the path
405
+ // (e.g. /root/.npm/_npx/.../node_modules/clay-server needs /root to be o+x)
406
+ for (var di = 0; di < dirs.length; di++) {
407
+ try {
408
+ var st = fs.statSync(dirs[di]);
409
+ // Add o+x (traverse) to all ancestors, o+rx to npm cache dirs
410
+ var isNpmDir = dirs[di].indexOf(".npm") !== -1 || dirs[di].indexOf("node_modules") !== -1;
411
+ var needed = isNpmDir ? 0o005 : 0o001; // rx for npm dirs, just x for ancestors like /root
412
+ if ((st.mode & needed) !== needed) {
413
+ fs.chmodSync(dirs[di], st.mode | needed);
414
+ }
415
+ } catch (e) {}
416
+ }
417
+ // Recursively make the package AND hoisted dependencies readable.
418
+ // npm/npx may hoist deps (e.g. @anthropic-ai/claude-agent-sdk) to the
419
+ // parent node_modules/ instead of inside clay-server/node_modules/.
420
+ var { execSync: chmodExec } = require("child_process");
421
+ // Find the top-level node_modules that contains clay-server
422
+ var topNodeModules = path.join(pkgDir, "..");
423
+ if (path.basename(topNodeModules) === "node_modules") {
424
+ chmodExec("chmod -R o+rX " + JSON.stringify(topNodeModules), { stdio: "ignore", timeout: 15000 });
425
+ } else {
426
+ chmodExec("chmod -R o+rX " + JSON.stringify(pkgDir), { stdio: "ignore", timeout: 5000 });
427
+ }
428
+ } catch (e) {}
429
+ })();
430
+
431
+ // resolveLinuxUser delegates to shared os-users utility
432
+ function resolveLinuxUser(username) {
433
+ return resolveOsUserInfo(username);
434
+ }
435
+
436
+ /**
437
+ * Spawn an SDK worker process running as the given Linux user.
438
+ * Returns a worker handle with send/kill/event methods.
439
+ */
440
+ function spawnWorker(linuxUser, workerScriptPath, cwd) {
441
+ var userInfo = resolveLinuxUser(linuxUser);
442
+ var socketId = crypto.randomUUID();
443
+ var socketPath = path.join(os.tmpdir(), "clay-worker-" + socketId + ".sock");
444
+
445
+ var worker = {
446
+ process: null,
447
+ connection: null,
448
+ socketPath: socketPath,
449
+ server: null,
450
+ messageHandlers: [],
451
+ ready: false,
452
+ readyPromise: null,
453
+ _readyResolve: null,
454
+ buffer: "",
455
+ };
456
+
457
+ worker.readyPromise = new Promise(function(resolve) {
458
+ worker._readyResolve = resolve;
459
+ });
460
+
461
+ // Resolves when the worker process actually exits.
462
+ // Used to prevent spawning a new worker before the old one finishes
463
+ // flushing SDK session state to disk (race condition on resume).
464
+ worker.exitPromise = new Promise(function(resolve) {
465
+ worker._exitResolve = resolve;
466
+ });
467
+
468
+ // Create Unix socket server
469
+ var spawnT0 = Date.now();
470
+ worker.server = net.createServer(function(connection) {
471
+ console.log("[PERF] spawnWorker: socket connection accepted +" + (Date.now() - spawnT0) + "ms");
472
+ worker.connection = connection;
473
+ connection.on("data", function(chunk) {
474
+ worker.buffer += chunk.toString();
475
+ var lines = worker.buffer.split("\n");
476
+ worker.buffer = lines.pop();
477
+ for (var i = 0; i < lines.length; i++) {
478
+ if (!lines[i].trim()) continue;
479
+ try {
480
+ var msg = JSON.parse(lines[i]);
481
+ if (msg.type === "ready") {
482
+ console.log("[PERF] spawnWorker: 'ready' IPC received +" + (Date.now() - spawnT0) + "ms");
483
+ worker.ready = true;
484
+ if (worker._readyResolve) {
485
+ worker._readyResolve();
486
+ worker._readyResolve = null;
487
+ }
488
+ }
489
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
490
+ worker.messageHandlers[h](msg);
491
+ }
492
+ } catch (e) {
493
+ console.error("[yoke/claude] Failed to parse worker message:", e.message);
494
+ }
495
+ }
496
+ });
497
+ connection.on("error", function(err) {
498
+ console.error("[yoke/claude] Worker connection error:", err.message);
499
+ });
500
+ });
501
+
502
+ worker.server.listen(socketPath, function() {
503
+ console.log("[PERF] spawnWorker: socket listen ready +" + (Date.now() - spawnT0) + "ms");
504
+ // Set socket permissions so the target user can connect
505
+ try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
506
+
507
+ // Spawn worker process as the target Linux user.
508
+ // Build a minimal, isolated env (no daemon env leakage).
509
+ var workerEnv = require("../../build-user-env").buildUserEnv({
510
+ uid: userInfo.uid,
511
+ gid: userInfo.gid,
512
+ home: userInfo.home,
513
+ user: linuxUser,
514
+ shell: userInfo.shell || "/bin/bash",
515
+ });
516
+
517
+ console.log("[yoke/claude] Spawning worker: uid=" + userInfo.uid + " gid=" + userInfo.gid + " cwd=" + cwd + " socket=" + socketPath);
518
+ console.log("[yoke/claude] Worker script: " + workerScriptPath);
519
+ console.log("[yoke/claude] Node: " + process.execPath);
520
+ worker.process = spawn(process.execPath, [workerScriptPath, socketPath], {
521
+ uid: userInfo.uid,
522
+ gid: userInfo.gid,
523
+ env: workerEnv,
524
+ cwd: cwd,
525
+ stdio: ["ignore", "pipe", "pipe"],
526
+ });
527
+
528
+ worker.process.stdout.on("data", function(data) {
529
+ console.log("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
530
+ });
531
+ worker._stderrBuf = "";
532
+ worker.process.stderr.on("data", function(data) {
533
+ var text = data.toString().trim();
534
+ worker._stderrBuf += text + "\n";
535
+ console.error("[sdk-worker:" + linuxUser + "] " + text);
536
+ });
537
+
538
+ worker.process.on("exit", function(code, signal) {
539
+ console.log("[yoke/claude] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")" + (worker._stderrBuf ? " stderr: " + worker._stderrBuf.trim() : ""));
540
+ // Reject readyPromise if worker dies before becoming ready
541
+ if (!worker.ready && worker._readyResolve) {
542
+ worker._readyResolve = null;
543
+ // Let the readyPromise hang; the query_error handler will clean up
544
+ }
545
+ // Notify message handlers about unexpected exit so sessions don't hang.
546
+ // Always dispatch a fallback query_error. The handler is idempotent:
547
+ // it checks isProcessing before taking action, and cleanupSessionWorker
548
+ // guards against stale workers. This covers all exit cases including
549
+ // signal kills (code=null) and normal exits where the IPC query_error
550
+ // was lost due to connection timing.
551
+ console.log("[yoke/claude] Exit handler: pid=" + (worker.process ? worker.process.pid : "?") + " ready=" + worker.ready + " _queryEnded=" + worker._queryEnded + " _abortSent=" + worker._abortSent + " handlers=" + worker.messageHandlers.length);
552
+ if (code === 0 && !worker.ready) {
553
+ // Worker exited cleanly before sending "ready"
554
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
555
+ worker.messageHandlers[h]({
556
+ type: "query_error",
557
+ error: "Worker exited before ready (code=0). stderr: " + (worker._stderrBuf || "(none)"),
558
+ exitCode: 0,
559
+ stderr: worker._stderrBuf || null,
560
+ });
561
+ }
562
+ } else if (code !== 0 || code === null || signal) {
563
+ // Worker crashed, was killed by signal, or exited abnormally
564
+ var stderrText = worker._stderrBuf || "";
565
+ var exitReason = signal
566
+ ? "Worker killed by " + signal
567
+ : (stderrText || "Worker exited with code " + code);
568
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
569
+ worker.messageHandlers[h]({
570
+ type: "query_error",
571
+ error: exitReason,
572
+ exitCode: code,
573
+ stderr: stderrText || null,
574
+ });
575
+ }
576
+ } else if (worker.messageHandlers.length > 0) {
577
+ // Normal exit (code=0, ready=true). Dispatch fallback in case the
578
+ // IPC query_done/query_error was lost (e.g. connection closed early).
579
+ var fallbackMsg = worker._abortSent
580
+ ? "Worker aborted"
581
+ : "Worker exited before query completed";
582
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
583
+ worker.messageHandlers[h]({
584
+ type: "query_error",
585
+ error: fallbackMsg,
586
+ exitCode: 0,
587
+ stderr: worker._stderrBuf || null,
588
+ _fallback: true,
589
+ });
590
+ }
591
+ }
592
+ cleanupWorker(worker);
593
+ if (worker._exitResolve) {
594
+ worker._exitResolve();
595
+ worker._exitResolve = null;
596
+ }
597
+ });
598
+ });
599
+
600
+ worker.send = function(msg) {
601
+ if (!worker.connection || worker.connection.destroyed) return;
602
+ try {
603
+ worker.connection.write(JSON.stringify(msg) + "\n");
604
+ } catch (e) {
605
+ console.error("[yoke/claude] Failed to send to worker:", e.message);
606
+ }
607
+ };
608
+
609
+ worker.onMessage = function(handler) {
610
+ worker.messageHandlers.push(handler);
611
+ };
612
+
613
+ worker.kill = function() {
614
+ console.log("[yoke/claude] worker.kill() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
615
+ worker.send({ type: "shutdown" });
616
+ // Force kill after 5 seconds if still alive (gives SDK time to save session)
617
+ setTimeout(function() {
618
+ if (worker.process && !worker.process.killed) {
619
+ try { worker.process.kill("SIGKILL"); } catch (e) {}
620
+ }
621
+ }, 5000);
622
+ // Don't call cleanupWorker here. Let the exit handler do it after
623
+ // the worker has had time to save SDK session state to disk.
624
+ // Closing the connection prematurely causes the worker to exit
625
+ // before the SDK can flush its session file, leading to "no
626
+ // conversation found" errors on resume (OS multi-user mode).
627
+ };
628
+
629
+ return worker;
630
+ }
631
+
632
+ function cleanupWorker(worker) {
633
+ console.log("[yoke/claude] cleanupWorker() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
634
+ if (worker._abortTimeout) { clearTimeout(worker._abortTimeout); worker._abortTimeout = null; }
635
+ if (worker.connection && !worker.connection.destroyed) {
636
+ try { worker.connection.end(); } catch (e) {}
637
+ }
638
+ if (worker.server) {
639
+ try { worker.server.close(); } catch (e) {}
640
+ }
641
+ // Remove socket file
642
+ try { fs.unlinkSync(worker.socketPath); } catch (e) {}
643
+ worker.ready = false;
644
+ }
645
+
646
+ // --- Worker QueryHandle ---
647
+ // Wraps worker IPC into the same async iterable + control interface as the
648
+ // in-process QueryHandle. This allows processQueryStream to iterate a worker
649
+ // query identically to an in-process query.
650
+
651
+ function createWorkerQueryHandle(worker, canUseTool, onElicitation) {
652
+ // Async iterable state
653
+ var iterQueue = [];
654
+ var iterWaiting = null;
655
+ var iterEnded = false;
656
+ var iterError = null;
657
+
658
+ function pushToIter(value) {
659
+ if (iterEnded) return;
660
+ if (iterWaiting) {
661
+ var resolve = iterWaiting;
662
+ iterWaiting = null;
663
+ resolve({ value: value, done: false });
664
+ } else {
665
+ iterQueue.push(value);
666
+ }
667
+ }
668
+
669
+ function endIter() {
670
+ if (iterEnded) return;
671
+ iterEnded = true;
672
+ if (iterWaiting) {
673
+ var resolve = iterWaiting;
674
+ iterWaiting = null;
675
+ resolve({ value: undefined, done: true });
676
+ }
677
+ }
678
+
679
+ function errorIter(err) {
680
+ if (iterEnded) return;
681
+ iterEnded = true;
682
+ iterError = err;
683
+ if (iterWaiting) {
684
+ var reject = iterWaiting;
685
+ iterWaiting = null;
686
+ // We stored the reject function below; for simplicity, use a combined approach
687
+ reject({ error: err });
688
+ }
689
+ }
690
+
691
+ // Set up message handler on the worker
692
+ worker.onMessage(function(msg) {
693
+ switch (msg.type) {
694
+ case "sdk_event":
695
+ pushToIter(flattenEvent(msg.event));
696
+ break;
697
+
698
+ case "permission_request":
699
+ if (canUseTool) {
700
+ canUseTool(msg.toolName, msg.input, {
701
+ toolUseID: msg.toolUseId,
702
+ decisionReason: msg.decisionReason,
703
+ signal: { addEventListener: function() {} },
704
+ }).then(function(result) {
705
+ worker.send({ type: "permission_response", requestId: msg.requestId, result: result });
706
+ }).catch(function(e) {
707
+ console.error("[yoke/claude] permission_response send failed:", e.message || e);
708
+ });
709
+ }
710
+ break;
711
+
712
+ case "ask_user_request":
713
+ if (canUseTool) {
714
+ canUseTool("AskUserQuestion", msg.input, {
715
+ toolUseID: msg.toolUseId,
716
+ signal: { addEventListener: function() {} },
717
+ }).then(function(result) {
718
+ worker.send({ type: "ask_user_response", toolUseId: msg.toolUseId, result: result });
719
+ }).catch(function(e) {
720
+ console.error("[yoke/claude] ask_user_response send failed:", e.message || e);
721
+ });
722
+ }
723
+ break;
724
+
725
+ case "elicitation_request":
726
+ if (onElicitation) {
727
+ onElicitation({
728
+ serverName: msg.serverName,
729
+ message: msg.message,
730
+ mode: msg.mode,
731
+ url: msg.url,
732
+ elicitationId: msg.elicitationId,
733
+ requestedSchema: msg.requestedSchema,
734
+ }, {
735
+ signal: { addEventListener: function() {} },
736
+ }).then(function(result) {
737
+ worker.send({ type: "elicitation_response", requestId: msg.requestId, result: result });
738
+ }).catch(function(e) {
739
+ console.error("[yoke/claude] elicitation_response send failed:", e.message || e);
740
+ });
741
+ }
742
+ break;
743
+
744
+ case "context_usage":
745
+ case "model_changed":
746
+ case "effort_changed":
747
+ case "permission_mode_changed":
748
+ case "worker_error":
749
+ // Yield these as _worker_meta events so processQueryStream can handle them
750
+ pushToIter({ type: "_worker_meta", subtype: msg.type, data: msg });
751
+ break;
752
+
753
+ case "query_done":
754
+ console.log("[yoke/claude] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
755
+ worker._queryEnded = true;
756
+ endIter();
757
+ break;
758
+
759
+ case "query_error": {
760
+ console.log("[yoke/claude] IPC query_error received, pid=" + (worker.process ? worker.process.pid : "?") + " _fallback=" + !!msg._fallback + " _queryEnded=" + worker._queryEnded + " error=" + (msg.error || "").substring(0, 100));
761
+ // Skip fallback errors from exit handler if we already handled the real one
762
+ if (msg._fallback && worker._queryEnded) break;
763
+ worker._queryEnded = true;
764
+ var err = new Error(msg.error || "Worker query error");
765
+ err.exitCode = msg.exitCode;
766
+ err.stderr = msg.stderr;
767
+ // Also store the worker stderr buffer for when msg.stderr is empty
768
+ if (!msg.stderr && worker._stderrBuf) {
769
+ err.stderr = worker._stderrBuf.trim();
770
+ }
771
+ errorIter(err);
772
+ break;
773
+ }
774
+ }
775
+ });
776
+
777
+ var handle = {
778
+ // Opaque adapter state: contains worker reference and exit promise
779
+ _adapterState: {
780
+ worker: worker,
781
+ exitPromise: worker.exitPromise,
782
+ },
783
+
784
+ [Symbol.asyncIterator]: function() {
785
+ return {
786
+ next: function() {
787
+ // Check for error first
788
+ if (iterError) {
789
+ return Promise.reject(iterError);
790
+ }
791
+ if (iterQueue.length > 0) {
792
+ var item = iterQueue.shift();
793
+ if (item && item.error && iterEnded) {
794
+ // This was an error signal
795
+ return Promise.reject(item.error);
796
+ }
797
+ return Promise.resolve({ value: item, done: false });
798
+ }
799
+ if (iterEnded) {
800
+ if (iterError) return Promise.reject(iterError);
801
+ return Promise.resolve({ value: undefined, done: true });
802
+ }
803
+ return new Promise(function(resolve, reject) {
804
+ iterWaiting = function(result) {
805
+ if (result && result.error) {
806
+ reject(result.error);
807
+ } else {
808
+ resolve(result);
809
+ }
810
+ };
811
+ });
812
+ },
813
+ };
814
+ },
815
+
816
+ pushMessage: function(text, images) {
817
+ var content = [];
818
+ if (images && images.length > 0) {
819
+ for (var i = 0; i < images.length; i++) {
820
+ content.push({
821
+ type: "image",
822
+ source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
823
+ });
824
+ }
825
+ }
826
+ if (text) content.push({ type: "text", text: text });
827
+ var userMsg = { type: "user", message: { role: "user", content: content } };
828
+ worker.send({ type: "push_message", content: userMsg });
829
+ },
830
+
831
+ setModel: function(model) {
832
+ worker.send({ type: "set_model", model: model });
833
+ return Promise.resolve();
834
+ },
835
+
836
+ setEffort: function(effort) {
837
+ worker.send({ type: "set_effort", effort: effort });
838
+ return Promise.resolve();
839
+ },
840
+
841
+ setToolPolicy: function(policy) {
842
+ var mode = policy === "allow-all" ? "bypassPermissions" : "default";
843
+ worker.send({ type: "set_permission_mode", mode: mode });
844
+ return Promise.resolve();
845
+ },
846
+
847
+ setPermissionMode: function(mode) {
848
+ worker.send({ type: "set_permission_mode", mode: mode });
849
+ return Promise.resolve();
850
+ },
851
+
852
+ stopTask: function(taskId) {
853
+ worker.send({ type: "stop_task", taskId: taskId });
854
+ return Promise.resolve();
855
+ },
856
+
857
+ getContextUsage: function() {
858
+ return Promise.resolve(null);
859
+ },
860
+
861
+ supportedModels: function() {
862
+ return Promise.resolve([]);
863
+ },
864
+
865
+ abort: function() {
866
+ console.log("[yoke/claude] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?"));
867
+ worker._abortSent = true;
868
+ try { worker.send({ type: "abort" }); } catch (e) {}
869
+ // If the worker doesn't finish within 5s (e.g. subagent stuck), force-kill it.
870
+ // The worker exit handler will dispatch a fallback query_error and send done.
871
+ if (worker._abortTimeout) clearTimeout(worker._abortTimeout);
872
+ worker._abortTimeout = setTimeout(function() {
873
+ if (worker.process && !worker.process.killed) {
874
+ console.log("[yoke/claude] Abort timeout: force-killing worker pid=" + (worker.process ? worker.process.pid : "?"));
875
+ try { worker.process.kill("SIGKILL"); } catch (e) {}
876
+ }
877
+ }, 5000);
878
+ },
879
+
880
+ close: function() {
881
+ // End the iterator
882
+ endIter();
883
+ // Send end_messages to worker
884
+ worker.send({ type: "end_messages" });
885
+ },
886
+
887
+ endInput: function() {
888
+ worker.send({ type: "end_messages" });
889
+ },
890
+ };
891
+
892
+ return handle;
893
+ }
894
+
895
+
896
+ // --- Adapter factory ---
897
+
898
+ function resolveClaudeBinaryPath() {
899
+ try {
900
+ var result = require("child_process").execSync("which claude", { encoding: "utf8", timeout: 5000 }).trim();
901
+ if (result && fs.existsSync(result)) return result;
902
+ } catch (e) {}
903
+ return null;
904
+ }
905
+
906
+ function createClaudeAdapter(opts) {
907
+ var _cwd = (opts && opts.cwd) || process.cwd();
908
+ var _cachedModels = [];
909
+ var _claudeBinaryPath = resolveClaudeBinaryPath();
910
+
911
+ // Path to the worker script (for OS-level user isolation)
912
+ var workerScriptPath = path.join(__dirname, "claude-worker.js");
913
+
914
+ var adapter = {
915
+ vendor: "claude",
916
+
917
+ // Path to worker script (sdk-bridge uses this to spawn worker processes)
918
+ workerScriptPath: workerScriptPath,
919
+
920
+ /**
921
+ * Initialize the adapter. Performs SDK warmup to discover models, skills, etc.
922
+ * If linuxUser is provided (via initOpts.linuxUser), delegates to a worker process.
923
+ *
924
+ * @param {object} initOpts
925
+ * @param {string} [initOpts.cwd]
926
+ * @param {boolean} [initOpts.dangerouslySkipPermissions]
927
+ * @param {string} [initOpts.linuxUser] - OS user for worker isolation
928
+ * @returns {Promise<{ models, defaultModel, skills, slashCommands, fastModeState, capabilities }>}
929
+ */
930
+ init: async function(initOpts) {
931
+ var linuxUser = initOpts && initOpts.linuxUser;
932
+ if (linuxUser) {
933
+ return initViaWorker(linuxUser, initOpts);
934
+ }
935
+
936
+ var sdk = await loadSDK();
937
+ var ac = new AbortController();
938
+ var mq = createMessageQueue();
939
+ mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
940
+ mq.end();
941
+
942
+ var warmupOptions = {
943
+ cwd: (initOpts && initOpts.cwd) || _cwd,
944
+ settingSources: ["user", "project", "local"],
945
+ abortController: ac,
946
+ };
947
+ if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
948
+
949
+ if (initOpts && initOpts.dangerouslySkipPermissions) {
950
+ warmupOptions.permissionMode = "bypassPermissions";
951
+ warmupOptions.allowDangerouslySkipPermissions = true;
952
+ }
953
+
954
+ var result = {
955
+ models: [],
956
+ defaultModel: "",
957
+ skills: [],
958
+ slashCommands: [],
959
+ fastModeState: null,
960
+ capabilities: {
961
+ thinking: true,
962
+ betas: true,
963
+ rewind: true,
964
+ sessionResume: true,
965
+ promptSuggestions: true,
966
+ elicitation: true,
967
+ fileCheckpointing: true,
968
+ contextCompacting: true,
969
+ toolPolicy: ["ask", "allow-all"],
970
+ },
971
+ };
972
+
973
+ try {
974
+ var stream = sdk.query({ prompt: mq, options: warmupOptions });
975
+
976
+ for await (var msg of stream) {
977
+ if (msg.type === "system" && msg.subtype === "init") {
978
+ result.skills = msg.skills || [];
979
+ result.defaultModel = msg.model || "";
980
+ result.slashCommands = msg.slash_commands || [];
981
+ result.fastModeState = msg.fast_mode_state || null;
982
+
983
+ try {
984
+ var models = await stream.supportedModels();
985
+ result.models = models || [];
986
+ _cachedModels = result.models;
987
+ } catch (e) {
988
+ // supportedModels may fail, models list will be empty
989
+ }
990
+
991
+ ac.abort();
992
+ break;
993
+ }
994
+ }
995
+ } catch (e) {
996
+ if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
997
+ throw e;
998
+ }
999
+ }
1000
+
1001
+ return result;
1002
+ },
1003
+
1004
+ /**
1005
+ * Return cached list of supported models.
1006
+ * @returns {Promise<string[]>}
1007
+ */
1008
+ supportedModels: function() {
1009
+ return Promise.resolve(_cachedModels.slice());
1010
+ },
1011
+
1012
+ /**
1013
+ * Create a tool server from runtime-agnostic definitions.
1014
+ * Synchronous because MCP servers are created during project setup.
1015
+ *
1016
+ * @param {object} def
1017
+ * @param {string} def.name
1018
+ * @param {string} def.version
1019
+ * @param {Array} def.tools - [{ name, description, inputSchema, handler }]
1020
+ * @returns {object|null} Opaque MCP server config
1021
+ */
1022
+ createToolServer: function(def) {
1023
+ var sdk = loadSDKSync();
1024
+ if (!sdk || !sdk.createSdkMcpServer || !sdk.tool) {
1025
+ console.error("[yoke/claude] SDK not available for createToolServer");
1026
+ return null;
1027
+ }
1028
+
1029
+ var sdkTools = [];
1030
+ for (var i = 0; i < def.tools.length; i++) {
1031
+ var t = def.tools[i];
1032
+ sdkTools.push(sdk.tool(t.name, t.description, t.inputSchema, t.handler));
1033
+ }
1034
+ return sdk.createSdkMcpServer({
1035
+ name: def.name,
1036
+ version: def.version,
1037
+ tools: sdkTools,
1038
+ });
1039
+ },
1040
+
1041
+ /**
1042
+ * Create a new query. Returns a QueryHandle (async iterable + control methods).
1043
+ *
1044
+ * If adapterOptions.CLAUDE.linuxUser is set, creates a worker-based query.
1045
+ * Otherwise, creates an in-process query.
1046
+ *
1047
+ * The caller must push the first message via handle.pushMessage()
1048
+ * and then iterate the handle for events.
1049
+ *
1050
+ * @param {object} queryOpts
1051
+ * @param {string} [queryOpts.cwd]
1052
+ * @param {string} [queryOpts.systemPrompt]
1053
+ * @param {string} [queryOpts.model]
1054
+ * @param {string} [queryOpts.effort]
1055
+ * @param {object} [queryOpts.toolServers] - mcpServers config object
1056
+ * @param {Function} [queryOpts.canUseTool]
1057
+ * @param {Function} [queryOpts.onElicitation]
1058
+ * @param {string} [queryOpts.resumeSessionId]
1059
+ * @param {AbortController} [queryOpts.abortController] - Phase 3: pass full controller
1060
+ * @param {object} [queryOpts.adapterOptions] - { CLAUDE: { ... } }
1061
+ * @returns {Promise<QueryHandle>}
1062
+ */
1063
+ createQuery: async function(queryOpts) {
1064
+ var co = (queryOpts.adapterOptions && queryOpts.adapterOptions.CLAUDE) || {};
1065
+ var linuxUser = co.linuxUser;
1066
+
1067
+ // Worker path: OS-level user isolation
1068
+ if (linuxUser) {
1069
+ return createWorkerQuery(queryOpts, co, linuxUser);
1070
+ }
1071
+
1072
+ // In-process path
1073
+ var sdk = await loadSDK();
1074
+ var mq = createMessageQueue();
1075
+ var ac = queryOpts.abortController || new AbortController();
1076
+
1077
+ // Build SDK-specific options
1078
+ var sdkOptions = {
1079
+ cwd: queryOpts.cwd || _cwd,
1080
+ abortController: ac,
1081
+ };
1082
+ if (_claudeBinaryPath) sdkOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1083
+
1084
+ // YOKE standard options -> SDK options
1085
+ if (queryOpts.systemPrompt) sdkOptions.systemPrompt = queryOpts.systemPrompt;
1086
+ if (queryOpts.model) sdkOptions.model = queryOpts.model;
1087
+ if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
1088
+ if (queryOpts.toolServers) sdkOptions.mcpServers = queryOpts.toolServers;
1089
+ if (queryOpts.canUseTool) sdkOptions.canUseTool = queryOpts.canUseTool;
1090
+ if (queryOpts.onElicitation) sdkOptions.onElicitation = queryOpts.onElicitation;
1091
+ if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
1092
+
1093
+ // Claude-specific options from adapterOptions.CLAUDE
1094
+ if (co.settingSources) sdkOptions.settingSources = co.settingSources;
1095
+ if (co.includePartialMessages != null) sdkOptions.includePartialMessages = co.includePartialMessages;
1096
+ if (co.enableFileCheckpointing != null) sdkOptions.enableFileCheckpointing = co.enableFileCheckpointing;
1097
+ if (co.extraArgs) sdkOptions.extraArgs = co.extraArgs;
1098
+ if (co.promptSuggestions != null) sdkOptions.promptSuggestions = co.promptSuggestions;
1099
+ if (co.agentProgressSummaries != null) sdkOptions.agentProgressSummaries = co.agentProgressSummaries;
1100
+ if (co.thinking) sdkOptions.thinking = co.thinking;
1101
+ if (co.betas && co.betas.length > 0) sdkOptions.betas = co.betas;
1102
+ if (co.permissionMode) sdkOptions.permissionMode = co.permissionMode;
1103
+ if (co.allowDangerouslySkipPermissions) sdkOptions.allowDangerouslySkipPermissions = true;
1104
+ if (co.resumeSessionAt) sdkOptions.resumeSessionAt = co.resumeSessionAt;
1105
+
1106
+ var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
1107
+ return createQueryHandle(rawQuery, mq, ac);
1108
+ },
1109
+
1110
+ // --- Title generation ---
1111
+ generateTitle: async function(messages, opts) {
1112
+ console.log("[auto-title/claude] generateTitle called with " + messages.length + " messages");
1113
+ var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
1114
+ 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";
1115
+ for (var i = 0; i < messages.length; i++) {
1116
+ prompt += "User message " + (i + 1) + ": " + messages[i] + "\n";
1117
+ }
1118
+ var ac = new AbortController();
1119
+ console.log("[auto-title/claude] Creating query with model=haiku...");
1120
+ var handle = await adapter.createQuery({
1121
+ cwd: (opts && opts.cwd) || _cwd,
1122
+ systemPrompt: systemPrompt,
1123
+ model: "haiku",
1124
+ adapterOptions: {
1125
+ CLAUDE: {
1126
+ settingSources: ["user"],
1127
+ permissionMode: "bypassPermissions",
1128
+ }
1129
+ },
1130
+ abortController: ac,
1131
+ });
1132
+ console.log("[auto-title/claude] Query created, pushing message...");
1133
+ handle.pushMessage(prompt);
1134
+ var title = "";
1135
+ var streamed = false;
1136
+ try {
1137
+ for await (var msg of handle) {
1138
+ if (msg.yokeType === "text_delta" && msg.text) {
1139
+ streamed = true;
1140
+ title += msg.text;
1141
+ } else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !streamed && msg.content) {
1142
+ // Fallback: extract text from non-streamed message content
1143
+ var content = msg.content;
1144
+ if (Array.isArray(content)) {
1145
+ for (var ci = 0; ci < content.length; ci++) {
1146
+ if (content[ci].type === "text" && content[ci].text) {
1147
+ title += content[ci].text;
1148
+ }
1149
+ }
1150
+ }
1151
+ } else if (msg.yokeType === "result") {
1152
+ break;
1153
+ }
1154
+ }
1155
+ } finally {
1156
+ handle.close();
1157
+ }
1158
+ console.log("[auto-title/claude] Generated: " + title.substring(0, 80));
1159
+ return title.replace(/[\r\n]+/g, " ").replace(/^["'\s]+|["'\s.]+$/g, "").trim();
1160
+ },
1161
+
1162
+ // --- Session management ---
1163
+ // These delegate to SDK module-level functions.
1164
+
1165
+ getSessionInfo: function(sessionId, sessionOpts) {
1166
+ return loadSDK().then(function(sdk) {
1167
+ return sdk.getSessionInfo(sessionId, sessionOpts);
1168
+ });
1169
+ },
1170
+
1171
+ listSessions: function(sessionOpts) {
1172
+ return loadSDK().then(function(sdk) {
1173
+ return sdk.listSessions(sessionOpts);
1174
+ });
1175
+ },
1176
+
1177
+ renameSession: function(sessionId, title, sessionOpts) {
1178
+ return loadSDK().then(function(sdk) {
1179
+ return sdk.renameSession(sessionId, title, sessionOpts);
1180
+ });
1181
+ },
1182
+
1183
+ forkSession: function(sessionId, sessionOpts) {
1184
+ return loadSDK().then(function(sdk) {
1185
+ return sdk.forkSession(sessionId, sessionOpts);
1186
+ });
1187
+ },
1188
+
1189
+ // --- Internal (Phase 3 transition) ---
1190
+ // These are NOT part of the YOKE interface. They exist to support
1191
+ // incremental migration and will be removed in later phases.
1192
+
1193
+ /**
1194
+ * Get the raw SDK module (async). Used by sdk-message-processor.js during transition.
1195
+ * @returns {Promise<object>}
1196
+ */
1197
+ _loadSDK: loadSDK,
1198
+ };
1199
+
1200
+ // --- Worker query creation (internal) ---
1201
+
1202
+ async function createWorkerQuery(queryOpts, claudeOpts, linuxUser) {
1203
+ var workerCwd = queryOpts.cwd || _cwd;
1204
+
1205
+ // Check for previous worker state (reuse pattern)
1206
+ var workerState = claudeOpts._workerState;
1207
+ var worker;
1208
+ var reusingWorker = false;
1209
+
1210
+ // Wait for previous worker exit if needed
1211
+ if (workerState && workerState.exitPromise && !workerState.worker) {
1212
+ await Promise.race([
1213
+ workerState.exitPromise,
1214
+ new Promise(function(resolve) { setTimeout(resolve, 3000); }),
1215
+ ]);
1216
+ }
1217
+
1218
+ // Reuse existing worker if alive
1219
+ if (workerState && workerState.worker && workerState.worker.ready &&
1220
+ workerState.worker.process && !workerState.worker.process.killed) {
1221
+ worker = workerState.worker;
1222
+ reusingWorker = true;
1223
+ // Clear old message handlers so they don't fire for the new query
1224
+ worker.messageHandlers = [];
1225
+ worker._queryEnded = false;
1226
+ worker._abortSent = false;
1227
+ } else {
1228
+ worker = spawnWorker(linuxUser, workerScriptPath, workerCwd);
1229
+ }
1230
+
1231
+ // Create the worker query handle (sets up message handler on worker)
1232
+ var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation);
1233
+
1234
+ // Wait for worker to be ready before sending query_start
1235
+ if (!reusingWorker) {
1236
+ await worker.readyPromise;
1237
+ }
1238
+
1239
+ // Build serializable query options (no callbacks, no AbortController)
1240
+ var queryOptions = {
1241
+ cwd: workerCwd,
1242
+ };
1243
+ if (claudeOpts.settingSources) queryOptions.settingSources = claudeOpts.settingSources;
1244
+ if (claudeOpts.includePartialMessages != null) queryOptions.includePartialMessages = claudeOpts.includePartialMessages;
1245
+ if (claudeOpts.enableFileCheckpointing != null) queryOptions.enableFileCheckpointing = claudeOpts.enableFileCheckpointing;
1246
+ if (claudeOpts.extraArgs) queryOptions.extraArgs = claudeOpts.extraArgs;
1247
+ if (claudeOpts.promptSuggestions != null) queryOptions.promptSuggestions = claudeOpts.promptSuggestions;
1248
+ if (claudeOpts.agentProgressSummaries != null) queryOptions.agentProgressSummaries = claudeOpts.agentProgressSummaries;
1249
+ if (claudeOpts.thinking) queryOptions.thinking = claudeOpts.thinking;
1250
+ if (claudeOpts.betas && claudeOpts.betas.length > 0) queryOptions.betas = claudeOpts.betas;
1251
+ if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
1252
+ if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
1253
+
1254
+ if (queryOpts.toolServers) queryOptions.mcpServers = queryOpts.toolServers;
1255
+ if (queryOpts.model) queryOptions.model = queryOpts.model;
1256
+ if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
1257
+ if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
1258
+ if (claudeOpts.resumeSessionAt) queryOptions.resumeSessionAt = claudeOpts.resumeSessionAt;
1259
+
1260
+ // Send query_start; the caller pushes the initial message via handle.pushMessage()
1261
+ // which routes through worker IPC.
1262
+ // NOTE: We do NOT send query_start with a prompt here. The caller (sdk-bridge)
1263
+ // will push the initial message and the worker receives it via push_message.
1264
+ // Instead, we send query_start with no prompt; the worker starts a query with
1265
+ // the message queue, and the first push_message will arrive.
1266
+ worker.send({
1267
+ type: "query_start",
1268
+ prompt: null,
1269
+ options: queryOptions,
1270
+ singleTurn: !!claudeOpts.singleTurn,
1271
+ originalHome: claudeOpts.originalHome || null,
1272
+ projectPath: claudeOpts.projectPath || null,
1273
+ _perfT0: claudeOpts._perfT0 || Date.now(),
1274
+ });
1275
+
1276
+ return handle;
1277
+ }
1278
+
1279
+ // --- Worker warmup (internal) ---
1280
+
1281
+ async function initViaWorker(linuxUser, initOpts) {
1282
+ var worker;
1283
+ try {
1284
+ worker = spawnWorker(linuxUser, workerScriptPath, (initOpts && initOpts.cwd) || _cwd);
1285
+ } catch (e) {
1286
+ throw new Error("Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e));
1287
+ }
1288
+
1289
+ var result = await new Promise(function(resolve, reject) {
1290
+ var warmupDone = false;
1291
+
1292
+ worker.onMessage(function(msg) {
1293
+ if (msg.type === "warmup_done" && !warmupDone) {
1294
+ warmupDone = true;
1295
+ var r = msg.result || {};
1296
+ resolve({
1297
+ models: r.models || [],
1298
+ defaultModel: r.model || "",
1299
+ skills: r.skills || [],
1300
+ slashCommands: r.slashCommands || [],
1301
+ fastModeState: r.fastModeState || null,
1302
+ capabilities: {
1303
+ thinking: true,
1304
+ betas: true,
1305
+ rewind: true,
1306
+ sessionResume: true,
1307
+ promptSuggestions: true,
1308
+ elicitation: true,
1309
+ fileCheckpointing: true,
1310
+ contextCompacting: true,
1311
+ toolPolicy: ["ask", "allow-all"],
1312
+ },
1313
+ });
1314
+ worker.kill();
1315
+ } else if (msg.type === "warmup_error" && !warmupDone) {
1316
+ warmupDone = true;
1317
+ worker.kill();
1318
+ reject(new Error(msg.error || "Warmup failed"));
1319
+ }
1320
+ });
1321
+
1322
+ // Handle case where worker fails to connect
1323
+ worker.readyPromise.catch(function(e) {
1324
+ if (!warmupDone) {
1325
+ warmupDone = true;
1326
+ cleanupWorker(worker);
1327
+ reject(new Error("Warmup worker failed to connect: " + (e.message || e)));
1328
+ }
1329
+ });
1330
+ });
1331
+
1332
+ // Wait for worker to be ready, then send warmup command
1333
+ // This is inside the Promise above, but we need readyPromise first
1334
+ // Actually, let's restructure: wait for ready, then send warmup
1335
+ // The Promise constructor above registers message handlers, but we need
1336
+ // to await readyPromise separately.
1337
+
1338
+ // Rethinking: the Promise above is returned directly. We need to await
1339
+ // readyPromise before sending warmup. Let me use a different approach.
1340
+
1341
+ return result;
1342
+ }
1343
+
1344
+ // Override initViaWorker to properly sequence ready + warmup
1345
+ adapter.init = async function(initOpts) {
1346
+ var linuxUser = initOpts && initOpts.linuxUser;
1347
+ if (!linuxUser) {
1348
+ // In-process warmup (original code)
1349
+ var sdk = await loadSDK();
1350
+ var ac = new AbortController();
1351
+ var mq = createMessageQueue();
1352
+ mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
1353
+ mq.end();
1354
+
1355
+ var warmupOptions = {
1356
+ cwd: (initOpts && initOpts.cwd) || _cwd,
1357
+ settingSources: ["user", "project", "local"],
1358
+ abortController: ac,
1359
+ };
1360
+ if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1361
+
1362
+ if (initOpts && initOpts.dangerouslySkipPermissions) {
1363
+ warmupOptions.permissionMode = "bypassPermissions";
1364
+ warmupOptions.allowDangerouslySkipPermissions = true;
1365
+ }
1366
+
1367
+ var result = {
1368
+ models: [],
1369
+ defaultModel: "",
1370
+ skills: [],
1371
+ slashCommands: [],
1372
+ fastModeState: null,
1373
+ capabilities: {
1374
+ thinking: true,
1375
+ betas: true,
1376
+ rewind: true,
1377
+ sessionResume: true,
1378
+ promptSuggestions: true,
1379
+ elicitation: true,
1380
+ fileCheckpointing: true,
1381
+ contextCompacting: true,
1382
+ toolPolicy: ["ask", "allow-all"],
1383
+ },
1384
+ };
1385
+
1386
+ try {
1387
+ var stream = sdk.query({ prompt: mq, options: warmupOptions });
1388
+
1389
+ for await (var msg of stream) {
1390
+ if (msg.type === "system" && msg.subtype === "init") {
1391
+ result.skills = msg.skills || [];
1392
+ result.defaultModel = msg.model || "";
1393
+ result.slashCommands = msg.slash_commands || [];
1394
+ result.fastModeState = msg.fast_mode_state || null;
1395
+
1396
+ try {
1397
+ var models = await stream.supportedModels();
1398
+ result.models = models || [];
1399
+ _cachedModels = result.models;
1400
+ } catch (e) {
1401
+ // supportedModels may fail, models list will be empty
1402
+ }
1403
+
1404
+ ac.abort();
1405
+ break;
1406
+ }
1407
+ }
1408
+ } catch (e) {
1409
+ if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
1410
+ throw e;
1411
+ }
1412
+ }
1413
+
1414
+ return result;
1415
+ }
1416
+
1417
+ // Worker-based warmup
1418
+ var worker;
1419
+ var workerCwd = (initOpts && initOpts.cwd) || _cwd;
1420
+ try {
1421
+ worker = spawnWorker(linuxUser, workerScriptPath, workerCwd);
1422
+ } catch (e) {
1423
+ throw new Error("Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e));
1424
+ }
1425
+
1426
+ try {
1427
+ await worker.readyPromise;
1428
+ } catch (e) {
1429
+ cleanupWorker(worker);
1430
+ throw new Error("Warmup worker failed to connect: " + (e.message || e));
1431
+ }
1432
+
1433
+ var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"] };
1434
+ if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1435
+ if (initOpts && initOpts.dangerouslySkipPermissions) {
1436
+ warmupOptions.permissionMode = "bypassPermissions";
1437
+ warmupOptions.allowDangerouslySkipPermissions = true;
1438
+ }
1439
+
1440
+ return new Promise(function(resolve, reject) {
1441
+ var warmupDone = false;
1442
+
1443
+ worker.onMessage(function(msg) {
1444
+ if (msg.type === "warmup_done" && !warmupDone) {
1445
+ warmupDone = true;
1446
+ var r = msg.result || {};
1447
+ resolve({
1448
+ models: r.models || [],
1449
+ defaultModel: r.model || "",
1450
+ skills: r.skills || [],
1451
+ slashCommands: r.slashCommands || [],
1452
+ fastModeState: r.fastModeState || null,
1453
+ capabilities: {
1454
+ thinking: true,
1455
+ betas: true,
1456
+ rewind: true,
1457
+ sessionResume: true,
1458
+ promptSuggestions: true,
1459
+ elicitation: true,
1460
+ fileCheckpointing: true,
1461
+ contextCompacting: true,
1462
+ toolPolicy: ["ask", "allow-all"],
1463
+ },
1464
+ });
1465
+ worker.kill();
1466
+ } else if (msg.type === "warmup_error" && !warmupDone) {
1467
+ warmupDone = true;
1468
+ worker.kill();
1469
+ reject(new Error(msg.error || "Warmup failed"));
1470
+ }
1471
+ });
1472
+
1473
+ worker.send({ type: "warmup", options: warmupOptions });
1474
+ });
1475
+ };
1476
+
1477
+ return adapter;
1478
+ }
1479
+
1480
+ module.exports = {
1481
+ createClaudeAdapter: createClaudeAdapter,
1482
+ createMessageQueue: createMessageQueue,
1483
+ };