nexo-brain 2.5.1 → 2.6.1
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 +33 -0
- package/.mcp.json +12 -0
- package/README.md +38 -26
- package/bin/nexo-brain.js +35 -32
- package/hooks/hooks.json +14 -0
- package/package.json +11 -4
- package/src/auto_update.py +44 -1
- package/src/cli.py +388 -23
- package/src/cron_recovery.py +283 -0
- package/src/crons/manifest.json +79 -21
- package/src/crons/sync.py +136 -31
- package/src/db/__init__.py +11 -0
- package/src/db/_personal_scripts.py +548 -0
- package/src/db/_schema.py +44 -1
- package/src/doctor/providers/runtime.py +272 -75
- package/src/evolution_cycle.py +4 -1
- package/src/nexo.db +0 -0
- package/src/plugins/personal_scripts.py +117 -0
- package/src/plugins/schedule.py +116 -27
- package/src/script_registry.py +877 -28
- package/src/scripts/nexo-catchup.py +74 -109
- package/src/scripts/nexo-evolution-run.py +37 -12
- package/src/scripts/nexo-watchdog.sh +242 -54
- package/src/tools_learnings.py +8 -0
- package/templates/launchagents/com.nexo.catchup.plist +7 -6
- package/templates/script-template.py +3 -0
- package/templates/script-template.sh +13 -0
- package/src/scripts/nexo-day-orchestrator.sh +0 -139
package/src/plugins/schedule.py
CHANGED
|
@@ -6,7 +6,11 @@ import platform
|
|
|
6
6
|
import subprocess
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from db import
|
|
9
|
+
from db import (
|
|
10
|
+
init_db, cron_runs_recent, cron_runs_summary,
|
|
11
|
+
upsert_personal_script, register_personal_script_schedule,
|
|
12
|
+
)
|
|
13
|
+
from script_registry import PERSONAL_SCHEDULE_MANAGED_ENV, parse_inline_metadata, classify_runtime
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
def handle_schedule_status(hours: int = 24, cron_id: str = '') -> str:
|
|
@@ -47,7 +51,7 @@ def handle_schedule_status(hours: int = 24, cron_id: str = '') -> str:
|
|
|
47
51
|
|
|
48
52
|
def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
|
|
49
53
|
interval_seconds: int = 0, description: str = '',
|
|
50
|
-
script_type: str = '
|
|
54
|
+
script_type: str = 'auto') -> str:
|
|
51
55
|
"""Add a new personal cron job. Generates and installs the LaunchAgent (macOS) or systemd timer (Linux).
|
|
52
56
|
|
|
53
57
|
Args:
|
|
@@ -56,7 +60,7 @@ def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
|
|
|
56
60
|
schedule: Time-based schedule as 'HH:MM' (daily) or 'HH:MM:weekday' (e.g. '08:00:1' for Monday 8AM). Mutually exclusive with interval_seconds.
|
|
57
61
|
interval_seconds: Run every N seconds (e.g. 300 for every 5 min). Mutually exclusive with schedule.
|
|
58
62
|
description: What this cron does (for logs and status).
|
|
59
|
-
script_type: '
|
|
63
|
+
script_type: 'auto' (default), 'python', 'shell', 'node', or 'php'.
|
|
60
64
|
"""
|
|
61
65
|
if not cron_id or not script:
|
|
62
66
|
return "ERROR: cron_id and script are required."
|
|
@@ -70,6 +74,12 @@ def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
|
|
|
70
74
|
if not script_path.exists():
|
|
71
75
|
return f"ERROR: script not found: {script_path}"
|
|
72
76
|
|
|
77
|
+
script_meta = parse_inline_metadata(script_path)
|
|
78
|
+
detected_runtime = classify_runtime(script_path, script_meta)
|
|
79
|
+
script_type = (script_type or "auto").strip().lower()
|
|
80
|
+
if script_type == "auto":
|
|
81
|
+
script_type = detected_runtime if detected_runtime != "unknown" else "python"
|
|
82
|
+
|
|
73
83
|
wrapper_path = nexo_home / "scripts" / "nexo-cron-wrapper.sh"
|
|
74
84
|
if not wrapper_path.exists():
|
|
75
85
|
return f"ERROR: wrapper not found at {wrapper_path}. Run crons/sync.py first."
|
|
@@ -77,15 +87,84 @@ def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
|
|
|
77
87
|
system = platform.system()
|
|
78
88
|
|
|
79
89
|
if system == "Darwin":
|
|
80
|
-
return _add_launchagent(
|
|
81
|
-
|
|
90
|
+
return _add_launchagent(
|
|
91
|
+
cron_id,
|
|
92
|
+
str(script_path),
|
|
93
|
+
str(wrapper_path),
|
|
94
|
+
schedule,
|
|
95
|
+
interval_seconds,
|
|
96
|
+
description or script_meta.get("description", ""),
|
|
97
|
+
script_type,
|
|
98
|
+
nexo_home,
|
|
99
|
+
)
|
|
82
100
|
elif system == "Linux":
|
|
83
|
-
return _add_systemd_timer(
|
|
84
|
-
|
|
101
|
+
return _add_systemd_timer(
|
|
102
|
+
cron_id,
|
|
103
|
+
str(script_path),
|
|
104
|
+
str(wrapper_path),
|
|
105
|
+
schedule,
|
|
106
|
+
interval_seconds,
|
|
107
|
+
description or script_meta.get("description", ""),
|
|
108
|
+
script_type,
|
|
109
|
+
nexo_home,
|
|
110
|
+
)
|
|
85
111
|
else:
|
|
86
112
|
return f"ERROR: unsupported platform: {system}"
|
|
87
113
|
|
|
88
114
|
|
|
115
|
+
def _runtime_command(script_type: str) -> str:
|
|
116
|
+
if script_type == "shell":
|
|
117
|
+
return "/bin/bash"
|
|
118
|
+
if script_type == "node":
|
|
119
|
+
return "node"
|
|
120
|
+
if script_type == "php":
|
|
121
|
+
return "php"
|
|
122
|
+
|
|
123
|
+
for p in ["/opt/homebrew/bin/python3", "/usr/local/bin/python3", "/usr/bin/python3"]:
|
|
124
|
+
if Path(p).exists():
|
|
125
|
+
return p
|
|
126
|
+
return "python3"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _register_schedule_metadata(cron_id, script_path, schedule, interval_seconds, description, script_type, label="", plist_path=""):
|
|
130
|
+
init_db()
|
|
131
|
+
script_meta = parse_inline_metadata(Path(script_path))
|
|
132
|
+
runtime = classify_runtime(Path(script_path), script_meta)
|
|
133
|
+
upsert_personal_script(
|
|
134
|
+
name=script_meta.get("name", Path(script_path).stem),
|
|
135
|
+
path=str(Path(script_path)),
|
|
136
|
+
description=script_meta.get("description", description),
|
|
137
|
+
runtime=runtime,
|
|
138
|
+
metadata=script_meta,
|
|
139
|
+
created_by="schedule:add",
|
|
140
|
+
source="filesystem",
|
|
141
|
+
has_inline_metadata=bool(script_meta),
|
|
142
|
+
)
|
|
143
|
+
if interval_seconds:
|
|
144
|
+
schedule_type = "interval"
|
|
145
|
+
schedule_value = str(interval_seconds)
|
|
146
|
+
schedule_label = f"every {interval_seconds}s"
|
|
147
|
+
elif schedule:
|
|
148
|
+
schedule_type = "calendar"
|
|
149
|
+
schedule_value = schedule
|
|
150
|
+
schedule_label = schedule
|
|
151
|
+
else:
|
|
152
|
+
schedule_type = "manual"
|
|
153
|
+
schedule_value = ""
|
|
154
|
+
schedule_label = ""
|
|
155
|
+
register_personal_script_schedule(
|
|
156
|
+
script_path=str(Path(script_path)),
|
|
157
|
+
cron_id=cron_id,
|
|
158
|
+
schedule_type=schedule_type,
|
|
159
|
+
schedule_value=schedule_value,
|
|
160
|
+
schedule_label=schedule_label,
|
|
161
|
+
launchd_label=label,
|
|
162
|
+
plist_path=plist_path,
|
|
163
|
+
description=description,
|
|
164
|
+
enabled=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
89
168
|
def _add_launchagent(cron_id, script_path, wrapper_path, schedule, interval_seconds,
|
|
90
169
|
description, script_type, nexo_home):
|
|
91
170
|
"""Create and load a macOS LaunchAgent."""
|
|
@@ -97,16 +176,8 @@ def _add_launchagent(cron_id, script_path, wrapper_path, schedule, interval_seco
|
|
|
97
176
|
if plist_path.exists():
|
|
98
177
|
return f"ERROR: cron '{cron_id}' already exists at {plist_path}. Use a different ID or remove it first."
|
|
99
178
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if Path(p).exists():
|
|
103
|
-
python_bin = p
|
|
104
|
-
break
|
|
105
|
-
|
|
106
|
-
if script_type == "shell":
|
|
107
|
-
program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
|
|
108
|
-
else:
|
|
109
|
-
program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
|
|
179
|
+
runtime_cmd = _runtime_command(script_type)
|
|
180
|
+
program_args = ["/bin/bash", wrapper_path, cron_id, runtime_cmd, script_path]
|
|
110
181
|
|
|
111
182
|
plist = {
|
|
112
183
|
"Label": label,
|
|
@@ -117,6 +188,8 @@ def _add_launchagent(cron_id, script_path, wrapper_path, schedule, interval_seco
|
|
|
117
188
|
"HOME": str(Path.home()),
|
|
118
189
|
"NEXO_HOME": str(nexo_home),
|
|
119
190
|
"PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + str(Path.home() / ".local/bin"),
|
|
191
|
+
PERSONAL_SCHEDULE_MANAGED_ENV: "1",
|
|
192
|
+
"NEXO_PERSONAL_CRON_ID": cron_id,
|
|
120
193
|
},
|
|
121
194
|
}
|
|
122
195
|
|
|
@@ -134,6 +207,17 @@ def _add_launchagent(cron_id, script_path, wrapper_path, schedule, interval_seco
|
|
|
134
207
|
|
|
135
208
|
subprocess.run(["launchctl", "bootstrap", f"gui/{os.getuid()}", str(plist_path)], capture_output=True)
|
|
136
209
|
|
|
210
|
+
_register_schedule_metadata(
|
|
211
|
+
cron_id,
|
|
212
|
+
script_path,
|
|
213
|
+
schedule,
|
|
214
|
+
interval_seconds,
|
|
215
|
+
description,
|
|
216
|
+
script_type,
|
|
217
|
+
label=label,
|
|
218
|
+
plist_path=str(plist_path),
|
|
219
|
+
)
|
|
220
|
+
|
|
137
221
|
return f"Cron '{cron_id}' installed at {plist_path} and loaded.{' Schedule: ' + schedule if schedule else f' Interval: {interval_seconds}s'}"
|
|
138
222
|
|
|
139
223
|
|
|
@@ -143,16 +227,8 @@ def _add_systemd_timer(cron_id, script_path, wrapper_path, schedule, interval_se
|
|
|
143
227
|
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
|
144
228
|
unit_dir.mkdir(parents=True, exist_ok=True)
|
|
145
229
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if Path(p).exists():
|
|
149
|
-
python_bin = p
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
if script_type == "shell":
|
|
153
|
-
exec_cmd = f"/bin/bash {wrapper_path} {cron_id} /bin/bash {script_path}"
|
|
154
|
-
else:
|
|
155
|
-
exec_cmd = f"/bin/bash {wrapper_path} {cron_id} {python_bin} {script_path}"
|
|
230
|
+
runtime_cmd = _runtime_command(script_type)
|
|
231
|
+
exec_cmd = f"/bin/bash {wrapper_path} {cron_id} {runtime_cmd} {script_path}"
|
|
156
232
|
|
|
157
233
|
# Service unit
|
|
158
234
|
service_content = f"""[Unit]
|
|
@@ -163,6 +239,8 @@ Type=oneshot
|
|
|
163
239
|
ExecStart={exec_cmd}
|
|
164
240
|
Environment=NEXO_HOME={nexo_home}
|
|
165
241
|
Environment=HOME={Path.home()}
|
|
242
|
+
Environment={PERSONAL_SCHEDULE_MANAGED_ENV}=1
|
|
243
|
+
Environment=NEXO_PERSONAL_CRON_ID={cron_id}
|
|
166
244
|
"""
|
|
167
245
|
service_path = unit_dir / f"nexo-{cron_id}.service"
|
|
168
246
|
service_path.write_text(service_content)
|
|
@@ -198,6 +276,17 @@ WantedBy=timers.target
|
|
|
198
276
|
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
199
277
|
subprocess.run(["systemctl", "--user", "enable", "--now", f"nexo-{cron_id}.timer"], capture_output=True)
|
|
200
278
|
|
|
279
|
+
_register_schedule_metadata(
|
|
280
|
+
cron_id,
|
|
281
|
+
script_path,
|
|
282
|
+
schedule,
|
|
283
|
+
interval_seconds,
|
|
284
|
+
description,
|
|
285
|
+
script_type,
|
|
286
|
+
label=f"nexo-{cron_id}",
|
|
287
|
+
plist_path="",
|
|
288
|
+
)
|
|
289
|
+
|
|
201
290
|
return f"Cron '{cron_id}' installed as systemd timer and enabled. Service: {service_path}, Timer: {timer_path}"
|
|
202
291
|
|
|
203
292
|
|