Skill: supadoopa/oura-cli (version 1.0.0)

# Oura Ring CLI Skill ## Description This tool allows retrieving health and biometric data from the Oura Ring API (V2) via a command-line interface. Use this to answer questions about the user's sleep, activity, readiness, and physiological stats. Repository: [https://github.com/ruhrpotter/oura-cli](https://github.com/ruhrpotter/oura-cli) ## Prerequisite The CLI must be authenticated. If a command fails with an auth error, notify the user to run `./oura auth login`. ## Syntax `./oura get <category> [flags]` ## Categories - `personal`: User profile (age, weight, height, email). - `sleep`: Daily sleep scores and efficiency. - `activity`: Daily activity scores, steps, and movement. - `readiness`: Daily readiness scores indicating recovery. - `heartrate`: Time-series heart rate data. - `workout`: Detailed workout sessions. - `spo2`: Blood oxygen saturation levels. - `sleep-details`: Detailed sleep sessions including hypnograms. - `sessions`: Activity sessions (e.g. naps, rest). - `sleep-times`: Optimal bedtime guidance. - `stress`: Daily stress levels. - `resilience`: Daily resilience scores and recovery. - `cv-age`: Cardiovascular age estimates. - `vo2-max`: VO2 Max measurements. - `ring-config`: Ring hardware configuration (color, size, etc.). - `rest-mode`: Rest mode periods. - `tags`: Enhanced tags (notes, lifestyle choices). ## Arguments - `--start <YYYY-MM-DD>`: REQUIRED for most time-series data. The start date of the range. - `--end <YYYY-MM-DD>`: OPTIONAL. The end date of the range. If omitted, it may default to the start date or return a single day depending on context. ## Agent Instructions 1. **Date Resolution**: You **MUST** resolve all relative date terms (e.g., "today", "yesterday", "last week", "this month") into absolute `YYYY-MM-DD` string format based on the current operational date. 2. **Date ranges**: - For "today": Set `--start` to today's date. - For "yesterday": Set `--start` to yesterday's date. - For "last 7 days": Set `--start` to 7 days ago and `--end` to today. 3. **Path**: Assume the binary is `./oura` in the current working directory unless the user specifies otherwise. 4. **Output**: The CLI returns JSON. Parse the JSON `data` array to formulate a natural language response. ## Examples **User Request**: "How was my sleep last night?" **Context**: Today is 2024-03-15. "Last night" usually implies the sleep session ending on the morning of today, or the previous day's data depending on how Oura dates it (Oura dates sleep by the morning it ends). **Reasoning**: Sleep for the night of the 14th to 15th is logged as `2024-03-15`. **Command**: ```bash ./oura get sleep --start 2024-03-15 ``` **User Request**: "What is my readiness score today?" **Context**: Today is 2024-03-15. **Command**: ```bash ./oura get readiness --start 2024-03-15 ``` **User Request**: "Show my heart rate for the first week of January 2024." **Command**: ```bash ./oura get heartrate --start 2024-01-01 --end 2024-01-07 ``` **User Request**: "Who am I?" **Command**: ```bash ./oura get personal ```

path: README.md
content: # Oura Ring Go CLI

A command-line interface for accessing your Oura Ring data using the official V2 API. This tool helps you easily retrieve personal info, sleep analysis, activity scores, readiness data, and more directly from your terminal.

## Prerequisites

- Go 1.21 or higher
- An active Oura Ring account

## Oura Application Setup (OAuth2)

To use this CLI, you must register a personal application in the Oura Cloud to obtain API credentials.

