nexo-brain 2.0.0 → 2.1.0

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.
Files changed (238) hide show
  1. package/README.md +140 -41
  2. package/package.json +15 -3
  3. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  4. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  5. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  6. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  7. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  8. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  9. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  10. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  11. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  12. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  17. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  18. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  19. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  21. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  22. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  23. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  38. package/src/crons/manifest.json +106 -0
  39. package/src/crons/sync.py +217 -0
  40. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  41. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  42. package/src/dashboard/app.py +16 -2
  43. package/src/dashboard/templates/dashboard.html +3 -2
  44. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  45. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  48. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  49. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  50. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  51. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  52. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  53. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  54. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  55. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  56. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  80. package/src/db/_episodic.py +1 -1
  81. package/src/db/_reminders.py +9 -5
  82. package/src/hooks/session-stop.sh +2 -1
  83. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  85. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  86. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  87. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  88. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  89. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  90. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  91. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  92. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  93. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  94. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  95. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  96. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  97. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  98. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  99. package/src/plugins/core_rules.py +34 -17
  100. package/src/plugins/update.py +18 -0
  101. package/src/scripts/check-context.py +4 -7
  102. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  103. package/src/scripts/deep-sleep/apply_findings.py +512 -167
  104. package/src/scripts/deep-sleep/collect.py +480 -0
  105. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  106. package/src/scripts/deep-sleep/extract.py +249 -0
  107. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  108. package/src/scripts/deep-sleep/synthesize.py +191 -0
  109. package/src/scripts/nexo-catchup.py +5 -8
  110. package/src/scripts/nexo-daily-self-audit.py +28 -19
  111. package/src/scripts/nexo-deep-sleep.sh +31 -16
  112. package/src/scripts/nexo-evolution-run.py +5 -20
  113. package/src/scripts/nexo-followup-hygiene.py +4 -2
  114. package/src/scripts/nexo-github-monitor.py +6 -9
  115. package/src/scripts/nexo-immune.py +4 -17
  116. package/src/scripts/nexo-learning-validator.py +0 -29
  117. package/src/scripts/nexo-postmortem-consolidator.py +9 -20
  118. package/src/scripts/nexo-proactive-dashboard.py +1 -0
  119. package/src/scripts/nexo-sleep.py +8 -18
  120. package/src/scripts/nexo-synthesis.py +8 -19
  121. package/src/tools_menu.py +1 -1
  122. package/src/tools_sessions.py +67 -0
  123. package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
  124. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  125. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  126. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  127. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  128. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  129. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  130. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  131. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  132. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  133. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  134. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  135. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  136. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  137. package/src/__pycache__/server.cpython-310.pyc +0 -0
  138. package/src/__pycache__/server.cpython-314.pyc +0 -0
  139. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  140. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  141. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  142. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  143. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  144. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  145. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  146. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  147. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  148. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  149. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  151. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  152. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  155. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  156. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  158. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  159. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  160. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  161. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  162. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  163. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  164. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  165. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  166. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  167. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  169. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  170. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  171. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  172. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  173. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  174. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  175. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  176. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  177. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  178. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  179. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  180. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  181. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  182. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  183. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  184. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  185. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  186. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  187. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  188. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  189. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  190. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  191. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  192. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  193. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  194. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  195. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  196. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  197. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  198. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  199. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  200. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  201. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  202. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  203. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  204. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  205. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  206. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  207. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  208. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  216. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  217. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  218. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  219. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  220. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  221. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  222. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  223. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  224. package/src/scripts/deep-sleep/prompt.md +0 -109
  225. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  226. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  228. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  229. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  230. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  231. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  232. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  233. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  234. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  235. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  236. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  237. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  238. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Cron Sync — Synchronize crons/manifest.json with system LaunchAgents (macOS).
