package cmd import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type ModelUsage struct { NumRequests int `json:"numRequests"` NumRequestsTotal int `json:"numRequestsTotal"` NumTokens int `json:"numTokens"` MaxTokenUsage *int `json:"maxTokenUsage"` MaxRequestUsage *int `json:"maxRequestUsage"` } type UsageData struct { StartOfMonth string `json:"startOfMonth"` Models map[string]ModelUsage `json:"-"` } type StripeProfile struct { MembershipType string `json:"membershipType"` SubscriptionStatus string `json:"subscriptionStatus"` DaysRemainingOnTrial *int `json:"daysRemainingOnTrial"` IsTeamMember bool `json:"isTeamMember"` IsYearlyPlan bool `json:"isYearlyPlan"` } func DecodeJWTPayload(token string) map[string]interface{} { parts := strings.Split(token, ".") if len(parts) < 2 { return nil } padded := strings.ReplaceAll(parts[1], "-", "+") padded = strings.ReplaceAll(padded, "_", "/") data, err := base64.StdEncoding.DecodeString(padded + strings.Repeat("=", (4-len(padded)%4)%4)) if err != nil { return nil } var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { return nil } return result } func TokenSub(token string) string { payload := DecodeJWTPayload(token) if payload == nil { return "" } if sub, ok := payload["sub"].(string); ok { return sub } return "" } func apiGet(path, token string) (map[string]interface{}, error) { client := &http.Client{Timeout: 8 * time.Second} req, err := http.NewRequest("GET", "https://api2.cursor.sh"+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { return nil, nil } return result, nil } func FetchAccountUsage(token string) (*UsageData, error) { raw, err := apiGet("/auth/usage", token) if err != nil || raw == nil { return nil, err } startOfMonth, _ := raw["startOfMonth"].(string) usage := &UsageData{ StartOfMonth: startOfMonth, Models: make(map[string]ModelUsage), } for k, v := range raw { if k == "startOfMonth" { continue } data, err := json.Marshal(v) if err != nil { continue } var mu ModelUsage if err := json.Unmarshal(data, &mu); err == nil { usage.Models[k] = mu } } return usage, nil } func FetchStripeProfile(token string) (*StripeProfile, error) { raw, err := apiGet("/auth/full_stripe_profile", token) if err != nil || raw == nil { return nil, err } profile := &StripeProfile{ MembershipType: fmt.Sprintf("%v", raw["membershipType"]), SubscriptionStatus: fmt.Sprintf("%v", raw["subscriptionStatus"]), IsTeamMember: raw["isTeamMember"] == true, IsYearlyPlan: raw["isYearlyPlan"] == true, } if d, ok := raw["daysRemainingOnTrial"].(float64); ok { di := int(d) profile.DaysRemainingOnTrial = &di } return profile, nil } func DescribePlan(profile *StripeProfile) string { if profile == nil { return "" } switch profile.MembershipType { case "free_trial": days := 0 if profile.DaysRemainingOnTrial != nil { days = *profile.DaysRemainingOnTrial } return fmt.Sprintf("Pro Trial (%dd left) — unlimited fast requests", days) case "pro": return "Pro — extended limits" case "pro_plus": return "Pro+ — extended limits" case "ultra": return "Ultra — extended limits" case "free", "hobby": return "Hobby (free) — limited agent requests" default: return fmt.Sprintf("%s · %s", profile.MembershipType, profile.SubscriptionStatus) } } var modelLabels = map[string]string{ "gpt-4": "Fast Premium Requests", "claude-sonnet-4-6": "Claude Sonnet 4.6", "claude-sonnet-4-5-20250929-v1": "Claude Sonnet 4.5", "claude-sonnet-4-20250514-v1": "Claude Sonnet 4", "claude-opus-4-6-v1": "Claude Opus 4.6", "claude-opus-4-5-20251101-v1": "Claude Opus 4.5", "claude-opus-4-1-20250805-v1": "Claude Opus 4.1", "claude-opus-4-20250514-v1": "Claude Opus 4", "claude-haiku-4-5-20251001-v1": "Claude Haiku 4.5", "claude-3-5-haiku-20241022-v1": "Claude 3.5 Haiku", "gpt-5": "GPT-5", "gpt-4o": "GPT-4o", "o1": "o1", "o3-mini": "o3-mini", "cursor-small": "Cursor Small (free)", } func modelLabel(key string) string { if label, ok := modelLabels[key]; ok { return label } return key } func FormatUsageSummary(usage *UsageData) []string { if usage == nil { return nil } var lines []string start := "?" if usage.StartOfMonth != "" { if t, err := time.Parse(time.RFC3339, usage.StartOfMonth); err == nil { start = t.Format("2006-01-02") } else { start = usage.StartOfMonth } } lines = append(lines, fmt.Sprintf(" Billing period from %s", start)) if len(usage.Models) == 0 { lines = append(lines, " No requests this billing period") return lines } type entry struct { key string usage ModelUsage } var entries []entry for k, v := range usage.Models { entries = append(entries, entry{k, v}) } // Sort: entries with limits first, then by usage descending for i := 1; i < len(entries); i++ { for j := i; j > 0; j-- { a, b := entries[j-1], entries[j] aHasLimit := a.usage.MaxRequestUsage != nil bHasLimit := b.usage.MaxRequestUsage != nil if !aHasLimit && bHasLimit { entries[j-1], entries[j] = entries[j], entries[j-1] } else if aHasLimit == bHasLimit && a.usage.NumRequests < b.usage.NumRequests { entries[j-1], entries[j] = entries[j], entries[j-1] } else { break } } } for _, e := range entries { used := e.usage.NumRequests max := e.usage.MaxRequestUsage label := modelLabel(e.key) if max != nil && *max > 0 { pct := int(float64(used) / float64(*max) * 100) bar := makeBar(used, *max, 12) lines = append(lines, fmt.Sprintf(" %s: %d/%d (%d%%) [%s]", label, used, *max, pct, bar)) } else if used > 0 { lines = append(lines, fmt.Sprintf(" %s: %d requests", label, used)) } else { lines = append(lines, fmt.Sprintf(" %s: 0 requests (unlimited)", label)) } } return lines } func makeBar(used, max, width int) string { fill := int(float64(used) / float64(max) * float64(width)) if fill > width { fill = width } return strings.Repeat("█", fill) + strings.Repeat("░", width-fill) }