@exreve/exk 1.0.38 → 1.0.39

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.
@@ -101,9 +101,17 @@ function extractToolName(toolResult) {
101
101
  }
102
102
  if (toolResult.stdout !== undefined || toolResult.stderr !== undefined)
103
103
  return 'Bash';
104
- // SDK 0.2.x: content-only results from nested tool calls (no stdout/stderr wrapper)
105
- if (toolResult.content && typeof toolResult.content === 'string' && toolResult.type === 'text')
104
+ // send_file tool: content is JSON with _type marker detect before Bash fallback
105
+ if (toolResult.content && typeof toolResult.content === 'string' && toolResult.type === 'text') {
106
+ try {
107
+ const parsed = JSON.parse(toolResult.content);
108
+ if (parsed._type === 'send_file')
109
+ return 'send_file';
110
+ }
111
+ catch { /* not JSON, fall through */ }
112
+ // SDK 0.2.x: content-only results from nested tool calls (no stdout/stderr wrapper)
106
113
  return 'Bash';
114
+ }
107
115
  return 'unknown';
108
116
  }
109
117
  // Look up tool name from the most recent assistant message's tool_use blocks by tool_use_id
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Module MCP Server
3
3
  *
4
- * Provides built-in tools like analyze_image for vision capabilities.
4
+ * Provides built-in tools like analyze_image for vision capabilities
5
+ * and send_file for displaying files to the user in chat.
5
6
  */
6
7
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
7
8
  import { z } from 'zod';
@@ -9,19 +10,54 @@ import * as fs from 'fs';
9
10
  import * as path from 'path';
10
11
  import * as os from 'os';
11
12
  import { getOpenrouterApiKey } from './agentSession.js';
13
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
14
+ /** Comprehensive MIME type map for file extension detection */
15
+ const MIME_MAP = {
16
+ // Images
17
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
18
+ gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp',
19
+ svg: 'image/svg+xml', ico: 'image/x-icon', tiff: 'image/tiff', tif: 'image/tiff',
20
+ avif: 'image/avif',
21
+ // Audio
22
+ mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg',
23
+ m4a: 'audio/mp4', flac: 'audio/flac', aac: 'audio/aac',
24
+ wma: 'audio/x-ms-wma', opus: 'audio/opus',
25
+ // Video
26
+ mp4: 'video/mp4', webm: 'video/webm', mkv: 'video/x-matroska',
27
+ avi: 'video/x-msvideo', mov: 'video/quicktime', wmv: 'video/x-ms-wmv',
28
+ m4v: 'video/mp4', '3gp': 'video/3gpp',
29
+ // Documents
30
+ pdf: 'application/pdf',
31
+ // Text / Code
32
+ txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
33
+ json: 'application/json', xml: 'text/xml', yaml: 'text/yaml', yml: 'text/yaml',
34
+ toml: 'text/plain', html: 'text/html', htm: 'text/html',
35
+ css: 'text/css', scss: 'text/x-scss', less: 'text/x-less',
36
+ js: 'text/javascript', mjs: 'text/javascript', cjs: 'text/javascript',
37
+ ts: 'text/typescript', tsx: 'text/typescript',
38
+ jsx: 'text/javascript', py: 'text/x-python', rs: 'text/x-rust',
39
+ go: 'text/x-go', java: 'text/x-java', c: 'text/x-c', cpp: 'text/x-c++',
40
+ h: 'text/x-c', hpp: 'text/x-c++', rb: 'text/x-ruby', php: 'text/x-php',
41
+ sh: 'text/x-shellscript', bash: 'text/x-shellscript', zsh: 'text/x-shellscript',
42
+ sql: 'text/x-sql', graphql: 'text/graphql', vue: 'text/x-vue',
43
+ svelte: 'text/x-svelte', dart: 'text/x-dart', swift: 'text/x-swift',
44
+ kt: 'text/x-kotlin', scala: 'text/x-scala', lua: 'text/x-lua',
45
+ r: 'text/x-r', dockerfile: 'text/x-dockerfile',
46
+ };
12
47
  /**
13
- * Convert a file to a data URI for vision API consumption
48
+ * Get MIME type from file extension
49
+ */
50
+ function getMimeType(filePath) {
51
+ const ext = path.extname(filePath).toLowerCase().replace('.', '');
52
+ return MIME_MAP[ext] || 'application/octet-stream';
53
+ }
54
+ /**
55
+ * Convert a file to a data URI (base64 encoded)
14
56
  */
15
57
  function fileToDataUri(filePath) {
16
58
  try {
17
59
  const buf = fs.readFileSync(filePath);
18
- const ext = path.extname(filePath).toLowerCase().replace('.', '');
19
- const mimeMap = {
20
- png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
21
- gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp',
22
- svg: 'image/svg+xml',
23
- };
24
- const mime = mimeMap[ext] || 'application/octet-stream';
60
+ const mime = getMimeType(filePath);
25
61
  return `data:${mime};base64,${buf.toString('base64')}`;
26
62
  }
27
63
  catch {
@@ -78,12 +114,76 @@ function createAnalyzeImageTool(attachmentDir) {
78
114
  });
79
115
  }
