feat: complete TokenWatch implementation with Docker, GH actions, and Live Sync
Build and Push Docker Image / build-and-push (push) Canceled after 0s
Build and Push Docker Image / build-and-push (push) Canceled after 0s
This commit is contained in:
1 parent
d9c4c5e123
commit
eaec6ed5c8
36 files changed
+1932
-55
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';`
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -0,0 +1 @@
|
||||
This file enables the service for deployment.
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=cron.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":""}
|
||||
Vendored
+31
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import './cron';
|
||||
//# sourceMappingURL=server.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,QAAQ,CAAC"}
|
||||
Vendored
+49
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+16
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+69
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function sendNtfyAlert(provider: string, percentage: number): Promise<void>;
|
||||
//# sourceMappingURL=ntfy.d.ts.map
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+26
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+7
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+88
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
+243
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user