1.  **Visit the Developer Console**:
    Go to the [Oura Cloud Developer Portal](https://cloud.ouraring.com/oauth/developer).

2.  **Create a New Application**:
    -   Click on "Create New Application".
    -   **Application Name**: Give it a name (e.g., "My CLI Tool").
    -   **Website URL**: You can use `http://localhost` if you don't have a website.
    -   **Redirect URIs**: **CRITICAL STEP**. Add exactly:
        ```
        http://localhost:8080/callback
        ```
    -   Agree to terms and Create.

3.  **Get Credentials**:
    -   On your application page, find the **Client ID** and **Client Secret**.
    -   Keep these secure. You will need them to authenticate the CLI.

## Installation

```bash
git clone <repository-url>
cd oura-cli
go build -o oura ./cmd/oura
```

## Usage

### 1. Authenticate

The first time you run the tool, you need to log in. You can pass your credentials securely via environment variables or follow the interactive prompts.

**Using Environment Variables (Recommended):**
```bash
export OURA_CLIENT_ID="your_client_id_here"
export OURA_CLIENT_SECRET="your_client_secret_here"
./oura auth login
```

**Using Interactive Prompts:**
```bash
./oura auth login
# Paste your Client ID and Secret when asked
```

A browser window will open asking you to authorize the application. Once approved, the access token is stored locally in `~/.config/oura-cli/config.json`.

### 2. Retrieve Data

The `get` command retrieves data. Dates should be in `YYYY-MM-DD` format.

**Personal Info:**
```bash
./oura get personal
```

**Sleep Data:**
```bash
# Get data for a specific range
./oura get sleep --start 2023-11-01 --end 2023-11-07

# Get data for a single day (start date only)
./oura get sleep --start 2023-11-01
```

### Available Data Commands

All commands support `--start` and `--end` flags (YYYY-MM-DD).

| Command | Description | Data Points |
| :--- | :--- | :--- |
| `get personal` | User Profile | Age, Weight, Height, Email |
| `get sleep` | Daily Sleep Summary | Score, Efficiency, Total Sleep Time |
| `get activity` | Daily Activity Summary | Score, Steps, Calorie Burn |
| `get readiness` | Daily Readiness Summary | Score, Recovery Index, RHR Balance |
| `get heartrate` | Heart Rate (Time Series) | BPM, Source, Timestamp |
| `get spo2` | SpO2 | Average Oxygen Saturation |
| `get workout` | Workouts | Type, Calories, Distance, Duration |
| `get sleep-details` | **Detailed** Sleep | Hypnograms (5min), HR (5min), Phases |
| `get sessions` | Activity Sessions | Naps, Rest periods, Motion count |
| `get sleep-times` | Bedtime Guidance | Optimal Bedtime Window, Status |
| `get stress` | Daily Stress | Stress High/Low duration, Recovery |
| `get resilience` | Daily Resilience | Resilience Level, Recovery Balance |
| `get cv-age` | Cardiovascular Age | Vascular Age Range |
| `get vo2-max` | VO2 Max | Estimated VO2 Max |
| `get ring-config` | Hardware Info | Model, Size, Firmware Version, Color |
| `get rest-mode` | Rest Mode | Start/End times, Status |
| `get tags` | Enhanced Tags | Tag type, Comments, Duration |

**Examples:**

Get detailed sleep data with hypnograms:
```bash
./oura get sleep-details --start 2024-01-01
```

Check your resilience and stress levels:
```bash
./oura get resilience --start 2024-01-01
./oura get stress --start 2024-01-01
```

Check your cardiovascular age:
```bash
./oura get cv-age --start 2024-01-01
```

## Help

For a full list of commands and flags:

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.



path: cmd/oura/main.go
content: package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"oura-cli/internal/api"
	"oura-cli/internal/auth"
	"oura-cli/internal/config"

	"github.com/spf13/cobra"
	"golang.org/x/oauth2"
)

var (
	clientID     string
	clientSecret string
	startDate    string
	endDate      string
)

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

var rootCmd = &cobra.Command{
	Use:   "oura",
	Short: "Oura Ring CLI",
	Long:  `A CLI implementation for the Oura Ring V2 API.`,
}

var authCmd = &cobra.Command{
	Use:   "auth",
	Short: "Authenticate with Oura",
	Run: func(cmd *cobra.Command, args []string) {
		// Try to look up env vars if flags not set
		if clientID == "" {
			clientID = os.Getenv("OURA_CLIENT_ID")
		}
		if clientSecret == "" {
			clientSecret = os.Getenv("OURA_CLIENT_SECRET")
		}

		if clientID == "" || clientSecret == "" {
			reader := bufio.NewReader(os.Stdin)
			if clientID == "" {
				fmt.Print("Enter Client ID: ")
				id, _ := reader.ReadString('\n')
				clientID = strings.TrimSpace(id)
			}
			if clientSecret == "" {
				fmt.Print("Enter Client Secret: ")
				secret, _ := reader.ReadString('\n')
				clientSecret = strings.TrimSpace(secret)
			}
		}

		cfg, err := config.LoadConfig()
		if err != nil {
			fmt.Printf("Error loading config: %v\n", err)
			return
		}

		cfg.ClientID = clientID
		cfg.ClientSecret = clientSecret

		if err := auth.Login(cfg); err != nil {
			fmt.Printf("Authentication failed: %v\n", err)
			os.Exit(1)
		}
	},
}