80
116
  /**
81
- * Create the MCP server with built-in tools (always includes analyze_image)
117
+ * Create the send_file tool for displaying files to the user in chat.
118
+ * Supports images, audio, video, PDFs, code, and other files.
119
+ */
120
+ function createSendFileTool(attachmentDir) {
121
+ const workDir = attachmentDir || os.tmpdir();
122
+ return tool('send_file', 'Send a file to the user for display in chat. Supports images (shown inline), audio/video (with player), PDFs, code files (syntax highlighted), and other files (download link). Use file_path for local files or data for base64-encoded content.', {
123
+ file_path: z.string().optional().describe('Path to a local file on this device (absolute or relative to project directory)'),
124
+ data: z.string().optional().describe('Base64-encoded file content (without data: prefix)'),
125
+ mime_type: z.string().optional().describe('MIME type of the file (required when using data, auto-detected from file_path)'),
126
+ filename: z.string().optional().describe('Display name for the file (auto-detected from file_path)'),
127
+ }, async (args) => {
128
+ try {
129
+ let dataUri;
130
+ let mimeType;
131
+ let fileName;
132
+ let fileSize;
133
+ if (args.file_path) {
134
+ // Read from local file
135
+ const filePath = path.resolve(workDir, args.file_path);
136
+ if (!fs.existsSync(filePath)) {
137
+ return { content: [{ type: 'text', text: `Error: File not found: ${args.file_path}` }], isError: true };
138
+ }
139
+ const stat = fs.statSync(filePath);
140
+ fileSize = stat.size;
141
+ if (fileSize > MAX_FILE_SIZE) {
142
+ return { content: [{ type: 'text', text: `Error: File too large (${(fileSize / (1024 * 1024)).toFixed(1)} MB). Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)} MB.` }], isError: true };
143
+ }
144
+ const buf = fs.readFileSync(filePath);
145
+ mimeType = args.mime_type || getMimeType(filePath);
146
+ fileName = args.filename || path.basename(filePath);
147
+ dataUri = `data:${mimeType};base64,${buf.toString('base64')}`;
148
+ }
149
+ else if (args.data) {
150
+ // Use provided base64 data
151
+ mimeType = args.mime_type || 'application/octet-stream';
152
+ fileName = args.filename || 'file';
153
+ const rawBase64 = args.data.replace(/^data:[^;]+;base64,/, '');
154
+ fileSize = Math.floor(rawBase64.length * 0.75);
155
+ if (fileSize > MAX_FILE_SIZE) {
156
+ return { content: [{ type: 'text', text: `Error: Data too large (~${(fileSize / (1024 * 1024)).toFixed(1)} MB). Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)} MB.` }], isError: true };
157
+ }
158
+ dataUri = `data:${mimeType};base64,${rawBase64}`;
159
+ }
160
+ else {
161
+ return { content: [{ type: 'text', text: 'Error: Either file_path or data must be provided.' }], isError: true };
162
+ }
163
+ // Return structured result that the frontend will detect
164
+ const result = JSON.stringify({
165
+ _type: 'send_file',
166
+ data: dataUri,
167
+ mime_type: mimeType,
168
+ filename: fileName,
169
+ size: fileSize,
170
+ });
171
+ return { content: [{ type: 'text', text: result }] };
172
+ }
173
+ catch (error) {
174
+ return { content: [{ type: 'text', text: `Error sending file: ${error.message}` }], isError: true };
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Create the MCP server with built-in tools
82
180
  */
83
181
  export function createModuleMcpServer(config) {
84
182
  const tools = [];
85
183
  // Always add analyze_image tool (uses OpenRouter key from ai-config via backend)
86
184
  tools.push(createAnalyzeImageTool(config.attachmentDir));
185
+ // Add send_file tool for displaying files to the user in chat
186
+ tools.push(createSendFileTool(config.attachmentDir));
87
187
  const server = createSdkMcpServer({
88
188
  name: 'claude-voice-modules',
89
189
  version: '1.0.0',
@@ -71,6 +71,8 @@ export function startSending(socket, transfer, foreground) {
71
71
  const msg = data.toString().trim();
72
72
  if (msg)
73
73
  log(`tar: ${msg}`);
74
+ // Count lines in tar stderr as approximate file count
75
+ fileCount += msg.split('\n').length;
74
76
  });
75
77
  tar.on('error', (err) => {
76
78
  sendError(`tar process error: ${err.message}`);
@@ -81,16 +83,6 @@ export function startSending(socket, transfer, foreground) {
81
83
  return;
82
84
  }
83
85
  log(`Packing complete. Final size: ${formatBytes(bytesTransferred)}`);
84
- const sha256 = hash.digest('hex');
85
- const duration = Date.now() - startTime;
86
- // Notify backend that upload is done
87
- socket.emit('transfer:uploaded', {
88
- transferId,
89
- totalBytes: bytesTransferred,
90
- sha256,
91
- fileCount,
92
- });
93
- log(`Upload complete: ${formatBytes(bytesTransferred)} in ${(duration / 1000).toFixed(1)}s (sha256: ${sha256.slice(0, 12)}...)`);
94
86
  });
95
87
  // Stream tar output through hash, then HTTP POST
96
88
  const uploadUrl = `${apiUrl}/transfer/${transferId}/upload`;
@@ -123,7 +115,18 @@ export function startSending(socket, transfer, foreground) {
123
115
  if (!res.ok) {
124
116
  const text = await res.text();
125
117
  sendError(`Upload failed: HTTP ${res.status} ${text}`);
118
+ return;
126
119
  }
120
+ const sha256 = hash.digest('hex');
121
+ const duration = Date.now() - startTime;
122
+ // Notify backend that upload is done (only after HTTP confirms success)
123
+ socket.emit('transfer:uploaded', {
124
+ transferId,
125
+ totalBytes: bytesTransferred,
126
+ sha256,
127
+ fileCount,
128
+ });
129
+ log(`Upload complete: ${formatBytes(bytesTransferred)} in ${(duration / 1000).toFixed(1)}s (sha256: ${sha256.slice(0, 12)}...)`);
127
130
  }).catch((err) => {
128
131
  sendError(`Upload network error: ${err.message}`);
129
132
  });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {