claude-connect 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Claude Connect
2
2
 
3
- > Conecta `Claude Code` con `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `OpenRouter` y `Qwen` desde una interfaz de consola clara, rápida y reversible.
3
+ > Conecta `Claude Code` con `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` desde una interfaz de consola clara, rápida y reversible.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/claude-connect?style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/claude-connect)
6
6
  [![node](https://img.shields.io/badge/node-%3E%3D22-2f7d32?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/badge/license-MIT-0f172a?style=for-the-badge)](./LICENSE)
8
- [![providers](https://img.shields.io/badge/providers-OpenCode%20Go%20%7C%20Zen%20%7C%20Kimi%20%7C%20DeepSeek%20%7C%20Ollama%20%7C%20OpenAI%20%7C%20OpenRouter%20%7C%20Qwen-0ea5e9?style=for-the-badge)](https://www.npmjs.com/package/claude-connect)
8
+ [![providers](https://img.shields.io/badge/providers-OpenCode%20Go%20%7C%20Zen%20%7C%20Kimi%20%7C%20DeepSeek%20%7C%20Ollama%20%7C%20OpenAI%20%7C%20Inception%20Labs%20%7C%20OpenRouter%20%7C%20Qwen-0ea5e9?style=for-the-badge)](https://www.npmjs.com/package/claude-connect)
9
9
 
10
10
  ## Why Claude Connect
11
11
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  ### Highlights
15
15
 
16
- - `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `OpenRouter` y `Qwen` listos desde el primer arranque
16
+ - `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` listos desde el primer arranque
17
17
  - soporte para `Token` y `OAuth` cuando el proveedor lo permite
18
18
  - API keys compartidas por proveedor para no repetir el mismo token en cada modelo
19
19
  - activación reversible sobre la instalación real de `Claude Code`
@@ -80,8 +80,10 @@ Al activar:
80
80
  - `DeepSeek` apunta a `https://api.deepseek.com/anthropic`
81
81
  - `Ollama` pide una URL local o remota, valida `/api/tags` y usa el gateway local sobre `.../api/chat`
82
82
  - `OpenAI` usa el gateway local sobre `https://api.openai.com/v1/chat/completions`
83
+ - `Inception Labs` usa el gateway local sobre `https://api.inceptionlabs.ai/v1/chat/completions`
83
84
  - `OpenRouter` usa `openrouter/free` por gateway sobre `https://openrouter.ai/api/v1`
84
85
  - `Qwen` apunta al gateway local `http://127.0.0.1:4310/anthropic`
86
+ - para algunos modelos con limites conocidos, el gateway ahora ajusta `max_tokens` y bloquea prompts sobredimensionados antes de que el upstream devuelva errores opacos
85
87
 
86
88
  ## Providers
87
89
 
@@ -93,6 +95,7 @@ Al activar:
93
95
  | `DeepSeek` | `deepseek-chat`, `deepseek-reasoner` | `Token` | Directa |
94
96
  | `Ollama` | modelos descubiertos desde tu servidor | `Servidor Ollama` | Gateway local |
95
97
  | `OpenAI` | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini` | `Token` | Gateway local |
98
+ | `Inception Labs` | `mercury-2` | `Token` | Gateway local |
96
99
  | `OpenRouter` | `openrouter/free` | `Token` | Gateway local |
97
100
  | `Qwen` | `qwen3-coder-plus` | `OAuth`, `Token` | Gateway local |
98
101
 
@@ -118,6 +121,25 @@ Nota sobre `OpenAI`:
118
121
  - https://platform.openai.com/docs/api-reference/authentication
119
122
  - https://developers.openai.com/api/docs/models
120
123
 
124
+ Nota sobre `Inception Labs`:
125
+
126
+ - esta primera integracion expone solo `mercury-2`, que es el modelo chat-compatible oficial en `v1/chat/completions`
127
+ - `mercury-2` se trata como modelo solo texto en Claude Connect; si envias una imagen, la app ahora corta la peticion con un mensaje claro
128
+ - Claude Connect aplica presupuesto preventivo de contexto para `mercury-2` usando ventana `128K` y salida maxima `16,384`
129
+ - `Mercury Edit 2` no se publica todavia en Claude Connect porque usa endpoints `fim/edit` que no encajan con Claude Code en esta arquitectura
130
+ - autenticacion soportada: `API key`
131
+ - referencias oficiales:
132
+ - https://docs.inceptionlabs.ai/get-started/get-started
133
+ - https://docs.inceptionlabs.ai/get-started/authentication
134
+ - https://docs.inceptionlabs.ai/get-started/models
135
+
136
+ Nota sobre `DeepSeek`:
137
+
138
+ - Claude Connect aplica presupuesto preventivo de contexto para `deepseek-chat` y `deepseek-reasoner`
139
+ - referencias oficiales:
140
+ - https://api-docs.deepseek.com/quick_start/pricing/
141
+ - https://api-docs.deepseek.com/guides/reasoning_model
142
+
121
143
  Nota sobre `Ollama`:
122
144
 
123
145
  - la URL del servidor se define al crear la conexión
@@ -151,14 +173,16 @@ Ahí viven:
151
173
  El catálogo SQLite local se genera automáticamente en:
152
174
 
153
175
  ```text
