update.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
 10	"git.secluded.site/go-lunatask"
 11	"git.secluded.site/lune/internal/config"
 12	"git.secluded.site/lune/internal/dateutil"
 13	"git.secluded.site/lune/internal/mcp/shared"
 14	"git.secluded.site/lune/internal/validate"
 15	"github.com/modelcontextprotocol/go-sdk/mcp"
 16)
 17
 18// UpdateToolName is the name of the update task tool.
 19const UpdateToolName = "update_task"
 20
 21// UpdateToolDescription describes the update task tool for LLMs.
 22const UpdateToolDescription = `Updates an existing task in Lunatask.
 23
 24Required:
 25- id: Task UUID or lunatask:// deep link
 26
 27Optional (only specified fields are updated):
 28- name: New task title
 29- area_id: Move to area (UUID, lunatask:// deep link, or config key)
 30- goal_id: Move to goal (UUID, lunatask:// deep link, or config key; requires area_id)
 31- status: later, next, started, waiting, completed
 32- note: New markdown note (replaces existing)
 33- priority: lowest, low, normal, high, highest
 34- estimate: Time estimate in minutes (0-720)
 35- motivation: must, should, want
 36- important: true/false for Eisenhower matrix
 37- urgent: true/false for Eisenhower matrix
 38- scheduled_on: Date to schedule (YYYY-MM-DD)
 39
 40Returns the updated task's ID and deep link.`
 41
 42// UpdateInput is the input schema for updating a task.
 43type UpdateInput struct {
 44	ID          string  `json:"id"                     jsonschema:"required"`
 45	Name        *string `json:"name,omitempty"`
 46	AreaID      *string `json:"area_id,omitempty"`
 47	GoalID      *string `json:"goal_id,omitempty"`
 48	Status      *string `json:"status,omitempty"`
 49	Note        *string `json:"note,omitempty"`
 50	Priority    *string `json:"priority,omitempty"`
 51	Estimate    *int    `json:"estimate,omitempty"`
 52	Motivation  *string `json:"motivation,omitempty"`
 53	Important   *bool   `json:"important,omitempty"`
 54	Urgent      *bool   `json:"urgent,omitempty"`
 55	ScheduledOn *string `json:"scheduled_on,omitempty"`
 56}
 57
 58// UpdateOutput is the output schema for updating a task.
 59type UpdateOutput struct {
 60	DeepLink string `json:"deep_link"`
 61}
 62
 63// parsedUpdateInput holds validated and parsed update input fields.
 64type parsedUpdateInput struct {
 65	ID          string
 66	Name        *string
 67	AreaID      *string
 68	GoalID      *string
 69	Status      *lunatask.TaskStatus
 70	Note        *string
 71	Priority    *lunatask.Priority
 72	Estimate    *int
 73	Motivation  *lunatask.Motivation
 74	Important   *bool
 75	Urgent      *bool
 76	ScheduledOn *lunatask.Date
 77}
 78
 79// HandleUpdate updates an existing task.
 80func (h *Handler) HandleUpdate(
 81	ctx context.Context,
 82	_ *mcp.CallToolRequest,
 83	input UpdateInput,
 84) (*mcp.CallToolResult, UpdateOutput, error) {
 85	parsed, errResult := parseUpdateInput(h.cfg, input)
 86	if errResult != nil {
 87		return errResult, UpdateOutput{}, nil
 88	}
 89
 90	builder := h.client.NewTaskUpdate(parsed.ID)
 91	applyToTaskUpdateBuilder(builder, parsed)
 92
 93	task, err := builder.Update(ctx)
 94	if err != nil {
 95		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
 96	}
 97
 98	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
 99
100	return &mcp.CallToolResult{
101		Content: []mcp.Content{&mcp.TextContent{
102			Text: "Task updated: " + deepLink,
103		}},
104	}, UpdateOutput{DeepLink: deepLink}, nil
105}
106
107//nolint:cyclop,funlen
108func parseUpdateInput(cfg *config.Config, input UpdateInput) (*parsedUpdateInput, *mcp.CallToolResult) {
109	_, id, err := lunatask.ParseReference(input.ID)
110	if err != nil {
111		return nil, shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link")
112	}
113
114	parsed := &parsedUpdateInput{
115		ID:        id,
116		Name:      input.Name,
117		Note:      input.Note,
118		Estimate:  input.Estimate,
119		Important: input.Important,
120		Urgent:    input.Urgent,
121	}
122
123	if input.AreaID != nil {
124		areaID, err := validate.AreaRef(cfg, *input.AreaID)
125		if err != nil {
126			return nil, shared.ErrorResult(err.Error())
127		}
128
129		parsed.AreaID = &areaID
130	}
131
132	if input.GoalID != nil {
133		areaID := ""
134		if parsed.AreaID != nil {
135			areaID = *parsed.AreaID
136		}
137
138		goalID, err := validate.GoalRef(cfg, areaID, *input.GoalID)
139		if err != nil {
140			return nil, shared.ErrorResult(err.Error())
141		}
142
143		parsed.GoalID = &goalID
144	}
145
146	if input.Estimate != nil {
147		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
148			return nil, shared.ErrorResult(err.Error())
149		}
150	}
151
152	if input.Status != nil {
153		status, err := lunatask.ParseTaskStatus(*input.Status)
154		if err != nil {
155			return nil, shared.ErrorResult(err.Error())
156		}
157
158		parsed.Status = &status
159	}
160
161	if input.Priority != nil {
162		priority, err := lunatask.ParsePriority(*input.Priority)
163		if err != nil {
164			return nil, shared.ErrorResult(err.Error())
165		}
166
167		parsed.Priority = &priority
168	}
169
170	if input.Motivation != nil {
171		motivation, err := lunatask.ParseMotivation(*input.Motivation)
172		if err != nil {
173			return nil, shared.ErrorResult(err.Error())
174		}
175
176		parsed.Motivation = &motivation
177	}
178
179	if input.ScheduledOn != nil {
180		date, err := dateutil.Parse(*input.ScheduledOn)
181		if err != nil {
182			return nil, shared.ErrorResult(err.Error())
183		}
184
185		parsed.ScheduledOn = &date
186	}
187
188	return parsed, nil
189}
190
191//nolint:cyclop
192func applyToTaskUpdateBuilder(builder *lunatask.TaskUpdateBuilder, parsed *parsedUpdateInput) {
193	if parsed.Name != nil {
194		builder.Name(*parsed.Name)
195	}
196
197	if parsed.AreaID != nil {
198		builder.InArea(*parsed.AreaID)
199	}
200
201	if parsed.GoalID != nil {
202		builder.InGoal(*parsed.GoalID)
203	}
204
205	if parsed.Status != nil {
206		builder.WithStatus(*parsed.Status)
207	}
208
209	if parsed.Note != nil {
210		builder.WithNote(*parsed.Note)
211	}
212
213	if parsed.Priority != nil {
214		builder.Priority(*parsed.Priority)
215	}
216
217	if parsed.Estimate != nil {
218		builder.WithEstimate(*parsed.Estimate)
219	}
220
221	if parsed.Motivation != nil {
222		builder.WithMotivation(*parsed.Motivation)
223	}
224
225	if parsed.Important != nil {
226		if *parsed.Important {
227			builder.Important()
228		} else {
229			builder.NotImportant()
230		}
231	}
232
233	if parsed.Urgent != nil {
234		if *parsed.Urgent {
235			builder.Urgent()
236		} else {
237			builder.NotUrgent()
238		}
239	}
240
241	if parsed.ScheduledOn != nil {
242		builder.ScheduledOn(*parsed.ScheduledOn)
243	}
244}