claude-connect 0.1.8 → 0.1.10

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,15 @@
1
1
  # Claude Connect
2
2
 
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.
3
+ > Conecta `Claude Code` con `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Z.AI`, `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%20Inception%20Labs%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%20Z.AI%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
+
10
+ <p align="center">
11
+ <img src="./ezgif-871b2bc9267494c5.gif" alt="Claude Connect demo" width="980" />
12
+ </p>
9
13
 
10
14
  ## Why Claude Connect
11
15
 
@@ -13,7 +17,7 @@
13
17
 
14
18
  ### Highlights
15
19
 
16
- - `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` listos desde el primer arranque
20
+ - `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Z.AI`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` listos desde el primer arranque
17
21
  - soporte para `Token` y `OAuth` cuando el proveedor lo permite
18
22
  - API keys compartidas por proveedor para no repetir el mismo token en cada modelo
19
23
  - activación reversible sobre la instalación real de `Claude Code`
@@ -78,12 +82,14 @@ Al activar:
78
82
  - `Zen` usa conexión directa o gateway según el modelo elegido
79
83
  - `Kimi` usa gateway local y reenvia al endpoint Anthropic de `https://api.kimi.com/coding/`
80
84
  - `DeepSeek` apunta a `https://api.deepseek.com/anthropic`
85
+ - `Z.AI` apunta a `https://api.z.ai/api/anthropic`
81
86
  - `Ollama` pide una URL local o remota, valida `/api/tags` y usa el gateway local sobre `.../api/chat`
82
87
  - `OpenAI` usa el gateway local sobre `https://api.openai.com/v1/chat/completions`
83
88
  - `Inception Labs` usa el gateway local sobre `https://api.inceptionlabs.ai/v1/chat/completions`
84
89
  - `OpenRouter` usa `openrouter/free` por gateway sobre `https://openrouter.ai/api/v1`
85
90
  - `Qwen` apunta al gateway local `http://127.0.0.1:4310/anthropic`
86
91
  - para algunos modelos con limites conocidos, el gateway ahora ajusta `max_tokens` y bloquea prompts sobredimensionados antes de que el upstream devuelva errores opacos
92
+ - para `Inception Labs`, el gateway tambien respeta un presupuesto local de input tokens por minuto para reducir errores de `Rate limit reached`
87
93
 
88
94
  ## Providers
89
95
 
@@ -93,6 +99,7 @@ Al activar:
93
99
  | `Zen` | `Claude*` de Zen + modelos `chat/completions` de Zen | `Token` | Mixta |
94
100
  | `Kimi` | `kimi-for-coding` | `Token` | Gateway local |
95
101
  | `DeepSeek` | `deepseek-chat`, `deepseek-reasoner` | `Token` | Directa |
102
+ | `Z.AI` | `glm-5.1`, `glm-4.7`, `glm-4.5-air` | `Token` | Directa |
96
103
  | `Ollama` | modelos descubiertos desde tu servidor | `Servidor Ollama` | Gateway local |
97
104
  | `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
105
  | `Inception Labs` | `mercury-2` | `Token` | Gateway local |
@@ -126,12 +133,14 @@ Nota sobre `Inception Labs`:
126
133
  - esta primera integracion expone solo `mercury-2`, que es el modelo chat-compatible oficial en `v1/chat/completions`
127
134
  - `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
135
  - Claude Connect aplica presupuesto preventivo de contexto para `mercury-2` usando ventana `128K` y salida maxima `16,384`
136
+ - Claude Connect tambien aplica una ventana deslizante local de `400,000` input tokens por minuto para reducir rechazos del upstream por rate limit
129
137
  - `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
138
  - autenticacion soportada: `API key`
131
139
  - referencias oficiales:
132
140
  - https://docs.inceptionlabs.ai/get-started/get-started
133
141
  - https://docs.inceptionlabs.ai/get-started/authentication
134
142
  - https://docs.inceptionlabs.ai/get-started/models
143
+ - https://docs.inceptionlabs.ai/get-started/rate-limits
135
144
 
136
145
  Nota sobre `DeepSeek`:
137
146
 
@@ -140,6 +149,14 @@ Nota sobre `DeepSeek`:
140
149
  - https://api-docs.deepseek.com/quick_start/pricing/
141
150
  - https://api-docs.deepseek.com/guides/reasoning_model
142
151
 
152
+ Nota sobre `Z.AI`:
153
+
154
+ - usa el endpoint Anthropic-compatible oficial `https://api.z.ai/api/anthropic`
155
+ - Claude Connect fija `API_TIMEOUT_MS=3000000`
156
+ - al activar un perfil de `Z.AI`, tambien mapea `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL` y `ANTHROPIC_DEFAULT_OPUS_MODEL` al modelo elegido para que Claude Code use `GLM` de forma consistente
157
+ - referencias oficiales:
158
+ - https://docs.z.ai/devpack/tool/claude
159
+
143
160
  Nota sobre `Ollama`:
