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.
- package/README.md +80 -4
- package/bin/nexo-brain.js +238 -12
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +11 -3
- package/src/auto_update.py +193 -9
- package/src/cli.py +719 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +700 -35
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -654
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +383 -578
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -346
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +521 -698
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -182
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +16 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +980 -274
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/evolution_cycle.py +86 -6
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +11 -3
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -48
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +141 -54
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- 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())
|