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/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}