@exreve/exk 1.0.57 → 1.0.59

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/bin/exk CHANGED
@@ -3,20 +3,18 @@
3
3
  // Runs compiled JavaScript directly (no tsx needed)
4
4
 
5
5
  import { resolve, dirname } from 'path'
6
- import { existsSync } from 'fs'
7
6
  import { fileURLToPath } from 'url'
7
+ import { existsSync } from 'fs'
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url)
10
10
  const __dirname = dirname(__filename)
11
11
 
12
12
  const pkgDir = resolve(__dirname, '..')
13
- // dist layout varies by tsc root inference check both locations
13
+ // tsc outputs to dist/cli/ (see CLAUDE.md "tsc output goes to dist/cli/")
14
14
  const entryPoint = existsSync(resolve(pkgDir, 'dist', 'cli', 'index.js'))
15
15
  ? resolve(pkgDir, 'dist', 'cli', 'index.js')
16
16
  : resolve(pkgDir, 'dist', 'index.js')
17
17
 
18
- const args = process.argv.slice(2)
19
-
20
18
  // Import and run the compiled CLI
21
19
  import(entryPoint).catch(err => {
22
20
  console.error('Failed to start exk:', err.message)
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from '
4
4
  import { symlink as fsSymlink } from 'fs';
5
5
  import { getSkillContent } from './skills/index.js';
6
6
  import { isLocalModel, unwrapModelName, startOpenAIAdapter, getAdapterConfig } from './openaiAdapter.js';
7
+ import { ensureProxy } from './proxyManager.js';
7
8
  import { createModuleMcpServer } from './moduleMcpServer.js';
8
9
  import path from 'path';
9
10
  import os from 'os';
@@ -203,6 +204,11 @@ const PROVIDERS = {
203
204
  baseUrl: 'https://openrouter.ai/api',
204
205
  models: ['gpt-oss-120b:cerebras'],
205
206
  },
207
+ cerebras: {
208
+ apiKey: '', // Populated from ai-config.json cerebrasApiKey (served by backend)
209
+ baseUrl: '', // Set dynamically when proxy starts
210
+ models: ['zai-glm-4.7'],
211
+ },
206
212
  };
207
213
  /** Resolve which provider to use based on model name or explicit provider ID.
208
214
  * 1. Populate provider API keys from ai-config.json (served by backend).
@@ -214,6 +220,7 @@ function resolveProvider(model, providerId) {
214
220
  const aiConfig = loadAiConfig();
215
221
  PROVIDERS.minimax.apiKey = aiConfig.minimaxApiKey || process.env.MINIMAX_API_KEY || '';
216
222
  PROVIDERS.openrouter.apiKey = aiConfig.openrouterApiKey || process.env.OPENROUTER_API_KEY || '';
223
+ PROVIDERS.cerebras.apiKey = aiConfig.cerebrasApiKey || process.env.CEREBRAS_API_KEY || '';
217
224
  if (!PROVIDERS.zai.apiKey)
218
225
  PROVIDERS.zai.apiKey = aiConfig.apiKey || '';
219
226
  // 1. Explicit provider selection
@@ -249,12 +256,13 @@ function loadAiConfig() {
249
256
  const proxy = typeof config.proxy === 'string' ? config.proxy.trim() : '';
250
257
  const minimaxApiKey = typeof config.minimaxApiKey === 'string' ? config.minimaxApiKey.trim() : '';
251
258
  const openrouterApiKey = typeof config.openrouterApiKey === 'string' ? config.openrouterApiKey.trim() : '';
252
- const result = { apiKey, baseUrl, model, proxy, minimaxApiKey, openrouterApiKey };
259
+ const cerebrasApiKey = typeof config.cerebrasApiKey === 'string' ? config.cerebrasApiKey.trim() : '';
260
+ const result = { apiKey, baseUrl, model, proxy, minimaxApiKey, openrouterApiKey, cerebrasApiKey };
253
261
  _aiConfigCache = { data: result, ts: now };
254
262
  return result;
255
263
  }
256
264
  catch {
257
- const fallback = { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '', minimaxApiKey: '', openrouterApiKey: '' };
265
+ const fallback = { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '', minimaxApiKey: '', openrouterApiKey: '', cerebrasApiKey: '' };
258
266
  _aiConfigCache = { data: fallback, ts: now };
259
267
  return fallback;
260
268
  }
@@ -873,6 +881,23 @@ export class AgentSessionManager {
873
881
  settingsEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolved.model;
874
882
  settingsEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
875
883
  }
884
+ // For Cerebras: start anthropic-proxy-rs and route through it
885
+ if (resolved.provider === 'cerebras') {
886
+ const proxyUrl = await ensureProxy({
887
+ upstreamUrl: 'https://api.cerebras.ai/v1',
888
+ apiKey: resolved.apiKey,
889
+ model: resolved.model,
890
+ });
891
+ effectiveApiKey = 'cerebras-via-proxy'; // Proxy doesn't validate the key
892
+ effectiveEnv = envForClaudeCodeChild(undefined, { ...resolved, baseUrl: proxyUrl });
893
+ settingsEnv.ANTHROPIC_API_KEY = effectiveApiKey;
894
+ settingsEnv.ANTHROPIC_BASE_URL = proxyUrl;
895
+ settingsEnv.ANTHROPIC_MODEL = resolved.model;
896
+ settingsEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = resolved.model;
897
+ settingsEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = resolved.model;
898
+ settingsEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolved.model;
899
+ settingsEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
900
+ }
876
901
  effectiveSettings = { env: settingsEnv };
877
902
  console.log(`[agentSession] Provider: ${resolved.provider}, baseUrl: ${resolved.baseUrl}, model: ${resolved.model}`);
878
903
  }
@@ -941,6 +966,36 @@ export class AgentSessionManager {
941
966
  session.claudeSessionId = msg.session_id;
942
967
  saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
943
968
  }
969
+ // Detect context window limit error from the API
970
+ // The SDK sends this as an assistant message with error: 'max_output_tokens'
971
+ // when the provider returns 'model_context_window_exceeded'
972
+ const isContextWindowError = msg.error === 'max_output_tokens' && ((typeof msg.message === 'string' && msg.message.includes('context window limit')) ||
973
+ (Array.isArray(msg.message?.content) && msg.message.content.some((c) => typeof c?.text === 'string' && c.text.includes('context window limit'))));
974
+ if (isContextWindowError) {
975
+ console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context for fresh start.`);
976
+ // Clear the session ID so next prompt starts fresh (no resume)
977
+ session.claudeSessionId = undefined;
978
+ session.contextLost = true;
979
+ deleteSessionState(sessionId);
980
+ // Emit a user-friendly system message
981
+ onOutput({
982
+ type: 'system',
983
+ data: {
984
+ message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
985
+ subtype: 'context_window_reset',
986
+ },
987
+ timestamp: Date.now(),
988
+ metadata: {
989
+ subtype: 'context_window_reset',
990
+ contextInfo: {
991
+ messageCount: session.messages.length,
992
+ totalInputTokens: session.totalInputTokens,
993
+ totalOutputTokens: session.totalOutputTokens,
994
+ totalTokens: session.totalInputTokens + session.totalOutputTokens,
995
+ }
996
+ }
997
+ });
998
+ }
944
999
  session.messages.push({
945
1000
  role: 'assistant',
946
1001
  content: msg.message,
@@ -211,7 +211,7 @@ async function connect() {
211
211
  const deviceId = await getDeviceId();
212
212
  return new Promise((resolve, reject) => {
213
213
  const socket = io(config.apiUrl, {
214
- transports: ['websocket', 'polling'],
214
+ transports: ['polling', 'websocket'],
215
215
  });
216
216
  socket.on('connect', () => {
217
217
  socket.emit('register', { type: 'cli', deviceId });
@@ -516,7 +516,7 @@ async function runDaemon(foreground = false, email) {
516
516
  const connectAndRegister = async () => {
517
517
  try {
518
518
  socket = io(config.apiUrl, {
519
- transports: ['websocket', 'polling'],
519
+ transports: ['polling', 'websocket'],
520
520
  reconnection: true,
521
521
  reconnectionDelay: 1000,
522
522
  reconnectionDelayMax: 10000,
@@ -0,0 +1,386 @@
1
+ /**
2
+ * SDK Benchmark: MiniMax M2.7-highspeed vs Cerebras zai-glm-4.7 (via anthropic-proxy-rs)
3
+ *
4
+ * Both providers are tested through the Claude Agent SDK query().
5
+ * Task: Generate a complete HTML real estate page.
6
+ * Measures: total output tokens, total duration, TPS.
7
+ *
8
+ * Usage:
9
+ * cd cli && npx tsx benchmark-models-sdk.ts
10
+ *
11
+ * Requirements:
12
+ * - @anthropic-ai/claude-agent-sdk installed
13
+ * - anthropic-proxy binary (cargo install --git https://github.com/m0n0x41d/anthropic-proxy-rs --locked)
14
+ * - MiniMax API key in ~/.talk-to-code/ai-config.json
15
+ */
16
+ import { query } from '@anthropic-ai/claude-agent-sdk';
17
+ import { createRequire } from 'module';
18
+ import { execSync, spawn } from 'child_process';
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ // ── Config ──────────────────────────────────────────────────────────
23
+ const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
24
+ const PROXY_PORT = 8401;
25
+ const EMPTY_DIR = path.join(os.homedir(), '.talk-to-code', 'empty-config-dir');
26
+ const TASK_PROMPT = `Create a complete, beautiful HTML real estate property listing page. Include:
27
+ - A hero section with a large property image placeholder and price overlay
28
+ - Property details: 4 bed, 3 bath, 2,450 sq ft, built 2021
29
+ - A photo gallery section with 6 image placeholders in a grid
30
+ - Features list with icons (pool, garage, garden, smart home, solar panels, fireplace)
31
+ - Neighborhood info section with nearby schools, restaurants, parks
32
+ - A mortgage calculator with sliders for down payment and interest rate
33
+ - An agent contact form with name, email, phone, message fields
34
+ - A Google Maps embed placeholder
35
+ - A "Similar Properties" section with 3 cards
36
+ - Full responsive design with a mobile hamburger menu
37
+ - Use CSS variables for the color scheme (warm earth tones)
38
+ - All in a single self-contained HTML file with embedded CSS and minimal JS for the calculator and menu
39
+ Output ONLY the HTML code, nothing else.`;
40
+ function loadConfig() {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(AI_CONFIG_PATH, 'utf-8'));
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ }
48
+ // ── Colors ──────────────────────────────────────────────────────────
49
+ const GREEN = '\x1b[32m', RED = '\x1b[31m', CYAN = '\x1b[36m', YELLOW = '\x1b[33m';
50
+ const BOLD = '\x1b[1m', DIM = '\x1b[2m', MAGENTA = '\x1b[35m', RESET = '\x1b[0m';
51
+ function hr() { console.log('─'.repeat(70)); }
52
+ // ── Resolve Claude Code executable ──────────────────────────────────
53
+ function resolveClaudeExe() {
54
+ for (const pkg of [
55
+ '@anthropic-ai/claude-agent-sdk-linux-x64',
56
+ '@anthropic-ai/claude-agent-sdk-linux-x64-musl',
57
+ ]) {
58
+ try {
59
+ const req = createRequire(import.meta.url);
60
+ const nativePkgPath = req.resolve(`${pkg}/package.json`);
61
+ const nativePath = path.join(path.dirname(nativePkgPath), 'claude');
62
+ if (fs.existsSync(nativePath)) {
63
+ try {
64
+ execSync(`${nativePath} --version`, { stdio: 'pipe', timeout: 5000 });
65
+ return nativePath;
66
+ }
67
+ catch { }
68
+ }
69
+ }
70
+ catch { }
71
+ }
72
+ try {
73
+ const req = createRequire(import.meta.url);
74
+ const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
75
+ const cliPath = path.join(path.dirname(pkgPath), 'cli.js');
76
+ if (fs.existsSync(cliPath))
77
+ return cliPath;
78
+ }
79
+ catch { }
80
+ return undefined;
81
+ }
82
+ const CLAUDE_EXE = resolveClaudeExe();
83
+ // ── Proxy management ────────────────────────────────────────────────
84
+ let proxyProcess = null;
85
+ async function startProxy(apiKey, model, port) {
86
+ const proxyBin = path.join(os.homedir(), '.cargo', 'bin', 'anthropic-proxy');
87
+ if (!fs.existsSync(proxyBin)) {
88
+ throw new Error(`anthropic-proxy not found at ${proxyBin}`);
89
+ }
90
+ const proxyEnv = {};
91
+ for (const [k, v] of Object.entries(process.env)) {
92
+ if (v !== undefined)
93
+ proxyEnv[k] = v;
94
+ }
95
+ proxyEnv.UPSTREAM_BASE_URL = 'https://api.cerebras.ai/v1';
96
+ proxyEnv.UPSTREAM_API_KEY = apiKey;
97
+ proxyEnv.PORT = String(port);
98
+ proxyEnv.COMPLETION_MODEL = model;
99
+ proxyEnv.REASONING_MODEL = model;
100
+ proxyEnv.PATH = `${path.join(os.homedir(), '.cargo', 'bin')}:${proxyEnv.PATH || ''}`;
101
+ proxyProcess = spawn(proxyBin, [], { env: proxyEnv, stdio: ['ignore', 'pipe', 'pipe'] });
102
+ // Wait for proxy to be ready
103
+ for (let i = 0; i < 30; i++) {
104
+ await new Promise(r => setTimeout(r, 300));
105
+ try {
106
+ const resp = await fetch(`http://127.0.0.1:${port}/v1/messages`, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json', 'x-api-key': 'ping', 'anthropic-version': '2023-06-01' },
109
+ body: JSON.stringify({ model, max_tokens: 4, messages: [{ role: 'user', content: 'hi' }] }),
110
+ });
111
+ if (resp.ok || resp.status === 400)
112
+ return;
113
+ }
114
+ catch { }
115
+ }
116
+ throw new Error('Proxy health check timed out');
117
+ }
118
+ function stopProxy() {
119
+ if (proxyProcess) {
120
+ try {
121
+ proxyProcess.kill('SIGTERM');
122
+ }
123
+ catch { }
124
+ setTimeout(() => { try {
125
+ proxyProcess?.kill('SIGKILL');
126
+ }
127
+ catch { } }, 2000);
128
+ proxyProcess = null;
129
+ }
130
+ }
131
+ async function runSdkBenchmark(provider) {
132
+ const start = Date.now();
133
+ let initMs = 0;
134
+ let firstTokenMs = 0;
135
+ let outputText = '';
136
+ let writeToolCalls = 0;
137
+ let assistantEvents = 0;
138
+ let toolResultEvents = 0;
139
+ let firstAssistant = true;
140
+ const eventTypes = new Set();
141
+ fs.mkdirSync(EMPTY_DIR, { recursive: true });
142
+ const env = {};
143
+ for (const [k, v] of Object.entries(process.env)) {
144
+ if (v !== undefined)
145
+ env[k] = v;
146
+ }
147
+ env.ANTHROPIC_API_KEY = provider.apiKey;
148
+ env.ANTHROPIC_BASE_URL = provider.baseUrl;
149
+ env.ANTHROPIC_MODEL = provider.model;
150
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.model;
151
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.model;
152
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = provider.model;
153
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
154
+ env.IS_SANDBOX = '1';
155
+ env.CLAUDE_CONFIG_DIR = EMPTY_DIR;
156
+ try {
157
+ const q = query({
158
+ prompt: TASK_PROMPT,
159
+ options: {
160
+ apiKey: provider.apiKey,
161
+ model: provider.model,
162
+ cwd: '/tmp',
163
+ permissionMode: 'bypassPermissions',
164
+ allowDangerouslySkipPermissions: true,
165
+ maxTurns: 5,
166
+ env,
167
+ settings: { env },
168
+ ...(CLAUDE_EXE ? { pathToClaudeCodeExecutable: CLAUDE_EXE } : {}),
169
+ },
170
+ });
171
+ for await (const event of q) {
172
+ const now = Date.now();
173
+ eventTypes.add(event.type);
174
+ if (event.type === 'system' && event.subtype === 'init') {
175
+ initMs = now - start;
176
+ }
177
+ if (event.type === 'assistant') {
178
+ if (firstAssistant) {
179
+ firstTokenMs = now - start;
180
+ firstAssistant = false;
181
+ }
182
+ assistantEvents++;
183
+ const data = event.data;
184
+ if (typeof data === 'string') {
185
+ try {
186
+ const parsed = JSON.parse(data);
187
+ for (const block of (parsed?.content || [])) {
188
+ if (block.type === 'text' && block.text) {
189
+ outputText += block.text;
190
+ }
191
+ if (block.type === 'tool_use') {
192
+ if (block.name === 'Write') {
193
+ writeToolCalls++;
194
+ // Capture the file content from Write tool input
195
+ if (block.input?.content) {
196
+ outputText += block.input.content;
197
+ }
198
+ if (block.input?.file_path) {
199
+ outputText += `\n<!-- File: ${block.input.file_path} -->\n`;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ catch {
206
+ outputText += data;
207
+ }
208
+ }
209
+ }
210
+ if (event.type === 'tool_use_summary') {
211
+ toolResultEvents++;
212
+ // tool_use_summary contains tool results in metadata
213
+ const data = event.data;
214
+ if (typeof data === 'string') {
215
+ try {
216
+ const parsed = JSON.parse(data);
217
+ // Extract tool result text
218
+ const result = parsed?.metadata?.toolResult;
219
+ if (result) {
220
+ if (typeof result === 'string')
221
+ outputText += result;
222
+ else if (typeof result === 'object')
223
+ outputText += JSON.stringify(result);
224
+ }
225
+ }
226
+ catch { }
227
+ }
228
+ }
229
+ if (event.type === 'result') {
230
+ const totalMs = now - start;
231
+ // Also try to extract text from the result event itself
232
+ const data = event.data;
233
+ if (typeof data === 'string') {
234
+ try {
235
+ const parsed = JSON.parse(data);
236
+ if (parsed.result)
237
+ outputText += parsed.result;
238
+ }
239
+ catch { }
240
+ }
241
+ const outputChars = outputText.length;
242
+ const estimatedTokens = Math.round(outputChars / 3.5);
243
+ const tps = estimatedTokens > 0 && totalMs > 0 ? (estimatedTokens / (totalMs / 1000)) : 0;
244
+ return { provider: provider.name, totalMs, initMs, firstTokenMs, outputText, outputChars, estimatedTokens, tps, writeToolCalls, assistantEvents, toolResultEvents, allEventTypes: Array.from(eventTypes) };
245
+ }
246
+ }
247
+ const totalMs = Date.now() - start;
248
+ const outputChars = outputText.length;
249
+ const estimatedTokens = Math.round(outputChars / 3.5);
250
+ const tps = estimatedTokens > 0 && totalMs > 0 ? (estimatedTokens / (totalMs / 1000)) : 0;
251
+ return { provider: provider.name, totalMs, initMs, firstTokenMs, outputText, outputChars, estimatedTokens, tps, writeToolCalls, assistantEvents, toolResultEvents, allEventTypes: Array.from(eventTypes) };
252
+ }
253
+ catch (err) {
254
+ return {
255
+ provider: provider.name, totalMs: Date.now() - start, initMs, firstTokenMs,
256
+ outputText, outputChars: outputText.length, estimatedTokens: 0, tps: 0,
257
+ writeToolCalls, assistantEvents, toolResultEvents, allEventTypes: Array.from(eventTypes),
258
+ error: err.message,
259
+ };
260
+ }
261
+ }
262
+ // ── Main ────────────────────────────────────────────────────────────
263
+ async function main() {
264
+ console.log(`\n${BOLD}╔══════════════════════════════════════════════════════════════════════╗${RESET}`);
265
+ console.log(`${BOLD}║ SDK Benchmark: MiniMax M2.7-highspeed vs Cerebras zai-glm-4.7 ║${RESET}`);
266
+ console.log(`${BOLD}║ Task: Generate a complete HTML real estate page ║${RESET}`);
267
+ console.log(`${BOLD}╚══════════════════════════════════════════════════════════════════════╝${RESET}\n`);
268
+ const config = loadConfig();
269
+ const minimaxKey = config.minimaxApiKey || process.env.MINIMAX_API_KEY || '';
270
+ const cerebrasKey = process.env.CEREBRAS_API_KEY || 'csk-866r3e66p52mcmj3v4f29fdc86nr8pjx6yd4chxhvk8v2f3r';
271
+ if (!CLAUDE_EXE) {
272
+ console.log(`${RED}ERROR: Claude Code executable not found. Need @anthropic-ai/claude-agent-sdk-linux-x64${RESET}`);
273
+ process.exit(1);
274
+ }
275
+ console.log(`${CYAN}Claude Exe:${RESET} ${CLAUDE_EXE}`);
276
+ console.log(`${CYAN}Task:${RESET} Generate HTML real estate page (~${TASK_PROMPT.length} char prompt)`);
277
+ console.log();
278
+ const providers = [];
279
+ if (minimaxKey) {
280
+ providers.push({
281
+ name: 'MiniMax M2.7-highspeed',
282
+ model: 'MiniMax-M2.7-highspeed',
283
+ apiKey: minimaxKey,
284
+ baseUrl: 'https://api.minimax.io/anthropic',
285
+ needsProxy: false,
286
+ });
287
+ }
288
+ else {
289
+ console.log(`${YELLOW}WARNING: No MiniMax API key. Skipping MiniMax test.${RESET}\n`);
290
+ }
291
+ providers.push({
292
+ name: 'Cerebras zai-glm-4.7',
293
+ model: 'zai-glm-4.7',
294
+ apiKey: cerebrasKey,
295
+ baseUrl: `http://127.0.0.1:${PROXY_PORT}`,
296
+ needsProxy: true,
297
+ proxyPort: PROXY_PORT,
298
+ });
299
+ const allResults = [];
300
+ for (const provider of providers) {
301
+ console.log(`${BOLD}${MAGENTA}═══ ${provider.name} ═══${RESET}`);
302
+ hr();
303
+ // Start proxy if needed
304
+ if (provider.needsProxy) {
305
+ console.log(`${DIM}Starting anthropic-proxy-rs for Cerebras...${RESET}`);
306
+ try {
307
+ await startProxy(cerebrasKey, provider.model, provider.proxyPort);
308
+ console.log(`${DIM}Proxy ready on port ${provider.proxyPort}${RESET}\n`);
309
+ }
310
+ catch (err) {
311
+ console.log(`${RED}Proxy failed: ${err.message}. Skipping Cerebras.${RESET}\n`);
312
+ continue;
313
+ }
314
+ }
315
+ console.log(`${CYAN}Model:${RESET} ${provider.model}`);
316
+ console.log(`${CYAN}Base URL:${RESET} ${provider.baseUrl}`);
317
+ console.log(`${CYAN}API Key:${RESET} ${provider.apiKey.slice(0, 12)}...`);
318
+ console.log();
319
+ // Run 1
320
+ console.log(`${DIM}[${new Date().toISOString()}] Starting query()...${RESET}`);
321
+ const result = await runSdkBenchmark(provider);
322
+ allResults.push(result);
323
+ if (result.error) {
324
+ console.log(`${RED}FAILED: ${result.error}${RESET}\n`);
325
+ }
326
+ else {
327
+ console.log(`${GREEN}Completed in ${(result.totalMs / 1000).toFixed(1)}s${RESET}`);
328
+ console.log(` Init (spawn): ${result.initMs}ms`);
329
+ console.log(` Time to 1st token: ${result.firstTokenMs}ms`);
330
+ console.log(` Total duration: ${(result.totalMs / 1000).toFixed(1)}s`);
331
+ console.log(` Event types: ${result.allEventTypes.join(', ')}`);
332
+ console.log(` Assistant events: ${result.assistantEvents}`);
333
+ console.log(` Tool result events: ${result.toolResultEvents}`);
334
+ console.log(` Write tool calls: ${result.writeToolCalls}`);
335
+ console.log(` Output chars: ${result.outputChars.toLocaleString()}`);
336
+ console.log(` Estimated tokens: ~${result.estimatedTokens.toLocaleString()}`);
337
+ console.log(` Throughput: ${BOLD}${GREEN}${result.tps.toFixed(1)} tokens/sec${RESET}`);
338
+ // Save the output
339
+ const slug = provider.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
340
+ const outputPath = `/tmp/benchmark-${slug}.txt`;
341
+ fs.writeFileSync(outputPath, result.outputText);
342
+ console.log(` Output saved: ${outputPath}`);
343
+ }
344
+ console.log();
345
+ // Stop proxy
346
+ if (provider.needsProxy) {
347
+ stopProxy();
348
+ }
349
+ }
350
+ // ── Comparison ────────────────────────────────────────────────────
351
+ console.log(`${BOLD}══════════════════════════════════════════════════════════════════════${RESET}`);
352
+ console.log(`${BOLD} COMPARISON${RESET}`);
353
+ console.log(`${BOLD}══════════════════════════════════════════════════════════════════════${RESET}\n`);
354
+ const validResults = allResults.filter(r => !r.error);
355
+ for (const r of validResults) {
356
+ console.log(` ${CYAN}${r.provider}:${RESET}`);
357
+ console.log(` Duration: ${(r.totalMs / 1000).toFixed(1)}s`);
358
+ console.log(` Tokens: ~${r.estimatedTokens.toLocaleString()}`);
359
+ console.log(` TPS: ${GREEN}${BOLD}${r.tps.toFixed(1)}${RESET}`);
360
+ console.log(` 1st token: ${(r.firstTokenMs / 1000).toFixed(1)}s`);
361
+ console.log();
362
+ }
363
+ if (validResults.length === 2) {
364
+ const [a, b] = validResults;
365
+ const faster = a.totalMs < b.totalMs ? a : b;
366
+ const slower = a.totalMs < b.totalMs ? b : a;
367
+ const speedup = (slower.totalMs / faster.totalMs).toFixed(1);
368
+ const saved = ((slower.totalMs - faster.totalMs) / 1000).toFixed(1);
369
+ console.log(` ${BOLD}Winner:${RESET} ${GREEN}${faster.provider}${RESET} is ${BOLD}${speedup}x faster${RESET}`);
370
+ console.log(` ${BOLD}Time saved:${RESET} ${saved}s`);
371
+ console.log();
372
+ if (faster.tps > slower.tps) {
373
+ const tpsDiff = (faster.tps / slower.tps).toFixed(1);
374
+ console.log(` ${BOLD}TPS advantage:${RESET} ${faster.provider} is ${tpsDiff}x higher throughput`);
375
+ }
376
+ }
377
+ console.log(`${BOLD}══════════════════════════════════════════════════════════════════════${RESET}\n`);
378
+ }
379
+ process.on('SIGINT', () => { stopProxy(); process.exit(130); });
380
+ process.on('SIGTERM', () => { stopProxy(); process.exit(143); });
381
+ process.on('exit', () => stopProxy());
382
+ main().catch((err) => {
383
+ console.error('Unhandled error:', err);
384
+ stopProxy();
385
+ process.exit(1);
386
+ });
package/dist/cli/index.js CHANGED
@@ -8,6 +8,7 @@ import path from 'path';
8
8
  import os, { networkInterfaces } from 'os';
9
9
  import { Buffer } from 'buffer';
10
10
  import crypto from 'crypto';
11
+ import { stopAllProxies } from './proxyManager.js';
11
12
  import { createProject, deleteProject } from './projectManager.js';
12
13
  import { startSending, startReceiving } from './transferService.js';
13
14
  import { registerGitHubHandlers } from './githubHandlers.js';
@@ -212,7 +213,7 @@ async function connect() {
212
213
  const deviceId = await getDeviceId();
213
214
  return new Promise((resolve, reject) => {
214
215
  const socket = io(config.apiUrl, {
215
- transports: ['websocket', 'polling'],
216
+ transports: ['polling', 'websocket'],
216
217
  });
217
218
  socket.on('connect', () => {
218
219
  socket.emit('register', { type: 'cli', deviceId });
@@ -473,7 +474,7 @@ async function runDaemon(foreground = false, email) {
473
474
  socket = null;
474
475
  }
475
476
  socket = io(config.apiUrl, {
476
- transports: ['websocket', 'polling'],
477
+ transports: ['polling', 'websocket'],
477
478
  reconnection: true,
478
479
  reconnectionDelay: 1000,
479
480
  reconnectionDelayMax: 10000,
@@ -971,6 +972,7 @@ async function runDaemon(foreground = false, email) {
971
972
  }
972
973
  socket.disconnect();
973
974
  }
975
+ stopAllProxies();
974
976
  process.exit(0);
975
977
  };
976
978
  process.on('SIGTERM', shutdown);
@@ -9,6 +9,7 @@ import { z } from 'zod';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
+ import sharp from 'sharp';
12
13
  import { getOpenrouterApiKey, getApiUrl } from './agentSession.js';
13
14
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
14
15
  /** Comprehensive MIME type map for file extension detection */
@@ -51,12 +52,53 @@ function getMimeType(filePath) {
51
52
  const ext = path.extname(filePath).toLowerCase().replace('.', '');
52
53
  return MIME_MAP[ext] || 'application/octet-stream';
53
54
  }
55
+ const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'avif']);
56
+ const MAX_IMAGE_DIMENSION = 2048; // max width or height in pixels
57
+ const MAX_IMAGE_BYTES = 2 * 1024 * 1024; // 2 MB target after compression
58
+ function isImageFile(filePath) {
59
+ const ext = path.extname(filePath).toLowerCase().replace('.', '');
60
+ return IMAGE_EXTENSIONS.has(ext);
61
+ }
62
+ /**
63
+ * Compress and resize an image buffer using sharp.
64
+ * - Resizes so neither dimension exceeds MAX_IMAGE_DIMENSION (fit: inside, no upscale)
65
+ * - Converts to JPEG quality 80 (or WebP for non-photo sources)
66
+ * - If already small enough, returns the original buffer unchanged
67
+ */
68
+ async function compressImage(buf) {
69
+ const metadata = await sharp(buf).metadata();
70
+ const { width = 0, height = 0, size = 0 } = metadata;
71
+ // If already under limits, keep as-is
72
+ const needsResize = width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION;
73
+ const needsCompress = (size || buf.length) > MAX_IMAGE_BYTES;
74
+ if (!needsResize && !needsCompress) {
75
+ // Keep original format
76
+ const fmt = metadata.format || 'jpeg';
77
+ const mime = fmt === 'png' ? 'image/png' : fmt === 'webp' ? 'image/webp' : 'image/jpeg';
78
+ return { data: buf, mime };
79
+ }
80
+ let pipeline = sharp(buf)
81
+ .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, { fit: 'inside', withoutEnlargement: true });
82
+ // Convert to JPEG for best compression on photos; use WebP for PNGs with alpha
83
+ const hasAlpha = metadata.hasAlpha;
84
+ if (hasAlpha) {
85
+ pipeline = pipeline.webp({ quality: 80 });
86
+ return { data: await pipeline.toBuffer(), mime: 'image/webp' };
87
+ }
88
+ pipeline = pipeline.jpeg({ quality: 80 });
89
+ return { data: await pipeline.toBuffer(), mime: 'image/jpeg' };
90
+ }
54
91
  /**
55
- * Convert a file to a data URI (base64 encoded)
92
+ * Convert a file to a data URI (base64 encoded).
93
+ * Images are compressed and resized before encoding.
56
94
  */
57
- function fileToDataUri(filePath) {
95
+ async function fileToDataUri(filePath) {
58
96
  try {
59
97
  const buf = fs.readFileSync(filePath);
98
+ if (isImageFile(filePath)) {
99
+ const { data, mime } = await compressImage(buf);
100
+ return `data:${mime};base64,${data.toString('base64')}`;
101
+ }
60
102
  const mime = getMimeType(filePath);
61
103
  return `data:${mime};base64,${buf.toString('base64')}`;
62
104
  }
@@ -83,7 +125,7 @@ function createAnalyzeImageTool(attachmentDir) {
83
125
  if (!fs.existsSync(imagePath)) {
84
126
  return { content: [{ type: 'text', text: `Error: Image file not found: ${args.image_path}` }], isError: true };
85
127
  }
86
- const dataUri = fileToDataUri(imagePath);
128
+ const dataUri = await fileToDataUri(imagePath);
87
129
  if (!dataUri) {
88
130
  return { content: [{ type: 'text', text: `Error: Could not read image file: ${args.image_path}` }], isError: true };
89
131
  }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Proxy Manager - Manages anthropic-proxy-rs instances
3
+ *
4
+ * Auto-installs anthropic-proxy-rs via cargo if not found.
5
+ * Provides start/stop/reuse of proxy instances for any OpenAI-compatible provider.
6
+ *
7
+ * Usage:
8
+ * import { ensureProxy, stopAllProxies } from './proxyManager.js'
9
+ *
10
+ * const proxyUrl = await ensureProxy({
11
+ * upstreamUrl: 'https://api.cerebras.ai/v1',
12
+ * apiKey: 'csk-...',
13
+ * model: 'zai-glm-4.7',
14
+ * })
15
+ * // proxyUrl = 'http://127.0.0.1:8321' — Anthropic-compatible endpoint
16
+ */
17
+ import { spawn, execSync } from 'child_process';
18
+ import { existsSync } from 'fs';
19
+ import path from 'path';
20
+ import os from 'os';
21
+ import net from 'net';
22
+ const runningProxies = new Map();
23
+ // ── Binary resolution ───────────────────────────────────────────────
24
+ function getProxyBinPath() {
25
+ return path.join(os.homedir(), '.cargo', 'bin', 'anthropic-proxy');
26
+ }
27
+ function isCargoInstalled() {
28
+ try {
29
+ execSync('cargo --version', { stdio: 'pipe', timeout: 5000 });
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ function isProxyInstalled() {
37
+ return existsSync(getProxyBinPath());
38
+ }
39
+ /**
40
+ * Install anthropic-proxy-rs via cargo.
41
+ * Throws if cargo is not available or install fails.
42
+ */
43
+ export async function installProxy() {
44
+ const binPath = getProxyBinPath();
45
+ if (existsSync(binPath))
46
+ return;
47
+ console.log('[proxyManager] anthropic-proxy not found, installing via cargo...');
48
+ if (!isCargoInstalled()) {
49
+ console.log('[proxyManager] Installing Rust toolchain...');
50
+ execSync("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y", {
51
+ stdio: 'pipe',
52
+ timeout: 120_000,
53
+ env: { ...process.env },
54
+ });
55
+ }
56
+ const cargoBin = path.join(os.homedir(), '.cargo', 'bin', 'cargo');
57
+ const cargoCmd = existsSync(cargoBin) ? cargoBin : 'cargo';
58
+ execSync(`${cargoCmd} install --git https://github.com/m0n0x41d/anthropic-proxy-rs --locked`, {
59
+ stdio: 'pipe',
60
+ timeout: 300_000,
61
+ env: {
62
+ ...process.env,
63
+ PATH: `${path.join(os.homedir(), '.cargo', 'bin')}:${process.env.PATH || ''}`,
64
+ },
65
+ });
66
+ if (!existsSync(binPath)) {
67
+ throw new Error('anthropic-proxy installation completed but binary not found at ' + binPath);
68
+ }
69
+ console.log('[proxyManager] anthropic-proxy installed successfully');
70
+ }
71
+ // ── Port finding ────────────────────────────────────────────────────
72
+ function findFreePort(start, end) {
73
+ return new Promise((resolve, reject) => {
74
+ function tryPort(port) {
75
+ if (port > end) {
76
+ reject(new Error(`No free port in range ${start}-${end}`));
77
+ return;
78
+ }
79
+ const server = net.createServer();
80
+ server.listen(port, '127.0.0.1', () => {
81
+ const addr = server.address();
82
+ server.close(() => resolve(typeof addr === 'object' && addr ? addr.port : port));
83
+ });
84
+ server.on('error', () => tryPort(port + 1));
85
+ }
86
+ tryPort(start);
87
+ });
88
+ }
89
+ // ── Wait for proxy to be ready ──────────────────────────────────────
90
+ function waitForProxy(port, timeoutMs) {
91
+ return new Promise((resolve, reject) => {
92
+ const start = Date.now();
93
+ function check() {
94
+ const socket = new net.Socket();
95
+ socket.setTimeout(500);
96
+ socket.on('connect', () => {
97
+ socket.destroy();
98
+ resolve();
99
+ });
100
+ socket.on('error', () => {
101
+ socket.destroy();
102
+ if (Date.now() - start > timeoutMs) {
103
+ reject(new Error(`Proxy on port ${port} did not become ready within ${timeoutMs}ms`));
104
+ }
105
+ else {
106
+ setTimeout(check, 100);
107
+ }
108
+ });
109
+ socket.on('timeout', () => {
110
+ socket.destroy();
111
+ if (Date.now() - start > timeoutMs) {
112
+ reject(new Error(`Proxy on port ${port} did not become ready within ${timeoutMs}ms`));
113
+ }
114
+ else {
115
+ setTimeout(check, 100);
116
+ }
117
+ });
118
+ socket.connect(port, '127.0.0.1');
119
+ }
120
+ check();
121
+ });
122
+ }
123
+ // ── Start / get proxy ───────────────────────────────────────────────
124
+ function proxyKey(config) {
125
+ return `${config.upstreamUrl}::${config.model}`;
126
+ }
127
+ /**
128
+ * Ensure a proxy is running for the given config.
129
+ * Reuses existing proxy if already running for the same upstream+model.
130
+ * Auto-installs anthropic-proxy-rs if not found.
131
+ *
132
+ * Returns the local proxy URL (e.g. http://127.0.0.1:8321).
133
+ */
134
+ export async function ensureProxy(config) {
135
+ const key = proxyKey(config);
136
+ // Reuse existing
137
+ const existing = runningProxies.get(key);
138
+ if (existing && !existing.process.killed) {
139
+ return existing.url;
140
+ }
141
+ // Auto-install if needed
142
+ await installProxy();
143
+ const portStart = config.portStart ?? 8321;
144
+ const portEnd = config.portEnd ?? 8340;
145
+ const port = await findFreePort(portStart, portEnd);
146
+ const binPath = getProxyBinPath();
147
+ const env = {};
148
+ for (const [k, v] of Object.entries(process.env)) {
149
+ if (v !== undefined)
150
+ env[k] = v;
151
+ }
152
+ env.UPSTREAM_BASE_URL = config.upstreamUrl;
153
+ env.UPSTREAM_API_KEY = config.apiKey;
154
+ env.PORT = String(port);
155
+ env.COMPLETION_MODEL = config.model;
156
+ env.REASONING_MODEL = config.model;
157
+ env.PATH = `${path.join(os.homedir(), '.cargo', 'bin')}:${env.PATH || ''}`;
158
+ const child = spawn(binPath, [], {
159
+ env,
160
+ stdio: ['ignore', 'pipe', 'pipe'],
161
+ detached: false,
162
+ });
163
+ // Log proxy stderr for debugging
164
+ child.stderr?.on('data', (data) => {
165
+ const msg = data.toString().trim();
166
+ if (msg.includes('ERROR') || msg.includes('WARN')) {
167
+ console.error(`[proxyManager:${port}] ${msg}`);
168
+ }
169
+ });
170
+ child.on('exit', (code) => {
171
+ runningProxies.delete(key);
172
+ if (code && code !== 0) {
173
+ console.error(`[proxyManager] Proxy exited with code ${code}`);
174
+ }
175
+ });
176
+ // Wait for proxy to be listening
177
+ await waitForProxy(port, 10_000);
178
+ const url = `http://127.0.0.1:${port}`;
179
+ runningProxies.set(key, { process: child, port, url, key });
180
+ console.log(`[proxyManager] Started proxy on port ${port} -> ${config.upstreamUrl} (model: ${config.model})`);
181
+ return url;
182
+ }
183
+ /**
184
+ * Stop a specific proxy by config.
185
+ */
186
+ export function stopProxy(config) {
187
+ const key = proxyKey(config);
188
+ const proxy = runningProxies.get(key);
189
+ if (proxy) {
190
+ try {
191
+ proxy.process.kill('SIGTERM');
192
+ }
193
+ catch { }
194
+ runningProxies.delete(key);
195
+ }
196
+ }
197
+ /**
198
+ * Stop all running proxies.
199
+ */
200
+ export function stopAllProxies() {
201
+ for (const [, proxy] of runningProxies) {
202
+ try {
203
+ proxy.process.kill('SIGTERM');
204
+ }
205
+ catch { }
206
+ }
207
+ runningProxies.clear();
208
+ }
209
+ /**
210
+ * Check if anthropic-proxy-rs is installed.
211
+ */
212
+ export function isInstalled() {
213
+ return isProxyInstalled();
214
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types for embeddable script feature
3
+ * Allows websites to embed a floating AI button connected to TalkToCode sessions
4
+ */
5
+ export {};
@@ -33,3 +33,5 @@ export function isWebFetchOutput(r) {
33
33
  export function isAgentOutput(r) {
34
34
  return typeof r === 'object' && r !== null && 'agentId' in r && 'status' in r && 'prompt' in r;
35
35
  }
36
+ // ============ Embed Types ============
37
+ export * from './types/embed.js';
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.57",
3
+ "version": "1.0.59",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,12 +44,14 @@
44
44
  "fastify": "^5.7.2",
45
45
  "node-fetch": "^3.3.2",
46
46
  "pino-pretty": "^11.0.0",
47
+ "sharp": "^0.34.5",
47
48
  "socket.io-client": "^4.8.1",
48
49
  "uuid": "^11.0.3"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/chokidar": "^2.1.3",
52
53
  "@types/node": "^22.10.2",
54
+ "@types/sharp": "^0.31.1",
53
55
  "@types/uuid": "^10.0.0",
54
56
  "@vercel/ncc": "^0.38.1",
55
57
  "esbuild": "^0.27.2",