Detailed changes
@@ -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(
@@ -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
@@ -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 {
@@ -0,0 +1,171 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// 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
+}
@@ -0,0 +1,358 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// 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
+}