154
- storage/claude-connect.sqlite
176
+ Linux: ~/.claude-connect/storage/claude-connect.sqlite
177
+ Windows: %APPDATA%\claude-connect\storage\claude-connect.sqlite
155
178
  ```
156
179
 
157
180
  Importante:
158
181
 
159
182
  - esa base ya no se versiona en git
160
183
  - el catálogo se siembra desde `src/data/catalog-store.js`
161
- - esto evita conflictos molestos al hacer `git pull`
184
+ - ya no se crea en la carpeta donde ejecutas el comando
185
+ - esto evita conflictos molestos al hacer `git pull` y carpetas `storage/` accidentales en proyectos ajenos
162
186
 
163
187
  ## Claude Code Switching
164
188
 
@@ -175,6 +199,7 @@ Eso permite:
175
199
  - activar otro proveedor sin tocar archivos manualmente
176
200
  - evitar el `Auth conflict` entre sesión `claude.ai` y `API key`
177
201
  - volver a tu estado original con `Revertir Claude`
202
+ - bloquear la activación si `Claude Code` no está realmente instalado todavía
178
203
 
179
204
  ## Qwen OAuth
180
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-connect",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "CLI para configurar Claude Code con proveedores de modelos externos",
5
5
  "author": "wmcarlosv",
6
6
  "type": "module",
@@ -1,8 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
+ import { resolveClaudeConnectHomeSync } from '../lib/app-paths.js';
4
5
 
5
- export const defaultCatalogDbPath = path.join(process.cwd(), 'storage', 'claude-connect.sqlite');
6
+ export function getDefaultCatalogDbPath(options = {}) {
7
+ const pathModule = options.platform === 'win32' ? path.win32 : path.posix;
8
+ return pathModule.join(resolveClaudeConnectHomeSync(options), 'storage', 'claude-connect.sqlite');
9
+ }
10
+
11
+ export const defaultCatalogDbPath = getDefaultCatalogDbPath();
6
12
 
7
13
  const schemaSql = `
8
14
  PRAGMA foreign_keys = ON;
@@ -33,6 +39,7 @@ CREATE TABLE IF NOT EXISTS models (
33
39
  api_base_url TEXT,
34
40
  api_path TEXT,
35
41
  auth_env_mode TEXT NOT NULL DEFAULT 'auth_token',
42
+ supports_vision INTEGER NOT NULL DEFAULT 1,
36
43
  sort_order INTEGER NOT NULL DEFAULT 0,
37
44
  is_default INTEGER NOT NULL DEFAULT 0
38
45
  );
@@ -673,6 +680,46 @@ const seedProviders = [
673
680
  }
674
681
  ]
675
682
  },
