nexo-brain 2.3.0 → 2.3.2
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 +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- 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__/plugin_loader.cpython-314.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/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- 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/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -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 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- 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__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.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__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.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/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.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 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
package/src/storage_router 2.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Storage Router — DB path abstraction for future multi-tenant support."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class StorageRouter:
|
|
9
|
-
def __init__(self, tenant_id: str = "default"):
|
|
10
|
-
self.tenant_id = tenant_id
|
|
11
|
-
|
|
12
|
-
def nexo_db_path(self) -> str:
|
|
13
|
-
if self.tenant_id == "default":
|
|
14
|
-
data_dir = os.path.join(NEXO_HOME, "data")
|
|
15
|
-
os.makedirs(data_dir, exist_ok=True)
|
|
16
|
-
return os.path.join(data_dir, "nexo.db")
|
|
17
|
-
return os.path.join(NEXO_HOME, "tenants", self.tenant_id, "nexo.db")
|
|
18
|
-
|
|
19
|
-
def cognitive_db_path(self) -> str:
|
|
20
|
-
if self.tenant_id == "default":
|
|
21
|
-
data_dir = os.path.join(NEXO_HOME, "data")
|
|
22
|
-
os.makedirs(data_dir, exist_ok=True)
|
|
23
|
-
return os.path.join(data_dir, "cognitive.db")
|
|
24
|
-
return os.path.join(NEXO_HOME, "tenants", self.tenant_id, "cognitive.db")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
_default_router = StorageRouter("default")
|
|
28
|
-
|
|
29
|
-
def get_router(tenant_id: str = "default") -> StorageRouter:
|
|
30
|
-
if tenant_id == "default":
|
|
31
|
-
return _default_router
|
|
32
|
-
return StorageRouter(tenant_id)
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"""Coordination tools: file tracking, messaging, Q&A."""
|
|
2
|
-
|
|
3
|
-
from db import (
|
|
4
|
-
track_files, untrack_files, get_all_tracked_files,
|
|
5
|
-
send_message, get_inbox,
|
|
6
|
-
ask_question, answer_question, get_pending_questions, check_answer,
|
|
7
|
-
now_epoch,
|
|
8
|
-
)
|
|
9
|
-
from tools_sessions import _format_age
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def handle_track(sid: str, paths: list[str]) -> str:
|
|
13
|
-
"""Track files being edited. Reports conflicts immediately."""
|
|
14
|
-
result = track_files(sid, paths)
|
|
15
|
-
if "error" in result:
|
|
16
|
-
return f"ERROR: {result['error']}"
|
|
17
|
-
|
|
18
|
-
lines = [f"Tracked: {', '.join(result['tracked'])}"]
|
|
19
|
-
|
|
20
|
-
if result["conflicts"]:
|
|
21
|
-
lines.append("")
|
|
22
|
-
lines.append("FILE CONFLICTS:")
|
|
23
|
-
for c in result["conflicts"]:
|
|
24
|
-
lines.append(f" {c['sid']} ({c['task']}):")
|
|
25
|
-
for f in c["files"]:
|
|
26
|
-
lines.append(f" {f}")
|
|
27
|
-
lines.append("")
|
|
28
|
-
lines.append("STOP and inform the user before editing.")
|
|
29
|
-
|
|
30
|
-
return "\n".join(lines)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def handle_untrack(sid: str, paths: list[str] | None = None) -> str:
|
|
34
|
-
"""Untrack files. If no paths given, untrack all."""
|
|
35
|
-
untrack_files(sid, paths)
|
|
36
|
-
if paths:
|
|
37
|
-
return f"Untracked: {', '.join(paths)}"
|
|
38
|
-
return "All files released."
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def handle_files() -> str:
|
|
42
|
-
"""Show all tracked files across sessions."""
|
|
43
|
-
data = get_all_tracked_files()
|
|
44
|
-
if not data:
|
|
45
|
-
return "No tracked files."
|
|
46
|
-
|
|
47
|
-
lines = ["TRACKED FILES:"]
|
|
48
|
-
all_paths = {}
|
|
49
|
-
for sid, info in data.items():
|
|
50
|
-
for path in info["files"]:
|
|
51
|
-
all_paths.setdefault(path, []).append(sid)
|
|
52
|
-
lines.append(f" {sid} ({info['task']}):")
|
|
53
|
-
for path in info["files"]:
|
|
54
|
-
lines.append(f" {path}")
|
|
55
|
-
|
|
56
|
-
conflicts = {p: sids for p, sids in all_paths.items() if len(sids) > 1}
|
|
57
|
-
if conflicts:
|
|
58
|
-
lines.append("")
|
|
59
|
-
lines.append("CONFLICTS:")
|
|
60
|
-
for path, sids in conflicts.items():
|
|
61
|
-
lines.append(f" {path} -> {', '.join(sids)}")
|
|
62
|
-
|
|
63
|
-
return "\n".join(lines)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def handle_send(from_sid: str, to_sid: str, text: str) -> str:
|
|
67
|
-
"""Send a message. to_sid='all' for broadcast."""
|
|
68
|
-
msg_id = send_message(from_sid, to_sid, text)
|
|
69
|
-
target = "all sessions" if to_sid == "all" else to_sid
|
|
70
|
-
return f"Message {msg_id} sent to {target}."
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def handle_ask(from_sid: str, to_sid: str, question: str) -> str:
|
|
74
|
-
"""Create a question to another session (non-blocking)."""
|
|
75
|
-
qid = ask_question(from_sid, to_sid, question)
|
|
76
|
-
return (
|
|
77
|
-
f"Question sent: {qid}\n"
|
|
78
|
-
f"To: {to_sid}\n"
|
|
79
|
-
f"Question: {question}\n\n"
|
|
80
|
-
f"The other session will see this question on its next nexo_heartbeat.\n"
|
|
81
|
-
f"Use nexo_check_answer(qid='{qid}') to check for a response."
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def handle_answer(qid: str, answer_text: str) -> str:
|
|
86
|
-
"""Answer a pending question."""
|
|
87
|
-
result = answer_question(qid, answer_text)
|
|
88
|
-
if "error" in result:
|
|
89
|
-
return f"ERROR: {result['error']}"
|
|
90
|
-
return f"Answered {qid}: {answer_text}"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def handle_check_answer(qid: str) -> str:
|
|
94
|
-
"""Check if a question has been answered."""
|
|
95
|
-
result = check_answer(qid)
|
|
96
|
-
if not result:
|
|
97
|
-
return f"Question {qid} not found."
|
|
98
|
-
if result["status"] == "answered":
|
|
99
|
-
return f"ANSWER for {qid}: {result['answer']}"
|
|
100
|
-
elif result["status"] == "expired":
|
|
101
|
-
return f"Question {qid} expired without answer."
|
|
102
|
-
return f"Question still pending. Retry in a few seconds."
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""Credentials CRUD tools: get, create, update, delete, list."""
|
|
2
|
-
|
|
3
|
-
from db import create_credential, update_credential, delete_credential, get_credential, list_credentials
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def handle_credential_get(service: str, key: str = '') -> str:
|
|
7
|
-
"""Retrieve credential(s) including their values. Use for reading secrets."""
|
|
8
|
-
results = get_credential(service, key if key else None)
|
|
9
|
-
if not results:
|
|
10
|
-
target = f"{service}/{key}" if key else service
|
|
11
|
-
return f"ERROR: No credentials found for '{target}'."
|
|
12
|
-
is_fuzzy = any(r.get("_fuzzy") for r in results)
|
|
13
|
-
lines = []
|
|
14
|
-
if is_fuzzy:
|
|
15
|
-
lines.append(f"⚠ No exact match for '{service}'. Similar results ({len(results)}):")
|
|
16
|
-
lines.append("")
|
|
17
|
-
for r in results:
|
|
18
|
-
lines.append(f"CREDENTIAL {r['service']}/{r['key']}:")
|
|
19
|
-
lines.append(f" Value: {r['value']}")
|
|
20
|
-
notes = r.get("notes") or ""
|
|
21
|
-
lines.append(f" Notes: {notes if notes else '—'}")
|
|
22
|
-
return "\n".join(lines)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def handle_credential_create(service: str, key: str, value: str, notes: str = '') -> str:
|
|
26
|
-
"""Create a new credential entry."""
|
|
27
|
-
result = create_credential(service, key, value, notes)
|
|
28
|
-
if "error" in result:
|
|
29
|
-
return f"ERROR: {result['error']}"
|
|
30
|
-
return f"Credential {service}/{key} created."
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def handle_credential_update(service: str, key: str, value: str = '', notes: str = '') -> str:
|
|
34
|
-
"""Update the value and/or notes of an existing credential."""
|
|
35
|
-
result = update_credential(
|
|
36
|
-
service,
|
|
37
|
-
key,
|
|
38
|
-
value if value else None,
|
|
39
|
-
notes if notes else None,
|
|
40
|
-
)
|
|
41
|
-
if "error" in result:
|
|
42
|
-
return f"ERROR: {result['error']}"
|
|
43
|
-
return f"Credential {service}/{key} updated."
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def handle_credential_delete(service: str, key: str = '') -> str:
|
|
47
|
-
"""Delete a credential or all credentials for a service."""
|
|
48
|
-
deleted = delete_credential(service, key if key else None)
|
|
49
|
-
if not deleted:
|
|
50
|
-
target = f"{service}/{key}" if key else service
|
|
51
|
-
return f"ERROR: No credentials found for '{target}'."
|
|
52
|
-
if key:
|
|
53
|
-
return f"Credential deleted."
|
|
54
|
-
return f"All credentials for service deleted."
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def handle_credential_list(service: str = '') -> str:
|
|
58
|
-
"""List credential service/key names and notes — values are never shown."""
|
|
59
|
-
results = list_credentials(service if service else None)
|
|
60
|
-
label = service if service else "ALL"
|
|
61
|
-
if not results:
|
|
62
|
-
return f"CREDENTIALS {label.upper()}: No entries."
|
|
63
|
-
lines = [f"CREDENTIALS {label.upper()} ({len(results)}):"]
|
|
64
|
-
for r in results:
|
|
65
|
-
notes = r.get("notes") or ""
|
|
66
|
-
suffix = f" — {notes}" if notes else ""
|
|
67
|
-
lines.append(f" {r['service']}/{r['key']}{suffix}")
|
|
68
|
-
return "\n".join(lines)
|
package/src/tools_learnings 2.py
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"""Learnings CRUD tools: add, search, update, delete, list."""
|
|
2
|
-
|
|
3
|
-
from db import (create_learning, update_learning, delete_learning, search_learnings,
|
|
4
|
-
list_learnings, find_similar_learnings, get_db, now_epoch)
|
|
5
|
-
|
|
6
|
-
def handle_learning_add(category: str, title: str, content: str, reasoning: str = '',
|
|
7
|
-
prevention: str = '', applies_to: str = '', review_days: int = 30,
|
|
8
|
-
priority: str = 'medium') -> str:
|
|
9
|
-
"""Add a new learning entry to the specified category.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
category: Free-form category name (e.g., 'backend', 'frontend', 'devops', 'infrastructure', 'security', 'nexo-ops'). Use consistent names — learnings are grouped and searched by category.
|
|
13
|
-
title: Short title for the learning
|
|
14
|
-
content: Full description of what was learned
|
|
15
|
-
reasoning: WHY this matters — what led to discovering this, what was the context
|
|
16
|
-
prevention: Concrete rule/check that prevents repeating this mistake
|
|
17
|
-
applies_to: Files, systems, or areas this learning applies to
|
|
18
|
-
review_days: Days until this learning should be reviewed again
|
|
19
|
-
priority: critical, high, medium, low (default: medium)
|
|
20
|
-
"""
|
|
21
|
-
if priority not in ('critical', 'high', 'medium', 'low'):
|
|
22
|
-
priority = 'medium'
|
|
23
|
-
category = category.lower().strip()
|
|
24
|
-
if not category:
|
|
25
|
-
return "ERROR: Category cannot be empty."
|
|
26
|
-
result = create_learning(
|
|
27
|
-
category, title, content, reasoning=reasoning
|
|
28
|
-
)
|
|
29
|
-
if "error" in result:
|
|
30
|
-
return f"ERROR: {result['error']}"
|
|
31
|
-
if prevention or applies_to or review_days > 0 or priority != 'medium':
|
|
32
|
-
initial_weight = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
|
|
33
|
-
conn = get_db()
|
|
34
|
-
conn.execute(
|
|
35
|
-
"UPDATE learnings SET prevention = ?, applies_to = ?, status = COALESCE(status, 'active'), "
|
|
36
|
-
"review_due_at = ?, updated_at = ?, priority = ?, weight = ? WHERE id = ?",
|
|
37
|
-
(prevention, applies_to, now_epoch() + (max(1, int(review_days)) * 86400), now_epoch(),
|
|
38
|
-
priority, initial_weight, result["id"])
|
|
39
|
-
)
|
|
40
|
-
conn.commit()
|
|
41
|
-
result = conn.execute("SELECT * FROM learnings WHERE id = ?", (result["id"],)).fetchone()
|
|
42
|
-
result = dict(result)
|
|
43
|
-
|
|
44
|
-
# Cognitive ingest — embed learning for semantic search
|
|
45
|
-
new_id = result["id"]
|
|
46
|
-
try:
|
|
47
|
-
import cognitive
|
|
48
|
-
cognitive.ingest(f"{title}: {content}", "learning", f"L{new_id}", title, category)
|
|
49
|
-
except Exception:
|
|
50
|
-
pass
|
|
51
|
-
|
|
52
|
-
# Similarity check — detect repeated errors
|
|
53
|
-
matches = find_similar_learnings(new_id, title, content, category)
|
|
54
|
-
repetition_msg = ""
|
|
55
|
-
if matches:
|
|
56
|
-
conn = get_db()
|
|
57
|
-
for original_id, similarity in matches:
|
|
58
|
-
conn.execute(
|
|
59
|
-
"INSERT INTO error_repetitions (new_learning_id, original_learning_id, similarity, area) VALUES (?,?,?,?)",
|
|
60
|
-
(new_id, original_id, similarity, category)
|
|
61
|
-
)
|
|
62
|
-
conn.commit()
|
|
63
|
-
repetition_msg = f"\n⚠️ REPETITION WARNING: Similar to {len(matches)} existing learning(s): " + \
|
|
64
|
-
", ".join(f"#{m[0]} ({m[1]:.0%})" for m in matches[:3])
|
|
65
|
-
|
|
66
|
-
# Somatic event logging (append-only in nexo.db, projected to cognitive.db nightly)
|
|
67
|
-
try:
|
|
68
|
-
if applies_to:
|
|
69
|
-
for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
|
|
70
|
-
get_db().execute(
|
|
71
|
-
"INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
|
|
72
|
-
(file_path, "file", "learning_add", 0.15, f"learning:{new_id}")
|
|
73
|
-
)
|
|
74
|
-
# Area + extra file pain ONLY for repeated errors
|
|
75
|
-
if matches:
|
|
76
|
-
get_db().execute(
|
|
77
|
-
"INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
|
|
78
|
-
(category, "area", "error_repetition", 0.15, f"learning:{new_id}")
|
|
79
|
-
)
|
|
80
|
-
if applies_to:
|
|
81
|
-
for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
|
|
82
|
-
get_db().execute(
|
|
83
|
-
"INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
|
|
84
|
-
(file_path, "file", "error_repetition", 0.25, f"learning:{new_id}")
|
|
85
|
-
)
|
|
86
|
-
get_db().commit()
|
|
87
|
-
except Exception:
|
|
88
|
-
pass # Somatic event logging is best-effort
|
|
89
|
-
|
|
90
|
-
# Knowledge graph incremental population
|
|
91
|
-
try:
|
|
92
|
-
from kg_populate import on_learning_add
|
|
93
|
-
on_learning_add(new_id, category, title, applies_to)
|
|
94
|
-
except Exception:
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
meta = []
|
|
98
|
-
if prevention:
|
|
99
|
-
meta.append("with prevention")
|
|
100
|
-
if applies_to:
|
|
101
|
-
meta.append(f"applies_to={applies_to}")
|
|
102
|
-
meta_str = f" ({', '.join(meta)})" if meta else ""
|
|
103
|
-
return f"Learning #{result['id']} added in {category}: {title}{meta_str}{repetition_msg}"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def handle_learning_search(query: str, category: str = '') -> str:
|
|
107
|
-
"""Search learnings by query string, optionally filtered by category."""
|
|
108
|
-
results = search_learnings(query, category if category else None)
|
|
109
|
-
if not results:
|
|
110
|
-
return f"No results for '{query}'."
|
|
111
|
-
lines = [f"RESULTS ({len(results)}):"]
|
|
112
|
-
for r in results:
|
|
113
|
-
snippet = r["content"][:100] + "..." if len(r["content"]) > 100 else r["content"]
|
|
114
|
-
status = r.get("status", "active")
|
|
115
|
-
review_due = r.get("review_due_at")
|
|
116
|
-
review_note = f" | review_due={review_due:.0f}" if isinstance(review_due, (int, float)) and review_due else ""
|
|
117
|
-
pri = r.get("priority", "medium") or "medium"
|
|
118
|
-
w = r.get("weight", 0.5) or 0.5
|
|
119
|
-
pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
|
|
120
|
-
lines.append(f" #{r['id']} [{r['category']}] [{status}] {pri_icon}{pri} w={w:.2f} {r['title']}{review_note}")
|
|
121
|
-
lines.append(f" {snippet}")
|
|
122
|
-
if r.get("prevention"):
|
|
123
|
-
lines.append(f" Prevention: {r['prevention'][:100]}")
|
|
124
|
-
|
|
125
|
-
# v1.2: Passive rehearsal — strengthen matching cognitive memories
|
|
126
|
-
try:
|
|
127
|
-
import cognitive
|
|
128
|
-
for r in results[:5]:
|
|
129
|
-
cognitive.rehearse_by_content(f"{r.get('title', '')} {r.get('content', '')[:200]}")
|
|
130
|
-
except Exception:
|
|
131
|
-
pass
|
|
132
|
-
|
|
133
|
-
return "\n".join(lines)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def handle_learning_update(id: int, title: str = '', content: str = '', category: str = '',
|
|
137
|
-
reasoning: str = '', prevention: str = '', applies_to: str = '',
|
|
138
|
-
status: str = '', review_days: int = 0, priority: str = '') -> str:
|
|
139
|
-
"""Update an existing learning, including review metadata and priority."""
|
|
140
|
-
kwargs = {}
|
|
141
|
-
if title:
|
|
142
|
-
kwargs["title"] = title
|
|
143
|
-
if content:
|
|
144
|
-
kwargs["content"] = content
|
|
145
|
-
if category:
|
|
146
|
-
kwargs["category"] = category.lower().strip()
|
|
147
|
-
if reasoning:
|
|
148
|
-
kwargs["reasoning"] = reasoning
|
|
149
|
-
if prevention:
|
|
150
|
-
kwargs["prevention"] = prevention
|
|
151
|
-
if applies_to:
|
|
152
|
-
kwargs["applies_to"] = applies_to
|
|
153
|
-
if status:
|
|
154
|
-
kwargs["status"] = status
|
|
155
|
-
if review_days > 0:
|
|
156
|
-
kwargs["review_days"] = review_days
|
|
157
|
-
if not kwargs:
|
|
158
|
-
return "ERROR: Nothing to update. Provide new fields."
|
|
159
|
-
basic_kwargs = {k: v for k, v in kwargs.items() if k in {"title", "content", "category", "reasoning"}}
|
|
160
|
-
result = update_learning(id, **basic_kwargs)
|
|
161
|
-
if "error" in result:
|
|
162
|
-
return f"ERROR: {result['error']}"
|
|
163
|
-
extra_updates = {}
|
|
164
|
-
if prevention:
|
|
165
|
-
extra_updates["prevention"] = prevention
|
|
166
|
-
if applies_to:
|
|
167
|
-
extra_updates["applies_to"] = applies_to
|
|
168
|
-
if status:
|
|
169
|
-
extra_updates["status"] = status
|
|
170
|
-
if priority and priority in ('critical', 'high', 'medium', 'low'):
|
|
171
|
-
extra_updates["priority"] = priority
|
|
172
|
-
extra_updates["weight"] = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
|
|
173
|
-
if review_days > 0:
|
|
174
|
-
extra_updates["review_due_at"] = now_epoch() + (max(1, int(review_days)) * 86400)
|
|
175
|
-
if extra_updates:
|
|
176
|
-
extra_updates["updated_at"] = now_epoch()
|
|
177
|
-
set_clause = ", ".join(f"{k} = ?" for k in extra_updates)
|
|
178
|
-
values = list(extra_updates.values()) + [id]
|
|
179
|
-
conn = get_db()
|
|
180
|
-
conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", values)
|
|
181
|
-
conn.commit()
|
|
182
|
-
return f"Learning #{id} updated."
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def handle_learning_delete(id: int) -> str:
|
|
186
|
-
"""Delete a learning entry by ID."""
|
|
187
|
-
deleted = delete_learning(id)
|
|
188
|
-
if not deleted:
|
|
189
|
-
return f"ERROR: Learning #{id} not found."
|
|
190
|
-
return f"Learning #{id} deleted."
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def handle_learning_list(category: str = '') -> str:
|
|
194
|
-
"""List all learnings, grouped by category if no filter given."""
|
|
195
|
-
results = list_learnings(category if category else None)
|
|
196
|
-
if not results:
|
|
197
|
-
label = category if category else "ALL"
|
|
198
|
-
return f"LEARNINGS {label} (0): No entries."
|
|
199
|
-
|
|
200
|
-
if category:
|
|
201
|
-
label = category.upper()
|
|
202
|
-
lines = [f"LEARNINGS {label} ({len(results)}):"]
|
|
203
|
-
for r in results:
|
|
204
|
-
pri = r.get("priority", "medium") or "medium"
|
|
205
|
-
w = r.get("weight", 0.5) or 0.5
|
|
206
|
-
pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
|
|
207
|
-
lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} {r['title']}")
|
|
208
|
-
else:
|
|
209
|
-
lines = [f"LEARNINGS ALL ({len(results)}):"]
|
|
210
|
-
current_cat = None
|
|
211
|
-
for r in results:
|
|
212
|
-
if r["category"] != current_cat:
|
|
213
|
-
current_cat = r["category"]
|
|
214
|
-
lines.append(f"\n [{current_cat.upper()}]")
|
|
215
|
-
pri = r.get("priority", "medium") or "medium"
|
|
216
|
-
w = r.get("weight", 0.5) or 0.5
|
|
217
|
-
pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
|
|
218
|
-
lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} {r['title']}")
|
|
219
|
-
|
|
220
|
-
return "\n".join(lines)
|
package/src/tools_menu 2.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
"""Menu generator — NEXO operations center."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timedelta
|
|
4
|
-
import json
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from tools_sessions import handle_status
|
|
9
|
-
from tools_reminders import handle_reminders
|
|
10
|
-
from db import get_db
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _get_date_str() -> str:
|
|
14
|
-
"""Get formatted date in Madrid timezone."""
|
|
15
|
-
try:
|
|
16
|
-
result = subprocess.run(
|
|
17
|
-
["date", "+%A %d %B %Y, %H:%M"],
|
|
18
|
-
capture_output=True, text=True,
|
|
19
|
-
env={"PATH": "/usr/bin:/bin"}
|
|
20
|
-
)
|
|
21
|
-
return result.stdout.strip()
|
|
22
|
-
except Exception:
|
|
23
|
-
return datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
MENU_ITEMS = [
|
|
27
|
-
("Projects", [
|
|
28
|
-
("1", "Projects - Review project status"),
|
|
29
|
-
("9", "Claude Agent VPS - Review autonomous changes"),
|
|
30
|
-
]),
|
|
31
|
-
("Advertising", [
|
|
32
|
-
("7", "Google Ads - Manage campaigns"),
|
|
33
|
-
("7b", "Meta Ads - Manage Facebook/Instagram campaigns"),
|
|
34
|
-
("7c", "Ads Tracking - Combined Google+Meta review"),
|
|
35
|
-
]),
|
|
36
|
-
("Shopify", [
|
|
37
|
-
("4", "Shopify Theme Sync - Sync theme"),
|
|
38
|
-
("5", "Shopify Scripts - Run periodic scripts"),
|
|
39
|
-
("6", "Change Shopify Promotion"),
|
|
40
|
-
]),
|
|
41
|
-
("Server & Infrastructure", [
|
|
42
|
-
("2", "Server - Health check your-server.example.com"),
|
|
43
|
-
("3", "WhatsApp Logs - Review logs your-whatsapp-account"),
|
|
44
|
-
("11", "File Tracker - PHP file report"),
|
|
45
|
-
("12", "Google Cloud - Spend, usage and GCP status"),
|
|
46
|
-
]),
|
|
47
|
-
("Communication & Monitoring", [
|
|
48
|
-
("8", "Recovery Optimizer - Weekly AI analysis (MONDAY)"),
|
|
49
|
-
("10", "Recovery Monitor - Email/WA recovery status (24h)"),
|
|
50
|
-
("13", "Review Monitor - Email/WA review status"),
|
|
51
|
-
("14", "WhatsApp Full Analysis - Global statistics"),
|
|
52
|
-
("15", "Google Analytics - Review web analytics"),
|
|
53
|
-
("16", "Email Review - Check inboxes and spam"),
|
|
54
|
-
]),
|
|
55
|
-
("Reports & SEO", [
|
|
56
|
-
("17", "Search Console Audit (every 2 weeks)"),
|
|
57
|
-
("18", "Sitemap resubmission (every 30 days)"),
|
|
58
|
-
("19", "SEO meta verification"),
|
|
59
|
-
("20", "Weekly Email Report (Sundays)"),
|
|
60
|
-
]),
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _get_dashboard_alerts() -> list[dict]:
|
|
65
|
-
"""Run proactive dashboard and return alerts."""
|
|
66
|
-
try:
|
|
67
|
-
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
68
|
-
script = nexo_home / "scripts" / "nexo-proactive-dashboard.py"
|
|
69
|
-
if not script.exists():
|
|
70
|
-
return []
|
|
71
|
-
result = subprocess.run(
|
|
72
|
-
[sys.executable, str(script), "--json"],
|
|
73
|
-
capture_output=True, text=True, timeout=10
|
|
74
|
-
)
|
|
75
|
-
if result.stdout.strip():
|
|
76
|
-
return json.loads(result.stdout)
|
|
77
|
-
except Exception:
|
|
78
|
-
pass
|
|
79
|
-
return []
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _get_memory_review_summary() -> dict:
|
|
83
|
-
"""Return counts of due memory reviews."""
|
|
84
|
-
try:
|
|
85
|
-
conn = get_db()
|
|
86
|
-
now_epoch = datetime.now().timestamp()
|
|
87
|
-
now_iso = datetime.now().isoformat(timespec="seconds")
|
|
88
|
-
due_learnings = conn.execute(
|
|
89
|
-
"SELECT COUNT(*) FROM learnings WHERE review_due_at IS NOT NULL AND status != 'superseded' AND review_due_at <= ?",
|
|
90
|
-
(now_epoch,)
|
|
91
|
-
).fetchone()[0]
|
|
92
|
-
due_decisions = conn.execute(
|
|
93
|
-
"SELECT COUNT(*) FROM decisions WHERE review_due_at IS NOT NULL AND status != 'reviewed' AND review_due_at <= ?",
|
|
94
|
-
(now_iso,)
|
|
95
|
-
).fetchone()[0]
|
|
96
|
-
return {
|
|
97
|
-
"learnings": due_learnings,
|
|
98
|
-
"decisions": due_decisions,
|
|
99
|
-
"total": due_learnings + due_decisions,
|
|
100
|
-
}
|
|
101
|
-
except Exception:
|
|
102
|
-
return {"learnings": 0, "decisions": 0, "total": 0}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def handle_menu() -> str:
|
|
106
|
-
"""Generate the full operations menu with alerts."""
|
|
107
|
-
date_str = _get_date_str()
|
|
108
|
-
W = 56 # inner width
|
|
109
|
-
|
|
110
|
-
lines = []
|
|
111
|
-
lines.append("╔" + "═" * W + "╗")
|
|
112
|
-
lines.append("║" + "NEXO — OPERATIONS CENTER".center(W) + "║")
|
|
113
|
-
lines.append("║" + date_str.center(W) + "║")
|
|
114
|
-
lines.append("╠" + "═" * W + "╣")
|
|
115
|
-
|
|
116
|
-
# Proactive dashboard alerts
|
|
117
|
-
dashboard_alerts = _get_dashboard_alerts()
|
|
118
|
-
memory_reviews = _get_memory_review_summary()
|
|
119
|
-
due = handle_reminders("due")
|
|
120
|
-
has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No reminders" not in due)
|
|
121
|
-
|
|
122
|
-
if has_alerts:
|
|
123
|
-
lines.append("║" + " PROACTIVE ALERTS".ljust(W) + "║")
|
|
124
|
-
lines.append("╠" + "═" * W + "╣")
|
|
125
|
-
|
|
126
|
-
if dashboard_alerts:
|
|
127
|
-
for alert in dashboard_alerts[:10]: # Top 10
|
|
128
|
-
sev = alert.get("severity", "low")
|
|
129
|
-
icon = {"high": "!!!", "medium": " ! ", "low": " . "}.get(sev, " . ")
|
|
130
|
-
text = alert.get("title", "")[:W - 8]
|
|
131
|
-
lines.append("║" + f" {icon} {text}".ljust(W) + "║")
|
|
132
|
-
if len(dashboard_alerts) > 10:
|
|
133
|
-
more = len(dashboard_alerts) - 10
|
|
134
|
-
lines.append("║" + f" ... and {more} more alerts".ljust(W) + "║")
|
|
135
|
-
|
|
136
|
-
if memory_reviews["total"] > 0:
|
|
137
|
-
text = (
|
|
138
|
-
f"MEMORY: {memory_reviews['total']} pending reviews "
|
|
139
|
-
f"({memory_reviews['decisions']} decisions, {memory_reviews['learnings']} learnings)"
|
|
140
|
-
)[:W - 4]
|
|
141
|
-
lines.append("║" + f" ! {text}".ljust(W) + "║")
|
|
142
|
-
|
|
143
|
-
if due and "No reminders" not in due:
|
|
144
|
-
for reminder_line in due.split("\n"):
|
|
145
|
-
if reminder_line.strip():
|
|
146
|
-
truncated = reminder_line[:W - 2]
|
|
147
|
-
lines.append("║" + f" {truncated}".ljust(W) + "║")
|
|
148
|
-
|
|
149
|
-
lines.append("╠" + "═" * W + "╣")
|
|
150
|
-
|
|
151
|
-
# Menu categories
|
|
152
|
-
for category, items in MENU_ITEMS:
|
|
153
|
-
lines.append("║" + f" {category.upper()}".ljust(W) + "║")
|
|
154
|
-
lines.append("║" + "─" * W + "║")
|
|
155
|
-
for num, desc in items:
|
|
156
|
-
entry = f" {num:>3}. {desc}"
|
|
157
|
-
lines.append("║" + entry.ljust(W) + "║")
|
|
158
|
-
lines.append("╠" + "═" * W + "╣")
|
|
159
|
-
|
|
160
|
-
# Backlog: ideas, future projects, undated or distant tasks
|
|
161
|
-
try:
|
|
162
|
-
conn = get_db()
|
|
163
|
-
cutoff = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|
164
|
-
# Reminders without date (backlog/ideas)
|
|
165
|
-
no_date = conn.execute(
|
|
166
|
-
"SELECT id, description, category FROM reminders WHERE status LIKE 'PENDING%' AND (date IS NULL OR date='') ORDER BY category, id"
|
|
167
|
-
).fetchall()
|
|
168
|
-
# Reminders with date > 7 days ahead (future)
|
|
169
|
-
future = conn.execute(
|
|
170
|
-
"SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDING%' AND date > ? ORDER BY date",
|
|
171
|
-
(cutoff,)
|
|
172
|
-
).fetchall()
|
|
173
|
-
# Followups without date
|
|
174
|
-
nf_no_date = conn.execute(
|
|
175
|
-
"SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETED%' AND status NOT IN ('DELETED','archived','blocked','waiting') AND (date IS NULL OR date='') ORDER BY id"
|
|
176
|
-
).fetchall()
|
|
177
|
-
|
|
178
|
-
if no_date or future or nf_no_date:
|
|
179
|
-
lines.append("║" + " BACKLOG / IDEAS / FUTURE".ljust(W) + "║")
|
|
180
|
-
lines.append("║" + "─" * W + "║")
|
|
181
|
-
|
|
182
|
-
if no_date:
|
|
183
|
-
by_cat = {}
|
|
184
|
-
for r in no_date:
|
|
185
|
-
cat = (r["category"] or "general").capitalize()
|
|
186
|
-
by_cat.setdefault(cat, []).append(r)
|
|
187
|
-
for cat, items in by_cat.items():
|
|
188
|
-
lines.append("║" + f" [{cat}]".ljust(W) + "║")
|
|
189
|
-
for r in items:
|
|
190
|
-
short = r["description"][:W - 10]
|
|
191
|
-
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
192
|
-
|
|
193
|
-
if future:
|
|
194
|
-
lines.append("║" + f" [Scheduled]".ljust(W) + "║")
|
|
195
|
-
for r in future:
|
|
196
|
-
short = r["description"][:W - 18]
|
|
197
|
-
lines.append("║" + f" {r['id']} ({r['date']}): {short}".ljust(W) + "║")
|
|
198
|
-
|
|
199
|
-
if nf_no_date:
|
|
200
|
-
lines.append("║" + f" [Pending followups]".ljust(W) + "║")
|
|
201
|
-
for r in nf_no_date:
|
|
202
|
-
short = r["description"][:W - 12]
|
|
203
|
-
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
204
|
-
|
|
205
|
-
lines.append("╠" + "═" * W + "╣")
|
|
206
|
-
except Exception as e:
|
|
207
|
-
lines.append("║" + f" ⚠ Error backlog: {e}".ljust(W) + "║")
|
|
208
|
-
lines.append("╠" + "═" * W + "╣")
|
|
209
|
-
|
|
210
|
-
# Active sessions
|
|
211
|
-
sessions = handle_status()
|
|
212
|
-
if "No sessions" not in sessions:
|
|
213
|
-
lines.append("║" + " ACTIVE SESSIONS".ljust(W) + "║")
|
|
214
|
-
lines.append("║" + "─" * W + "║")
|
|
215
|
-
for s_line in sessions.split("\n"):
|
|
216
|
-
if s_line.strip() and "ACTIVE SESSIONS" not in s_line:
|
|
217
|
-
truncated = s_line[:W - 2]
|
|
218
|
-
lines.append("║" + f" {truncated}".ljust(W) + "║")
|
|
219
|
-
lines.append("╠" + "═" * W + "╣")
|
|
220
|
-
|
|
221
|
-
# Replace last ╠═╣ with bottom border
|
|
222
|
-
if lines[-1].startswith("╠"):
|
|
223
|
-
lines[-1] = "╚" + "═" * W + "╝"
|
|
224
|
-
else:
|
|
225
|
-
lines.append("╚" + "═" * W + "╝")
|
|
226
|
-
|
|
227
|
-
return "\n".join(lines)
|