144
161
 
145
162
  - la URL del servidor se define al crear la conexión
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-connect",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CLI para configurar Claude Code con proveedores de modelos externos",
5
5
  "author": "wmcarlosv",
6
6
  "type": "module",
@@ -528,6 +528,75 @@ const seedProviders = [
528
528
  }
529
529
  ]
530
530
  },
531
+ {
532
+ id: 'zai',
533
+ name: 'Z.AI',
534
+ vendor: 'Zhipu AI',
535
+ description: 'GLM Coding Plan para Claude Code usando el endpoint Anthropic-compatible oficial de z.ai.',
536
+ docsUrl: 'https://docs.z.ai/devpack/tool/claude',
537
+ docsVerifiedAt: '2026-04-04',
538
+ baseUrl: 'https://api.z.ai/api/anthropic',
539
+ defaultModelId: 'glm-5.1',
540
+ defaultAuthMethodId: 'token',
541
+ defaultApiKeyEnvVar: 'ZAI_API_KEY',
542
+ models: [
543
+ {
544
+ id: 'glm-5.1',
545
+ name: 'GLM-5.1',
546
+ category: 'Coding',
547
+ contextWindow: 'Auto',
548
+ summary: 'Modelo recomendado de la documentacion oficial de z.ai para usuarios Max que quieren usar GLM-5.1 en Claude Code.',
549
+ upstreamModelId: 'glm-5.1',
550
+ transportMode: 'direct',
551
+ apiStyle: 'anthropic',
552
+ apiBaseUrl: 'https://api.z.ai/api/anthropic',
553
+ apiPath: '/v1/messages',
554
+ authEnvMode: 'auth_token',
555
+ sortOrder: 1,
556
+ isDefault: 1
557
+ },
558
+ {
559
+ id: 'glm-4.7',
560
+ name: 'GLM-4.7',
561
+ category: 'General',
562
+ contextWindow: 'Auto',
563
+ summary: 'Modelo default recomendado por z.ai para Opus y Sonnet dentro del GLM Coding Plan.',
564
+ upstreamModelId: 'glm-4.7',
565
+ transportMode: 'direct',
566
+ apiStyle: 'anthropic',
567
+ apiBaseUrl: 'https://api.z.ai/api/anthropic',
568
+ apiPath: '/v1/messages',
569
+ authEnvMode: 'auth_token',
570
+ sortOrder: 2,
571
+ isDefault: 0
572
+ },
573
+ {
574
+ id: 'glm-4.5-air',
575
+ name: 'GLM-4.5-Air',
576
+ category: 'Fast',
577
+ contextWindow: 'Auto',
578
+ summary: 'Modelo ligero recomendado por z.ai para la clase Haiku dentro del GLM Coding Plan.',
579
+ upstreamModelId: 'glm-4.5-air',
580
+ transportMode: 'direct',
581
+ apiStyle: 'anthropic',
582
+ apiBaseUrl: 'https://api.z.ai/api/anthropic',
583
+ apiPath: '/v1/messages',
584
+ authEnvMode: 'auth_token',
585
+ sortOrder: 3,
586
+ isDefault: 0
587
+ }
588
+ ],
589
+ authMethods: [
590
+ {
591
+ id: 'token',
592
+ name: 'Token',
593
+ description: 'Conexion por API key contra el endpoint Anthropic-compatible oficial de z.ai.',
594
+ credentialKind: 'env_var',
595
+ sortOrder: 1,
596
+ isDefault: 1
597
+ }
598
+ ]
599
+ },
531
600
  {
532
601
  id: 'ollama',
533
602
  name: 'Ollama',
@@ -27,6 +27,7 @@ import { readSwitchState } from '../lib/claude-settings.js';
27
27
  import { enforceModelTokenBudget } from '../lib/model-budget.js';
28
28
  import { readOAuthToken, refreshOAuthToken } from '../lib/oauth.js';
29
29
  import { readProfileFile } from '../lib/profile.js';
30
+ import { reserveProviderInputTokens } from '../lib/provider-rate-limit.js';
30
31
  import { readManagedProviderTokenSecret, readManagedTokenSecret } from '../lib/secrets.js';
31
32
 
32
33
  const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -509,6 +510,10 @@ async function handleMessages(request, response) {
509
510
  body: rawBody,
510
511
  profile: context.profile
511
512
  });
513
+ await reserveProviderInputTokens({
514
+ profile: context.profile,
515
+ inputTokens: estimateTokenCountFromAnthropicRequest(body)
516
+ });
512
517
 
