nexo-brain 3.1.2 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/dashboard/app.py +131 -106
- package/src/db/__init__.py +3 -2
- package/src/db/_core.py +20 -1
- package/src/db/_reminders.py +451 -124
- package/src/db/_schema.py +30 -0
- package/src/server.py +120 -22
- package/src/tools_reminders.py +8 -5
- package/src/tools_reminders_crud.py +180 -15
package/src/db/_schema.py
CHANGED
|
@@ -674,6 +674,35 @@ def _m28_automation_runs(conn):
|
|
|
674
674
|
_migrate_add_index(conn, "idx_automation_runs_status", "automation_runs", "status")
|
|
675
675
|
|
|
676
676
|
|
|
677
|
+
def _m29_item_history_and_soft_delete(conn):
|
|
678
|
+
"""Persist reminder/followup history and read-before-mutate tokens."""
|
|
679
|
+
conn.execute("""
|
|
680
|
+
CREATE TABLE IF NOT EXISTS item_history (
|
|
681
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
682
|
+
item_type TEXT NOT NULL,
|
|
683
|
+
item_id TEXT NOT NULL,
|
|
684
|
+
event_type TEXT NOT NULL,
|
|
685
|
+
note TEXT DEFAULT '',
|
|
686
|
+
actor TEXT DEFAULT '',
|
|
687
|
+
metadata TEXT DEFAULT '{}',
|
|
688
|
+
created_at REAL NOT NULL
|
|
689
|
+
)
|
|
690
|
+
""")
|
|
691
|
+
conn.execute("""
|
|
692
|
+
CREATE TABLE IF NOT EXISTS item_read_tokens (
|
|
693
|
+
token TEXT PRIMARY KEY,
|
|
694
|
+
item_type TEXT NOT NULL,
|
|
695
|
+
item_id TEXT NOT NULL,
|
|
696
|
+
history_seq INTEGER DEFAULT 0,
|
|
697
|
+
issued_at REAL NOT NULL,
|
|
698
|
+
expires_at REAL NOT NULL
|
|
699
|
+
)
|
|
700
|
+
""")
|
|
701
|
+
_migrate_add_index(conn, "idx_item_history_lookup", "item_history", "item_type, item_id, created_at")
|
|
702
|
+
_migrate_add_index(conn, "idx_item_history_item", "item_history", "item_id")
|
|
703
|
+
_migrate_add_index(conn, "idx_item_read_tokens_lookup", "item_read_tokens", "item_type, item_id, expires_at")
|
|
704
|
+
|
|
705
|
+
|
|
677
706
|
MIGRATIONS = [
|
|
678
707
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
679
708
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -703,6 +732,7 @@ MIGRATIONS = [
|
|
|
703
732
|
(26, "protocol_answer_confidence", _m26_protocol_answer_confidence),
|
|
704
733
|
(27, "state_watchers", _m27_state_watchers),
|
|
705
734
|
(28, "automation_runs", _m28_automation_runs),
|
|
735
|
+
(29, "item_history_and_soft_delete", _m29_item_history_and_soft_delete),
|
|
706
736
|
]
|
|
707
737
|
|
|
708
738
|
|
package/src/server.py
CHANGED
|
@@ -24,10 +24,10 @@ from tools_coordination import (
|
|
|
24
24
|
from tools_reminders import handle_reminders
|
|
25
25
|
from tools_menu import handle_menu
|
|
26
26
|
from tools_reminders_crud import (
|
|
27
|
-
handle_reminder_create, handle_reminder_update,
|
|
28
|
-
handle_reminder_complete, handle_reminder_delete,
|
|
29
|
-
handle_followup_create, handle_followup_update,
|
|
30
|
-
handle_followup_complete, handle_followup_delete,
|
|
27
|
+
handle_reminder_create, handle_reminder_get, handle_reminder_update,
|
|
28
|
+
handle_reminder_complete, handle_reminder_note, handle_reminder_restore, handle_reminder_delete,
|
|
29
|
+
handle_followup_create, handle_followup_get, handle_followup_update,
|
|
30
|
+
handle_followup_complete, handle_followup_note, handle_followup_restore, handle_followup_delete,
|
|
31
31
|
)
|
|
32
32
|
from tools_learnings import (
|
|
33
33
|
handle_learning_add, handle_learning_search,
|
|
@@ -191,6 +191,8 @@ mcp = FastMCP(
|
|
|
191
191
|
"React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
|
|
192
192
|
"- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
|
|
193
193
|
"Reminders=user's, alert when due\n"
|
|
194
|
+
"- **Reminder/followup history:** before update/delete/restore/note, call the corresponding "
|
|
195
|
+
"`nexo_reminder_get` / `nexo_followup_get` first and use its `READ_TOKEN`.\n"
|
|
194
196
|
"- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
|
|
195
197
|
"- **Trust events:** When user expresses satisfaction/thanks (any language)→`nexo_cognitive_trust(event='explicit_thanks')`. "
|
|
196
198
|
"When user corrects you→`nexo_cognitive_trust(event='correction')`. "
|
|
@@ -488,7 +490,7 @@ def nexo_reminders(filter: str = "due") -> str:
|
|
|
488
490
|
"""Check reminders and followups.
|
|
489
491
|
|
|
490
492
|
Args:
|
|
491
|
-
filter: 'due'
|
|
493
|
+
filter: 'due', 'all', 'followups', 'completed', 'deleted', 'history', or 'any'
|
|
492
494
|
"""
|
|
493
495
|
return handle_reminders(filter)
|
|
494
496
|
|
|
@@ -503,7 +505,7 @@ def nexo_menu() -> str:
|
|
|
503
505
|
return handle_menu()
|
|
504
506
|
|
|
505
507
|
|
|
506
|
-
# ── Reminders CRUD (
|
|
508
|
+
# ── Reminders CRUD (7 tools) ──────────────────────────────────────
|
|
507
509
|
|
|
508
510
|
@mcp.tool
|
|
509
511
|
def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
|
|
@@ -519,17 +521,36 @@ def nexo_reminder_create(id: str, description: str, date: str = "", category: st
|
|
|
519
521
|
|
|
520
522
|
|
|
521
523
|
@mcp.tool
|
|
522
|
-
def
|
|
524
|
+
def nexo_reminder_get(id: str) -> str:
|
|
525
|
+
"""Read a reminder with its history and usage rules.
|
|
526
|
+
|
|
527
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
528
|
+
"""
|
|
529
|
+
return handle_reminder_get(id)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@mcp.tool
|
|
533
|
+
def nexo_reminder_update(
|
|
534
|
+
id: str,
|
|
535
|
+
description: str = "",
|
|
536
|
+
date: str = "",
|
|
537
|
+
status: str = "",
|
|
538
|
+
category: str = "",
|
|
539
|
+
read_token: str = "",
|
|
540
|
+
) -> str:
|
|
523
541
|
"""Update fields of an existing reminder. Only non-empty fields are changed.
|
|
524
542
|
|
|
543
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
544
|
+
|
|
525
545
|
Args:
|
|
526
546
|
id: Reminder ID (e.g., R87).
|
|
527
547
|
description: New description (optional).
|
|
528
548
|
date: New date YYYY-MM-DD (optional).
|
|
529
549
|
status: New status (optional).
|
|
530
550
|
category: New category (optional).
|
|
551
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
531
552
|
"""
|
|
532
|
-
return handle_reminder_update(id, description, date, status, category)
|
|
553
|
+
return handle_reminder_update(id, description, date, status, category, read_token)
|
|
533
554
|
|
|
534
555
|
|
|
535
556
|
@mcp.tool
|
|
@@ -543,16 +564,47 @@ def nexo_reminder_complete(id: str) -> str:
|
|
|
543
564
|
|
|
544
565
|
|
|
545
566
|
@mcp.tool
|
|
546
|
-
def
|
|
547
|
-
"""
|
|
567
|
+
def nexo_reminder_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
568
|
+
"""Append a note to reminder history.
|
|
569
|
+
|
|
570
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
id: Reminder ID (e.g., R87).
|
|
574
|
+
note: Operational note to append to history.
|
|
575
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
576
|
+
actor: Actor label for the history note.
|
|
577
|
+
"""
|
|
578
|
+
return handle_reminder_note(id, note, read_token, actor)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@mcp.tool
|
|
582
|
+
def nexo_reminder_restore(id: str, read_token: str = "") -> str:
|
|
583
|
+
"""Restore a soft-deleted reminder back to PENDING.
|
|
584
|
+
|
|
585
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
id: Reminder ID (e.g., R87).
|
|
589
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
590
|
+
"""
|
|
591
|
+
return handle_reminder_restore(id, read_token)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@mcp.tool
|
|
595
|
+
def nexo_reminder_delete(id: str, read_token: str = "") -> str:
|
|
596
|
+
"""Soft-delete a reminder.
|
|
597
|
+
|
|
598
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
548
599
|
|
|
549
600
|
Args:
|
|
550
601
|
id: Reminder ID (e.g., R87).
|
|
602
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
551
603
|
"""
|
|
552
|
-
return handle_reminder_delete(id)
|
|
604
|
+
return handle_reminder_delete(id, read_token)
|
|
553
605
|
|
|
554
606
|
|
|
555
|
-
# ── Followups CRUD (
|
|
607
|
+
# ── Followups CRUD (7 tools) ──────────────────────────────────────
|
|
556
608
|
|
|
557
609
|
@mcp.tool
|
|
558
610
|
def nexo_followup_create(id: str, description: str, date: str = "", verification: str = "", reasoning: str = "", recurrence: str = "", priority: str = "medium") -> str:
|
|
@@ -577,9 +629,28 @@ def nexo_followup_create(id: str, description: str, date: str = "", verification
|
|
|
577
629
|
|
|
578
630
|
|
|
579
631
|
@mcp.tool
|
|
580
|
-
def
|
|
632
|
+
def nexo_followup_get(id: str) -> str:
|
|
633
|
+
"""Read a followup with its history and usage rules.
|
|
634
|
+
|
|
635
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
636
|
+
"""
|
|
637
|
+
return handle_followup_get(id)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@mcp.tool
|
|
641
|
+
def nexo_followup_update(
|
|
642
|
+
id: str,
|
|
643
|
+
description: str = "",
|
|
644
|
+
date: str = "",
|
|
645
|
+
verification: str = "",
|
|
646
|
+
status: str = "",
|
|
647
|
+
priority: str = "",
|
|
648
|
+
read_token: str = "",
|
|
649
|
+
) -> str:
|
|
581
650
|
"""Update fields of an existing followup. Only non-empty fields are changed.
|
|
582
651
|
|
|
652
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
653
|
+
|
|
583
654
|
Args:
|
|
584
655
|
id: Followup ID (e.g., NF45).
|
|
585
656
|
description: New description (optional).
|
|
@@ -587,13 +658,9 @@ def nexo_followup_update(id: str, description: str = "", date: str = "", verific
|
|
|
587
658
|
verification: New verification text (optional).
|
|
588
659
|
status: New status (optional).
|
|
589
660
|
priority: critical, high, medium, low (optional).
|
|
661
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
590
662
|
"""
|
|
591
|
-
|
|
592
|
-
if priority in ('critical', 'high', 'medium', 'low'):
|
|
593
|
-
from db import get_db
|
|
594
|
-
get_db().execute("UPDATE followups SET priority = ? WHERE id = ?", (priority, id))
|
|
595
|
-
get_db().commit()
|
|
596
|
-
return result
|
|
663
|
+
return handle_followup_update(id, description, date, verification, status, priority, read_token)
|
|
597
664
|
|
|
598
665
|
|
|
599
666
|
@mcp.tool
|
|
@@ -608,13 +675,44 @@ def nexo_followup_complete(id: str, result: str = "") -> str:
|
|
|
608
675
|
|
|
609
676
|
|
|
610
677
|
@mcp.tool
|
|
611
|
-
def
|
|
612
|
-
"""
|
|
678
|
+
def nexo_followup_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
679
|
+
"""Append a note to followup history.
|
|
680
|
+
|
|
681
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
id: Followup ID (e.g., NF45).
|
|
685
|
+
note: Operational note to append to history.
|
|
686
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
687
|
+
actor: Actor label for the history note.
|
|
688
|
+
"""
|
|
689
|
+
return handle_followup_note(id, note, read_token, actor)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@mcp.tool
|
|
693
|
+
def nexo_followup_restore(id: str, read_token: str = "") -> str:
|
|
694
|
+
"""Restore a soft-deleted followup back to PENDING.
|
|
695
|
+
|
|
696
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
id: Followup ID (e.g., NF45).
|
|
700
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
701
|
+
"""
|
|
702
|
+
return handle_followup_restore(id, read_token)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@mcp.tool
|
|
706
|
+
def nexo_followup_delete(id: str, read_token: str = "") -> str:
|
|
707
|
+
"""Soft-delete a followup.
|
|
708
|
+
|
|
709
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
613
710
|
|
|
614
711
|
Args:
|
|
615
712
|
id: Followup ID (e.g., NF45).
|
|
713
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
616
714
|
"""
|
|
617
|
-
return handle_followup_delete(id)
|
|
715
|
+
return handle_followup_delete(id, read_token)
|
|
618
716
|
|
|
619
717
|
|
|
620
718
|
# ── Learnings CRUD (5 tools) ──────────────────────────────────────
|
package/src/tools_reminders.py
CHANGED
|
@@ -19,16 +19,16 @@ def handle_reminders(filter_type: str = "due") -> str:
|
|
|
19
19
|
"""Read reminders and followups from SQLite, return relevant ones.
|
|
20
20
|
|
|
21
21
|
Args:
|
|
22
|
-
filter_type: 'due'
|
|
22
|
+
filter_type: 'due', 'all', 'followups', 'completed', 'deleted', 'history', 'any'
|
|
23
23
|
"""
|
|
24
24
|
parts = []
|
|
25
25
|
|
|
26
|
-
if filter_type in ("due", "all"):
|
|
26
|
+
if filter_type in ("due", "all", "completed", "deleted", "history", "any"):
|
|
27
27
|
r = _format_reminders(filter_type)
|
|
28
28
|
if r:
|
|
29
29
|
parts.append(r)
|
|
30
30
|
|
|
31
|
-
if filter_type in ("due", "all", "followups"):
|
|
31
|
+
if filter_type in ("due", "all", "followups", "completed", "deleted", "history", "any"):
|
|
32
32
|
f = _format_followups(filter_type)
|
|
33
33
|
if f:
|
|
34
34
|
parts.append(f)
|
|
@@ -52,7 +52,8 @@ def _format_reminders(filter_type: str) -> str:
|
|
|
52
52
|
desc = desc.replace("**", "")
|
|
53
53
|
due_marker = " [DUE]" if _is_due(fecha) else ""
|
|
54
54
|
fecha_display = f"({fecha})" if fecha else "(—)"
|
|
55
|
-
|
|
55
|
+
status_tag = f" [{status}]" if status and status != "PENDING" else ""
|
|
56
|
+
lines.append(f" {rid} {fecha_display}{due_marker}{status_tag} — {desc[:120]}")
|
|
56
57
|
if "RECURRENTE" in status.upper():
|
|
57
58
|
lines.append(f" Status: {status}")
|
|
58
59
|
|
|
@@ -81,6 +82,8 @@ def _format_followups(filter_type: str) -> str:
|
|
|
81
82
|
pri = r.get("priority") or "medium"
|
|
82
83
|
pri_icon = {"critical": "🔴", "high": "🟠", "medium": "", "low": "⚪"}.get(pri, "")
|
|
83
84
|
pri_tag = f" {pri_icon}" if pri_icon else ""
|
|
84
|
-
|
|
85
|
+
status = r.get("status") or ""
|
|
86
|
+
status_tag = f" [{status}]" if status and status != "PENDING" else ""
|
|
87
|
+
lines.append(f" {nfid} {fecha_display}{due_marker}{pri_tag}{rec_tag}{status_tag} — {desc[:120]}")
|
|
85
88
|
|
|
86
89
|
return "\n".join(lines)
|
|
@@ -2,13 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
from db import (
|
|
4
4
|
create_reminder, update_reminder, complete_reminder, delete_reminder,
|
|
5
|
-
|
|
5
|
+
restore_reminder, add_reminder_note, get_reminder,
|
|
6
6
|
create_followup, update_followup, complete_followup, delete_followup,
|
|
7
|
-
|
|
7
|
+
restore_followup, add_followup_note, get_followup,
|
|
8
|
+
validate_item_read_token,
|
|
8
9
|
find_decisions_by_context_ref, update_decision_outcome,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def _require_item_read(item_type: str, item_id: str, read_token: str) -> str | None:
|
|
14
|
+
ok, message = validate_item_read_token(read_token, item_type, item_id)
|
|
15
|
+
if ok:
|
|
16
|
+
return None
|
|
17
|
+
prefix = "followup" if item_type == "followup" else "reminder"
|
|
18
|
+
return f"ERROR: {message} Use nexo_{prefix}_get(id='{item_id}') first."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _history_lines(history: list[dict]) -> list[str]:
|
|
22
|
+
if not history:
|
|
23
|
+
return ["- (no history)"]
|
|
24
|
+
lines: list[str] = []
|
|
25
|
+
for event in history:
|
|
26
|
+
created_at = event.get("created_at") or "?"
|
|
27
|
+
event_type = event.get("event_type") or "event"
|
|
28
|
+
actor = event.get("actor") or "system"
|
|
29
|
+
note = (event.get("note") or "").strip()
|
|
30
|
+
suffix = f" — {note}" if note else ""
|
|
31
|
+
lines.append(f"- {created_at} [{event_type}] ({actor}){suffix}")
|
|
32
|
+
return lines
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _format_reminder_payload(reminder: dict) -> str:
|
|
36
|
+
lines = [
|
|
37
|
+
f"REMINDER {reminder['id']}",
|
|
38
|
+
f"Description: {reminder.get('description') or ''}",
|
|
39
|
+
f"Date: {reminder.get('date') or '—'}",
|
|
40
|
+
f"Status: {reminder.get('status') or '—'}",
|
|
41
|
+
f"Category: {reminder.get('category') or 'general'}",
|
|
42
|
+
]
|
|
43
|
+
history_rules = reminder.get("history_rules") or []
|
|
44
|
+
if history_rules:
|
|
45
|
+
lines.append("Usage rules:")
|
|
46
|
+
lines.extend(f"- {rule}" for rule in history_rules)
|
|
47
|
+
lines.append("History:")
|
|
48
|
+
lines.extend(_history_lines(reminder.get("history") or []))
|
|
49
|
+
if reminder.get("read_token"):
|
|
50
|
+
lines.append(f"READ_TOKEN: {reminder['read_token']}")
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_followup_payload(followup: dict) -> str:
|
|
55
|
+
lines = [
|
|
56
|
+
f"FOLLOWUP {followup['id']}",
|
|
57
|
+
f"Description: {followup.get('description') or ''}",
|
|
58
|
+
f"Date: {followup.get('date') or '—'}",
|
|
59
|
+
f"Status: {followup.get('status') or '—'}",
|
|
60
|
+
f"Verification: {followup.get('verification') or '—'}",
|
|
61
|
+
f"Reasoning: {followup.get('reasoning') or '—'}",
|
|
62
|
+
f"Recurrence: {followup.get('recurrence') or '—'}",
|
|
63
|
+
f"Priority: {followup.get('priority') or 'medium'}",
|
|
64
|
+
]
|
|
65
|
+
history_rules = followup.get("history_rules") or []
|
|
66
|
+
if history_rules:
|
|
67
|
+
lines.append("Usage rules:")
|
|
68
|
+
lines.extend(f"- {rule}" for rule in history_rules)
|
|
69
|
+
lines.append("History:")
|
|
70
|
+
lines.extend(_history_lines(followup.get("history") or []))
|
|
71
|
+
if followup.get("read_token"):
|
|
72
|
+
lines.append(f"READ_TOKEN: {followup['read_token']}")
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
|
|
12
76
|
# ── Reminders ──────────────────────────────────────────────────────────────────
|
|
13
77
|
|
|
14
78
|
def handle_reminder_create(id: str, description: str, date: str = '', category: str = 'general') -> str:
|
|
@@ -25,8 +89,27 @@ def handle_reminder_create(id: str, description: str, date: str = '', category:
|
|
|
25
89
|
return f"Reminder created. Date: {date_str}. Category: {category}."
|
|
26
90
|
|
|
27
91
|
|
|
28
|
-
def
|
|
92
|
+
def handle_reminder_get(id: str) -> str:
|
|
93
|
+
"""Read a reminder with history and return a read token for safe mutations."""
|
|
94
|
+
result = get_reminder(id=id, include_history=True)
|
|
95
|
+
if not result:
|
|
96
|
+
return f"ERROR: Reminder {id} not found."
|
|
97
|
+
return _format_reminder_payload(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def handle_reminder_update(
|
|
101
|
+
id: str,
|
|
102
|
+
description: str = '',
|
|
103
|
+
date: str = '',
|
|
104
|
+
status: str = '',
|
|
105
|
+
category: str = '',
|
|
106
|
+
read_token: str = '',
|
|
107
|
+
) -> str:
|
|
29
108
|
"""Update one or more fields of an existing reminder."""
|
|
109
|
+
error = _require_item_read("reminder", id, read_token)
|
|
110
|
+
if error:
|
|
111
|
+
return error
|
|
112
|
+
|
|
30
113
|
fields: dict = {}
|
|
31
114
|
if description:
|
|
32
115
|
fields['description'] = description
|
|
@@ -41,8 +124,9 @@ def handle_reminder_update(id: str, description: str = '', date: str = '', statu
|
|
|
41
124
|
return f"ERROR: No fields specified to update for {id}."
|
|
42
125
|
|
|
43
126
|
result = update_reminder(id=id, **fields)
|
|
44
|
-
if not result:
|
|
45
|
-
|
|
127
|
+
if not result or "error" in result:
|
|
128
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
129
|
+
return f"ERROR: {error_msg}"
|
|
46
130
|
|
|
47
131
|
changed = ', '.join(fields.keys())
|
|
48
132
|
return f"Reminder {id} updated: {changed}."
|
|
@@ -57,13 +141,42 @@ def handle_reminder_complete(id: str) -> str:
|
|
|
57
141
|
return f"Reminder {id} marked COMPLETED."
|
|
58
142
|
|
|
59
143
|
|
|
60
|
-
def
|
|
61
|
-
"""
|
|
144
|
+
def handle_reminder_note(id: str, note: str, read_token: str = '', actor: str = 'nexo') -> str:
|
|
145
|
+
"""Append a note to reminder history."""
|
|
146
|
+
if not note.strip():
|
|
147
|
+
return "ERROR: note is required."
|
|
148
|
+
error = _require_item_read("reminder", id, read_token)
|
|
149
|
+
if error:
|
|
150
|
+
return error
|
|
151
|
+
result = add_reminder_note(id=id, note=note.strip(), actor=actor or "nexo")
|
|
152
|
+
if not result or "error" in result:
|
|
153
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
154
|
+
return f"ERROR: {error_msg}"
|
|
155
|
+
return f"Reminder {id} note added."
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def handle_reminder_restore(id: str, read_token: str = '') -> str:
|
|
159
|
+
"""Restore a soft-deleted reminder."""
|
|
160
|
+
error = _require_item_read("reminder", id, read_token)
|
|
161
|
+
if error:
|
|
162
|
+
return error
|
|
163
|
+
result = restore_reminder(id=id)
|
|
164
|
+
if not result or "error" in result:
|
|
165
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
166
|
+
return f"ERROR: {error_msg}"
|
|
167
|
+
return f"Reminder {id} restored to PENDING."
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def handle_reminder_delete(id: str, read_token: str = '') -> str:
|
|
171
|
+
"""Soft-delete a reminder."""
|
|
172
|
+
error = _require_item_read("reminder", id, read_token)
|
|
173
|
+
if error:
|
|
174
|
+
return error
|
|
62
175
|
result = delete_reminder(id=id)
|
|
63
176
|
if not result:
|
|
64
177
|
return f"ERROR: Reminder {id} not found."
|
|
65
178
|
|
|
66
|
-
return f"Reminder {id} deleted."
|
|
179
|
+
return f"Reminder {id} soft-deleted."
|
|
67
180
|
|
|
68
181
|
|
|
69
182
|
# ── Followups ──────────────────────────────────────────────────────────────────
|
|
@@ -95,8 +208,28 @@ def handle_followup_create(id: str, description: str, date: str = '', verificati
|
|
|
95
208
|
return f"Followup created. Date: {date_str}.{rec_str}{warn_str}"
|
|
96
209
|
|
|
97
210
|
|
|
98
|
-
def
|
|
211
|
+
def handle_followup_get(id: str) -> str:
|
|
212
|
+
"""Read a followup with history and return a read token for safe mutations."""
|
|
213
|
+
result = get_followup(id=id, include_history=True)
|
|
214
|
+
if not result:
|
|
215
|
+
return f"ERROR: Followup {id} not found."
|
|
216
|
+
return _format_followup_payload(result)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def handle_followup_update(
|
|
220
|
+
id: str,
|
|
221
|
+
description: str = '',
|
|
222
|
+
date: str = '',
|
|
223
|
+
verification: str = '',
|
|
224
|
+
status: str = '',
|
|
225
|
+
priority: str = '',
|
|
226
|
+
read_token: str = '',
|
|
227
|
+
) -> str:
|
|
99
228
|
"""Update one or more fields of an existing followup."""
|
|
229
|
+
error = _require_item_read("followup", id, read_token)
|
|
230
|
+
if error:
|
|
231
|
+
return error
|
|
232
|
+
|
|
100
233
|
fields: dict = {}
|
|
101
234
|
if description:
|
|
102
235
|
fields['description'] = description
|
|
@@ -106,16 +239,19 @@ def handle_followup_update(id: str, description: str = '', date: str = '', verif
|
|
|
106
239
|
fields['verification'] = verification
|
|
107
240
|
if status:
|
|
108
241
|
fields['status'] = status
|
|
242
|
+
if priority:
|
|
243
|
+
fields['priority'] = priority
|
|
109
244
|
|
|
110
245
|
if not fields:
|
|
111
246
|
return f"ERROR: No fields specified to update for {id}."
|
|
112
247
|
|
|
113
248
|
result = update_followup(id=id, **fields)
|
|
114
|
-
if not result:
|
|
115
|
-
|
|
249
|
+
if not result or "error" in result:
|
|
250
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
251
|
+
return f"ERROR: {error_msg}"
|
|
116
252
|
|
|
117
253
|
changed = ', '.join(fields.keys())
|
|
118
|
-
return f"Followup updated: {changed}."
|
|
254
|
+
return f"Followup {id} updated: {changed}."
|
|
119
255
|
|
|
120
256
|
|
|
121
257
|
def handle_followup_complete(id: str, result: str = '') -> str:
|
|
@@ -157,10 +293,39 @@ def handle_followup_complete(id: str, result: str = '') -> str:
|
|
|
157
293
|
return msg
|
|
158
294
|
|
|
159
295
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
296
|
+
def handle_followup_note(id: str, note: str, read_token: str = '', actor: str = 'nexo') -> str:
|
|
297
|
+
"""Append a note to followup history."""
|
|
298
|
+
if not note.strip():
|
|
299
|
+
return "ERROR: note is required."
|
|
300
|
+
error = _require_item_read("followup", id, read_token)
|
|
301
|
+
if error:
|
|
302
|
+
return error
|
|
303
|
+
result = add_followup_note(id=id, note=note.strip(), actor=actor or "nexo")
|
|
304
|
+
if not result or "error" in result:
|
|
305
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
306
|
+
return f"ERROR: {error_msg}"
|
|
307
|
+
return f"Followup {id} note added."
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def handle_followup_restore(id: str, read_token: str = '') -> str:
|
|
311
|
+
"""Restore a soft-deleted followup."""
|
|
312
|
+
error = _require_item_read("followup", id, read_token)
|
|
313
|
+
if error:
|
|
314
|
+
return error
|
|
315
|
+
result = restore_followup(id=id)
|
|
316
|
+
if not result or "error" in result:
|
|
317
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
318
|
+
return f"ERROR: {error_msg}"
|
|
319
|
+
return f"Followup {id} restored to PENDING."
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def handle_followup_delete(id: str, read_token: str = '') -> str:
|
|
323
|
+
"""Soft-delete a followup."""
|
|
324
|
+
error = _require_item_read("followup", id, read_token)
|
|
325
|
+
if error:
|
|
326
|
+
return error
|
|
162
327
|
result = delete_followup(id=id)
|
|
163
328
|
if not result:
|
|
164
329
|
return f"ERROR: Followup {id} not found."
|
|
165
330
|
|
|
166
|
-
return f"Followup deleted."
|
|
331
|
+
return f"Followup {id} soft-deleted."
|