pi-antigravity-rotator 1.2.0 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-antigravity-rotator",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/dashboard.ts CHANGED
@@ -366,6 +366,82 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
366
366
 
367
367
  .pulse { animation: pulse 2s ease-in-out infinite; }
368
368
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
369
+
370
+ .badge-pro { background: rgba(52, 211, 153, 0.15); color: var(--green); }
371
+ .badge-free { background: rgba(110, 110, 130, 0.08); color: var(--text-dim); }
372
+ .badge-fmgr { background: rgba(124, 92, 252, 0.15); color: var(--accent); font-size: 9px; }
373
+
374
+ .advisor-panel {
375
+ background: var(--surface);
376
+ border: 1px solid var(--border);
377
+ border-radius: var(--radius);
378
+ padding: 16px 18px;
379
+ margin-bottom: 24px;
380
+ }
381
+
382
+ .advisor-title {
383
+ font-size: 11px;
384
+ text-transform: uppercase;
385
+ letter-spacing: 0.8px;
386
+ color: var(--text-dim);
387
+ margin-bottom: 10px;
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 8px;
391
+ }
392
+
393
+ .advisor-slots {
394
+ font-size: 12px;
395
+ font-family: 'JetBrains Mono', monospace;
396
+ color: var(--text);
397
+ margin-left: auto;
398
+ text-transform: none;
399
+ letter-spacing: 0;
400
+ }
401
+
402
+ .advisor-action {
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 10px;
406
+ padding: 8px 10px;
407
+ margin-bottom: 6px;
408
+ border-radius: 8px;
409
+ font-size: 12px;
410
+ }
411
+
412
+ .advisor-action.add-pro {
413
+ background: rgba(52, 211, 153, 0.06);
414
+ border-left: 3px solid var(--green);
415
+ }
416
+
417
+ .advisor-action.remove-pro {
418
+ background: rgba(251, 191, 36, 0.06);
419
+ border-left: 3px solid var(--yellow);
420
+ }
421
+
422
+ .advisor-action-type {
423
+ font-weight: 600;
424
+ font-size: 10px;
425
+ text-transform: uppercase;
426
+ letter-spacing: 0.5px;
427
+ padding: 2px 6px;
428
+ border-radius: 4px;
429
+ flex-shrink: 0;
430
+ }
431
+
432
+ .advisor-action.add-pro .advisor-action-type {
433
+ background: rgba(52, 211, 153, 0.15);
434
+ color: var(--green);
435
+ }
436
+
437
+ .advisor-action.remove-pro .advisor-action-type {
438
+ background: rgba(251, 191, 36, 0.15);
439
+ color: var(--yellow);
440
+ }
441
+
442
+ .advisor-action-label { font-weight: 500; }
443
+ .advisor-action-reason { color: var(--text-dim); font-size: 11px; margin-left: auto; }
444
+ .advisor-empty { color: var(--text-dim); font-size: 12px; font-style: italic; }
369
445
  </style>
370
446
  </head>
371
447
  <body>
@@ -397,6 +473,8 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
397
473
 
398
474
  <div class="model-routing" id="modelRouting"></div>
399
475
 
476
+ <div class="advisor-panel" id="proAdvisor" style="display:none"></div>
477
+
400
478
  <div class="accounts-grid" id="accounts"></div>
401
479
 
402
480
  <script>
@@ -501,6 +579,8 @@ function renderAccounts(data) {
501
579
  '<div class="card-header">' +
502
580
  '<div class="card-label">' + maskText(a.label) + '</div>' +
503
581
  '<div class="card-badges">' +
582
+ (a.proDetected ? '<span class="badge badge-pro">PRO</span>' : '<span class="badge badge-free">FREE</span>') +
583
+ (a.familyManager ? '<span class="badge badge-fmgr">FAMILY MGR</span>' : '') +
504
584
  '<span class="badge badge-' + a.status + (isActive ? ' pulse' : '') + '">' + a.status + '</span>' +
505
585
  modelBadges +
506
586
  '</div>' +
@@ -529,6 +609,8 @@ function renderAccounts(data) {
529
609
  (isCooldown && cooldownPercent > 0 ? '<div class="cooldown-bar" style="width:' + cooldownPercent + '%"></div>' : '') +
530
610
  '</div>';
531
611
  }).join('');
612
+
613
+ renderProAdvisor(data.proAdvisor);
532
614
  }
533
615
 
534
616
  var MASK_MODE = new URLSearchParams(window.location.search).has('mask');
