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 = `Records that a habit was performed on a specific date.
 22
 23Required:
 24- habit_id: Habit UUID, deep link, or config key
 25
 26Optional:
 27- performed_on: Date performed (YYYY-MM-DD or natural language, default: today)
 28
 29Use the lunatask://habits resource to discover valid habit IDs.`
 30
 31// TrackInput is the input schema for tracking a habit.
 32type TrackInput struct {
 33	HabitID     string  `json:"habit_id"               jsonschema:"required"`
 34	PerformedOn *string `json:"performed_on,omitempty"`
 35}
 36
 37// TrackOutput is the output schema for tracking a habit.
 38type TrackOutput struct {
 39	Success     bool   `json:"success"`
 40	HabitID     string `json:"habit_id"`
 41	PerformedOn string `json:"performed_on"`
 42}
 43
 44// Handler handles habit-related MCP tool requests.
 45type Handler struct {
 46	client *lunatask.Client
 47	habits []shared.HabitProvider
 48}
 49
 50// NewHandler creates a new habit handler.
 51func NewHandler(accessToken string, habits []shared.HabitProvider) *Handler {
 52	return &Handler{
 53		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 54		habits: habits,
 55	}
 56}
 57
 58// HandleTrack records a habit activity.
 59func (h *Handler) HandleTrack(
 60	ctx context.Context,
 61	_ *mcp.CallToolRequest,
 62	input TrackInput,
 63) (*mcp.CallToolResult, TrackOutput, error) {
 64	habitID := h.resolveHabitRef(input.HabitID)
 65	if habitID == "" {
 66		return shared.ErrorResult("unknown habit: " + input.HabitID), TrackOutput{}, nil
 67	}
 68
 69	dateStr := ""
 70	if input.PerformedOn != nil {
 71		dateStr = *input.PerformedOn
 72	}
 73
 74	performedOn, err := dateutil.Parse(dateStr)
 75	if err != nil {
 76		return shared.ErrorResult(err.Error()), TrackOutput{}, nil
 77	}
 78
 79	req := &lunatask.TrackHabitActivityRequest{
 80		PerformedOn: performedOn,
 81	}
 82
 83	_, err = h.client.TrackHabitActivity(ctx, habitID, req)
 84	if err != nil {
 85		return shared.ErrorResult(err.Error()), TrackOutput{}, nil
 86	}
 87
 88	return nil, TrackOutput{
 89		Success:     true,
 90		HabitID:     habitID,
 91		PerformedOn: performedOn.Format("2006-01-02"),
 92	}, nil
 93}
 94
 95// resolveHabitRef resolves a habit reference to a UUID.
 96// Accepts config key, UUID, or deep link.
 97func (h *Handler) resolveHabitRef(input string) string {
 98	// Try UUID or deep link first
 99	if _, id, err := lunatask.ParseReference(input); err == nil {
100		return id
101	}
102
103	// Try config key lookup
104	for _, habit := range h.habits {
105		if habit.Key == input {
106			return habit.ID
107		}
108	}
109
110	return ""
111}