683
+ {
684
+ id: 'inception',
685
+ name: 'Inception Labs',
686
+ vendor: 'Inception Labs',
687
+ description: 'Inception Platform con Mercury 2 sobre un endpoint OpenAI-compatible. Claude Code se conecta a traves del gateway local para mantener compatibilidad Anthropic y herramientas.',
688
+ docsUrl: 'https://docs.inceptionlabs.ai/get-started/get-started',
689
+ docsVerifiedAt: '2026-04-03',
690
+ baseUrl: 'https://api.inceptionlabs.ai/v1',
691
+ defaultModelId: 'mercury-2',
692
+ defaultAuthMethodId: 'token',
693
+ defaultApiKeyEnvVar: 'INCEPTION_API_KEY',
694
+ models: [
695
+ {
696
+ id: 'mercury-2',
697
+ name: 'Mercury 2',
698
+ category: 'OpenAI Chat Completions',
699
+ contextWindow: '128K',
700
+ summary: 'Modelo generalista y de razonamiento de Inception Labs expuesto por v1/chat/completions.',
701
+ upstreamModelId: 'mercury-2',
702
+ transportMode: 'gateway',
703
+ apiStyle: 'openai-chat',
704
+ apiBaseUrl: 'https://api.inceptionlabs.ai/v1',
705
+ apiPath: '/chat/completions',
706
+ authEnvMode: 'auth_token',
707
+ supportsVision: false,
708
+ sortOrder: 1,
709
+ isDefault: 1
710
+ }
711
+ ],
712
+ authMethods: [
713
+ {
714
+ id: 'token',
715
+ name: 'Token',
716
+ description: 'Conexion por API key de Inception Labs.',
717
+ credentialKind: 'env_var',
718
+ sortOrder: 1,
719
+ isDefault: 1
720
+ }
721
+ ]
722
+ },
676
723
  {
677
724
  id: 'openrouter',
678
725
  name: 'OpenRouter',
@@ -793,10 +840,10 @@ function seedCatalog(db) {
793
840
  INSERT INTO models (
794
841
  id, provider_id, name, category, context_window, summary,
795
842
  upstream_model_id,
796
- transport_mode, api_style, api_base_url, api_path, auth_env_mode,
843
+ transport_mode, api_style, api_base_url, api_path, auth_env_mode, supports_vision,
797
844
  sort_order, is_default
798
845
  )
799
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
846
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
800
847
  ON CONFLICT(id) DO UPDATE SET
801
848
  provider_id = excluded.provider_id,
802
849
  name = excluded.name,
@@ -809,6 +856,7 @@ function seedCatalog(db) {
809
856
  api_base_url = excluded.api_base_url,
810
857
  api_path = excluded.api_path,
811
858
  auth_env_mode = excluded.auth_env_mode,
859
+ supports_vision = excluded.supports_vision,
812
860
  sort_order = excluded.sort_order,
813
861
  is_default = excluded.is_default
814
862
  `);
@@ -879,6 +927,7 @@ function seedCatalog(db) {
879
927
  model.apiBaseUrl ?? null,
880
928
  model.apiPath ?? null,
881
929
  model.authEnvMode ?? 'auth_token',
930
+ model.supportsVision === false ? 0 : 1,
882
931
  model.sortOrder,
883
932
  model.isDefault
884
933
  );
@@ -950,6 +999,10 @@ function ensureSchemaMigrations(db) {
950
999
  alterStatements.push(`ALTER TABLE models ADD COLUMN auth_env_mode TEXT NOT NULL DEFAULT 'auth_token'`);
951
1000
  }
952
1001
 
1002
+ if (!modelColumns.has('supports_vision')) {
1003
+ alterStatements.push(`ALTER TABLE models ADD COLUMN supports_vision INTEGER NOT NULL DEFAULT 1`);
1004
+ }
1005
+
953
1006
  if (!modelColumns.has('upstream_model_id')) {
954
1007
  alterStatements.push(`ALTER TABLE models ADD COLUMN upstream_model_id TEXT`);
955
1008
  }
@@ -992,6 +1045,7 @@ function mapModelRow(row) {
992
1045
  apiBaseUrl: row.api_base_url,
993
1046
  apiPath: row.api_path,
994
1047
  authEnvMode: row.auth_env_mode,
1048
+ supportsVision: Boolean(row.supports_vision),
995
1049
  sortOrder: Number(row.sort_order),
996
1050
  isDefault: Boolean(row.is_default)
997
1051
  };
@@ -1021,7 +1075,7 @@ function mapOAuthRow(row) {
1021
1075
  };
1022
1076
  }
1023
1077
 
1024
- export function createCatalogStore({ filename = defaultCatalogDbPath } = {}) {
1078
+ export function createCatalogStore({ filename = getDefaultCatalogDbPath() } = {}) {
1025
1079
  if (filename !== ':memory:') {
1026
1080
  fs.mkdirSync(path.dirname(filename), { recursive: true });
1027
1081
  }
@@ -24,6 +24,7 @@ import {
24
24
  } from './state.js';
25
25
  import { resolveClaudeConnectPaths } from '../lib/app-paths.js';
26
26
  import { readSwitchState } from '../lib/claude-settings.js';
27
+ import { enforceModelTokenBudget } from '../lib/model-budget.js';
27
28
  import { readOAuthToken, refreshOAuthToken } from '../lib/oauth.js';
28
29
  import { readProfileFile } from '../lib/profile.js';
29
30
  import { readManagedProviderTokenSecret, readManagedTokenSecret } from '../lib/secrets.js';
@@ -161,6 +162,48 @@ function getUpstreamModelId(profile) {
161
162
  return profile?.model?.upstreamModelId ?? profile?.model?.id ?? 'unknown';
162
163
  }
163
164
 
165
+ function requestContainsImageInput(body) {
166
+ return Array.isArray(body?.messages)
167
+ && body.messages.some((messageItem) => Array.isArray(messageItem?.content)
168
+ && messageItem.content.some((part) => part?.type === 'image'));
169
+ }
170
+
171
+ function profileSupportsImageInput(profile) {
172
+ if (typeof profile?.model?.supportsVision === 'boolean') {
173
+ return profile.model.supportsVision;
174
+ }
175
+
176
+ if (profile?.provider?.id === 'inception') {
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ }
182
+
183
+ function stringifyUpstreamMessage(value) {
184
+ if (typeof value === 'string') {
185
+ return value;
186
+ }
187
+
188
+ if (value == null) {
189
+ return '';
190
+ }
191
+
192
+ if (typeof value === 'object') {
193
+ if ('message' in value && typeof value.message === 'string') {
194
+ return value.message;
195
+ }
196
+
197
+ try {
198
+ return JSON.stringify(value);
199
+ } catch (_error) {
200
+ return String(value);
201
+ }
202
+ }
203
+
204
+ return String(value);
205
+ }
206
+
164
207
  function resolveGatewayUpstreamConfig(profile) {
165
208
  if (profile?.provider?.id === 'ollama') {
166
209
  return {
@@ -335,9 +378,9 @@ async function forwardUpstreamRequest({ targetUrl, headers, payload, context, re
335
378
  });
336
379
  }
337
380
 
338
- const message = responsePayload?.error?.message
339
- || responsePayload?.message
340
- || responsePayload?.error
381
+ const message = stringifyUpstreamMessage(responsePayload?.error?.message)
382
+ || stringifyUpstreamMessage(responsePayload?.message)
383
+ || stringifyUpstreamMessage(responsePayload?.error)
341
384
  || `HTTP ${response.status}`;
342
385
  const providerName = context?.profile?.provider?.name ?? context?.profile?.provider?.id ?? 'El proveedor';
343
386
  const containsImageInput = Array.isArray(payload?.messages)
@@ -453,9 +496,20 @@ async function handleCountTokens(request, response) {
453
496
  }
454
497
 
455
498
  async function handleMessages(request, response) {
456
- const body = await readJsonBody(request);
499
+ const rawBody = await readJsonBody(request);
457
500
  const context = await resolveGatewayContext();
458
501
 
502
+ if (requestContainsImageInput(rawBody) && !profileSupportsImageInput(context.profile)) {
503
+ const providerName = context.profile.provider.name;
504
+ const modelName = context.profile.model.name;
505
+ throw new Error(`${providerName} no admite imagenes con el modelo ${modelName} en esta integracion. Usa un proveedor o modelo con soporte visual.`);
506
+ }
507
+
508
+ const body = enforceModelTokenBudget({
509
+ body: rawBody,
510
+ profile: context.profile
511
+ });
512
+
459
513
  if (context.upstreamApiStyle === 'anthropic') {
460
514
  const upstreamResponse = await forwardAnthropicMessage({
461
515
  requestBody: body,
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import process from 'node:process';
@@ -48,6 +49,14 @@ async function pathExists(targetPath) {
48
49
  }
49
50
  }
50
51
 
52
+ function pathExistsSync(targetPath) {
53
+ try {
54
+ return fsSync.existsSync(targetPath);
55
+ } catch (_error) {
56
+ return false;
57
+ }
58
+ }
59
+
51
60
  function defaultHomedir(env, fallbackHomedir) {
52
61
  const pathModule = getPathModule(process.platform);
53
62
 
@@ -179,6 +188,18 @@ export async function resolveClaudeConnectHome(options = {}) {
179
188
  return candidates[0];
180
189
  }
181
190
 
191
+ export function resolveClaudeConnectHomeSync(options = {}) {
192
+ const candidates = buildClaudeConnectHomeCandidates(options);
193
+
194
+ for (const candidate of candidates) {
195
+ if (pathExistsSync(candidate)) {
196
+ return candidate;
197
+ }
198
+ }
199
+
200
+ return candidates[0];
201
+ }
202
+
182
203
  export async function resolveClaudeSettingsPath(options = {}) {
183
204
  if (typeof options.env?.CLAUDE_SETTINGS_PATH === 'string' && options.env.CLAUDE_SETTINGS_PATH.trim().length > 0) {
184
205
  return buildClaudeSettingsPathCandidates(options)[0];
@@ -244,6 +265,8 @@ export async function resolveClaudeConnectPaths(options = {}) {
244
265
 
245
266
  return {
246
267
  claudeConnectHome,
268
+ storageDir: path.join(claudeConnectHome, 'storage'),
269
+ catalogDbPath: path.join(claudeConnectHome, 'storage', 'claude-connect.sqlite'),
247
270
  profilesDir: path.join(claudeConnectHome, 'profiles'),
248
271
  tokensDir: path.join(claudeConnectHome, 'tokens'),
249
272
  secretsDir: path.join(claudeConnectHome, 'secrets'),
@@ -269,3 +292,90 @@ export async function resolveClaudePaths(options = {}) {
269
292
  ...claudeConnectPaths
270
293
  };
271
294
  }
295
+
296
+ function buildExecutableNames(command, platform = process.platform, env = process.env) {
297
+ if (platform !== 'win32') {
298
+ return [command];
299
+ }
300
+
301
+ const pathext = typeof env.PATHEXT === 'string' && env.PATHEXT.length > 0
302
+ ? env.PATHEXT.split(';').filter(Boolean)
303
+ : ['.EXE', '.CMD', '.BAT', '.COM'];
304
+ const hasExt = path.win32.extname(command).length > 0;
305
+
306
+ if (hasExt) {
307
+ return [command];
308
+ }
309
+
310
+ return pathext.map((ext) => `${command}${ext.toLowerCase()}`);
311
+ }
312
+
313
+ export async function findExecutableOnPath(command, {
314
+ platform = process.platform,
315
+ env = process.env
316
+ } = {}) {
317
+ const pathModule = getPathModule(platform);
318
+ const pathValue = typeof env.PATH === 'string' ? env.PATH : '';
319
+ const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
320
+ const commandNames = buildExecutableNames(command, platform, env);
321
+
322
+ for (const directory of pathEntries) {
323
+ for (const commandName of commandNames) {
324
+ const candidate = pathModule.join(directory, commandName);
325
+
326
+ if (await pathExists(candidate)) {
327
+ return candidate;
328
+ }
329
+ }
330
+ }
331
+
332
+ return null;
333
+ }
334
+
335
+ export async function detectClaudeCodeInstallation(options = {}) {
336
+ const settingsCandidates = buildClaudeSettingsPathCandidates(options);
337
+ const accountCandidates = buildClaudeAccountPathCandidates(options);
338
+ const credentialsCandidates = buildClaudeCredentialsPathCandidates(options);
339
+ const executablePath = await findExecutableOnPath('claude', options);
340
+
341
+ const [existingSettingsPath, existingAccountPath, existingCredentialsPath] = await Promise.all([
342
+ (async () => {
343
+ for (const candidate of settingsCandidates) {
344
+ if (await pathExists(candidate)) {
345
+ return candidate;
346
+ }
347
+ }
348
+
349
+ return null;
350
+ })(),
351
+ (async () => {
352
+ for (const candidate of accountCandidates) {
353
+ if (await pathExists(candidate)) {
354
+ return candidate;
355
+ }
356
+ }
357
+
358
+ return null;
359
+ })(),
360
+ (async () => {
361
+ for (const candidate of credentialsCandidates) {
362
+ if (await pathExists(candidate)) {
363
+ return candidate;
364
+ }
365
+ }
366
+
367
+ return null;
368
+ })()
369
+ ]);
370
+
371
+ return {
372
+ isInstalled: Boolean(executablePath || existingSettingsPath || existingAccountPath || existingCredentialsPath),
373
+ executablePath,
374
+ existingSettingsPath,
375
+ existingAccountPath,
376
+ existingCredentialsPath,
377
+ settingsCandidates,
378
+ accountCandidates,
379
+ credentialsCandidates
380
+ };
381
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { resolveClaudePaths } from './app-paths.js';
3
+ import { detectClaudeCodeInstallation, resolveClaudePaths } from './app-paths.js';
4
4
  import { readManagedProviderTokenSecret, readManagedTokenSecret } from './secrets.js';
5
5
 
6
6
  function isObject(value) {
@@ -240,6 +240,14 @@ export function buildClaudeSettingsForProfile({
240
240
  }
241
241
 
242
242
  export async function activateClaudeProfile({ profile, gatewayBaseUrl = 'http://127.0.0.1:4310/anthropic' }) {
243
+ const installation = await detectClaudeCodeInstallation();
244
+
245
+ if (!installation.isInstalled) {
246
+ throw new Error(
247
+ 'Claude Code no parece estar instalado en esta maquina. Instala o ejecuta Claude Code primero y luego vuelve a activar la conexion.'
248
+ );
249
+ }
250
+
243
251
  const {
244
252
  claudeSettingsPath,
245
253
  claudeAccountPath,
@@ -0,0 +1,73 @@
1
+ import { estimateTokenCountFromAnthropicRequest } from '../gateway/messages.js';
2
+
3
+ function getModelIdentity(profile) {
4
+ return profile?.model?.upstreamModelId ?? profile?.model?.id ?? '';
5
+ }
6
+
7
+ export function getModelTokenLimits(profile) {
8
+ const providerId = profile?.provider?.id;
9
+ const modelId = getModelIdentity(profile);
10
+
11
+ if (providerId === 'inception' && modelId === 'mercury-2') {
12
+ return {
13
+ contextWindowTokens: 128_000,
14
+ defaultOutputTokens: 8_192,
15
+ maxOutputTokens: 16_384
16
+ };
17
+ }
18
+
19
+ if (providerId === 'deepseek' && modelId === 'deepseek-chat') {
20
+ return {
21
+ contextWindowTokens: 128_000,
22
+ defaultOutputTokens: 4_000,
23
+ maxOutputTokens: 8_000
24
+ };
25
+ }
26
+
27
+ if (providerId === 'deepseek' && modelId === 'deepseek-reasoner') {
28
+ return {
29
+ contextWindowTokens: 128_000,
30
+ defaultOutputTokens: 32_000,
31
+ maxOutputTokens: 64_000
32
+ };
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export function enforceModelTokenBudget({ body, profile, safetyMarginTokens = 1024 }) {
39
+ const limits = getModelTokenLimits(profile);
40
+
41
+ if (!limits) {
42
+ return body;
43
+ }
44
+
45
+ const estimatedInputTokens = estimateTokenCountFromAnthropicRequest(body);
46
+ const availableForOutput = limits.contextWindowTokens - estimatedInputTokens - safetyMarginTokens;
47
+
48
+ if (availableForOutput <= 0) {
49
+ throw new Error(
50
+ `La conversacion actual excede el contexto aproximado de ${limits.contextWindowTokens.toLocaleString('en-US')} tokens para ${profile.model.name}. Usa /compact o /clear antes de continuar.`
51
+ );
52
+ }
53
+
54
+ const requestedOutputTokens = typeof body?.max_tokens === 'number'
55
+ ? body.max_tokens
56
+ : limits.defaultOutputTokens;
57
+ const clampedOutputTokens = Math.min(
58
+ requestedOutputTokens,
59
+ limits.maxOutputTokens,
60
+ availableForOutput
61
+ );
62
+
63
+ if (clampedOutputTokens < 256) {
64
+ throw new Error(
65
+ `Queda muy poco margen de salida para ${profile.model.name} dentro de su contexto aproximado de ${limits.contextWindowTokens.toLocaleString('en-US')} tokens. Usa /compact o /clear antes de continuar.`
66
+ );
67
+ }
68
+
69
+ return {
70
+ ...body,
71
+ max_tokens: clampedOutputTokens
72
+ };
73
+ }
@@ -32,7 +32,8 @@ export function buildProfile({ provider, model, authMethod, profileName, apiKeyE
32
32
  apiStyle: model.apiStyle,
33
33
  apiBaseUrl: model.apiBaseUrl,
34
34
  apiPath: model.apiPath,
35
- authEnvMode: model.authEnvMode
35
+ authEnvMode: model.authEnvMode,
36
+ supportsVision: model.supportsVision ?? true
36
37
  },
37
38
  auth: {
38
39
  method: authMethod.id
@@ -19,7 +19,26 @@ export function openAppScreen() {
19
19
  }
20
20
 
21
21
  export function closeAppScreen() {
22
- process.stdout.write('\x1b[?25h\x1b[?1049l');
22
+ process.stdout.write('\x1b[?25h\x1b[?1049l\r\n');
23
+ }
24
+
25
+ function beginKeyboardCapture() {
26
+ readline.emitKeypressEvents(process.stdin);
27
+ process.stdin.resume();
28
+ process.stdin.setRawMode(true);
29
+ }
30
+
31
+ function restoreKeyboardState(onKeypress) {
32
+ if (typeof onKeypress === 'function') {
33
+ process.stdin.removeListener('keypress', onKeypress);
34
+ }
35
+
36
+ if (process.stdin.isTTY && process.stdin.isRaw) {
37
+ process.stdin.setRawMode(false);
38
+ }
39
+
40
+ process.stdin.pause();
41
+ process.stdout.write('\x1b[?25h');
23
42
  }
24
43
 
25
44
  export function clearScreen() {
@@ -64,13 +83,11 @@ export function buildFrame({ eyebrow, title, subtitle, body = [], footer = [] })
64
83
 
65
84
  export function waitForAnyKey(message = 'Presiona una tecla para continuar.') {
66
85
  return new Promise((resolve, reject) => {
67
- readline.emitKeypressEvents(process.stdin);
68
- process.stdin.setRawMode(true);
86
+ beginKeyboardCapture();
69
87
  let escapePending = false;
70
88
 
71
89
  const cleanup = () => {
72
- process.stdin.removeListener('keypress', onKeypress);
73
- process.stdin.setRawMode(false);
90
+ restoreKeyboardState(onKeypress);
74
91
  };
75
92
 
76
93
  const onKeypress = (_input, key = {}) => {
@@ -124,12 +141,10 @@ export function selectFromList({
124
141
  let selectedIndex = 0;
125
142
  let escapePending = false;
126
143
 
127
- readline.emitKeypressEvents(process.stdin);
128
- process.stdin.setRawMode(true);
144
+ beginKeyboardCapture();
129
145
 
130
146
  const cleanup = () => {
131
- process.stdin.removeListener('keypress', onKeypress);
132
- process.stdin.setRawMode(false);
147
+ restoreKeyboardState(onKeypress);
133
148
  };
134
149
 
135
150
  const render = () => {
@@ -247,12 +262,10 @@ export function promptText({
247
262
  let value = '';
248
263
  let escapePending = false;
249
264
 
250
- readline.emitKeypressEvents(process.stdin);
251
- process.stdin.setRawMode(true);
265
+ beginKeyboardCapture();
252
266
 
253
267
  const cleanup = () => {
254
- process.stdin.removeListener('keypress', onKeypress);
255
- process.stdin.setRawMode(false);
268
+ restoreKeyboardState(onKeypress);
256
269
  };
257
270
 
258
271
  const render = () => {
package/src/wizard.js CHANGED
@@ -433,7 +433,22 @@ async function activateClaudeFromSavedProfile() {
433
433
  return profile;
434
434
  }
435
435
 
436
- const result = await activateClaudeProfile({ profile });
436
+ let result;
437
+
438
+ try {
439
+ result = await activateClaudeProfile({ profile });
440
+ } catch (error) {
441
+ renderInfoScreen({
442
+ title: 'No se pudo activar Claude',
443
+ subtitle: 'Claude Connect no pudo aplicar la conexion en Claude Code.',
444
+ lines: [
445
+ colorize(error instanceof Error ? error.message : String(error), colors.warning)
446
+ ],
447
+ footer: 'Presiona una tecla para volver'
448
+ });
449
+ return await waitForAnyKey();
450
+ }
451
+
437
452
  const gateway = result.connectionMode === 'gateway'
438
453
  ? await restartGatewayInBackground()
439
454
  : await stopGateway();