var getCmd = &cobra.Command{
	Use:   "get",
	Short: "Get data from Oura API",
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
		return nil
	},
}

func getClient() (*api.Client, error) {
	cfg, err := config.LoadConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to load config: %w", err)
	}

	if cfg.AccessToken == "" {
		return nil, fmt.Errorf("not authenticated. please run 'oura auth'")
	}

	token := &oauth2.Token{
		AccessToken:  cfg.AccessToken,
		RefreshToken: cfg.RefreshToken,
		Expiry:       cfg.Expiry,
		TokenType:    "Bearer",
	}

	// Token source that automatically refreshes
	ts := auth.GetOAuthConfig(cfg.ClientID, cfg.ClientSecret).TokenSource(context.Background(), token)

	// Check if token needs refresh and save if it does
	newToken, err := ts.Token()
	if err != nil {
		return nil, fmt.Errorf("failed to refresh token: %w", err)
	}

	if newToken.AccessToken != cfg.AccessToken {
		cfg.AccessToken = newToken.AccessToken
		cfg.RefreshToken = newToken.RefreshToken
		cfg.Expiry = newToken.Expiry
		config.SaveConfig(cfg)
	}

	return api.NewClient(context.Background(), ts), nil
}

func printJSON(v interface{}) {
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	enc.Encode(v)
}

