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 +30 -5
- package/package.json +1 -1
- package/src/data/catalog-store.js +58 -4
- package/src/gateway/server.js +58 -4
- package/src/lib/app-paths.js +110 -0
- package/src/lib/claude-settings.js +9 -1
- package/src/lib/model-budget.js +73 -0
- package/src/lib/profile.js +2 -1
- package/src/lib/terminal.js +26 -13
- package/src/wizard.js +16 -1
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
|
[](https://www.npmjs.com/package/claude-connect)
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
7
|
[](./LICENSE)
|
|
8
|
-
[](https://www.npmjs.com/package/claude-connect)
|
|
8
|
+
[](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
|
-
-
|
|
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,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
|
|
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 =
|
|
1078
|
+
export function createCatalogStore({ filename = getDefaultCatalogDbPath() } = {}) {
|
|
1025
1079
|
if (filename !== ':memory:') {
|
|
1026
1080
|
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
1027
1081
|
}
|
package/src/gateway/server.js
CHANGED
|
@@ -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
|
|
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,
|
package/src/lib/app-paths.js
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/profile.js
CHANGED
|
@@ -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
|
package/src/lib/terminal.js
CHANGED
|
@@ -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
|
-
|
|
68
|
-
process.stdin.setRawMode(true);
|
|
86
|
+
beginKeyboardCapture();
|
|
69
87
|
let escapePending = false;
|
|
70
88
|
|
|
71
89
|
const cleanup = () => {
|
|
72
|
-
|
|
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
|
-
|
|
128
|
-
process.stdin.setRawMode(true);
|
|
144
|
+
beginKeyboardCapture();
|
|
129
145
|
|
|
130
146
|
const cleanup = () => {
|
|
131
|
-
|
|
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
|
-
|
|
251
|
-
process.stdin.setRawMode(true);
|
|
265
|
+
beginKeyboardCapture();
|
|
252
266
|
|
|
253
267
|
const cleanup = () => {
|
|
254
|
-
|
|
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
|
-
|
|
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();
|