Detailed changes
@@ -23,7 +23,6 @@ just run # Build and run
```
cmd/lunatask-mcp-server.go → Config, tool registration, SSE server
tools/ → MCP tool handlers
-lunatask/ → HTTP client for Lunatask API
```
**Data flow**: SSE request → MCP server → tool handler → lunatask client → Lunatask API
@@ -4,9 +4,10 @@
module git.sr.ht/~amolith/lunatask-mcp-server
-go 1.24.2
+go 1.25.5
require (
+ git.secluded.site/go-lunatask v0.1.0-rc1
github.com/BurntSushi/toml v1.5.0
github.com/ijt/go-anytime v1.9.2
github.com/mark3labs/mcp-go v0.23.1
@@ -1,3 +1,5 @@
+git.secluded.site/go-lunatask v0.1.0-rc1 h1:02np5gzm7f0D9uOcvq0/6qsLjREbyowQ++ZQTG9JNVA=
+git.secluded.site/go-lunatask v0.1.0-rc1/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -1,304 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import "time"
-
-// TaskBuilder provides a fluent interface for constructing CreateTaskRequest.
-type TaskBuilder struct {
- req CreateTaskRequest
-}
-
-// NewTask creates a new TaskBuilder with the required name field.
-func NewTask(name string) *TaskBuilder {
- return &TaskBuilder{req: CreateTaskRequest{Name: name}}
-}
-
-// InArea sets the area ID for the task.
-func (b *TaskBuilder) InArea(areaID string) *TaskBuilder {
- b.req.AreaID = &areaID
- return b
-}
-
-// InGoal sets the goal ID for the task.
-func (b *TaskBuilder) InGoal(goalID string) *TaskBuilder {
- b.req.GoalID = &goalID
- return b
-}
-
-// WithNote sets the note/description for the task.
-func (b *TaskBuilder) WithNote(note string) *TaskBuilder {
- b.req.Note = ¬e
- return b
-}
-
-// WithStatus sets the task status (later, next, started, waiting, completed).
-func (b *TaskBuilder) WithStatus(status string) *TaskBuilder {
- b.req.Status = &status
- return b
-}
-
-// WithMotivation sets the motivation (must, should, want).
-func (b *TaskBuilder) WithMotivation(motivation string) *TaskBuilder {
- b.req.Motivation = &motivation
- return b
-}
-
-// WithEstimate sets the time estimate in minutes (0-720).
-func (b *TaskBuilder) WithEstimate(minutes int) *TaskBuilder {
- b.req.Estimate = &minutes
- return b
-}
-
-// WithPriority sets the priority (-2 to 2: lowest, low, neutral, high, highest).
-func (b *TaskBuilder) WithPriority(priority int) *TaskBuilder {
- b.req.Priority = &priority
- return b
-}
-
-// WithEisenhower sets the Eisenhower matrix category (0-4).
-func (b *TaskBuilder) WithEisenhower(eisenhower int) *TaskBuilder {
- b.req.Eisenhower = &eisenhower
- return b
-}
-
-// ScheduledOn sets the scheduled date for the task.
-func (b *TaskBuilder) ScheduledOn(date Date) *TaskBuilder {
- b.req.ScheduledOn = &date
- return b
-}
-
-// CompletedAt sets the completion timestamp.
-func (b *TaskBuilder) CompletedAt(t time.Time) *TaskBuilder {
- b.req.CompletedAt = &t
- return b
-}
-
-// FromSource sets the external source integration.
-func (b *TaskBuilder) FromSource(source, sourceID string) *TaskBuilder {
- b.req.Source = &source
- b.req.SourceID = &sourceID
- return b
-}
-
-// Build returns the constructed CreateTaskRequest.
-func (b *TaskBuilder) Build() *CreateTaskRequest {
- return &b.req
-}
-
-// TaskUpdateBuilder provides a fluent interface for constructing UpdateTaskRequest.
-type TaskUpdateBuilder struct {
- req UpdateTaskRequest
-}
-
-// NewTaskUpdate creates a new TaskUpdateBuilder.
-func NewTaskUpdate() *TaskUpdateBuilder {
- return &TaskUpdateBuilder{}
-}
-
-// Name sets the task name.
-func (b *TaskUpdateBuilder) Name(name string) *TaskUpdateBuilder {
- b.req.Name = &name
- return b
-}
-
-// InArea sets the area ID for the task.
-func (b *TaskUpdateBuilder) InArea(areaID string) *TaskUpdateBuilder {
- b.req.AreaID = &areaID
- return b
-}
-
-// InGoal sets the goal ID for the task.
-func (b *TaskUpdateBuilder) InGoal(goalID string) *TaskUpdateBuilder {
- b.req.GoalID = &goalID
- return b
-}
-
-// WithNote sets the note/description for the task.
-func (b *TaskUpdateBuilder) WithNote(note string) *TaskUpdateBuilder {
- b.req.Note = ¬e
- return b
-}
-
-// WithStatus sets the task status (later, next, started, waiting, completed).
-func (b *TaskUpdateBuilder) WithStatus(status string) *TaskUpdateBuilder {
- b.req.Status = &status
- return b
-}
-
-// WithMotivation sets the motivation (must, should, want).
-func (b *TaskUpdateBuilder) WithMotivation(motivation string) *TaskUpdateBuilder {
- b.req.Motivation = &motivation
- return b
-}
-
-// WithEstimate sets the time estimate in minutes (0-720).
-func (b *TaskUpdateBuilder) WithEstimate(minutes int) *TaskUpdateBuilder {
- b.req.Estimate = &minutes
- return b
-}
-
-// WithPriority sets the priority (-2 to 2: lowest, low, neutral, high, highest).
-func (b *TaskUpdateBuilder) WithPriority(priority int) *TaskUpdateBuilder {
- b.req.Priority = &priority
- return b
-}
-
-// WithEisenhower sets the Eisenhower matrix category (0-4).
-func (b *TaskUpdateBuilder) WithEisenhower(eisenhower int) *TaskUpdateBuilder {
- b.req.Eisenhower = &eisenhower
- return b
-}
-
-// ScheduledOn sets the scheduled date for the task.
-func (b *TaskUpdateBuilder) ScheduledOn(date Date) *TaskUpdateBuilder {
- b.req.ScheduledOn = &date
- return b
-}
-
-// CompletedAt sets the completion timestamp.
-func (b *TaskUpdateBuilder) CompletedAt(t time.Time) *TaskUpdateBuilder {
- b.req.CompletedAt = &t
- return b
-}
-
-// Build returns the constructed UpdateTaskRequest.
-func (b *TaskUpdateBuilder) Build() *UpdateTaskRequest {
- return &b.req
-}
-
-// NoteBuilder provides a fluent interface for constructing CreateNoteRequest.
-type NoteBuilder struct {
- req CreateNoteRequest
-}
-
-// NewNote creates a new NoteBuilder.
-func NewNote() *NoteBuilder {
- return &NoteBuilder{}
-}
-
-// WithName sets the note name/title.
-func (b *NoteBuilder) WithName(name string) *NoteBuilder {
- b.req.Name = &name
- return b
-}
-
-// WithContent sets the note content.
-func (b *NoteBuilder) WithContent(content string) *NoteBuilder {
- b.req.Content = &content
- return b
-}
-
-// InNotebook sets the notebook ID.
-func (b *NoteBuilder) InNotebook(notebookID string) *NoteBuilder {
- b.req.NotebookID = ¬ebookID
- return b
-}
-
-// FromSource sets the external source integration.
-func (b *NoteBuilder) FromSource(source, sourceID string) *NoteBuilder {
- b.req.Source = &source
- b.req.SourceID = &sourceID
- return b
-}
-
-// Build returns the constructed CreateNoteRequest.
-func (b *NoteBuilder) Build() *CreateNoteRequest {
- return &b.req
-}
-
-// JournalEntryBuilder provides a fluent interface for constructing CreateJournalEntryRequest.
-type JournalEntryBuilder struct {
- req CreateJournalEntryRequest
-}
-
-// NewJournalEntry creates a new JournalEntryBuilder with the required date.
-func NewJournalEntry(date Date) *JournalEntryBuilder {
- return &JournalEntryBuilder{req: CreateJournalEntryRequest{DateOn: date}}
-}
-
-// WithName sets the entry title (defaults to weekday name if omitted).
-func (b *JournalEntryBuilder) WithName(name string) *JournalEntryBuilder {
- b.req.Name = &name
- return b
-}
-
-// WithContent sets the Markdown content.
-func (b *JournalEntryBuilder) WithContent(content string) *JournalEntryBuilder {
- b.req.Content = &content
- return b
-}
-
-// Build returns the constructed CreateJournalEntryRequest.
-func (b *JournalEntryBuilder) Build() *CreateJournalEntryRequest {
- return &b.req
-}
-
-// PersonBuilder provides a fluent interface for constructing CreatePersonRequest.
-type PersonBuilder struct {
- req CreatePersonRequest
-}
-
-// NewPerson creates a new PersonBuilder.
-func NewPerson() *PersonBuilder {
- return &PersonBuilder{}
-}
-
-// WithFirstName sets the person's first name.
-func (b *PersonBuilder) WithFirstName(name string) *PersonBuilder {
- b.req.FirstName = &name
- return b
-}
-
-// WithLastName sets the person's last name.
-func (b *PersonBuilder) WithLastName(name string) *PersonBuilder {
- b.req.LastName = &name
- return b
-}
-
-// WithRelationshipStrength sets the relationship strength category.
-func (b *PersonBuilder) WithRelationshipStrength(strength string) *PersonBuilder {
- b.req.RelationshipStrength = &strength
- return b
-}
-
-// FromSource sets the external source integration.
-func (b *PersonBuilder) FromSource(source, sourceID string) *PersonBuilder {
- b.req.Source = &source
- b.req.SourceID = &sourceID
- return b
-}
-
-// Build returns the constructed CreatePersonRequest.
-func (b *PersonBuilder) Build() *CreatePersonRequest {
- return &b.req
-}
-
-// TimelineNoteBuilder provides a fluent interface for constructing CreatePersonTimelineNoteRequest.
-type TimelineNoteBuilder struct {
- req CreatePersonTimelineNoteRequest
-}
-
-// NewTimelineNote creates a new TimelineNoteBuilder with the required person ID.
-func NewTimelineNote(personID string) *TimelineNoteBuilder {
- return &TimelineNoteBuilder{req: CreatePersonTimelineNoteRequest{PersonID: personID}}
-}
-
-// OnDate sets the date for the timeline note.
-func (b *TimelineNoteBuilder) OnDate(date Date) *TimelineNoteBuilder {
- b.req.DateOn = &date
- return b
-}
-
-// WithContent sets the Markdown content.
-func (b *TimelineNoteBuilder) WithContent(content string) *TimelineNoteBuilder {
- b.req.Content = &content
- return b
-}
-
-// Build returns the constructed CreatePersonTimelineNoteRequest.
-func (b *TimelineNoteBuilder) Build() *CreatePersonTimelineNoteRequest {
- return &b.req
-}
@@ -1,196 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
-)
-
-// API error types for typed error handling
-var (
- // ErrBadRequest indicates invalid, malformed, or missing parameters (400)
- ErrBadRequest = errors.New("bad request")
- // ErrUnauthorized indicates missing, wrong, or revoked access token (401)
- ErrUnauthorized = errors.New("unauthorized")
- // ErrPaymentRequired indicates a subscription is required (402)
- ErrPaymentRequired = errors.New("subscription required")
- // ErrNotFound indicates the specified entity could not be found (404)
- ErrNotFound = errors.New("not found")
- // ErrUnprocessableEntity indicates the provided entity is not valid (422)
- ErrUnprocessableEntity = errors.New("unprocessable entity")
- // ErrServerError indicates an internal server error (500)
- ErrServerError = errors.New("server error")
- // ErrServiceUnavailable indicates temporary maintenance (503)
- ErrServiceUnavailable = errors.New("service unavailable")
- // ErrTimeout indicates request timed out (524)
- ErrTimeout = errors.New("request timed out")
-)
-
-// APIError wraps an API error with status code and response body
-type APIError struct {
- StatusCode int
- Body string
- Err error
-}
-
-func (e *APIError) Error() string {
- if e.Body != "" {
- return fmt.Sprintf("%s (status %d): %s", e.Err.Error(), e.StatusCode, e.Body)
- }
- return fmt.Sprintf("%s (status %d)", e.Err.Error(), e.StatusCode)
-}
-
-func (e *APIError) Unwrap() error {
- return e.Err
-}
-
-// newAPIError creates an APIError from an HTTP status code
-func newAPIError(statusCode int, body string) *APIError {
- var err error
- switch statusCode {
- case http.StatusBadRequest:
- err = ErrBadRequest
- case http.StatusUnauthorized:
- err = ErrUnauthorized
- case http.StatusPaymentRequired:
- err = ErrPaymentRequired
- case http.StatusNotFound:
- err = ErrNotFound
- case http.StatusUnprocessableEntity:
- err = ErrUnprocessableEntity
- case http.StatusInternalServerError:
- err = ErrServerError
- case http.StatusServiceUnavailable:
- err = ErrServiceUnavailable
- case 524:
- err = ErrTimeout
- default:
- err = fmt.Errorf("unexpected status %d", statusCode)
- }
- return &APIError{StatusCode: statusCode, Body: body, Err: err}
-}
-
-// DefaultBaseURL is the default Lunatask API base URL.
-const DefaultBaseURL = "https://api.lunatask.app/v1"
-
-// Client handles communication with the Lunatask API.
-type Client struct {
- accessToken string
- baseURL string
- httpClient *http.Client
-}
-
-// Option configures a Client.
-type Option func(*Client)
-
-// WithHTTPClient sets a custom HTTP client.
-func WithHTTPClient(client *http.Client) Option {
- return func(c *Client) {
- c.httpClient = client
- }
-}
-
-// WithBaseURL sets a custom base URL (useful for testing).
-func WithBaseURL(url string) Option {
- return func(c *Client) {
- c.baseURL = url
- }
-}
-
-// NewClient creates a new Lunatask API client.
-func NewClient(accessToken string, opts ...Option) *Client {
- c := &Client{
- accessToken: accessToken,
- baseURL: DefaultBaseURL,
- httpClient: &http.Client{},
- }
- for _, opt := range opts {
- opt(c)
- }
- return c
-}
-
-// doRequest performs an HTTP request and handles common response processing.
-// Returns the response body and status code. 204 No Content returns nil body.
-func (c *Client) doRequest(req *http.Request) ([]byte, int, error) {
- req.Header.Set("Authorization", "bearer "+c.accessToken)
-
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, 0, fmt.Errorf("failed to send HTTP request: %w", err)
- }
- defer func() { _ = resp.Body.Close() }()
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, resp.StatusCode, fmt.Errorf("failed to read response body: %w", err)
- }
-
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- return nil, resp.StatusCode, newAPIError(resp.StatusCode, string(body))
- }
-
- // 204 No Content is a valid success with no body
- if resp.StatusCode == http.StatusNoContent {
- return nil, resp.StatusCode, nil
- }
-
- return body, resp.StatusCode, nil
-}
-
-// doJSON performs an HTTP request with optional JSON body and unmarshals the response.
-// Returns (nil, true, nil) for 204 No Content responses.
-func doJSON[T any](c *Client, ctx context.Context, method, path string, body any) (*T, bool, error) {
- var reqBody io.Reader
- if body != nil {
- data, err := json.Marshal(body)
- if err != nil {
- return nil, false, fmt.Errorf("failed to marshal request body: %w", err)
- }
- reqBody = bytes.NewReader(data)
- }
-
- req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
- if err != nil {
- return nil, false, fmt.Errorf("failed to create HTTP request: %w", err)
- }
- if body != nil {
- req.Header.Set("Content-Type", "application/json")
- }
-
- respBody, statusCode, err := c.doRequest(req)
- if err != nil {
- return nil, false, err
- }
-
- if statusCode == http.StatusNoContent {
- return nil, true, nil
- }
-
- var result T
- if err := json.Unmarshal(respBody, &result); err != nil {
- return nil, false, fmt.Errorf("failed to parse response: %w", err)
- }
-
- return &result, false, nil
-}
-
-// PingResponse represents the response from the /ping endpoint.
-type PingResponse struct {
- Message string `json:"message"`
-}
-
-// Ping verifies the access token is valid by calling the /ping endpoint.
-// Returns the ping response on success, or an error (typically ErrUnauthorized) on failure.
-func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
- resp, _, err := doJSON[PingResponse](c, ctx, http.MethodGet, "/ping", nil)
- return resp, err
-}
@@ -1,41 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "fmt"
- "net/http"
-)
-
-// TrackHabitActivityRequest represents the request to track a habit activity.
-type TrackHabitActivityRequest struct {
- // PerformedOn is the date when the activity was performed.
- PerformedOn Date `json:"performed_on"`
-}
-
-// TrackHabitActivityResponse represents the response from Lunatask API when tracking a habit activity.
-type TrackHabitActivityResponse struct {
- Status string `json:"status"`
- Message string `json:"message,omitempty"`
-}
-
-// TrackHabitActivity tracks an activity for a habit in Lunatask.
-// The PerformedOn field must be an ISO-8601 date (e.g., "2024-08-26").
-func (c *Client) TrackHabitActivity(ctx context.Context, habitID string, request *TrackHabitActivityRequest) (*TrackHabitActivityResponse, error) {
- if habitID == "" {
- return nil, fmt.Errorf("%w: habit ID cannot be empty", ErrBadRequest)
- }
- if request.PerformedOn.IsZero() {
- return nil, fmt.Errorf("%w: performed_on is required", ErrBadRequest)
- }
-
- resp, _, err := doJSON[TrackHabitActivityResponse](c, ctx, http.MethodPost, "/habits/"+habitID+"/track", request)
- if err != nil {
- return nil, err
- }
-
- return resp, nil
-}
@@ -1,45 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "net/http"
- "time"
-)
-
-// JournalEntry represents a journal entry returned from the Lunatask API.
-// Note: name and content are E2EE and not returned by the API.
-type JournalEntry struct {
- ID string `json:"id"`
- DateOn Date `json:"date_on"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// CreateJournalEntryRequest represents the request to create a journal entry.
-type CreateJournalEntryRequest struct {
- // DateOn is the date for the journal entry (required).
- DateOn Date `json:"date_on"`
- // Name is the title for the entry (optional, defaults to weekday name like "Tuesday, July 1st").
- Name *string `json:"name,omitempty"`
- // Content is the Markdown content of the entry (optional).
- Content *string `json:"content,omitempty"`
-}
-
-// journalEntryResponse represents a single journal entry response from the API.
-type journalEntryResponse struct {
- JournalEntry JournalEntry `json:"journal_entry"`
-}
-
-// CreateJournalEntry creates a new journal entry for a given date.
-func (c *Client) CreateJournalEntry(ctx context.Context, entry *CreateJournalEntryRequest) (*JournalEntry, error) {
- resp, _, err := doJSON[journalEntryResponse](c, ctx, http.MethodPost, "/journal_entries", entry)
- if err != nil {
- return nil, err
- }
-
- return &resp.JournalEntry, nil
-}
@@ -1,142 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "time"
-)
-
-// Note represents a note returned from the Lunatask API.
-// Note: name and content are E2EE and not returned by the API.
-type Note struct {
- ID string `json:"id"`
- NotebookID *string `json:"notebook_id"`
- DateOn *Date `json:"date_on"`
- Sources []Source `json:"sources"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// CreateNoteRequest represents the request to create a note in Lunatask.
-type CreateNoteRequest struct {
- Name *string `json:"name,omitempty"`
- Content *string `json:"content,omitempty"`
- NotebookID *string `json:"notebook_id,omitempty"`
- Source *string `json:"source,omitempty"`
- SourceID *string `json:"source_id,omitempty"`
-}
-
-// UpdateNoteRequest represents the request to update a note in Lunatask.
-// All fields are optional; only provided fields will be updated.
-// Note: updating content replaces the entire content (E2EE prevents appending).
-type UpdateNoteRequest struct {
- Name *string `json:"name,omitempty"`
- Content *string `json:"content,omitempty"`
- NotebookID *string `json:"notebook_id,omitempty"`
- DateOn *Date `json:"date_on,omitempty"`
-}
-
-// noteResponse represents a single note response from the API.
-type noteResponse struct {
- Note Note `json:"note"`
-}
-
-// notesResponse represents a list of notes response from the API.
-type notesResponse struct {
- Notes []Note `json:"notes"`
-}
-
-// ListNotesOptions contains optional filters for listing notes.
-type ListNotesOptions struct {
- Source *string
- SourceID *string
-}
-
-// ListNotes retrieves all notes, optionally filtered by source and/or source_id.
-func (c *Client) ListNotes(ctx context.Context, opts *ListNotesOptions) ([]Note, error) {
- path := "/notes"
-
- if opts != nil {
- params := url.Values{}
- if opts.Source != nil && *opts.Source != "" {
- params.Set("source", *opts.Source)
- }
- if opts.SourceID != nil && *opts.SourceID != "" {
- params.Set("source_id", *opts.SourceID)
- }
- if len(params) > 0 {
- path = fmt.Sprintf("%s?%s", path, params.Encode())
- }
- }
-
- resp, _, err := doJSON[notesResponse](c, ctx, http.MethodGet, path, nil)
- if err != nil {
- return nil, err
- }
-
- return resp.Notes, nil
-}
-
-// GetNote retrieves a specific note by ID.
-// Note: name and content fields are E2EE and will be null in the response.
-func (c *Client) GetNote(ctx context.Context, noteID string) (*Note, error) {
- if noteID == "" {
- return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[noteResponse](c, ctx, http.MethodGet, "/notes/"+noteID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Note, nil
-}
-
-// CreateNote creates a new note in Lunatask.
-// Returns nil, nil if a matching note already exists in the same notebook
-// with the same source/source_id (HTTP 204).
-func (c *Client) CreateNote(ctx context.Context, note *CreateNoteRequest) (*Note, error) {
- resp, noContent, err := doJSON[noteResponse](c, ctx, http.MethodPost, "/notes", note)
- if err != nil {
- return nil, err
- }
- if noContent {
- return nil, nil
- }
-
- return &resp.Note, nil
-}
-
-// UpdateNote updates an existing note in Lunatask.
-func (c *Client) UpdateNote(ctx context.Context, noteID string, note *UpdateNoteRequest) (*Note, error) {
- if noteID == "" {
- return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[noteResponse](c, ctx, http.MethodPut, "/notes/"+noteID, note)
- if err != nil {
- return nil, err
- }
-
- return &resp.Note, nil
-}
-
-// DeleteNote deletes a note in Lunatask.
-func (c *Client) DeleteNote(ctx context.Context, noteID string) (*Note, error) {
- if noteID == "" {
- return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[noteResponse](c, ctx, http.MethodDelete, "/notes/"+noteID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Note, nil
-}
@@ -1,116 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "time"
-)
-
-// Person represents a person/relationship returned from the Lunatask API.
-// Note: first_name and last_name are E2EE and not returned by the API.
-type Person struct {
- ID string `json:"id"`
- RelationshipStrength *string `json:"relationship_strength"`
- Sources []Source `json:"sources"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// CreatePersonRequest represents the request to create a person in Lunatask.
-type CreatePersonRequest struct {
- FirstName *string `json:"first_name,omitempty"`
- LastName *string `json:"last_name,omitempty"`
- RelationshipStrength *string `json:"relationship_strength,omitempty"`
- Source *string `json:"source,omitempty"`
- SourceID *string `json:"source_id,omitempty"`
-}
-
-// personResponse represents a single person response from the API.
-type personResponse struct {
- Person Person `json:"person"`
-}
-
-// peopleResponse represents a list of people response from the API.
-type peopleResponse struct {
- People []Person `json:"people"`
-}
-
-// ListPeopleOptions contains optional filters for listing people.
-type ListPeopleOptions struct {
- Source *string
- SourceID *string
-}
-
-// ListPeople retrieves all people, optionally filtered by source and/or source_id.
-func (c *Client) ListPeople(ctx context.Context, opts *ListPeopleOptions) ([]Person, error) {
- path := "/people"
-
- if opts != nil {
- params := url.Values{}
- if opts.Source != nil && *opts.Source != "" {
- params.Set("source", *opts.Source)
- }
- if opts.SourceID != nil && *opts.SourceID != "" {
- params.Set("source_id", *opts.SourceID)
- }
- if len(params) > 0 {
- path = fmt.Sprintf("%s?%s", path, params.Encode())
- }
- }
-
- resp, _, err := doJSON[peopleResponse](c, ctx, http.MethodGet, path, nil)
- if err != nil {
- return nil, err
- }
-
- return resp.People, nil
-}
-
-// GetPerson retrieves a specific person by ID.
-func (c *Client) GetPerson(ctx context.Context, personID string) (*Person, error) {
- if personID == "" {
- return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[personResponse](c, ctx, http.MethodGet, "/people/"+personID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Person, nil
-}
-
-// CreatePerson creates a new person/relationship in Lunatask.
-// Returns nil, nil if a matching person already exists with the same
-// source/source_id (HTTP 204).
-func (c *Client) CreatePerson(ctx context.Context, person *CreatePersonRequest) (*Person, error) {
- resp, noContent, err := doJSON[personResponse](c, ctx, http.MethodPost, "/people", person)
- if err != nil {
- return nil, err
- }
- if noContent {
- return nil, nil
- }
-
- return &resp.Person, nil
-}
-
-// DeletePerson deletes a person in Lunatask.
-func (c *Client) DeletePerson(ctx context.Context, personID string) (*Person, error) {
- if personID == "" {
- return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[personResponse](c, ctx, http.MethodDelete, "/people/"+personID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Person, nil
-}
@@ -1,168 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "time"
-)
-
-// Task represents a task returned from the Lunatask API
-type Task struct {
- ID string `json:"id"`
- AreaID *string `json:"area_id"`
- GoalID *string `json:"goal_id"`
- Name *string `json:"name"`
- Note *string `json:"note"`
- Status *string `json:"status"`
- PreviousStatus *string `json:"previous_status"`
- Estimate *int `json:"estimate"`
- Priority *int `json:"priority"`
- Progress *int `json:"progress"`
- Motivation *string `json:"motivation"`
- Eisenhower *int `json:"eisenhower"`
- Sources []Source `json:"sources"`
- ScheduledOn *Date `json:"scheduled_on"`
- CompletedAt *time.Time `json:"completed_at"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// CreateTaskRequest represents the request to create a task in Lunatask
-type CreateTaskRequest struct {
- Name string `json:"name"`
- AreaID *string `json:"area_id,omitempty"`
- GoalID *string `json:"goal_id,omitempty"`
- Note *string `json:"note,omitempty"`
- Status *string `json:"status,omitempty"`
- Motivation *string `json:"motivation,omitempty"`
- Estimate *int `json:"estimate,omitempty"`
- Priority *int `json:"priority,omitempty"`
- Eisenhower *int `json:"eisenhower,omitempty"`
- ScheduledOn *Date `json:"scheduled_on,omitempty"`
- CompletedAt *time.Time `json:"completed_at,omitempty"`
- Source *string `json:"source,omitempty"`
- SourceID *string `json:"source_id,omitempty"`
-}
-
-// UpdateTaskRequest represents the request to update a task in Lunatask.
-// All fields are optional; only provided fields will be updated.
-type UpdateTaskRequest struct {
- Name *string `json:"name,omitempty"`
- AreaID *string `json:"area_id,omitempty"`
- GoalID *string `json:"goal_id,omitempty"`
- Note *string `json:"note,omitempty"`
- Status *string `json:"status,omitempty"`
- Motivation *string `json:"motivation,omitempty"`
- Estimate *int `json:"estimate,omitempty"`
- Priority *int `json:"priority,omitempty"`
- Eisenhower *int `json:"eisenhower,omitempty"`
- ScheduledOn *Date `json:"scheduled_on,omitempty"`
- CompletedAt *time.Time `json:"completed_at,omitempty"`
-}
-
-// taskResponse represents a single task response from the API
-type taskResponse struct {
- Task Task `json:"task"`
-}
-
-// tasksResponse represents a list of tasks response from the API
-type tasksResponse struct {
- Tasks []Task `json:"tasks"`
-}
-
-// ListTasksOptions contains optional filters for listing tasks
-type ListTasksOptions struct {
- Source *string
- SourceID *string
-}
-
-// ListTasks retrieves all tasks, optionally filtered by source and/or source_id
-func (c *Client) ListTasks(ctx context.Context, opts *ListTasksOptions) ([]Task, error) {
- path := "/tasks"
-
- if opts != nil {
- params := url.Values{}
- if opts.Source != nil && *opts.Source != "" {
- params.Set("source", *opts.Source)
- }
- if opts.SourceID != nil && *opts.SourceID != "" {
- params.Set("source_id", *opts.SourceID)
- }
- if len(params) > 0 {
- path = fmt.Sprintf("%s?%s", path, params.Encode())
- }
- }
-
- resp, _, err := doJSON[tasksResponse](c, ctx, http.MethodGet, path, nil)
- if err != nil {
- return nil, err
- }
-
- return resp.Tasks, nil
-}
-
-// GetTask retrieves a specific task by ID
-func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error) {
- if taskID == "" {
- return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[taskResponse](c, ctx, http.MethodGet, "/tasks/"+taskID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Task, nil
-}
-
-// CreateTask creates a new task in Lunatask.
-// Returns nil, nil if a matching task already exists (HTTP 204).
-func (c *Client) CreateTask(ctx context.Context, task *CreateTaskRequest) (*Task, error) {
- if task.Name == "" {
- return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
- }
-
- resp, noContent, err := doJSON[taskResponse](c, ctx, http.MethodPost, "/tasks", task)
- if err != nil {
- return nil, err
- }
- if noContent {
- return nil, nil
- }
-
- return &resp.Task, nil
-}
-
-// UpdateTask updates an existing task in Lunatask
-func (c *Client) UpdateTask(ctx context.Context, taskID string, task *UpdateTaskRequest) (*Task, error) {
- if taskID == "" {
- return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[taskResponse](c, ctx, http.MethodPut, "/tasks/"+taskID, task)
- if err != nil {
- return nil, err
- }
-
- return &resp.Task, nil
-}
-
-// DeleteTask deletes a task in Lunatask
-func (c *Client) DeleteTask(ctx context.Context, taskID string) (*Task, error) {
- if taskID == "" {
- return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
- }
-
- resp, _, err := doJSON[taskResponse](c, ctx, http.MethodDelete, "/tasks/"+taskID, nil)
- if err != nil {
- return nil, err
- }
-
- return &resp.Task, nil
-}
@@ -1,50 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "context"
- "fmt"
- "net/http"
- "time"
-)
-
-// PersonTimelineNote represents a note on a person's memory timeline.
-// Note: content is E2EE and not returned by the API.
-type PersonTimelineNote struct {
- ID string `json:"id"`
- DateOn *Date `json:"date_on"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// CreatePersonTimelineNoteRequest represents the request to create a timeline note.
-type CreatePersonTimelineNoteRequest struct {
- // PersonID is the ID of the person to add the note to (required).
- PersonID string `json:"person_id"`
- // DateOn is the ISO-8601 date for the note (optional, defaults to today).
- DateOn *Date `json:"date_on,omitempty"`
- // Content is the Markdown content of the note (optional but impractical if empty).
- Content *string `json:"content,omitempty"`
-}
-
-// personTimelineNoteResponse represents a single timeline note response from the API.
-type personTimelineNoteResponse struct {
- PersonTimelineNote PersonTimelineNote `json:"person_timeline_note"`
-}
-
-// CreatePersonTimelineNote creates a new note on a person's memory timeline.
-func (c *Client) CreatePersonTimelineNote(ctx context.Context, note *CreatePersonTimelineNoteRequest) (*PersonTimelineNote, error) {
- if note.PersonID == "" {
- return nil, fmt.Errorf("%w: person_id is required", ErrBadRequest)
- }
-
- resp, _, err := doJSON[personTimelineNoteResponse](c, ctx, http.MethodPost, "/person_timeline_notes", note)
- if err != nil {
- return nil, err
- }
-
- return &resp.PersonTimelineNote, nil
-}
@@ -1,80 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package lunatask
-
-import (
- "encoding/json"
- "time"
-)
-
-// Source represents an external source integration (e.g., GitHub, Todoist).
-// Used across multiple entity types for tracking where items originated.
-type Source struct {
- Source string `json:"source"`
- SourceID string `json:"source_id"`
-}
-
-// Date represents a date-only value (YYYY-MM-DD) with proper JSON marshaling.
-// Used for fields like scheduled_on that don't include time components.
-type Date struct {
- time.Time
-}
-
-const dateFormat = "2006-01-02"
-
-// MarshalJSON implements json.Marshaler for Date.
-func (d Date) MarshalJSON() ([]byte, error) {
- if d.IsZero() {
- return []byte("null"), nil
- }
- return json.Marshal(d.Format(dateFormat))
-}
-
-// UnmarshalJSON implements json.Unmarshaler for Date.
-func (d *Date) UnmarshalJSON(data []byte) error {
- if string(data) == "null" {
- return nil
- }
-
- var s string
- if err := json.Unmarshal(data, &s); err != nil {
- return err
- }
-
- t, err := time.Parse(dateFormat, s)
- if err != nil {
- return err
- }
-
- d.Time = t
- return nil
-}
-
-// String returns the date in YYYY-MM-DD format.
-func (d Date) String() string {
- if d.IsZero() {
- return ""
- }
- return d.Format(dateFormat)
-}
-
-// NewDate creates a Date from a time.Time, discarding time components.
-func NewDate(t time.Time) Date {
- return Date{time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)}
-}
-
-// ParseDate parses a date string in YYYY-MM-DD format.
-func ParseDate(s string) (Date, error) {
- t, err := time.Parse(dateFormat, s)
- if err != nil {
- return Date{}, err
- }
- return Date{t}, nil
-}
-
-// Today returns the current date.
-func Today() Date {
- return NewDate(time.Now())
-}
@@ -9,7 +9,7 @@ import (
"fmt"
"strings"
- "git.sr.ht/~amolith/lunatask-mcp-server/lunatask"
+ "git.secluded.site/go-lunatask"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -9,7 +9,7 @@ import (
"fmt"
"strings"
- "git.sr.ht/~amolith/lunatask-mcp-server/lunatask"
+ "git.secluded.site/go-lunatask"
"github.com/mark3labs/mcp-go/mcp"
)