clay-server 2.31.0 → 2.32.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser-mcp-server.js +32 -44
- package/lib/debate-mcp-server.js +14 -31
- package/lib/mcp-local.js +31 -1
- package/lib/project-connection.js +4 -2
- package/lib/project-filesystem.js +47 -1
- package/lib/project-http.js +75 -8
- package/lib/project-mcp.js +4 -0
- package/lib/project-sessions.js +88 -51
- package/lib/project-user-message.js +12 -7
- package/lib/project.js +204 -90
- package/lib/public/app.js +123 -448
- package/lib/public/codex-avatar.png +0 -0
- package/lib/public/css/debate.css +3 -2
- package/lib/public/css/filebrowser.css +91 -1
- package/lib/public/css/icon-strip.css +21 -5
- package/lib/public/css/input.css +181 -100
- package/lib/public/css/mates.css +43 -0
- package/lib/public/css/mention.css +48 -4
- package/lib/public/css/menus.css +1 -1
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/notifications-center.css +19 -0
- package/lib/public/index.html +46 -24
- package/lib/public/modules/app-connection.js +138 -37
- package/lib/public/modules/app-cursors.js +18 -17
- package/lib/public/modules/app-debate-ui.js +9 -9
- package/lib/public/modules/app-dm.js +170 -131
- package/lib/public/modules/app-favicon.js +28 -26
- package/lib/public/modules/app-header.js +79 -68
- package/lib/public/modules/app-home-hub.js +55 -47
- package/lib/public/modules/app-loop-ui.js +34 -18
- package/lib/public/modules/app-loop-wizard.js +6 -6
- package/lib/public/modules/app-messages.js +195 -152
- package/lib/public/modules/app-misc.js +23 -12
- package/lib/public/modules/app-notifications.js +97 -3
- package/lib/public/modules/app-panels.js +203 -49
- package/lib/public/modules/app-projects.js +159 -150
- package/lib/public/modules/app-rate-limit.js +5 -4
- package/lib/public/modules/app-rendering.js +149 -101
- package/lib/public/modules/app-skills-install.js +4 -4
- package/lib/public/modules/context-sources.js +12 -41
- package/lib/public/modules/dom-refs.js +21 -0
- package/lib/public/modules/filebrowser.js +173 -2
- package/lib/public/modules/input.js +86 -0
- package/lib/public/modules/mate-sidebar.js +38 -0
- package/lib/public/modules/mention.js +24 -6
- package/lib/public/modules/scheduler.js +1 -1
- package/lib/public/modules/sidebar-mates.js +66 -34
- package/lib/public/modules/sidebar-mobile.js +34 -30
- package/lib/public/modules/sidebar-projects.js +60 -57
- package/lib/public/modules/sidebar-sessions.js +75 -69
- package/lib/public/modules/sidebar.js +12 -20
- package/lib/public/modules/skills.js +8 -9
- package/lib/public/modules/sticky-notes.js +1 -2
- package/lib/public/modules/store.js +9 -2
- package/lib/public/modules/stt.js +4 -1
- package/lib/public/modules/tools.js +14 -9
- package/lib/sdk-bridge.js +511 -1113
- package/lib/sdk-message-processor.js +123 -134
- package/lib/sdk-worker.js +4 -0
- package/lib/server-dm.js +1 -0
- package/lib/server.js +86 -1
- package/lib/sessions.js +47 -36
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +559 -0
- package/lib/yoke/adapters/claude.js +1418 -0
- package/lib/yoke/adapters/codex.js +968 -0
- package/lib/yoke/adapters/gemini.js +668 -0
- package/lib/yoke/codex-app-server.js +307 -0
- package/lib/yoke/index.js +199 -0
- package/lib/yoke/instructions.js +62 -0
- package/lib/yoke/interface.js +92 -0
- package/lib/yoke/mcp-bridge-server.js +294 -0
- package/lib/yoke/package.json +7 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1418 @@
|
|
|
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 createClaudeAdapter(opts) {
|
|
899
|
+
var _cwd = (opts && opts.cwd) || process.cwd();
|
|
900
|
+
var _cachedModels = [];
|
|
901
|
+
|
|
902
|
+
// Path to the worker script (for OS-level user isolation)
|
|
903
|
+
var workerScriptPath = path.join(__dirname, "claude-worker.js");
|
|
904
|
+
|
|
905
|
+
var adapter = {
|
|
906
|
+
vendor: "claude",
|
|
907
|
+
|
|
908
|
+
// Path to worker script (sdk-bridge uses this to spawn worker processes)
|
|
909
|
+
workerScriptPath: workerScriptPath,
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Initialize the adapter. Performs SDK warmup to discover models, skills, etc.
|
|
913
|
+
* If linuxUser is provided (via initOpts.linuxUser), delegates to a worker process.
|
|
914
|
+
*
|
|
915
|
+
* @param {object} initOpts
|
|
916
|
+
* @param {string} [initOpts.cwd]
|
|
917
|
+
* @param {boolean} [initOpts.dangerouslySkipPermissions]
|
|
918
|
+
* @param {string} [initOpts.linuxUser] - OS user for worker isolation
|
|
919
|
+
* @returns {Promise<{ models, defaultModel, skills, slashCommands, fastModeState, capabilities }>}
|
|
920
|
+
*/
|
|
921
|
+
init: async function(initOpts) {
|
|
922
|
+
var linuxUser = initOpts && initOpts.linuxUser;
|
|
923
|
+
if (linuxUser) {
|
|
924
|
+
return initViaWorker(linuxUser, initOpts);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
var sdk = await loadSDK();
|
|
928
|
+
var ac = new AbortController();
|
|
929
|
+
var mq = createMessageQueue();
|
|
930
|
+
mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
|
|
931
|
+
mq.end();
|
|
932
|
+
|
|
933
|
+
var warmupOptions = {
|
|
934
|
+
cwd: (initOpts && initOpts.cwd) || _cwd,
|
|
935
|
+
settingSources: ["user", "project", "local"],
|
|
936
|
+
abortController: ac,
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
if (initOpts && initOpts.dangerouslySkipPermissions) {
|
|
940
|
+
warmupOptions.permissionMode = "bypassPermissions";
|
|
941
|
+
warmupOptions.allowDangerouslySkipPermissions = true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
var result = {
|
|
945
|
+
models: [],
|
|
946
|
+
defaultModel: "",
|
|
947
|
+
skills: [],
|
|
948
|
+
slashCommands: [],
|
|
949
|
+
fastModeState: null,
|
|
950
|
+
capabilities: {
|
|
951
|
+
thinking: true,
|
|
952
|
+
betas: true,
|
|
953
|
+
rewind: true,
|
|
954
|
+
sessionResume: true,
|
|
955
|
+
promptSuggestions: true,
|
|
956
|
+
elicitation: true,
|
|
957
|
+
fileCheckpointing: true,
|
|
958
|
+
contextCompacting: true,
|
|
959
|
+
toolPolicy: ["ask", "allow-all"],
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
var stream = sdk.query({ prompt: mq, options: warmupOptions });
|
|
965
|
+
|
|
966
|
+
for await (var msg of stream) {
|
|
967
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
968
|
+
result.skills = msg.skills || [];
|
|
969
|
+
result.defaultModel = msg.model || "";
|
|
970
|
+
result.slashCommands = msg.slash_commands || [];
|
|
971
|
+
result.fastModeState = msg.fast_mode_state || null;
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
var models = await stream.supportedModels();
|
|
975
|
+
result.models = models || [];
|
|
976
|
+
_cachedModels = result.models;
|
|
977
|
+
} catch (e) {
|
|
978
|
+
// supportedModels may fail, models list will be empty
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
ac.abort();
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} catch (e) {
|
|
986
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
987
|
+
throw e;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return result;
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Return cached list of supported models.
|
|
996
|
+
* @returns {Promise<string[]>}
|
|
997
|
+
*/
|
|
998
|
+
supportedModels: function() {
|
|
999
|
+
return Promise.resolve(_cachedModels.slice());
|
|
1000
|
+
},
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Create a tool server from runtime-agnostic definitions.
|
|
1004
|
+
* Synchronous because MCP servers are created during project setup.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {object} def
|
|
1007
|
+
* @param {string} def.name
|
|
1008
|
+
* @param {string} def.version
|
|
1009
|
+
* @param {Array} def.tools - [{ name, description, inputSchema, handler }]
|
|
1010
|
+
* @returns {object|null} Opaque MCP server config
|
|
1011
|
+
*/
|
|
1012
|
+
createToolServer: function(def) {
|
|
1013
|
+
var sdk = loadSDKSync();
|
|
1014
|
+
if (!sdk || !sdk.createSdkMcpServer || !sdk.tool) {
|
|
1015
|
+
console.error("[yoke/claude] SDK not available for createToolServer");
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
var sdkTools = [];
|
|
1020
|
+
for (var i = 0; i < def.tools.length; i++) {
|
|
1021
|
+
var t = def.tools[i];
|
|
1022
|
+
sdkTools.push(sdk.tool(t.name, t.description, t.inputSchema, t.handler));
|
|
1023
|
+
}
|
|
1024
|
+
return sdk.createSdkMcpServer({
|
|
1025
|
+
name: def.name,
|
|
1026
|
+
version: def.version,
|
|
1027
|
+
tools: sdkTools,
|
|
1028
|
+
});
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Create a new query. Returns a QueryHandle (async iterable + control methods).
|
|
1033
|
+
*
|
|
1034
|
+
* If adapterOptions.CLAUDE.linuxUser is set, creates a worker-based query.
|
|
1035
|
+
* Otherwise, creates an in-process query.
|
|
1036
|
+
*
|
|
1037
|
+
* The caller must push the first message via handle.pushMessage()
|
|
1038
|
+
* and then iterate the handle for events.
|
|
1039
|
+
*
|
|
1040
|
+
* @param {object} queryOpts
|
|
1041
|
+
* @param {string} [queryOpts.cwd]
|
|
1042
|
+
* @param {string} [queryOpts.systemPrompt]
|
|
1043
|
+
* @param {string} [queryOpts.model]
|
|
1044
|
+
* @param {string} [queryOpts.effort]
|
|
1045
|
+
* @param {object} [queryOpts.toolServers] - mcpServers config object
|
|
1046
|
+
* @param {Function} [queryOpts.canUseTool]
|
|
1047
|
+
* @param {Function} [queryOpts.onElicitation]
|
|
1048
|
+
* @param {string} [queryOpts.resumeSessionId]
|
|
1049
|
+
* @param {AbortController} [queryOpts.abortController] - Phase 3: pass full controller
|
|
1050
|
+
* @param {object} [queryOpts.adapterOptions] - { CLAUDE: { ... } }
|
|
1051
|
+
* @returns {Promise<QueryHandle>}
|
|
1052
|
+
*/
|
|
1053
|
+
createQuery: async function(queryOpts) {
|
|
1054
|
+
var co = (queryOpts.adapterOptions && queryOpts.adapterOptions.CLAUDE) || {};
|
|
1055
|
+
var linuxUser = co.linuxUser;
|
|
1056
|
+
|
|
1057
|
+
// Worker path: OS-level user isolation
|
|
1058
|
+
if (linuxUser) {
|
|
1059
|
+
return createWorkerQuery(queryOpts, co, linuxUser);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// In-process path
|
|
1063
|
+
var sdk = await loadSDK();
|
|
1064
|
+
var mq = createMessageQueue();
|
|
1065
|
+
var ac = queryOpts.abortController || new AbortController();
|
|
1066
|
+
|
|
1067
|
+
// Build SDK-specific options
|
|
1068
|
+
var sdkOptions = {
|
|
1069
|
+
cwd: queryOpts.cwd || _cwd,
|
|
1070
|
+
abortController: ac,
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// YOKE standard options -> SDK options
|
|
1074
|
+
if (queryOpts.systemPrompt) sdkOptions.systemPrompt = queryOpts.systemPrompt;
|
|
1075
|
+
if (queryOpts.model) sdkOptions.model = queryOpts.model;
|
|
1076
|
+
if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
|
|
1077
|
+
if (queryOpts.toolServers) sdkOptions.mcpServers = queryOpts.toolServers;
|
|
1078
|
+
if (queryOpts.canUseTool) sdkOptions.canUseTool = queryOpts.canUseTool;
|
|
1079
|
+
if (queryOpts.onElicitation) sdkOptions.onElicitation = queryOpts.onElicitation;
|
|
1080
|
+
if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
|
|
1081
|
+
|
|
1082
|
+
// Claude-specific options from adapterOptions.CLAUDE
|
|
1083
|
+
if (co.settingSources) sdkOptions.settingSources = co.settingSources;
|
|
1084
|
+
if (co.includePartialMessages != null) sdkOptions.includePartialMessages = co.includePartialMessages;
|
|
1085
|
+
if (co.enableFileCheckpointing != null) sdkOptions.enableFileCheckpointing = co.enableFileCheckpointing;
|
|
1086
|
+
if (co.extraArgs) sdkOptions.extraArgs = co.extraArgs;
|
|
1087
|
+
if (co.promptSuggestions != null) sdkOptions.promptSuggestions = co.promptSuggestions;
|
|
1088
|
+
if (co.agentProgressSummaries != null) sdkOptions.agentProgressSummaries = co.agentProgressSummaries;
|
|
1089
|
+
if (co.thinking) sdkOptions.thinking = co.thinking;
|
|
1090
|
+
if (co.betas && co.betas.length > 0) sdkOptions.betas = co.betas;
|
|
1091
|
+
if (co.permissionMode) sdkOptions.permissionMode = co.permissionMode;
|
|
1092
|
+
if (co.allowDangerouslySkipPermissions) sdkOptions.allowDangerouslySkipPermissions = true;
|
|
1093
|
+
if (co.resumeSessionAt) sdkOptions.resumeSessionAt = co.resumeSessionAt;
|
|
1094
|
+
|
|
1095
|
+
var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
|
|
1096
|
+
return createQueryHandle(rawQuery, mq, ac);
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
// --- Session management ---
|
|
1100
|
+
// These delegate to SDK module-level functions.
|
|
1101
|
+
|
|
1102
|
+
getSessionInfo: function(sessionId, sessionOpts) {
|
|
1103
|
+
return loadSDK().then(function(sdk) {
|
|
1104
|
+
return sdk.getSessionInfo(sessionId, sessionOpts);
|
|
1105
|
+
});
|
|
1106
|
+
},
|
|
1107
|
+
|
|
1108
|
+
listSessions: function(sessionOpts) {
|
|
1109
|
+
return loadSDK().then(function(sdk) {
|
|
1110
|
+
return sdk.listSessions(sessionOpts);
|
|
1111
|
+
});
|
|
1112
|
+
},
|
|
1113
|
+
|
|
1114
|
+
renameSession: function(sessionId, title, sessionOpts) {
|
|
1115
|
+
return loadSDK().then(function(sdk) {
|
|
1116
|
+
return sdk.renameSession(sessionId, title, sessionOpts);
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
forkSession: function(sessionId, sessionOpts) {
|
|
1121
|
+
return loadSDK().then(function(sdk) {
|
|
1122
|
+
return sdk.forkSession(sessionId, sessionOpts);
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
// --- Internal (Phase 3 transition) ---
|
|
1127
|
+
// These are NOT part of the YOKE interface. They exist to support
|
|
1128
|
+
// incremental migration and will be removed in later phases.
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Get the raw SDK module (async). Used by sdk-message-processor.js during transition.
|
|
1132
|
+
* @returns {Promise<object>}
|
|
1133
|
+
*/
|
|
1134
|
+
_loadSDK: loadSDK,
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// --- Worker query creation (internal) ---
|
|
1138
|
+
|
|
1139
|
+
async function createWorkerQuery(queryOpts, claudeOpts, linuxUser) {
|
|
1140
|
+
var workerCwd = queryOpts.cwd || _cwd;
|
|
1141
|
+
|
|
1142
|
+
// Check for previous worker state (reuse pattern)
|
|
1143
|
+
var workerState = claudeOpts._workerState;
|
|
1144
|
+
var worker;
|
|
1145
|
+
var reusingWorker = false;
|
|
1146
|
+
|
|
1147
|
+
// Wait for previous worker exit if needed
|
|
1148
|
+
if (workerState && workerState.exitPromise && !workerState.worker) {
|
|
1149
|
+
await Promise.race([
|
|
1150
|
+
workerState.exitPromise,
|
|
1151
|
+
new Promise(function(resolve) { setTimeout(resolve, 3000); }),
|
|
1152
|
+
]);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Reuse existing worker if alive
|
|
1156
|
+
if (workerState && workerState.worker && workerState.worker.ready &&
|
|
1157
|
+
workerState.worker.process && !workerState.worker.process.killed) {
|
|
1158
|
+
worker = workerState.worker;
|
|
1159
|
+
reusingWorker = true;
|
|
1160
|
+
// Clear old message handlers so they don't fire for the new query
|
|
1161
|
+
worker.messageHandlers = [];
|
|
1162
|
+
worker._queryEnded = false;
|
|
1163
|
+
worker._abortSent = false;
|
|
1164
|
+
} else {
|
|
1165
|
+
worker = spawnWorker(linuxUser, workerScriptPath, workerCwd);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Create the worker query handle (sets up message handler on worker)
|
|
1169
|
+
var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation);
|
|
1170
|
+
|
|
1171
|
+
// Wait for worker to be ready before sending query_start
|
|
1172
|
+
if (!reusingWorker) {
|
|
1173
|
+
await worker.readyPromise;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Build serializable query options (no callbacks, no AbortController)
|
|
1177
|
+
var queryOptions = {
|
|
1178
|
+
cwd: workerCwd,
|
|
1179
|
+
};
|
|
1180
|
+
if (claudeOpts.settingSources) queryOptions.settingSources = claudeOpts.settingSources;
|
|
1181
|
+
if (claudeOpts.includePartialMessages != null) queryOptions.includePartialMessages = claudeOpts.includePartialMessages;
|
|
1182
|
+
if (claudeOpts.enableFileCheckpointing != null) queryOptions.enableFileCheckpointing = claudeOpts.enableFileCheckpointing;
|
|
1183
|
+
if (claudeOpts.extraArgs) queryOptions.extraArgs = claudeOpts.extraArgs;
|
|
1184
|
+
if (claudeOpts.promptSuggestions != null) queryOptions.promptSuggestions = claudeOpts.promptSuggestions;
|
|
1185
|
+
if (claudeOpts.agentProgressSummaries != null) queryOptions.agentProgressSummaries = claudeOpts.agentProgressSummaries;
|
|
1186
|
+
if (claudeOpts.thinking) queryOptions.thinking = claudeOpts.thinking;
|
|
1187
|
+
if (claudeOpts.betas && claudeOpts.betas.length > 0) queryOptions.betas = claudeOpts.betas;
|
|
1188
|
+
if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
|
|
1189
|
+
if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
|
|
1190
|
+
|
|
1191
|
+
if (queryOpts.toolServers) queryOptions.mcpServers = queryOpts.toolServers;
|
|
1192
|
+
if (queryOpts.model) queryOptions.model = queryOpts.model;
|
|
1193
|
+
if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
|
|
1194
|
+
if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
|
|
1195
|
+
if (claudeOpts.resumeSessionAt) queryOptions.resumeSessionAt = claudeOpts.resumeSessionAt;
|
|
1196
|
+
|
|
1197
|
+
// Send query_start; the caller pushes the initial message via handle.pushMessage()
|
|
1198
|
+
// which routes through worker IPC.
|
|
1199
|
+
// NOTE: We do NOT send query_start with a prompt here. The caller (sdk-bridge)
|
|
1200
|
+
// will push the initial message and the worker receives it via push_message.
|
|
1201
|
+
// Instead, we send query_start with no prompt; the worker starts a query with
|
|
1202
|
+
// the message queue, and the first push_message will arrive.
|
|
1203
|
+
worker.send({
|
|
1204
|
+
type: "query_start",
|
|
1205
|
+
prompt: null,
|
|
1206
|
+
options: queryOptions,
|
|
1207
|
+
singleTurn: !!claudeOpts.singleTurn,
|
|
1208
|
+
originalHome: claudeOpts.originalHome || null,
|
|
1209
|
+
projectPath: claudeOpts.projectPath || null,
|
|
1210
|
+
_perfT0: claudeOpts._perfT0 || Date.now(),
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
return handle;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// --- Worker warmup (internal) ---
|
|
1217
|
+
|
|
1218
|
+
async function initViaWorker(linuxUser, initOpts) {
|
|
1219
|
+
var worker;
|
|
1220
|
+
try {
|
|
1221
|
+
worker = spawnWorker(linuxUser, workerScriptPath, (initOpts && initOpts.cwd) || _cwd);
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
throw new Error("Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
var result = await new Promise(function(resolve, reject) {
|
|
1227
|
+
var warmupDone = false;
|
|
1228
|
+
|
|
1229
|
+
worker.onMessage(function(msg) {
|
|
1230
|
+
if (msg.type === "warmup_done" && !warmupDone) {
|
|
1231
|
+
warmupDone = true;
|
|
1232
|
+
var r = msg.result || {};
|
|
1233
|
+
resolve({
|
|
1234
|
+
models: r.models || [],
|
|
1235
|
+
defaultModel: r.model || "",
|
|
1236
|
+
skills: r.skills || [],
|
|
1237
|
+
slashCommands: r.slashCommands || [],
|
|
1238
|
+
fastModeState: r.fastModeState || null,
|
|
1239
|
+
capabilities: {
|
|
1240
|
+
thinking: true,
|
|
1241
|
+
betas: true,
|
|
1242
|
+
rewind: true,
|
|
1243
|
+
sessionResume: true,
|
|
1244
|
+
promptSuggestions: true,
|
|
1245
|
+
elicitation: true,
|
|
1246
|
+
fileCheckpointing: true,
|
|
1247
|
+
contextCompacting: true,
|
|
1248
|
+
toolPolicy: ["ask", "allow-all"],
|
|
1249
|
+
},
|
|
1250
|
+
});
|
|
1251
|
+
worker.kill();
|
|
1252
|
+
} else if (msg.type === "warmup_error" && !warmupDone) {
|
|
1253
|
+
warmupDone = true;
|
|
1254
|
+
worker.kill();
|
|
1255
|
+
reject(new Error(msg.error || "Warmup failed"));
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// Handle case where worker fails to connect
|
|
1260
|
+
worker.readyPromise.catch(function(e) {
|
|
1261
|
+
if (!warmupDone) {
|
|
1262
|
+
warmupDone = true;
|
|
1263
|
+
cleanupWorker(worker);
|
|
1264
|
+
reject(new Error("Warmup worker failed to connect: " + (e.message || e)));
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// Wait for worker to be ready, then send warmup command
|
|
1270
|
+
// This is inside the Promise above, but we need readyPromise first
|
|
1271
|
+
// Actually, let's restructure: wait for ready, then send warmup
|
|
1272
|
+
// The Promise constructor above registers message handlers, but we need
|
|
1273
|
+
// to await readyPromise separately.
|
|
1274
|
+
|
|
1275
|
+
// Rethinking: the Promise above is returned directly. We need to await
|
|
1276
|
+
// readyPromise before sending warmup. Let me use a different approach.
|
|
1277
|
+
|
|
1278
|
+
return result;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Override initViaWorker to properly sequence ready + warmup
|
|
1282
|
+
adapter.init = async function(initOpts) {
|
|
1283
|
+
var linuxUser = initOpts && initOpts.linuxUser;
|
|
1284
|
+
if (!linuxUser) {
|
|
1285
|
+
// In-process warmup (original code)
|
|
1286
|
+
var sdk = await loadSDK();
|
|
1287
|
+
var ac = new AbortController();
|
|
1288
|
+
var mq = createMessageQueue();
|
|
1289
|
+
mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
|
|
1290
|
+
mq.end();
|
|
1291
|
+
|
|
1292
|
+
var warmupOptions = {
|
|
1293
|
+
cwd: (initOpts && initOpts.cwd) || _cwd,
|
|
1294
|
+
settingSources: ["user", "project", "local"],
|
|
1295
|
+
abortController: ac,
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
if (initOpts && initOpts.dangerouslySkipPermissions) {
|
|
1299
|
+
warmupOptions.permissionMode = "bypassPermissions";
|
|
1300
|
+
warmupOptions.allowDangerouslySkipPermissions = true;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
var result = {
|
|
1304
|
+
models: [],
|
|
1305
|
+
defaultModel: "",
|
|
1306
|
+
skills: [],
|
|
1307
|
+
slashCommands: [],
|
|
1308
|
+
fastModeState: null,
|
|
1309
|
+
capabilities: {
|
|
1310
|
+
thinking: true,
|
|
1311
|
+
betas: true,
|
|
1312
|
+
rewind: true,
|
|
1313
|
+
sessionResume: true,
|
|
1314
|
+
promptSuggestions: true,
|
|
1315
|
+
elicitation: true,
|
|
1316
|
+
fileCheckpointing: true,
|
|
1317
|
+
contextCompacting: true,
|
|
1318
|
+
toolPolicy: ["ask", "allow-all"],
|
|
1319
|
+
},
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
var stream = sdk.query({ prompt: mq, options: warmupOptions });
|
|
1324
|
+
|
|
1325
|
+
for await (var msg of stream) {
|
|
1326
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
1327
|
+
result.skills = msg.skills || [];
|
|
1328
|
+
result.defaultModel = msg.model || "";
|
|
1329
|
+
result.slashCommands = msg.slash_commands || [];
|
|
1330
|
+
result.fastModeState = msg.fast_mode_state || null;
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
var models = await stream.supportedModels();
|
|
1334
|
+
result.models = models || [];
|
|
1335
|
+
_cachedModels = result.models;
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
// supportedModels may fail, models list will be empty
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
ac.abort();
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} catch (e) {
|
|
1345
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
1346
|
+
throw e;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return result;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Worker-based warmup
|
|
1354
|
+
var worker;
|
|
1355
|
+
var workerCwd = (initOpts && initOpts.cwd) || _cwd;
|
|
1356
|
+
try {
|
|
1357
|
+
worker = spawnWorker(linuxUser, workerScriptPath, workerCwd);
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
throw new Error("Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e));
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
try {
|
|
1363
|
+
await worker.readyPromise;
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
cleanupWorker(worker);
|
|
1366
|
+
throw new Error("Warmup worker failed to connect: " + (e.message || e));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"] };
|
|
1370
|
+
if (initOpts && initOpts.dangerouslySkipPermissions) {
|
|
1371
|
+
warmupOptions.permissionMode = "bypassPermissions";
|
|
1372
|
+
warmupOptions.allowDangerouslySkipPermissions = true;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
return new Promise(function(resolve, reject) {
|
|
1376
|
+
var warmupDone = false;
|
|
1377
|
+
|
|
1378
|
+
worker.onMessage(function(msg) {
|
|
1379
|
+
if (msg.type === "warmup_done" && !warmupDone) {
|
|
1380
|
+
warmupDone = true;
|
|
1381
|
+
var r = msg.result || {};
|
|
1382
|
+
resolve({
|
|
1383
|
+
models: r.models || [],
|
|
1384
|
+
defaultModel: r.model || "",
|
|
1385
|
+
skills: r.skills || [],
|
|
1386
|
+
slashCommands: r.slashCommands || [],
|
|
1387
|
+
fastModeState: r.fastModeState || null,
|
|
1388
|
+
capabilities: {
|
|
1389
|
+
thinking: true,
|
|
1390
|
+
betas: true,
|
|
1391
|
+
rewind: true,
|
|
1392
|
+
sessionResume: true,
|
|
1393
|
+
promptSuggestions: true,
|
|
1394
|
+
elicitation: true,
|
|
1395
|
+
fileCheckpointing: true,
|
|
1396
|
+
contextCompacting: true,
|
|
1397
|
+
toolPolicy: ["ask", "allow-all"],
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
worker.kill();
|
|
1401
|
+
} else if (msg.type === "warmup_error" && !warmupDone) {
|
|
1402
|
+
warmupDone = true;
|
|
1403
|
+
worker.kill();
|
|
1404
|
+
reject(new Error(msg.error || "Warmup failed"));
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
worker.send({ type: "warmup", options: warmupOptions });
|
|
1409
|
+
});
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
return adapter;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
module.exports = {
|
|
1416
|
+
createClaudeAdapter: createClaudeAdapter,
|
|
1417
|
+
createMessageQueue: createMessageQueue,
|
|
1418
|
+
};
|