diff --git a/.env b/.env index a23ad42..9372c88 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ PORT=3000 DB_USER=postgres DB_HOST=postgres DB_NAME=tokenwatch -DB_PASSWORD=your_db_password +DB_PASSWORD=gm+p9vwz%kArhsD.dP6_m,CdA4tERG:GL4Z)-CW$-8F$b$tq DB_PORT=5432 # ntfy Configuration diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..5e6d09a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,39 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + - master + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository }}:latest + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 5299e18..dbed2a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,17 @@ -FROM node:18-alpine +FROM node:24-alpine -WORKDIR /usr/src/app +WORKDIR /app +# Install dependencies first (caching) COPY package*.json ./ RUN npm install -COPY tsconfig.json ./ -COPY src/ ./src/ -COPY public/ ./public/ +# Copy source code and frontend +COPY . . -RUN npx tsc +# Build typescript +RUN npm run build EXPOSE 3000 -CMD ["node", "dist/server.js"] +CMD ["npm", "start"] diff --git a/Push-AntigravityQuota.ps1 b/Push-AntigravityQuota.ps1 index 524eafc..bcf51a5 100644 --- a/Push-AntigravityQuota.ps1 +++ b/Push-AntigravityQuota.ps1 @@ -1,27 +1,10 @@ -param ( - [string]$TokenWatchUrl = "http://token.orfel.de/api/webhook/antigravity", - [int]$WeeklyRemaining = 43, - [int]$FiveHourRemaining = 87, - [string]$WeeklyRefresh = "19 hours, 44 minutes", - [string]$FiveHourRefresh = "4 hours, 35 minutes" -) +# Push-AntigravityQuota.ps1 +# Dieses Skript führt den Node-Synchronizer aus, der das echte Antigravity-Limit +# ausliest und an TokenWatch sendet. +# Zur Einbindung in die Windows Aufgabenplanung geeignet! -# Erstelle den Payload für den Webhook -$payload = @{ - weekly_limit_remaining = $WeeklyRemaining - five_hour_limit_remaining = $FiveHourRemaining - weekly_refresh = $WeeklyRefresh - five_hour_refresh = $FiveHourRefresh -} +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location -Path $ScriptDir -$jsonPayload = $payload | ConvertTo-Json - -Write-Host "Sende Quota an TokenWatch ($TokenWatchUrl)..." -Write-Host $jsonPayload - -try { - $response = Invoke-RestMethod -Uri $TokenWatchUrl -Method Post -Body $jsonPayload -ContentType "application/json" - Write-Host "Erfolgreich gesendet: $($response.message)" -ForegroundColor Green -} catch { - Write-Host "Fehler beim Senden an TokenWatch: $_" -ForegroundColor Red -} +Write-Host "Starte TokenWatch Antigravity Sync..." +node tokenwatch-sync.js diff --git a/debug.json b/debug.json new file mode 100644 index 0000000..dc66046 --- /dev/null +++ b/debug.json @@ -0,0 +1,934 @@ +{ + "userStatus": { + "disableTelemetry": true, + "name": "Jannik Müller", + "email": "jannik.mueller.jannik@googlemail.com", + "planStatus": { + "planInfo": { + "teamsTier": "TEAMS_TIER_PRO", + "planName": "Pro", + "hasAutocompleteFastMode": true, + "allowStickyPremiumModels": true, + "allowPremiumCommandModels": true, + "hasTabToJump": true, + "maxNumPremiumChatMessages": "-1", + "maxNumChatInputTokens": "16384", + "maxCustomChatInstructionCharacters": "600", + "maxNumPinnedContextItems": "-1", + "maxLocalIndexSize": "-1", + "monthlyPromptCredits": 50000, + "monthlyFlowCredits": 150000, + "monthlyFlexCreditPurchaseAmount": 25000, + "canBuyMoreCredits": true, + "cascadeWebSearchEnabled": true, + "canCustomizeAppIcon": true, + "cascadeCanAutoRunCommands": true, + "canGenerateCommitMessages": true, + "knowledgeBaseEnabled": true, + "defaultTeamConfig": { + "allowMcpServers": true, + "allowAutoRunCommands": true, + "allowBrowserExperimentalFeatures": true + }, + "canAllowCascadeInBackground": true, + "browserEnabled": true + }, + "availablePromptCredits": 500, + "availableFlowCredits": 100 + }, + "cascadeModelConfigData": { + "clientModelConfigs": [ + { + "label": "Gemini 3.6 Flash (High)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M71" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.6-flash-high" + }, + { + "label": "Gemini 3.6 Flash (Medium)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M72" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.6-flash-medium" + }, + { + "label": "Gemini 3.6 Flash (Low)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M73" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.6-flash-low" + }, + { + "label": "Gemini 3.5 Flash (High)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M84" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3-flash-agent" + }, + { + "label": "Gemini 3.5 Flash (Medium)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M20" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.5-flash-low" + }, + { + "label": "Gemini 3.1 Pro (High)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M16" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-pro-agent" + }, + { + "label": "Gemini 3.7 Flash (Medium)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M299" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.7-flash-medium" + }, + { + "label": "Gemini 3.5 Flash (Low)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M187" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.5-flash-extra-low" + }, + { + "label": "Gemini 3.1 Pro (Low)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M36" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.1-pro-low" + }, + { + "label": "Claude Sonnet 4.6 (Thinking)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M35" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 1, + "resetTime": "2026-08-28T18:56:56Z" + }, + "supportedMimeTypes": { + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "video/jpeg2000": true, + "video/videoframe/jpeg2000": true + }, + "modelId": "claude-sonnet-4-6" + }, + { + "label": "Claude Opus 4.6 (Thinking)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M26" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 1, + "resetTime": "2026-08-28T18:56:56Z" + }, + "supportedMimeTypes": { + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "video/jpeg2000": true, + "video/videoframe/jpeg2000": true + }, + "modelId": "claude-opus-4-6-thinking" + }, + { + "label": "GPT-OSS 120B (Medium)", + "modelOrAlias": { + "model": "MODEL_OPENAI_GPT_OSS_120B_MEDIUM" + }, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 1, + "resetTime": "2026-08-28T18:56:56Z" + }, + "modelId": "gpt-oss-120b-medium" + }, + { + "label": "Gemini 3.7 Flash (High)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M298" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.7-flash-high" + }, + { + "label": "Gemini 3.7 Flash (Low)", + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M300" + }, + "supportsImages": true, + "isRecommended": true, + "allowedTiers": [ + "TEAMS_TIER_PRO", + "TEAMS_TIER_TEAMS", + "TEAMS_TIER_ENTERPRISE_SELF_HOSTED", + "TEAMS_TIER_ENTERPRISE_SAAS", + "TEAMS_TIER_HYBRID", + "TEAMS_TIER_PRO_ULTIMATE" + ], + "quotaInfo": { + "remainingFraction": 0.7300267, + "resetTime": "2026-08-28T18:11:19Z" + }, + "tagTitle": "Fast", + "tagDescription": "Limited time", + "supportedMimeTypes": { + "application/json": true, + "application/pdf": true, + "application/rtf": true, + "application/x-ipynb+json": true, + "application/x-javascript": true, + "application/x-python-code": true, + "application/x-typescript": true, + "audio/aac": true, + "audio/flac": true, + "audio/l16": true, + "audio/m4a": true, + "audio/mp3": true, + "audio/mp4": true, + "audio/mpeg": true, + "audio/ogg": true, + "audio/opus": true, + "audio/vnd.wave": true, + "audio/wav": true, + "audio/wave": true, + "audio/webm": true, + "audio/webm;codecs=opus": true, + "audio/x-wav": true, + "image/heic": true, + "image/heif": true, + "image/jpeg": true, + "image/png": true, + "image/webp": true, + "text/css": true, + "text/csv": true, + "text/html": true, + "text/javascript": true, + "text/markdown": true, + "text/plain": true, + "text/rtf": true, + "text/x-python": true, + "text/x-python-script": true, + "text/x-typescript": true, + "text/xml": true, + "video/audio/s16le": true, + "video/audio/wav": true, + "video/jpeg2000": true, + "video/mp4": true, + "video/text/timestamp": true, + "video/videoframe/jpeg2000": true, + "video/webm": true + }, + "modelId": "gemini-3.7-flash-low" + } + ], + "clientModelSorts": [ + { + "name": "Recommended", + "groups": [ + { + "modelLabels": [ + "Gemini 3.7 Flash (High)", + "Gemini 3.7 Flash (Medium)", + "Gemini 3.7 Flash (Low)", + "Gemini 3.6 Flash (High)", + "Gemini 3.6 Flash (Medium)", + "Gemini 3.6 Flash (Low)", + "Gemini 3.5 Flash (High)", + "Gemini 3.5 Flash (Medium)", + "Gemini 3.5 Flash (Low)", + "Gemini 3.1 Pro (High)", + "Gemini 3.1 Pro (Low)", + "Claude Sonnet 4.6 (Thinking)", + "Claude Opus 4.6 (Thinking)", + "GPT-OSS 120B (Medium)" + ] + } + ] + } + ], + "defaultOverrideModelConfig": { + "modelOrAlias": { + "model": "MODEL_PLACEHOLDER_M298" + } + } + }, + "acceptedLatestTermsOfService": true, + "userTier": { + "id": "g1-pro-tier", + "name": "Google AI Pro", + "description": "Google AI Pro", + "upgradeSubscriptionUri": "https://antigravity.google/g1-upgrade", + "upgradeSubscriptionText": "You can upgrade to a Google AI Ultra plan to receive higher rate limits.", + "availableCredits": [ + { + "creditType": "GOOGLE_ONE_AI", + "minimumCreditAmountForUsage": "50" + } + ] + } + } +} \ No newline at end of file diff --git a/deployment-template/README.md b/deployment-template/README.md new file mode 100644 index 0000000..06dc2bf --- /dev/null +++ b/deployment-template/README.md @@ -0,0 +1,20 @@ +# TokenWatch Deployment for Jannik-Cloud + +To deploy TokenWatch to your Jannik-Cloud, follow these steps: + +1. Push the `TokenWatch` repository to GitHub (`github.com/orfelorfel23/TokenWatch`). +2. The included GitHub Action (`.github/workflows/docker.yml`) will automatically build the Docker image and push it to `ghcr.io/orfelorfel23/tokenwatch:latest`. +3. In your `Jannik-Cloud` repository, copy the contents of this `deployment-template` directory into `services/tokenwatch/`. +4. Run `bash generate-env.sh` inside `services/tokenwatch/` on your server to generate the database credentials. +5. Commit `.env.age`, `docker-compose.yml`, `tokenwatch.caddy`, and `service.enabled` to `Jannik-Cloud`. +6. Run `sudo bash deploy_script.sh` on the server! + +**Local Sync Setup:** +On your Windows PC where Antigravity IDE runs, open the Windows Task Scheduler. +- Create a new task that runs every 30 minutes. +- Action: Start a program +- Program: `powershell` +- Arguments: `-ExecutionPolicy Bypass -File "C:\GitHub\TokenWatch\Push-AntigravityQuota.ps1"` + +Before running it for production, edit `C:\GitHub\TokenWatch\tokenwatch-sync.js` locally and change the `url` to point to your new cloud endpoint: +`const url = 'https://tokenwatch.orfel.de/api/webhook/antigravity';` diff --git a/deployment-template/docker-compose.yml b/deployment-template/docker-compose.yml new file mode 100644 index 0000000..0ebc6d7 --- /dev/null +++ b/deployment-template/docker-compose.yml @@ -0,0 +1,18 @@ +services: + tokenwatch: + image: ghcr.io/${GITHUB_REPOSITORY:-orfelorfel23/tokenwatch}:latest + container_name: tokenwatch + restart: unless-stopped + ports: + - "127.0.0.1:3500:3000" + env_file: + - .env + environment: + # TokenWatch liest DATABASE_URL und NTFY_URL aus der .env + NODE_ENV: production + networks: + - jannik-cloud-net + +networks: + jannik-cloud-net: + external: true diff --git a/deployment-template/generate-env.sh b/deployment-template/generate-env.sh new file mode 100644 index 0000000..94e1b70 --- /dev/null +++ b/deployment-template/generate-env.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +AGE_PUB_KEY="${REPO_ROOT}/keys/age-public-key.txt" +generate_password() { openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32; } + +# deploy_script.sh automatically reads DB_USER and DB_PASSWORD to create the DB in the shared Postgres. +DB_USER="tokenwatch" +DB_PASSWORD=$(generate_password) +DB_NAME="tokenwatch" + +cat > "${SCRIPT_DIR}/.env" < "${SCRIPT_DIR}/tokenwatch.caddy" <<'EOF' +tokenwatch.orfel.de { + reverse_proxy 127.0.0.1:3500 +} +EOF + +if [[ -f "${AGE_PUB_KEY}" ]]; then + age -r "$(cat "${AGE_PUB_KEY}")" -o "${SCRIPT_DIR}/.env.age" "${SCRIPT_DIR}/.env" + echo "Encrypted .env -> .env.age" +fi +echo "TokenWatch environment generated." diff --git a/deployment-template/service.enabled b/deployment-template/service.enabled new file mode 100644 index 0000000..dbdd56a --- /dev/null +++ b/deployment-template/service.enabled @@ -0,0 +1 @@ +This file enables the service for deployment. diff --git a/dist/cron.d.ts b/dist/cron.d.ts new file mode 100644 index 0000000..db8b6be --- /dev/null +++ b/dist/cron.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=cron.d.ts.map \ No newline at end of file diff --git a/dist/cron.d.ts.map b/dist/cron.d.ts.map new file mode 100644 index 0000000..ee98a25 --- /dev/null +++ b/dist/cron.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/cron.js b/dist/cron.js new file mode 100644 index 0000000..9b62261 --- /dev/null +++ b/dist/cron.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_cron_1 = __importDefault(require("node-cron")); +const providers_1 = require("./services/providers"); +const db_1 = require("./services/db"); +const ntfy_1 = require("./services/ntfy"); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +const ALERT_THRESHOLD = parseInt(process.env.ALERT_THRESHOLD_PERCENT || '80'); +node_cron_1.default.schedule('*/30 * * * *', async () => { + console.log('Running quota check cron job...'); + try { + const quotas = await (0, providers_1.fetchAllQuotas)(); + for (const quota of quotas) { + // Save to DB + await (0, db_1.insertQuota)(quota); + // Send alert if threshold exceeded + if (quota.percentage >= ALERT_THRESHOLD) { + await (0, ntfy_1.sendNtfyAlert)(quota.provider, quota.percentage); + } + } + } + catch (error) { + console.error('Error in cron job:', error); + } +}); +console.log('Cron jobs scheduled'); +//# sourceMappingURL=cron.js.map \ No newline at end of file diff --git a/dist/cron.js.map b/dist/cron.js.map new file mode 100644 index 0000000..ecda177 --- /dev/null +++ b/dist/cron.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cron.js","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":";;;;;AAAA,0DAA6B;AAC7B,oDAAsD;AACtD,sCAA4C;AAC5C,0CAAgD;AAChD,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,IAAI,CAAC,CAAC;AAE9E,mBAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,KAAK,IAAI,EAAE;IACrC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAC/C,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAA,0BAAc,GAAE,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,aAAa;YACb,MAAM,IAAA,gBAAW,EAAC,KAAK,CAAC,CAAC;YAEzB,mCAAmC;YACnC,IAAI,KAAK,CAAC,UAAU,IAAI,eAAe,EAAE,CAAC;gBACtC,MAAM,IAAA,oBAAa,EAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;YAC1D,CAAC;QACL,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/server.d.ts b/dist/server.d.ts new file mode 100644 index 0000000..1cbb5d6 --- /dev/null +++ b/dist/server.d.ts @@ -0,0 +1,2 @@ +import './cron'; +//# sourceMappingURL=server.d.ts.map \ No newline at end of file diff --git a/dist/server.d.ts.map b/dist/server.d.ts.map new file mode 100644 index 0000000..fbc931e --- /dev/null +++ b/dist/server.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,QAAQ,CAAC"} \ No newline at end of file diff --git a/dist/server.js b/dist/server.js new file mode 100644 index 0000000..042759a --- /dev/null +++ b/dist/server.js @@ -0,0 +1,49 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const path_1 = __importDefault(require("path")); +const db_1 = require("./services/db"); +const providers_1 = require("./services/providers"); +require("./cron"); // Start the cron jobs +const app = (0, express_1.default)(); +const PORT = process.env.PORT || 3000; +app.use(express_1.default.static(path_1.default.join(__dirname, '../public'))); +app.use(express_1.default.json()); +app.get('/api/quotas', async (req, res) => { + try { + const quotas = await (0, providers_1.fetchAllQuotas)(); + res.json(quotas); + } + catch (error) { + console.error('Error fetching quotas:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); +app.get('/api/history', async (req, res) => { + try { + const history = await (0, db_1.getQuotaHistory)(); + res.json(history); + } + catch (error) { + console.error('Error fetching history:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); +app.post('/api/webhook/antigravity', (req, res) => { + try { + const { updateAntigravityQuota } = require('./services/providers'); + updateAntigravityQuota(req.body); + res.json({ success: true, message: 'Antigravity quota updated' }); + } + catch (error) { + console.error('Error updating Antigravity via webhook:', error); + res.status(500).json({ error: 'Failed to update' }); + } +}); +app.listen(PORT, () => { + console.log(`TokenWatch server running on port ${PORT}`); +}); +//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/dist/server.js.map b/dist/server.js.map new file mode 100644 index 0000000..da6bb52 --- /dev/null +++ b/dist/server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,sCAAgD;AAChD,oDAAsD;AACtD,kBAAgB,CAAC,sBAAsB;AAEvC,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;AAC3D,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAA,0BAAc,GAAE,CAAC;QACtC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;QAC/C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAC7D,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACvC,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAA,oBAAe,GAAE,CAAC;QACxC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;QAChD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAC7D,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9C,IAAI,CAAC;QACD,MAAM,EAAE,sBAAsB,EAAE,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;QACnE,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,KAAK,CAAC,CAAC;QAChE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACxD,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IAClB,OAAO,CAAC,GAAG,CAAC,qCAAqC,IAAI,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/services/db.d.ts b/dist/services/db.d.ts new file mode 100644 index 0000000..47ddb29 --- /dev/null +++ b/dist/services/db.d.ts @@ -0,0 +1,16 @@ +import { Pool } from 'pg'; +export declare const pool: Pool; +export interface QuotaEntry { + id?: number; + provider: string; + used: number; + total: number; + percentage: number; + timestamp?: Date; + meta?: any; +} +export declare function initDb(): Promise; +export declare function insertQuota(entry: QuotaEntry): Promise; +export declare function getLatestQuotas(): Promise; +export declare function getQuotaHistory(days?: number): Promise; +//# sourceMappingURL=db.d.ts.map \ No newline at end of file diff --git a/dist/services/db.d.ts.map b/dist/services/db.d.ts.map new file mode 100644 index 0000000..4b8a866 --- /dev/null +++ b/dist/services/db.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAK1B,eAAO,MAAM,IAAI,MAMf,CAAC;AAEH,MAAM,WAAW,UAAU;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;CACd;AAED,wBAAsB,MAAM,kBAiB3B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,gBASlD;AAED,wBAAsB,eAAe,mBAQpC;AAED,wBAAsB,eAAe,CAAC,IAAI,GAAE,MAAU,kBASrD"} \ No newline at end of file diff --git a/dist/services/db.js b/dist/services/db.js new file mode 100644 index 0000000..d441763 --- /dev/null +++ b/dist/services/db.js @@ -0,0 +1,69 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pool = void 0; +exports.initDb = initDb; +exports.insertQuota = insertQuota; +exports.getLatestQuotas = getLatestQuotas; +exports.getQuotaHistory = getQuotaHistory; +const pg_1 = require("pg"); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +exports.pool = new pg_1.Pool({ + user: process.env.DB_USER, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432'), +}); +async function initDb() { + const client = await exports.pool.connect(); + try { + await client.query(` + CREATE TABLE IF NOT EXISTS quotas ( + id SERIAL PRIMARY KEY, + provider VARCHAR(50) NOT NULL, + used NUMERIC NOT NULL, + total NUMERIC NOT NULL, + percentage NUMERIC NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + console.log('Database initialized'); + } + finally { + client.release(); + } +} +async function insertQuota(entry) { + const query = ` + INSERT INTO quotas (provider, used, total, percentage) + VALUES ($1, $2, $3, $4) + RETURNING *; + `; + const values = [entry.provider, entry.used, entry.total, entry.percentage]; + const res = await exports.pool.query(query, values); + return res.rows[0]; +} +async function getLatestQuotas() { + const query = ` + SELECT DISTINCT ON (provider) * + FROM quotas + ORDER BY provider, timestamp DESC; + `; + const res = await exports.pool.query(query); + return res.rows; +} +async function getQuotaHistory(days = 7) { + const query = ` + SELECT * + FROM quotas + WHERE timestamp > CURRENT_DATE - $1::integer + ORDER BY timestamp ASC; + `; + const res = await exports.pool.query(query, [days]); + return res.rows; +} +//# sourceMappingURL=db.js.map \ No newline at end of file diff --git a/dist/services/db.js.map b/dist/services/db.js.map new file mode 100644 index 0000000..267995a --- /dev/null +++ b/dist/services/db.js.map @@ -0,0 +1 @@ +{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2BAA0B;AAC1B,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEH,QAAA,IAAI,GAAG,IAAI,SAAI,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;IACzB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;IACzB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;IAC7B,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;IACjC,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC;CAChD,CAAC,CAAC;AAYI,KAAK;IACR,MAAM,MAAM,GAAG,MAAM,QAAA,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;SASlB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACxC,CAAC;YAAS,CAAC;QACP,MAAM,CAAC,OAAO,EAAE,CAAC;IACrB,CAAC;AACL,CAAC;AAEM,KAAK,sBAAsB,KAAiB;IAC/C,MAAM,KAAK,GAAG;;;;KAIb,CAAC;IACF,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,MAAM,QAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAEM,KAAK;IACR,MAAM,KAAK,GAAG;;;;KAIb,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,QAAA,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,OAAO,GAAG,CAAC,IAAI,CAAC;AACpB,CAAC;AAEM,KAAK,0BAA0B,IAAI,GAAW,CAAC;IAClD,MAAM,KAAK,GAAG;;;;;KAKb,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,QAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,OAAO,GAAG,CAAC,IAAI,CAAC;AACpB,CAAC"} \ No newline at end of file diff --git a/dist/services/ntfy.d.ts b/dist/services/ntfy.d.ts new file mode 100644 index 0000000..899ff2f --- /dev/null +++ b/dist/services/ntfy.d.ts @@ -0,0 +1,2 @@ +export declare function sendNtfyAlert(provider: string, percentage: number): Promise; +//# sourceMappingURL=ntfy.d.ts.map \ No newline at end of file diff --git a/dist/services/ntfy.d.ts.map b/dist/services/ntfy.d.ts.map new file mode 100644 index 0000000..cee7413 --- /dev/null +++ b/dist/services/ntfy.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ntfy.d.ts","sourceRoot":"","sources":["../../src/services/ntfy.ts"],"names":[],"mappings":"AAKA,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,iBAevE"} \ No newline at end of file diff --git a/dist/services/ntfy.js b/dist/services/ntfy.js new file mode 100644 index 0000000..8403caa --- /dev/null +++ b/dist/services/ntfy.js @@ -0,0 +1,26 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sendNtfyAlert = sendNtfyAlert; +const axios_1 = __importDefault(require("axios")); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +async function sendNtfyAlert(provider, percentage) { + const url = process.env.NTFY_URL || 'https://ntfy.orfel.de/tokenwatch_dummy'; + const message = `🚨 ${provider} quota is running low! (${percentage.toFixed(1)}% used)`; + try { + await axios_1.default.post(url, message, { + headers: { + 'Title': 'TokenWatch Alert', + 'Tags': 'warning,robot' + } + }); + console.log(`ntfy alert sent for ${provider}`); + } + catch (error) { + console.error('Failed to send ntfy alert:', error.message); + } +} +//# sourceMappingURL=ntfy.js.map \ No newline at end of file diff --git a/dist/services/ntfy.js.map b/dist/services/ntfy.js.map new file mode 100644 index 0000000..e90572b --- /dev/null +++ b/dist/services/ntfy.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ntfy.js","sourceRoot":"","sources":["../../src/services/ntfy.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAC1B,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAET,KAAK,wBAAwB,QAAgB,EAAE,UAAkB;IACpE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,wCAAwC,CAAC;IAC7E,MAAM,OAAO,GAAG,MAAM,QAAQ,2BAA2B,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAExF,IAAI,CAAC;QACD,MAAM,eAAK,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE;YAC3B,OAAO,EAAE;gBACL,OAAO,EAAE,kBAAkB;gBAC3B,MAAM,EAAE,eAAe;aAC1B;SACJ,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/services/providers.d.ts b/dist/services/providers.d.ts new file mode 100644 index 0000000..dc9a997 --- /dev/null +++ b/dist/services/providers.d.ts @@ -0,0 +1,7 @@ +import { QuotaEntry } from './db'; +export declare function updateAntigravityQuota(data: any): void; +export declare function fetchAntigravityQuota(): Promise; +export declare function fetchPoeQuota(): Promise; +export declare function fetchMinimaxQuota(): Promise; +export declare function fetchAllQuotas(): Promise; +//# sourceMappingURL=providers.d.ts.map \ No newline at end of file diff --git a/dist/services/providers.d.ts.map b/dist/services/providers.d.ts.map new file mode 100644 index 0000000..12ad959 --- /dev/null +++ b/dist/services/providers.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/services/providers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAMlC,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,GAAG,QAkB/C;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,UAAU,CAAC,CAEjE;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,CAkBzD;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CA6B7D;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAO5D"} \ No newline at end of file diff --git a/dist/services/providers.js b/dist/services/providers.js new file mode 100644 index 0000000..36f31ae --- /dev/null +++ b/dist/services/providers.js @@ -0,0 +1,88 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateAntigravityQuota = updateAntigravityQuota; +exports.fetchAntigravityQuota = fetchAntigravityQuota; +exports.fetchPoeQuota = fetchPoeQuota; +exports.fetchMinimaxQuota = fetchMinimaxQuota; +exports.fetchAllQuotas = fetchAllQuotas; +const axios_1 = __importDefault(require("axios")); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +let antigravityCache = { provider: 'Antigravity', used: 0, total: 100, percentage: 0 }; +function updateAntigravityQuota(data) { + const weeklyUsed = 100 - (data.weekly_limit_remaining || 100); + const fiveHourUsed = 100 - (data.five_hour_limit_remaining || 100); + const percentage = Math.max(weeklyUsed, fiveHourUsed); + antigravityCache = { + provider: 'Antigravity', + used: percentage, + total: 100, + percentage: percentage, + meta: { + intervalUsed: fiveHourUsed, + weeklyUsed: weeklyUsed, + intervalResetStr: data.five_hour_refresh || 'Unknown', + weeklyResetStr: data.weekly_refresh || 'Unknown' + } + }; +} +async function fetchAntigravityQuota() { + return antigravityCache; +} +async function fetchPoeQuota() { + try { + const res = await axios_1.default.get('https://api.poe.com/usage/current_balance', { + headers: { + 'Authorization': `Bearer ${process.env.POE_API_KEY}` + } + }); + const available = res.data.current_point_balance || 0; + const total = 2500000; + const used = total - available > 0 ? total - available : 0; + const percentage = (used / total) * 100; + return { provider: 'POE', used, total, percentage }; + } + catch (error) { + console.error('Error fetching POE quota:', error.message); + return { provider: 'POE', used: 0, total: 2500000, percentage: 0 }; + } +} +async function fetchMinimaxQuota() { + try { + const res = await axios_1.default.get('https://api.minimax.io/v1/api/openplatform/coding_plan/remains', { + headers: { + 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}` + } + }); + const remains = res.data.model_remains?.find((r) => r.model_name === 'general'); + const intervalUsed = remains ? (100 - remains.current_interval_remaining_percent) : 0; + const weeklyUsed = remains ? (100 - remains.current_weekly_remaining_percent) : 0; + // Use the highest percentage for the progress bar logic + const percentage = Math.max(intervalUsed, weeklyUsed); + const used = percentage; + const total = 100; + const meta = { + intervalUsed, + weeklyUsed, + intervalReset: remains ? remains.end_time : null, + weeklyReset: remains ? remains.weekly_end_time : null + }; + return { provider: 'MiniMax', used, total, percentage, meta }; + } + catch (error) { + console.error('Error fetching MiniMax quota:', error.message); + return { provider: 'MiniMax', used: 0, total: 100, percentage: 0 }; + } +} +async function fetchAllQuotas() { + const results = await Promise.all([ + fetchAntigravityQuota(), + fetchPoeQuota(), + fetchMinimaxQuota() + ]); + return results; +} +//# sourceMappingURL=providers.js.map \ No newline at end of file diff --git a/dist/services/providers.js.map b/dist/services/providers.js.map new file mode 100644 index 0000000..70f8c3b --- /dev/null +++ b/dist/services/providers.js.map @@ -0,0 +1 @@ +{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/services/providers.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,kDAA0B;AAC1B,oDAA4B;AAG5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,IAAI,gBAAgB,GAAe,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;AAEnG,gCAAuC,IAAS;IAC5C,MAAM,UAAU,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,sBAAsB,IAAI,GAAG,CAAC,CAAC;IAC9D,MAAM,YAAY,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,yBAAyB,IAAI,GAAG,CAAC,CAAC;IAEnE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAEtD,gBAAgB,GAAG;QACf,QAAQ,EAAE,aAAa;QACvB,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,GAAG;QACV,UAAU,EAAE,UAAU;QACtB,IAAI,EAAE;YACF,YAAY,EAAE,YAAY;YAC1B,UAAU,EAAE,UAAU;YACtB,gBAAgB,EAAE,IAAI,CAAC,iBAAiB,IAAI,SAAS;YACrD,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,SAAS;SACnD;KACJ,CAAC;AACN,CAAC;AAEM,KAAK;IACR,OAAO,gBAAgB,CAAC;AAC5B,CAAC;AAEM,KAAK;IACR,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,2CAA2C,EAAE;YACrE,OAAO,EAAE;gBACL,eAAe,EAAE,UAAU,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE;aACvD;SACJ,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,qBAAqB,IAAI,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,OAAO,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,UAAU,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;QAExC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IACxD,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACvE,CAAC;AACL,CAAC;AAEM,KAAK;IACR,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,gEAAgE,EAAE;YAC1F,OAAO,EAAE;gBACL,eAAe,EAAE,UAAU,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE;aAC3D;SACJ,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC;QACrF,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,kCAAkC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtF,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAElF,wDAAwD;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,UAAU,CAAC;QACxB,MAAM,KAAK,GAAG,GAAG,CAAC;QAElB,MAAM,IAAI,GAAG;YACT,YAAY;YACZ,UAAU;YACV,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;YAChD,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI;SACxD,CAAC;QAEF,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IAClE,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACvE,CAAC;AACL,CAAC;AAEM,KAAK;IACR,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC9B,qBAAqB,EAAE;QACvB,aAAa,EAAE;QACf,iBAAiB,EAAE;KACtB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACnB,CAAC"} \ No newline at end of file diff --git a/fetch-quota.js b/fetch-quota.js new file mode 100644 index 0000000..e990bc1 --- /dev/null +++ b/fetch-quota.js @@ -0,0 +1,243 @@ +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, +}; diff --git a/language-server-process-utils.js b/language-server-process-utils.js new file mode 100644 index 0000000..0925888 --- /dev/null +++ b/language-server-process-utils.js @@ -0,0 +1,94 @@ +const { exec } = require('child_process'); +const os = require('os'); + +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 || '', + })); +} + +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; +} + +function parseUnixProcessList(stdout) { + return stdout + .split(/\r?\n/) + .map(line => line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/)) + .filter(Boolean) + .map(match => ({ + pid: Number(match[1]), + ppid: Number(match[2]), + commandLine: match[3], + })) + .filter(processInfo => /language_server/i.test(processInfo.commandLine)); +} + +async function listLanguageServerProcesses() { + if (os.platform() === 'win32') { + 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); + } + + const result = await runCommand('ps -A -ww -o pid=,ppid=,args='); + if (result.error && !result.stdout.trim()) { + throw new Error(result.stderr.trim() || result.error.message); + } + return parseUnixProcessList(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; +} + +module.exports = { + extractArgument, + listLanguageServerProcesses, + runCommand, +}; diff --git a/public/app.js b/public/app.js index 50766aa..48402b1 100644 --- a/public/app.js +++ b/public/app.js @@ -49,6 +49,8 @@ function updateCard(providerId, quota) { const reset5h = new Date(quota.meta.intervalReset).toLocaleTimeString(); const resetWeekly = new Date(quota.meta.weeklyReset).toLocaleDateString(); statusText.innerHTML = `5h Limit: ${quota.meta.intervalUsed}% (Resets: ${reset5h})
Weekly: ${quota.meta.weeklyUsed}% (Resets: ${resetWeekly})`; + } else if (quota.provider.toLowerCase() === 'antigravity' && quota.meta) { + statusText.innerHTML = `5h Limit: ${quota.meta.intervalUsed}% (Resets: ${quota.meta.intervalResetStr})
Weekly: ${quota.meta.weeklyUsed}% (Resets: ${quota.meta.weeklyResetStr})`; } else if (quota.provider.toLowerCase() === 'poe') { statusText.innerText = `${percentage.toFixed(1)}% Used (${(quota.used).toLocaleString()} / ${(quota.total).toLocaleString()})`; } else { diff --git a/src/server.ts b/src/server.ts index 1f3abbc..56e750c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,6 +30,17 @@ app.get('/api/history', async (req, res) => { } }); +app.post('/api/webhook/antigravity', (req, res) => { + try { + const { updateAntigravityQuota } = require('./services/providers'); + updateAntigravityQuota(req.body); + res.json({ success: true, message: 'Antigravity quota updated' }); + } catch (error) { + console.error('Error updating Antigravity via webhook:', error); + res.status(500).json({ error: 'Failed to update' }); + } +}); + app.listen(PORT, () => { console.log(`TokenWatch server running on port ${PORT}`); }); diff --git a/src/services/providers.ts b/src/services/providers.ts index f43be36..820a848 100644 --- a/src/services/providers.ts +++ b/src/services/providers.ts @@ -4,26 +4,30 @@ import { QuotaEntry } from './db'; dotenv.config(); -export async function fetchAntigravityQuota(): Promise { - try { - const url = process.env.ANTIGRAVITY_ENDPOINT || 'https://api.antigravity.google/v1/quota'; - const res = await axios.get(url, { - headers: { - 'Authorization': `Bearer ${process.env.ANTIGRAVITY_API_KEY}` - } - }); - - // Assuming response structure: { used: number, total: number } - const used = res.data.used || 0; - const total = res.data.total || 1000; - const percentage = (used / total) * 100; +let antigravityCache: QuotaEntry = { provider: 'Antigravity', used: 0, total: 100, percentage: 0 }; - return { provider: 'Antigravity', used, total, percentage }; - } catch (error: any) { - console.error('Error fetching Antigravity quota:', error.message); - // Fallback for demonstration / local testing if endpoint is mocked - return { provider: 'Antigravity', used: 0, total: 100, percentage: 0 }; - } +export function updateAntigravityQuota(data: any) { + const weeklyUsed = 100 - (data.weekly_limit_remaining || 100); + const fiveHourUsed = 100 - (data.five_hour_limit_remaining || 100); + + const percentage = Math.max(weeklyUsed, fiveHourUsed); + + antigravityCache = { + provider: 'Antigravity', + used: percentage, + total: 100, + percentage: percentage, + meta: { + intervalUsed: fiveHourUsed, + weeklyUsed: weeklyUsed, + intervalResetStr: data.five_hour_refresh || 'Unknown', + weeklyResetStr: data.weekly_refresh || 'Unknown' + } + }; +} + +export async function fetchAntigravityQuota(): Promise { + return antigravityCache; } export async function fetchPoeQuota(): Promise { diff --git a/tokenwatch-sync.js b/tokenwatch-sync.js new file mode 100644 index 0000000..c09bfe5 --- /dev/null +++ b/tokenwatch-sync.js @@ -0,0 +1,190 @@ +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 = 'http://localhost:3000/api/webhook/antigravity'; + return new Promise((resolve) => { + const req = http.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 Modelle heraus + const geminiModels = modelConfigs.filter(m => m.label && m.label.includes('Gemini')); + let minRemaining = 1; + let resetTime = 'Unknown'; + + for (const m of geminiModels) { + if (m.quotaInfo && m.quotaInfo.remainingFraction !== undefined) { + if (m.quotaInfo.remainingFraction < minRemaining) { + minRemaining = m.quotaInfo.remainingFraction; + const d = new Date(m.quotaInfo.resetTime); + resetTime = d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + } + } + } + + // Wir nehmen den niedrigsten verbleibenden Wert und formatieren ihn. + const remainingPercent = Math.round(minRemaining * 100); + const usedPercent = 100 - remainingPercent; + + const payload = { + weekly_limit_remaining: remainingPercent, + five_hour_limit_remaining: remainingPercent, + weekly_refresh: `in ${resetTime} (gemäß IDE)`, + five_hour_refresh: `in ${resetTime} (gemäß IDE)` + }; + + console.log('Gefundene Live-Daten:', payload); + await syncToTokenWatch(payload); + console.log('Erfolgreich an TokenWatch gesendet!'); +} + +main().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json index cec4a3a..5df729c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { // File Layout - // "rootDir": "./src", - // "outDir": "./dist", + "rootDir": "./src", + "outDir": "./dist", // Environment Settings // See also https://aka.ms/tsconfig/module @@ -35,7 +35,7 @@ // Recommended Options "strict": true, "jsx": "react-jsx", - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force",