Files
TokenWatch/fetch-quota.js
T
Jannik eaec6ed5c8
Build and Push Docker Image / build-and-push (push) Canceled after 0s
feat: complete TokenWatch implementation with Docker, GH actions, and Live Sync
2026-08-28 16:35:08 +02:00

244 lines
8.1 KiB
JavaScript

const http = require('http');
const https = require('https');
const os = require('os');
const fs = require('fs');
const {
extractArgument,
listLanguageServerProcesses,
runCommand,
} = require('./language-server-process-utils');
const API_PATH = '/exa.language_server_pb.LanguageServerService/GetUserStatus';
const REQUEST_TIMEOUT_MS = 3000;
const requestBody = JSON.stringify({
metadata: {
ideName: 'antigravity',
extensionName: 'antigravity',
locale: 'en',
},
});
function printHelp() {
console.log(`Usage: node scripts/debug/fetch-real-quota.js
Discover a running Antigravity Language Server and fetch GetUserStatus.
The complete real response is printed.`);
}
function belongsToProcess(line, pid) {
if (os.platform() === 'win32') {
return new RegExp(`\\s${pid}\\s*$`).test(line);
}
if (os.platform() === 'darwin') {
const columns = line.trim().split(/\s+/);
return Number(columns[1]) === pid;
}
return new RegExp(`(?:pid=${pid}[,)]|\\s${pid}/)`).test(line);
}
function extractListeningPorts(stdout, pid) {
const ports = [];
for (const line of stdout.split(/\r?\n/)) {
if (!/(LISTEN|ABH)/i.test(line) || !belongsToProcess(line, pid)) 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;
}
async function discoverProcListeningPorts(pid) {
let fdNames;
try {
fdNames = await fs.promises.readdir(`/proc/${pid}/fd`);
} catch {
return [];
}
const socketInodes = new Set();
await Promise.all(fdNames.map(async fdName => {
try {
const target = await fs.promises.readlink(`/proc/${pid}/fd/${fdName}`);
const match = target.match(/^socket:\[(\d+)\]$/);
if (match) socketInodes.add(match[1]);
} catch {
// File descriptors can disappear while the process is running.
}
}));
if (socketInodes.size === 0) return [];
const ports = [];
for (const tablePath of ['/proc/net/tcp', '/proc/net/tcp6']) {
let table;
try {
table = await fs.promises.readFile(tablePath, 'utf8');
} catch {
continue;
}
for (const row of table.split(/\r?\n/).slice(1)) {
const columns = row.trim().split(/\s+/);
if (columns.length < 10 || columns[3] !== '0A' || !socketInodes.has(columns[9])) continue;
const [address, portHex] = columns[1].split(':');
const isIpv4Loopback = address === '0100007F';
const isIpv6Loopback = address === '00000000000000000000000001000000';
if (!portHex || (!isIpv4Loopback && !isIpv6Loopback)) continue;
const port = Number.parseInt(portHex, 16);
if (port > 0 && port < 65536) ports.push(port);
}
}
return ports;
}
async function discoverListeningPorts(pid) {
let ports = [];
if (os.platform() === 'win32') {
const result = await runCommand('netstat -ano');
ports = extractListeningPorts(result.stdout, pid);
} else if (os.platform() === 'darwin') {
const result = await runCommand('lsof -nP -iTCP -sTCP:LISTEN');
ports = extractListeningPorts(result.stdout, pid);
} else {
const ssResult = await runCommand('ss -tlnp');
ports = extractListeningPorts(ssResult.stdout, pid);
if (ports.length === 0) {
const netstatResult = await runCommand('netstat -tlnp');
ports = extractListeningPorts(netstatResult.stdout, pid);
}
if (ports.length === 0) {
ports = await discoverProcListeningPorts(pid);
}
}
return [...new Set(ports)].sort((a, b) => a - b);
}
function tryPort(port, csrfToken, protocol) {
return new Promise(resolve => {
const client = protocol === 'https' ? https : http;
const request = 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: REQUEST_TIMEOUT_MS,
}, response => {
let body = '';
response.setEncoding('utf8');
response.on('data', chunk => {
body += chunk;
});
response.on('end', () => {
resolve({ port, statusCode: response.statusCode || 0, body });
});
});
request.on('error', () => resolve(null));
request.on('timeout', () => {
request.destroy();
resolve(null);
});
request.end(requestBody);
});
}
function parseResponse(body) {
const trimmed = body.trim();
if (!trimmed) return [];
try {
return [JSON.parse(trimmed)];
} catch {
// Streaming responses may contain newline-delimited JSON or framing lines.
}
const values = [];
for (const line of trimmed.split(/\r?\n/).filter(Boolean)) {
try {
values.push(JSON.parse(line));
} catch {
// Streaming responses can include non-JSON framing lines.
}
}
return values;
}
async function main() {
if (process.argv.includes('--help') || process.argv.includes('-h')) {
printHelp();
return;
}
console.log('Fetch real Antigravity quota');
console.log(`Platform: ${os.platform()} (${os.arch()})`);
const processes = await listLanguageServerProcesses();
const processInfo = processes.find(item => extractArgument(item.commandLine, 'csrf_token'));
if (!processInfo) {
throw new Error('No language_server process with --csrf_token was found.');
}
const csrfToken = extractArgument(processInfo.commandLine, 'csrf_token');
const extensionPort = Number(extractArgument(processInfo.commandLine, 'extension_server_port')) || null;
console.log(`Process: PID ${processInfo.pid}, token ${csrfToken}`);
const listeningPorts = await discoverListeningPorts(processInfo.pid);
const candidates = listeningPorts.filter(port => port !== extensionPort);
console.log(`Candidate localhost ports: ${candidates.join(', ') || 'none'}`);
if (candidates.length === 0) {
throw new Error('No candidate localhost listening ports were found.');
}
for (const port of candidates) {
for (const protocol of ['https', 'http']) {
process.stdout.write(`Trying ${protocol}://127.0.0.1:${port}... `);
const result = await tryPort(port, csrfToken, protocol);
if (!result) {
console.log('no response');
continue;
}
if (result.statusCode === 401 || result.statusCode === 403) {
console.log(`HTTP ${result.statusCode}, token rejected`);
continue;
}
if (result.statusCode < 200 || result.statusCode >= 300) {
console.log(`HTTP ${result.statusCode}`);
continue;
}
console.log(`connected (${protocol.toUpperCase()}, HTTP ${result.statusCode})`);
const parsed = parseResponse(result.body);
if (parsed.length === 0) {
throw new Error(`Port ${port} returned no parseable JSON response.`);
}
console.log('\nLanguage Server response:');
console.log(JSON.stringify(parsed.length === 1 ? parsed[0] : parsed, null, 2));
return;
}
}
throw new Error('No candidate port accepted the Language Server request.');
}
if (require.main === module) {
main().catch(error => {
console.error(`Quota fetch failed: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
});
}
module.exports = {
discoverListeningPorts,
};