@@ -555,6 +637,28 @@ async function enableAccount(email) {
555
637
  refresh();
556
638
  }
557
639
 
640
+ function renderProAdvisor(advisor) {
641
+ var panel = document.getElementById('proAdvisor');
642
+ if (!advisor) { panel.style.display = 'none'; return; }
643
+ panel.style.display = 'block';
644
+ var title = '<div class="advisor-title">Pro Family Advisor' +
645
+ '<span class="advisor-slots">Slots: ' + advisor.currentProCount + '/' + advisor.maxProSlots + '</span></div>';
646
+ if (advisor.actions.length === 0) {
647
+ panel.innerHTML = title + '<div class="advisor-empty">No actions recommended</div>';
648
+ return;
649
+ }
650
+ var rows = advisor.actions.map(function(a) {
651
+ var cls = a.type === 'add-pro' ? 'add-pro' : 'remove-pro';
652
+ var typeLabel = a.type === 'add-pro' ? 'Add Pro' : 'Remove Pro';
653
+ return '<div class="advisor-action ' + cls + '">' +
654
+ '<span class="advisor-action-type">' + typeLabel + '</span>' +
655
+ '<span class="advisor-action-label">' + maskText(a.label) + '</span>' +
656
+ '<span class="advisor-action-reason">' + a.reason + '</span>' +
657
+ '</div>';
658
+ }).join('');
659
+ panel.innerHTML = title + rows;
660
+ }
661
+
558
662
  async function refresh() {
559
663
  try {
560
664
  var res = await fetch('/api/status');
package/src/rotator.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type ModelQuota,
10
10
  type ModelRotationState,
11
11
  type PersistedState,
12
+ type ProAdvisorAction,
12
13
  type StatusResponse,
13
14
  CLIENT_ID,
14
15
  CLIENT_SECRET,
@@ -614,6 +615,8 @@ export class AccountRotator {
614
615
  consecutiveErrors: a.consecutiveErrors,
615
616
  hasValidToken: !!(a.accessToken && a.tokenExpires > now),
616
617
  quota: a.quota,
618
+ proDetected: this.isProAccount(a),
619
+ familyManager: !!a.config.familyManager,
617
620
  };
618
621
  });
619
622
 
@@ -624,6 +627,7 @@ export class AccountRotator {
624
627
  totalRequestsAllAccounts: this.accounts.reduce((sum, a) => sum + a.totalRequests, 0),
625
628
  uptime: now - this.startTime,
626
629
  accounts,
630
+ proAdvisor: this.getProAdvisor(),
627
631
  };
628
632
  }
629
633
 
@@ -635,4 +639,90 @@ export class AccountRotator {
635
639
  const ts = new Date().toISOString().slice(11, 19);
636
640
  console.log(`[${ts}] [rotator] ${msg}`);
637
641
  }
