blits-bridge 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.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ClaudeAuth handles authentication with Claude CLI.
3
+ * Checks existing auth, triggers login flow if needed,
4
+ * and extracts OAuth token for API calls.
5
+ */
6
+ export declare class ClaudeAuth {
7
+ private claudePath;
8
+ constructor();
9
+ private findClaude;
10
+ checkAuth(): Promise<boolean>;
11
+ login(): Promise<boolean>;
12
+ getToken(): Promise<string | null>;
13
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClaudeAuth = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const os_1 = require("os");
7
+ const path_1 = require("path");
8
+ /**
9
+ * ClaudeAuth handles authentication with Claude CLI.
10
+ * Checks existing auth, triggers login flow if needed,
11
+ * and extracts OAuth token for API calls.
12
+ */
13
+ class ClaudeAuth {
14
+ claudePath = null;
15
+ constructor() {
16
+ this.claudePath = this.findClaude();
17
+ }
18
+ findClaude() {
19
+ const candidates = [
20
+ 'claude',
21
+ (0, path_1.join)((0, os_1.homedir)(), '.npm', 'bin', 'claude'),
22
+ '/usr/local/bin/claude',
23
+ '/usr/bin/claude',
24
+ ];
25
+ for (const c of candidates) {
26
+ try {
27
+ (0, child_process_1.execSync)(`${c} --version`, { encoding: 'utf8', stdio: 'pipe' });
28
+ return c;
29
+ }
30
+ catch { }
31
+ }
32
+ return null;
33
+ }
34
+ async checkAuth() {
35
+ if (!this.claudePath) {
36
+ console.error('❌ Claude CLI не найден. Установите: npm install -g @anthropic-ai/claude-code');
37
+ return false;
38
+ }
39
+ try {
40
+ const result = (0, child_process_1.execSync)(`${this.claudePath} auth status`, {
41
+ encoding: 'utf8',
42
+ stdio: 'pipe',
43
+ timeout: 10000,
44
+ });
45
+ return result.toLowerCase().includes('logged in') || result.toLowerCase().includes('authenticated');
46
+ }
47
+ catch {
48
+ // auth status might fail if not logged in
49
+ return false;
50
+ }
51
+ }
52
+ async login() {
53
+ if (!this.claudePath)
54
+ return false;
55
+ return new Promise((resolve) => {
56
+ const proc = (0, child_process_1.spawn)(this.claudePath, ['auth', 'login'], {
57
+ stdio: 'inherit', // Let user interact directly
58
+ env: { ...process.env },
59
+ });
60
+ proc.on('close', (code) => {
61
+ resolve(code === 0);
62
+ });
63
+ proc.on('error', () => {
64
+ resolve(false);
65
+ });
66
+ });
67
+ }
68
+ async getToken() {
69
+ // Try to read OAuth token from Claude CLI config
70
+ const credPaths = [
71
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', '.credentials.json'),
72
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', 'credentials.json'),
73
+ (0, path_1.join)((0, os_1.homedir)(), '.config', 'claude', 'credentials.json'),
74
+ ];
75
+ for (const p of credPaths) {
76
+ if ((0, fs_1.existsSync)(p)) {
77
+ try {
78
+ const data = JSON.parse((0, fs_1.readFileSync)(p, 'utf8'));
79
+ // Claude CLI stores OAuth token in various formats
80
+ const token = data.claudeAiOauth?.accessToken
81
+ || data.oauth?.access_token
82
+ || data.accessToken
83
+ || data.access_token;
84
+ if (token)
85
+ return token;
86
+ }
87
+ catch { }
88
+ }
89
+ }
90
+ // Fallback: try to extract from claude CLI
91
+ if (this.claudePath) {
92
+ try {
93
+ const result = (0, child_process_1.execSync)(`${this.claudePath} auth token 2>/dev/null || true`, {
94
+ encoding: 'utf8',
95
+ stdio: 'pipe',
96
+ timeout: 10000,
97
+ });
98
+ const token = result.trim();
99
+ if (token && token.length > 20)
100
+ return token;
101
+ }
102
+ catch { }
103
+ }
104
+ return null;
105
+ }
106
+ }
107
+ exports.ClaudeAuth = ClaudeAuth;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * BlitsBridge connects to the Blits server via WebSocket,
3
+ * receives Claude API requests, executes them locally using
4
+ * the user's OAuth token, and sends results back.
5
+ */
6
+ export declare class BlitsBridge {
7
+ private wsUrl;
8
+ private sessionToken;
9
+ private oauthToken;
10
+ private ws;
11
+ private reconnecting;
12
+ private shouldReconnect;
13
+ private heartbeatTimer;
14
+ private activeRequests;
15
+ constructor(wsUrl: string, sessionToken: string, oauthToken: string);
16
+ connect(): void;
17
+ disconnect(): void;
18
+ private send;
19
+ private handleRequest;
20
+ private callClaudeAPI;
21
+ private scheduleReconnect;
22
+ private startHeartbeat;
23
+ private stopHeartbeat;
24
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BlitsBridge = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
9
+ const RECONNECT_DELAY = 5000;
10
+ const HEARTBEAT_INTERVAL = 30000;
11
+ /**
12
+ * BlitsBridge connects to the Blits server via WebSocket,
13
+ * receives Claude API requests, executes them locally using
14
+ * the user's OAuth token, and sends results back.
15
+ */
16
+ class BlitsBridge {
17
+ wsUrl;
18
+ sessionToken;
19
+ oauthToken;
20
+ ws = null;
21
+ reconnecting = false;
22
+ shouldReconnect = true;
23
+ heartbeatTimer = null;
24
+ activeRequests = 0;
25
+ constructor(wsUrl, sessionToken, oauthToken) {
26
+ this.wsUrl = wsUrl;
27
+ this.sessionToken = sessionToken;
28
+ this.oauthToken = oauthToken;
29
+ }
30
+ connect() {
31
+ const url = `${this.wsUrl}?token=${encodeURIComponent(this.sessionToken)}`;
32
+ try {
33
+ this.ws = new ws_1.default(url);
34
+ }
35
+ catch (err) {
36
+ console.error('❌ Ошибка подключения:', err.message);
37
+ this.scheduleReconnect();
38
+ return;
39
+ }
40
+ this.ws.on('open', () => {
41
+ console.log('✅ Подключён к Blits');
42
+ console.log('⏳ Ожидаю задачи... (Ctrl+C для выхода)');
43
+ console.log('');
44
+ this.reconnecting = false;
45
+ this.startHeartbeat();
46
+ });
47
+ this.ws.on('message', async (data) => {
48
+ try {
49
+ const msg = JSON.parse(data.toString());
50
+ if (msg.type === 'ping') {
51
+ this.send({ type: 'pong' });
52
+ return;
53
+ }
54
+ if (msg.type === 'claude_api') {
55
+ await this.handleRequest(msg);
56
+ }
57
+ }
58
+ catch (err) {
59
+ console.error('⚠️ Ошибка обработки сообщения:', err.message);
60
+ }
61
+ });
62
+ this.ws.on('close', (code, reason) => {
63
+ this.stopHeartbeat();
64
+ if (this.shouldReconnect) {
65
+ console.log(`🔄 Соединение закрыто (${code}). Переподключение...`);
66
+ this.scheduleReconnect();
67
+ }
68
+ });
69
+ this.ws.on('error', (err) => {
70
+ console.error('⚠️ WebSocket ошибка:', err.message);
71
+ });
72
+ }
73
+ disconnect() {
74
+ this.shouldReconnect = false;
75
+ this.stopHeartbeat();
76
+ if (this.ws) {
77
+ this.ws.close(1000, 'Client shutdown');
78
+ this.ws = null;
79
+ }
80
+ }
81
+ send(data) {
82
+ if (this.ws?.readyState === ws_1.default.OPEN) {
83
+ this.ws.send(JSON.stringify(data));
84
+ }
85
+ }
86
+ async handleRequest(req) {
87
+ this.activeRequests++;
88
+ const startTime = Date.now();
89
+ console.log(`📨 Задача ${req.id.slice(0, 8)}... (модель: ${req.payload.model})`);
90
+ try {
91
+ const response = await this.callClaudeAPI(req.payload);
92
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
93
+ const result = {
94
+ id: req.id,
95
+ type: 'claude_api_result',
96
+ payload: response,
97
+ };
98
+ this.send(result);
99
+ console.log(`✅ Задача ${req.id.slice(0, 8)}... выполнена (${elapsed}с)`);
100
+ }
101
+ catch (err) {
102
+ const result = {
103
+ id: req.id,
104
+ type: 'claude_api_result',
105
+ error: err.message,
106
+ };
107
+ this.send(result);
108
+ console.error(`❌ Задача ${req.id.slice(0, 8)}... ошибка: ${err.message}`);
109
+ }
110
+ this.activeRequests--;
111
+ }
112
+ async callClaudeAPI(payload) {
113
+ const res = await fetch(ANTHROPIC_API_URL, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Authorization': `Bearer ${this.oauthToken}`,
117
+ 'anthropic-version': '2023-06-01',
118
+ 'content-type': 'application/json',
119
+ },
120
+ body: JSON.stringify(payload),
121
+ });
122
+ if (!res.ok) {
123
+ const errText = await res.text().catch(() => '');
124
+ throw new Error(`Claude API ${res.status}: ${errText.slice(0, 200)}`);
125
+ }
126
+ return res.json();
127
+ }
128
+ scheduleReconnect() {
129
+ if (this.reconnecting)
130
+ return;
131
+ this.reconnecting = true;
132
+ setTimeout(() => {
133
+ if (this.shouldReconnect) {
134
+ this.connect();
135
+ }
136
+ }, RECONNECT_DELAY);
137
+ }
138
+ startHeartbeat() {
139
+ this.heartbeatTimer = setInterval(() => {
140
+ this.send({ type: 'heartbeat', activeRequests: this.activeRequests });
141
+ }, HEARTBEAT_INTERVAL);
142
+ }
143
+ stopHeartbeat() {
144
+ if (this.heartbeatTimer) {
145
+ clearInterval(this.heartbeatTimer);
146
+ this.heartbeatTimer = null;
147
+ }
148
+ }
149
+ }
150
+ exports.BlitsBridge = BlitsBridge;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const bridge_1 = require("./bridge");
5
+ const auth_1 = require("./auth");
6
+ const BLITS_WS_URL = process.env.BLITS_WS_URL || 'wss://blits.app/bridge/ws';
7
+ function parseArgs() {
8
+ const args = process.argv.slice(2);
9
+ const result = {};
10
+ for (let i = 0; i < args.length; i++) {
11
+ if (args[i] === '--token' && args[i + 1]) {
12
+ result.token = args[++i];
13
+ }
14
+ else if (args[i] === '--server' && args[i + 1]) {
15
+ result.server = args[++i];
16
+ }
17
+ else if (args[i] === '--help' || args[i] === '-h') {
18
+ console.log(`
19
+ blits-bridge — мост между Blits и вашей подпиской Claude
20
+
21
+ Использование:
22
+ npx blits-bridge --token <session-token>
23
+
24
+ Опции:
25
+ --token <token> Токен сессии (получите на train.blits.app/settings)
26
+ --server <url> WebSocket URL сервера (по умолчанию: ${BLITS_WS_URL})
27
+ --help Показать справку
28
+
29
+ Как это работает:
30
+ 1. Bridge подключается к серверу Blits через WebSocket
31
+ 2. Когда агент запускает задачу, запрос приходит сюда
32
+ 3. Bridge выполняет запрос через ваш локальный Claude CLI
33
+ 4. Результат отправляется обратно на сервер
34
+ 5. Вы используете свою подписку Pro/Max — без API ключей
35
+ `);
36
+ process.exit(0);
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ async function main() {
42
+ const { token, server } = parseArgs();
43
+ if (!token) {
44
+ console.error('❌ Укажите токен: blits-bridge --token <session-token>');
45
+ console.error(' Получите токен на train.blits.app/settings');
46
+ process.exit(1);
47
+ }
48
+ console.log('🔗 Blits Bridge v0.1.0');
49
+ console.log('');
50
+ // Step 1: Check Claude CLI auth
51
+ const auth = new auth_1.ClaudeAuth();
52
+ console.log('🔍 Проверяю авторизацию Claude CLI...');
53
+ const isAuthed = await auth.checkAuth();
54
+ if (!isAuthed) {
55
+ console.log('⚠️ Claude CLI не авторизован. Запускаю авторизацию...');
56
+ console.log('');
57
+ const success = await auth.login();
58
+ if (!success) {
59
+ console.error('❌ Не удалось авторизоваться. Запустите вручную: claude auth login');
60
+ process.exit(1);
61
+ }
62
+ console.log('');
63
+ }
64
+ const oauthToken = await auth.getToken();
65
+ if (!oauthToken) {
66
+ console.error('❌ Не удалось получить OAuth токен. Перезапустите: claude auth login');
67
+ process.exit(1);
68
+ }
69
+ console.log('✅ Claude CLI авторизован');
70
+ // Step 2: Connect to Blits
71
+ const wsUrl = server || BLITS_WS_URL;
72
+ console.log(`🌐 Подключаюсь к ${wsUrl}...`);
73
+ const bridge = new bridge_1.BlitsBridge(wsUrl, token, oauthToken);
74
+ bridge.connect();
75
+ // Graceful shutdown
76
+ const shutdown = () => {
77
+ console.log('\n👋 Отключение...');
78
+ bridge.disconnect();
79
+ process.exit(0);
80
+ };
81
+ process.on('SIGINT', shutdown);
82
+ process.on('SIGTERM', shutdown);
83
+ }
84
+ main().catch(err => {
85
+ console.error('❌ Ошибка:', err.message);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,2 @@
1
+ export { BlitsBridge } from './bridge';
2
+ export { ClaudeAuth } from './auth';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClaudeAuth = exports.BlitsBridge = void 0;
4
+ var bridge_1 = require("./bridge");
5
+ Object.defineProperty(exports, "BlitsBridge", { enumerable: true, get: function () { return bridge_1.BlitsBridge; } });
6
+ var auth_1 = require("./auth");
7
+ Object.defineProperty(exports, "ClaudeAuth", { enumerable: true, get: function () { return auth_1.ClaudeAuth; } });
@@ -0,0 +1,36 @@
1
+ export declare class BridgeServer {
2
+ private validateToken;
3
+ private wss;
4
+ private bridges;
5
+ private pendingRequests;
6
+ constructor(validateToken: (token: string) => Promise<string | null>);
7
+ /**
8
+ * Attach to an existing HTTP server or create standalone
9
+ */
10
+ attach(server: any, path?: string): void;
11
+ /**
12
+ * Check if a bridge is connected for a user
13
+ */
14
+ isConnected(userId: string): boolean;
15
+ /**
16
+ * Send a Claude API request through the user's bridge
17
+ */
18
+ callClaude(userId: string, payload: {
19
+ model: string;
20
+ max_tokens: number;
21
+ system?: string;
22
+ messages: Array<{
23
+ role: string;
24
+ content: string;
25
+ }>;
26
+ tools?: any[];
27
+ }, timeoutMs?: number): Promise<any>;
28
+ /**
29
+ * Get status of all connected bridges
30
+ */
31
+ getStatus(): Array<{
32
+ userId: string;
33
+ connectedAt: number;
34
+ activeRequests: number;
35
+ }>;
36
+ }
package/dist/server.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BridgeServer = void 0;
7
+ /**
8
+ * Bridge Server — runs on the Blits server side.
9
+ * Accepts WebSocket connections from blits-bridge clients,
10
+ * routes Claude API requests through them.
11
+ *
12
+ * This module can be imported by any Blits app (training-ground, digital-office, etc.)
13
+ */
14
+ const ws_1 = require("ws");
15
+ const crypto_1 = __importDefault(require("crypto"));
16
+ class BridgeServer {
17
+ validateToken;
18
+ wss = null;
19
+ bridges = new Map(); // userId -> bridge
20
+ pendingRequests = new Map(); // requestId -> pending
21
+ constructor(validateToken) {
22
+ this.validateToken = validateToken;
23
+ }
24
+ /**
25
+ * Attach to an existing HTTP server or create standalone
26
+ */
27
+ attach(server, path = '/bridge/ws') {
28
+ this.wss = new ws_1.WebSocketServer({ server, path });
29
+ this.wss.on('connection', async (ws, req) => {
30
+ const url = new URL(req.url || '', 'http://localhost');
31
+ const token = url.searchParams.get('token');
32
+ if (!token) {
33
+ ws.close(4001, 'Missing token');
34
+ return;
35
+ }
36
+ const userId = await this.validateToken(token);
37
+ if (!userId) {
38
+ ws.close(4002, 'Invalid token');
39
+ return;
40
+ }
41
+ // Close existing bridge for this user
42
+ const existing = this.bridges.get(userId);
43
+ if (existing) {
44
+ existing.ws.close(4003, 'Replaced by new connection');
45
+ }
46
+ const bridge = {
47
+ ws,
48
+ userId,
49
+ connectedAt: Date.now(),
50
+ lastHeartbeat: Date.now(),
51
+ activeRequests: 0,
52
+ };
53
+ this.bridges.set(userId, bridge);
54
+ console.log(`[Bridge] Connected: ${userId}`);
55
+ ws.on('message', (data) => {
56
+ try {
57
+ const msg = JSON.parse(data.toString());
58
+ if (msg.type === 'pong' || msg.type === 'heartbeat') {
59
+ bridge.lastHeartbeat = Date.now();
60
+ bridge.activeRequests = msg.activeRequests || 0;
61
+ return;
62
+ }
63
+ if (msg.type === 'claude_api_result') {
64
+ const pending = this.pendingRequests.get(msg.id);
65
+ if (pending) {
66
+ clearTimeout(pending.timeout);
67
+ this.pendingRequests.delete(msg.id);
68
+ if (msg.error) {
69
+ pending.reject(new Error(msg.error));
70
+ }
71
+ else {
72
+ pending.resolve(msg.payload);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch { }
78
+ });
79
+ ws.on('close', () => {
80
+ if (this.bridges.get(userId)?.ws === ws) {
81
+ this.bridges.delete(userId);
82
+ console.log(`[Bridge] Disconnected: ${userId}`);
83
+ }
84
+ });
85
+ // Send initial ping
86
+ ws.send(JSON.stringify({ type: 'ping' }));
87
+ });
88
+ // Heartbeat check every 60s
89
+ setInterval(() => {
90
+ const now = Date.now();
91
+ for (const [userId, bridge] of this.bridges) {
92
+ if (now - bridge.lastHeartbeat > 90_000) {
93
+ console.log(`[Bridge] Stale connection: ${userId}`);
94
+ bridge.ws.close(4004, 'Heartbeat timeout');
95
+ this.bridges.delete(userId);
96
+ }
97
+ else {
98
+ bridge.ws.send(JSON.stringify({ type: 'ping' }));
99
+ }
100
+ }
101
+ }, 60_000);
102
+ }
103
+ /**
104
+ * Check if a bridge is connected for a user
105
+ */
106
+ isConnected(userId) {
107
+ const bridge = this.bridges.get(userId);
108
+ return !!bridge && bridge.ws.readyState === ws_1.WebSocket.OPEN;
109
+ }
110
+ /**
111
+ * Send a Claude API request through the user's bridge
112
+ */
113
+ async callClaude(userId, payload, timeoutMs = 120_000) {
114
+ const bridge = this.bridges.get(userId);
115
+ if (!bridge || bridge.ws.readyState !== ws_1.WebSocket.OPEN) {
116
+ throw new Error('BRIDGE_NOT_CONNECTED');
117
+ }
118
+ const requestId = crypto_1.default.randomUUID();
119
+ return new Promise((resolve, reject) => {
120
+ const timeout = setTimeout(() => {
121
+ this.pendingRequests.delete(requestId);
122
+ reject(new Error('BRIDGE_TIMEOUT'));
123
+ }, timeoutMs);
124
+ this.pendingRequests.set(requestId, { resolve, reject, timeout });
125
+ bridge.ws.send(JSON.stringify({
126
+ id: requestId,
127
+ type: 'claude_api',
128
+ payload,
129
+ }));
130
+ });
131
+ }
132
+ /**
133
+ * Get status of all connected bridges
134
+ */
135
+ getStatus() {
136
+ return Array.from(this.bridges.entries()).map(([userId, b]) => ({
137
+ userId,
138
+ connectedAt: b.connectedAt,
139
+ activeRequests: b.activeRequests,
140
+ }));
141
+ }
142
+ }
143
+ exports.BridgeServer = BridgeServer;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "blits-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Bridge between Blits platform and local Claude CLI. Runs Claude API calls through your subscription.",
5
+ "bin": {
6
+ "blits-bridge": "./dist/cli.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsc --watch"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "dependencies": {
19
+ "ws": "^8.18.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.19.13",
23
+ "@types/ws": "^8.18.1",
24
+ "typescript": "^5.9.3"
25
+ }
26
+ }