@exreve/exk 1.0.63 → 1.0.65

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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
9
9
  import { z } from 'zod';
10
- import { executeAnalyzeImage, executeSendFile, executeBrowserQuery } from './sharedTools.js';
10
+ import { executeAnalyzeImage, executeSendFile, executeBrowserQuery, executeGenImage } from './sharedTools.js';
11
11
  /**
12
12
  * Build the shared config from MCP server config.
13
13
  */
@@ -56,6 +56,15 @@ export function createModuleMcpServer(config) {
56
56
  country: args.country,
57
57
  mobile: args.mobile,
58
58
  }, sharedConfig)),
59
+ tool('gen_image', 'Generate an image from a text prompt using Flux.2-flex. The generated PNG is saved to a temp directory and the file path is returned. ' +
60
+ 'Use this to create illustrations, icons, diagrams, or any visual content for the user. ' +
61
+ 'The image is saved locally — use send_file to display it in chat after generation.', {
62
+ prompt: z.string().describe('Detailed text description of the image to generate. Be specific about style, colors, composition, etc.'),
63
+ filename: z.string().optional().describe('Output filename (without extension, or with .png). Defaults to gen_<timestamp>.png'),
64
+ }, async (args) => executeGenImage({
65
+ prompt: args.prompt,
66
+ filename: args.filename,
67
+ }, sharedConfig)),
59
68
  ];
60
69
  return createSdkMcpServer({
61
70
  name: 'claude-voice-modules',
@@ -257,3 +257,94 @@ export async function executeBrowserQuery(args, config) {
257
257
  };
258
258
  }
259
259
  }
260
+ // ============ Image Generation ============
261
+ const FLUX_MODEL = 'black-forest-labs/flux.2-flex';
262
+ /**
263
+ * gen_image — generate an image using Flux.2-flex via OpenRouter.
264
+ * Saves the result as a PNG file in a temp directory and returns the file path.
265
+ */
266
+ export async function executeGenImage(args, _config) {
267
+ const apiKey = getOpenrouterApiKey();
268
+ if (!apiKey) {
269
+ return { content: [{ type: 'text', text: 'Error: OPENROUTER_API_KEY not configured. Set it in ai-config.json or OPENROUTER_API_KEY env var.' }], isError: true };
270
+ }
271
+ try {
272
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Authorization': `Bearer ${apiKey}`,
276
+ 'Content-Type': 'application/json',
277
+ 'HTTP-Referer': 'https://talk-to-code.com',
278
+ 'X-Title': 'TalkToCode',
279
+ },
280
+ body: JSON.stringify({
281
+ model: FLUX_MODEL,
282
+ messages: [{ role: 'user', content: args.prompt }],
283
+ modalities: ['image'],
284
+ }),
285
+ signal: AbortSignal.timeout(120_000), // 2 min timeout for image gen
286
+ });
287
+ if (!res.ok) {
288
+ const errText = await res.text();
289
+ return { content: [{ type: 'text', text: `Error from Flux API (${res.status}): ${errText.slice(0, 500)}` }], isError: true };
290
+ }
291
+ const data = await res.json();
292
+ // Extract image from response
293
+ const images = data.choices?.[0]?.message?.images;
294
+ if (!images || images.length === 0) {
295
+ return { content: [{ type: 'text', text: 'Error: No image in response from Flux API.' }], isError: true };
296
+ }
297
+ const dataUrl = images[0]?.image_url?.url || images[0];
298
+ if (!dataUrl || !dataUrl.startsWith('data:')) {
299
+ return { content: [{ type: 'text', text: `Error: Unexpected image format from Flux API.` }], isError: true };
300
+ }
301
+ // Decode base64
302
+ const base64 = dataUrl.split(',')[1];
303
+ if (!base64) {
304
+ return { content: [{ type: 'text', text: 'Error: Empty image data from Flux API.' }], isError: true };
305
+ }
306
+ const rawBuf = Buffer.from(base64, 'base64');
307
+ const rawKb = (rawBuf.length / 1024).toFixed(0);
308
+ // Compress to WebP with sharp (high quality, same dimensions)
309
+ let finalBuf;
310
+ let ext = 'webp';
311
+ try {
312
+ const sharp = (await import('sharp')).default;
313
+ finalBuf = await sharp(rawBuf)
314
+ .webp({ quality: 90, effort: 4 })
315
+ .toBuffer();
316
+ // If compression didn't help (rare), keep original PNG
317
+ if (finalBuf.length >= rawBuf.length) {
318
+ finalBuf = rawBuf;
319
+ ext = 'png';
320
+ }
321
+ }
322
+ catch {
323
+ // sharp not available — save raw PNG
324
+ finalBuf = rawBuf;
325
+ ext = 'png';
326
+ }
327
+ const tmpDir = path.join(os.tmpdir(), 'talk-to-code', 'gen-images');
328
+ fs.mkdirSync(tmpDir, { recursive: true });
329
+ const safeName = (args.filename || `gen_${Date.now()}`).replace(/[^a-zA-Z0-9._-]/g, '_');
330
+ const baseName = safeName.replace(/\.(png|webp|jpg|jpeg)$/i, '');
331
+ const fileName = `${baseName}.${ext}`;
332
+ const filePath = path.join(tmpDir, fileName);
333
+ fs.writeFileSync(filePath, finalBuf);
334
+ const sizeKb = (finalBuf.length / 1024).toFixed(0);
335
+ const saved = ext === 'webp' ? ` (${rawKb} KB PNG → ${sizeKb} KB WebP)` : '';
336
+ return {
337
+ content: [{
338
+ type: 'text',
339
+ text: `Image generated successfully.\n\nFile: ${filePath}\nSize: ${sizeKb} KB${saved}\nPrompt: ${args.prompt}`,
340
+ }],
341
+ details: { filePath, sizeKb, prompt: args.prompt },
342
+ };
343
+ }
344
+ catch (error) {
345
+ if (error.name === 'TimeoutError') {
346
+ return { content: [{ type: 'text', text: 'Image generation timed out after 2 minutes. Try a simpler prompt.' }], isError: true };
347
+ }
348
+ return { content: [{ type: 'text', text: `Error generating image: ${error.message}` }], isError: true };
349
+ }
350
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {