clay-server 2.34.0-beta.2 → 2.34.0-beta.3

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.
@@ -1,709 +0,0 @@
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
- };