nexo-brain 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,11 @@ import platform
6
6
  import subprocess
7
7
  from pathlib import Path
8
8
 
9
- from db import cron_runs_recent, cron_runs_summary
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 = 'python') -> 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: 'python' (default) or 'shell'.
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(cron_id, str(script_path), str(wrapper_path),
81
- schedule, interval_seconds, description, script_type, nexo_home)
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(cron_id, str(script_path), str(wrapper_path),
84
- schedule, interval_seconds, description, script_type, nexo_home)
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
- python_bin = "/opt/homebrew/bin/python3"
101
- for p in ["/opt/homebrew/bin/python3", "/usr/local/bin/python3", "/usr/bin/python3"]:
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
- python_bin = "/usr/bin/python3"
147
- for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
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