513
518
  if (context.upstreamApiStyle === 'anthropic') {
514
519
  const upstreamResponse = await forwardAnthropicMessage({
@@ -154,6 +154,13 @@ export async function resolveClaudeTransportForProfile({
154
154
  extraEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.model.id;
155
155
  }
156
156
 
157
+ if (profile.provider.id === 'zai') {
158
+ extraEnv.API_TIMEOUT_MS = '3000000';
159
+ extraEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.model.id;
160
+ extraEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = profile.model.id;
161
+ extraEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = profile.model.id;
162
+ }
163
+
157
164
  if (profile.provider.id === 'kimi') {
158
165
  extraEnv.ENABLE_TOOL_SEARCH = 'false';
159
166
  }
@@ -220,6 +227,8 @@ export function buildClaudeSettingsForProfile({
220
227
  delete env.ENABLE_TOOL_SEARCH;
221
228
  delete env.ANTHROPIC_MODEL;
222
229
  delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
230
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
231
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
223
232
 
224
233
  Object.assign(env, extraEnv);
225
234
 
@@ -259,9 +268,16 @@ export async function activateClaudeProfile({ profile, gatewayBaseUrl = 'http://
259
268
  const currentAccount = await readClaudeAccount();
260
269
  const currentCredentials = await readJsonIfExists(claudeCredentialsPath);
261
270
  const currentState = await readSwitchState();
262
- const originalSettings = currentState?.originalSettings ?? currentSettings;
263
- const originalAccount = currentState?.originalAccount ?? currentAccount;
264
- const originalCredentials = currentState && Object.prototype.hasOwnProperty.call(currentState, 'originalCredentials')
271
+ const canReuseActiveSnapshot = currentState?.active === true;
272
+ const originalSettings = canReuseActiveSnapshot
273
+ ? currentState?.originalSettings ?? currentSettings
274
+ : currentSettings;
275
+ const originalAccount = canReuseActiveSnapshot
276
+ ? currentState?.originalAccount ?? currentAccount
277
+ : currentAccount;
278
+ const originalCredentials = canReuseActiveSnapshot
279
+ && currentState
280
+ && Object.prototype.hasOwnProperty.call(currentState, 'originalCredentials')
265
281
  ? currentState.originalCredentials
266
282
  : currentCredentials;
267
283
  const transport = await resolveClaudeTransportForProfile({
@@ -0,0 +1,109 @@
1
+ function getProviderId(profile) {
2
+ return profile?.provider?.id ?? '';
3
+ }
4
+
5
+ function getProviderInputTokensPerMinute(profile) {
6
+ const providerId = getProviderId(profile);
7
+
8
+ if (providerId === 'inception') {
9
+ return 400_000;
10
+ }
11
+
12
+ return null;
13
+ }
14
+
15
+ const recentReservations = [];
16
+
17
+ function pruneReservations(now) {
18
+ for (let index = recentReservations.length - 1; index >= 0; index -= 1) {
19
+ if (now - recentReservations[index].timestamp >= 60_000) {
20
+ recentReservations.splice(index, 1);
21
+ }
22
+ }
23
+ }
24
+
25
+ function sumReservedTokens(profile, now) {
26
+ const providerId = getProviderId(profile);
27
+
28
+ return recentReservations.reduce((total, entry) => {
29
+ if (entry.providerId !== providerId) {
30
+ return total;
31
+ }
32
+
33
+ if (now - entry.timestamp >= 60_000) {
34
+ return total;
35
+ }
36
+
37
+ return total + entry.tokens;
38
+ }, 0);
39
+ }
40
+
41
+ function earliestExpiryForProvider(profile, now) {
42
+ const providerId = getProviderId(profile);
43
+ let minExpiry = null;
44
+
45
+ for (const entry of recentReservations) {
46
+ if (entry.providerId !== providerId) {
47
+ continue;
48
+ }
49
+
50
+ const expiry = entry.timestamp + 60_000;
51
+
52
+ if (expiry <= now) {
53
+ continue;
54
+ }
55
+
56
+ if (minExpiry == null || expiry < minExpiry) {
57
+ minExpiry = expiry;
58
+ }
59
+ }
60
+
61
+ return minExpiry;
62
+ }
63
+
64
+ export function resetProviderRateLimitState() {
65
+ recentReservations.length = 0;
66
+ }
67
+
68
+ export async function reserveProviderInputTokens({
69
+ profile,
70
+ inputTokens,
71
+ now = () => Date.now(),
72
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
73
+ }) {
74
+ const tokensPerMinute = getProviderInputTokensPerMinute(profile);
75
+
76
+ if (!tokensPerMinute) {
77
+ return;
78
+ }
79
+
80
+ if (inputTokens > tokensPerMinute) {
81
+ throw new Error(
82
+ `${profile.provider.name} rechazo la solicitud porque la entrada estimada excede el limite de ${tokensPerMinute.toLocaleString('en-US')} tokens por minuto. Usa /compact o /clear antes de continuar.`
83
+ );
84
+ }
85
+
86
+ while (true) {
87
+ const currentTime = now();
88
+ pruneReservations(currentTime);
89
+ const usedTokens = sumReservedTokens(profile, currentTime);
90
+
91
+ if (usedTokens + inputTokens <= tokensPerMinute) {
92
+ recentReservations.push({
93
+ providerId: getProviderId(profile),
94
+ tokens: inputTokens,
95
+ timestamp: currentTime
96
+ });
97
+ return;
98
+ }
99
+
100
+ const nextExpiry = earliestExpiryForProvider(profile, currentTime);
101
+
102
+ if (nextExpiry == null) {
103
+ throw new Error(`No se pudo reservar presupuesto de tokens para ${profile.provider.name}.`);
104
+ }
105
+
106
+ const waitMs = Math.max(250, nextExpiry - currentTime + 25);
107
+ await sleep(waitMs);
108
+ }
109
+ }
package/src/lib/theme.js CHANGED
@@ -17,10 +17,33 @@ export const colors = {
17
17
  bold: '\x1b[1m'
18
18
  };
19
19
 
20
+ export function rgb(r, g, b) {
21
+ return `\x1b[38;2;${r};${g};${b}m`;
22
+ }
23
+
20
24
  export function colorize(text, ...tokens) {
21
25
  return `${tokens.join('')}${text}${RESET}`;
22
26
  }
23
27
 
28
+ export function gradientizeLines(lines, palette) {
29
+ if (!Array.isArray(lines) || lines.length === 0) {
30
+ return [];
31
+ }
32
+
33
+ if (!Array.isArray(palette) || palette.length === 0) {
34
+ return [...lines];
35
+ }
36
+
37
+ if (palette.length === 1) {
38
+ return lines.map((line) => colorize(line, palette[0], colors.bold));
39
+ }
40
+
41
+ return lines.map((line, index) => {
42
+ const paletteIndex = Math.round((index / Math.max(1, lines.length - 1)) * (palette.length - 1));
43
+ return colorize(line, palette[paletteIndex], colors.bold);
44
+ });
45
+ }
46
+
24
47
  export function stripAnsi(value) {
25
48
  return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
26
49
  }
package/src/wizard.js CHANGED
@@ -34,7 +34,26 @@ import {
34
34
  selectFromList,
35
35
  waitForAnyKey
36
36
  } from './lib/terminal.js';
37
- import { colorize, colors } from './lib/theme.js';
37
+ import { colorize, colors, gradientizeLines, rgb } from './lib/theme.js';
38
+
39
+ function buildBrandWordmark() {
40
+ const wordmark = [
41
+ ' ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗',
42
+ ' ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝',
43
+ ' ██║ ██║ ███████║██║ ██║██║ ██║█████╗ ',
44
+ ' ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ',
45
+ ' ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗',
46
+ ' ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝',
47
+ ' C L A U D E · C O N N E C T'
48
+ ];
49
+
50
+ return gradientizeLines(wordmark, [
51
+ rgb(103, 232, 249),
52
+ rgb(56, 189, 248),
53
+ rgb(59, 130, 246),
54
+ rgb(14, 165, 233)
55
+ ]);
56
+ }
38
57
 
39
58
  function isBack(value) {
40
59
  return value === navigation.BACK;
@@ -229,6 +248,8 @@ function renderWelcome() {
229
248
  title: 'Conecta Claude Code con otros modelos',
230
249
  subtitle: 'Flujo guiado, catalogo SQLite y perfiles locales listos para reutilizar.',
231
250
  body: [
251
+ ...buildBrandWordmark(),
252
+ '',
232
253
  colorize('Experiencia inicial', colors.bold, colors.accentSoft),
233
254
  colorize('1. Elegir proveedor desde la base local', colors.soft),
234
255
  colorize('2. Elegir modelo y tipo de conexion', colors.soft),
@@ -236,7 +257,7 @@ function renderWelcome() {
236
257
  colorize('4. Guardar perfil y credenciales locales', colors.soft),
237
258
  '',
238
259
  colorize('Catalogo actual', colors.bold, colors.accentSoft),
239
- colorize('OpenCode Go, Zen, Kimi, DeepSeek, Ollama, OpenAI, OpenRouter y Qwen ya vienen almacenados en SQLite.', colors.soft),
260
+ colorize('OpenCode Go, Zen, Kimi, DeepSeek, Z.AI, Ollama, OpenAI, Inception Labs, OpenRouter y Qwen ya vienen almacenados en SQLite.', colors.soft),
240
261
  '',
241
262
  colorize('Seguridad', colors.bold, colors.accentSoft),
242
263
  colorize('El token OAuth se guarda localmente y el modo Token puede guardarse una sola vez por proveedor.', colors.soft)