func init() {
	authCmd.Flags().StringVar(&clientID, "client-id", "", "Oura Client ID")
	authCmd.Flags().StringVar(&clientSecret, "client-secret", "", "Oura Client Secret")
	rootCmd.AddCommand(authCmd)

	getCmd.PersistentFlags().StringVar(&startDate, "start", "", "Start date (YYYY-MM-DD)")
	getCmd.PersistentFlags().StringVar(&endDate, "end", "", "End date (YYYY-MM-DD)")
	rootCmd.AddCommand(getCmd)

	// Subcommands
	getCmd.AddCommand(&cobra.Command{
		Use:   "personal",
		Short: "Get personal info",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetPersonalInfo()
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "sleep",
		Short: "Get daily sleep",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetDailySleep(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "activity",
		Short: "Get daily activity",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetDailyActivity(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "readiness",
		Short: "Get daily readiness",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetDailyReadiness(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "heartrate",
		Short: "Get heart rate",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetHeartRate(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "workout",
		Short: "Get workouts",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetWorkouts(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "spo2",
		Short: "Get SpO2",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetSpO2(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "sleep-details",
		Short: "Get detailed sleep sessions",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetSleep(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "sessions",
		Short: "Get activity sessions",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetSessions(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "sleep-times",
		Short: "Get optimal sleep times",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetSleepTimes(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "stress",
		Short: "Get daily stress",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetDailyStress(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "resilience",
		Short: "Get daily resilience",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetDailyResilience(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "cv-age",
		Short: "Get cardiovascular age",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetCVAge(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "vo2-max",
		Short: "Get VO2 Max",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetVO2Max(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "ring-config",
		Short: "Get ring configuration",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetRingConfiguration(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "rest-mode",
		Short: "Get rest mode periods",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetRestModePeriod(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})

	getCmd.AddCommand(&cobra.Command{
		Use:   "tags",
		Short: "Get enhanced tags",
		RunE: func(cmd *cobra.Command, args []string) error {
			client, err := getClient()
			if err != nil {
				return err
			}
			data, err := client.GetEnhancedTags(startDate, endDate)
			if err != nil {
				return err
			}
			printJSON(data)
			return nil
		},
	})
}


path: internal/api/client.go
content: package api

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"

	"golang.org/x/oauth2"
)

const BaseURL = "https://api.ouraring.com/v2/usercollection/"

type Client struct {
	httpClient *http.Client
	baseURL    string
}

func NewClient(ctx context.Context, tokenSource oauth2.TokenSource) *Client {
	return &Client{
		httpClient: oauth2.NewClient(ctx, tokenSource),
		baseURL:    BaseURL,
	}
}

func (c *Client) get(endpoint string, params url.Values, v interface{}) error {
	u, err := url.Parse(c.baseURL + endpoint)
	if err != nil {
		return err
	}

	if params != nil {
		u.RawQuery = params.Encode()
	}

	req, err := http.NewRequest("GET", u.String(), nil)
	if err != nil {
		return err
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
	}

	if v != nil {
		if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
			return err
		}
	}

	return nil
}


path: internal/api/endpoints.go
content: package api

import (
	"net/url"
)

// --- Types ---

type PersonalInfo struct {
	ID     string  `json:"id"`
	Age    int     `json:"age"`
	Weight float64 `json:"weight"`
	Height float64 `json:"height"`
	Gender string  `json:"gender"`
	Email  string  `json:"email"`
}

type DailySleep struct {
	ID        string `json:"id"`
	Day       string `json:"day"`
	Score     int    `json:"score"`
	Timestamp string `json:"timestamp"`
	// Add more fields as needed per docs, keeping it minimal for CLI summary for now
}

type DailySleepResponse struct {
	Data      []DailySleep `json:"data"`
	NextToken string       `json:"next_token"`
}

type DailyActivity struct {
	ID    string `json:"id"`
	Day   string `json:"day"`
	Score int    `json:"score"`
	Steps int    `json:"steps"`
	// ...
}

type DailyActivityResponse struct {
	Data      []DailyActivity `json:"data"`
	NextToken string          `json:"next_token"`
}

type DailyReadiness struct {
	ID    string `json:"id"`
	Day   string `json:"day"`
	Score int    `json:"score"`
	// ...
}

type DailyReadinessResponse struct {
	Data      []DailyReadiness `json:"data"`
	NextToken string           `json:"next_token"`
}

type HeartRate struct {
	ID        string `json:"id"`
	BPM       int    `json:"bpm"`
	Source    string `json:"source"`
	Timestamp string `json:"timestamp"`
}

type HeartRateResponse struct {
	Data      []HeartRate `json:"data"`
	NextToken string      `json:"next_token"`
}

type Workout struct {
	ID            string  `json:"id"`
	Activity      string  `json:"activity"`
	Calories      float64 `json:"calories"`
	Day           string  `json:"day"`
	Distance      float64 `json:"distance"`
	EndDatetime   string  `json:"end_datetime"`
	StartDatetime string  `json:"start_datetime"`
	Label         string  `json:"label"`
}

type WorkoutResponse struct {
	Data      []Workout `json:"data"`
	NextToken string    `json:"next_token"`
}

type SpO2 struct {
	ID             string `json:"id"`
	Day            string `json:"day"`
	SpO2Percentage struct {
		Average float64 `json:"average"`
	} `json:"spo2_percentage"`
}

type SpO2Response struct {
	Data      []SpO2 `json:"data"`
	NextToken string `json:"next_token"`
}

type Sleep struct {
	ID            string `json:"id"`
	Day           string `json:"day"`
	Score         int    `json:"score"`
	Efficiency    int    `json:"efficiency"`
	Type          string `json:"type"`
	BedtimeStart  string `json:"bedtime_start"`
	BedtimeEnd    string `json:"bedtime_end"`
	Hypnogram5Min string `json:"hypnogram_5min"`
}

type SleepResponse struct {
	Data      []Sleep `json:"data"`
	NextToken string  `json:"next_token"`
}

type Session struct {
	ID            string `json:"id"`
	Day           string `json:"day"`
	StartDatetime string `json:"start_datetime"`
	EndDatetime   string `json:"end_datetime"`
	Type          string `json:"type"`
	Status        string `json:"status"`
	MotionCount   int    `json:"motion_count"`
}

type SessionResponse struct {
	Data      []Session `json:"data"`
	NextToken string    `json:"next_token"`
}

type SleepTime struct {
	ID             string `json:"id"`
	Day            string `json:"day"`
	OptimalBedtime struct {
		DayTZ       int    `json:"day_tz"`
		EndOffset   int    `json:"end_offset"`
		StartOffset int    `json:"start_offset"`
		Status      string `json:"status"`
	} `json:"optimal_bedtime"`
	Recommendation string `json:"recommendation"`
}

type SleepTimeResponse struct {
	Data      []SleepTime `json:"data"`
	NextToken string      `json:"next_token"`
}

type EnhancedTag struct {
	ID          string `json:"id"`
	TagTypeCode string `json:"tag_type_code"`
	StartTime   string `json:"start_time"`
	EndTime     string `json:"end_time"`
	Comment     string `json:"comment"`
}

type EnhancedTagResponse struct {
	Data      []EnhancedTag `json:"data"`
	NextToken string        `json:"next_token"`
}

type DailyStress struct {
	ID           string `json:"id"`
	Day          string `json:"day"`
	StressHigh   int    `json:"stress_high"`
	RecoveryHigh int    `json:"recovery_high"`
	DaySummary   string `json:"day_summary"`
}

type DailyStressResponse struct {
	Data      []DailyStress `json:"data"`
	NextToken string        `json:"next_token"`
}

type DailyResilience struct {
	ID           string `json:"id"`
	Day          string `json:"day"`
	Level        string `json:"level"`
	Contributors struct {
		SleepRecovery   float64 `json:"sleep_recovery"`
		DaytimeRecovery float64 `json:"daytime_recovery"`
		Stress          float64 `json:"stress"`
	} `json:"contributors"`
}

type DailyResilienceResponse struct {
	Data      []DailyResilience `json:"data"`
	NextToken string            `json:"next_token"`
}

type DailyCardiovascularAge struct {
	ID               string `json:"id"`
	Day              string `json:"day"`
	VascularAgeRange int    `json:"vascular_age_range"`
}

type DailyCardiovascularAgeResponse struct {
	Data      []DailyCardiovascularAge `json:"data"`
	NextToken string                   `json:"next_token"`
}

type VO2Max struct {
	ID     string  `json:"id"`
	Day    string  `json:"day"`
	VO2Max float64 `json:"vo2_max"`
}

type VO2MaxResponse struct {
	Data      []VO2Max `json:"data"`
	NextToken string   `json:"next_token"`
}

type RingConfiguration struct {
	ID              string `json:"id"`
	Color           string `json:"color"`
	Design          string `json:"design"`
	FirmwareVersion string `json:"firmware_version"`
	HardwareType    string `json:"hardware_type"`
	Size            int    `json:"size"`
}

type RingConfigurationResponse struct {
	Data      []RingConfiguration `json:"data"`
	NextToken string              `json:"next_token"`
}

type RestModePeriod struct {
	ID        string `json:"id"`
	Day       string `json:"day"`
	EndDay    string `json:"end_day"`
	StartDay  string `json:"start_day"`
	StartTime string `json:"start_time"`
	EndTime   string `json:"end_time"`
}

type RestModePeriodResponse struct {
	Data      []RestModePeriod `json:"data"`
	NextToken string           `json:"next_token"`
}

// --- Methods ---

func (c *Client) GetPersonalInfo() (*PersonalInfo, error) {
	var p PersonalInfo
	err := c.get("personal_info", nil, &p)
	return &p, err
}

func (c *Client) GetDailySleep(start, end string) (*DailySleepResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res DailySleepResponse
	err := c.get("daily_sleep", params, &res)
	return &res, err
}

func (c *Client) GetDailyActivity(start, end string) (*DailyActivityResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res DailyActivityResponse
	err := c.get("daily_activity", params, &res)
	return &res, err
}

func (c *Client) GetDailyReadiness(start, end string) (*DailyReadinessResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res DailyReadinessResponse
	err := c.get("daily_readiness", params, &res)
	return &res, err
}

func (c *Client) GetHeartRate(start, end string) (*HeartRateResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_datetime", start)
	}
	if end != "" {
		params.Set("end_datetime", end)
	}

	var res HeartRateResponse
	err := c.get("heartrate", params, &res)
	return &res, err
}

func (c *Client) GetWorkouts(start, end string) (*WorkoutResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res WorkoutResponse
	err := c.get("workout", params, &res)
	return &res, err
}

func (c *Client) GetSpO2(start, end string) (*SpO2Response, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res SpO2Response
	err := c.get("daily_spo2", params, &res)
	return &res, err
}

func (c *Client) GetSleep(start, end string) (*SleepResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res SleepResponse
	err := c.get("sleep", params, &res)
	return &res, err
}

func (c *Client) GetSessions(start, end string) (*SessionResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res SessionResponse
	err := c.get("session", params, &res)
	return &res, err
}

func (c *Client) GetSleepTimes(start, end string) (*SleepTimeResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}

	var res SleepTimeResponse
	err := c.get("sleep_time", params, &res)
	return &res, err
}

func (c *Client) GetEnhancedTags(start, end string) (*EnhancedTagResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res EnhancedTagResponse
	err := c.get("enhanced_tag", params, &res)
	return &res, err
}

func (c *Client) GetDailyStress(start, end string) (*DailyStressResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res DailyStressResponse
	err := c.get("daily_stress", params, &res)
	return &res, err
}

func (c *Client) GetDailyResilience(start, end string) (*DailyResilienceResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res DailyResilienceResponse
	err := c.get("daily_resilience", params, &res)
	return &res, err
}

func (c *Client) GetCVAge(start, end string) (*DailyCardiovascularAgeResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res DailyCardiovascularAgeResponse
	err := c.get("daily_cardiovascular_age", params, &res)
	return &res, err
}

func (c *Client) GetVO2Max(start, end string) (*VO2MaxResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res VO2MaxResponse
	err := c.get("vO2_max", params, &res)
	return &res, err
}

func (c *Client) GetRingConfiguration(start, end string) (*RingConfigurationResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res RingConfigurationResponse
	err := c.get("ring_configuration", params, &res)
	return &res, err
}

func (c *Client) GetRestModePeriod(start, end string) (*RestModePeriodResponse, error) {
	params := url.Values{}
	if start != "" {
		params.Set("start_date", start)
	}
	if end != "" {
		params.Set("end_date", end)
	}
	var res RestModePeriodResponse
	err := c.get("rest_mode_period", params, &res)
	return &res, err
}


path: internal/auth/oauth.go
content: package auth

import (
	"context"
	"fmt"
	"net/http"

	"oura-cli/internal/config"

	"golang.org/x/oauth2"
)

const (
	AuthURL  = "https://cloud.ouraring.com/oauth/authorize"
	TokenURL = "https://api.ouraring.com/oauth/token"
)

func GetOAuthConfig(clientID, clientSecret string) *oauth2.Config {
	return &oauth2.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		Endpoint: oauth2.Endpoint{
			AuthURL:  AuthURL,
			TokenURL: TokenURL,
		},
		RedirectURL: "http://localhost:8080/callback",
		Scopes:      []string{"email", "personal", "daily", "heartrate", "workout", "tag", "session", "spo2", "stress", "ring_configuration", "cardiovascular", "heart_health"},
	}
}

func Login(cfg *config.Config) error {
	oauthConfig := GetOAuthConfig(cfg.ClientID, cfg.ClientSecret)
	url := oauthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)

	fmt.Printf("Please visit this URL to log in: %s\n", url)

	// Create a channel to wait for the token
	tokenChan := make(chan *oauth2.Token)
	errChan := make(chan error)

	server := &http.Server{Addr: ":8080"}

	http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
		code := r.URL.Query().Get("code")
		if code == "" {
			errChan <- fmt.Errorf("code not found")
			http.Error(w, "Code not found", http.StatusBadRequest)
			return
		}

		token, err := oauthConfig.Exchange(context.Background(), code)
		if err != nil {
			errChan <- err
			http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
			return
		}

		tokenChan <- token
		fmt.Fprintf(w, "Login successful! You can close this window.")
	})

	go func() {
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			errChan <- err
		}
	}()

	// Manual input goroutine
	go func() {
		fmt.Println("\nIf the browser callback fails (e.g. Connection Refused locally), copy the 'code' parameter from the URL you were redirected to and paste it here:")
		fmt.Print("Code > ")
		var code string
		fmt.Scanln(&code)
		if code != "" {
			token, err := oauthConfig.Exchange(context.Background(), code)
			if err != nil {
				errChan <- err
				return
			}
			tokenChan <- token
		}
	}()

	select {
	case token := <-tokenChan:
		cfg.AccessToken = token.AccessToken
		cfg.RefreshToken = token.RefreshToken
		cfg.Expiry = token.Expiry
		if err := config.SaveConfig(cfg); err != nil {
			server.Shutdown(context.Background())
			return err
		}
		fmt.Println("Successfully authenticated and saved token.")
	case err := <-errChan:
		server.Shutdown(context.Background())
		return err
	}

	return server.Shutdown(context.Background())
}


path: internal/config/config.go
content: package config

import (
	"encoding/json"
	"os"
	"path/filepath"
	"time"
)

type Config struct {
	ClientID     string    `json:"client_id"`
	ClientSecret string    `json:"client_secret"`
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token"`
	Expiry       time.Time `json:"expiry"`
}

func GetConfigDir() (string, error) {
	configDir, err := os.UserConfigDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(configDir, "oura-cli"), nil
}

func LoadConfig() (*Config, error) {
	dir, err := GetConfigDir()
	if err != nil {
		return nil, err
	}

	path := filepath.Join(dir, "config.json")
	file, err := os.Open(path)
	if os.IsNotExist(err) {
		return &Config{}, nil
	}
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var config Config
	if err := json.NewDecoder(file).Decode(&config); err != nil {
		return nil, err
	}

	return &config, nil
}

func SaveConfig(config *Config) error {
	dir, err := GetConfigDir()
	if err != nil {
		return err
	}

	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	path := filepath.Join(dir, "config.json")
	file, err := os.Create(path)
	if err != nil {
		return err
	}
	defer file.Close()

	return json.NewEncoder(file).Encode(config)
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)