642
+
643
+ // =========================================================================
644
+ // Pro Family Sharing Advisor
645
+ // =========================================================================
646
+
647
+ // Model keys relevant for Pro advisor decisions (ignore Flash)
648
+ private static PRO_ADVISOR_MODELS = ["gemini-3.1-pro", "claude-opus-4-6-thinking"];
649
+
650
+ private isProAccount(account: AccountRuntime): boolean {
651
+ return account.quota.some((q) => q.timerType === "5h");
652
+ }
653
+
654
+ private getProAdvisor(): StatusResponse["proAdvisor"] {
655
+ const maxSlots = this.config.proSlots ?? 6;
656
+ const proAccounts = this.accounts.filter((a) => !a.disabled && !a.flagged && this.isProAccount(a));
657
+ const currentProCount = proAccounts.length;
658
+ const actions: ProAdvisorAction[] = [];
659
+
660
+ // Suggest "remove-pro" for Pro accounts (not family manager) with 0% on all advisor models
661
+ for (const account of proAccounts) {
662
+ if (account.config.familyManager) continue;
663
+ const advisorQuotas = account.quota.filter((q) =>
664
+ AccountRotator.PRO_ADVISOR_MODELS.some((m) => q.modelKey.includes(m) || m.includes(q.modelKey)),
665
+ );
666
+ if (advisorQuotas.length === 0) continue;
667
+ const allExhausted = advisorQuotas.every((q) => q.percentRemaining === 0);
668
+ if (allExhausted) {
669
+ actions.push({
670
+ type: "remove-pro",
671
+ email: account.config.email,
672
+ label: account.config.label || account.config.email,
673
+ reason: "Pro quota exhausted on G3Pro and Claude",
674
+ });
675
+ }
676
+ }
677
+
678
+ // Suggest "add-pro" for Free accounts with 0% quota and long reset, if slots available
679
+ const slotsAvailable = maxSlots - currentProCount + actions.filter((a) => a.type === "remove-pro").length;
680
+ if (slotsAvailable > 0) {
681
+ const candidates: { account: AccountRuntime; maxResetMs: number }[] = [];
682
+
683
+ for (const account of this.accounts) {
684
+ if (account.disabled || account.flagged) continue;
685
+ if (this.isProAccount(account)) continue;
686
+
687
+ const advisorQuotas = account.quota.filter((q) =>
688
+ AccountRotator.PRO_ADVISOR_MODELS.some((m) => q.modelKey.includes(m) || m.includes(q.modelKey)),
689
+ );
690
+ if (advisorQuotas.length === 0) continue;
691
+
692
+ // Only suggest if at least one advisor model is at 0%
693
+ const hasExhausted = advisorQuotas.some((q) => q.percentRemaining === 0);
694
+ if (!hasExhausted) continue;
695
+
696
+ // Find the longest reset time among exhausted models
697
+ let maxResetMs = 0;
698
+ for (const q of advisorQuotas) {
699
+ if (q.percentRemaining === 0 && q.resetTime) {
700
+ const resetMs = new Date(q.resetTime).getTime() - Date.now();
701
+ if (resetMs > maxResetMs) maxResetMs = resetMs;
702
+ }
703
+ }
704
+
705
+ // Only suggest if reset is > 24h away (otherwise not worth the Pro slot)
706
+ if (maxResetMs > 24 * 60 * 60 * 1000) {
707
+ candidates.push({ account, maxResetMs });
708
+ }
709
+ }
710
+
711
+ // Sort by longest reset time first (maximizes benefit)
712
+ candidates.sort((a, b) => b.maxResetMs - a.maxResetMs);
713
+
714
+ for (const { account, maxResetMs } of candidates.slice(0, slotsAvailable)) {
715
+ const days = Math.floor(maxResetMs / (24 * 60 * 60 * 1000));
716
+ const hours = Math.floor((maxResetMs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
717
+ actions.push({
718
+ type: "add-pro",
719
+ email: account.config.email,
720
+ label: account.config.label || account.config.email,
721
+ reason: `0% quota, resets in ${days}d ${hours}h`,
722
+ });
723
+ }
724
+ }
725
+
726
+ return { currentProCount, maxProSlots: maxSlots, actions };
727
+ }
638
728
  }
package/src/types.ts CHANGED
@@ -9,6 +9,8 @@ export interface AccountConfig {
9
9
  label?: string;
10
10
  // Optional - pro/free is detected dynamically from quota API reset times
11
11
  type?: AccountType;
12
+ // This account owns the family plan and can never be removed from Pro
13
+ familyManager?: boolean;
12
14
  }
13
15
 
14
16
  export interface Config {
@@ -19,6 +21,8 @@ export interface Config {
19
21
  rotateOnQuotaDrop: number;
20
22
  // How often to poll quota (ms). Default: 5min
21
23
  quotaPollIntervalMs: number;
24
+ // Max simultaneous Pro accounts (owner + members). Default: 6
25
+ proSlots?: number;
22
26
  }
23
27
 
24
28
  // Quota API response from Google
@@ -134,6 +138,12 @@ export interface StatusResponse {
134
138
  // Per-model active account
135
139
  activeAccounts: Record<string, string>;
136
140
  accounts: AccountStatus[];
141
+ // Pro family sharing advisor
142
+ proAdvisor: {
143
+ currentProCount: number;
144
+ maxProSlots: number;
145
+ actions: ProAdvisorAction[];
146
+ };
137
147
  }
138
148
 
139
149
  export interface AccountStatus {
@@ -151,6 +161,17 @@ export interface AccountStatus {
151
161
  consecutiveErrors: number;
152
162
  hasValidToken: boolean;
153
163
  quota: ModelQuota[];
164
+ // Pro family sharing
165
+ proDetected: boolean;
166
+ familyManager: boolean;
167
+ }
168
+
169
+ // Pro advisor suggestion
170
+ export interface ProAdvisorAction {
171
+ type: "add-pro" | "remove-pro";
172
+ email: string;
173
+ label: string;
174
+ reason: string;
154
175
  }
155
176
 
156
177
  // Antigravity OAuth constants (same as pi-mono)