From 22a00769ab784dc98a16079b540077c38194cb61 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 24 Dec 2025 12:39:35 -0700 Subject: [PATCH] feat(mcp): add semantic task list resources Add MCP resources for filtered task lists, providing semantic views like today, overdue, now, and high-priority without requiring the generic list_tasks tool. Static resources: - lunatask://tasks/all (discouraged - expensive) - lunatask://tasks/today - lunatask://tasks/overdue - lunatask://tasks/next-7-days - lunatask://tasks/high-priority - lunatask://tasks/now - lunatask://tasks/recent-completions Area-scoped templates (discovered via resources/templates/list): - lunatask://area/{area}/tasks - lunatask://area/{area}/today - lunatask://area/{area}/overdue - etc. The {area} parameter accepts config keys or UUIDs. Also default-disables list_tasks tool since resources are now the primary interface for task discovery. Assisted-by: Claude Opus 4.5 via Crush --- cmd/mcp/server.go | 126 ++++++++- internal/config/config.go | 2 +- internal/mcp/resources/areas/handler.go | 20 +- internal/mcp/resources/tasks/filters.go | 171 +++++++++++ internal/mcp/resources/tasks/handler.go | 358 ++++++++++++++++++++++++ 5 files changed, 657 insertions(+), 20 deletions(-) create mode 100644 internal/mcp/resources/tasks/filters.go create mode 100644 internal/mcp/resources/tasks/handler.go diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index 9f310d9a4f74dc3a61bed5fb60d964a5b794d56e..a11e116d10ccae88be1a33baacf4b4fcd171e8c6 100644 --- a/cmd/mcp/server.go +++ b/cmd/mcp/server.go @@ -18,6 +18,7 @@ import ( "git.secluded.site/lune/internal/mcp/resources/notebooks" personrs "git.secluded.site/lune/internal/mcp/resources/person" taskrs "git.secluded.site/lune/internal/mcp/resources/task" + "git.secluded.site/lune/internal/mcp/resources/tasks" "git.secluded.site/lune/internal/mcp/shared" "git.secluded.site/lune/internal/mcp/tools/habit" "git.secluded.site/lune/internal/mcp/tools/journal" @@ -44,8 +45,8 @@ func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server { habitProviders := shared.ToHabitProviders(cfg.Habits) notebookProviders := shared.ToNotebookProviders(cfg.Notebooks) - registerResources(mcpServer, areaProviders, habitProviders, notebookProviders) - registerResourceTemplates(mcpServer, accessToken) + registerResources(mcpServer, accessToken, areaProviders, habitProviders, notebookProviders) + registerResourceTemplates(mcpServer, accessToken, areaProviders) registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders, notebookProviders) return mcpServer @@ -69,6 +70,7 @@ func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider { func registerResources( mcpServer *mcp.Server, + accessToken string, areaProviders []shared.AreaProvider, habitProviders []shared.HabitProvider, notebookProviders []shared.NotebookProvider, @@ -96,9 +98,11 @@ func registerResources( Description: notebooks.ResourceDescription, MIMEType: "application/json", }, notebooksHandler.HandleRead) + + registerTaskListResources(mcpServer, accessToken, areaProviders) } -func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) { +func registerResourceTemplates(mcpServer *mcp.Server, accessToken string, areaProviders []shared.AreaProvider) { taskHandler := taskrs.NewHandler(accessToken) mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ Name: "task", @@ -122,6 +126,122 @@ func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) { Description: personrs.ResourceDescription, MIMEType: "application/json", }, personHandler.HandleRead) + + registerAreaTaskTemplates(mcpServer, accessToken, areaProviders) +} + +func registerTaskListResources( + mcpServer *mcp.Server, + accessToken string, + areaProviders []shared.AreaProvider, +) { + handler := tasks.NewHandler(accessToken, areaProviders) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-all", + URI: tasks.ResourceURIAll, + Description: tasks.AllDescription, + MIMEType: "application/json", + }, handler.HandleReadAll) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-today", + URI: tasks.ResourceURIToday, + Description: tasks.TodayDescription, + MIMEType: "application/json", + }, handler.HandleReadToday) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-overdue", + URI: tasks.ResourceURIOverdue, + Description: tasks.OverdueDescription, + MIMEType: "application/json", + }, handler.HandleReadOverdue) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-next-7-days", + URI: tasks.ResourceURINext7Days, + Description: tasks.Next7DaysDescription, + MIMEType: "application/json", + }, handler.HandleReadNext7Days) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-high-priority", + URI: tasks.ResourceURIHighPriority, + Description: tasks.HighPriorityDescription, + MIMEType: "application/json", + }, handler.HandleReadHighPriority) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-now", + URI: tasks.ResourceURINow, + Description: tasks.NowDescription, + MIMEType: "application/json", + }, handler.HandleReadNow) + + mcpServer.AddResource(&mcp.Resource{ + Name: "tasks-recent-completions", + URI: tasks.ResourceURIRecentCompletions, + Description: tasks.RecentCompletionsDescription, + MIMEType: "application/json", + }, handler.HandleReadRecentCompletions) +} + +func registerAreaTaskTemplates( + mcpServer *mcp.Server, + accessToken string, + areaProviders []shared.AreaProvider, +) { + handler := tasks.NewHandler(accessToken, areaProviders) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks", + URITemplate: tasks.AreaTasksTemplate, + Description: tasks.AreaTasksDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-today", + URITemplate: tasks.AreaTodayTemplate, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-overdue", + URITemplate: tasks.AreaOverdueTemplate, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-next-7-days", + URITemplate: tasks.AreaNext7DaysTemplate, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-high-priority", + URITemplate: tasks.AreaHighPriorityTemplate, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-now", + URITemplate: tasks.AreaNowTemplate, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) + + mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "area-tasks-recent-completions", + URITemplate: tasks.AreaRecentCompletionsTempl, + Description: tasks.AreaFilteredDescription, + MIMEType: "application/json", + }, handler.HandleReadAreaTasks) } func registerTools( diff --git a/internal/config/config.go b/internal/config/config.go index 8b20385d3d9f4b2c1ff00a7b8a3eabc781373cc2..9ab7d740a8970bdf98f79b02a9ddb281a2c305d0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -99,7 +99,7 @@ func (t *ToolsConfig) ApplyDefaults() { t.CreateTask = true t.UpdateTask = true t.DeleteTask = true - t.ListTasks = true + // ListTasks: default-disabled (fallback for lunatask://tasks/* resources) // ShowTask: default-disabled (fallback for lunatask://task/{id} resource) t.CreateNote = true t.UpdateNote = true diff --git a/internal/mcp/resources/areas/handler.go b/internal/mcp/resources/areas/handler.go index 9017302e4d5d7ccb65740077ff96c376ea45be6d..7cd8ca43d93d90a9a526ae00ad41673c449af9ab 100644 --- a/internal/mcp/resources/areas/handler.go +++ b/internal/mcp/resources/areas/handler.go @@ -18,22 +18,10 @@ import ( const ResourceURI = "lunatask://areas" // ResourceDescription describes the areas resource for LLMs. -const ResourceDescription = `Lists all configured Lunatask areas and their goals. - -Each area represents a life domain (e.g., Work, Personal, Health) and contains: -- id: UUID to use when creating tasks in this area -- name: Human-readable area name -- key: Short alias for CLI usage -- workflow: Task management style (determines which fields are relevant) -- workflow_description: Human-readable explanation of the workflow -- valid_statuses: Task statuses valid for this workflow -- uses_motivation: Whether motivation field (must/should/want) is relevant -- uses_eisenhower: Whether eisenhower matrix is relevant -- uses_scheduling: Whether date scheduling is relevant -- uses_priority: Whether priority field is relevant -- goals: List of goals within the area - -Use workflow information to determine which task fields to set for each area.` +const ResourceDescription = `Configured Lunatask areas and goals. + +Each area contains: id, name, key, workflow info, and goals. +Use area key or id in resource templates like lunatask://area/{area}/today.` // Handler handles area resource requests. type Handler struct { diff --git a/internal/mcp/resources/tasks/filters.go b/internal/mcp/resources/tasks/filters.go new file mode 100644 index 0000000000000000000000000000000000000000..f399d9d94c7438affafadc96ccc0fe9daec65497 --- /dev/null +++ b/internal/mcp/resources/tasks/filters.go @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package tasks + +import ( + "time" + + "git.secluded.site/go-lunatask" +) + +// FilterFunc filters a slice of tasks. +type FilterFunc func([]lunatask.Task) []lunatask.Task + +// FilterToday returns tasks scheduled for today. +func FilterToday(tasks []lunatask.Task) []lunatask.Task { + today := lunatask.Today() + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.ScheduledOn != nil && sameDay(task.ScheduledOn.Time, today.Time) { + result = append(result, task) + } + } + + return result +} + +// FilterOverdue returns tasks scheduled before today that are not completed. +func FilterOverdue(tasks []lunatask.Task) []lunatask.Task { + today := lunatask.Today() + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.ScheduledOn == nil { + continue + } + + if task.ScheduledOn.Before(today.Time) && !isCompleted(&task) { + result = append(result, task) + } + } + + return result +} + +// Next7DaysWindow is the number of days to look ahead for the next 7 days filter. +const Next7DaysWindow = 7 + +// FilterNext7Days returns tasks scheduled within the next 7 days. +func FilterNext7Days(tasks []lunatask.Task) []lunatask.Task { + today := lunatask.Today() + weekFromNow := lunatask.NewDate(time.Now().AddDate(0, 0, Next7DaysWindow)) + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.ScheduledOn == nil { + continue + } + + // Include today through 7 days from now + if !task.ScheduledOn.Before(today.Time) && !task.ScheduledOn.After(weekFromNow.Time) { + result = append(result, task) + } + } + + return result +} + +// FilterHighPriority returns tasks with highest priority. +func FilterHighPriority(tasks []lunatask.Task) []lunatask.Task { + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.Priority != nil && *task.Priority == lunatask.PriorityHighest { + result = append(result, task) + } + } + + return result +} + +// FilterNow returns tasks that need attention now. +// Matches tasks where ANY of: +// - Status = "started" +// - Priority = highest +// - Motivation = "must" +// - Eisenhower = urgent AND important. +func FilterNow(tasks []lunatask.Task) []lunatask.Task { + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if isNow(&task) { + result = append(result, task) + } + } + + return result +} + +//nolint:cyclop // Multiple independent conditions are inherently complex. +func isNow(task *lunatask.Task) bool { + // Exclude completed tasks + if isCompleted(task) { + return false + } + + // Status = started + if task.Status != nil && *task.Status == lunatask.StatusInProgress { + return true + } + + // Priority = highest + if task.Priority != nil && *task.Priority == lunatask.PriorityHighest { + return true + } + + // Motivation = must + if task.Motivation != nil && *task.Motivation == lunatask.MotivationMust { + return true + } + + // Eisenhower = urgent AND important + if task.Eisenhower != nil && task.Eisenhower.IsUrgent() && task.Eisenhower.IsImportant() { + return true + } + + return false +} + +// RecentCompletionsWindow is the time window for recent completions. +const RecentCompletionsWindow = 72 * time.Hour + +// FilterRecentCompletions returns tasks completed in the last 72 hours. +func FilterRecentCompletions(tasks []lunatask.Task) []lunatask.Task { + cutoff := time.Now().Add(-RecentCompletionsWindow) + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.CompletedAt != nil && task.CompletedAt.After(cutoff) { + result = append(result, task) + } + } + + return result +} + +// FilterByArea returns tasks in a specific area. +func FilterByArea(tasks []lunatask.Task, areaID string) []lunatask.Task { + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if task.AreaID != nil && *task.AreaID == areaID { + result = append(result, task) + } + } + + return result +} + +func isCompleted(task *lunatask.Task) bool { + return task.Status != nil && *task.Status == lunatask.StatusCompleted +} + +func sameDay(a, b time.Time) bool { + y1, m1, d1 := a.Date() + y2, m2, d2 := b.Date() + + return y1 == y2 && m1 == m2 && d1 == d2 +} diff --git a/internal/mcp/resources/tasks/handler.go b/internal/mcp/resources/tasks/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..61b6f0daed4645d6e555fb7f04148c5bb9c1fbcc --- /dev/null +++ b/internal/mcp/resources/tasks/handler.go @@ -0,0 +1,358 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package tasks provides MCP resources for filtered task lists. +package tasks + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/mcp/shared" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// ErrUnknownArea indicates the area reference could not be resolved. +var ErrUnknownArea = errors.New("unknown area") + +// Resource URIs for task list filters. +const ( + ResourceURIAll = "lunatask://tasks/all" + ResourceURIToday = "lunatask://tasks/today" + ResourceURIOverdue = "lunatask://tasks/overdue" + ResourceURINext7Days = "lunatask://tasks/next-7-days" + ResourceURIHighPriority = "lunatask://tasks/high-priority" + ResourceURINow = "lunatask://tasks/now" + ResourceURIRecentCompletions = "lunatask://tasks/recent-completions" +) + +// Area-scoped resource templates. +const ( + AreaTasksTemplate = "lunatask://area/{area}/tasks" + AreaTodayTemplate = "lunatask://area/{area}/today" + AreaOverdueTemplate = "lunatask://area/{area}/overdue" + AreaNext7DaysTemplate = "lunatask://area/{area}/next-7-days" + AreaHighPriorityTemplate = "lunatask://area/{area}/high-priority" + AreaNowTemplate = "lunatask://area/{area}/now" + AreaRecentCompletionsTempl = "lunatask://area/{area}/recent-completions" +) + +// Resource descriptions. +const ( + AllDescription = `All incomplete tasks. EXPENSIVE - prefer filtered resources.` + + TodayDescription = `Tasks scheduled for today.` + + OverdueDescription = `Overdue tasks (scheduled before today, incomplete).` + + Next7DaysDescription = `Tasks scheduled within the next 7 days.` + + HighPriorityDescription = `Tasks with highest priority.` + + NowDescription = `Tasks needing immediate attention: started, highest priority, must-do, or urgent+important.` + + RecentCompletionsDescription = `Tasks completed in the last 72 hours.` + + AreaTasksDescription = `Incomplete tasks in area. {area} accepts config key or UUID.` + + AreaFilteredDescription = `Filtered tasks in area. {area} accepts config key or UUID.` +) + +// Handler handles task list resource requests. +type Handler struct { + client *lunatask.Client + areas []shared.AreaProvider +} + +// NewHandler creates a new tasks resource handler. +func NewHandler(accessToken string, areas []shared.AreaProvider) *Handler { + return &Handler{ + client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")), + areas: areas, + } +} + +// taskSummary represents a task in list output. +type taskSummary struct { + DeepLink string `json:"deep_link"` + Status *string `json:"status,omitempty"` + Priority *int `json:"priority,omitempty"` + ScheduledOn *string `json:"scheduled_on,omitempty"` + CompletedAt *string `json:"completed_at,omitempty"` + CreatedAt string `json:"created_at"` + AreaID *string `json:"area_id,omitempty"` + GoalID *string `json:"goal_id,omitempty"` + Important *bool `json:"important,omitempty"` + Urgent *bool `json:"urgent,omitempty"` +} + +// FilterType identifies which filter to apply. +type FilterType int + +// Filter type constants. +const ( + TypeAll FilterType = iota + TypeToday + TypeOverdue + TypeNext7Days + TypeHighPriority + TypeNow + TypeRecentCompletions +) + +// HandleReadAll handles the all tasks resource. +func (h *Handler) HandleReadAll( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeAll, "") +} + +// HandleReadToday handles the today tasks resource. +func (h *Handler) HandleReadToday( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeToday, "") +} + +// HandleReadOverdue handles the overdue tasks resource. +func (h *Handler) HandleReadOverdue( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeOverdue, "") +} + +// HandleReadNext7Days handles the next 7 days tasks resource. +func (h *Handler) HandleReadNext7Days( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeNext7Days, "") +} + +// HandleReadHighPriority handles the high priority tasks resource. +func (h *Handler) HandleReadHighPriority( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeHighPriority, "") +} + +// HandleReadNow handles the now tasks resource. +func (h *Handler) HandleReadNow( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeNow, "") +} + +// HandleReadRecentCompletions handles the recent completions resource. +func (h *Handler) HandleReadRecentCompletions( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + return h.handleFiltered(ctx, req, TypeRecentCompletions, "") +} + +// HandleReadAreaTasks handles area-scoped task resources. +func (h *Handler) HandleReadAreaTasks( + ctx context.Context, + req *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { + areaRef, filterType := parseAreaURI(req.Params.URI) + if areaRef == "" { + return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI)) + } + + areaID, err := h.resolveAreaRef(areaRef) + if err != nil { + return nil, fmt.Errorf("invalid area %q: %w", areaRef, mcp.ResourceNotFoundError(req.Params.URI)) + } + + return h.handleFiltered(ctx, req, filterType, areaID) +} + +// resolveAreaRef resolves an area reference to a UUID. +// Accepts config key or UUID. +func (h *Handler) resolveAreaRef(input string) (string, error) { + // Try UUID or deep link first + if _, id, err := lunatask.ParseReference(input); err == nil { + return id, nil + } + + // Try config key lookup + for _, area := range h.areas { + if area.Key == input { + return area.ID, nil + } + } + + return "", fmt.Errorf("%w: %s", ErrUnknownArea, input) +} + +func (h *Handler) handleFiltered( + ctx context.Context, + req *mcp.ReadResourceRequest, + filterType FilterType, + areaID string, +) (*mcp.ReadResourceResult, error) { + tasks, err := h.client.ListTasks(ctx, nil) + if err != nil { + return nil, fmt.Errorf("fetching tasks: %w", err) + } + + // Apply area filter first if specified + if areaID != "" { + tasks = FilterByArea(tasks, areaID) + } + + // Apply semantic filter + tasks = applyFilter(tasks, filterType) + + summaries := buildSummaries(tasks) + + data, err := json.MarshalIndent(summaries, "", " ") + if err != nil { + return nil, fmt.Errorf("marshaling tasks: %w", err) + } + + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(data), + }}, + }, nil +} + +func applyFilter(tasks []lunatask.Task, filterType FilterType) []lunatask.Task { + switch filterType { + case TypeAll: + return filterIncomplete(tasks) + case TypeToday: + return FilterToday(tasks) + case TypeOverdue: + return FilterOverdue(tasks) + case TypeNext7Days: + return FilterNext7Days(tasks) + case TypeHighPriority: + return FilterHighPriority(tasks) + case TypeNow: + return FilterNow(tasks) + case TypeRecentCompletions: + return FilterRecentCompletions(tasks) + default: + return filterIncomplete(tasks) + } +} + +func filterIncomplete(tasks []lunatask.Task) []lunatask.Task { + result := make([]lunatask.Task, 0) + + for _, task := range tasks { + if !isCompleted(&task) { + result = append(result, task) + } + } + + return result +} + +func buildSummaries(tasks []lunatask.Task) []taskSummary { + summaries := make([]taskSummary, 0, len(tasks)) + + for _, task := range tasks { + summary := taskSummary{ + CreatedAt: task.CreatedAt.Format(time.RFC3339), + AreaID: task.AreaID, + GoalID: task.GoalID, + } + + summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID) + + if task.Status != nil { + s := string(*task.Status) + summary.Status = &s + } + + if task.Priority != nil { + p := int(*task.Priority) + summary.Priority = &p + } + + if task.ScheduledOn != nil { + s := task.ScheduledOn.Format("2006-01-02") + summary.ScheduledOn = &s + } + + if task.CompletedAt != nil { + s := task.CompletedAt.Format(time.RFC3339) + summary.CompletedAt = &s + } + + if task.Eisenhower != nil { + important := task.Eisenhower.IsImportant() + urgent := task.Eisenhower.IsUrgent() + summary.Important = &important + summary.Urgent = &urgent + } + + summaries = append(summaries, summary) + } + + return summaries +} + +// parseAreaURI extracts area_id and filter type from area-scoped URIs. +// Examples: +// - lunatask://area/uuid/tasks -> uuid, FilterAll +// - lunatask://area/uuid/today -> uuid, FilterToday +func parseAreaURI(uri string) (string, FilterType) { + const ( + prefix = "lunatask://area/" + maxParts = 2 + minLength = 1 + ) + + filterNameToType := map[string]FilterType{ + "tasks": TypeAll, + "today": TypeToday, + "overdue": TypeOverdue, + "next-7-days": TypeNext7Days, + "high-priority": TypeHighPriority, + "now": TypeNow, + "recent-completions": TypeRecentCompletions, + } + + if !strings.HasPrefix(uri, prefix) { + return "", TypeAll + } + + rest := strings.TrimPrefix(uri, prefix) + parts := strings.SplitN(rest, "/", maxParts) + + if len(parts) == 0 || parts[0] == "" { + return "", TypeAll + } + + areaID := parts[0] + + if len(parts) == minLength { + return areaID, TypeAll + } + + filterType, ok := filterNameToType[parts[1]] + if !ok { + return "", TypeAll + } + + return areaID, filterType +}