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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -745,18 +745,21 @@ async def api_reminders_list(
745
745
  ):
746
746
  """List reminders."""
747
747
  db = _db()
748
- conn = db.get_db()
749
- query = "SELECT * FROM reminders WHERE 1=1"
750
- params = []
748
+ reminders = db.get_reminders("any")
751
749
  if status:
752
- query += " AND status = ?"
753
- params.append(status)
750
+ if status in {"any", "all", "history"}:
751
+ pass
752
+ elif status == "completed":
753
+ reminders = [r for r in reminders if str(r.get("status") or "").startswith("COMPLETED")]
754
+ elif status == "deleted":
755
+ reminders = [r for r in reminders if r.get("status") == "DELETED"]
756
+ else:
757
+ reminders = [r for r in reminders if r.get("status") == status]
758
+ else:
759
+ reminders = [r for r in reminders if r.get("status") != "DELETED"]
754
760
  if category:
755
- query += " AND category = ?"
756
- params.append(category)
757
- query += " ORDER BY created_at DESC"
758
- rows = conn.execute(query, params).fetchall()
759
- reminders = [dict(r) for r in rows]
761
+ reminders = [r for r in reminders if r.get("category") == category]
762
+ reminders = sorted(reminders, key=lambda item: item.get("updated_at") or item.get("created_at") or 0, reverse=True)
760
763
  return {"count": len(reminders), "reminders": reminders}
761
764
 
762
765
 
@@ -766,59 +769,60 @@ async def api_reminders_create(body: ReminderCreate):
766
769
  db = _db()
767
770
  conn = db.get_db()
768
771
  rid = _next_reminder_id(conn)
