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