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}