list.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package task
  6
  7import (
  8	"context"
  9	"fmt"
 10	"strings"
 11	"time"
 12
 13	"git.secluded.site/go-lunatask"
 14	"git.secluded.site/lune/internal/mcp/shared"
 15	"github.com/modelcontextprotocol/go-sdk/mcp"
 16)
 17
 18// ListToolName is the name of the list tasks tool.
 19const ListToolName = "list_tasks"
 20
 21// ListToolDescription describes the list tasks tool for LLMs.
 22const ListToolDescription = `Lists tasks from Lunatask.
 23
 24Optional filters:
 25- area_id: Filter by area UUID
 26- status: Filter by status (later, next, started, waiting, completed)
 27- include_completed: Include completed tasks (default: false, only shows today's)
 28
 29Note: Due to end-to-end encryption, task names and notes are not available.
 30Only metadata (ID, status, dates, priority, etc.) is returned.
 31
 32Returns a list of tasks with their metadata and deep links.`
 33
 34// ListInput is the input schema for listing tasks.
 35type ListInput struct {
 36	AreaID           *string `json:"area_id,omitempty"`
 37	Status           *string `json:"status,omitempty"`
 38	IncludeCompleted *bool   `json:"include_completed,omitempty"`
 39}
 40
 41// ListOutput is the output schema for listing tasks.
 42type ListOutput struct {
 43	Tasks []Summary `json:"tasks"`
 44	Count int       `json:"count"`
 45}
 46
 47// Summary represents a task in list output.
 48type Summary struct {
 49	DeepLink    string  `json:"deep_link"`
 50	Status      *string `json:"status,omitempty"`
 51	Priority    *int    `json:"priority,omitempty"`
 52	ScheduledOn *string `json:"scheduled_on,omitempty"`
 53	CreatedAt   string  `json:"created_at"`
 54	AreaID      *string `json:"area_id,omitempty"`
 55	GoalID      *string `json:"goal_id,omitempty"`
 56}
 57
 58// HandleList lists tasks.
 59func (h *Handler) HandleList(
 60	ctx context.Context,
 61	_ *mcp.CallToolRequest,
 62	input ListInput,
 63) (*mcp.CallToolResult, ListOutput, error) {
 64	if input.AreaID != nil {
 65		if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
 66			return shared.ErrorResult("invalid area_id: expected UUID"), ListOutput{}, nil
 67		}
 68	}
 69
 70	if input.Status != nil {
 71		if _, err := lunatask.ParseTaskStatus(*input.Status); err != nil {
 72			return shared.ErrorResult("invalid status: must be later, next, started, waiting, or completed"), ListOutput{}, nil
 73		}
 74	}
 75
 76	tasks, err := h.client.ListTasks(ctx, nil)
 77	if err != nil {
 78		return shared.ErrorResult(err.Error()), ListOutput{}, nil
 79	}
 80
 81	opts := &lunatask.TaskFilterOptions{
 82		AreaID:           input.AreaID,
 83		IncludeCompleted: input.IncludeCompleted != nil && *input.IncludeCompleted,
 84		Today:            time.Now(),
 85	}
 86
 87	if input.Status != nil {
 88		s := lunatask.TaskStatus(*input.Status)
 89		opts.Status = &s
 90	}
 91
 92	filtered := lunatask.FilterTasks(tasks, opts)
 93	summaries := buildSummaries(filtered)
 94	text := formatListText(summaries)
 95
 96	return &mcp.CallToolResult{
 97			Content: []mcp.Content{&mcp.TextContent{Text: text}},
 98		}, ListOutput{
 99			Tasks: summaries,
100			Count: len(summaries),
101		}, nil
102}
103
104func buildSummaries(tasks []lunatask.Task) []Summary {
105	summaries := make([]Summary, 0, len(tasks))
106
107	for _, task := range tasks {
108		summary := Summary{
109			CreatedAt: task.CreatedAt.Format(time.RFC3339),
110			AreaID:    task.AreaID,
111			GoalID:    task.GoalID,
112		}
113
114		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
115
116		if task.Status != nil {
117			s := string(*task.Status)
118			summary.Status = &s
119		}
120
121		if task.Priority != nil {
122			p := int(*task.Priority)
123			summary.Priority = &p
124		}
125
126		if task.ScheduledOn != nil {
127			s := task.ScheduledOn.Format("2006-01-02")
128			summary.ScheduledOn = &s
129		}
130
131		summaries = append(summaries, summary)
132	}
133
134	return summaries
135}
136
137func formatListText(summaries []Summary) string {
138	if len(summaries) == 0 {
139		return "No tasks found."
140	}
141
142	var builder strings.Builder
143
144	builder.WriteString(fmt.Sprintf("Found %d task(s):\n", len(summaries)))
145
146	for _, summary := range summaries {
147		status := "unknown"
148		if summary.Status != nil {
149			status = *summary.Status
150		}
151
152		builder.WriteString(fmt.Sprintf("- %s (%s)\n", summary.DeepLink, status))
153	}
154
155	builder.WriteString("\nUse show_task for full details.")
156
157	return builder.String()
158}