timeline.go

 1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5package person
 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// TimelineToolName is the name of the add timeline note tool.
17const TimelineToolName = "add_timeline_note"
18
19// TimelineToolDescription describes the add timeline note tool for LLMs.
20const TimelineToolDescription = `Adds a timeline note to a person's memory timeline in Lunatask.
21
22Required:
23- person_id: Person UUID or lunatask://person/... deep link
24
25Optional:
26- content: Markdown content describing the interaction
27- date: Date of interaction (YYYY-MM-DD or natural language, default: today)
28
29This is append-only — adds to the person's memory timeline.
30Great for tracking when you last interacted with someone.`
31
32// TimelineInput is the input schema for adding a timeline note.
33type TimelineInput struct {
34	PersonID string  `json:"person_id"         jsonschema:"required"`
35	Content  *string `json:"content,omitempty"`
36	Date     *string `json:"date,omitempty"`
37}
38
39// TimelineOutput is the output schema for adding a timeline note.
40type TimelineOutput struct {
41	Success        bool   `json:"success"`
42	PersonDeepLink string `json:"person_deep_link"`
43	NoteID         string `json:"note_id"`
44}
45
46// HandleTimeline adds a timeline note to a person.
47func (h *Handler) HandleTimeline(
48	ctx context.Context,
49	_ *mcp.CallToolRequest,
50	input TimelineInput,
51) (*mcp.CallToolResult, TimelineOutput, error) {
52	personID, err := parseReference(input.PersonID)
53	if err != nil {
54		return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
55	}
56
57	builder := h.client.NewTimelineNote(personID)
58
59	if input.Content != nil {
60		builder.WithContent(*input.Content)
61	}
62
63	if input.Date != nil {
64		date, err := dateutil.Parse(*input.Date)
65		if err != nil {
66			return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
67		}
68
69		builder.OnDate(date)
70	}
71
72	note, err := builder.Create(ctx)
73	if err != nil {
74		return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
75	}
76
77	personDeepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, personID)
78
79	dateStr := "today"
80
81	if input.Date != nil {
82		date, _ := dateutil.Parse(*input.Date)
83		dateStr = date.Format("2006-01-02")
84	}
85
86	return &mcp.CallToolResult{
87			Content: []mcp.Content{&mcp.TextContent{
88				Text: "Timeline note added for " + personDeepLink + " on " + dateStr,
89			}},
90		}, TimelineOutput{
91			Success:        true,
92			PersonDeepLink: personDeepLink,
93			NoteID:         note.ID,
94		}, nil
95}