refactor: use external go-lunatask package

Amolith created

Remove internal lunatask/ package and depend on
git.secluded.site/go-lunatask instead. This allows the Lunatask API
client to be reused across projects.

Assisted-by: Claude Opus 4.5 via Crush

Change summary

AGENTS.md            |   1 
go.mod               |   3 
go.sum               |   2 
lunatask/builders.go | 304 ----------------------------------------------
lunatask/client.go   | 196 -----------------------------
lunatask/habits.go   |  41 ------
lunatask/journal.go  |  45 ------
lunatask/notes.go    | 142 ---------------------
lunatask/people.go   | 116 -----------------
lunatask/tasks.go    | 168 -------------------------
lunatask/timeline.go |  50 -------
lunatask/types.go    |  80 ------------
tools/habits.go      |   2 
tools/tasks.go       |   2 
14 files changed, 6 insertions(+), 1,146 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -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

go.mod 🔗

@@ -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

go.sum 🔗

@@ -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=

lunatask/builders.go 🔗

@@ -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 = &note
-	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 = &note
-	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 = &notebookID
-	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
-}

lunatask/client.go 🔗

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

lunatask/habits.go 🔗

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

lunatask/journal.go 🔗

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

lunatask/notes.go 🔗

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

lunatask/people.go 🔗

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

lunatask/tasks.go 🔗

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

lunatask/timeline.go 🔗

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

lunatask/types.go 🔗

@@ -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())
-}

tools/habits.go 🔗

@@ -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"
 )
 

tools/tasks.go 🔗

@@ -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"
 )