@exreve/exk 1.0.62 → 1.0.64

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.
@@ -273,16 +273,17 @@ export class AgentSessionManager {
273
273
  // Priority: explicit parameter > env var
274
274
  const requested = agentBackend || process.env.TTC_AGENT_BACKEND;
275
275
  if (requested === 'pi') {
276
+ let piBackend;
276
277
  try {
277
- const { piBackend } = require('./piBackend.js');
278
- if (piBackend.isAvailable()) {
279
- return piBackend;
280
- }
281
- console.warn('[AgentSessionManager] Pi backend requested but SDK not installed, falling back to Claude');
278
+ piBackend = require('./piBackend.js').piBackend;
279
+ }
280
+ catch (err) {
281
+ throw new Error(`Pi backend requested but module failed to load: ${err.message}. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent`);
282
282
  }
283
- catch {
284
- console.warn('[AgentSessionManager] Pi backend not available, falling back to Claude');
283
+ if (!piBackend.isAvailable()) {
284
+ throw new Error('Pi backend requested but SDK (@mariozechner/pi-coding-agent) is not installed. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent');
285
285
  }
286
+ return piBackend;
286
287
  }
287
288
  return claudeBackend;
288
289
  }
@@ -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',
@@ -15,10 +15,8 @@
15
15
  */
16
16
  import { executeAnalyzeImage, executeSendFile, executeBrowserQuery } from './sharedTools.js';
17
17
  // Conditional import — will be undefined if package not installed
18
- // @ts-expect-error — Pi SDK is an optional dependency, may not be installed
19
18
  let piSdk;
20
19
  try {
21
- // @ts-expect-error — optional dependency
22
20
  piSdk = await import('@mariozechner/pi-coding-agent');
23
21
  }
24
22
  catch {
@@ -62,20 +60,12 @@ export class PiBackend {
62
60
  // Set model if specified
63
61
  if (model) {
64
62
  try {
65
- // Try to find a built-in model first
66
- const builtInModel = piSdk.getModel?.('anthropic', model);
67
- if (builtInModel) {
68
- sessionOpts.model = builtInModel;
69
- }
70
- else {
71
- // Try as a custom model — Pi supports custom providers via models.json
72
- // For now, we try the model string directly
73
- const found = modelRegistry.find(provider || 'anthropic', model);
74
- if (found) {
75
- sessionOpts.model = found;
76
- }
77
- // If not found, Pi will use its default model
63
+ // Try as a custom model via ModelRegistry
64
+ const found = modelRegistry.find(provider || 'anthropic', model);
65
+ if (found) {
66
+ sessionOpts.model = found;
78
67
  }
68
+ // If not found, Pi will use its default model
79
69
  }
80
70
  catch {
81
71
  // Model not found — Pi will use default
@@ -330,7 +320,8 @@ export class PiBackend {
330
320
  required: ['image_path', 'question'],
331
321
  },
332
322
  execute: async (_toolCallId, params) => {
333
- return executeAnalyzeImage({ image_path: params.image_path, question: params.question }, sharedConfig);
323
+ const result = await executeAnalyzeImage({ image_path: params.image_path, question: params.question }, sharedConfig);
324
+ return { ...result, details: {} };
334
325
  },
335
326
  });
336
327
  }
@@ -352,7 +343,8 @@ export class PiBackend {
352
343
  },
353
344
  },
354
345
  execute: async (_toolCallId, params) => {
355
- return executeSendFile(params, sharedConfig);
346
+ const result = await executeSendFile(params, sharedConfig);
347
+ return { ...result, details: {} };
356
348
  },
357
349
  });
358
350
  }
@@ -373,7 +365,8 @@ export class PiBackend {
373
365
  required: ['query'],
374
366
  },
375
367
  execute: async (_toolCallId, params) => {
376
- return executeBrowserQuery({ query: params.query, maxSteps: params.maxSteps }, sharedConfig);
368
+ const result = await executeBrowserQuery({ query: params.query, maxSteps: params.maxSteps }, sharedConfig);
369
+ return { ...result, details: {} };
377
370
  },
378
371
  });
379
372
  }
@@ -257,3 +257,72 @@ 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 and save to temp dir
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 imgBuf = Buffer.from(base64, 'base64');
307
+ const tmpDir = path.join(os.tmpdir(), 'talk-to-code', 'gen-images');
308
+ fs.mkdirSync(tmpDir, { recursive: true });
309
+ const safeName = (args.filename || `gen_${Date.now()}`).replace(/[^a-zA-Z0-9._-]/g, '_');
310
+ const fileName = safeName.endsWith('.png') ? safeName : `${safeName}.png`;
311
+ const filePath = path.join(tmpDir, fileName);
312
+ fs.writeFileSync(filePath, imgBuf);
313
+ const sizeKb = (imgBuf.length / 1024).toFixed(0);
314
+ return {
315
+ content: [{
316
+ type: 'text',
317
+ text: `Image generated successfully.\n\nFile: ${filePath}\nSize: ${sizeKb} KB\nPrompt: ${args.prompt}`,
318
+ }],
319
+ details: { filePath, sizeKb, prompt: args.prompt },
320
+ };
321
+ }
322
+ catch (error) {
323
+ if (error.name === 'TimeoutError') {
324
+ return { content: [{ type: 'text', text: 'Image generation timed out after 2 minutes. Try a simpler prompt.' }], isError: true };
325
+ }
326
+ return { content: [{ type: 'text', text: `Error generating image: ${error.message}` }], isError: true };
327
+ }
328
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.62",
3
+ "version": "1.0.64",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,7 @@
36
36
  "@anthropic-ai/claude-agent-sdk": "^0.2.126",
37
37
  "@anthropic-ai/sdk": "^0.92.0",
38
38
  "@fastify/static": "^9.0.0",
39
+ "@mariozechner/pi-coding-agent": "^0.73.1",
39
40
  "@xenova/transformers": "^2.17.2",
40
41
  "anthropic-proxy": "^1.3.0",
41
42
  "chokidar": "^3.6.0",