Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

187 lines
4.5 KiB
Go

package main
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/cursorusage"
"go.kenn.io/agentsview/internal/db"
)
var newCursorUsageClient = cursorusage.NewClient
type UsageCursorConfig struct {
Since string
Until string
All bool
PageSize int
Email string
UserID string
EmailChanged bool
UserIDChanged bool
}
func newUsageCursorCommand() *cobra.Command {
var cfg UsageCursorConfig
cmd := &cobra.Command{
Use: "cursor",
Short: "Ingest Cursor admin usage events",
SilenceUsage: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cfg.EmailChanged = cmd.Flags().Changed("email")
cfg.UserIDChanged = cmd.Flags().Changed("user-id")
return runUsageCursor(cfg)
},
}
cmd.Flags().StringVar(&cfg.Since, "since", "", "Start date (YYYY-MM-DD)")
cmd.Flags().StringVar(&cfg.Until, "until", "", "End date (YYYY-MM-DD)")
cmd.Flags().BoolVar(&cfg.All, "all", false, "Include all history")
cmd.Flags().IntVar(&cfg.PageSize, "page-size", 100, "Events per request page")
cmd.Flags().StringVar(&cfg.Email, "email", "", "Filter by user email")
cmd.Flags().StringVar(&cfg.UserID, "user-id", "", "Filter by user ID")
return cmd
}
func runUsageCursor(cfg UsageCursorConfig) error {
appCfg, err := config.LoadMinimal()
if err != nil {
return err
}
database, writeLock, err := openWriteDB(
context.Background(),
appCfg,
)
if err != nil {
return err
}
defer closeWriteDB(database, writeLock)
apiKey := strings.TrimSpace(appCfg.CursorAdminAPIKey)
if apiKey == "" {
return fmt.Errorf("missing Cursor admin API key")
}
email := strings.TrimSpace(cfg.Email)
userID := strings.TrimSpace(cfg.UserID)
if !cfg.EmailChanged && !cfg.UserIDChanged {
email = strings.TrimSpace(appCfg.CursorAdminEmail)
userID = strings.TrimSpace(appCfg.CursorAdminUserID)
} else {
if !cfg.EmailChanged {
email = ""
}
if !cfg.UserIDChanged {
userID = ""
}
}
loc, err := time.LoadLocation(localTimezone())
if err != nil {
loc = time.Local
}
start, end, err := resolveCursorUsageWindow(cfg, loc)
if err != nil {
return err
}
pageSize := cfg.PageSize
if pageSize <= 0 {
pageSize = 100
}
client := newCursorUsageClient(apiKey)
events, err := client.FetchAllUsageEvents(context.Background(), cursorusage.Query{
StartDate: start,
EndDate: end,
PageSize: pageSize,
Email: email,
UserID: userID,
})
if err != nil {
return err
}
rows := make([]db.CursorUsageEvent, 0, len(events))
for _, ev := range events {
rows = append(rows, db.CursorUsageEvent{
OccurredAt: ev.Timestamp.UTC().Format(time.RFC3339Nano),
Model: ev.Model,
Kind: ev.Kind,
InputTokens: ev.TokenUsage.InputTokens,
OutputTokens: ev.TokenUsage.OutputTokens,
CacheWriteTokens: ev.TokenUsage.CacheWriteTokens,
CacheReadTokens: ev.TokenUsage.CacheReadTokens,
ChargedCents: ev.ChargedCents,
CursorTokenFee: ev.CursorTokenFee,
UserID: ev.UserID,
UserEmail: ev.UserEmail,
IsHeadless: ev.IsHeadless,
})
}
if err := database.InsertCursorUsageEvents(rows); err != nil {
return err
}
fmt.Fprintf(os.Stdout,
"Fetched %d Cursor usage events into the archive\n",
len(rows),
)
return nil
}
func resolveCursorUsageWindow(
cfg UsageCursorConfig, loc *time.Location,
) (time.Time, time.Time, error) {
now := time.Now().In(loc)
if cfg.All {
return time.Unix(0, 0).UTC(), now.UTC(), nil
}
startDate := strings.TrimSpace(cfg.Since)
endDate := strings.TrimSpace(cfg.Until)
startDate, endDate = defaultUsageDateRange(startDate, endDate, now)
var start time.Time
var end time.Time
var err error
if startDate != "" {
start, err = time.ParseInLocation("2006-01-02", startDate, loc)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf(
"invalid since date %q: %w", startDate, err,
)
}
} else {
start = time.Unix(0, 0).In(loc)
}
if endDate != "" {
end, err = time.ParseInLocation("2006-01-02", endDate, loc)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf(
"invalid until date %q: %w", endDate, err,
)
}
end = end.AddDate(0, 0, 1).Add(-time.Millisecond)
} else {
end = now
}
if start.After(end) {
return time.Time{}, time.Time{}, fmt.Errorf(
"since date %q is after until date %q", startDate, endDate,
)
}
return start.UTC(), end.UTC(), nil
}