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.
- package/lib/browser-mcp-server.js +32 -44
- package/lib/codex-defaults.js +18 -0
- package/lib/debate-mcp-server.js +14 -31
- package/lib/mcp-local.js +31 -1
- package/lib/project-connection.js +9 -6
- package/lib/project-debate.js +8 -0
- package/lib/project-filesystem.js +47 -1
- package/lib/project-http.js +75 -8
- package/lib/project-mate-interaction.js +102 -16
- package/lib/project-mcp.js +4 -0
- package/lib/project-notifications.js +9 -0
- package/lib/project-sessions.js +94 -51
- package/lib/project-user-message.js +12 -7
- package/lib/project.js +234 -99
- package/lib/public/app.js +135 -454
- 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 +338 -104
- 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 +26 -0
- package/lib/public/css/tooltip.css +47 -0
- package/lib/public/index.html +78 -26
- 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 +175 -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 +199 -153
- package/lib/public/modules/app-misc.js +23 -12
- package/lib/public/modules/app-notifications.js +119 -9
- package/lib/public/modules/app-panels.js +203 -49
- package/lib/public/modules/app-projects.js +161 -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 +102 -66
- package/lib/public/modules/dom-refs.js +21 -0
- package/lib/public/modules/filebrowser.js +173 -2
- package/lib/public/modules/input.js +122 -0
- package/lib/public/modules/markdown.js +5 -1
- 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 +79 -35
- 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/terminal.js +12 -0
- package/lib/public/modules/tools.js +18 -13
- package/lib/public/modules/tooltip.js +32 -5
- package/lib/sdk-bridge.js +562 -1114
- package/lib/sdk-message-processor.js +150 -135
- package/lib/sdk-worker.js +4 -0
- package/lib/server-dm.js +1 -0
- package/lib/server.js +86 -1
- package/lib/sessions.js +81 -37
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +559 -0
- package/lib/yoke/adapters/claude.js +1483 -0
- package/lib/yoke/adapters/codex.js +1121 -0
- package/lib/yoke/adapters/gemini.js +709 -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 +98 -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,709 @@
|
|
|
1
|
+
// YOKE Gemini Adapter
|
|
2
|
+
// --------------------
|
|
3
|
+
// Implements the YOKE interface using @google/genai SDK.
|
|
4
|
+
// This is the ONLY file that imports the Gemini SDK.
|
|
5
|
+
|
|
6
|
+
// --- SDK loading ---
|
|
7
|
+
var _sdkPromise = null;
|
|
8
|
+
function loadSDK() {
|
|
9
|
+
if (!_sdkPromise) _sdkPromise = import("@google/genai");
|
|
10
|
+
return _sdkPromise;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// --- Event flattening ---
|
|
14
|
+
// Converts Gemini streaming chunks into flat objects with a yokeType field.
|
|
15
|
+
// Gemini chunks contain candidates[0].content.parts[] with text, thinking, or functionCall.
|
|
16
|
+
|
|
17
|
+
function flattenChunk(chunk, state) {
|
|
18
|
+
var events = [];
|
|
19
|
+
if (!chunk || !chunk.candidates || !chunk.candidates[0]) {
|
|
20
|
+
return events;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var candidate = chunk.candidates[0];
|
|
24
|
+
var parts = (candidate.content && candidate.content.parts) || [];
|
|
25
|
+
|
|
26
|
+
// First chunk -> turn_start
|
|
27
|
+
if (!state.turnStarted) {
|
|
28
|
+
state.turnStarted = true;
|
|
29
|
+
events.push({ yokeType: "turn_start" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (var i = 0; i < parts.length; i++) {
|
|
33
|
+
var part = parts[i];
|
|
34
|
+
|
|
35
|
+
// Thinking text
|
|
36
|
+
if (part.thought && part.text) {
|
|
37
|
+
if (!state.thinkingStarted) {
|
|
38
|
+
state.thinkingStarted = true;
|
|
39
|
+
state.blockCounter++;
|
|
40
|
+
state.thinkingBlockId = "blk_" + state.blockCounter;
|
|
41
|
+
events.push({ yokeType: "thinking_start", blockId: state.thinkingBlockId });
|
|
42
|
+
}
|
|
43
|
+
events.push({
|
|
44
|
+
yokeType: "thinking_delta",
|
|
45
|
+
blockId: state.thinkingBlockId,
|
|
46
|
+
text: part.text,
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Regular text
|
|
52
|
+
if (part.text && !part.thought) {
|
|
53
|
+
// If thinking was active, close it
|
|
54
|
+
if (state.thinkingStarted) {
|
|
55
|
+
events.push({ yokeType: "thinking_stop", blockId: state.thinkingBlockId });
|
|
56
|
+
state.thinkingStarted = false;
|
|
57
|
+
}
|
|
58
|
+
if (!state.textStarted) {
|
|
59
|
+
state.textStarted = true;
|
|
60
|
+
state.blockCounter++;
|
|
61
|
+
state.textBlockId = "blk_" + state.blockCounter;
|
|
62
|
+
events.push({ yokeType: "text_start", blockId: state.textBlockId });
|
|
63
|
+
}
|
|
64
|
+
events.push({
|
|
65
|
+
yokeType: "text_delta",
|
|
66
|
+
blockId: state.textBlockId,
|
|
67
|
+
text: part.text,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Function call
|
|
73
|
+
if (part.functionCall) {
|
|
74
|
+
// Close thinking/text if active
|
|
75
|
+
if (state.thinkingStarted) {
|
|
76
|
+
events.push({ yokeType: "thinking_stop", blockId: state.thinkingBlockId });
|
|
77
|
+
state.thinkingStarted = false;
|
|
78
|
+
}
|
|
79
|
+
state.blockCounter++;
|
|
80
|
+
var toolBlockId = "blk_" + state.blockCounter;
|
|
81
|
+
var toolId = "tool_" + state.toolCounter++;
|
|
82
|
+
events.push({
|
|
83
|
+
yokeType: "tool_start",
|
|
84
|
+
blockId: toolBlockId,
|
|
85
|
+
toolId: toolId,
|
|
86
|
+
toolName: part.functionCall.name,
|
|
87
|
+
});
|
|
88
|
+
events.push({
|
|
89
|
+
yokeType: "tool_executing",
|
|
90
|
+
blockId: toolBlockId,
|
|
91
|
+
toolId: toolId,
|
|
92
|
+
toolName: part.functionCall.name,
|
|
93
|
+
input: part.functionCall.args || {},
|
|
94
|
+
});
|
|
95
|
+
// Store for tool loop
|
|
96
|
+
state.pendingToolCalls.push({
|
|
97
|
+
toolId: toolId,
|
|
98
|
+
blockId: toolBlockId,
|
|
99
|
+
name: part.functionCall.name,
|
|
100
|
+
args: part.functionCall.args || {},
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Usage metadata
|
|
107
|
+
if (chunk.usageMetadata) {
|
|
108
|
+
state.lastUsage = {
|
|
109
|
+
promptTokenCount: chunk.usageMetadata.promptTokenCount || 0,
|
|
110
|
+
candidatesTokenCount: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
111
|
+
totalTokenCount: chunk.usageMetadata.totalTokenCount || 0,
|
|
112
|
+
thoughtsTokenCount: chunk.usageMetadata.thoughtsTokenCount || 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Finish reason
|
|
117
|
+
if (candidate.finishReason) {
|
|
118
|
+
state.finishReason = candidate.finishReason;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return events;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- QueryHandle ---
|
|
125
|
+
|
|
126
|
+
function createGeminiQueryHandle(chat, model, toolHandlers, canUseTool, abortController, adapterOptions) {
|
|
127
|
+
var state = {
|
|
128
|
+
blockCounter: 0,
|
|
129
|
+
toolCounter: 0,
|
|
130
|
+
turnStarted: false,
|
|
131
|
+
thinkingStarted: false,
|
|
132
|
+
thinkingBlockId: null,
|
|
133
|
+
textStarted: false,
|
|
134
|
+
textBlockId: null,
|
|
135
|
+
pendingToolCalls: [],
|
|
136
|
+
lastUsage: null,
|
|
137
|
+
finishReason: null,
|
|
138
|
+
done: false,
|
|
139
|
+
aborted: false,
|
|
140
|
+
loopStarted: false,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Internal event buffer for the async iterator
|
|
144
|
+
var eventBuffer = [];
|
|
145
|
+
var eventWaiting = null;
|
|
146
|
+
var iteratorDone = false;
|
|
147
|
+
|
|
148
|
+
function pushEvent(evt) {
|
|
149
|
+
if (iteratorDone) return;
|
|
150
|
+
if (eventWaiting) {
|
|
151
|
+
var resolve = eventWaiting;
|
|
152
|
+
eventWaiting = null;
|
|
153
|
+
resolve({ value: evt, done: false });
|
|
154
|
+
} else {
|
|
155
|
+
eventBuffer.push(evt);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function endIterator() {
|
|
160
|
+
iteratorDone = true;
|
|
161
|
+
if (eventWaiting) {
|
|
162
|
+
var resolve = eventWaiting;
|
|
163
|
+
eventWaiting = null;
|
|
164
|
+
resolve({ value: undefined, done: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Message queue for multi-turn
|
|
169
|
+
var messageQueue = [];
|
|
170
|
+
var messageWaiting = null;
|
|
171
|
+
var messageQueueEnded = false;
|
|
172
|
+
|
|
173
|
+
function pushMessageToQueue(msg) {
|
|
174
|
+
if (messageQueueEnded) return;
|
|
175
|
+
if (messageWaiting) {
|
|
176
|
+
var resolve = messageWaiting;
|
|
177
|
+
messageWaiting = null;
|
|
178
|
+
resolve(msg);
|
|
179
|
+
} else {
|
|
180
|
+
messageQueue.push(msg);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function waitForMessage() {
|
|
185
|
+
if (messageQueue.length > 0) return Promise.resolve(messageQueue.shift());
|
|
186
|
+
if (messageQueueEnded) return Promise.resolve(null);
|
|
187
|
+
return new Promise(function(resolve) { messageWaiting = resolve; });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Tool execution with canUseTool ---
|
|
191
|
+
async function executeTools(pendingCalls) {
|
|
192
|
+
var responses = [];
|
|
193
|
+
for (var i = 0; i < pendingCalls.length; i++) {
|
|
194
|
+
var tc = pendingCalls[i];
|
|
195
|
+
|
|
196
|
+
// Check permission via canUseTool callback
|
|
197
|
+
if (canUseTool) {
|
|
198
|
+
var approval = await canUseTool(tc.name, tc.args, {});
|
|
199
|
+
if (approval && approval.behavior === "deny") {
|
|
200
|
+
pushEvent({
|
|
201
|
+
yokeType: "tool_result",
|
|
202
|
+
toolId: tc.toolId,
|
|
203
|
+
blockId: tc.blockId,
|
|
204
|
+
content: approval.message || "Tool use denied",
|
|
205
|
+
isError: true,
|
|
206
|
+
});
|
|
207
|
+
responses.push({
|
|
208
|
+
functionResponse: {
|
|
209
|
+
name: tc.name,
|
|
210
|
+
response: { error: approval.message || "Tool use denied" },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Execute tool handler
|
|
218
|
+
var handler = toolHandlers[tc.name];
|
|
219
|
+
if (!handler) {
|
|
220
|
+
pushEvent({
|
|
221
|
+
yokeType: "tool_result",
|
|
222
|
+
toolId: tc.toolId,
|
|
223
|
+
blockId: tc.blockId,
|
|
224
|
+
content: "Unknown tool: " + tc.name,
|
|
225
|
+
isError: true,
|
|
226
|
+
});
|
|
227
|
+
responses.push({
|
|
228
|
+
functionResponse: {
|
|
229
|
+
name: tc.name,
|
|
230
|
+
response: { error: "Unknown tool: " + tc.name },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
var result = await handler(tc.args);
|
|
238
|
+
var resultText = typeof result === "string" ? result : JSON.stringify(result);
|
|
239
|
+
pushEvent({
|
|
240
|
+
yokeType: "tool_result",
|
|
241
|
+
toolId: tc.toolId,
|
|
242
|
+
blockId: tc.blockId,
|
|
243
|
+
content: resultText,
|
|
244
|
+
isError: false,
|
|
245
|
+
});
|
|
246
|
+
responses.push({
|
|
247
|
+
functionResponse: {
|
|
248
|
+
name: tc.name,
|
|
249
|
+
response: { result: result },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
} catch (e) {
|
|
253
|
+
pushEvent({
|
|
254
|
+
yokeType: "tool_result",
|
|
255
|
+
toolId: tc.toolId,
|
|
256
|
+
blockId: tc.blockId,
|
|
257
|
+
content: e.message || String(e),
|
|
258
|
+
isError: true,
|
|
259
|
+
});
|
|
260
|
+
responses.push({
|
|
261
|
+
functionResponse: {
|
|
262
|
+
name: tc.name,
|
|
263
|
+
response: { error: e.message || String(e) },
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return responses;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Main query loop (runs in background) ---
|
|
272
|
+
async function runQueryLoop(initialMessage) {
|
|
273
|
+
var currentMessage = initialMessage;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
while (!state.aborted) {
|
|
277
|
+
// Reset per-turn state
|
|
278
|
+
state.turnStarted = false;
|
|
279
|
+
state.thinkingStarted = false;
|
|
280
|
+
state.textStarted = false;
|
|
281
|
+
state.pendingToolCalls = [];
|
|
282
|
+
state.finishReason = null;
|
|
283
|
+
|
|
284
|
+
// Send message and stream response
|
|
285
|
+
var streamConfig = {};
|
|
286
|
+
if (adapterOptions && adapterOptions.GEMINI) {
|
|
287
|
+
var go = adapterOptions.GEMINI;
|
|
288
|
+
if (go.temperature != null) streamConfig.temperature = go.temperature;
|
|
289
|
+
if (go.maxOutputTokens != null) streamConfig.maxOutputTokens = go.maxOutputTokens;
|
|
290
|
+
if (go.topP != null) streamConfig.topP = go.topP;
|
|
291
|
+
if (go.topK != null) streamConfig.topK = go.topK;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var sendParams = { message: currentMessage };
|
|
295
|
+
if (Object.keys(streamConfig).length > 0) sendParams.config = streamConfig;
|
|
296
|
+
|
|
297
|
+
var response = await chat.sendMessageStream(sendParams);
|
|
298
|
+
|
|
299
|
+
for await (var chunk of response) {
|
|
300
|
+
if (state.aborted) break;
|
|
301
|
+
var events = flattenChunk(chunk, state);
|
|
302
|
+
for (var i = 0; i < events.length; i++) {
|
|
303
|
+
pushEvent(events[i]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (state.aborted) break;
|
|
308
|
+
|
|
309
|
+
// Close open blocks
|
|
310
|
+
if (state.thinkingStarted) {
|
|
311
|
+
pushEvent({ yokeType: "thinking_stop", blockId: state.thinkingBlockId });
|
|
312
|
+
state.thinkingStarted = false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Tool calls pending?
|
|
316
|
+
if (state.pendingToolCalls.length > 0) {
|
|
317
|
+
var toolResponses = await executeTools(state.pendingToolCalls);
|
|
318
|
+
if (state.aborted) break;
|
|
319
|
+
// Send tool results back as next message
|
|
320
|
+
currentMessage = toolResponses;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// No tool calls -> this turn is done
|
|
325
|
+
pushEvent({
|
|
326
|
+
yokeType: "result",
|
|
327
|
+
cost: null,
|
|
328
|
+
duration: null,
|
|
329
|
+
usage: state.lastUsage,
|
|
330
|
+
sessionId: null,
|
|
331
|
+
finishReason: state.finishReason,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Wait for next message (multi-turn)
|
|
335
|
+
var nextMsg = await waitForMessage();
|
|
336
|
+
if (nextMsg === null) {
|
|
337
|
+
// Message queue ended
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
currentMessage = nextMsg;
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
if (!state.aborted) {
|
|
344
|
+
console.error("[yoke/gemini] runQueryLoop error:", e.message || e);
|
|
345
|
+
console.error("[yoke/gemini] stack:", e.stack || "(no stack)");
|
|
346
|
+
pushEvent({
|
|
347
|
+
yokeType: "error",
|
|
348
|
+
text: e.message || String(e),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
state.done = true;
|
|
354
|
+
endIterator();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var handle = {
|
|
358
|
+
[Symbol.asyncIterator]: function() {
|
|
359
|
+
return {
|
|
360
|
+
next: function() {
|
|
361
|
+
if (eventBuffer.length > 0) {
|
|
362
|
+
return Promise.resolve({ value: eventBuffer.shift(), done: false });
|
|
363
|
+
}
|
|
364
|
+
if (iteratorDone) {
|
|
365
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
366
|
+
}
|
|
367
|
+
return new Promise(function(resolve) { eventWaiting = resolve; });
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
pushMessage: function(text, images) {
|
|
373
|
+
var parts = [];
|
|
374
|
+
if (images && images.length > 0) {
|
|
375
|
+
for (var i = 0; i < images.length; i++) {
|
|
376
|
+
parts.push({
|
|
377
|
+
inlineData: {
|
|
378
|
+
mimeType: images[i].mediaType,
|
|
379
|
+
data: images[i].data,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (text) parts.push({ text: text });
|
|
385
|
+
var msg = parts.length === 1 && parts[0].text ? text : parts;
|
|
386
|
+
|
|
387
|
+
// First pushMessage starts the query loop
|
|
388
|
+
if (!state.loopStarted) {
|
|
389
|
+
state.loopStarted = true;
|
|
390
|
+
runQueryLoop(msg);
|
|
391
|
+
} else {
|
|
392
|
+
pushMessageToQueue(msg);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
setModel: function(newModel) {
|
|
397
|
+
// Gemini fixes model at Chat creation.
|
|
398
|
+
// To change model, would need to create new Chat with history.
|
|
399
|
+
// For now, store and apply on next query.
|
|
400
|
+
return Promise.resolve();
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
setEffort: function(effort) {
|
|
404
|
+
// Stored for next query
|
|
405
|
+
return Promise.resolve();
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
setToolPolicy: function(policy) {
|
|
409
|
+
// Gemini uses functionCallingConfig which is set at Chat creation.
|
|
410
|
+
// For now, no mid-session change.
|
|
411
|
+
return Promise.resolve();
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
stopTask: function(taskId) {
|
|
415
|
+
// Gemini has no sub-agents
|
|
416
|
+
return Promise.resolve();
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
getContextUsage: function() {
|
|
420
|
+
return Promise.resolve(state.lastUsage);
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
abort: function() {
|
|
424
|
+
state.aborted = true;
|
|
425
|
+
if (abortController) {
|
|
426
|
+
try { abortController.abort(); } catch (e) {}
|
|
427
|
+
}
|
|
428
|
+
endIterator();
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
close: function() {
|
|
432
|
+
messageQueueEnded = true;
|
|
433
|
+
if (messageWaiting) {
|
|
434
|
+
var resolve = messageWaiting;
|
|
435
|
+
messageWaiting = null;
|
|
436
|
+
resolve(null);
|
|
437
|
+
}
|
|
438
|
+
endIterator();
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
endInput: function() {
|
|
442
|
+
messageQueueEnded = true;
|
|
443
|
+
if (messageWaiting) {
|
|
444
|
+
var resolve = messageWaiting;
|
|
445
|
+
messageWaiting = null;
|
|
446
|
+
resolve(null);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
return handle;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// --- Adapter factory ---
|
|
456
|
+
|
|
457
|
+
function createGeminiAdapter(opts) {
|
|
458
|
+
var _cwd = (opts && opts.cwd) || process.cwd();
|
|
459
|
+
var _cachedModels = [];
|
|
460
|
+
var _ai = null;
|
|
461
|
+
|
|
462
|
+
var adapter = {
|
|
463
|
+
vendor: "gemini",
|
|
464
|
+
|
|
465
|
+
init: async function(initOpts) {
|
|
466
|
+
var genaiModule = await loadSDK();
|
|
467
|
+
var GoogleGenAI = genaiModule.GoogleGenAI;
|
|
468
|
+
|
|
469
|
+
var apiKey = null;
|
|
470
|
+
if (initOpts && initOpts.adapterOptions && initOpts.adapterOptions.GEMINI) {
|
|
471
|
+
apiKey = initOpts.adapterOptions.GEMINI.apiKey;
|
|
472
|
+
}
|
|
473
|
+
apiKey = apiKey || process.env.GEMINI_API_KEY;
|
|
474
|
+
|
|
475
|
+
if (!apiKey) {
|
|
476
|
+
throw new Error("[yoke/gemini] No API key. Set GEMINI_API_KEY or pass adapterOptions.GEMINI.apiKey");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_ai = new GoogleGenAI({ apiKey: apiKey });
|
|
480
|
+
|
|
481
|
+
// Hardcoded model list for now. Gemini API model listing requires
|
|
482
|
+
// additional API call that may not be worth the latency on init.
|
|
483
|
+
_cachedModels = [
|
|
484
|
+
"gemini-2.5-flash",
|
|
485
|
+
"gemini-2.5-pro",
|
|
486
|
+
"gemini-2.0-flash",
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
models: _cachedModels,
|
|
491
|
+
defaultModel: "gemini-2.5-flash",
|
|
492
|
+
skills: [],
|
|
493
|
+
slashCommands: [],
|
|
494
|
+
fastModeState: null,
|
|
495
|
+
capabilities: {
|
|
496
|
+
thinking: true,
|
|
497
|
+
betas: false,
|
|
498
|
+
rewind: false,
|
|
499
|
+
sessionResume: false,
|
|
500
|
+
promptSuggestions: false,
|
|
501
|
+
elicitation: false,
|
|
502
|
+
fileCheckpointing: false,
|
|
503
|
+
contextCompacting: false,
|
|
504
|
+
toolPolicy: ["ask", "allow-all"],
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
supportedModels: function() {
|
|
510
|
+
return Promise.resolve(_cachedModels.slice());
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
createToolServer: function(def) {
|
|
514
|
+
// Convert YOKE tool definitions to Gemini FunctionDeclaration format.
|
|
515
|
+
// YOKE tool inputSchema is a Zod shape object (from buildShape()).
|
|
516
|
+
// Gemini SDK expects JSON Schema. We convert using Zod's toJSONSchema().
|
|
517
|
+
var declarations = [];
|
|
518
|
+
var handlers = {};
|
|
519
|
+
|
|
520
|
+
for (var i = 0; i < def.tools.length; i++) {
|
|
521
|
+
var t = def.tools[i];
|
|
522
|
+
var params = undefined;
|
|
523
|
+
|
|
524
|
+
if (t.inputSchema) {
|
|
525
|
+
try {
|
|
526
|
+
// inputSchema is a Zod shape (plain object of Zod fields).
|
|
527
|
+
// Wrap in z.object() to get a proper Zod schema, then convert.
|
|
528
|
+
var z = require("zod");
|
|
529
|
+
var zodSchema = z.object(t.inputSchema);
|
|
530
|
+
var jsonSchema = zodSchema.toJSONSchema();
|
|
531
|
+
// Gemini expects { type: "object", properties: {...}, required: [...] }
|
|
532
|
+
// Remove $schema and additionalProperties that Gemini doesn't want
|
|
533
|
+
params = {
|
|
534
|
+
type: jsonSchema.type || "object",
|
|
535
|
+
properties: jsonSchema.properties || {},
|
|
536
|
+
};
|
|
537
|
+
if (jsonSchema.required && jsonSchema.required.length > 0) {
|
|
538
|
+
params.required = jsonSchema.required;
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.error("[yoke/gemini] Failed to convert schema for tool '" + t.name + "':", e.message);
|
|
542
|
+
// Fallback: no parameters
|
|
543
|
+
params = undefined;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
declarations.push({
|
|
548
|
+
name: t.name,
|
|
549
|
+
description: t.description,
|
|
550
|
+
parameters: params,
|
|
551
|
+
});
|
|
552
|
+
if (t.handler) {
|
|
553
|
+
handlers[t.name] = t.handler;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
_type: "gemini_tool_server",
|
|
559
|
+
name: def.name,
|
|
560
|
+
declarations: declarations,
|
|
561
|
+
handlers: handlers,
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
createQuery: async function(queryOpts) {
|
|
566
|
+
if (!_ai) {
|
|
567
|
+
throw new Error("[yoke/gemini] Adapter not initialized. Call init() first.");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
var model = queryOpts.model || "gemini-2.5-flash";
|
|
571
|
+
// If a non-Gemini model name is passed (e.g. from Claude session), fallback
|
|
572
|
+
if (model && model.indexOf("gemini") === -1) {
|
|
573
|
+
console.log("[yoke/gemini] Non-Gemini model '" + model + "', falling back to gemini-2.5-flash");
|
|
574
|
+
model = "gemini-2.5-flash";
|
|
575
|
+
}
|
|
576
|
+
var ac = queryOpts.abortController || new AbortController();
|
|
577
|
+
|
|
578
|
+
// Build chat config
|
|
579
|
+
var chatConfig = {};
|
|
580
|
+
|
|
581
|
+
// System prompt (includes merged project instructions from YOKE layer)
|
|
582
|
+
if (queryOpts.systemPrompt) {
|
|
583
|
+
chatConfig.systemInstruction = queryOpts.systemPrompt;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Tools - only accept Gemini-native tool servers (_type === "gemini_tool_server").
|
|
587
|
+
// Claude MCP server objects and tools with Zod schemas are silently skipped.
|
|
588
|
+
// TODO: Zod-to-JSON-Schema conversion for full tool support.
|
|
589
|
+
var toolHandlers = {};
|
|
590
|
+
if (queryOpts.toolServers) {
|
|
591
|
+
var allDeclarations = [];
|
|
592
|
+
var servers = queryOpts.toolServers;
|
|
593
|
+
// toolServers can be an object { name: server } or array
|
|
594
|
+
if (Array.isArray(servers)) {
|
|
595
|
+
for (var i = 0; i < servers.length; i++) {
|
|
596
|
+
if (servers[i] && servers[i]._type === "gemini_tool_server") {
|
|
597
|
+
allDeclarations = allDeclarations.concat(servers[i].declarations);
|
|
598
|
+
Object.assign(toolHandlers, servers[i].handlers);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} else if (typeof servers === "object") {
|
|
602
|
+
for (var key in servers) {
|
|
603
|
+
var srv = servers[key];
|
|
604
|
+
if (srv && srv._type === "gemini_tool_server") {
|
|
605
|
+
allDeclarations = allDeclarations.concat(srv.declarations);
|
|
606
|
+
Object.assign(toolHandlers, srv.handlers);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (allDeclarations.length > 0) {
|
|
611
|
+
chatConfig.tools = [{ functionDeclarations: allDeclarations }];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
console.log("[yoke/gemini] createQuery: model=" + model + " tools=" + Object.keys(toolHandlers).length + " systemPrompt=" + (queryOpts.systemPrompt ? "yes" : "no"));
|
|
615
|
+
|
|
616
|
+
// Thinking config from adapterOptions
|
|
617
|
+
var geminiOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.GEMINI) || {};
|
|
618
|
+
if (geminiOpts.thinkingConfig) {
|
|
619
|
+
chatConfig.thinkingConfig = geminiOpts.thinkingConfig;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Safety settings
|
|
623
|
+
if (geminiOpts.safetySettings) {
|
|
624
|
+
chatConfig.safetySettings = geminiOpts.safetySettings;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Temperature, maxOutputTokens, etc.
|
|
628
|
+
if (geminiOpts.temperature != null) chatConfig.temperature = geminiOpts.temperature;
|
|
629
|
+
if (geminiOpts.maxOutputTokens != null) chatConfig.maxOutputTokens = geminiOpts.maxOutputTokens;
|
|
630
|
+
if (geminiOpts.topP != null) chatConfig.topP = geminiOpts.topP;
|
|
631
|
+
if (geminiOpts.topK != null) chatConfig.topK = geminiOpts.topK;
|
|
632
|
+
|
|
633
|
+
// Tool policy
|
|
634
|
+
if (queryOpts.toolPolicy === "allow-all" && chatConfig.tools) {
|
|
635
|
+
chatConfig.toolConfig = {
|
|
636
|
+
functionCallingConfig: { mode: "ANY" },
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Create chat
|
|
641
|
+
var chat = _ai.chats.create({
|
|
642
|
+
model: model,
|
|
643
|
+
config: chatConfig,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
var handle = createGeminiQueryHandle(
|
|
647
|
+
chat, model, toolHandlers,
|
|
648
|
+
queryOpts.canUseTool || null,
|
|
649
|
+
ac,
|
|
650
|
+
queryOpts.adapterOptions
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
return handle;
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
// --- Title generation ---
|
|
657
|
+
generateTitle: async function(messages, opts) {
|
|
658
|
+
var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
|
|
659
|
+
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";
|
|
660
|
+
for (var i = 0; i < messages.length; i++) {
|
|
661
|
+
prompt += "User message " + (i + 1) + ": " + messages[i] + "\n";
|
|
662
|
+
}
|
|
663
|
+
var ac = new AbortController();
|
|
664
|
+
var handle = await adapter.createQuery({
|
|
665
|
+
cwd: (opts && opts.cwd) || undefined,
|
|
666
|
+
systemPrompt: systemPrompt,
|
|
667
|
+
model: "gemini-2.5-flash",
|
|
668
|
+
abortController: ac,
|
|
669
|
+
});
|
|
670
|
+
handle.pushMessage(prompt);
|
|
671
|
+
var title = "";
|
|
672
|
+
var streamed = false;
|
|
673
|
+
try {
|
|
674
|
+
for await (var msg of handle) {
|
|
675
|
+
if (msg.yokeType === "text_delta" && msg.text) {
|
|
676
|
+
streamed = true;
|
|
677
|
+
title += msg.text;
|
|
678
|
+
} else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !streamed && msg.content) {
|
|
679
|
+
var content = msg.content;
|
|
680
|
+
if (Array.isArray(content)) {
|
|
681
|
+
for (var ci = 0; ci < content.length; ci++) {
|
|
682
|
+
if (content[ci].type === "text" && content[ci].text) {
|
|
683
|
+
title += content[ci].text;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} else if (msg.yokeType === "result") {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
} finally {
|
|
692
|
+
handle.close();
|
|
693
|
+
}
|
|
694
|
+
return title.replace(/[\r\n]+/g, " ").replace(/^["'\s]+|["'\s.]+$/g, "").trim();
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
// Gemini has no session management (stateless chat)
|
|
698
|
+
getSessionInfo: function() { return Promise.resolve(null); },
|
|
699
|
+
listSessions: function() { return Promise.resolve([]); },
|
|
700
|
+
renameSession: function() { return Promise.resolve(); },
|
|
701
|
+
forkSession: function() { return Promise.resolve(null); },
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
return adapter;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
module.exports = {
|
|
708
|
+
createGeminiAdapter: createGeminiAdapter,
|
|
709
|
+
};
|