769
- now = time.time()
770
- conn.execute(
771
- "INSERT INTO reminders (id, description, date, status, category, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
772
- (rid, body.description, body.date, "PENDING", body.category or "general", now, now),
772
+ result = db.create_reminder(
773
+ rid,
774
+ body.description,
775
+ date=body.date,
776
+ category=body.category or "general",
773
777
  )
774
- conn.commit()
775
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
776
- return {"success": True, "reminder": dict(row)}
778
+ if not result or "error" in result:
779
+ return JSONResponse({"error": result.get("error", "Failed to create reminder")}, status_code=400)
780
+ return {"success": True, "reminder": result}
781
+
782
+
783
+ @app.get("/api/reminders/{rid}")
784
+ async def api_reminders_get(rid: str):
785
+ """Get a reminder with history."""
786
+ db = _db()
787
+ row = db.get_reminder(rid, include_history=True)
788
+ if not row:
789
+ return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
790
+ return {"success": True, "reminder": row}
777
791
 
778
792
 
779
793
  @app.put("/api/reminders/{rid}")
780
794
  async def api_reminders_update(rid: str, body: ReminderUpdate):
781
795
  """Update a reminder."""
782
796
  db = _db()
783
- conn = db.get_db()
784
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
797
+ row = db.get_reminder(rid)
785
798
  if not row:
786
799
  return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
787
- fields = []
788
- params = []
800
+ fields = {}
789
801
  if body.description is not None:
790
- fields.append("description = ?")
791
- params.append(body.description)
802
+ fields["description"] = body.description
792
803
  if body.date is not None:
793
- fields.append("date = ?")
794
- params.append(body.date)
804
+ fields["date"] = body.date
795
805
  if body.status is not None:
796
- fields.append("status = ?")
797
- params.append(body.status)
806
+ fields["status"] = body.status
798
807
  if body.category is not None:
799
- fields.append("category = ?")
800
- params.append(body.category)
808
+ fields["category"] = body.category
801
809
  if not fields:
802
- return {"success": True, "reminder": dict(row)}
803
- fields.append("updated_at = ?")
804
- params.append(time.time())
805
- params.append(rid)
806
- conn.execute(f"UPDATE reminders SET {', '.join(fields)} WHERE id = ?", params)
807
- conn.commit()
808
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
809
- return {"success": True, "reminder": dict(row)}
810
+ return {"success": True, "reminder": row}
811
+ result = db.update_reminder(rid, history_actor="dashboard", **fields)
812
+ if not result or "error" in result:
813
+ return JSONResponse({"error": result.get("error", f"Reminder {rid} not found")}, status_code=400)
814
+ return {"success": True, "reminder": result}
810
815
 
811
816
 
812
817
  @app.delete("/api/reminders/{rid}")
813
818
  async def api_reminders_delete(rid: str):
814
- """Delete a reminder."""
819
+ """Soft-delete a reminder."""
815
820
  db = _db()
816
- conn = db.get_db()
817
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
821
+ row = db.get_reminder(rid)
818
822
  if not row:
819
823
  return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
820
- conn.execute("DELETE FROM reminders WHERE id = ?", (rid,))
821
- conn.commit()
824
+ db.add_reminder_note(rid, "Soft-deleted from dashboard.", actor="dashboard")
825
+ db.delete_reminder(rid)
822
826
  return {"success": True, "deleted_id": rid}
823
827
 
824
828
 
@@ -847,15 +851,19 @@ async def api_followups_list(
847
851
  ):
848
852
  """List followups."""
849
853
  db = _db()
850
- conn = db.get_db()
851
- query = "SELECT * FROM followups WHERE 1=1"
852
- params = []
854
+ followups = db.get_followups("any")
853
855
  if status:
854
- query += " AND status = ?"
855
- params.append(status)
856
- query += " ORDER BY created_at DESC"
857
- rows = conn.execute(query, params).fetchall()
858
- followups = [dict(r) for r in rows]
856
+ if status in {"any", "all", "history"}:
857
+ pass
858
+ elif status == "completed":
859
+ followups = [r for r in followups if str(r.get("status") or "").startswith("COMPLETED")]
860
+ elif status == "deleted":
861
+ followups = [r for r in followups if r.get("status") == "DELETED"]
862
+ else:
863
+ followups = [r for r in followups if r.get("status") == status]
864
+ else:
865
+ followups = [r for r in followups if r.get("status") != "DELETED"]
866
+ followups = sorted(followups, key=lambda item: item.get("updated_at") or item.get("created_at") or 0, reverse=True)
859
867
  return {"count": len(followups), "followups": followups}
860
868
 
861
869
 
@@ -865,62 +873,63 @@ async def api_followups_create(body: FollowupCreate):
865
873
  db = _db()
866
874
  conn = db.get_db()
867
875
  fid = _next_followup_id(conn)
868
- now = time.time()
869
- conn.execute(
870
- "INSERT INTO followups (id, description, date, verification, status, reasoning, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
871
- (fid, body.description, body.date, body.verification, "PENDING", body.reasoning, now, now),
876
+ result = db.create_followup(
877
+ fid,
878
+ body.description,
879
+ date=body.date,
880
+ verification=body.verification or "",
881
+ reasoning=body.reasoning or "",
872
882
  )
873
- conn.commit()
874
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
875
- return {"success": True, "followup": dict(row)}
883
+ if not result or "error" in result:
884
+ return JSONResponse({"error": result.get("error", "Failed to create followup")}, status_code=400)
885
+ return {"success": True, "followup": result}
886
+
887
+
888
+ @app.get("/api/followups/{fid}")
889
+ async def api_followups_get(fid: str):
890
+ """Get a followup with history."""
891
+ db = _db()
892
+ row = db.get_followup(fid, include_history=True)
893
+ if not row:
894
+ return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
895
+ return {"success": True, "followup": row}
876
896
 
877
897
 
878
898
  @app.put("/api/followups/{fid}")
879
899
  async def api_followups_update(fid: str, body: FollowupUpdate):
880
900
  """Update a followup."""
881
901
  db = _db()
882
- conn = db.get_db()
883
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
902
+ row = db.get_followup(fid)
884
903
  if not row:
885
904
  return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
886
- fields = []
887
- params = []
905
+ fields = {}
888
906
  if body.description is not None:
889
- fields.append("description = ?")
890
- params.append(body.description)
907
+ fields["description"] = body.description
891
908
  if body.date is not None:
892
- fields.append("date = ?")
893
- params.append(body.date)
909
+ fields["date"] = body.date
894
910
  if body.status is not None:
895
- fields.append("status = ?")
896
- params.append(body.status)
911
+ fields["status"] = body.status
897
912
  if body.verification is not None:
898
- fields.append("verification = ?")
899
- params.append(body.verification)
913
+ fields["verification"] = body.verification
900
914
  if body.reasoning is not None:
901
- fields.append("reasoning = ?")
902
- params.append(body.reasoning)
915
+ fields["reasoning"] = body.reasoning
903
916
  if not fields:
904
- return {"success": True, "followup": dict(row)}
905
- fields.append("updated_at = ?")
906
- params.append(time.time())
907
- params.append(fid)
908
- conn.execute(f"UPDATE followups SET {', '.join(fields)} WHERE id = ?", params)
909
- conn.commit()
910
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
911
- return {"success": True, "followup": dict(row)}
917
+ return {"success": True, "followup": row}
918
+ result = db.update_followup(fid, history_actor="dashboard", **fields)
919
+ if not result or "error" in result:
920
+ return JSONResponse({"error": result.get("error", f"Followup {fid} not found")}, status_code=400)
921
+ return {"success": True, "followup": result}
912
922
 
913
923
 
914
924
  @app.delete("/api/followups/{fid}")
915
925
  async def api_followups_delete(fid: str):
916
- """Delete a followup."""
926
+ """Soft-delete a followup."""
917
927
  db = _db()
918
- conn = db.get_db()
919
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
928
+ row = db.get_followup(fid)
920
929
  if not row:
921
930
  return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
922
- conn.execute("DELETE FROM followups WHERE id = ?", (fid,))
923
- conn.commit()
931
+ db.add_followup_note(fid, "Soft-deleted from dashboard.", actor="dashboard")
932
+ db.delete_followup(fid)
924
933
  return {"success": True, "deleted_id": fid}
925
934
 
926
935
 
@@ -933,36 +942,49 @@ async def api_ops_move(body: MoveRequest):
933
942
  """Move an item between reminders and followups."""
934
943
  db = _db()
935
944
  conn = db.get_db()
936
- now = time.time()
937
945
 
938
946
  if body.direction == "to_followup":
939
- # Read from reminders
940
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (body.id,)).fetchone()
941
- if not row:
947
+ item = db.get_reminder(body.id)
948
+ if not item:
942
949
  return JSONResponse({"error": f"Reminder {body.id} not found"}, status_code=404)
943
- item = dict(row)
944
950
  fid = _next_followup_id(conn)
945
- conn.execute(
946
- "INSERT INTO followups (id, description, date, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
947
- (fid, item["description"], item.get("date"), "PENDING", now, now),
951
+ created = db.create_followup(
952
+ fid,
953
+ item["description"],
954
+ date=item.get("date"),
955
+ reasoning=f"Moved from reminder {body.id} via dashboard.",
948
956
  )
949
- conn.execute("DELETE FROM reminders WHERE id = ?", (body.id,))
950
- conn.commit()
957
+ if not created or "error" in created:
958
+ return JSONResponse({"error": created.get("error", "Failed to create followup")}, status_code=400)
959
+ db.add_followup_note(fid, f"Created from reminder {body.id} via dashboard move.", actor="dashboard")
960
+ db.add_reminder_note(body.id, f"Moved to followup {fid} via dashboard.", actor="dashboard")
961
+ db.delete_reminder(body.id)
951
962
  return {"success": True, "new_id": fid, "direction": "to_followup"}
952
963
 
953
964
  elif body.direction == "to_reminder":
954
- # Read from followups
955
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (body.id,)).fetchone()
956
- if not row:
965
+ item = db.get_followup(body.id)
966
+ if not item:
957
967
  return JSONResponse({"error": f"Followup {body.id} not found"}, status_code=404)
958
- item = dict(row)
959
968
  rid = _next_reminder_id(conn)
960
- conn.execute(
961
- "INSERT INTO reminders (id, description, date, status, category, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
962
- (rid, item["description"], item.get("date"), "PENDING", "general", now, now),
969
+ created = db.create_reminder(
970
+ rid,
971
+ item["description"],
972
+ date=item.get("date"),
973
+ category="general",
963
974
  )
964
- conn.execute("DELETE FROM followups WHERE id = ?", (body.id,))
965
- conn.commit()
975
+ if not created or "error" in created:
976
+ return JSONResponse({"error": created.get("error", "Failed to create reminder")}, status_code=400)
977
+ migration_note = f"Created from followup {body.id} via dashboard move."
978
+ extra = []
979
+ if item.get("verification"):
980
+ extra.append(f"Previous verification: {item['verification']}")
981
+ if item.get("reasoning"):
982
+ extra.append(f"Previous reasoning: {item['reasoning']}")
983
+ if extra:
984
+ migration_note += " " + " ".join(extra)
985
+ db.add_reminder_note(rid, migration_note, actor="dashboard")
986
+ db.add_followup_note(body.id, f"Moved to reminder {rid} via dashboard.", actor="dashboard")
987
+ db.delete_followup(body.id)
966
988
  return {"success": True, "new_id": rid, "direction": "to_reminder"}
967
989
 
968
990
  else:
@@ -977,7 +999,10 @@ async def api_ops_execute(fid: str):
977
999
  """Execute a followup by opening Terminal with the configured NEXO client."""
978
1000
  db = _db()
979
1001
  conn = db.get_db()
980
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
1002
+ row = conn.execute(
1003
+ "SELECT * FROM followups WHERE id = ? AND status != 'DELETED'",
1004
+ (fid,),
1005
+ ).fetchone()
981
1006
  if not row:
982
1007
  return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
983
1008
  item = dict(row)
@@ -1092,12 +1117,12 @@ async def api_calendar(
1092
1117
  month_prefix = f"{year}-{month:02d}%"
1093
1118
 
1094
1119
  reminder_rows = conn.execute(
1095
- "SELECT *, 'reminder' as item_type FROM reminders WHERE date LIKE ? ORDER BY date ASC",
1120
+ "SELECT *, 'reminder' as item_type FROM reminders WHERE date LIKE ? AND status != 'DELETED' ORDER BY date ASC",
1096
1121
  (month_prefix,),
1097
1122
  ).fetchall()
1098
1123
 
1099
1124
  followup_rows = conn.execute(
1100
- "SELECT *, 'followup' as item_type FROM followups WHERE date LIKE ? ORDER BY date ASC",
1125
+ "SELECT *, 'followup' as item_type FROM followups WHERE date LIKE ? AND status != 'DELETED' ORDER BY date ASC",
1101
1126
  (month_prefix,),
1102
1127
  ).fetchall()
1103
1128
 
@@ -36,10 +36,11 @@ from db._sessions import (
36
36
  # Reminders and followups
37
37
  from db._reminders import (
38
38
  create_reminder, update_reminder, complete_reminder, delete_reminder,
39
- get_reminders, get_reminder,
39
+ restore_reminder, add_reminder_note, get_reminders, get_reminder, get_reminder_history,
40
40
  create_followup, update_followup, complete_followup, delete_followup,
41
- get_followups, get_followup,
41
+ restore_followup, add_followup_note, get_followups, get_followup, get_followup_history,
42
42
  find_similar_followups,
43
+ add_item_history, get_item_history, validate_item_read_token,
43
44
  )
44
45
 
45
46
  # Learnings
package/src/db/_core.py CHANGED
@@ -190,6 +190,26 @@ def init_db():
190
190
  updated_at REAL NOT NULL
191
191
  );
192
192
 
193
+ CREATE TABLE IF NOT EXISTS item_history (
194
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
195
+ item_type TEXT NOT NULL,
196
+ item_id TEXT NOT NULL,
197
+ event_type TEXT NOT NULL,
198
+ note TEXT DEFAULT '',
199
+ actor TEXT DEFAULT '',
200
+ metadata TEXT DEFAULT '{}',
201
+ created_at REAL NOT NULL
202
+ );
203
+
204
+ CREATE TABLE IF NOT EXISTS item_read_tokens (
205
+ token TEXT PRIMARY KEY,
206
+ item_type TEXT NOT NULL,
207
+ item_id TEXT NOT NULL,
208
+ history_seq INTEGER DEFAULT 0,
209
+ issued_at REAL NOT NULL,
210
+ expires_at REAL NOT NULL
211
+ );
212
+
193
213
  CREATE TABLE IF NOT EXISTS learnings (
194
214
  id INTEGER PRIMARY KEY AUTOINCREMENT,
195
215
  category TEXT NOT NULL,
@@ -415,4 +435,3 @@ def _multi_word_like(query: str, columns: list[str]) -> tuple[str, list]:
415
435
  word_conditions.append(f"({col_or})")
416
436
  params.extend([pattern] * len(columns))
417
437
  return " AND ".join(word_conditions), params
418
-