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/cli.py
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
"""NEXO Runtime CLI — operational commands for scripts and diagnostics.
|
|
3
3
|
|
|
4
4
|
Entry points:
|
|
5
|
+
nexo chat [PATH]
|
|
5
6
|
nexo scripts list [--all] [--json]
|
|
7
|
+
nexo scripts create NAME [--runtime python|shell] [--description TEXT]
|
|
8
|
+
nexo scripts classify [--json]
|
|
9
|
+
nexo scripts sync [--json]
|
|
10
|
+
nexo scripts reconcile [--dry-run] [--json]
|
|
11
|
+
nexo scripts ensure-schedules [--dry-run] [--json]
|
|
12
|
+
nexo scripts schedules [--json]
|
|
13
|
+
nexo scripts unschedule NAME [--json]
|
|
14
|
+
nexo scripts remove NAME [--keep-file] [--json]
|
|
6
15
|
nexo scripts run NAME_OR_PATH [-- args...]
|
|
7
16
|
nexo scripts doctor [NAME_OR_PATH] [--json]
|
|
8
17
|
nexo scripts call TOOL --input JSON [--json-output]
|
|
@@ -23,6 +32,7 @@ import contextlib
|
|
|
23
32
|
import io
|
|
24
33
|
import json
|
|
25
34
|
import os
|
|
35
|
+
import shutil
|
|
26
36
|
import subprocess
|
|
27
37
|
import sys
|
|
28
38
|
from pathlib import Path
|
|
@@ -32,11 +42,17 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
|
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
def _get_version() -> str:
|
|
35
|
-
"""Read version from package.json automatically."""
|
|
36
|
-
|
|
45
|
+
"""Read version from runtime version.json or package.json automatically."""
|
|
46
|
+
json_candidates = [
|
|
47
|
+
(NEXO_HOME / "version.json", "version"),
|
|
48
|
+
(NEXO_CODE.parent / "version.json", "version"),
|
|
49
|
+
(NEXO_CODE.parent / "package.json", "version"),
|
|
50
|
+
(NEXO_HOME / "package.json", "version"),
|
|
51
|
+
]
|
|
52
|
+
for candidate, key in json_candidates:
|
|
37
53
|
try:
|
|
38
54
|
if candidate.is_file():
|
|
39
|
-
return json.loads(candidate.read_text()).get(
|
|
55
|
+
return json.loads(candidate.read_text()).get(key, "?")
|
|
40
56
|
except Exception:
|
|
41
57
|
continue
|
|
42
58
|
return "?"
|
|
@@ -47,8 +63,16 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
47
63
|
|
|
48
64
|
|
|
49
65
|
def _scripts_list(args):
|
|
50
|
-
from
|
|
51
|
-
|
|
66
|
+
from db import init_db, list_personal_scripts
|
|
67
|
+
from script_registry import list_scripts, sync_personal_scripts
|
|
68
|
+
|
|
69
|
+
init_db()
|
|
70
|
+
sync_personal_scripts()
|
|
71
|
+
if args.all:
|
|
72
|
+
scripts = list_scripts(include_core=True)
|
|
73
|
+
else:
|
|
74
|
+
scripts = list_personal_scripts()
|
|
75
|
+
|
|
52
76
|
if args.json:
|
|
53
77
|
print(json.dumps(scripts, indent=2))
|
|
54
78
|
else:
|
|
@@ -60,13 +84,169 @@ def _scripts_list(args):
|
|
|
60
84
|
rt_w = max(len(s["runtime"]) for s in scripts)
|
|
61
85
|
for s in scripts:
|
|
62
86
|
tag = " [core]" if s.get("core") else ""
|
|
63
|
-
|
|
87
|
+
schedule_tag = ""
|
|
88
|
+
if s.get("has_schedule"):
|
|
89
|
+
schedule_labels = [sch.get("schedule_label", "") for sch in s.get("schedules", []) if sch.get("schedule_label")]
|
|
90
|
+
if schedule_labels:
|
|
91
|
+
schedule_tag = f" [{'; '.join(schedule_labels[:2])}]"
|
|
92
|
+
print(f" {s['name']:<{name_w}} {s['runtime']:<{rt_w}} {s.get('description', '')}{schedule_tag}{tag}")
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _scripts_sync(args):
|
|
97
|
+
from db import init_db
|
|
98
|
+
from script_registry import sync_personal_scripts
|
|
99
|
+
|
|
100
|
+
init_db()
|
|
101
|
+
result = sync_personal_scripts()
|
|
102
|
+
if args.json:
|
|
103
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
104
|
+
else:
|
|
105
|
+
print(
|
|
106
|
+
f"Synced personal scripts: {result['scripts_upserted']} script(s), "
|
|
107
|
+
f"{result['schedules_upserted']} schedule(s), "
|
|
108
|
+
f"{result['scripts_pruned']} script(s) pruned, "
|
|
109
|
+
f"{result['schedules_pruned']} schedule(s) pruned."
|
|
110
|
+
)
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _scripts_classify(args):
|
|
115
|
+
from script_registry import classify_scripts_dir
|
|
116
|
+
|
|
117
|
+
report = classify_scripts_dir()
|
|
118
|
+
if args.json:
|
|
119
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
entries = report.get("entries", [])
|
|
123
|
+
if not entries:
|
|
124
|
+
print("No scripts directory found:", report.get("scripts_dir", NEXO_HOME / "scripts"))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
path_w = max(len(Path(entry["path"]).name) for entry in entries)
|
|
128
|
+
for entry in entries:
|
|
129
|
+
reason = f" — {entry['reason']}" if entry.get("reason") else ""
|
|
130
|
+
print(f" {Path(entry['path']).name:<{path_w}} {entry['classification']}{reason}")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _scripts_reconcile(args):
|
|
135
|
+
from script_registry import reconcile_personal_scripts
|
|
136
|
+
|
|
137
|
+
result = reconcile_personal_scripts(dry_run=args.dry_run)
|
|
138
|
+
if args.json:
|
|
139
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
140
|
+
else:
|
|
141
|
+
sync = result.get("sync", {})
|
|
142
|
+
ensured = result.get("ensure_schedules", {})
|
|
143
|
+
print(
|
|
144
|
+
f"Reconciled personal scripts: {sync.get('registered_scripts', 0)} registered, "
|
|
145
|
+
f"{len(ensured.get('created', []))} schedule(s) created, "
|
|
146
|
+
f"{len(ensured.get('repaired', []))} repaired, "
|
|
147
|
+
f"{len(ensured.get('invalid', []))} invalid."
|
|
148
|
+
)
|
|
149
|
+
if args.dry_run:
|
|
150
|
+
print(" Dry run only — no schedules changed.")
|
|
151
|
+
return 0 if not result.get("ensure_schedules", {}).get("invalid") else 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _scripts_ensure_schedules(args):
|
|
155
|
+
from script_registry import ensure_personal_schedules
|
|
156
|
+
|
|
157
|
+
result = ensure_personal_schedules(dry_run=args.dry_run)
|
|
158
|
+
if args.json:
|
|
159
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
160
|
+
else:
|
|
161
|
+
print(
|
|
162
|
+
f"Ensured schedules: {len(result.get('created', []))} created, "
|
|
163
|
+
f"{len(result.get('repaired', []))} repaired, "
|
|
164
|
+
f"{len(result.get('already_present', []))} already present, "
|
|
165
|
+
f"{len(result.get('invalid', []))} invalid."
|
|
166
|
+
)
|
|
167
|
+
if args.dry_run:
|
|
168
|
+
print(" Dry run only — no schedules changed.")
|
|
169
|
+
return 0 if not result.get("invalid") else 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _scripts_create(args):
|
|
173
|
+
from script_registry import create_script
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
result = create_script(
|
|
177
|
+
args.name,
|
|
178
|
+
description=args.description,
|
|
179
|
+
runtime=args.runtime,
|
|
180
|
+
force=args.force,
|
|
181
|
+
)
|
|
182
|
+
except FileExistsError as e:
|
|
183
|
+
print(str(e), file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
if args.json:
|
|
187
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
188
|
+
else:
|
|
189
|
+
print(f"Created personal script: {result['path']}")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _scripts_schedules(args):
|
|
194
|
+
from db import init_db, list_personal_script_schedules
|
|
195
|
+
from script_registry import sync_personal_scripts
|
|
196
|
+
|
|
197
|
+
init_db()
|
|
198
|
+
sync_personal_scripts()
|
|
199
|
+
schedules = list_personal_script_schedules()
|
|
200
|
+
if args.json:
|
|
201
|
+
print(json.dumps(schedules, indent=2, ensure_ascii=False))
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
if not schedules:
|
|
205
|
+
print("No personal script schedules registered.")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
cron_w = max(len(s["cron_id"]) for s in schedules)
|
|
209
|
+
for schedule in schedules:
|
|
210
|
+
label = schedule.get("schedule_label") or schedule.get("schedule_value") or schedule.get("schedule_type")
|
|
211
|
+
print(f" {schedule['cron_id']:<{cron_w}} {label}")
|
|
64
212
|
return 0
|
|
65
213
|
|
|
66
214
|
|
|
215
|
+
def _scripts_unschedule(args):
|
|
216
|
+
from script_registry import unschedule_personal_script
|
|
217
|
+
|
|
218
|
+
result = unschedule_personal_script(args.name)
|
|
219
|
+
if args.json:
|
|
220
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
221
|
+
else:
|
|
222
|
+
if not result.get("ok"):
|
|
223
|
+
print(result.get("error", "Failed to unschedule script"), file=sys.stderr)
|
|
224
|
+
return 1
|
|
225
|
+
print(f"Removed {len(result.get('removed_schedules', []))} schedule(s) from {result['script']}")
|
|
226
|
+
return 0 if result.get("ok") else 1
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _scripts_remove(args):
|
|
230
|
+
from script_registry import remove_personal_script
|
|
231
|
+
|
|
232
|
+
result = remove_personal_script(args.name, keep_file=args.keep_file)
|
|
233
|
+
if args.json:
|
|
234
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
235
|
+
else:
|
|
236
|
+
if not result.get("ok"):
|
|
237
|
+
print(result.get("error", "Failed to remove script"), file=sys.stderr)
|
|
238
|
+
return 1
|
|
239
|
+
action = "unregistered" if args.keep_file else "removed"
|
|
240
|
+
print(f"Script {result['script']} {action}")
|
|
241
|
+
return 0 if result.get("ok") else 1
|
|
242
|
+
|
|
243
|
+
|
|
67
244
|
def _scripts_run(args):
|
|
68
|
-
from
|
|
245
|
+
from db import init_db, record_personal_script_run
|
|
246
|
+
from script_registry import resolve_script_reference, sync_personal_scripts
|
|
69
247
|
|
|
248
|
+
init_db()
|
|
249
|
+
sync_personal_scripts()
|
|
70
250
|
info = resolve_script_reference(args.name)
|
|
71
251
|
if not info:
|
|
72
252
|
print(f"Script not found: {args.name}", file=sys.stderr)
|
|
@@ -106,23 +286,37 @@ def _scripts_run(args):
|
|
|
106
286
|
cmd = [sys.executable, str(path)] + args.script_args
|
|
107
287
|
elif runtime == "shell":
|
|
108
288
|
cmd = ["bash", str(path)] + args.script_args
|
|
289
|
+
elif runtime == "node":
|
|
290
|
+
cmd = ["node", str(path)] + args.script_args
|
|
291
|
+
elif runtime == "php":
|
|
292
|
+
cmd = ["php", str(path)] + args.script_args
|
|
109
293
|
else:
|
|
110
294
|
# Try to execute directly
|
|
111
295
|
cmd = [str(path)] + args.script_args
|
|
112
296
|
|
|
113
297
|
try:
|
|
114
298
|
result = subprocess.run(cmd, env=env, timeout=timeout)
|
|
299
|
+
if not is_core:
|
|
300
|
+
record_personal_script_run(str(path), result.returncode)
|
|
115
301
|
return result.returncode
|
|
116
302
|
except subprocess.TimeoutExpired:
|
|
303
|
+
if not is_core:
|
|
304
|
+
record_personal_script_run(str(path), 124)
|
|
117
305
|
print(f"Script timed out after {timeout}s", file=sys.stderr)
|
|
118
306
|
return 124
|
|
119
307
|
except Exception as e:
|
|
308
|
+
if not is_core:
|
|
309
|
+
record_personal_script_run(str(path), 1)
|
|
120
310
|
print(f"Error running script: {e}", file=sys.stderr)
|
|
121
311
|
return 1
|
|
122
312
|
|
|
123
313
|
|
|
124
314
|
def _scripts_doctor(args):
|
|
125
|
-
from
|
|
315
|
+
from db import init_db
|
|
316
|
+
from script_registry import doctor_script, doctor_all_scripts, sync_personal_scripts
|
|
317
|
+
|
|
318
|
+
init_db()
|
|
319
|
+
sync_personal_scripts()
|
|
126
320
|
|
|
127
321
|
if args.name:
|
|
128
322
|
results = [doctor_script(args.name)]
|
|
@@ -249,13 +443,90 @@ def _scripts_call(args):
|
|
|
249
443
|
|
|
250
444
|
|
|
251
445
|
def _update(args):
|
|
252
|
-
"""
|
|
446
|
+
"""Update the installed runtime.
|
|
447
|
+
|
|
448
|
+
Modes:
|
|
449
|
+
- Dev-linked runtime: sync from the source repo recorded in version.json
|
|
450
|
+
- Explicit dev env: sync from NEXO_CODE/src
|
|
451
|
+
- Packaged/runtime-only install: delegate to plugins.update handle_update()
|
|
452
|
+
"""
|
|
253
453
|
import shutil
|
|
254
454
|
|
|
255
|
-
src_dir = NEXO_CODE
|
|
256
|
-
repo_dir = NEXO_CODE.parent
|
|
257
455
|
dest = NEXO_HOME
|
|
258
456
|
|
|
457
|
+
def _runtime_version_source() -> Path | None:
|
|
458
|
+
version_file = NEXO_HOME / "version.json"
|
|
459
|
+
if not version_file.is_file():
|
|
460
|
+
return None
|
|
461
|
+
try:
|
|
462
|
+
data = json.loads(version_file.read_text())
|
|
463
|
+
except Exception:
|
|
464
|
+
return None
|
|
465
|
+
source = str(data.get("source", "")).strip()
|
|
466
|
+
if not source:
|
|
467
|
+
return None
|
|
468
|
+
candidate = Path(source).expanduser()
|
|
469
|
+
if (candidate / "src").is_dir() and (candidate / "package.json").is_file():
|
|
470
|
+
return candidate
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
474
|
+
try:
|
|
475
|
+
same_as_runtime = NEXO_CODE.resolve() == dest.resolve()
|
|
476
|
+
except Exception:
|
|
477
|
+
same_as_runtime = NEXO_CODE == dest
|
|
478
|
+
|
|
479
|
+
# Explicit dev mode: NEXO_CODE points at repo/src, never the installed runtime itself.
|
|
480
|
+
if (
|
|
481
|
+
not same_as_runtime
|
|
482
|
+
and (NEXO_CODE / "db").is_dir()
|
|
483
|
+
and (NEXO_CODE.parent / "package.json").is_file()
|
|
484
|
+
):
|
|
485
|
+
return NEXO_CODE, NEXO_CODE.parent
|
|
486
|
+
|
|
487
|
+
# Installed runtime linked back to a source checkout
|
|
488
|
+
version_source = _runtime_version_source()
|
|
489
|
+
if version_source:
|
|
490
|
+
return version_source / "src", version_source
|
|
491
|
+
|
|
492
|
+
return None, None
|
|
493
|
+
|
|
494
|
+
src_dir, repo_dir = _resolve_sync_source()
|
|
495
|
+
|
|
496
|
+
if src_dir is not None:
|
|
497
|
+
try:
|
|
498
|
+
if src_dir.resolve() == dest.resolve():
|
|
499
|
+
version_source = _runtime_version_source()
|
|
500
|
+
if version_source:
|
|
501
|
+
src_dir = version_source / "src"
|
|
502
|
+
repo_dir = version_source
|
|
503
|
+
else:
|
|
504
|
+
src_dir = None
|
|
505
|
+
repo_dir = None
|
|
506
|
+
except Exception:
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
if src_dir is None or repo_dir is None:
|
|
510
|
+
try:
|
|
511
|
+
from plugins.update import handle_update
|
|
512
|
+
except Exception as e:
|
|
513
|
+
print(
|
|
514
|
+
"No source repo recorded for this runtime and packaged updater is unavailable: "
|
|
515
|
+
f"{e}",
|
|
516
|
+
file=sys.stderr,
|
|
517
|
+
)
|
|
518
|
+
return 1
|
|
519
|
+
|
|
520
|
+
result = handle_update()
|
|
521
|
+
if args.json:
|
|
522
|
+
print(json.dumps({
|
|
523
|
+
"mode": "packaged",
|
|
524
|
+
"message": result,
|
|
525
|
+
}, indent=2, ensure_ascii=False))
|
|
526
|
+
else:
|
|
527
|
+
print(result)
|
|
528
|
+
return 0 if "UPDATE SUCCESSFUL" in result or "Already up to date" in result else 1
|
|
529
|
+
|
|
259
530
|
# Packages (directories with __init__.py or known structure)
|
|
260
531
|
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
|
|
261
532
|
copied_packages = 0
|
|
@@ -280,6 +551,7 @@ def _update(args):
|
|
|
280
551
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
281
552
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
282
553
|
"cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
|
|
554
|
+
"cron_recovery.py",
|
|
283
555
|
"requirements.txt",
|
|
284
556
|
]
|
|
285
557
|
copied_files = 0
|
|
@@ -302,6 +574,7 @@ def _update(args):
|
|
|
302
574
|
scripts_src = src_dir / "scripts"
|
|
303
575
|
scripts_dest = dest / "scripts"
|
|
304
576
|
if scripts_src.is_dir():
|
|
577
|
+
scripts_dest.mkdir(parents=True, exist_ok=True)
|
|
305
578
|
for f in scripts_src.iterdir():
|
|
306
579
|
if f.name == "__pycache__" or f.name.startswith("."):
|
|
307
580
|
continue
|
|
@@ -324,6 +597,20 @@ def _update(args):
|
|
|
324
597
|
if f.is_file():
|
|
325
598
|
shutil.copy2(str(f), str(templates_dest / f.name))
|
|
326
599
|
|
|
600
|
+
# Runtime version metadata
|
|
601
|
+
package_json = repo_dir / "package.json"
|
|
602
|
+
if package_json.is_file():
|
|
603
|
+
shutil.copy2(str(package_json), str(dest / "package.json"))
|
|
604
|
+
try:
|
|
605
|
+
pkg = json.loads(package_json.read_text())
|
|
606
|
+
version_payload = {
|
|
607
|
+
"version": pkg.get("version", "?"),
|
|
608
|
+
"source": str(repo_dir),
|
|
609
|
+
}
|
|
610
|
+
(dest / "version.json").write_text(json.dumps(version_payload, indent=2))
|
|
611
|
+
except Exception:
|
|
612
|
+
pass
|
|
613
|
+
|
|
327
614
|
# Core skills
|
|
328
615
|
skills_src = src_dir / "skills"
|
|
329
616
|
skills_dest = dest / "skills-core"
|
|
@@ -354,7 +641,17 @@ def _update(args):
|
|
|
354
641
|
wrapper.write_text(wrapper_content)
|
|
355
642
|
wrapper.chmod(0o755)
|
|
356
643
|
|
|
644
|
+
try:
|
|
645
|
+
from db import init_db
|
|
646
|
+
from script_registry import sync_personal_scripts
|
|
647
|
+
|
|
648
|
+
init_db()
|
|
649
|
+
sync_personal_scripts()
|
|
650
|
+
except Exception:
|
|
651
|
+
pass
|
|
652
|
+
|
|
357
653
|
result = {
|
|
654
|
+
"mode": "sync",
|
|
358
655
|
"packages": copied_packages,
|
|
359
656
|
"files": copied_files,
|
|
360
657
|
"nexo_home": str(dest),
|
|
@@ -429,8 +726,18 @@ def _dashboard(args):
|
|
|
429
726
|
return _service_control("dashboard", args.action)
|
|
430
727
|
|
|
431
728
|
|
|
432
|
-
def
|
|
433
|
-
|
|
729
|
+
def _chat(args):
|
|
730
|
+
target = args.path or "."
|
|
731
|
+
claude_bin = os.environ.get("CLAUDE_BIN") or shutil.which("claude")
|
|
732
|
+
if not claude_bin:
|
|
733
|
+
print("Claude Code launcher not found in PATH. Install `claude` first.", file=sys.stderr)
|
|
734
|
+
return 1
|
|
735
|
+
|
|
736
|
+
result = subprocess.run(
|
|
737
|
+
[claude_bin, "--dangerously-skip-permissions", target],
|
|
738
|
+
env=os.environ.copy(),
|
|
739
|
+
)
|
|
740
|
+
return int(result.returncode)
|
|
434
741
|
|
|
435
742
|
|
|
436
743
|
def _doctor(args):
|
|
@@ -563,12 +870,13 @@ def _print_help():
|
|
|
563
870
|
print(f"""NEXO Runtime CLI v{v}
|
|
564
871
|
|
|
565
872
|
Commands:
|
|
873
|
+
nexo chat [path] Launch Claude Code
|
|
566
874
|
nexo doctor [--tier boot|runtime|deep|all] [--fix] System diagnostics
|
|
567
|
-
nexo scripts list|run|doctor|call
|
|
875
|
+
nexo scripts list|create|classify|sync|reconcile|ensure-schedules|schedules|run|doctor|call|unschedule|remove
|
|
876
|
+
Personal scripts
|
|
568
877
|
nexo skills list|apply|sync|approve Executable skills
|
|
569
|
-
nexo update
|
|
878
|
+
nexo update Update installed runtime
|
|
570
879
|
nexo dashboard on|off|status Web dashboard control
|
|
571
|
-
nexo orchestrator on|off|status Autonomous mode control
|
|
572
880
|
|
|
573
881
|
Run 'nexo <command> --help' for details.
|
|
574
882
|
Homepage: https://nexo-brain.com
|
|
@@ -581,6 +889,10 @@ def main():
|
|
|
581
889
|
parser.add_argument("-v", "--version", action="store_true", help="Show version")
|
|
582
890
|
sub = parser.add_subparsers(dest="command")
|
|
583
891
|
|
|
892
|
+
# -- chat --
|
|
893
|
+
chat_parser = sub.add_parser("chat", help="Launch Claude Code")
|
|
894
|
+
chat_parser.add_argument("path", nargs="?", default=".", help="Working directory (default: current directory)")
|
|
895
|
+
|
|
584
896
|
# -- scripts --
|
|
585
897
|
scripts_parser = sub.add_parser("scripts", help="Manage personal scripts")
|
|
586
898
|
scripts_sub = scripts_parser.add_subparsers(dest="scripts_command")
|
|
@@ -590,6 +902,47 @@ def main():
|
|
|
590
902
|
list_p.add_argument("--all", action="store_true", help="Include core/internal scripts")
|
|
591
903
|
list_p.add_argument("--json", action="store_true", help="JSON output")
|
|
592
904
|
|
|
905
|
+
# scripts create
|
|
906
|
+
create_p = scripts_sub.add_parser("create", help="Create a personal script scaffold")
|
|
907
|
+
create_p.add_argument("name", help="Human/script name")
|
|
908
|
+
create_p.add_argument("--description", default="", help="One-line description")
|
|
909
|
+
create_p.add_argument("--runtime", default="python", choices=["python", "shell"], help="Script runtime")
|
|
910
|
+
create_p.add_argument("--force", action="store_true", help="Overwrite if the target file exists")
|
|
911
|
+
create_p.add_argument("--json", action="store_true", help="JSON output")
|
|
912
|
+
|
|
913
|
+
# scripts classify
|
|
914
|
+
classify_p = scripts_sub.add_parser("classify", help="Classify all files in NEXO_HOME/scripts")
|
|
915
|
+
classify_p.add_argument("--json", action="store_true", help="JSON output")
|
|
916
|
+
|
|
917
|
+
# scripts sync
|
|
918
|
+
sync_p = scripts_sub.add_parser("sync", help="Sync script registry from filesystem and personal LaunchAgents")
|
|
919
|
+
sync_p.add_argument("--json", action="store_true", help="JSON output")
|
|
920
|
+
|
|
921
|
+
# scripts reconcile
|
|
922
|
+
reconcile_p = scripts_sub.add_parser("reconcile", help="Classify, sync, and ensure declared schedules")
|
|
923
|
+
reconcile_p.add_argument("--dry-run", action="store_true", help="Show what would change without editing schedules")
|
|
924
|
+
reconcile_p.add_argument("--json", action="store_true", help="JSON output")
|
|
925
|
+
|
|
926
|
+
# scripts ensure-schedules
|
|
927
|
+
ensure_p = scripts_sub.add_parser("ensure-schedules", help="Create or repair declared personal schedules")
|
|
928
|
+
ensure_p.add_argument("--dry-run", action="store_true", help="Show what would change without editing schedules")
|
|
929
|
+
ensure_p.add_argument("--json", action="store_true", help="JSON output")
|
|
930
|
+
|
|
931
|
+
# scripts schedules
|
|
932
|
+
schedules_p = scripts_sub.add_parser("schedules", help="List registered personal script schedules")
|
|
933
|
+
schedules_p.add_argument("--json", action="store_true", help="JSON output")
|
|
934
|
+
|
|
935
|
+
# scripts unschedule
|
|
936
|
+
unschedule_p = scripts_sub.add_parser("unschedule", help="Remove all personal schedules from a script")
|
|
937
|
+
unschedule_p.add_argument("name", help="Script name or path")
|
|
938
|
+
unschedule_p.add_argument("--json", action="store_true", help="JSON output")
|
|
939
|
+
|
|
940
|
+
# scripts remove
|
|
941
|
+
remove_p = scripts_sub.add_parser("remove", help="Remove a personal script and any attached schedules")
|
|
942
|
+
remove_p.add_argument("name", help="Script name or path")
|
|
943
|
+
remove_p.add_argument("--keep-file", action="store_true", help="Keep the script file and only unregister/unschedule it")
|
|
944
|
+
remove_p.add_argument("--json", action="store_true", help="JSON output")
|
|
945
|
+
|
|
593
946
|
# scripts run
|
|
594
947
|
run_p = scripts_sub.add_parser("run", help="Run a script by name")
|
|
595
948
|
run_p.add_argument("name", help="Script name")
|
|
@@ -607,7 +960,7 @@ def main():
|
|
|
607
960
|
call_p.add_argument("--json-output", action="store_true", help="Force JSON output")
|
|
608
961
|
|
|
609
962
|
# -- update --
|
|
610
|
-
update_parser = sub.add_parser("update", help="
|
|
963
|
+
update_parser = sub.add_parser("update", help="Update installed runtime")
|
|
611
964
|
update_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
612
965
|
|
|
613
966
|
# -- doctor --
|
|
@@ -659,10 +1012,6 @@ def main():
|
|
|
659
1012
|
dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
|
|
660
1013
|
dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
|
|
661
1014
|
|
|
662
|
-
# -- orchestrator --
|
|
663
|
-
orchestrator_parser = sub.add_parser("orchestrator", help="Autonomous mode control")
|
|
664
|
-
orchestrator_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check orchestrator")
|
|
665
|
-
|
|
666
1015
|
args = parser.parse_args()
|
|
667
1016
|
|
|
668
1017
|
if args.help or (not args.command and not args.version):
|
|
@@ -675,6 +1024,22 @@ def main():
|
|
|
675
1024
|
if args.command == "scripts":
|
|
676
1025
|
if args.scripts_command == "list":
|
|
677
1026
|
return _scripts_list(args)
|
|
1027
|
+
elif args.scripts_command == "create":
|
|
1028
|
+
return _scripts_create(args)
|
|
1029
|
+
elif args.scripts_command == "classify":
|
|
1030
|
+
return _scripts_classify(args)
|
|
1031
|
+
elif args.scripts_command == "sync":
|
|
1032
|
+
return _scripts_sync(args)
|
|
1033
|
+
elif args.scripts_command == "reconcile":
|
|
1034
|
+
return _scripts_reconcile(args)
|
|
1035
|
+
elif args.scripts_command == "ensure-schedules":
|
|
1036
|
+
return _scripts_ensure_schedules(args)
|
|
1037
|
+
elif args.scripts_command == "schedules":
|
|
1038
|
+
return _scripts_schedules(args)
|
|
1039
|
+
elif args.scripts_command == "unschedule":
|
|
1040
|
+
return _scripts_unschedule(args)
|
|
1041
|
+
elif args.scripts_command == "remove":
|
|
1042
|
+
return _scripts_remove(args)
|
|
678
1043
|
elif args.scripts_command == "run":
|
|
679
1044
|
return _scripts_run(args)
|
|
680
1045
|
elif args.scripts_command == "doctor":
|
|
@@ -684,6 +1049,8 @@ def main():
|
|
|
684
1049
|
else:
|
|
685
1050
|
scripts_parser.print_help()
|
|
686
1051
|
return 0
|
|
1052
|
+
elif args.command == "chat":
|
|
1053
|
+
return _chat(args)
|
|
687
1054
|
elif args.command == "update":
|
|
688
1055
|
return _update(args)
|
|
689
1056
|
elif args.command == "doctor":
|
|
@@ -708,8 +1075,6 @@ def main():
|
|
|
708
1075
|
return 0
|
|
709
1076
|
elif args.command == "dashboard":
|
|
710
1077
|
return _dashboard(args)
|
|
711
|
-
elif args.command == "orchestrator":
|
|
712
|
-
return _orchestrator(args)
|
|
713
1078
|
else:
|
|
714
1079
|
_print_help()
|
|
715
1080
|
return 0
|