feat: complete TokenWatch implementation with Docker, GH actions, and Live Sync
Build and Push Docker Image / build-and-push (push) Canceled after 0s

This commit is contained in:
Jannik committed 2026-08-28 16:35:08 +02:00
1 parent d9c4c5e123
commit eaec6ed5c8
36 files changed
+1932 -55

No files matched your search

+1 -1
View File
@@ -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
+39
View File
@@ -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 }}
+8 -7
View File
@@ -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"]
+8 -25
View File
@@ -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
+934
View File
@@ -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"
}
]
}
}
}
+20
View File
@@ -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';`
+18
View File
@@ -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
+35
View File
@@ -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" <<EOF
# TokenWatch
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
DB_NAME=${DB_NAME}
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}?sslmode=disable
NTFY_URL=https://ntfy.orfel.de/Token-Watch
EOF
chmod 600 "${SCRIPT_DIR}/.env"
cat > "${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."
+1
View File
@@ -0,0 +1 @@
This file enables the service for deployment.
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=cron.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":""}
+31
View File
@@ -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
+1
View File
@@ -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"}
+2
View File
@@ -0,0 +1,2 @@
import './cron';
//# sourceMappingURL=server.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,QAAQ,CAAC"}
+49
View File
@@ -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
+1
View File
@@ -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"}
+16
View File
@@ -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<void>;
export declare function insertQuota(entry: QuotaEntry): Promise<any>;
export declare function getLatestQuotas(): Promise<any[]>;
export declare function getQuotaHistory(days?: number): Promise<any[]>;
//# sourceMappingURL=db.d.ts.map
+1
View File
@@ -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"}
+69
View File
@@ -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
+1
View File
@@ -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"}
+2
View File
@@ -0,0 +1,2 @@
export declare function sendNtfyAlert(provider: string, percentage: number): Promise<void>;
//# sourceMappingURL=ntfy.d.ts.map
+1
View File
@@ -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"}
+26
View File
@@ -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
+1
View File
@@ -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"}
+7
View File
@@ -0,0 +1,7 @@
import { QuotaEntry } from './db';
export declare function updateAntigravityQuota(data: any): void;
export declare function fetchAntigravityQuota(): Promise<QuotaEntry>;
export declare function fetchPoeQuota(): Promise<QuotaEntry>;
export declare function fetchMinimaxQuota(): Promise<QuotaEntry>;
export declare function fetchAllQuotas(): Promise<QuotaEntry[]>;
//# sourceMappingURL=providers.d.ts.map
+1
View File
@@ -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"}
+88
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/services/providers.ts"],"names":[],"mappings":"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"}
+243
View File
@@ -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,
};
+94
View File
@@ -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,
};
+2
View File
@@ -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})<br>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})<br>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 {
+11
View File
@@ -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}`);
});
+23 -19
View File
@@ -4,26 +4,30 @@ import { QuotaEntry } from './db';
dotenv.config();
let antigravityCache: QuotaEntry = { 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<QuotaEntry> {
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;
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 };
}
return antigravityCache;
}
export async function fetchPoeQuota(): Promise<QuotaEntry> {
+190
View File
@@ -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);
+3 -3
View File
@@ -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",