claude-connect 0.1.0

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.
@@ -0,0 +1,481 @@
1
+ import fs from 'node:fs';
2
+ import fsPromises from 'node:fs/promises';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ buildAnthropicMessageFromOpenAI,
10
+ buildOpenAIRequestFromAnthropic,
11
+ estimateTokenCountFromAnthropicRequest,
12
+ writeAnthropicStreamFromMessage
13
+ } from './messages.js';
14
+ import { gatewayBasePath, gatewayBaseUrl, gatewayHost, gatewayPort } from './constants.js';
15
+ import { getGatewayStatus, readGatewayState, writeGatewayState, isProcessAlive } from './state.js';
16
+ import { resolveClaudeConnectPaths } from '../lib/app-paths.js';
17
+ import { readSwitchState } from '../lib/claude-settings.js';
18
+ import { readOAuthToken, refreshOAuthToken } from '../lib/oauth.js';
19
+ import { readProfileFile } from '../lib/profile.js';
20
+ import { readManagedTokenSecret } from '../lib/secrets.js';
21
+
22
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
23
+ const cliEntryPath = path.join(projectRoot, 'bin', 'claude-connect.js');
24
+
25
+ function isObject(value) {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ }
28
+
29
+ function buildErrorResponse(statusCode, message, type = 'api_error') {
30
+ return {
31
+ statusCode,
32
+ body: {
33
+ type: 'error',
34
+ error: {
35
+ type,
36
+ message
37
+ }
38
+ }
39
+ };
40
+ }
41
+
42
+ function sendJson(response, statusCode, payload) {
43
+ response.writeHead(statusCode, {
44
+ 'content-type': 'application/json; charset=utf-8',
45
+ 'cache-control': 'no-store'
46
+ });
47
+ response.end(`${JSON.stringify(payload, null, 2)}\n`);
48
+ }
49
+
50
+ async function readJsonBody(request) {
51
+ const chunks = [];
52
+ let totalLength = 0;
53
+
54
+ for await (const chunk of request) {
55
+ totalLength += chunk.length;
56
+
57
+ if (totalLength > 10 * 1024 * 1024) {
58
+ throw new Error('La peticion excede el limite de 10 MB.');
59
+ }
60
+
61
+ chunks.push(chunk);
62
+ }
63
+
64
+ const raw = Buffer.concat(chunks).toString('utf8');
65
+ return raw.length === 0 ? {} : JSON.parse(raw);
66
+ }
67
+
68
+ function normalizeOauthResourceUrl(resourceUrl) {
69
+ if (typeof resourceUrl !== 'string' || resourceUrl.trim().length === 0) {
70
+ return null;
71
+ }
72
+
73
+ const normalized = resourceUrl.startsWith('http://') || resourceUrl.startsWith('https://')
74
+ ? resourceUrl
75
+ : `https://${resourceUrl}`;
76
+
77
+ return normalized.replace(/\/$/, '');
78
+ }
79
+
80
+ function buildOauthApiBaseUrl(profile, tokenRecord) {
81
+ if (typeof profile?.auth?.oauth?.apiBaseUrl === 'string' && profile.auth.oauth.apiBaseUrl.length > 0) {
82
+ return profile.auth.oauth.apiBaseUrl;
83
+ }
84
+
85
+ const resourceUrl = normalizeOauthResourceUrl(tokenRecord?.token?.resource_url);
86
+
87
+ if (resourceUrl) {
88
+ return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;
89
+ }
90
+
91
+ return 'https://portal.qwen.ai/v1';
92
+ }
93
+
94
+ async function resolveGatewayContext() {
95
+ const switchState = await readSwitchState();
96
+
97
+ if (!switchState?.active || typeof switchState.profilePath !== 'string') {
98
+ throw new Error('Claude Connect no tiene un perfil activo en Claude Code.');
99
+ }
100
+
101
+ const profile = await readProfileFile(switchState.profilePath);
102
+ const authMethod = profile?.auth?.method === 'api_key' ? 'token' : profile?.auth?.method;
103
+
104
+ if (authMethod === 'token') {
105
+ const envVar = profile?.auth?.envVar;
106
+ let token = typeof envVar === 'string' ? process.env[envVar] : '';
107
+
108
+ if ((!token || token.trim().length === 0) && typeof profile?.auth?.secretFile === 'string') {
109
+ const secret = await readManagedTokenSecret(profile.auth.secretFile);
110
+ token = typeof secret?.token === 'string' ? secret.token : '';
111
+ }
112
+
113
+ if (typeof token !== 'string' || token.trim().length === 0) {
114
+ throw new Error(`Falta la variable de entorno ${envVar} y tampoco hay una API key guardada para este perfil.`);
115
+ }
116
+
117
+ return {
118
+ profile,
119
+ authMethod,
120
+ upstreamBaseUrl: profile.endpoint.baseUrl,
121
+ accessToken: token.trim()
122
+ };
123
+ }
124
+
125
+ if (authMethod === 'oauth') {
126
+ const tokenFile = profile?.auth?.oauth?.tokenFile;
127
+
128
+ if (typeof tokenFile !== 'string' || tokenFile.length === 0) {
129
+ throw new Error('El perfil OAuth no tiene un token local asociado.');
130
+ }
131
+
132
+ let record = await readOAuthToken(tokenFile);
133
+ let accessToken = typeof record.token?.access_token === 'string' ? record.token.access_token : '';
134
+
135
+ if (!accessToken && typeof record.token?.refresh_token === 'string') {
136
+ record = await refreshOAuthToken({
137
+ filePath: tokenFile,
138
+ tokenUrl: profile.auth.oauth.tokenUrl,
139
+ clientId: profile.auth.oauth.clientId
140
+ });
141
+ accessToken = record.token.access_token;
142
+ }
143
+
144
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
145
+ throw new Error('No se encontro un access_token valido para el perfil OAuth.');
146
+ }
147
+
148
+ return {
149
+ profile,
150
+ authMethod,
151
+ upstreamBaseUrl: buildOauthApiBaseUrl(profile, record),
152
+ accessToken
153
+ };
154
+ }
155
+
156
+ throw new Error(`Metodo de autenticacion no soportado por el gateway: ${authMethod}`);
157
+ }
158
+
159
+ async function forwardChatCompletion({ openAiRequest, context, refreshOnUnauthorized = true }) {
160
+ const response = await fetch(`${context.upstreamBaseUrl.replace(/\/$/, '')}/chat/completions`, {
161
+ method: 'POST',
162
+ headers: {
163
+ 'content-type': 'application/json',
164
+ accept: 'application/json',
165
+ authorization: `Bearer ${context.accessToken}`,
166
+ 'user-agent': 'claude-connect-gateway/0.1.0'
167
+ },
168
+ body: JSON.stringify(openAiRequest)
169
+ });
170
+
171
+ const payload = await response.json().catch(() => ({}));
172
+
173
+ if (response.ok) {
174
+ return payload;
175
+ }
176
+
177
+ if (response.status === 401 && refreshOnUnauthorized && context.authMethod === 'oauth') {
178
+ const refreshed = await refreshOAuthToken({
179
+ filePath: context.profile.auth.oauth.tokenFile,
180
+ tokenUrl: context.profile.auth.oauth.tokenUrl,
181
+ clientId: context.profile.auth.oauth.clientId
182
+ });
183
+
184
+ return forwardChatCompletion({
185
+ openAiRequest,
186
+ context: {
187
+ ...context,
188
+ accessToken: refreshed.token.access_token
189
+ },
190
+ refreshOnUnauthorized: false
191
+ });
192
+ }
193
+
194
+ const message = payload?.error?.message || payload?.message || payload?.error || `HTTP ${response.status}`;
195
+ throw new Error(`Qwen devolvio un error: ${message}`);
196
+ }
197
+
198
+ function buildHealthPayload(context) {
199
+ return {
200
+ ok: true,
201
+ service: 'claude-connect-gateway',
202
+ baseUrl: gatewayBaseUrl,
203
+ profileName: context.profile.profileName,
204
+ provider: context.profile.provider.id,
205
+ model: context.profile.model.id,
206
+ authMethod: context.authMethod,
207
+ upstreamBaseUrl: context.upstreamBaseUrl,
208
+ pid: process.pid
209
+ };
210
+ }
211
+
212
+ async function handleHealth(_request, response) {
213
+ const context = await resolveGatewayContext();
214
+ sendJson(response, 200, buildHealthPayload(context));
215
+ }
216
+
217
+ async function handleModels(_request, response) {
218
+ const context = await resolveGatewayContext();
219
+ const model = context.profile.model;
220
+
221
+ sendJson(response, 200, {
222
+ data: [
223
+ {
224
+ type: 'model',
225
+ id: model.id,
226
+ display_name: model.name,
227
+ created_at: '2026-03-31'
228
+ }
229
+ ],
230
+ first_id: model.id,
231
+ has_more: false,
232
+ last_id: model.id
233
+ });
234
+ }
235
+
236
+ async function handleCountTokens(request, response) {
237
+ const body = await readJsonBody(request);
238
+ sendJson(response, 200, {
239
+ input_tokens: estimateTokenCountFromAnthropicRequest(body)
240
+ });
241
+ }
242
+
243
+ async function handleMessages(request, response) {
244
+ const body = await readJsonBody(request);
245
+ const context = await resolveGatewayContext();
246
+ const openAiRequest = buildOpenAIRequestFromAnthropic({
247
+ body,
248
+ model: context.profile.model.id
249
+ });
250
+ const upstreamResponse = await forwardChatCompletion({
251
+ openAiRequest,
252
+ context
253
+ });
254
+ const anthropicMessage = buildAnthropicMessageFromOpenAI({
255
+ response: upstreamResponse,
256
+ requestedModel: context.profile.model.id
257
+ });
258
+
259
+ if (body.stream === true) {
260
+ response.writeHead(200, {
261
+ 'content-type': 'text/event-stream; charset=utf-8',
262
+ 'cache-control': 'no-cache, no-transform',
263
+ connection: 'keep-alive',
264
+ 'x-accel-buffering': 'no'
265
+ });
266
+ writeAnthropicStreamFromMessage(response, anthropicMessage);
267
+ response.end();
268
+ return;
269
+ }
270
+
271
+ sendJson(response, 200, anthropicMessage);
272
+ }
273
+
274
+ async function routeRequest(request, response) {
275
+ const requestUrl = new URL(request.url || '/', gatewayBaseUrl);
276
+
277
+ if (request.method === 'GET' && requestUrl.pathname === `${gatewayBasePath}/health`) {
278
+ await handleHealth(request, response);
279
+ return;
280
+ }
281
+
282
+ if (request.method === 'GET' && requestUrl.pathname === `${gatewayBasePath}/v1/models`) {
283
+ await handleModels(request, response);
284
+ return;
285
+ }
286
+
287
+ if (request.method === 'POST' && requestUrl.pathname === `${gatewayBasePath}/v1/messages/count_tokens`) {
288
+ await handleCountTokens(request, response);
289
+ return;
290
+ }
291
+
292
+ if (request.method === 'POST' && requestUrl.pathname === `${gatewayBasePath}/v1/messages`) {
293
+ await handleMessages(request, response);
294
+ return;
295
+ }
296
+
297
+ const error = buildErrorResponse(404, `Ruta no soportada: ${request.method} ${requestUrl.pathname}`, 'not_found_error');
298
+ sendJson(response, error.statusCode, error.body);
299
+ }
300
+
301
+ export async function serveGateway() {
302
+ const initialContext = await resolveGatewayContext();
303
+ const server = http.createServer(async (request, response) => {
304
+ try {
305
+ await routeRequest(request, response);
306
+ } catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ const apiError = buildErrorResponse(500, message);
309
+ sendJson(response, apiError.statusCode, apiError.body);
310
+ }
311
+ });
312
+
313
+ server.on('clientError', () => {
314
+ // Silence malformed local requests.
315
+ });
316
+
317
+ const stop = async () => {
318
+ await writeGatewayState({
319
+ active: false,
320
+ pid: process.pid,
321
+ stoppedAt: new Date().toISOString()
322
+ });
323
+
324
+ await new Promise((resolve) => server.close(resolve));
325
+ process.exit(0);
326
+ };
327
+
328
+ process.on('SIGINT', () => {
329
+ void stop();
330
+ });
331
+ process.on('SIGTERM', () => {
332
+ void stop();
333
+ });
334
+
335
+ await new Promise((resolve, reject) => {
336
+ server.once('error', reject);
337
+ server.listen(gatewayPort, gatewayHost, resolve);
338
+ });
339
+
340
+ await writeGatewayState({
341
+ active: true,
342
+ pid: process.pid,
343
+ startedAt: new Date().toISOString(),
344
+ logPath: (await resolveClaudeConnectPaths()).gatewayLogPath,
345
+ profileName: initialContext.profile.profileName,
346
+ authMethod: initialContext.authMethod,
347
+ upstreamBaseUrl: initialContext.upstreamBaseUrl,
348
+ baseUrl: gatewayBaseUrl
349
+ });
350
+
351
+ console.log(`claude-connect gateway escuchando en ${gatewayBaseUrl}`);
352
+ return new Promise(() => {});
353
+ }
354
+
355
+ export async function startGatewayInBackground() {
356
+ const currentStatus = await getGatewayStatus();
357
+ const { gatewayLogPath } = await resolveClaudeConnectPaths();
358
+
359
+ if (currentStatus.active) {
360
+ return {
361
+ ...currentStatus,
362
+ alreadyRunning: true
363
+ };
364
+ }
365
+
366
+ await fsPromises.mkdir(path.dirname(gatewayLogPath), { recursive: true });
367
+ const outputFd = fs.openSync(gatewayLogPath, 'a');
368
+ const child = spawn(process.execPath, ['--no-warnings=ExperimentalWarning', cliEntryPath, 'gateway', 'serve'], {
369
+ cwd: projectRoot,
370
+ detached: true,
371
+ stdio: ['ignore', outputFd, outputFd]
372
+ });
373
+
374
+ child.unref();
375
+ fs.closeSync(outputFd);
376
+
377
+ await writeGatewayState({
378
+ active: false,
379
+ pid: child.pid,
380
+ startedAt: new Date().toISOString(),
381
+ logPath: gatewayLogPath,
382
+ baseUrl: gatewayBaseUrl
383
+ });
384
+
385
+ for (let attempt = 0; attempt < 20; attempt += 1) {
386
+ await new Promise((resolve) => setTimeout(resolve, 250));
387
+ const status = await getGatewayStatus({ timeoutMs: 500 });
388
+
389
+ if (status.active) {
390
+ return {
391
+ ...status,
392
+ alreadyRunning: false
393
+ };
394
+ }
395
+
396
+ if (!isProcessAlive(child.pid)) {
397
+ break;
398
+ }
399
+ }
400
+
401
+ const adoptedStatus = await getGatewayStatus({ timeoutMs: 700 });
402
+
403
+ if (adoptedStatus.active) {
404
+ return {
405
+ ...adoptedStatus,
406
+ alreadyRunning: true
407
+ };
408
+ }
409
+
410
+ await writeGatewayState({
411
+ active: false,
412
+ pid: child.pid,
413
+ startedAt: new Date().toISOString(),
414
+ logPath: gatewayLogPath,
415
+ baseUrl: gatewayBaseUrl,
416
+ lastError: 'El gateway no llego a responder en el tiempo esperado.'
417
+ });
418
+
419
+ throw new Error(`No pude iniciar el gateway local. Revisa el log: ${gatewayLogPath}`);
420
+ }
421
+
422
+ export async function stopGateway() {
423
+ const { gatewayLogPath } = await resolveClaudeConnectPaths();
424
+ const state = await readGatewayState();
425
+ const pid = Number(state?.pid ?? 0);
426
+
427
+ if (!isProcessAlive(pid)) {
428
+ await writeGatewayState({
429
+ active: false,
430
+ stoppedAt: new Date().toISOString()
431
+ });
432
+
433
+ return {
434
+ stopped: false,
435
+ pid: null,
436
+ logPath: gatewayLogPath
437
+ };
438
+ }
439
+
440
+ if (process.platform === 'win32') {
441
+ await new Promise((resolve, reject) => {
442
+ const child = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
443
+ stdio: 'ignore',
444
+ windowsHide: true
445
+ });
446
+
447
+ child.once('error', reject);
448
+ child.once('exit', (code) => {
449
+ if (code === 0 || code === 128) {
450
+ resolve();
451
+ return;
452
+ }
453
+
454
+ reject(new Error(`taskkill devolvio ${code}`));
455
+ });
456
+ });
457
+ } else {
458
+ process.kill(pid, 'SIGTERM');
459
+ }
460
+
461
+ for (let attempt = 0; attempt < 20; attempt += 1) {
462
+ await new Promise((resolve) => setTimeout(resolve, 100));
463
+
464
+ if (!isProcessAlive(pid)) {
465
+ await writeGatewayState({
466
+ active: false,
467
+ pid,
468
+ stoppedAt: new Date().toISOString(),
469
+ logPath: gatewayLogPath
470
+ });
471
+
472
+ return {
473
+ stopped: true,
474
+ pid,
475
+ logPath: gatewayLogPath
476
+ };
477
+ }
478
+ }
479
+
480
+ throw new Error(`No pude detener el gateway local (pid ${pid}).`);
481
+ }
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs/promises';
2
+ import { gatewayBaseUrl } from './constants.js';
3
+ import { resolveClaudeConnectPaths } from '../lib/app-paths.js';
4
+
5
+ function isObject(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ async function readJsonIfExists(filePath) {
10
+ try {
11
+ const raw = await fs.readFile(filePath, 'utf8');
12
+ return JSON.parse(raw);
13
+ } catch (error) {
14
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
15
+ return null;
16
+ }
17
+
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ export async function writeGatewayState(payload) {
23
+ const { gatewayDir, gatewayStatePath } = await resolveClaudeConnectPaths();
24
+ await fs.mkdir(gatewayDir, { recursive: true });
25
+ await fs.writeFile(gatewayStatePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
26
+ }
27
+
28
+ export async function readGatewayState() {
29
+ const { gatewayStatePath } = await resolveClaudeConnectPaths();
30
+ const state = await readJsonIfExists(gatewayStatePath);
31
+ return isObject(state) ? state : null;
32
+ }
33
+
34
+ export function isProcessAlive(pid) {
35
+ if (!Number.isInteger(pid) || pid <= 0) {
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ } catch (error) {
43
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
44
+ return false;
45
+ }
46
+
47
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
48
+ return true;
49
+ }
50
+
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ async function probeGatewayHealth(timeoutMs) {
56
+ try {
57
+ const response = await fetch(`${gatewayBaseUrl}/health`, {
58
+ headers: { accept: 'application/json' },
59
+ signal: AbortSignal.timeout(timeoutMs)
60
+ });
61
+
62
+ if (!response.ok) {
63
+ return null;
64
+ }
65
+
66
+ return await response.json();
67
+ } catch (_error) {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ export async function getGatewayStatus({ timeoutMs = 700 } = {}) {
73
+ const { gatewayLogPath, gatewayStatePath } = await resolveClaudeConnectPaths();
74
+ const state = await readGatewayState();
75
+ const pid = Number(state?.pid ?? 0);
76
+ const processAlive = isProcessAlive(pid);
77
+ const health = await probeGatewayHealth(timeoutMs);
78
+
79
+ return {
80
+ active: Boolean(health),
81
+ pid: processAlive ? pid : null,
82
+ baseUrl: gatewayBaseUrl,
83
+ logPath: gatewayLogPath,
84
+ statePath: gatewayStatePath,
85
+ startedAt: typeof state?.startedAt === 'string' ? state.startedAt : null,
86
+ lastError: typeof state?.lastError === 'string' ? state.lastError : null,
87
+ profileName: typeof health?.profileName === 'string'
88
+ ? health.profileName
89
+ : typeof state?.profileName === 'string'
90
+ ? state.profileName
91
+ : null,
92
+ upstreamBaseUrl: typeof health?.upstreamBaseUrl === 'string'
93
+ ? health.upstreamBaseUrl
94
+ : typeof state?.upstreamBaseUrl === 'string'
95
+ ? state.upstreamBaseUrl
96
+ : null,
97
+ authMethod: typeof health?.authMethod === 'string'
98
+ ? health.authMethod
99
+ : typeof state?.authMethod === 'string'
100
+ ? state.authMethod
101
+ : null,
102
+ health
103
+ };
104
+ }
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ import { getCatalogStore } from './data/catalog-store.js';
2
+ import { gatewayBaseUrl } from './gateway/constants.js';
3
+ import { getGatewayStatus } from './gateway/state.js';
4
+ import { serveGateway, startGatewayInBackground, stopGateway } from './gateway/server.js';
5
+ import { runWizard } from './wizard.js';
6
+
7
+ function printGatewayStatus(status) {
8
+ const lines = [
9
+ `active=${status.active ? 'yes' : 'no'}`,
10
+ `base_url=${status.baseUrl}`,
11
+ `pid=${status.pid ?? 'none'}`,
12
+ `profile=${status.profileName ?? 'none'}`,
13
+ `auth=${status.authMethod ?? 'none'}`,
14
+ `upstream=${status.upstreamBaseUrl ?? 'none'}`,
15
+ `log=${status.logPath}`
16
+ ];
17
+
18
+ process.stdout.write(`${lines.join('\n')}\n`);
19
+ }
20
+
21
+ async function runGatewayCommand(args) {
22
+ const action = args[0] ?? 'status';
23
+
24
+ if (action === 'serve') {
25
+ await serveGateway();
26
+ return;
27
+ }
28
+
29
+ if (action === 'start') {
30
+ const status = await startGatewayInBackground();
31
+ process.stdout.write(`Gateway listo en ${gatewayBaseUrl} (pid ${status.pid})\n`);
32
+ return;
33
+ }
34
+
35
+ if (action === 'stop') {
36
+ const result = await stopGateway();
37
+ process.stdout.write(
38
+ result.stopped
39
+ ? `Gateway detenido (pid ${result.pid})\n`
40
+ : 'No habia un gateway en ejecucion.\n'
41
+ );
42
+ return;
43
+ }
44
+
45
+ if (action === 'status') {
46
+ printGatewayStatus(await getGatewayStatus());
47
+ return;
48
+ }
49
+
50
+ throw new Error(`Comando de gateway no soportado: ${action}`);
51
+ }
52
+
53
+ export async function run(argv = process.argv.slice(2)) {
54
+ if (argv[0] === 'gateway') {
55
+ await runGatewayCommand(argv.slice(1));
56
+ return;
57
+ }
58
+
59
+ getCatalogStore();
60
+ await runWizard();
61
+ }