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.
- package/README.md +140 -41
- package/package.json +15 -3
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/crons/manifest.json +106 -0
- package/src/crons/sync.py +217 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app.py +16 -2
- package/src/dashboard/templates/dashboard.html +3 -2
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_episodic.py +1 -1
- package/src/db/_reminders.py +9 -5
- package/src/hooks/session-stop.sh +2 -1
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/core_rules.py +34 -17
- package/src/plugins/update.py +18 -0
- package/src/scripts/check-context.py +4 -7
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +512 -167
- package/src/scripts/deep-sleep/collect.py +480 -0
- package/src/scripts/deep-sleep/extract-prompt.md +233 -0
- package/src/scripts/deep-sleep/extract.py +249 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
- package/src/scripts/deep-sleep/synthesize.py +191 -0
- package/src/scripts/nexo-catchup.py +5 -8
- package/src/scripts/nexo-daily-self-audit.py +28 -19
- package/src/scripts/nexo-deep-sleep.sh +31 -16
- package/src/scripts/nexo-evolution-run.py +5 -20
- package/src/scripts/nexo-followup-hygiene.py +4 -2
- package/src/scripts/nexo-github-monitor.py +6 -9
- package/src/scripts/nexo-immune.py +4 -17
- package/src/scripts/nexo-learning-validator.py +0 -29
- package/src/scripts/nexo-postmortem-consolidator.py +9 -20
- package/src/scripts/nexo-proactive-dashboard.py +1 -0
- package/src/scripts/nexo-sleep.py +8 -18
- package/src/scripts/nexo-synthesis.py +8 -19
- package/src/tools_menu.py +1 -1
- package/src/tools_sessions.py +67 -0
- package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-310.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/analyze_session.py +0 -217
- package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
- package/src/scripts/deep-sleep/prompt.md +0 -109
- package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
- 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)
|
|
Binary file
|
|
Binary file
|
package/src/dashboard/app.py
CHANGED
|
@@ -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
|
|
451
|
+
!excludeStatus.includes(r.status) && r.date && r.date <= today
|
|
451
452
|
);
|
|
452
453
|
const followups = (followupsData?.followups || []).filter(f =>
|
|
453
|
-
f.status
|
|
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');
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/db/_episodic.py
CHANGED
|
@@ -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
|
|
85
|
+
"AND status NOT IN ('DELETED','archived','blocked','waiting')"
|
|
86
86
|
).fetchall()
|
|
87
87
|
|
|
88
88
|
if not open_followups:
|
package/src/db/_reminders.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 —
|
|
19
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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:
|
package/src/plugins/update.py
CHANGED
|
@@ -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",
|
|
193
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
194
|
-
capture_output=True, text=True, timeout=
|
|
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()
|
|
Binary file
|