4
+
5
+ Called by nexo_update after pulling new code. Ensures:
6
+ - New crons in manifest → installed
7
+ - Removed crons from manifest → unloaded + deleted
8
+ - Changed schedule/interval → plist updated + reloaded
9
+ - Personal (non-core) crons → left untouched
10
+
11
+ Usage:
12
+ python3 crons/sync.py [--dry-run]
13
+
14
+ Environment:
15
+ NEXO_HOME — root of NEXO installation
16
+ NEXO_CODE — path to NEXO source (defaults to script parent's parent)
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import platform
22
+ import plistlib
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
28
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
29
+ MANIFEST = Path(__file__).resolve().parent / "manifest.json"
30
+ LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
31
+ LABEL_PREFIX = "com.nexo."
32
+ LOG_DIR = NEXO_HOME / "logs"
33
+
34
+
35
+ def log(msg: str):
36
+ print(f"[cron-sync] {msg}", flush=True)
37
+
38
+
39
+ def load_manifest() -> list[dict]:
40
+ with open(MANIFEST) as f:
41
+ data = json.load(f)
42
+ return data.get("crons", [])
43
+
44
+
45
+ def build_plist(cron: dict) -> dict:
46
+ """Build a macOS LaunchAgent plist dict from a manifest entry."""
47
+ cron_id = cron["id"]
48
+ label = f"{LABEL_PREFIX}{cron_id}"
49
+ script_path = str(NEXO_CODE / cron["script"])
50
+ script_type = cron.get("type", "python")
51
+
52
+ if script_type == "shell":
53
+ program_args = ["/bin/bash", script_path]
54
+ else:
55
+ # Find python3
56
+ python_candidates = [
57
+ "/opt/homebrew/bin/python3",
58
+ "/usr/local/bin/python3",
59
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
60
+ "/usr/bin/python3",
61
+ ]
62
+ python_bin = "python3"
63
+ for p in python_candidates:
64
+ if Path(p).exists():
65
+ python_bin = p
66
+ break
67
+ program_args = [python_bin, script_path]
68
+
69
+ plist = {
70
+ "Label": label,
71
+ "ProgramArguments": program_args,
72
+ "StandardOutPath": str(LOG_DIR / f"{cron_id}-stdout.log"),
73
+ "StandardErrorPath": str(LOG_DIR / f"{cron_id}-stderr.log"),
74
+ "EnvironmentVariables": {
75
+ "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:"
76
+ + str(Path.home() / ".local" / "bin") + ":"
77
+ + str(Path.home() / ".nvm/versions/node/v22.14.0/bin") + ":"
78
+ + "/Library/Frameworks/Python.framework/Versions/3.12/bin",
79
+ "HOME": str(Path.home()),
80
+ "NEXO_HOME": str(NEXO_HOME),
81
+ "NEXO_CODE": str(NEXO_CODE),
82
+ "PYTHONUNBUFFERED": "1",
83
+ },
84
+ }
85
+
86
+ # Schedule
87
+ if "interval_seconds" in cron:
88
+ plist["StartInterval"] = cron["interval_seconds"]
89
+ elif "schedule" in cron:
90
+ cal = {}
91
+ s = cron["schedule"]
92
+ if "hour" in s:
93
+ cal["Hour"] = s["hour"]
94
+ if "minute" in s:
95
+ cal["Minute"] = s["minute"]
96
+ if "weekday" in s:
97
+ cal["Weekday"] = s["weekday"]
98
+ plist["StartCalendarInterval"] = cal
99
+
100
+ return plist
101
+
102
+
103
+ def get_installed_nexo_crons() -> dict[str, Path]:
104
+ """Return dict of cron_id → plist_path for installed NEXO crons."""
105
+ installed = {}
106
+ if not LAUNCH_AGENTS_DIR.exists():
107
+ return installed
108
+ for f in LAUNCH_AGENTS_DIR.glob(f"{LABEL_PREFIX}*.plist"):
109
+ cron_id = f.stem.replace(LABEL_PREFIX, "")
110
+ installed[cron_id] = f
111
+ return installed
112
+
113
+
114
+ def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
115
+ """Check if the installed plist differs from what we'd generate."""
116
+ try:
117
+ with open(existing_path, "rb") as f:
118
+ existing = plistlib.load(f)
119
+ except Exception:
120
+ return True
121
+
122
+ # Compare key fields
123
+ if existing.get("ProgramArguments") != new_plist.get("ProgramArguments"):
124
+ return True
125
+ if existing.get("StartInterval") != new_plist.get("StartInterval"):
126
+ return True
127
+ if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
128
+ return True
129
+ return False
130
+
131
+
132
+ def install_plist(label: str, plist: dict, plist_path: Path, dry_run: bool):
133
+ """Write plist and load it."""
134
+ if dry_run:
135
+ log(f" DRY-RUN: would install {plist_path.name}")
136
+ return
137
+
138
+ # Unload if already loaded
139
+ subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
140
+
141
+ with open(plist_path, "wb") as f:
142
+ plistlib.dump(plist, f)
143
+
144
+ subprocess.run(["launchctl", "load", str(plist_path)], capture_output=True)
145
+ log(f" Installed + loaded: {plist_path.name}")
146
+
147
+
148
+ def unload_plist(plist_path: Path, dry_run: bool):
149
+ """Unload and remove a plist."""
150
+ if dry_run:
151
+ log(f" DRY-RUN: would remove {plist_path.name}")
152
+ return
153
+
154
+ subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
155
+ plist_path.unlink(missing_ok=True)
156
+ log(f" Removed: {plist_path.name}")
157
+
158
+
159
+ def sync(dry_run: bool = False):
160
+ if platform.system() != "Darwin":
161
+ log("Not macOS — cron sync only supports LaunchAgents. Skipping.")
162
+ return
163
+
164
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
165
+ LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
166
+
167
+ manifest_crons = load_manifest()
168
+ manifest_ids = {c["id"] for c in manifest_crons}
169
+ installed = get_installed_nexo_crons()
170
+
171
+ log(f"Manifest: {len(manifest_crons)} core crons")
172
+ log(f"Installed: {len(installed)} NEXO crons")
173
+
174
+ # 1. Install or update crons from manifest
175
+ for cron in manifest_crons:
176
+ cron_id = cron["id"]
177
+ label = f"{LABEL_PREFIX}{cron_id}"
178
+ plist_path = LAUNCH_AGENTS_DIR / f"{label}.plist"
179
+ new_plist = build_plist(cron)
180
+
181
+ if cron_id not in installed:
182
+ log(f" NEW: {cron_id}")
183
+ install_plist(label, new_plist, plist_path, dry_run)
184
+ elif plist_needs_update(installed[cron_id], new_plist):
185
+ log(f" UPDATE: {cron_id}")
186
+ install_plist(label, new_plist, plist_path, dry_run)
187
+ else:
188
+ log(f" OK: {cron_id}")
189
+
190
+ # 2. Remove crons that are in installed but NOT in manifest and ARE core
191
+ # (personal crons like shopify-backup, email-monitor are left alone)
192
+ for cron_id, plist_path in installed.items():
193
+ if cron_id not in manifest_ids:
194
+ # Check if this was previously a core cron by reading the plist
195
+ # If it points to NEXO_CODE scripts → it's core, safe to remove
196
+ try:
197
+ with open(plist_path, "rb") as f:
198
+ existing = plistlib.load(f)
199
+ args = existing.get("ProgramArguments", [])
200
+ is_core = any(str(NEXO_CODE) in str(a) for a in args)
201
+ except Exception:
202
+ is_core = False
203
+
204
+ if is_core:
205
+ log(f" REMOVE (no longer in manifest): {cron_id}")
206
+ unload_plist(plist_path, dry_run)
207
+ else:
208
+ log(f" SKIP (personal): {cron_id}")
209
+
210
+ log("Sync complete.")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ dry_run = "--dry-run" in sys.argv
215
+ if dry_run:
216
+ log("DRY RUN MODE — no changes will be made")
217
+ sync(dry_run=dry_run)
@@ -308,15 +308,29 @@ async def api_adaptive():
308
308
 
309
309
  @app.get("/api/sessions")
310
310
  async def api_sessions(limit: int = Query(10, ge=1, le=50)):
311
- """Recent session diaries."""
311
+ """Recent session diaries + active sessions from sessions table."""
312
312
  db = _db()
313
313
  conn = db.get_db()
314
+ # Active sessions (from sessions table, not diaries)
315
+ active_rows = conn.execute(
316
+ "SELECT sid as session_id, task, last_update_epoch, claude_session_id "
317
+ "FROM sessions WHERE last_update_epoch > (strftime('%s','now') - 900) "
318
+ "ORDER BY last_update_epoch DESC"
319
+ ).fetchall()
320
+ active = [dict(r) for r in active_rows]
321
+ # Add last_heartbeat as ISO string for frontend
322
+ for a in active:
323
+ epoch = a.get("last_update_epoch", 0)
324
+ if epoch:
325
+ import datetime
326
+ a["last_heartbeat"] = datetime.datetime.fromtimestamp(epoch).isoformat()
327
+ # Recent diaries
314
328
  rows = conn.execute(
315
329
  "SELECT * FROM session_diary ORDER BY created_at DESC LIMIT ?",
316
330
  (limit,),
317
331
  ).fetchall()
318
332
  diaries = [dict(r) for r in rows]
319
- return {"count": len(diaries), "sessions": diaries}
333
+ return {"count": len(diaries), "sessions": active, "diaries": diaries}
320
334
 
321
335
 
322
336
  @app.get("/api/kg/nodes")
@@ -446,11 +446,12 @@
446
446
 
447
447
  // --- Overdue Items ---
448
448
  if (remindersData || followupsData) {
449
+ const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED', 'blocked', 'waiting'];
449
450
  const reminders = (remindersData?.reminders || []).filter(r =>
450
- r.status === 'PENDING' && r.date && r.date < today
451
+ !excludeStatus.includes(r.status) && r.date && r.date <= today
451
452
  );
452
453
  const followups = (followupsData?.followups || []).filter(f =>
453
- f.status === 'PENDING' && f.date && f.date < today
454
+ !excludeStatus.includes(f.status) && f.date && f.date <= today
454
455
  );
455
456
  const total = reminders.length + followups.length;
456
457
  const el = document.getElementById('overdue-count');
@@ -82,7 +82,7 @@ def auto_resolve_followups(change: dict) -> list[str]:
82
82
  conn = get_db()
83
83
  open_followups = conn.execute(
84
84
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
85
- "AND status != 'DELETED'"
85
+ "AND status NOT IN ('DELETED','archived','blocked','waiting')"
86
86
  ).fetchall()
87
87
 
88
88
  if not open_followups:
@@ -69,14 +69,16 @@ def get_reminders(filter_type: str = 'all') -> list[dict]:
69
69
  elif filter_type == 'due':
70
70
  rows = conn.execute(
71
71
  "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
72
- "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
72
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
73
+ "AND date IS NOT NULL AND date <= ? "
73
74
  "ORDER BY date ASC",
74
75
  (today,)
75
76
  ).fetchall()
76
77
  else: # 'all' — active only
77
78
  rows = conn.execute(
78
79
  "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
79
- "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
80
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
81
+ "ORDER BY date ASC NULLS LAST"
80
82
  ).fetchall()
81
83
  return [dict(r) for r in rows]
82
84
 
@@ -100,7 +102,7 @@ def find_similar_followups(description: str, threshold: float = 0.3) -> list[dic
100
102
  conn = get_db()
101
103
  rows = conn.execute(
102
104
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
103
- "AND status != 'DELETED'"
105
+ "AND status NOT IN ('DELETED','archived','blocked','waiting')"
104
106
  ).fetchall()
105
107
 
106
108
  def tokenize(text: str) -> set:
@@ -313,14 +315,16 @@ def get_followups(filter_type: str = 'all') -> list[dict]:
313
315
  elif filter_type == 'due':
314
316
  rows = conn.execute(
315
317
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
316
- "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
318
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
319
+ "AND date IS NOT NULL AND date <= ? "
317
320
  "ORDER BY date ASC",
318
321
  (today,)
319
322
  ).fetchall()
320
323
  else: # 'all' — active only
321
324
  rows = conn.execute(
322
325
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
323
- "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
326
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
327
+ "ORDER BY date ASC NULLS LAST"
324
328
  ).fetchall()
325
329
  return [dict(r) for r in rows]
326
330
 
@@ -58,7 +58,8 @@ SESSION_START_TS="$NEXO_HOME/operations/.session-start-ts"
58
58
  # 0.5. Detect non-interactive (claude -p) sessions — skip post-mortem entirely
59
59
  # SessionStart hook writes .session-start-ts. If missing or stale (>30 min),
60
60
  # this is likely a -p script session — approve immediately.
61
- if [ ! -f "$SESSION_START_TS" ] || [ "$(($(date +%s) - $(cat "$SESSION_START_TS" 2>/dev/null || echo 0)))" -gt 1800 ]; then
61
+ # Also skip if NEXO_HEADLESS=1 is set (explicit headless mode for scripts).
62
+ if [ "${NEXO_HEADLESS:-}" = "1" ] || [ ! -f "$SESSION_START_TS" ] || [ "$(($(date +%s) - $(cat "$SESSION_START_TS" 2>/dev/null || echo 0)))" -gt 1800 ]; then
62
63
  cat << 'HOOKEOF'
63
64
  {
64
65
  "decision": "approve"
@@ -11,35 +11,52 @@ def _get_db():
11
11
 
12
12
  def _seed_if_empty():
13
13
  """Seed rules from JSON if table is empty (first run after migration)."""
14
+ import sys
14
15
  conn = _get_db()
15
16
  try:
16
17
  count = conn.execute("SELECT COUNT(*) FROM core_rules WHERE is_active = 1").fetchone()[0]
17
18
  except Exception:
18
- # Table doesn't exist yet — migrations haven't run. Bail gracefully.
19
- return
19
+ # Table doesn't exist yet — create it
20
+ conn.execute("""CREATE TABLE IF NOT EXISTS core_rules (
21
+ id TEXT PRIMARY KEY, category TEXT NOT NULL, rule TEXT NOT NULL,
22
+ why TEXT NOT NULL, importance INTEGER NOT NULL DEFAULT 3,
23
+ type TEXT NOT NULL DEFAULT 'advisory', added_in TEXT DEFAULT '',
24
+ removed_in TEXT DEFAULT NULL, is_active INTEGER NOT NULL DEFAULT 1)""")
25
+ conn.execute("""CREATE TABLE IF NOT EXISTS core_rules_version (
26
+ id INTEGER PRIMARY KEY, version TEXT NOT NULL, updated_at TEXT NOT NULL)""")
27
+ conn.execute("INSERT OR IGNORE INTO core_rules_version (id, version, updated_at) VALUES (1, '0.0.0', datetime('now'))")
28
+ conn.commit()
29
+ count = 0
20
30
  if count > 0:
21
31
  return
22
32
 
23
33
  rules_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
24
34
  "rules", "core-rules.json")
25
35
  if not os.path.exists(rules_file):
36
+ print(f"[core_rules] WARNING: {rules_file} not found, skipping seed", file=sys.stderr)
26
37
  return
27
38
 
28
- with open(rules_file) as f:
29
- data = json.load(f)
30
-
31
- version = data["_meta"]["version"]
32
- for cat_key, cat in data["categories"].items():
33
- for rule in cat["rules"]:
34
- conn.execute(
35
- """INSERT OR REPLACE INTO core_rules (id, category, rule, why, importance, type, added_in)
36
- VALUES (?, ?, ?, ?, ?, ?, ?)""",
37
- (rule["id"], cat_key, rule["rule"], rule["why"],
38
- rule["importance"], rule["type"], rule.get("added_in", version))
39
- )
40
-
41
- conn.execute("UPDATE core_rules_version SET version = ?, updated_at = datetime('now') WHERE id = 1", (version,))
42
- conn.commit()
39
+ try:
40
+ with open(rules_file) as f:
41
+ data = json.load(f)
42
+
43
+ version = data["_meta"]["version"]
44
+ loaded = 0
45
+ for cat_key, cat in data["categories"].items():
46
+ for rule in cat["rules"]:
47
+ conn.execute(
48
+ """INSERT OR REPLACE INTO core_rules (id, category, rule, why, importance, type, added_in)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
50
+ (rule["id"], cat_key, rule["rule"], rule["why"],
51
+ rule["importance"], rule["type"], rule.get("added_in", version))
52
+ )
53
+ loaded += 1
54
+
55
+ conn.execute("UPDATE core_rules_version SET version = ?, updated_at = datetime('now') WHERE id = 1", (version,))
56
+ conn.commit()
57
+ print(f"[core_rules] Seeded {loaded} rules (v{version})", file=sys.stderr)
58
+ except Exception as e:
59
+ print(f"[core_rules] ERROR seeding rules: {e}", file=sys.stderr)
43
60
 
44
61
 
45
62
  def handle_rules_check(area: str = "", importance_min: int = 0) -> str:
@@ -196,6 +196,22 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
196
196
  raise RuntimeError(f"Verification failed: {verify_err}")
197
197
  steps_done.append("verify")
198
198
 
199
+ # Step 7: Sync crons with manifest
200
+ cron_sync_result = ""
201
+ try:
202
+ cron_sync_path = NEXO_CODE / "crons" / "sync.py"
203
+ if cron_sync_path.exists():
204
+ import subprocess as _sp
205
+ r = _sp.run(
206
+ [sys.executable, str(cron_sync_path)],
207
+ capture_output=True, text=True, timeout=30,
208
+ env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(NEXO_CODE)},
209
+ )
210
+ cron_sync_result = r.stdout.strip()
211
+ steps_done.append("cron-sync")
212
+ except Exception as e:
213
+ cron_sync_result = f"Cron sync warning: {e}"
214
+
199
215
  # Build result
200
216
  if pull_out == "Already up to date.":
201
217
  return f"Already up to date (v{old_version}). No changes pulled."
@@ -209,6 +225,8 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
209
225
  lines.append(f" Backup: {backup_dir}")
210
226
  if version_changed:
211
227
  lines.append(" Migrations: applied")
228
+ if "cron-sync" in steps_done:
229
+ lines.append(" Crons: synced with manifest")
212
230
  lines.append("")
213
231
  lines.append("MCP server restart needed to load new code.")
214
232
  return "\n".join(lines)
@@ -174,24 +174,21 @@ Rules:
174
174
  - Same file modification with same content = redundant
175
175
  - Similar but different scope (e.g., different recipients) = NOT redundant
176
176
  - When in doubt, say not redundant (false negatives are cheaper than false positives)"""
177
-
178
- auth_check = subprocess.run(
179
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
180
- capture_output=True, text=True, timeout=15
181
177
  )
182
178
  if auth_check.returncode != 0:
183
179
  # CLI not authenticated, skip gracefully
184
180
  return {"redundant": False, "reason": "CLI not authenticated — skipped analysis", "suggestion": "N/A"}
185
181
 
186
182
  env = os.environ.copy()
183
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
187
184
  env.pop("CLAUDECODE", None)
188
185
  env.pop("CLAUDE_CODE", None)
189
186
 
190
187
  try:
191
188
  result = subprocess.run(
192
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text", "--bare",
193
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
194
- capture_output=True, text=True, timeout=60, env=env
189
+ [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
190
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
191
+ capture_output=True, text=True, timeout=21600, env=env
195
192
  )
196
193
  if result.returncode == 0:
197
194
  text = result.stdout.strip()