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 +13 -0
- package/dist/auth.js +107 -0
- package/dist/bridge.d.ts +24 -0
- package/dist/bridge.js +150 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +87 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +143 -0
- package/package.json +26 -0
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;
|
package/dist/bridge.d.ts
ADDED
|
@@ -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
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
|
+
});
|
package/dist/index.d.ts
ADDED
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; } });
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|