feat(api): one API for curl and MCP, REST reads with keys, origin badge with key name
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m16s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m16s
API keys reach tasks and lists over plain REST too (GET /task-lists, GET /tasks with
status/due filters, PATCH/DELETE /thoughts/{id}, GET /lists, list items), guarded by a route
table; notes and settings stay closed. Rights hold for every access alike (webhook:task also
allows create_task over MCP, tasks:write also task delivery).
Tasks, thoughts and list items record which key created them and which one changed them last
(migration 13 replaces agent_mark; origin stays after the user's own edits and survives the
triage into a list item). OriginBadge shows the key name with a webhook or robot icon, details
on hover and tap, and replaces the Webhook and KI badges everywhere, now also on list items.
Settings: category "API" with rights grouped by tasks, lists and inbox and two tabs
(curl / smart home with a Home Assistant sensor, AI agents over MCP). Docs, help, README and
changelog updated; v0.30.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ca715ba64
commit
b883315914
36 files changed
+1119
-563
No files matched your search
@@ -76,14 +76,11 @@ func (s *Store) CreateThoughtWithDetails(userID, content, priority, source, dueD
|
||||
Notes: notes,
|
||||
CreatedAt: domain.NowISO(),
|
||||
}
|
||||
if source == "agent" {
|
||||
t.AgentMark = AgentMarkCreated
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO thoughts (id, user_id, content, tags, priority, source, status, is_completed, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, subtasks, agent_mark)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 0, ?, '[]', ?)`,
|
||||
t.ID, t.UserID, t.Content, string(tagsJSON), t.Priority, t.Source, string(t.Status), t.DueDate, t.TaskListID, t.KanbanStatus, t.Notes, t.CreatedAt, t.AgentMark,
|
||||
`INSERT INTO thoughts (id, user_id, content, tags, priority, source, status, is_completed, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, subtasks)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 0, ?, '[]')`,
|
||||
t.ID, t.UserID, t.Content, string(tagsJSON), t.Priority, t.Source, string(t.Status), t.DueDate, t.TaskListID, t.KanbanStatus, t.Notes, t.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -98,16 +95,19 @@ func scanThought(rows interface {
|
||||
t := &domain.Thought{}
|
||||
var tagsStr, subtasksStr, statusStr string
|
||||
var isCompleted, alarmSent, sortOrder int
|
||||
var origin originDest
|
||||
err := rows.Scan(
|
||||
&t.ID, &t.UserID, &t.Content, &tagsStr, &t.Priority, &t.Source,
|
||||
&statusStr, &isCompleted, &t.ConvertedType, &t.ConvertedRefID,
|
||||
&t.DueDate, &t.TaskListID, &t.KanbanStatus, &t.Notes, &alarmSent,
|
||||
&t.CreatedAt, &t.TriagedAt, &subtasksStr, &sortOrder, &t.Recurrence, &t.AgentMark,
|
||||
&t.CreatedAt, &t.TriagedAt, &subtasksStr, &sortOrder, &t.Recurrence,
|
||||
&origin.createdVia, &origin.createdKey, &origin.changedVia, &origin.changedKey, &origin.changedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.SortOrder = sortOrder
|
||||
t.Origin = origin.result()
|
||||
t.Status = domain.ThoughtStatus(statusStr)
|
||||
t.IsCompleted = isCompleted != 0
|
||||
t.AlarmSent = alarmSent != 0
|
||||
@@ -139,7 +139,7 @@ func (s *Store) accessibleTaskListIDsQuery() string {
|
||||
|
||||
func (s *Store) GetThoughtByID(userID, thoughtID string) (*domain.Thought, error) {
|
||||
row := s.db.QueryRow(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, agent_mark
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, `+originColumns("")+`
|
||||
FROM thoughts WHERE id = ? AND (user_id = ? OR task_list_id IN (`+s.accessibleTaskListIDsQuery()+`))`,
|
||||
thoughtID, userID, userID, userID, userID,
|
||||
)
|
||||
@@ -160,14 +160,14 @@ func (s *Store) GetThoughts(userID, statusFilter string, limit int) ([]domain.Th
|
||||
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, agent_mark
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, `+originColumns("")+`
|
||||
FROM thoughts WHERE user_id = ? AND status = ?
|
||||
ORDER BY CASE priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END, sort_order ASC, created_at DESC LIMIT ?`,
|
||||
userID, statusFilter, limit,
|
||||
)
|
||||
} else {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, agent_mark
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, `+originColumns("")+`
|
||||
FROM thoughts WHERE user_id = ?
|
||||
ORDER BY sort_order ASC, created_at DESC LIMIT ?`,
|
||||
userID, limit,
|
||||
@@ -193,7 +193,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
var err error
|
||||
if taskListID != "" {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, agent_mark
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, `+originColumns("")+`
|
||||
FROM thoughts
|
||||
WHERE (user_id = ? OR task_list_id IN (`+s.accessibleTaskListIDsQuery()+`))
|
||||
AND task_list_id = ?
|
||||
@@ -203,7 +203,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
)
|
||||
} else {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, agent_mark
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks, sort_order, recurrence, `+originColumns("")+`
|
||||
FROM thoughts
|
||||
WHERE (user_id = ? OR task_list_id IN (`+s.accessibleTaskListIDsQuery()+`))
|
||||
AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))
|
||||
@@ -384,6 +384,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
item, err := s.CreateListItem(targetListID, thought.Content, "", "", 0, userID)
|
||||
if err == nil && item != nil {
|
||||
convertedRefID = item.ID
|
||||
s.copyOriginToListItem(thought.ID, item.ID)
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -512,7 +513,8 @@ func (s *Store) DeleteThought(userID, thoughtID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// migrateAgentMark: entries an AI agent created or changed via MCP and the user has not touched since
|
||||
// migrateAgentMark: released in v0.29.0; agent_mark is superseded by the origin columns (migrateOrigin)
|
||||
// and only read once to carry old marks over
|
||||
func (s *Store) migrateAgentMark() error {
|
||||
if err := s.addColumnIfMissing("thoughts", "agent_mark", "TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
return err
|
||||
@@ -520,51 +522,6 @@ func (s *Store) migrateAgentMark() error {
|
||||
return s.addColumnIfMissing("list_items", "agent_mark", "TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
|
||||
// Values of agent_mark on thoughts and list items
|
||||
const (
|
||||
AgentMarkCreated = "created"
|
||||
AgentMarkEdited = "edited"
|
||||
)
|
||||
|
||||
// nextAgentMark: an agent's change marks the entry (an entry it created stays "created"), a change by
|
||||
// the user removes the mark.
|
||||
func nextAgentMark(current string, byAgent bool) string {
|
||||
if !byAgent {
|
||||
return ""
|
||||
}
|
||||
if current == AgentMarkCreated {
|
||||
return AgentMarkCreated
|
||||
}
|
||||
return AgentMarkEdited
|
||||
}
|
||||
|
||||
// SetThoughtAgentMark records who changed a task last and returns the new mark.
|
||||
func (s *Store) SetThoughtAgentMark(thoughtID, current string, byAgent bool) string {
|
||||
mark := nextAgentMark(current, byAgent)
|
||||
if mark != current {
|
||||
_, _ = s.db.Exec(`UPDATE thoughts SET agent_mark = ? WHERE id = ?`, mark, thoughtID)
|
||||
}
|
||||
return mark
|
||||
}
|
||||
|
||||
// SetListItemAgentMark is SetThoughtAgentMark for list items; the current mark is read here because
|
||||
// the item queries after a change do not carry it.
|
||||
func (s *Store) SetListItemAgentMark(itemID string, byAgent bool) string {
|
||||
var current string
|
||||
_ = s.db.QueryRow(`SELECT agent_mark FROM list_items WHERE id = ?`, itemID).Scan(¤t)
|
||||
mark := nextAgentMark(current, byAgent)
|
||||
if mark != current {
|
||||
_, _ = s.db.Exec(`UPDATE list_items SET agent_mark = ? WHERE id = ?`, mark, itemID)
|
||||
}
|
||||
return mark
|
||||
}
|
||||
|
||||
// MarkListItemAgentCreated marks an item an agent just added.
|
||||
func (s *Store) MarkListItemAgentCreated(itemID string) string {
|
||||
_, _ = s.db.Exec(`UPDATE list_items SET agent_mark = ? WHERE id = ?`, AgentMarkCreated, itemID)
|
||||
return AgentMarkCreated
|
||||
}
|
||||
|
||||
func (s *Store) PurgeDiscardedThoughts(userID string) (int64, error) {
|
||||
res, err := s.db.Exec(`DELETE FROM thoughts WHERE user_id = ? AND status = 'discarded'`, userID)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user