show.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// ShowToolName is the name of the show task tool.
 19const ShowToolName = "show_task"
 20
 21// ShowToolDescription describes the show task tool for LLMs.
 22const ShowToolDescription = `Shows details of a specific task from Lunatask.
 23
 24Required:
 25- id: Task UUID or lunatask:// deep link
 26
 27Note: Due to end-to-end encryption, task name and note content are not available.
 28Only metadata (ID, status, dates, priority, etc.) is returned.`
 29
 30// ShowInput is the input schema for showing a task.
 31type ShowInput struct {
 32	ID string `json:"id" jsonschema:"required"`
 33}
 34
 35// ShowOutput is the output schema for showing a task.
 36type ShowOutput struct {
 37	DeepLink    string  `json:"deep_link"`
 38	Status      *string `json:"status,omitempty"`
 39	Priority    *int    `json:"priority,omitempty"`
 40	Estimate    *int    `json:"estimate,omitempty"`
 41	ScheduledOn *string `json:"scheduled_on,omitempty"`
 42	CompletedAt *string `json:"completed_at,omitempty"`
 43	CreatedAt   string  `json:"created_at"`
 44	UpdatedAt   string  `json:"updated_at"`
 45	AreaID      *string `json:"area_id,omitempty"`
 46	GoalID      *string `json:"goal_id,omitempty"`
 47	Important   *bool   `json:"important,omitempty"`
 48	Urgent      *bool   `json:"urgent,omitempty"`
 49}
 50
 51// HandleShow shows a task's details.
 52func (h *Handler) HandleShow(
 53	ctx context.Context,
 54	_ *mcp.CallToolRequest,
 55	input ShowInput,
 56) (*mcp.CallToolResult, ShowOutput, error) {
 57	_, id, err := lunatask.ParseReference(input.ID)
 58	if err != nil {
 59		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), ShowOutput{}, nil
 60	}
 61
 62	task, err := h.client.GetTask(ctx, id)
 63	if err != nil {
 64		return shared.ErrorResult(err.Error()), ShowOutput{}, nil
 65	}
 66
 67	output := ShowOutput{
 68		CreatedAt: task.CreatedAt.Format(time.RFC3339),
 69		UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
 70		AreaID:    task.AreaID,
 71		GoalID:    task.GoalID,
 72	}
 73
 74	output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
 75
 76	if task.Status != nil {
 77		s := string(*task.Status)
 78		output.Status = &s
 79	}
 80
 81	if task.Priority != nil {
 82		p := int(*task.Priority)
 83		output.Priority = &p
 84	}
 85
 86	if task.Estimate != nil {
 87		output.Estimate = task.Estimate
 88	}
 89
 90	if task.ScheduledOn != nil {
 91		s := task.ScheduledOn.Format("2006-01-02")
 92		output.ScheduledOn = &s
 93	}
 94
 95	if task.CompletedAt != nil {
 96		s := task.CompletedAt.Format(time.RFC3339)
 97		output.CompletedAt = &s
 98	}
 99
100	if task.Eisenhower != nil {
101		important := task.Eisenhower.IsImportant()
102		urgent := task.Eisenhower.IsUrgent()
103		output.Important = &important
104		output.Urgent = &urgent
105	}
106
107	text := formatShowText(output)
108
109	return &mcp.CallToolResult{
110		Content: []mcp.Content{&mcp.TextContent{Text: text}},
111	}, output, nil
112}
113
114func formatShowText(output ShowOutput) string {
115	var builder strings.Builder
116
117	builder.WriteString(fmt.Sprintf("Task: %s\n", output.DeepLink))
118	writeOptionalField(&builder, "Status", output.Status)
119	writeOptionalIntField(&builder, "Priority", output.Priority)
120	writeOptionalField(&builder, "Scheduled", output.ScheduledOn)
121	writeOptionalMinutesField(&builder, "Estimate", output.Estimate)
122	writeEisenhowerField(&builder, output.Important, output.Urgent)
123	builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
124	builder.WriteString("Updated: " + output.UpdatedAt)
125	writeOptionalField(&builder, "\nCompleted", output.CompletedAt)
126
127	return builder.String()
128}
129
130func writeOptionalField(builder *strings.Builder, label string, value *string) {
131	if value != nil {
132		fmt.Fprintf(builder, "%s: %s\n", label, *value)
133	}
134}
135
136func writeOptionalIntField(builder *strings.Builder, label string, value *int) {
137	if value != nil {
138		fmt.Fprintf(builder, "%s: %d\n", label, *value)
139	}
140}
141
142func writeOptionalMinutesField(builder *strings.Builder, label string, value *int) {
143	if value != nil {
144		fmt.Fprintf(builder, "%s: %d min\n", label, *value)
145	}
146}
147
148func writeEisenhowerField(builder *strings.Builder, important, urgent *bool) {
149	var parts []string
150
151	if important != nil && *important {
152		parts = append(parts, "important")
153	}
154
155	if urgent != nil && *urgent {
156		parts = append(parts, "urgent")
157	}
158
159	if len(parts) > 0 {
160		fmt.Fprintf(builder, "Eisenhower: %s\n", strings.Join(parts, ", "))
161	}
162}