nexo-brain 2.4.0 → 2.5.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.
Files changed (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
package/src/cli.py ADDED
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env python3
2
+ """NEXO Runtime CLI — operational commands for scripts and diagnostics.
3
+
4
+ Entry points:
5
+ nexo scripts list [--all] [--json]
6
+ nexo scripts run NAME_OR_PATH [-- args...]
7
+ nexo scripts doctor [NAME_OR_PATH] [--json]
8
+ nexo scripts call TOOL --input JSON [--json-output]
9
+ nexo skills list [--level ...] [--source-kind ...] [--json]
10
+ nexo skills get ID [--json]
11
+ nexo skills apply ID [--params JSON] [--mode ...] [--dry-run] [--json]
12
+ nexo skills sync [--json]
13
+ nexo skills approve ID [--execution-level ...] [--approved-by ...] [--json]
14
+ nexo skills featured [--limit N] [--json]
15
+ nexo skills evolution [--json]
16
+ nexo doctor [--tier boot|runtime|deep|all] [--json] [--fix]
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import asyncio
22
+ import contextlib
23
+ import io
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
31
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
32
+
33
+
34
+ def _get_version() -> str:
35
+ """Read version from package.json automatically."""
36
+ for candidate in [NEXO_CODE.parent / "package.json", NEXO_HOME / "package.json"]:
37
+ try:
38
+ if candidate.is_file():
39
+ return json.loads(candidate.read_text()).get("version", "?")
40
+ except Exception:
41
+ continue
42
+ return "?"
43
+
44
+ # Ensure src/ is on path for imports
45
+ if str(NEXO_CODE) not in sys.path:
46
+ sys.path.insert(0, str(NEXO_CODE))
47
+
48
+
49
+ def _scripts_list(args):
50
+ from script_registry import list_scripts
51
+ scripts = list_scripts(include_core=args.all)
52
+ if args.json:
53
+ print(json.dumps(scripts, indent=2))
54
+ else:
55
+ if not scripts:
56
+ print("No personal scripts found in", NEXO_HOME / "scripts")
57
+ return 0
58
+ # Table output
59
+ name_w = max(len(s["name"]) for s in scripts)
60
+ rt_w = max(len(s["runtime"]) for s in scripts)
61
+ for s in scripts:
62
+ tag = " [core]" if s.get("core") else ""
63
+ print(f" {s['name']:<{name_w}} {s['runtime']:<{rt_w}} {s['description']}{tag}")
64
+ return 0
65
+
66
+
67
+ def _scripts_run(args):
68
+ from script_registry import resolve_script_reference
69
+
70
+ info = resolve_script_reference(args.name)
71
+ if not info:
72
+ print(f"Script not found: {args.name}", file=sys.stderr)
73
+ return 1
74
+
75
+ path = Path(info["path"])
76
+ runtime = info["runtime"]
77
+ meta = info["metadata"]
78
+ is_core = info.get("core", False)
79
+
80
+ # Build environment
81
+ env = {
82
+ **os.environ,
83
+ "NEXO_HOME": str(NEXO_HOME),
84
+ "NEXO_CODE": str(NEXO_CODE),
85
+ "NEXO_SCRIPT_NAME": info["name"],
86
+ "NEXO_SCRIPT_PATH": str(path),
87
+ "NEXO_CLI": "nexo",
88
+ }
89
+
90
+ # Only inject DB paths for core scripts
91
+ if is_core:
92
+ env["NEXO_DB"] = str(NEXO_HOME / "data" / "nexo.db")
93
+ env["NEXO_COGNITIVE_DB"] = str(NEXO_HOME / "data" / "cognitive.db")
94
+
95
+ # Timeout
96
+ timeout = None
97
+ timeout_str = meta.get("timeout", "")
98
+ if timeout_str:
99
+ try:
100
+ timeout = int(timeout_str)
101
+ except ValueError:
102
+ pass
103
+
104
+ # Build command
105
+ if runtime == "python":
106
+ cmd = [sys.executable, str(path)] + args.script_args
107
+ elif runtime == "shell":
108
+ cmd = ["bash", str(path)] + args.script_args
109
+ else:
110
+ # Try to execute directly
111
+ cmd = [str(path)] + args.script_args
112
+
113
+ try:
114
+ result = subprocess.run(cmd, env=env, timeout=timeout)
115
+ return result.returncode
116
+ except subprocess.TimeoutExpired:
117
+ print(f"Script timed out after {timeout}s", file=sys.stderr)
118
+ return 124
119
+ except Exception as e:
120
+ print(f"Error running script: {e}", file=sys.stderr)
121
+ return 1
122
+
123
+
124
+ def _scripts_doctor(args):
125
+ from script_registry import doctor_script, doctor_all_scripts
126
+
127
+ if args.name:
128
+ results = [doctor_script(args.name)]
129
+ else:
130
+ results = doctor_all_scripts()
131
+
132
+ if args.json:
133
+ print(json.dumps(results, indent=2))
134
+ else:
135
+ if not results:
136
+ print("No personal scripts to check.")
137
+ return 0
138
+ any_fail = False
139
+ for r in results:
140
+ name = r.get("name", "?")
141
+ status = r.get("status", "?")
142
+ icon = {"pass": "✓", "warn": "⚠", "fail": "✗"}.get(status, "?")
143
+ print(f"\n{icon} {name} [{status}]")
144
+ for item in r.get("items", []):
145
+ lvl = item["level"]
146
+ prefix = {"pass": " ✓", "warn": " ⚠", "fail": " ✗"}.get(lvl, " ?")
147
+ print(f"{prefix} {item['msg']}")
148
+ if status == "fail":
149
+ any_fail = True
150
+ print()
151
+ return 1 if any_fail else 0
152
+
153
+ return 0
154
+
155
+
156
+ def _scripts_call(args):
157
+ """Call a NEXO MCP tool via in-process fastmcp client."""
158
+ tool_name = args.tool
159
+ try:
160
+ payload = json.loads(args.input) if args.input else {}
161
+ except json.JSONDecodeError as e:
162
+ print(f"Invalid JSON input: {e}", file=sys.stderr)
163
+ return 1
164
+
165
+ def _bootstrap_mcp():
166
+ os.environ["NEXO_CLI_MODE"] = "1"
167
+ from db import init_db
168
+ from plugin_loader import load_all_plugins
169
+ from server import mcp
170
+
171
+ init_db()
172
+
173
+ # Plugin loading is required so scripts can call plugin tools such as
174
+ # nexo_doctor, but the loader is noisy on stderr and would pollute CLI output.
175
+ with contextlib.redirect_stderr(io.StringIO()):
176
+ load_all_plugins(mcp)
177
+
178
+ return mcp
179
+
180
+ def _extract_tool_value(result):
181
+ structured = getattr(result, "structured_content", None)
182
+ if structured not in (None, {}):
183
+ return structured
184
+
185
+ content = getattr(result, "content", None)
186
+ if isinstance(content, list):
187
+ texts = [item.text for item in content if hasattr(item, "text")]
188
+ if texts:
189
+ return "\n".join(texts)
190
+
191
+ dumped = getattr(result, "model_dump", None)
192
+ if callable(dumped):
193
+ data = dumped()
194
+ if isinstance(data, dict):
195
+ return data.get("structured_content") or data.get("content") or data
196
+
197
+ return str(result)
198
+
199
+ try:
200
+ mcp = _bootstrap_mcp()
201
+
202
+ async def _call():
203
+ tool = await mcp.get_tool(tool_name)
204
+ if tool is None:
205
+ tools = await mcp.list_tools()
206
+ available = sorted(t.name for t in tools)
207
+ raise LookupError(
208
+ f"Tool not found: {tool_name}\nAvailable tools: {', '.join(available)}"
209
+ )
210
+ return await mcp.call_tool(tool_name, payload)
211
+
212
+ result = asyncio.run(_call())
213
+ value = _extract_tool_value(result)
214
+
215
+ if args.json_output:
216
+ if (
217
+ isinstance(value, dict)
218
+ and set(value.keys()) == {"result"}
219
+ and isinstance(value["result"], str)
220
+ ):
221
+ try:
222
+ value = json.loads(value["result"])
223
+ except json.JSONDecodeError:
224
+ pass
225
+ if isinstance(value, str):
226
+ try:
227
+ value = json.loads(value)
228
+ except json.JSONDecodeError:
229
+ value = {"result": value}
230
+ elif not isinstance(value, (dict, list)):
231
+ value = {"result": value}
232
+ print(json.dumps(value, indent=2, ensure_ascii=False))
233
+ return 0
234
+
235
+ if isinstance(value, dict) and set(value.keys()) == {"result"} and isinstance(value["result"], str):
236
+ print(value["result"])
237
+ elif isinstance(value, (dict, list)):
238
+ print(json.dumps(value, indent=2, ensure_ascii=False))
239
+ else:
240
+ print(value)
241
+ return 0
242
+
243
+ except LookupError as e:
244
+ print(str(e), file=sys.stderr)
245
+ return 1
246
+ except Exception as e:
247
+ print(f"Error calling tool {tool_name}: {e}", file=sys.stderr)
248
+ return 1
249
+
250
+
251
+ def _update(args):
252
+ """Sync all repo files to NEXO_HOME."""
253
+ import shutil
254
+
255
+ src_dir = NEXO_CODE
256
+ repo_dir = NEXO_CODE.parent
257
+ dest = NEXO_HOME
258
+
259
+ # Packages (directories with __init__.py or known structure)
260
+ packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
261
+ copied_packages = 0
262
+ for pkg in packages:
263
+ pkg_src = src_dir / pkg
264
+ pkg_dest = dest / pkg
265
+ if pkg_src.is_dir():
266
+ if pkg_dest.exists():
267
+ shutil.rmtree(str(pkg_dest), ignore_errors=True)
268
+ shutil.copytree(
269
+ str(pkg_src), str(pkg_dest),
270
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.db"),
271
+ )
272
+ copied_packages += 1
273
+
274
+ # Flat Python files
275
+ flat_files = [
276
+ "server.py", "plugin_loader.py", "knowledge_graph.py", "kg_populate.py",
277
+ "maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
278
+ "evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
279
+ "auto_update.py", "tools_sessions.py", "tools_coordination.py",
280
+ "tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
281
+ "tools_credentials.py", "tools_task_history.py", "tools_menu.py",
282
+ "cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
283
+ "requirements.txt",
284
+ ]
285
+ copied_files = 0
286
+ for f in flat_files:
287
+ src_f = src_dir / f
288
+ if src_f.is_file():
289
+ shutil.copy2(str(src_f), str(dest / f))
290
+ copied_files += 1
291
+
292
+ # Plugins
293
+ plugins_src = src_dir / "plugins"
294
+ plugins_dest = dest / "plugins"
295
+ if plugins_src.is_dir():
296
+ plugins_dest.mkdir(parents=True, exist_ok=True)
297
+ for f in plugins_src.iterdir():
298
+ if f.is_file() and f.suffix == ".py":
299
+ shutil.copy2(str(f), str(plugins_dest / f.name))
300
+
301
+ # Scripts
302
+ scripts_src = src_dir / "scripts"
303
+ scripts_dest = dest / "scripts"
304
+ if scripts_src.is_dir():
305
+ for f in scripts_src.iterdir():
306
+ if f.name == "__pycache__" or f.name.startswith("."):
307
+ continue
308
+ dst = scripts_dest / f.name
309
+ if f.is_dir():
310
+ if dst.exists():
311
+ shutil.rmtree(str(dst), ignore_errors=True)
312
+ shutil.copytree(str(f), str(dst), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
313
+ elif f.is_file():
314
+ shutil.copy2(str(f), str(dst))
315
+ if f.suffix == ".sh":
316
+ dst.chmod(0o755)
317
+
318
+ # Templates
319
+ templates_src = repo_dir / "templates"
320
+ templates_dest = dest / "templates"
321
+ if templates_src.is_dir():
322
+ templates_dest.mkdir(parents=True, exist_ok=True)
323
+ for f in templates_src.iterdir():
324
+ if f.is_file():
325
+ shutil.copy2(str(f), str(templates_dest / f.name))
326
+
327
+ # Core skills
328
+ skills_src = src_dir / "skills"
329
+ skills_dest = dest / "skills-core"
330
+ if skills_src.is_dir():
331
+ if skills_dest.exists():
332
+ shutil.rmtree(str(skills_dest), ignore_errors=True)
333
+ shutil.copytree(
334
+ str(skills_src), str(skills_dest),
335
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
336
+ )
337
+
338
+ # Runtime CLI wrapper
339
+ bin_dir = dest / "bin"
340
+ bin_dir.mkdir(parents=True, exist_ok=True)
341
+ wrapper = bin_dir / "nexo"
342
+ wrapper_content = (
343
+ "#!/usr/bin/env bash\n"
344
+ "set -euo pipefail\n\n"
345
+ f'NEXO_HOME="{dest}"\n'
346
+ 'PYTHON="$NEXO_HOME/.venv/bin/python3"\n'
347
+ 'if [ ! -x "$PYTHON" ]; then\n'
348
+ ' if command -v python3 >/dev/null 2>&1; then PYTHON="python3"; else PYTHON="python"; fi\n'
349
+ 'fi\n'
350
+ 'export NEXO_HOME\n'
351
+ 'export NEXO_CODE="$NEXO_HOME"\n'
352
+ 'exec "$PYTHON" "$NEXO_HOME/cli.py" "$@"\n'
353
+ )
354
+ wrapper.write_text(wrapper_content)
355
+ wrapper.chmod(0o755)
356
+
357
+ result = {
358
+ "packages": copied_packages,
359
+ "files": copied_files,
360
+ "nexo_home": str(dest),
361
+ "source": str(src_dir),
362
+ }
363
+ if args.json:
364
+ print(json.dumps(result, indent=2))
365
+ else:
366
+ print(f"Updated NEXO_HOME ({dest})")
367
+ print(f" {copied_packages} packages, {copied_files} files synced from {src_dir}")
368
+ return 0
369
+
370
+
371
+ def _service_control(service_name: str, action: str) -> int:
372
+ """Control a LaunchAgent/systemd service: on, off, status."""
373
+ import platform as plat
374
+
375
+ label = f"com.nexo.{service_name}"
376
+
377
+ if plat.system() != "Darwin":
378
+ print(f"Service control only supported on macOS for now.", file=sys.stderr)
379
+ return 1
380
+
381
+ plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
382
+ uid = os.getuid()
383
+
384
+ if action == "status":
385
+ result = subprocess.run(
386
+ ["launchctl", "list"],
387
+ capture_output=True, text=True,
388
+ )
389
+ running = label in (result.stdout or "")
390
+ if running:
391
+ print(f"{service_name}: running")
392
+ else:
393
+ print(f"{service_name}: stopped")
394
+ return 0
395
+
396
+ if action == "on":
397
+ if not plist_path.is_file():
398
+ print(f"LaunchAgent not found: {plist_path}", file=sys.stderr)
399
+ print(f"Run 'nexo-brain' to install it, or enable it during setup.", file=sys.stderr)
400
+ return 1
401
+ subprocess.run(
402
+ ["launchctl", "bootout", f"gui/{uid}", str(plist_path)],
403
+ capture_output=True,
404
+ )
405
+ result = subprocess.run(
406
+ ["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)],
407
+ capture_output=True, text=True,
408
+ )
409
+ if result.returncode == 0:
410
+ print(f"{service_name}: started")
411
+ else:
412
+ print(f"Failed to start {service_name}: {result.stderr.strip()}", file=sys.stderr)
413
+ return 1
414
+ return 0
415
+
416
+ if action == "off":
417
+ result = subprocess.run(
418
+ ["launchctl", "bootout", f"gui/{uid}", str(plist_path)],
419
+ capture_output=True, text=True,
420
+ )
421
+ print(f"{service_name}: stopped")
422
+ return 0
423
+
424
+ print(f"Unknown action: {action}. Use on, off, or status.", file=sys.stderr)
425
+ return 1
426
+
427
+
428
+ def _dashboard(args):
429
+ return _service_control("dashboard", args.action)
430
+
431
+
432
+ def _orchestrator(args):
433
+ return _service_control("day-orchestrator", args.action)
434
+
435
+
436
+ def _doctor(args):
437
+ """Run unified doctor diagnostics."""
438
+ try:
439
+ from db import init_db
440
+ from doctor.orchestrator import run_doctor
441
+ from doctor.formatters import format_report
442
+ except ImportError:
443
+ print("Doctor module not found. Ensure NEXO is properly installed.", file=sys.stderr)
444
+ return 1
445
+
446
+ init_db()
447
+ report = run_doctor(tier=args.tier, fix=args.fix)
448
+ output = format_report(report, fmt="json" if args.json else "text")
449
+ print(output)
450
+
451
+ if report.overall_status == "critical":
452
+ return 2
453
+ elif report.overall_status == "degraded":
454
+ return 1
455
+ return 0
456
+
457
+
458
+ def _skills_list(args):
459
+ from db import init_db, list_skills, sync_skill_directories
460
+
461
+ init_db()
462
+ sync_skill_directories()
463
+ skills = list_skills(level=args.level, tag=args.tag, source_kind=args.source_kind)
464
+ if args.json:
465
+ print(json.dumps(skills, indent=2, ensure_ascii=False))
466
+ return 0
467
+
468
+ if not skills:
469
+ print("No skills found.")
470
+ return 0
471
+
472
+ for skill in skills:
473
+ print(
474
+ f"[{skill['id']}] {skill['name']} "
475
+ f"({skill['level']}, {skill.get('mode', 'guide')}, {skill.get('source_kind', 'personal')}, "
476
+ f"trust={skill['trust_score']}, used={skill['use_count']}x)"
477
+ )
478
+ return 0
479
+
480
+
481
+ def _skills_get(args):
482
+ from db import get_skill, init_db, sync_skill_directories
483
+
484
+ init_db()
485
+ sync_skill_directories()
486
+ skill = get_skill(args.id)
487
+ if not skill:
488
+ print(f"Skill not found: {args.id}", file=sys.stderr)
489
+ return 1
490
+ if args.json:
491
+ print(json.dumps(skill, indent=2, ensure_ascii=False))
492
+ else:
493
+ print(json.dumps(skill, indent=2, ensure_ascii=False))
494
+ return 0
495
+
496
+
497
+ def _skills_apply(args):
498
+ from skills_runtime import apply_skill
499
+
500
+ try:
501
+ params = json.loads(args.params) if args.params else {}
502
+ except json.JSONDecodeError as e:
503
+ print(f"Invalid params JSON: {e}", file=sys.stderr)
504
+ return 1
505
+
506
+ result = apply_skill(args.id, params=params, mode=args.mode, dry_run=args.dry_run, context=args.context)
507
+ if args.json:
508
+ print(json.dumps(result, indent=2, ensure_ascii=False))
509
+ else:
510
+ print(json.dumps(result, indent=2, ensure_ascii=False))
511
+ return 0 if result.get("ok") else 1
512
+
513
+
514
+ def _skills_sync(args):
515
+ from skills_runtime import sync_skills
516
+
517
+ result = sync_skills()
518
+ if args.json:
519
+ print(json.dumps(result, indent=2, ensure_ascii=False))
520
+ else:
521
+ print(json.dumps(result, indent=2, ensure_ascii=False))
522
+ return 0 if not result.get("issues") else 1
523
+
524
+
525
+ def _skills_approve(args):
526
+ from skills_runtime import approve_skill_execution
527
+
528
+ result = approve_skill_execution(args.id, execution_level=args.execution_level, approved_by=args.approved_by)
529
+ if "error" in result:
530
+ print(result["error"], file=sys.stderr)
531
+ return 1
532
+ if args.json:
533
+ print(json.dumps(result, indent=2, ensure_ascii=False))
534
+ else:
535
+ print(json.dumps(result, indent=2, ensure_ascii=False))
536
+ return 0
537
+
538
+
539
+ def _skills_featured(args):
540
+ from skills_runtime import get_featured_skill_summaries
541
+
542
+ result = get_featured_skill_summaries(limit=args.limit)
543
+ if args.json:
544
+ print(json.dumps(result, indent=2, ensure_ascii=False))
545
+ else:
546
+ print(json.dumps(result, indent=2, ensure_ascii=False))
547
+ return 0
548
+
549
+
550
+ def _skills_evolution(args):
551
+ from skills_runtime import list_evolution_candidates
552
+
553
+ result = list_evolution_candidates()
554
+ if args.json:
555
+ print(json.dumps(result, indent=2, ensure_ascii=False))
556
+ else:
557
+ print(json.dumps(result, indent=2, ensure_ascii=False))
558
+ return 0
559
+
560
+
561
+ def _print_help():
562
+ v = _get_version()
563
+ print(f"""NEXO Runtime CLI v{v}
564
+
565
+ Commands:
566
+ nexo doctor [--tier boot|runtime|deep|all] [--fix] System diagnostics
567
+ nexo scripts list|run|doctor|call Personal scripts
568
+ nexo skills list|apply|sync|approve Executable skills
569
+ nexo update Sync repo to NEXO_HOME
570
+ nexo dashboard on|off|status Web dashboard control
571
+ nexo orchestrator on|off|status Autonomous mode control
572
+
573
+ Run 'nexo <command> --help' for details.
574
+ Homepage: https://nexo-brain.com
575
+ GitHub: https://github.com/wazionapps/nexo""")
576
+
577
+
578
+ def main():
579
+ parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI", add_help=False)
580
+ parser.add_argument("-h", "--help", action="store_true", help="Show help")
581
+ parser.add_argument("-v", "--version", action="store_true", help="Show version")
582
+ sub = parser.add_subparsers(dest="command")
583
+
584
+ # -- scripts --
585
+ scripts_parser = sub.add_parser("scripts", help="Manage personal scripts")
586
+ scripts_sub = scripts_parser.add_subparsers(dest="scripts_command")
587
+
588
+ # scripts list
589
+ list_p = scripts_sub.add_parser("list", help="List scripts")
590
+ list_p.add_argument("--all", action="store_true", help="Include core/internal scripts")
591
+ list_p.add_argument("--json", action="store_true", help="JSON output")
592
+
593
+ # scripts run
594
+ run_p = scripts_sub.add_parser("run", help="Run a script by name")
595
+ run_p.add_argument("name", help="Script name")
596
+ run_p.add_argument("script_args", nargs="*", help="Arguments to pass to the script")
597
+
598
+ # scripts doctor
599
+ doc_p = scripts_sub.add_parser("doctor", help="Validate scripts")
600
+ doc_p.add_argument("name", nargs="?", help="Specific script to check")
601
+ doc_p.add_argument("--json", action="store_true", help="JSON output")
602
+
603
+ # scripts call
604
+ call_p = scripts_sub.add_parser("call", help="Call a NEXO MCP tool")
605
+ call_p.add_argument("tool", help="MCP tool name")
606
+ call_p.add_argument("--input", default="{}", help="JSON input payload")
607
+ call_p.add_argument("--json-output", action="store_true", help="Force JSON output")
608
+
609
+ # -- update --
610
+ update_parser = sub.add_parser("update", help="Sync all repo files to NEXO_HOME")
611
+ update_parser.add_argument("--json", action="store_true", help="JSON output")
612
+
613
+ # -- doctor --
614
+ doctor_parser = sub.add_parser("doctor", help="Unified diagnostics")
615
+ doctor_parser.add_argument("--tier", default="boot", choices=["boot", "runtime", "deep", "all"],
616
+ help="Diagnostic tier (default: boot)")
617
+ doctor_parser.add_argument("--json", action="store_true", help="JSON output")
618
+ doctor_parser.add_argument("--fix", action="store_true", help="Apply deterministic fixes")
619
+
620
+ # -- skills --
621
+ skills_parser = sub.add_parser("skills", help="Skills v2 runtime")
622
+ skills_sub = skills_parser.add_subparsers(dest="skills_command")
623
+
624
+ skills_list_p = skills_sub.add_parser("list", help="List skills")
625
+ skills_list_p.add_argument("--level", default="", help="Filter by level")
626
+ skills_list_p.add_argument("--tag", default="", help="Filter by tag")
627
+ skills_list_p.add_argument("--source-kind", default="", help="Filter by source kind")
628
+ skills_list_p.add_argument("--json", action="store_true", help="JSON output")
629
+
630
+ skills_get_p = skills_sub.add_parser("get", help="Get skill")
631
+ skills_get_p.add_argument("id", help="Skill ID")
632
+ skills_get_p.add_argument("--json", action="store_true", help="JSON output")
633
+
634
+ skills_apply_p = skills_sub.add_parser("apply", help="Apply a skill")
635
+ skills_apply_p.add_argument("id", help="Skill ID")
636
+ skills_apply_p.add_argument("--params", default="{}", help="JSON parameters")
637
+ skills_apply_p.add_argument("--mode", default="auto", choices=["auto", "guide", "execute", "hybrid"])
638
+ skills_apply_p.add_argument("--dry-run", action="store_true", help="Render without executing")
639
+ skills_apply_p.add_argument("--context", default="", help="Usage context for feedback loop")
640
+ skills_apply_p.add_argument("--json", action="store_true", help="JSON output")
641
+
642
+ skills_sync_p = skills_sub.add_parser("sync", help="Sync filesystem skills")
643
+ skills_sync_p.add_argument("--json", action="store_true", help="JSON output")
644
+
645
+ skills_approve_p = skills_sub.add_parser("approve", help="Approve an executable skill")
646
+ skills_approve_p.add_argument("id", help="Skill ID")
647
+ skills_approve_p.add_argument("--execution-level", default="", choices=["", "read-only", "local", "remote"])
648
+ skills_approve_p.add_argument("--approved-by", default="", help="Approver name")
649
+ skills_approve_p.add_argument("--json", action="store_true", help="JSON output")
650
+
651
+ skills_featured_p = skills_sub.add_parser("featured", help="Featured startup skills")
652
+ skills_featured_p.add_argument("--limit", type=int, default=5)
653
+ skills_featured_p.add_argument("--json", action="store_true", help="JSON output")
654
+
655
+ skills_evolution_p = skills_sub.add_parser("evolution", help="Evolution candidates")
656
+ skills_evolution_p.add_argument("--json", action="store_true", help="JSON output")
657
+
658
+ # -- dashboard --
659
+ dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
660
+ dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
661
+
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
+ args = parser.parse_args()
667
+
668
+ if args.help or (not args.command and not args.version):
669
+ _print_help()
670
+ return 0
671
+ if args.version:
672
+ print(f"nexo v{_get_version()}")
673
+ return 0
674
+
675
+ if args.command == "scripts":
676
+ if args.scripts_command == "list":
677
+ return _scripts_list(args)
678
+ elif args.scripts_command == "run":
679
+ return _scripts_run(args)
680
+ elif args.scripts_command == "doctor":
681
+ return _scripts_doctor(args)
682
+ elif args.scripts_command == "call":
683
+ return _scripts_call(args)
684
+ else:
685
+ scripts_parser.print_help()
686
+ return 0
687
+ elif args.command == "update":
688
+ return _update(args)
689
+ elif args.command == "doctor":
690
+ return _doctor(args)
691
+ elif args.command == "skills":
692
+ if args.skills_command == "list":
693
+ return _skills_list(args)
694
+ elif args.skills_command == "get":
695
+ return _skills_get(args)
696
+ elif args.skills_command == "apply":
697
+ return _skills_apply(args)
698
+ elif args.skills_command == "sync":
699
+ return _skills_sync(args)
700
+ elif args.skills_command == "approve":
701
+ return _skills_approve(args)
702
+ elif args.skills_command == "featured":
703
+ return _skills_featured(args)
704
+ elif args.skills_command == "evolution":
705
+ return _skills_evolution(args)
706
+ else:
707
+ skills_parser.print_help()
708
+ return 0
709
+ elif args.command == "dashboard":
710
+ return _dashboard(args)
711
+ elif args.command == "orchestrator":
712
+ return _orchestrator(args)
713
+ else:
714
+ _print_help()
715
+ return 0
716
+
717
+
718
+ if __name__ == "__main__":
719
+ raise SystemExit(main())