Files
TokenWatch/tokenwatch-sync.js
T
Jannik 44e88f7793
Build and Push Docker Image / build-and-push (push) Canceled after 0s
feat: split antigravity into 4 separate quota bars
2026-08-28 22:22:24 +02:00

209 lines
8.2 KiB
JavaScript

const http = require('http');
const https = require('https');
const os = require('os');
const { exec } = require('child_process');
const fs = require('fs');
const COMMAND_TIMEOUT_MS = 5000;
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
function runCommand(command, timeout = COMMAND_TIMEOUT_MS) {
return new Promise((resolve) => {
exec(command, { timeout, maxBuffer: MAX_BUFFER_BYTES }, (error, stdout, stderr) => {
resolve({ error, stdout: stdout || '', stderr: stderr || '' });
});
});
}
function parseWindowsProcessJson(stdout) {
if (!stdout.trim()) return [];
const parsed = JSON.parse(stdout);
const rows = Array.isArray(parsed) ? parsed : [parsed];
return rows
.filter(row => row && row.ProcessId)
.map(row => ({
pid: Number(row.ProcessId),
ppid: Number(row.ParentProcessId || 0),
commandLine: row.CommandLine || row.ExecutablePath || '',
}));
}
async function listLanguageServerProcesses() {
const command = [
'powershell -NoProfile -Command',
'"Get-CimInstance Win32_Process -Filter \\"name like \'language_server%\'\\"',
'| Select-Object ProcessId,ParentProcessId,CommandLine,ExecutablePath',
'| ConvertTo-Json -Depth 2"',
].join(' ');
const result = await runCommand(command);
if (result.error && !result.stdout.trim()) {
throw new Error(result.stderr.trim() || result.error.message);
}
return parseWindowsProcessJson(result.stdout);
}
function extractArgument(commandLine, argumentName) {
const escapedName = argumentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`--${escapedName}(?:=|\\s+)(?:"([^"]*)"|'([^']*)'|([^\\s]+))`, 'i');
const match = commandLine.match(pattern);
return match ? (match[1] ?? match[2] ?? match[3] ?? null) : null;
}
const API_PATH = '/exa.language_server_pb.LanguageServerService/GetUserStatus';
const requestBody = JSON.stringify({ metadata: { ideName: 'antigravity', extensionName: 'antigravity', locale: 'en' } });
function tryPort(port, csrfToken, protocol) {
return new Promise(resolve => {
const client = protocol === 'https' ? https : http;
const req = client.request({
hostname: '127.0.0.1', port, path: API_PATH, method: 'POST',
...(protocol === 'https' ? { rejectUnauthorized: false } : {}),
headers: {
'X-Codeium-Csrf-Token': csrfToken,
'Connect-Protocol-Version': '1',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody),
},
timeout: 3000,
}, res => {
let body = '';
res.setEncoding('utf8');
res.on('data', chunk => body += chunk);
res.on('end', () => resolve({ port, statusCode: res.statusCode, body }));
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
req.end(requestBody);
});
}
function parseResponse(body) {
const trimmed = body.trim();
if (!trimmed) return null;
try { return JSON.parse(trimmed); } catch {}
// Fallback for NDJSON or streamed lines
for (const line of trimmed.split(/\r?\n/).filter(Boolean)) {
try { return JSON.parse(line); } catch {}
}
return null;
}
async function syncToTokenWatch(quotaData) {
const postData = JSON.stringify(quotaData);
// Ersetze diese URL durch die echte von TokenWatch auf der Jannik-Cloud, sobald sie läuft.
const url = 'https://tokenwatch.orfel.de/api/webhook/antigravity';
const isHttps = url.startsWith('https');
const client = isHttps ? https : http;
return new Promise((resolve) => {
const req = client.request(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }
}, (res) => {
res.on('data', () => {});
res.on('end', () => resolve(true));
});
req.on('error', (err) => {
console.error('Webhook error:', err.message);
resolve(false);
});
req.write(postData);
req.end();
});
}
async function main() {
console.log('Suche Antigravity Language Server...');
const processes = await listLanguageServerProcesses();
const processInfo = processes.find(item => extractArgument(item.commandLine, 'csrf_token'));
if (!processInfo) { console.error('Fehler: Kein Antigravity language_server Prozess gefunden.'); return; }
const csrfToken = extractArgument(processInfo.commandLine, 'csrf_token');
const extensionPort = Number(extractArgument(processInfo.commandLine, 'extension_server_port')) || null;
function extractListeningPorts(stdout, pid) {
const ports = [];
for (const line of stdout.split(/\r?\n/)) {
// (LISTEN|ABH) matches German 'ABHÖREN' from netstat
if (!/(LISTEN|ABH)/i.test(line) || !new RegExp(`\\s${pid}\\s*$`).test(line)) continue;
for (const match of line.matchAll(/(?:127\.0\.0\.1|\[?::1\]?):(\d{2,5})/g)) {
const port = Number(match[1]);
if (port > 0 && port < 65536) ports.push(port);
}
}
return ports;
}
const netstatResult = await runCommand('netstat -ano');
const listeningPorts = extractListeningPorts(netstatResult.stdout, processInfo.pid);
const candidates = [...new Set(listeningPorts)].filter(port => port !== extensionPort).sort((a,b)=>a-b);
let rawData = null;
for (const port of candidates) {
for (const protocol of ['https', 'http']) {
const result = await tryPort(port, csrfToken, protocol);
if (result && result.statusCode === 200) {
rawData = parseResponse(result.body);
if (rawData) break;
}
}
if (rawData) break;
}
const actualData = Array.isArray(rawData) ? rawData[0] : rawData;
const modelConfigs = actualData?.userStatus?.cascadeModelConfigData?.clientModelConfigs || actualData?.userStatus?.modelConfigs;
if (!modelConfigs) {
console.error('Fehler: Konnte keine JSON-Antwort mit Quota auslesen.');
return;
}
// Wir filtern die Gemini und Claude Modelle heraus
const geminiModels = modelConfigs.filter(m => m.label && m.label.includes('Gemini'));
const claudeModels = modelConfigs.filter(m => m.label && m.label.includes('Claude'));
let geminiMinRemaining = 1;
let geminiResetTime = 'Unknown';
let claudeMinRemaining = 1;
let claudeResetTime = 'Unknown';
for (const m of geminiModels) {
if (m.quotaInfo && m.quotaInfo.remainingFraction !== undefined) {
if (m.quotaInfo.remainingFraction < geminiMinRemaining) {
geminiMinRemaining = m.quotaInfo.remainingFraction;
const d = new Date(m.quotaInfo.resetTime);
geminiResetTime = d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
}
}
for (const m of claudeModels) {
if (m.quotaInfo && m.quotaInfo.remainingFraction !== undefined) {
if (m.quotaInfo.remainingFraction < claudeMinRemaining) {
claudeMinRemaining = m.quotaInfo.remainingFraction;
const d = new Date(m.quotaInfo.resetTime);
claudeResetTime = d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
}
}
const geminiRemainingPercent = Math.round(geminiMinRemaining * 100);
const claudeRemainingPercent = Math.round(claudeMinRemaining * 100);
const payload = {
gemini_5h_remaining: geminiRemainingPercent,
gemini_weekly_remaining: geminiRemainingPercent, // Duplicate since API doesn't distinguish
claude_5h_remaining: claudeRemainingPercent,
claude_weekly_remaining: claudeRemainingPercent, // Duplicate since API doesn't distinguish
gemini_reset: `in ${geminiResetTime}`,
claude_reset: `in ${claudeResetTime}`
};
console.log('Gefundene Live-Daten:', payload);
await syncToTokenWatch(payload);
console.log('Erfolgreich an TokenWatch gesendet!');
}
main().catch(console.error);