track.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package habit provides MCP tools for Lunatask habit operations.
  6package habit
  7
  8import (
  9	"context"
 10
 11	"git.secluded.site/go-lunatask"
 12	"git.secluded.site/lune/internal/dateutil"
 13	"git.secluded.site/lune/internal/mcp/shared"
 14	"github.com/modelcontextprotocol/go-sdk/mcp"
 15)
 16
 17// TrackToolName is the name of the track habit tool.
 18const TrackToolName = "track_habit"
 19
 20// TrackToolDescription describes the track habit tool for LLMs.
 21const TrackToolDescription = `Record that a habit was performed.
 22
 23Use lunatask://habits resource to discover valid habit IDs and config keys.
 24Tracks for today by default. Idempotent—re-tracking the same date has no effect.`
 25
 26// TrackToolAnnotations returns hints about tool behavior.
 27func TrackToolAnnotations() *mcp.ToolAnnotations {
 28	return &mcp.ToolAnnotations{
 29		IdempotentHint: true,
 30	}
 31}
 32
 33// TrackInput is the input schema for tracking a habit.
 34type TrackInput struct {
 35	HabitID     string  `json:"habit_id"               jsonschema:"Habit UUID, lunatask:// deep link, or config key"`
 36	PerformedOn *string `json:"performed_on,omitempty" jsonschema:"Date performed (strtotime syntax, default: today)"`
 37}
 38
 39// TrackOutput is the output schema for tracking a habit.
 40type TrackOutput struct {
 41	Success     bool   `json:"success"`
 42	HabitID     string `json:"habit_id"`
 43	PerformedOn string `json:"performed_on"`
 44}
 45
 46// Handler handles habit-related MCP tool requests.
 47type Handler struct {
 48	client *lunatask.Client
 49	habits []shared.HabitProvider
 50}
 51
 52// NewHandler creates a new habit handler.
 53func NewHandler(accessToken string, habits []shared.HabitProvider) *Handler {
 54	return &Handler{
 55		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 56		habits: habits,
 57	}
 58}
 59
 60// HandleTrack records a habit activity.
 61func (h *Handler) HandleTrack(
 62	ctx context.Context,
 63	_ *mcp.CallToolRequest,
 64	input TrackInput,
 65) (*mcp.CallToolResult, TrackOutput, error) {
 66	habitID := h.resolveHabitRef(input.HabitID)
 67	if habitID == "" {
 68		return shared.ErrorResult("unknown habit: " + input.HabitID), TrackOutput{}, nil
 69	}
 70
 71	dateStr := ""
 72	if input.PerformedOn != nil {
 73		dateStr = *input.PerformedOn
 74	}
 75
 76	performedOn, err := dateutil.Parse(dateStr)
 77	if err != nil {
 78		return shared.ErrorResult(err.Error()), TrackOutput{}, nil
 79	}
 80
 81	req := &lunatask.TrackHabitActivityRequest{
 82		PerformedOn: performedOn,
 83	}
 84
 85	_, err = h.client.TrackHabitActivity(ctx, habitID, req)
 86	if err != nil {
 87		return shared.ErrorResult(err.Error()), TrackOutput{}, nil
 88	}
 89
 90	return nil, TrackOutput{
 91		Success:     true,
 92		HabitID:     habitID,
 93		PerformedOn: performedOn.Format("2006-01-02"),
 94	}, nil
 95}
 96
 97// resolveHabitRef resolves a habit reference to a UUID.
 98// Accepts config key, UUID, or deep link.
 99func (h *Handler) resolveHabitRef(input string) string {
100	// Try UUID or deep link first
101	if _, id, err := lunatask.ParseReference(input); err == nil {
102		return id
103	}
104
105	// Try config key lookup
106	for _, habit := range h.habits {
107		if habit.Key == input {
108			return habit.ID
109		}
110	}
111
112	return ""
113}