nexo-brain 7.1.0 → 7.1.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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/bin/nexo-brain.js +198 -92
- package/package.json +1 -1
- package/src/agent_runner.py +10 -8
- package/src/auto_close_sessions.py +19 -2
- package/src/auto_update.py +305 -42
- package/src/autonomy_mandate.py +260 -0
- package/src/bootstrap_docs.py +22 -1
- package/src/cli.py +181 -1
- package/src/cli_email.py +104 -73
- package/src/client_sync.py +22 -1
- package/src/cognitive/_core.py +5 -3
- package/src/core_prompts.py +50 -0
- package/src/cron_recovery.py +81 -7
- package/src/crons/manifest.json +57 -0
- package/src/crons/sync.py +95 -26
- package/src/dashboard/app.py +59 -0
- package/src/dashboard/templates/base.html +2 -0
- package/src/dashboard/templates/feature-disabled.html +27 -0
- package/src/db/_email_accounts.py +67 -18
- package/src/db/_fts.py +5 -5
- package/src/db/_personal_scripts.py +1 -1
- package/src/db/_skills.py +3 -3
- package/src/doctor/providers/runtime.py +35 -20
- package/src/email_config.py +18 -9
- package/src/enforcement_classifier.py +3 -12
- package/src/evolution_cycle.py +37 -149
- package/src/guardian_telemetry.py +3 -2
- package/src/hook_guardrails.py +61 -0
- package/src/hooks/capture-tool-logs.sh +11 -3
- package/src/hooks/daily-briefing-check.sh +7 -2
- package/src/hooks/heartbeat-enforcement.py +14 -1
- package/src/hooks/heartbeat-posttool.sh +2 -0
- package/src/hooks/heartbeat-user-msg.sh +2 -0
- package/src/hooks/inbox-hook.sh +6 -2
- package/src/hooks/post-compact.sh +12 -4
- package/src/hooks/pre-compact.sh +12 -4
- package/src/migrate_embeddings.py +5 -3
- package/src/nexo_migrate.py +3 -1
- package/src/plugin_loader.py +14 -5
- package/src/plugins/adaptive_mode.py +4 -1
- package/src/plugins/backup.py +32 -20
- package/src/plugins/evolution.py +2 -0
- package/src/plugins/memory_export.py +6 -1
- package/src/plugins/personal_plugins.py +17 -7
- package/src/plugins/personal_scripts.py +64 -3
- package/src/presets/entities_universal.json +67 -4
- package/src/product_mode.py +201 -0
- package/src/r14_correction_learning.py +5 -20
- package/src/r15_project_context.py +4 -10
- package/src/r16_declared_done.py +3 -16
- package/src/r17_promise_debt.py +3 -16
- package/src/r18_followup_autocomplete.py +5 -7
- package/src/r19_project_grep.py +5 -8
- package/src/r20_constant_change.py +5 -15
- package/src/r21_legacy_path.py +5 -7
- package/src/r22_personal_script.py +4 -8
- package/src/r23_ssh_without_atlas.py +4 -11
- package/src/r23b_deploy_vhost.py +7 -6
- package/src/r23c_cwd_mismatch.py +7 -6
- package/src/r23d_chown_chmod_recursive.py +6 -6
- package/src/r23e_force_push_main.py +5 -6
- package/src/r23f_db_no_where.py +5 -6
- package/src/r23g_secrets_in_output.py +5 -5
- package/src/r23h_shebang_mismatch.py +6 -5
- package/src/r23i_auto_deploy_ignored.py +5 -6
- package/src/r23j_global_install.py +5 -6
- package/src/r23k_script_duplicates_skill.py +7 -6
- package/src/r23l_resource_collision.py +7 -6
- package/src/r23m_message_duplicate.py +6 -5
- package/src/r24_stale_memory.py +4 -9
- package/src/r25_nora_maria_read_only.py +5 -10
- package/src/r34_identity_coherence.py +6 -13
- package/src/r_catalog.py +3 -7
- package/src/resonance_map.py +13 -13
- package/src/runtime_power.py +29 -80
- package/src/script_registry.py +236 -6
- package/src/scripts/check-context.py +8 -25
- package/src/scripts/deep-sleep/extract.py +6 -10
- package/src/scripts/nexo-auto-update.py +27 -4
- package/src/scripts/nexo-catchup.py +9 -19
- package/src/scripts/nexo-cognitive-decay.py +26 -3
- package/src/scripts/nexo-daily-self-audit.py +50 -51
- package/src/scripts/nexo-email-migrate-config.py +30 -11
- package/src/scripts/nexo-email-monitor.py +97 -238
- package/src/scripts/nexo-followup-runner.py +70 -133
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-immune.py +6 -31
- package/src/scripts/nexo-impact-scorer.py +27 -4
- package/src/scripts/nexo-learning-housekeep.py +26 -3
- package/src/scripts/nexo-learning-validator.py +34 -32
- package/src/scripts/nexo-migrate.py +28 -12
- package/src/scripts/nexo-morning-agent.py +9 -23
- package/src/scripts/nexo-outcome-checker.py +27 -4
- package/src/scripts/nexo-postmortem-consolidator.py +30 -62
- package/src/scripts/nexo-pre-commit.py +28 -0
- package/src/scripts/nexo-proactive-dashboard.py +27 -0
- package/src/scripts/nexo-reflection.py +33 -3
- package/src/scripts/nexo-runtime-preflight.py +27 -2
- package/src/scripts/nexo-send-reply.py +10 -8
- package/src/scripts/nexo-sleep.py +11 -25
- package/src/scripts/nexo-synthesis.py +7 -40
- package/src/scripts/nexo-watchdog-smoke.py +30 -1
- package/src/scripts/nexo-watchdog.sh +23 -17
- package/src/scripts/phase_guardian_analysis.py +27 -4
- package/src/server.py +14 -3
- package/src/storage_router.py +8 -6
- package/src/tools_drive.py +5 -13
- package/src/tools_guardian.py +3 -4
- package/src/tools_menu.py +2 -2
- package/src/tools_reminders_crud.py +17 -0
- package/src/tools_sessions.py +1 -4
- package/src/user_context.py +3 -6
- package/src/user_data_portability.py +31 -23
- package/templates/CLAUDE.md.template +11 -3
- package/templates/CODEX.AGENTS.md.template +11 -3
- package/templates/core-prompts/catchup-assessment.md +19 -0
- package/templates/core-prompts/check-context.md +24 -0
- package/templates/core-prompts/daily-self-audit.md +42 -0
- package/templates/core-prompts/daily-synthesis.md +40 -0
- package/templates/core-prompts/deep-sleep-extract-json-output.md +8 -0
- package/templates/core-prompts/drive-signal-classifier-system.md +4 -0
- package/templates/core-prompts/drive-signal-classifier-user.md +6 -0
- package/templates/core-prompts/email-monitor.md +202 -0
- package/templates/core-prompts/enforcement-classifier-retry.md +1 -0
- package/templates/core-prompts/enforcement-classifier-strict.md +1 -0
- package/templates/core-prompts/evolution-public-contribution.md +32 -0
- package/templates/core-prompts/evolution-public-pr-review.md +38 -0
- package/templates/core-prompts/evolution-weekly.md +71 -0
- package/templates/core-prompts/followup-runner-operator-attention-context.md +4 -0
- package/templates/core-prompts/followup-runner-operator-attention-question.md +1 -0
- package/templates/core-prompts/followup-runner.md +74 -0
- package/templates/core-prompts/immune-triage.md +31 -0
- package/templates/core-prompts/interactive-startup.md +1 -0
- package/templates/core-prompts/json-object-only.md +1 -0
- package/templates/core-prompts/learning-validator.md +25 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -0
- package/templates/core-prompts/morning-agent.md +23 -0
- package/templates/core-prompts/postmortem-consolidator.md +60 -0
- package/templates/core-prompts/r-catalog.md +1 -0
- package/templates/core-prompts/r14-correction-learning-injection.md +1 -0
- package/templates/core-prompts/r14-correction-learning-question.md +1 -0
- package/templates/core-prompts/r15-project-context-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-question.md +1 -0
- package/templates/core-prompts/r17-promise-debt-injection.md +1 -0
- package/templates/core-prompts/r17-promise-debt-question.md +1 -0
- package/templates/core-prompts/r18-followup-autocomplete-injection.md +3 -0
- package/templates/core-prompts/r19-project-grep-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-question.md +1 -0
- package/templates/core-prompts/r21-legacy-path-injection.md +1 -0
- package/templates/core-prompts/r22-personal-script-injection.md +1 -0
- package/templates/core-prompts/r23-ssh-without-atlas-injection.md +1 -0
- package/templates/core-prompts/r23b-deploy-vhost-injection.md +1 -0
- package/templates/core-prompts/r23c-cwd-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23d-chown-chmod-recursive-injection.md +1 -0
- package/templates/core-prompts/r23e-force-push-main-injection.md +1 -0
- package/templates/core-prompts/r23f-db-no-where-injection.md +1 -0
- package/templates/core-prompts/r23g-secrets-in-output-injection.md +1 -0
- package/templates/core-prompts/r23h-shebang-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23i-auto-deploy-ignored-injection.md +1 -0
- package/templates/core-prompts/r23j-global-install-injection.md +1 -0
- package/templates/core-prompts/r23k-script-duplicates-skill-injection.md +1 -0
- package/templates/core-prompts/r23l-resource-collision-injection.md +1 -0
- package/templates/core-prompts/r23m-message-duplicate-injection.md +1 -0
- package/templates/core-prompts/r24-stale-memory-injection.md +1 -0
- package/templates/core-prompts/r25-read-only-host-injection.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-probe.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-question.md +1 -0
- package/templates/core-prompts/sleep.md +25 -0
- package/templates/email-template.md +55 -0
- package/templates/nexo_helper.py +31 -13
- package/templates/plugin-template.py +3 -3
- package/templates/skill-template.md +2 -1
|
@@ -6,6 +6,8 @@ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
|
6
6
|
HELPER=""
|
|
7
7
|
if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
|
|
8
8
|
HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
|
|
9
|
+
elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
|
|
10
|
+
HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
|
|
9
11
|
elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
|
|
10
12
|
HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
|
|
11
13
|
fi
|
package/src/hooks/inbox-hook.sh
CHANGED
|
@@ -28,8 +28,12 @@ echo "$NOW" > "$DEBOUNCE_FILE"
|
|
|
28
28
|
|
|
29
29
|
# 4. Find NEXO SID mapped to this Claude session_id
|
|
30
30
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
DATA_DIR="$NEXO_HOME/runtime/data"
|
|
32
|
+
if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
|
|
33
|
+
DATA_DIR="$NEXO_HOME/data"
|
|
34
|
+
fi
|
|
35
|
+
DB="$DATA_DIR/nexo.db"
|
|
36
|
+
mkdir -p "$DATA_DIR"
|
|
33
37
|
[ -f "$DB" ] || exit 0
|
|
34
38
|
|
|
35
39
|
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE (external_session_id = '${CLAUDE_SID}' OR claude_session_id = '${CLAUDE_SID}') AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
7
7
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
DATA_DIR="$NEXO_HOME/runtime/data"
|
|
9
|
+
if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
|
|
10
|
+
DATA_DIR="$NEXO_HOME/data"
|
|
11
|
+
fi
|
|
12
|
+
OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
|
|
13
|
+
if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
|
|
14
|
+
OPERATIONS_DIR="$NEXO_HOME/operations"
|
|
15
|
+
fi
|
|
16
|
+
NEXO_DB="$DATA_DIR/nexo.db"
|
|
17
|
+
mkdir -p "$DATA_DIR" "$OPERATIONS_DIR"
|
|
10
18
|
TODAY=$(date +%Y-%m-%d)
|
|
11
|
-
LOG_FILE="$
|
|
19
|
+
LOG_FILE="$OPERATIONS_DIR/tool-logs/${TODAY}.jsonl"
|
|
12
20
|
LOG_LINES=0
|
|
13
21
|
if [ -f "$LOG_FILE" ]; then
|
|
14
22
|
LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
|
@@ -120,7 +128,7 @@ if [ -f "$NEXO_DB" ]; then
|
|
|
120
128
|
BLOCK="$BLOCK\n**Session tasks so far:** $TASKS_SEEN"
|
|
121
129
|
fi
|
|
122
130
|
|
|
123
|
-
BLOCK="$BLOCK\n**Tool logs:** ${
|
|
131
|
+
BLOCK="$BLOCK\n**Tool logs:** ${OPERATIONS_DIR}/tool-logs/${TODAY}.jsonl ($LOG_LINES entries)"
|
|
124
132
|
BLOCK="$BLOCK\n\n**POST-COMPACTION INSTRUCTIONS:**"
|
|
125
133
|
BLOCK="$BLOCK\n1. Call nexo_heartbeat with the SID above to reconnect with the session"
|
|
126
134
|
BLOCK="$BLOCK\n2. If you need specific lost data, query tool logs with jq"
|
package/src/hooks/pre-compact.sh
CHANGED
|
@@ -7,10 +7,18 @@ set -uo pipefail
|
|
|
7
7
|
|
|
8
8
|
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
9
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
DATA_DIR="$NEXO_HOME/runtime/data"
|
|
11
|
+
if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
|
|
12
|
+
DATA_DIR="$NEXO_HOME/data"
|
|
13
|
+
fi
|
|
14
|
+
OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
|
|
15
|
+
if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
|
|
16
|
+
OPERATIONS_DIR="$NEXO_HOME/operations"
|
|
17
|
+
fi
|
|
18
|
+
NEXO_DB="$DATA_DIR/nexo.db"
|
|
19
|
+
mkdir -p "$DATA_DIR" "$OPERATIONS_DIR"
|
|
12
20
|
TODAY=$(date +%Y-%m-%d)
|
|
13
|
-
LOG_FILE="$
|
|
21
|
+
LOG_FILE="$OPERATIONS_DIR/tool-logs/${TODAY}.jsonl"
|
|
14
22
|
LOG_LINES=0
|
|
15
23
|
if [ -f "$LOG_FILE" ]; then
|
|
16
24
|
LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
|
@@ -164,6 +172,6 @@ fi
|
|
|
164
172
|
|
|
165
173
|
cat << HOOKEOF
|
|
166
174
|
{
|
|
167
|
-
"systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${
|
|
175
|
+
"systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${OPERATIONS_DIR}/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
|
|
168
176
|
}
|
|
169
177
|
HOOKEOF
|
|
@@ -15,10 +15,12 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
18
|
+
import paths
|
|
19
|
+
|
|
18
20
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
DB_PATH =
|
|
21
|
+
_cognitive_dir = paths.cognitive_dir()
|
|
22
|
+
_cognitive_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
DB_PATH = str(_cognitive_dir / "cognitive.db")
|
|
22
24
|
BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
|
|
23
25
|
|
|
24
26
|
MODELS = {
|
package/src/nexo_migrate.py
CHANGED
|
@@ -30,6 +30,8 @@ import time
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
from typing import Callable
|
|
32
32
|
|
|
33
|
+
import paths
|
|
34
|
+
|
|
33
35
|
|
|
34
36
|
def _nexo_home() -> Path:
|
|
35
37
|
env = os.environ.get("NEXO_HOME")
|
|
@@ -42,7 +44,7 @@ def _db_path() -> Path:
|
|
|
42
44
|
env = os.environ.get("NEXO_DB_PATH")
|
|
43
45
|
if env:
|
|
44
46
|
return Path(env)
|
|
45
|
-
return
|
|
47
|
+
return paths.resolve_db_path()
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def _structure_version_path() -> Path:
|
package/src/plugin_loader.py
CHANGED
|
@@ -8,6 +8,7 @@ import signal
|
|
|
8
8
|
import sys
|
|
9
9
|
import time
|
|
10
10
|
|
|
11
|
+
import paths
|
|
11
12
|
from db import get_db
|
|
12
13
|
from fastmcp.tools import Tool
|
|
13
14
|
|
|
@@ -25,9 +26,9 @@ except ModuleNotFoundError as exc:
|
|
|
25
26
|
SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
26
27
|
PLUGINS_DIR = os.path.join(SERVER_DIR, "plugins")
|
|
27
28
|
|
|
28
|
-
# Personal plugins directory: NEXO_HOME/plugins/ (env var, defaults to ~/.nexo/)
|
|
29
|
+
# Personal plugins directory: NEXO_HOME/personal/plugins/ (env var, defaults to ~/.nexo/)
|
|
29
30
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
30
|
-
PERSONAL_PLUGINS_DIR =
|
|
31
|
+
PERSONAL_PLUGINS_DIR = str(paths.personal_plugins_dir())
|
|
31
32
|
|
|
32
33
|
PLUGIN_LOAD_TIMEOUT = 10 # seconds per plugin
|
|
33
34
|
|
|
@@ -141,14 +142,22 @@ def load_all_plugins(mcp) -> int:
|
|
|
141
142
|
continue
|
|
142
143
|
plugin_map[f] = (PLUGINS_DIR, "repo")
|
|
143
144
|
|
|
144
|
-
# 2. Personal plugins
|
|
145
|
+
# 2. Personal plugins. Never let a personal file shadow a packaged core
|
|
146
|
+
# plugin at startup: creation already rejects that collision, and keeping
|
|
147
|
+
# the repo plugin canonical avoids hybrid installs with two sources of truth.
|
|
145
148
|
if os.path.isdir(PERSONAL_PLUGINS_DIR):
|
|
146
149
|
for f in sorted(os.listdir(PERSONAL_PLUGINS_DIR)):
|
|
147
150
|
if f.endswith(".py") and f != "__init__.py":
|
|
148
151
|
if is_duplicate_artifact_name(os.path.join(PERSONAL_PLUGINS_DIR, f)):
|
|
149
152
|
continue
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
if f in plugin_map:
|
|
154
|
+
print(
|
|
155
|
+
f"[PLUGIN SHADOW SKIP] {f}: personal plugin collides with packaged core filename; "
|
|
156
|
+
"keeping core plugin canonical",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
plugin_map[f] = (PERSONAL_PLUGINS_DIR, "personal")
|
|
152
161
|
|
|
153
162
|
# Load all in sorted order
|
|
154
163
|
for f in sorted(plugin_map):
|
|
@@ -28,10 +28,13 @@ import time
|
|
|
28
28
|
import math
|
|
29
29
|
import subprocess
|
|
30
30
|
from datetime import datetime, timedelta
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import paths
|
|
31
34
|
from db import get_db
|
|
32
35
|
|
|
33
36
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
34
|
-
ADAPTIVE_STATE_FILE =
|
|
37
|
+
ADAPTIVE_STATE_FILE = str(paths.brain_dir() / "adaptive_state.json")
|
|
35
38
|
|
|
36
39
|
# Mode definitions
|
|
37
40
|
MODES = {
|
package/src/plugins/backup.py
CHANGED
|
@@ -14,12 +14,12 @@ import shutil
|
|
|
14
14
|
import sqlite3
|
|
15
15
|
import threading
|
|
16
16
|
import time
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
|
|
19
|
+
import paths
|
|
18
20
|
from db import get_db
|
|
19
21
|
|
|
20
22
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
21
|
-
DB_PATH = os.path.join(NEXO_HOME, "data", "nexo.db")
|
|
22
|
-
BACKUP_DIR = os.path.join(NEXO_HOME, "backups")
|
|
23
23
|
|
|
24
24
|
RETENTION_DAYS = 7
|
|
25
25
|
|
|
@@ -66,6 +66,14 @@ def _reset_rate_limit_state_for_tests() -> None:
|
|
|
66
66
|
_last_call_ts[key] = 0.0
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _db_path() -> Path:
|
|
70
|
+
return paths.db_path()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _backup_dir() -> Path:
|
|
74
|
+
return paths.backups_dir()
|
|
75
|
+
|
|
76
|
+
|
|
69
77
|
def handle_backup_now() -> str:
|
|
70
78
|
"""Create an immediate backup of the NEXO database.
|
|
71
79
|
|
|
@@ -75,14 +83,15 @@ def handle_backup_now() -> str:
|
|
|
75
83
|
if err is not None:
|
|
76
84
|
return err
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
backup_dir = _backup_dir()
|
|
87
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
79
88
|
timestamp = time.strftime("%Y-%m-%d-%H%M")
|
|
80
|
-
dest =
|
|
89
|
+
dest = backup_dir / f"nexo-{timestamp}.db"
|
|
81
90
|
|
|
82
91
|
# Use SQLite backup API for consistency
|
|
83
|
-
src_conn = sqlite3.connect(
|
|
92
|
+
src_conn = sqlite3.connect(str(_db_path()))
|
|
84
93
|
try:
|
|
85
|
-
dst_conn = sqlite3.connect(dest)
|
|
94
|
+
dst_conn = sqlite3.connect(str(dest))
|
|
86
95
|
try:
|
|
87
96
|
src_conn.backup(dst_conn)
|
|
88
97
|
finally:
|
|
@@ -90,16 +99,17 @@ def handle_backup_now() -> str:
|
|
|
90
99
|
finally:
|
|
91
100
|
src_conn.close()
|
|
92
101
|
|
|
93
|
-
size_kb =
|
|
102
|
+
size_kb = dest.stat().st_size / 1024
|
|
94
103
|
_cleanup_old()
|
|
95
|
-
return f"Backup created: {
|
|
104
|
+
return f"Backup created: {dest.name} ({size_kb:.0f} KB)"
|
|
96
105
|
|
|
97
106
|
|
|
98
107
|
def handle_backup_list() -> str:
|
|
99
108
|
"""List available backups with dates and sizes."""
|
|
100
|
-
|
|
109
|
+
backup_dir = _backup_dir()
|
|
110
|
+
if not backup_dir.is_dir():
|
|
101
111
|
return "No backups."
|
|
102
|
-
files = sorted(glob.glob(
|
|
112
|
+
files = sorted(glob.glob(str(backup_dir / "nexo-*.db")), reverse=True)
|
|
103
113
|
if not files:
|
|
104
114
|
return "No backups."
|
|
105
115
|
lines = [f"BACKUPS ({len(files)}):"]
|
|
@@ -127,15 +137,16 @@ def handle_backup_restore(filename: str) -> str:
|
|
|
127
137
|
if err is not None:
|
|
128
138
|
return err
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
backup_dir = _backup_dir()
|
|
141
|
+
src = backup_dir / filename
|
|
142
|
+
if not src.is_file():
|
|
132
143
|
return f"Backup not found: {filename}"
|
|
133
144
|
|
|
134
145
|
# Create safety backup first
|
|
135
|
-
safety =
|
|
136
|
-
src_conn = sqlite3.connect(
|
|
146
|
+
safety = backup_dir / f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db"
|
|
147
|
+
src_conn = sqlite3.connect(str(_db_path()))
|
|
137
148
|
try:
|
|
138
|
-
dst_conn = sqlite3.connect(safety)
|
|
149
|
+
dst_conn = sqlite3.connect(str(safety))
|
|
139
150
|
try:
|
|
140
151
|
src_conn.backup(dst_conn)
|
|
141
152
|
finally:
|
|
@@ -144,9 +155,9 @@ def handle_backup_restore(filename: str) -> str:
|
|
|
144
155
|
src_conn.close()
|
|
145
156
|
|
|
146
157
|
# Restore
|
|
147
|
-
restore_conn = sqlite3.connect(src)
|
|
158
|
+
restore_conn = sqlite3.connect(str(src))
|
|
148
159
|
try:
|
|
149
|
-
target_conn = sqlite3.connect(
|
|
160
|
+
target_conn = sqlite3.connect(str(_db_path()))
|
|
150
161
|
try:
|
|
151
162
|
restore_conn.backup(target_conn)
|
|
152
163
|
finally:
|
|
@@ -163,7 +174,7 @@ def handle_backup_restore(filename: str) -> str:
|
|
|
163
174
|
pass
|
|
164
175
|
db._shared_conn = None
|
|
165
176
|
|
|
166
|
-
return f"DB restaurada desde {filename}. Safety backup: {
|
|
177
|
+
return f"DB restaurada desde {filename}. Safety backup: {safety.name}"
|
|
167
178
|
|
|
168
179
|
|
|
169
180
|
def _cleanup_old():
|
|
@@ -173,12 +184,13 @@ def _cleanup_old():
|
|
|
173
184
|
`nexo-pre-restore-*.db` safety snapshots created by handle_backup_restore.
|
|
174
185
|
Failures are swallowed — housekeeping must never interrupt the caller.
|
|
175
186
|
"""
|
|
176
|
-
|
|
187
|
+
backup_dir = _backup_dir()
|
|
188
|
+
if not backup_dir.is_dir():
|
|
177
189
|
return
|
|
178
190
|
cutoff = time.time() - (RETENTION_DAYS * 86400)
|
|
179
191
|
# glob `nexo-*.db` matches both the hourly pattern and pre-restore
|
|
180
192
|
# snapshots, so a single loop prunes both with a single pass.
|
|
181
|
-
for f in glob.glob(
|
|
193
|
+
for f in glob.glob(str(backup_dir / "nexo-*.db")):
|
|
182
194
|
try:
|
|
183
195
|
if os.path.getmtime(f) < cutoff:
|
|
184
196
|
os.remove(f)
|
package/src/plugins/evolution.py
CHANGED
|
@@ -42,6 +42,8 @@ def handle_evolution_status() -> str:
|
|
|
42
42
|
|
|
43
43
|
from user_context import get_context
|
|
44
44
|
lines = [f"{get_context().assistant_name} EVOLUTION STATUS:"]
|
|
45
|
+
if objective and objective.get("evolution_enabled") is False:
|
|
46
|
+
lines.append(f" Disabled: {objective.get('disabled_reason', 'unknown')}")
|
|
45
47
|
has_output = False
|
|
46
48
|
for key, label in CANONICAL_DIMENSIONS.items():
|
|
47
49
|
m = metrics.get(key)
|
|
@@ -11,6 +11,7 @@ import cognitive
|
|
|
11
11
|
import claim_graph
|
|
12
12
|
import compaction_memory
|
|
13
13
|
import media_memory
|
|
14
|
+
import paths
|
|
14
15
|
import user_state_model
|
|
15
16
|
from db import get_db
|
|
16
17
|
from memory_backends import get_backend, list_backends
|
|
@@ -69,7 +70,11 @@ def handle_memory_export(format: str = "markdown", output_dir: str = "") -> str:
|
|
|
69
70
|
return "ERROR: only markdown export is supported for now."
|
|
70
71
|
|
|
71
72
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
72
|
-
root =
|
|
73
|
+
root = (
|
|
74
|
+
Path(output_dir).expanduser()
|
|
75
|
+
if output_dir.strip()
|
|
76
|
+
else (paths.exports_dir() / "memory" / stamp)
|
|
77
|
+
)
|
|
73
78
|
root.mkdir(parents=True, exist_ok=True)
|
|
74
79
|
|
|
75
80
|
conn = get_db()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""NEXO Personal Plugins — scaffold persistent MCP tools in NEXO_HOME/plugins."""
|
|
1
|
+
"""NEXO Personal Plugins — scaffold persistent MCP tools in NEXO_HOME/personal/plugins."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -16,11 +16,15 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _plugins_dir() -> Path:
|
|
19
|
-
path = paths.
|
|
19
|
+
path = paths.personal_plugins_dir()
|
|
20
20
|
path.mkdir(parents=True, exist_ok=True)
|
|
21
21
|
return path
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _core_plugin_path(filename: str) -> Path:
|
|
25
|
+
return paths.core_plugins_dir() / filename
|
|
26
|
+
|
|
27
|
+
|
|
24
28
|
def _safe_slug(value: str) -> str:
|
|
25
29
|
chars: list[str] = []
|
|
26
30
|
for ch in str(value or "").lower():
|
|
@@ -68,11 +72,11 @@ def _render_plugin_template(*, plugin_stem: str, tool_name: str, description: st
|
|
|
68
72
|
content = content.replace("handle_example_tool", handler_name)
|
|
69
73
|
content = content.replace("nexo_example_tool", tool_name)
|
|
70
74
|
content = content.replace(
|
|
71
|
-
"Personal plugin scaffold created. Edit this handler in NEXO_HOME/plugins.",
|
|
75
|
+
"Personal plugin scaffold created. Edit this handler in NEXO_HOME/personal/plugins.",
|
|
72
76
|
description or f"Personal plugin scaffold for {plugin_stem}.",
|
|
73
77
|
)
|
|
74
78
|
content = content.replace(
|
|
75
|
-
"Example personal MCP tool scaffold. Edit it in NEXO_HOME/plugins.",
|
|
79
|
+
"Example personal MCP tool scaffold. Edit it in NEXO_HOME/personal/plugins.",
|
|
76
80
|
description or f"Personal MCP tool scaffold for {plugin_stem}.",
|
|
77
81
|
)
|
|
78
82
|
return content
|
|
@@ -86,15 +90,21 @@ def handle_personal_plugin_create(
|
|
|
86
90
|
script_runtime: str = "python",
|
|
87
91
|
force: bool = False,
|
|
88
92
|
) -> str:
|
|
89
|
-
"""Create a personal MCP plugin scaffold in NEXO_HOME/plugins.
|
|
93
|
+
"""Create a personal MCP plugin scaffold in NEXO_HOME/personal/plugins.
|
|
90
94
|
|
|
91
|
-
Optionally also creates a companion script in NEXO_HOME/scripts.
|
|
95
|
+
Optionally also creates a companion script in NEXO_HOME/personal/scripts.
|
|
92
96
|
"""
|
|
93
97
|
init_db()
|
|
94
98
|
plugin_stem = _safe_slug(name)
|
|
95
99
|
filename = f"{plugin_stem}.py"
|
|
96
100
|
tool_name = (tool_name or f"nexo_{plugin_stem.replace('-', '_')}").strip()
|
|
97
101
|
plugin_path = _plugins_dir() / filename
|
|
102
|
+
core_plugin_path = _core_plugin_path(filename)
|
|
103
|
+
if core_plugin_path.exists() and not force:
|
|
104
|
+
return json.dumps({
|
|
105
|
+
"ok": False,
|
|
106
|
+
"error": f"Personal plugin name collides with a core plugin identity: {filename}",
|
|
107
|
+
}, ensure_ascii=False)
|
|
98
108
|
if plugin_path.exists() and not force:
|
|
99
109
|
return json.dumps({
|
|
100
110
|
"ok": False,
|
|
@@ -137,6 +147,6 @@ TOOLS = [
|
|
|
137
147
|
(
|
|
138
148
|
handle_personal_plugin_create,
|
|
139
149
|
"nexo_personal_plugin_create",
|
|
140
|
-
"Create a persistent personal MCP plugin scaffold in NEXO_HOME/plugins, optionally with a companion script in NEXO_HOME/scripts.",
|
|
150
|
+
"Create a persistent personal MCP plugin scaffold in NEXO_HOME/personal/plugins, optionally with a companion script in NEXO_HOME/personal/scripts.",
|
|
141
151
|
),
|
|
142
152
|
]
|
|
@@ -8,8 +8,13 @@ from script_registry import (
|
|
|
8
8
|
classify_scripts_dir,
|
|
9
9
|
create_script,
|
|
10
10
|
ensure_personal_schedules,
|
|
11
|
+
get_automation_status,
|
|
12
|
+
list_operator_automations,
|
|
11
13
|
reconcile_personal_scripts,
|
|
12
14
|
remove_personal_script,
|
|
15
|
+
set_automation_enabled,
|
|
16
|
+
set_automation_instructions,
|
|
17
|
+
set_automation_schedule,
|
|
13
18
|
sync_personal_scripts,
|
|
14
19
|
unschedule_personal_script,
|
|
15
20
|
)
|
|
@@ -98,23 +103,79 @@ def handle_personal_script_remove(name: str, keep_file: bool = False) -> str:
|
|
|
98
103
|
return json.dumps(remove_personal_script(name, keep_file=keep_file), ensure_ascii=False)
|
|
99
104
|
|
|
100
105
|
|
|
106
|
+
def handle_automations_list(include_all: bool = False) -> str:
|
|
107
|
+
init_db()
|
|
108
|
+
return json.dumps({"ok": True, "automations": list_operator_automations(include_all=include_all)}, ensure_ascii=False)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_automation_status(name: str) -> str:
|
|
112
|
+
init_db()
|
|
113
|
+
return json.dumps(get_automation_status(name), ensure_ascii=False)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def handle_automation_enable(name: str) -> str:
|
|
117
|
+
init_db()
|
|
118
|
+
return json.dumps(set_automation_enabled(name, True), ensure_ascii=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_automation_disable(name: str) -> str:
|
|
122
|
+
init_db()
|
|
123
|
+
return json.dumps(set_automation_enabled(name, False), ensure_ascii=False)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def handle_automation_instructions(name: str, text: str = "", clear: bool = False) -> str:
|
|
127
|
+
init_db()
|
|
128
|
+
return json.dumps(set_automation_instructions(name, "" if clear else text), ensure_ascii=False)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def handle_automation_schedule(
|
|
132
|
+
name: str,
|
|
133
|
+
every_seconds: int = 0,
|
|
134
|
+
daily_at: str = "",
|
|
135
|
+
clear: bool = False,
|
|
136
|
+
) -> str:
|
|
137
|
+
init_db()
|
|
138
|
+
interval_seconds = int(every_seconds or 0) or None
|
|
139
|
+
return json.dumps(
|
|
140
|
+
set_automation_schedule(
|
|
141
|
+
name,
|
|
142
|
+
interval_seconds=interval_seconds,
|
|
143
|
+
daily_at=str(daily_at or "").strip() or None,
|
|
144
|
+
clear=bool(clear),
|
|
145
|
+
),
|
|
146
|
+
ensure_ascii=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
101
150
|
TOOLS = [
|
|
102
151
|
(handle_personal_scripts_sync, "nexo_personal_scripts_sync",
|
|
103
152
|
"Sync personal scripts and personal cron schedules from filesystem and LaunchAgents into the registry."),
|
|
104
153
|
(handle_personal_scripts_classify, "nexo_personal_scripts_classify",
|
|
105
|
-
"Classify files in NEXO_HOME/scripts into personal, core, ignored, and non-script buckets."),
|
|
154
|
+
"Classify files in NEXO_HOME/personal/scripts into personal, core, ignored, and non-script buckets."),
|
|
106
155
|
(handle_personal_scripts_list, "nexo_personal_scripts_list",
|
|
107
156
|
"List personal scripts known to NEXO, optionally including attached schedules."),
|
|
108
157
|
(handle_personal_script_create, "nexo_personal_script_create",
|
|
109
|
-
"Create a new personal script in NEXO_HOME/scripts, register it, and optionally attach a schedule."),
|
|
158
|
+
"Create a new personal script in NEXO_HOME/personal/scripts, register it, and optionally attach a schedule."),
|
|
110
159
|
(handle_personal_script_schedules, "nexo_personal_script_schedules",
|
|
111
160
|
"List registered personal script schedules."),
|
|
112
161
|
(handle_personal_scripts_reconcile, "nexo_personal_scripts_reconcile",
|
|
113
|
-
"Classify, sync, and ensure declared personal schedules so NEXO_HOME/scripts and personal crons stay aligned."),
|
|
162
|
+
"Classify, sync, and ensure declared personal schedules so NEXO_HOME/personal/scripts and personal crons stay aligned."),
|
|
114
163
|
(handle_personal_scripts_ensure_schedules, "nexo_personal_scripts_ensure_schedules",
|
|
115
164
|
"Create or repair personal script schedules declared in inline metadata."),
|
|
116
165
|
(handle_personal_script_unschedule, "nexo_personal_script_unschedule",
|
|
117
166
|
"Remove all personal schedules attached to a script without touching core crons."),
|
|
118
167
|
(handle_personal_script_remove, "nexo_personal_script_remove",
|
|
119
168
|
"Remove a personal script from the registry and optionally delete its file after unscheduling it."),
|
|
169
|
+
(handle_automations_list, "nexo_automations_list",
|
|
170
|
+
"List the operator-facing automations NEXO Desktop manages directly, with optional support/debug widening."),
|
|
171
|
+
(handle_automation_status, "nexo_automation_status",
|
|
172
|
+
"Read the composed runtime status for one automation, including availability, schedule, and operator overrides."),
|
|
173
|
+
(handle_automation_enable, "nexo_automation_enable",
|
|
174
|
+
"Enable one operator-facing automation."),
|
|
175
|
+
(handle_automation_disable, "nexo_automation_disable",
|
|
176
|
+
"Disable one operator-facing automation."),
|
|
177
|
+
(handle_automation_instructions, "nexo_automation_instructions",
|
|
178
|
+
"Set or clear operator-side extra instructions for one automation without editing the core prompt."),
|
|
179
|
+
(handle_automation_schedule, "nexo_automation_schedule",
|
|
180
|
+
"Set or clear the cadence override for one operator-facing automation."),
|
|
120
181
|
]
|
|
@@ -109,8 +109,8 @@
|
|
|
109
109
|
"aliases": [],
|
|
110
110
|
"metadata": {
|
|
111
111
|
"old": "~/claude/hooks",
|
|
112
|
-
"canonical": "~/.nexo/hooks",
|
|
113
|
-
"note": "Pre-v6 runtime lived under ~/claude/*;
|
|
112
|
+
"canonical": "~/.nexo/personal/hooks",
|
|
113
|
+
"note": "Pre-v6 runtime lived under ~/claude/*; operator-owned hooks now live under ~/.nexo/personal/*."
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
{
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"aliases": [],
|
|
120
120
|
"metadata": {
|
|
121
121
|
"old": "~/claude/scripts",
|
|
122
|
-
"canonical": "~/.nexo/scripts"
|
|
122
|
+
"canonical": "~/.nexo/personal/scripts"
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
{
|
|
@@ -128,7 +128,70 @@
|
|
|
128
128
|
"aliases": [],
|
|
129
129
|
"metadata": {
|
|
130
130
|
"old": "~/claude/brain",
|
|
131
|
-
"canonical": "~/.nexo/brain"
|
|
131
|
+
"canonical": "~/.nexo/personal/brain"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"type": "legacy_path",
|
|
136
|
+
"name": "flat_nexo_scripts_to_personal",
|
|
137
|
+
"aliases": [],
|
|
138
|
+
"metadata": {
|
|
139
|
+
"old": "~/.nexo/scripts",
|
|
140
|
+
"canonical": "~/.nexo/personal/scripts"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"type": "legacy_path",
|
|
145
|
+
"name": "flat_nexo_brain_to_personal",
|
|
146
|
+
"aliases": [],
|
|
147
|
+
"metadata": {
|
|
148
|
+
"old": "~/.nexo/brain",
|
|
149
|
+
"canonical": "~/.nexo/personal/brain"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"type": "legacy_path",
|
|
154
|
+
"name": "flat_nexo_config_to_personal",
|
|
155
|
+
"aliases": [],
|
|
156
|
+
"metadata": {
|
|
157
|
+
"old": "~/.nexo/config",
|
|
158
|
+
"canonical": "~/.nexo/personal/config"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"type": "legacy_path",
|
|
163
|
+
"name": "flat_nexo_data_to_runtime",
|
|
164
|
+
"aliases": [],
|
|
165
|
+
"metadata": {
|
|
166
|
+
"old": "~/.nexo/data",
|
|
167
|
+
"canonical": "~/.nexo/runtime/data"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"type": "legacy_path",
|
|
172
|
+
"name": "flat_nexo_logs_to_runtime",
|
|
173
|
+
"aliases": [],
|
|
174
|
+
"metadata": {
|
|
175
|
+
"old": "~/.nexo/logs",
|
|
176
|
+
"canonical": "~/.nexo/runtime/logs"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"type": "legacy_path",
|
|
181
|
+
"name": "flat_nexo_operations_to_runtime",
|
|
182
|
+
"aliases": [],
|
|
183
|
+
"metadata": {
|
|
184
|
+
"old": "~/.nexo/operations",
|
|
185
|
+
"canonical": "~/.nexo/runtime/operations"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"type": "legacy_path",
|
|
190
|
+
"name": "flat_nexo_exports_to_runtime",
|
|
191
|
+
"aliases": [],
|
|
192
|
+
"metadata": {
|
|
193
|
+
"old": "~/.nexo/exports",
|
|
194
|
+
"canonical": "~/.nexo/runtime/exports"
|
|
132
195
|
}
|
|
133
196
|
},
|
|
134
197
|
{
|