pi-antigravity-rotator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,522 @@
1
+ // Web dashboard for monitoring account rotation status
2
+
3
+ import type { ServerResponse } from "node:http";
4
+ import type { AccountRotator } from "./rotator.js";
5
+
6
+ export function serveDashboard(res: ServerResponse): void {
7
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
8
+ res.end(DASHBOARD_HTML);
9
+ }
10
+
11
+ export function serveStatusApi(res: ServerResponse, rotator: AccountRotator): void {
12
+ res.writeHead(200, {
13
+ "Content-Type": "application/json",
14
+ "Access-Control-Allow-Origin": "*",
15
+ });
16
+ res.end(JSON.stringify(rotator.getStatus()));
17
+ }
18
+
19
+ export function serveEnableApi(res: ServerResponse, rotator: AccountRotator, email: string): void {
20
+ const ok = rotator.enableAccount(email);
21
+ res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" });
22
+ res.end(JSON.stringify({ ok, email }));
23
+ }
24
+
25
+ const DASHBOARD_HTML = `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <title>Pi Antigravity Rotator</title>
31
+ <style>
32
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
33
+
34
+ :root {
35
+ --bg: #0a0a0f;
36
+ --surface: #12121a;
37
+ --surface-hover: #1a1a25;
38
+ --border: #1e1e2e;
39
+ --text: #e0e0e8;
40
+ --text-dim: #6e6e82;
41
+ --accent: #7c5cfc;
42
+ --accent-glow: rgba(124, 92, 252, 0.15);
43
+ --green: #34d399;
44
+ --yellow: #fbbf24;
45
+ --red: #f87171;
46
+ --blue: #60a5fa;
47
+ --orange: #fb923c;
48
+ --radius: 12px;
49
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
50
+ }
51
+
52
+ * { margin: 0; padding: 0; box-sizing: border-box; }
53
+
54
+ body {
55
+ background: var(--bg);
56
+ color: var(--text);
57
+ font-family: var(--font);
58
+ min-height: 100vh;
59
+ padding: 24px;
60
+ }
61
+
62
+ .header {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ margin-bottom: 28px;
67
+ padding-bottom: 20px;
68
+ border-bottom: 1px solid var(--border);
69
+ }
70
+
71
+ .header h1 {
72
+ font-size: 22px;
73
+ font-weight: 700;
74
+ background: linear-gradient(135deg, var(--accent), #a78bfa);
75
+ -webkit-background-clip: text;
76
+ -webkit-text-fill-color: transparent;
77
+ letter-spacing: -0.5px;
78
+ }
79
+
80
+ .header-stats {
81
+ display: flex;
82
+ gap: 24px;
83
+ font-size: 13px;
84
+ color: var(--text-dim);
85
+ }
86
+
87
+ .header-stats span {
88
+ font-family: 'JetBrains Mono', monospace;
89
+ color: var(--text);
90
+ font-weight: 500;
91
+ }
92
+
93
+ .stats-row {
94
+ display: grid;
95
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
96
+ gap: 12px;
97
+ margin-bottom: 24px;
98
+ }
99
+
100
+ .stat-card {
101
+ background: var(--surface);
102
+ border: 1px solid var(--border);
103
+ border-radius: var(--radius);
104
+ padding: 16px 18px;
105
+ }
106
+
107
+ .stat-card .label {
108
+ font-size: 11px;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.8px;
111
+ color: var(--text-dim);
112
+ margin-bottom: 6px;
113
+ }
114
+
115
+ .stat-card .value {
116
+ font-size: 24px;
117
+ font-weight: 700;
118
+ font-family: 'JetBrains Mono', monospace;
119
+ }
120
+
121
+ .model-routing {
122
+ background: var(--surface);
123
+ border: 1px solid var(--border);
124
+ border-radius: var(--radius);
125
+ padding: 16px 18px;
126
+ margin-bottom: 24px;
127
+ }
128
+
129
+ .model-routing-title {
130
+ font-size: 11px;
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.8px;
133
+ color: var(--text-dim);
134
+ margin-bottom: 10px;
135
+ }
136
+
137
+ .model-route {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 10px;
141
+ padding: 6px 0;
142
+ font-size: 13px;
143
+ }
144
+
145
+ .model-route .model-name {
146
+ font-family: 'JetBrains Mono', monospace;
147
+ font-weight: 500;
148
+ width: 180px;
149
+ flex-shrink: 0;
150
+ }
151
+
152
+ .model-route .route-arrow {
153
+ color: var(--text-dim);
154
+ }
155
+
156
+ .model-route .account-name {
157
+ color: var(--accent);
158
+ font-weight: 500;
159
+ }
160
+
161
+ .accounts-grid {
162
+ display: grid;
163
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
164
+ gap: 14px;
165
+ }
166
+
167
+ .account-card {
168
+ background: var(--surface);
169
+ border: 1px solid var(--border);
170
+ border-radius: var(--radius);
171
+ padding: 18px;
172
+ transition: border-color 0.2s, box-shadow 0.2s;
173
+ position: relative;
174
+ overflow: hidden;
175
+ }
176
+
177
+ .account-card:hover { border-color: #2a2a3e; }
178
+ .account-card.active { border-color: var(--accent); box-shadow: 0 0 20px var(--accent-glow); }
179
+ .account-card.cooldown { border-color: rgba(251, 191, 36, 0.3); }
180
+ .account-card.disabled { opacity: 0.5; }
181
+ .account-card.flagged { opacity: 0.6; border-color: rgba(255, 68, 68, 0.4); }
182
+
183
+ .card-header {
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: space-between;
187
+ margin-bottom: 14px;
188
+ }
189
+
190
+ .card-label {
191
+ font-weight: 600;
192
+ font-size: 14px;
193
+ white-space: nowrap;
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ max-width: 180px;
197
+ }
198
+
199
+ .card-badges { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
200
+
201
+ .badge {
202
+ font-size: 10px;
203
+ font-weight: 600;
204
+ text-transform: uppercase;
205
+ letter-spacing: 0.5px;
206
+ padding: 3px 8px;
207
+ border-radius: 6px;
208
+ white-space: nowrap;
209
+ }
210
+
211
+ .badge-active { background: rgba(52, 211, 153, 0.15); color: var(--green); }
212
+ .badge-ready { background: rgba(110, 110, 130, 0.1); color: var(--text-dim); }
213
+ .badge-cooldown { background: rgba(251, 191, 36, 0.15); color: var(--yellow); }
214
+ .badge-exhausted { background: rgba(248, 113, 113, 0.15); color: var(--red); }
215
+ .badge-disabled { background: rgba(248, 113, 113, 0.1); color: #888; }
216
+ .badge-error { background: rgba(251, 146, 60, 0.15); color: var(--orange); }
217
+ .badge-flagged { background: rgba(248, 113, 113, 0.25); color: #ff4444; font-weight: 700; }
218
+ .badge-model { background: rgba(124, 92, 252, 0.1); color: var(--accent); }
219
+
220
+ .card-email {
221
+ font-size: 12px;
222
+ color: var(--text-dim);
223
+ margin-bottom: 12px;
224
+ font-family: 'JetBrains Mono', monospace;
225
+ white-space: nowrap;
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ }
229
+
230
+ .card-stats {
231
+ display: grid;
232
+ grid-template-columns: 1fr 1fr;
233
+ gap: 8px;
234
+ }
235
+
236
+ .card-stat { font-size: 12px; }
237
+ .card-stat .stat-label { color: var(--text-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
238
+ .card-stat .stat-value { font-family: 'JetBrains Mono', monospace; font-weight: 500; font-size: 13px; margin-top: 2px; }
239
+
240
+ .card-error {
241
+ margin-top: 10px;
242
+ padding: 8px 10px;
243
+ background: rgba(248, 113, 113, 0.08);
244
+ border-radius: 8px;
245
+ font-size: 11px;
246
+ color: var(--red);
247
+ font-family: 'JetBrains Mono', monospace;
248
+ word-break: break-all;
249
+ }
250
+
251
+ .card-actions { margin-top: 10px; display: flex; gap: 8px; }
252
+
253
+ .btn-enable {
254
+ font-size: 11px;
255
+ padding: 4px 12px;
256
+ border: 1px solid var(--accent);
257
+ background: transparent;
258
+ color: var(--accent);
259
+ border-radius: 6px;
260
+ cursor: pointer;
261
+ font-family: var(--font);
262
+ font-weight: 500;
263
+ transition: background 0.2s;
264
+ }
265
+ .btn-enable:hover { background: var(--accent-glow); }
266
+
267
+ .cooldown-bar {
268
+ position: absolute;
269
+ bottom: 0;
270
+ left: 0;
271
+ height: 3px;
272
+ background: linear-gradient(90deg, var(--yellow), var(--orange));
273
+ transition: width 1s linear;
274
+ border-radius: 0 3px 0 0;
275
+ }
276
+
277
+ .quota-section {
278
+ margin-top: 10px;
279
+ padding-top: 10px;
280
+ border-top: 1px solid var(--border);
281
+ }
282
+
283
+ .quota-section-title {
284
+ font-size: 10px;
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.5px;
287
+ color: var(--text-dim);
288
+ margin-bottom: 6px;
289
+ }
290
+
291
+ .quota-row {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 8px;
295
+ margin-bottom: 5px;
296
+ }
297
+
298
+ .quota-model {
299
+ font-size: 11px;
300
+ font-family: 'JetBrains Mono', monospace;
301
+ color: var(--text-dim);
302
+ width: 52px;
303
+ flex-shrink: 0;
304
+ }
305
+
306
+ .quota-timer {
307
+ font-size: 9px;
308
+ font-family: 'JetBrains Mono', monospace;
309
+ padding: 1px 4px;
310
+ border-radius: 3px;
311
+ flex-shrink: 0;
312
+ }
313
+
314
+ .timer-fresh { background: rgba(52, 211, 153, 0.1); color: var(--green); }
315
+ .timer-7d { background: rgba(96, 165, 250, 0.1); color: var(--blue); }
316
+ .timer-5h { background: rgba(251, 191, 36, 0.1); color: var(--yellow); }
317
+
318
+ .quota-bar-bg {
319
+ flex: 1;
320
+ height: 8px;
321
+ background: rgba(255,255,255,0.05);
322
+ border-radius: 4px;
323
+ overflow: hidden;
324
+ }
325
+
326
+ .quota-bar-fill {
327
+ height: 100%;
328
+ border-radius: 4px;
329
+ transition: width 0.5s ease;
330
+ }
331
+
332
+ .quota-pct {
333
+ font-size: 11px;
334
+ font-family: 'JetBrains Mono', monospace;
335
+ font-weight: 600;
336
+ width: 36px;
337
+ text-align: right;
338
+ flex-shrink: 0;
339
+ }
340
+
341
+ .quota-reset {
342
+ font-size: 9px;
343
+ font-family: 'JetBrains Mono', monospace;
344
+ color: var(--text-dim);
345
+ width: 55px;
346
+ text-align: right;
347
+ flex-shrink: 0;
348
+ }
349
+
350
+ .pulse { animation: pulse 2s ease-in-out infinite; }
351
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
352
+ </style>
353
+ </head>
354
+ <body>
355
+
356
+ <div class="header">
357
+ <h1>Pi Antigravity Rotator</h1>
358
+ <div class="header-stats">
359
+ Uptime: <span id="uptime">--</span> |
360
+ Port: <span id="port">--</span> |
361
+ Rotation: <span id="rotation">--</span> reqs
362
+ </div>
363
+ </div>
364
+
365
+ <div class="stats-row">
366
+ <div class="stat-card">
367
+ <div class="label">Total Requests</div>
368
+ <div class="value" id="totalRequests">0</div>
369
+ </div>
370
+ <div class="stat-card">
371
+ <div class="label">Accounts</div>
372
+ <div class="value" id="accountCounts">0</div>
373
+ </div>
374
+ <div class="stat-card">
375
+ <div class="label">Healthy</div>
376
+ <div class="value" id="healthyCount" style="color:var(--green)">0</div>
377
+ </div>
378
+ </div>
379
+
380
+ <div class="model-routing" id="modelRouting"></div>
381
+
382
+ <div class="accounts-grid" id="accounts"></div>
383
+
384
+ <script>
385
+ function formatDuration(ms) {
386
+ if (ms <= 0) return '--';
387
+ var s = Math.floor(ms / 1000);
388
+ if (s < 60) return s + 's';
389
+ var m = Math.floor(s / 60);
390
+ if (m < 60) return m + 'm ' + (s % 60) + 's';
391
+ var h = Math.floor(m / 60);
392
+ if (h < 24) return h + 'h ' + (m % 60) + 'm';
393
+ var d = Math.floor(h / 24);
394
+ return d + 'd ' + (h % 24) + 'h';
395
+ }
396
+
397
+ function formatTime(ts) {
398
+ if (!ts) return '--';
399
+ return new Date(ts).toLocaleTimeString();
400
+ }
401
+
402
+ function quotaBarColor(pct) {
403
+ if (pct >= 60) return 'var(--green)';
404
+ if (pct >= 30) return 'var(--yellow)';
405
+ return 'var(--red)';
406
+ }
407
+
408
+ function renderQuotaBars(quota) {
409
+ if (!quota || quota.length === 0) return '';
410
+ var rows = quota.map(function(q) {
411
+ var color = quotaBarColor(q.percentRemaining);
412
+ var timerClass = 'timer-' + q.timerType;
413
+ var resetLabel = '';
414
+ if (q.resetTime && q.timerType !== 'fresh') {
415
+ var remaining = new Date(q.resetTime).getTime() - Date.now();
416
+ if (remaining > 0) resetLabel = formatDuration(remaining);
417
+ }
418
+ return '<div class="quota-row">' +
419
+ '<span class="quota-model">' + q.displayName + '</span>' +
420
+ '<span class="quota-timer ' + timerClass + '">' + q.timerType + '</span>' +
421
+ '<div class="quota-bar-bg"><div class="quota-bar-fill" style="width:' + q.percentRemaining + '%;background:' + color + '"></div></div>' +
422
+ '<span class="quota-pct" style="color:' + color + '">' + q.percentRemaining + '%</span>' +
423
+ '<span class="quota-reset">' + (resetLabel || '--') + '</span>' +
424
+ '</div>';
425
+ }).join('');
426
+ return '<div class="quota-section"><div class="quota-section-title">Quota (per model)</div>' + rows + '</div>';
427
+ }
428
+
429
+ function renderModelRouting(activeAccounts) {
430
+ var container = document.getElementById('modelRouting');
431
+ var entries = Object.entries(activeAccounts || {});
432
+ if (entries.length === 0) {
433
+ container.innerHTML = '<div class="model-routing-title">Model Routing</div><div style="color:var(--text-dim);font-size:13px;">No model assignments yet (waiting for first request)</div>';
434
+ return;
435
+ }
436
+ var rows = entries.map(function(e) {
437
+ return '<div class="model-route">' +
438
+ '<span class="model-name">' + e[0] + '</span>' +
439
+ '<span class="route-arrow">-></span>' +
440
+ '<span class="account-name">' + e[1] + '</span>' +
441
+ '</div>';
442
+ }).join('');
443
+ container.innerHTML = '<div class="model-routing-title">Model Routing</div>' + rows;
444
+ }
445
+
446
+ function renderAccounts(data) {
447
+ var now = Date.now();
448
+ document.getElementById('uptime').textContent = formatDuration(data.uptime);
449
+ document.getElementById('port').textContent = data.proxyPort;
450
+ document.getElementById('rotation').textContent = data.requestsPerRotation;
451
+ document.getElementById('totalRequests').textContent = data.totalRequestsAllAccounts;
452
+ document.getElementById('accountCounts').textContent = data.accounts.length;
453
+ document.getElementById('healthyCount').textContent =
454
+ data.accounts.filter(function(a) { return a.status === 'active' || a.status === 'ready'; }).length;
455
+
456
+ renderModelRouting(data.activeAccounts);
457
+
458
+ var container = document.getElementById('accounts');
459
+ container.innerHTML = data.accounts.map(function(a) {
460
+ var isActive = a.status === 'active';
461
+ var isCooldown = a.status === 'cooldown' || a.status === 'exhausted';
462
+ var isDisabled = a.status === 'disabled' || a.status === 'flagged';
463
+
464
+ var cooldownPercent = 0;
465
+ if (isCooldown && a.cooldownRemaining > 0) {
466
+ var totalCooldown = a.cooldownUntil - (a.lastUsed || now);
467
+ cooldownPercent = Math.max(0, Math.min(100, (a.cooldownRemaining / Math.max(totalCooldown, 1)) * 100));
468
+ }
469
+
470
+ var modelBadges = (a.activeForModels || []).map(function(m) {
471
+ return '<span class="badge badge-model">' + m.split('-').slice(0, 2).join('-') + '</span>';
472
+ }).join('');
473
+
474
+ return '<div class="account-card ' + a.status + '">' +
475
+ '<div class="card-header">' +
476
+ '<div class="card-label">' + a.label + '</div>' +
477
+ '<div class="card-badges">' +
478
+ '<span class="badge badge-' + a.status + (isActive ? ' pulse' : '') + '">' + a.status + '</span>' +
479
+ modelBadges +
480
+ '</div>' +
481
+ '</div>' +
482
+ '<div class="card-email">' + a.email + '</div>' +
483
+ (a.quota && a.quota.length > 0 ? renderQuotaBars(a.quota) : '') +
484
+ '<div class="card-stats">' +
485
+ '<div class="card-stat"><div class="stat-label">Requests</div><div class="stat-value">' +
486
+ a.requestsSinceRotation + ' / ' + a.totalRequests + ' total</div></div>' +
487
+ '<div class="card-stat"><div class="stat-label">Last Used</div><div class="stat-value">' +
488
+ (a.lastUsed ? formatTime(a.lastUsed) : '--') + '</div></div>' +
489
+ (isCooldown ? '<div class="card-stat"><div class="stat-label">Cooldown</div><div class="stat-value" style="color:var(--yellow)">' +
490
+ formatDuration(a.cooldownRemaining) + '</div></div>' : '') +
491
+ '<div class="card-stat"><div class="stat-label">Token</div><div class="stat-value" style="color:' +
492
+ (a.hasValidToken ? 'var(--green)' : 'var(--text-dim)') + '">' +
493
+ (a.hasValidToken ? 'Valid' : 'Expired') + '</div></div>' +
494
+ '</div>' +
495
+ (a.lastError ? '<div class="card-error">' + a.lastError.slice(0, 150) + '</div>' : '') +
496
+ (isDisabled ? '<div class="card-actions"><button class="btn-enable" onclick="enableAccount(\\'' +
497
+ a.email + '\\')">Re-enable</button></div>' : '') +
498
+ (isCooldown && cooldownPercent > 0 ? '<div class="cooldown-bar" style="width:' + cooldownPercent + '%"></div>' : '') +
499
+ '</div>';
500
+ }).join('');
501
+ }
502
+
503
+ async function enableAccount(email) {
504
+ await fetch('/api/enable/' + encodeURIComponent(email), { method: 'POST' });
505
+ refresh();
506
+ }
507
+
508
+ async function refresh() {
509
+ try {
510
+ var res = await fetch('/api/status');
511
+ var data = await res.json();
512
+ renderAccounts(data);
513
+ } catch (err) {
514
+ console.error('Status fetch failed:', err);
515
+ }
516
+ }
517
+
518
+ refresh();
519
+ setInterval(refresh, 3000);
520
+ </script>
521
+ </body>
522
+ </html>`;
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ // Entry point - loads config and starts the proxy
2
+
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import type { Config } from "./types.js";
5
+ import { AccountRotator } from "./rotator.js";
6
+ import { startProxy } from "./proxy.js";
7
+ import { getAccountsPath } from "./paths.js";
8
+
9
+ function loadConfig(): Config {
10
+ const configPath = getAccountsPath();
11
+ if (!existsSync(configPath)) {
12
+ console.error(`Config not found: ${configPath}`);
13
+ console.error("Run 'pi-antigravity-rotator login' to add your first account.");
14
+ process.exit(1);
15
+ }
16
+
17
+ try {
18
+ const raw = readFileSync(configPath, "utf-8");
19
+ const config: Config = JSON.parse(raw);
20
+
21
+ if (!config.accounts || config.accounts.length === 0) {
22
+ console.error("No accounts configured. Run 'pi-antigravity-rotator login' to add one.");
23
+ process.exit(1);
24
+ }
25
+
26
+ // Set defaults
27
+ config.proxyPort = config.proxyPort || 51200;
28
+ config.requestsPerRotation = config.requestsPerRotation || 5;
29
+ config.rotateOnQuotaDrop = config.rotateOnQuotaDrop ?? 20;
30
+ config.quotaPollIntervalMs = config.quotaPollIntervalMs || 300_000;
31
+
32
+ return config;
33
+ } catch (err) {
34
+ console.error(`Failed to parse ${configPath}: ${err}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ export function main(): void {
40
+ console.log("=== Pi Antigravity Rotator ===");
41
+ console.log();
42
+
43
+ const config = loadConfig();
44
+ console.log(`Loaded ${config.accounts.length} accounts`);
45
+ console.log(`Rotation: ${config.requestsPerRotation} requests / ${config.rotateOnQuotaDrop}% quota drop`);
46
+ console.log(`Quota poll: every ${Math.round((config.quotaPollIntervalMs || 300000) / 1000)}s`);
47
+ console.log();
48
+
49
+ for (const account of config.accounts) {
50
+ console.log(` ${account.label || account.email} (${account.email})`);
51
+ }
52
+ console.log();
53
+
54
+ const rotator = new AccountRotator(config);
55
+ startProxy(rotator, config.proxyPort);
56
+ }
57
+
58
+ // Direct execution
59
+ if (process.argv[1]?.includes("index")) {
60
+ main();
61
+ }