diff --git a/dump.json b/dump.json new file mode 100644 index 0000000..5acdeb3 --- /dev/null +++ b/dump.json @@ -0,0 +1,49 @@ +{ + "response": { + "groups": [ + { + "displayName": "Gemini Models", + "description": "Models within this group: Gemini Flash, Gemini Pro", + "buckets": [ + { + "bucketId": "gemini-weekly", + "displayName": "Weekly Limit Remaining", + "description": "You have used some of your weekly limit, it will fully refresh in 6 days, 10 hours.", + "window": "weekly", + "remainingFraction": 0.9899833, + "resetTime": "2026-09-05T09:20:06Z" + }, + { + "bucketId": "gemini-5h", + "displayName": "Five Hour Limit Remaining", + "description": "You have used some of your 5-hour limit, it will fully refresh in 4 hours, 54 minutes.", + "window": "5h", + "remainingFraction": 0.9699, + "resetTime": "2026-08-30T03:16:17Z" + } + ] + }, + { + "displayName": "Claude and GPT models", + "description": "Models within this group: Claude Opus, Claude Sonnet, GPT-OSS", + "buckets": [ + { + "bucketId": "3p-weekly", + "displayName": "Weekly Limit Remaining", + "window": "weekly", + "remainingFraction": 1, + "resetTime": "2026-09-05T22:21:46Z" + }, + { + "bucketId": "3p-5h", + "displayName": "Five Hour Limit Remaining", + "window": "5h", + "remainingFraction": 1, + "resetTime": "2026-08-30T03:21:46Z" + } + ] + } + ], + "description": "Within each group, models share a weekly limit and a 5-hour limit. Quota is consumed proportionally to the cost of the tokens. Thus, limits will last longer with shorter tasks or using more cost-effective models. The 5-hour limit smooths out aggregate demand to fairly distribute global capacity across all users, while your weekly limit is tied directly to your individual tier." + } +} \ No newline at end of file diff --git a/test-dump.js b/test-dump.js new file mode 100644 index 0000000..07fa342 --- /dev/null +++ b/test-dump.js @@ -0,0 +1,209 @@ +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/RetrieveUserQuotaSummary'; +const requestBody = JSON.stringify({ metadata: { ideName: 'antigravity', extensionName: 'ag-usage', 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 groups = actualData?.response?.groups; + + if (!groups) { + console.error('Fehler: Konnte keine JSON-Antwort mit Quota auslesen.', rawData); + return; + } + + let gemini5h = 1, geminiWeekly = 1, gemini5hReset = 'Unknown', geminiWeeklyReset = 'Unknown'; + let claude5h = 1, claudeWeekly = 1, claude5hReset = 'Unknown', claudeWeeklyReset = 'Unknown'; + + const formatTime = (ts) => { + if (!ts) return 'Unknown'; + return new Date(ts).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + }; + + for (const group of groups) { + const isGemini = group.displayName && group.displayName.toLowerCase().includes('gemini'); + for (const bucket of group.buckets || []) { + if (isGemini) { + if (bucket.window === '5h') { + gemini5h = bucket.remainingFraction ?? 1; + gemini5hReset = formatTime(bucket.resetTime); + } else if (bucket.window === 'weekly') { + geminiWeekly = bucket.remainingFraction ?? 1; + geminiWeeklyReset = formatTime(bucket.resetTime); + } + } else { + if (bucket.window === '5h') { + claude5h = bucket.remainingFraction ?? 1; + claude5hReset = formatTime(bucket.resetTime); + } else if (bucket.window === 'weekly') { + claudeWeekly = bucket.remainingFraction ?? 1; + claudeWeeklyReset = formatTime(bucket.resetTime); + } + } + } + } + + const payload = { + gemini_5h_remaining: Math.round(gemini5h * 100), + gemini_weekly_remaining: Math.round(geminiWeekly * 100), + claude_5h_remaining: Math.round(claude5h * 100), + claude_weekly_remaining: Math.round(claudeWeekly * 100), + gemini_5h_reset: `in ${gemini5hReset}`, + gemini_weekly_reset: `in ${geminiWeeklyReset}`, + claude_5h_reset: `in ${claude5hReset}`, + claude_weekly_reset: `in ${claudeWeeklyReset}` + }; + + console.log('Gefundene Live-Daten:', payload); + fs.writeFileSync('dump.json', JSON.stringify(actualData, null, 2)); console.log('Dumped'); + console.log('Erfolgreich an TokenWatch gesendet!'); +} + +main().catch(console.error); diff --git a/test-raw.js b/test-raw.js new file mode 100644 index 0000000..356b357 --- /dev/null +++ b/test-raw.js @@ -0,0 +1,68 @@ +const http = require('http'); +const https = require('https'); +const { execSync } = require('child_process'); + +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 = execSync(command, { encoding: 'utf8' }); + return JSON.parse(result); +} + +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/RetrieveUserQuotaSummary'; +const requestBody = JSON.stringify({ metadata: { ideName: 'antigravity', extensionName: 'ag-usage', 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.write(requestBody); + req.end(); + }); +} + +(async () => { + const processes = await listLanguageServerProcesses(); + let procs = Array.isArray(processes) ? processes : (processes ? [processes] : []); + let port, csrfToken, protocol; + for (const proc of procs) { + if (!proc.CommandLine) continue; + const p = extractArgument(proc.CommandLine, 'api_server_port'); + const c = extractArgument(proc.CommandLine, 'csrf_token'); + if (p && c) { + port = p; csrfToken = c; + protocol = proc.CommandLine.includes('--api_server_protocol=https') ? 'https' : 'http'; + break; + } + } + if (!port) return console.log('not found'); + const response = await tryPort(port, csrfToken, protocol); + console.log(response.body); +})();