feat(garmin): implement data-fetch methods via generic call dispatch

GetActivities/GetActivitySplits/GetActivityDetails/GetWorkoutByID now send
{"cmd":"call","params":{"method":...,"args":...}} instead of named MCP
tools. subprocessClient fully implements Client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 21:02:49 +02:00
parent 9cbcf62aed
commit 8c94c94c3f
2 changed files with 243 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import (
"io"
"os"
"os/exec"
"strconv"
"sync"
)
@@ -311,3 +312,127 @@ func truncate(s string, n int) string {
}
return s[:n] + "...(truncated)"
}
var _ Client = (*subprocessClient)(nil)
// NewClient builds a Client. The subprocess is not spawned until the first
// call that needs it (Authenticate, or any data call once authenticated).
func NewClient(cfg Config) Client {
return &subprocessClient{cfg: cfg}
}
func (c *subprocessClient) GetActivities(ctx context.Context, startDate, endDate string, limit int) ([]Activity, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := c.ensureStarted(); err != nil {
return nil, err
}
raw, err := c.roundTrip("call", callParams{
Method: "get_activities_by_date",
Args: map[string]any{"startdate": startDate, "enddate": endDate},
})
if err != nil {
return nil, err
}
var rawActivities []json.RawMessage
if err := json.Unmarshal(raw, &rawActivities); err != nil {
return nil, fmt.Errorf("get_activities_by_date did not return a JSON array (%q): %w", truncate(string(raw), 200), err)
}
if limit > 0 && limit < len(rawActivities) {
rawActivities = rawActivities[:limit]
}
activities := make([]Activity, 0, len(rawActivities))
for _, raw := range rawActivities {
var a Activity
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("parse activity: %w", err)
}
a.Raw = raw
activities = append(activities, a)
}
return activities, nil
}
func (c *subprocessClient) GetActivitySplits(ctx context.Context, activityID int64) (ActivitySplits, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := c.ensureStarted(); err != nil {
return ActivitySplits{}, err
}
raw, err := c.roundTrip("call", callParams{
Method: "get_activity_splits",
Args: map[string]any{"activity_id": strconv.FormatInt(activityID, 10)},
})
if err != nil {
return ActivitySplits{}, err
}
var envelope struct {
ActivityID int64 `json:"activityId"`
Laps []json.RawMessage `json:"lapDTOs"`
}
if err := json.Unmarshal(raw, &envelope); err != nil {
return ActivitySplits{}, fmt.Errorf("get_activity_splits did not return valid JSON (%q): %w", truncate(string(raw), 200), err)
}
splits := ActivitySplits{ActivityID: envelope.ActivityID, Laps: make([]Lap, 0, len(envelope.Laps))}
for _, raw := range envelope.Laps {
var l Lap
if err := json.Unmarshal(raw, &l); err != nil {
return ActivitySplits{}, fmt.Errorf("parse lap: %w", err)
}
l.Raw = raw
splits.Laps = append(splits.Laps, l)
}
return splits, nil
}
func (c *subprocessClient) GetActivityDetails(ctx context.Context, activityID int64) (ActivityDetails, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := c.ensureStarted(); err != nil {
return ActivityDetails{}, err
}
raw, err := c.roundTrip("call", callParams{
Method: "get_activity_details",
Args: map[string]any{"activity_id": strconv.FormatInt(activityID, 10)},
})
if err != nil {
return ActivityDetails{}, err
}
var details ActivityDetails
if err := json.Unmarshal(raw, &details); err != nil {
return ActivityDetails{}, fmt.Errorf("get_activity_details did not return valid JSON (%q): %w", truncate(string(raw), 200), err)
}
details.Raw = json.RawMessage(raw)
return details, nil
}
func (c *subprocessClient) GetWorkoutByID(ctx context.Context, workoutID int64) (Workout, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := c.ensureStarted(); err != nil {
return Workout{}, err
}
raw, err := c.roundTrip("call", callParams{
Method: "get_workout_by_id",
Args: map[string]any{"workout_id": strconv.FormatInt(workoutID, 10)},
})
if err != nil {
return Workout{}, err
}
var workout Workout
if err := json.Unmarshal(raw, &workout); err != nil {
return Workout{}, fmt.Errorf("get_workout_by_id did not return valid JSON (%q): %w", truncate(string(raw), 200), err)
}
workout.Raw = json.RawMessage(raw)
return workout, nil
}