nexo-brain 2.4.0 → 2.5.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.
Files changed (80) hide show
  1. package/README.md +65 -2
  2. package/bin/nexo-brain.js +208 -11
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +5 -2
  6. package/src/auto_update.py +158 -8
  7. package/src/cli.py +605 -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/hooks/post-compact.sh +5 -1
  54. package/src/hooks/pre-compact.sh +1 -1
  55. package/src/plugins/doctor.py +36 -0
  56. package/src/plugins/evolution.py +2 -1
  57. package/src/plugins/skills.py +135 -175
  58. package/src/requirements.txt +1 -0
  59. package/src/script_registry.py +322 -0
  60. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  61. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  62. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  63. package/src/scripts/deep-sleep/synthesize.py +37 -1
  64. package/src/scripts/nexo-dashboard.sh +29 -0
  65. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  66. package/src/scripts/nexo-evolution-run.py +2 -1
  67. package/src/scripts/nexo-learning-housekeep.py +1 -1
  68. package/src/scripts/nexo-watchdog.sh +1 -1
  69. package/src/server.py +9 -5
  70. package/src/skills/run-runtime-doctor/guide.md +12 -0
  71. package/src/skills/run-runtime-doctor/script.py +21 -0
  72. package/src/skills/run-runtime-doctor/skill.json +25 -0
  73. package/src/skills_runtime.py +347 -0
  74. package/src/tools_menu.py +3 -2
  75. package/src/tools_sessions.py +126 -0
  76. package/src/user_context.py +46 -0
  77. package/templates/nexo_helper.py +45 -0
  78. package/templates/script-template.py +44 -0
  79. package/templates/skill-script-template.py +39 -0
  80. package/templates/skill-template.md +33 -0
package/src/cli.py ADDED
@@ -0,0 +1,605 @@
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
+ # Ensure src/ is on path for imports
34
+ if str(NEXO_CODE) not in sys.path:
35
+ sys.path.insert(0, str(NEXO_CODE))
36
+
37
+
38
+ def _scripts_list(args):
39
+ from script_registry import list_scripts
40
+ scripts = list_scripts(include_core=args.all)
41
+ if args.json:
42
+ print(json.dumps(scripts, indent=2))
43
+ else:
44
+ if not scripts:
45
+ print("No personal scripts found in", NEXO_HOME / "scripts")
46
+ return 0
47
+ # Table output
48
+ name_w = max(len(s["name"]) for s in scripts)
49
+ rt_w = max(len(s["runtime"]) for s in scripts)
50
+ for s in scripts:
51
+ tag = " [core]" if s.get("core") else ""
52
+ print(f" {s['name']:<{name_w}} {s['runtime']:<{rt_w}} {s['description']}{tag}")
53
+ return 0
54
+
55
+
56
+ def _scripts_run(args):
57
+ from script_registry import resolve_script_reference
58
+
59
+ info = resolve_script_reference(args.name)
60
+ if not info:
61
+ print(f"Script not found: {args.name}", file=sys.stderr)
62
+ return 1
63
+
64
+ path = Path(info["path"])
65
+ runtime = info["runtime"]
66
+ meta = info["metadata"]
67
+ is_core = info.get("core", False)
68
+
69
+ # Build environment
70
+ env = {
71
+ **os.environ,
72
+ "NEXO_HOME": str(NEXO_HOME),
73
+ "NEXO_CODE": str(NEXO_CODE),
74
+ "NEXO_SCRIPT_NAME": info["name"],
75
+ "NEXO_SCRIPT_PATH": str(path),
76
+ "NEXO_CLI": "nexo",
77
+ }
78
+
79
+ # Only inject DB paths for core scripts
80
+ if is_core:
81
+ env["NEXO_DB"] = str(NEXO_HOME / "data" / "nexo.db")
82
+ env["NEXO_COGNITIVE_DB"] = str(NEXO_HOME / "data" / "cognitive.db")
83
+
84
+ # Timeout
85
+ timeout = None
86
+ timeout_str = meta.get("timeout", "")
87
+ if timeout_str:
88
+ try:
89
+ timeout = int(timeout_str)
90
+ except ValueError:
91
+ pass
92
+
93
+ # Build command
94
+ if runtime == "python":
95
+ cmd = [sys.executable, str(path)] + args.script_args
96
+ elif runtime == "shell":
97
+ cmd = ["bash", str(path)] + args.script_args
98
+ else:
99
+ # Try to execute directly
100
+ cmd = [str(path)] + args.script_args
101
+
102
+ try:
103
+ result = subprocess.run(cmd, env=env, timeout=timeout)
104
+ return result.returncode
105
+ except subprocess.TimeoutExpired:
106
+ print(f"Script timed out after {timeout}s", file=sys.stderr)
107
+ return 124
108
+ except Exception as e:
109
+ print(f"Error running script: {e}", file=sys.stderr)
110
+ return 1
111
+
112
+
113
+ def _scripts_doctor(args):
114
+ from script_registry import doctor_script, doctor_all_scripts
115
+
116
+ if args.name:
117
+ results = [doctor_script(args.name)]
118
+ else:
119
+ results = doctor_all_scripts()
120
+
121
+ if args.json:
122
+ print(json.dumps(results, indent=2))
123
+ else:
124
+ if not results:
125
+ print("No personal scripts to check.")
126
+ return 0
127
+ any_fail = False
128
+ for r in results:
129
+ name = r.get("name", "?")
130
+ status = r.get("status", "?")
131
+ icon = {"pass": "✓", "warn": "⚠", "fail": "✗"}.get(status, "?")
132
+ print(f"\n{icon} {name} [{status}]")
133
+ for item in r.get("items", []):
134
+ lvl = item["level"]
135
+ prefix = {"pass": " ✓", "warn": " ⚠", "fail": " ✗"}.get(lvl, " ?")
136
+ print(f"{prefix} {item['msg']}")
137
+ if status == "fail":
138
+ any_fail = True
139
+ print()
140
+ return 1 if any_fail else 0
141
+
142
+ return 0
143
+
144
+
145
+ def _scripts_call(args):
146
+ """Call a NEXO MCP tool via in-process fastmcp client."""
147
+ tool_name = args.tool
148
+ try:
149
+ payload = json.loads(args.input) if args.input else {}
150
+ except json.JSONDecodeError as e:
151
+ print(f"Invalid JSON input: {e}", file=sys.stderr)
152
+ return 1
153
+
154
+ def _bootstrap_mcp():
155
+ os.environ["NEXO_CLI_MODE"] = "1"
156
+ from db import init_db
157
+ from plugin_loader import load_all_plugins
158
+ from server import mcp
159
+
160
+ init_db()
161
+
162
+ # Plugin loading is required so scripts can call plugin tools such as
163
+ # nexo_doctor, but the loader is noisy on stderr and would pollute CLI output.
164
+ with contextlib.redirect_stderr(io.StringIO()):
165
+ load_all_plugins(mcp)
166
+
167
+ return mcp
168
+
169
+ def _extract_tool_value(result):
170
+ structured = getattr(result, "structured_content", None)
171
+ if structured not in (None, {}):
172
+ return structured
173
+
174
+ content = getattr(result, "content", None)
175
+ if isinstance(content, list):
176
+ texts = [item.text for item in content if hasattr(item, "text")]
177
+ if texts:
178
+ return "\n".join(texts)
179
+
180
+ dumped = getattr(result, "model_dump", None)
181
+ if callable(dumped):
182
+ data = dumped()
183
+ if isinstance(data, dict):
184
+ return data.get("structured_content") or data.get("content") or data
185
+
186
+ return str(result)
187
+
188
+ try:
189
+ mcp = _bootstrap_mcp()
190
+
191
+ async def _call():
192
+ tool = await mcp.get_tool(tool_name)
193
+ if tool is None:
194
+ tools = await mcp.list_tools()
195
+ available = sorted(t.name for t in tools)
196
+ raise LookupError(
197
+ f"Tool not found: {tool_name}\nAvailable tools: {', '.join(available)}"
198
+ )
199
+ return await mcp.call_tool(tool_name, payload)
200
+
201
+ result = asyncio.run(_call())
202
+ value = _extract_tool_value(result)
203
+
204
+ if args.json_output:
205
+ if (
206
+ isinstance(value, dict)
207
+ and set(value.keys()) == {"result"}
208
+ and isinstance(value["result"], str)
209
+ ):
210
+ try:
211
+ value = json.loads(value["result"])
212
+ except json.JSONDecodeError:
213
+ pass
214
+ if isinstance(value, str):
215
+ try:
216
+ value = json.loads(value)
217
+ except json.JSONDecodeError:
218
+ value = {"result": value}
219
+ elif not isinstance(value, (dict, list)):
220
+ value = {"result": value}
221
+ print(json.dumps(value, indent=2, ensure_ascii=False))
222
+ return 0
223
+
224
+ if isinstance(value, dict) and set(value.keys()) == {"result"} and isinstance(value["result"], str):
225
+ print(value["result"])
226
+ elif isinstance(value, (dict, list)):
227
+ print(json.dumps(value, indent=2, ensure_ascii=False))
228
+ else:
229
+ print(value)
230
+ return 0
231
+
232
+ except LookupError as e:
233
+ print(str(e), file=sys.stderr)
234
+ return 1
235
+ except Exception as e:
236
+ print(f"Error calling tool {tool_name}: {e}", file=sys.stderr)
237
+ return 1
238
+
239
+
240
+ def _update(args):
241
+ """Sync all repo files to NEXO_HOME."""
242
+ import shutil
243
+
244
+ src_dir = NEXO_CODE
245
+ repo_dir = NEXO_CODE.parent
246
+ dest = NEXO_HOME
247
+
248
+ # Packages (directories with __init__.py or known structure)
249
+ packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
250
+ copied_packages = 0
251
+ for pkg in packages:
252
+ pkg_src = src_dir / pkg
253
+ pkg_dest = dest / pkg
254
+ if pkg_src.is_dir():
255
+ if pkg_dest.exists():
256
+ shutil.rmtree(str(pkg_dest), ignore_errors=True)
257
+ shutil.copytree(
258
+ str(pkg_src), str(pkg_dest),
259
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.db"),
260
+ )
261
+ copied_packages += 1
262
+
263
+ # Flat Python files
264
+ flat_files = [
265
+ "server.py", "plugin_loader.py", "knowledge_graph.py", "kg_populate.py",
266
+ "maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
267
+ "evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
268
+ "auto_update.py", "tools_sessions.py", "tools_coordination.py",
269
+ "tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
270
+ "tools_credentials.py", "tools_task_history.py", "tools_menu.py",
271
+ "cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
272
+ "requirements.txt",
273
+ ]
274
+ copied_files = 0
275
+ for f in flat_files:
276
+ src_f = src_dir / f
277
+ if src_f.is_file():
278
+ shutil.copy2(str(src_f), str(dest / f))
279
+ copied_files += 1
280
+
281
+ # Plugins
282
+ plugins_src = src_dir / "plugins"
283
+ plugins_dest = dest / "plugins"
284
+ if plugins_src.is_dir():
285
+ plugins_dest.mkdir(parents=True, exist_ok=True)
286
+ for f in plugins_src.iterdir():
287
+ if f.is_file() and f.suffix == ".py":
288
+ shutil.copy2(str(f), str(plugins_dest / f.name))
289
+
290
+ # Scripts
291
+ scripts_src = src_dir / "scripts"
292
+ scripts_dest = dest / "scripts"
293
+ if scripts_src.is_dir():
294
+ for f in scripts_src.iterdir():
295
+ if f.name == "__pycache__" or f.name.startswith("."):
296
+ continue
297
+ dst = scripts_dest / f.name
298
+ if f.is_dir():
299
+ if dst.exists():
300
+ shutil.rmtree(str(dst), ignore_errors=True)
301
+ shutil.copytree(str(f), str(dst), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
302
+ elif f.is_file():
303
+ shutil.copy2(str(f), str(dst))
304
+ if f.suffix == ".sh":
305
+ dst.chmod(0o755)
306
+
307
+ # Templates
308
+ templates_src = repo_dir / "templates"
309
+ templates_dest = dest / "templates"
310
+ if templates_src.is_dir():
311
+ templates_dest.mkdir(parents=True, exist_ok=True)
312
+ for f in templates_src.iterdir():
313
+ if f.is_file():
314
+ shutil.copy2(str(f), str(templates_dest / f.name))
315
+
316
+ # Core skills
317
+ skills_src = src_dir / "skills"
318
+ skills_dest = dest / "skills-core"
319
+ if skills_src.is_dir():
320
+ if skills_dest.exists():
321
+ shutil.rmtree(str(skills_dest), ignore_errors=True)
322
+ shutil.copytree(
323
+ str(skills_src), str(skills_dest),
324
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
325
+ )
326
+
327
+ # Runtime CLI wrapper
328
+ bin_dir = dest / "bin"
329
+ bin_dir.mkdir(parents=True, exist_ok=True)
330
+ wrapper = bin_dir / "nexo"
331
+ wrapper_content = (
332
+ "#!/usr/bin/env bash\n"
333
+ "set -euo pipefail\n\n"
334
+ f'NEXO_HOME="{dest}"\n'
335
+ 'PYTHON="$NEXO_HOME/.venv/bin/python3"\n'
336
+ 'if [ ! -x "$PYTHON" ]; then\n'
337
+ ' if command -v python3 >/dev/null 2>&1; then PYTHON="python3"; else PYTHON="python"; fi\n'
338
+ 'fi\n'
339
+ 'export NEXO_HOME\n'
340
+ 'export NEXO_CODE="$NEXO_HOME"\n'
341
+ 'exec "$PYTHON" "$NEXO_HOME/cli.py" "$@"\n'
342
+ )
343
+ wrapper.write_text(wrapper_content)
344
+ wrapper.chmod(0o755)
345
+
346
+ result = {
347
+ "packages": copied_packages,
348
+ "files": copied_files,
349
+ "nexo_home": str(dest),
350
+ "source": str(src_dir),
351
+ }
352
+ if args.json:
353
+ print(json.dumps(result, indent=2))
354
+ else:
355
+ print(f"Updated NEXO_HOME ({dest})")
356
+ print(f" {copied_packages} packages, {copied_files} files synced from {src_dir}")
357
+ return 0
358
+
359
+
360
+ def _doctor(args):
361
+ """Run unified doctor diagnostics."""
362
+ try:
363
+ from db import init_db
364
+ from doctor.orchestrator import run_doctor
365
+ from doctor.formatters import format_report
366
+ except ImportError:
367
+ print("Doctor module not found. Ensure NEXO is properly installed.", file=sys.stderr)
368
+ return 1
369
+
370
+ init_db()
371
+ report = run_doctor(tier=args.tier, fix=args.fix)
372
+ output = format_report(report, fmt="json" if args.json else "text")
373
+ print(output)
374
+
375
+ if report.overall_status == "critical":
376
+ return 2
377
+ elif report.overall_status == "degraded":
378
+ return 1
379
+ return 0
380
+
381
+
382
+ def _skills_list(args):
383
+ from db import init_db, list_skills, sync_skill_directories
384
+
385
+ init_db()
386
+ sync_skill_directories()
387
+ skills = list_skills(level=args.level, tag=args.tag, source_kind=args.source_kind)
388
+ if args.json:
389
+ print(json.dumps(skills, indent=2, ensure_ascii=False))
390
+ return 0
391
+
392
+ if not skills:
393
+ print("No skills found.")
394
+ return 0
395
+
396
+ for skill in skills:
397
+ print(
398
+ f"[{skill['id']}] {skill['name']} "
399
+ f"({skill['level']}, {skill.get('mode', 'guide')}, {skill.get('source_kind', 'personal')}, "
400
+ f"trust={skill['trust_score']}, used={skill['use_count']}x)"
401
+ )
402
+ return 0
403
+
404
+
405
+ def _skills_get(args):
406
+ from db import get_skill, init_db, sync_skill_directories
407
+
408
+ init_db()
409
+ sync_skill_directories()
410
+ skill = get_skill(args.id)
411
+ if not skill:
412
+ print(f"Skill not found: {args.id}", file=sys.stderr)
413
+ return 1
414
+ if args.json:
415
+ print(json.dumps(skill, indent=2, ensure_ascii=False))
416
+ else:
417
+ print(json.dumps(skill, indent=2, ensure_ascii=False))
418
+ return 0
419
+
420
+
421
+ def _skills_apply(args):
422
+ from skills_runtime import apply_skill
423
+
424
+ try:
425
+ params = json.loads(args.params) if args.params else {}
426
+ except json.JSONDecodeError as e:
427
+ print(f"Invalid params JSON: {e}", file=sys.stderr)
428
+ return 1
429
+
430
+ result = apply_skill(args.id, params=params, mode=args.mode, dry_run=args.dry_run, context=args.context)
431
+ if args.json:
432
+ print(json.dumps(result, indent=2, ensure_ascii=False))
433
+ else:
434
+ print(json.dumps(result, indent=2, ensure_ascii=False))
435
+ return 0 if result.get("ok") else 1
436
+
437
+
438
+ def _skills_sync(args):
439
+ from skills_runtime import sync_skills
440
+
441
+ result = sync_skills()
442
+ if args.json:
443
+ print(json.dumps(result, indent=2, ensure_ascii=False))
444
+ else:
445
+ print(json.dumps(result, indent=2, ensure_ascii=False))
446
+ return 0 if not result.get("issues") else 1
447
+
448
+
449
+ def _skills_approve(args):
450
+ from skills_runtime import approve_skill_execution
451
+
452
+ result = approve_skill_execution(args.id, execution_level=args.execution_level, approved_by=args.approved_by)
453
+ if "error" in result:
454
+ print(result["error"], file=sys.stderr)
455
+ return 1
456
+ if args.json:
457
+ print(json.dumps(result, indent=2, ensure_ascii=False))
458
+ else:
459
+ print(json.dumps(result, indent=2, ensure_ascii=False))
460
+ return 0
461
+
462
+
463
+ def _skills_featured(args):
464
+ from skills_runtime import get_featured_skill_summaries
465
+
466
+ result = get_featured_skill_summaries(limit=args.limit)
467
+ if args.json:
468
+ print(json.dumps(result, indent=2, ensure_ascii=False))
469
+ else:
470
+ print(json.dumps(result, indent=2, ensure_ascii=False))
471
+ return 0
472
+
473
+
474
+ def _skills_evolution(args):
475
+ from skills_runtime import list_evolution_candidates
476
+
477
+ result = list_evolution_candidates()
478
+ if args.json:
479
+ print(json.dumps(result, indent=2, ensure_ascii=False))
480
+ else:
481
+ print(json.dumps(result, indent=2, ensure_ascii=False))
482
+ return 0
483
+
484
+
485
+ def main():
486
+ parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI")
487
+ sub = parser.add_subparsers(dest="command")
488
+
489
+ # -- scripts --
490
+ scripts_parser = sub.add_parser("scripts", help="Manage personal scripts")
491
+ scripts_sub = scripts_parser.add_subparsers(dest="scripts_command")
492
+
493
+ # scripts list
494
+ list_p = scripts_sub.add_parser("list", help="List scripts")
495
+ list_p.add_argument("--all", action="store_true", help="Include core/internal scripts")
496
+ list_p.add_argument("--json", action="store_true", help="JSON output")
497
+
498
+ # scripts run
499
+ run_p = scripts_sub.add_parser("run", help="Run a script by name")
500
+ run_p.add_argument("name", help="Script name")
501
+ run_p.add_argument("script_args", nargs="*", help="Arguments to pass to the script")
502
+
503
+ # scripts doctor
504
+ doc_p = scripts_sub.add_parser("doctor", help="Validate scripts")
505
+ doc_p.add_argument("name", nargs="?", help="Specific script to check")
506
+ doc_p.add_argument("--json", action="store_true", help="JSON output")
507
+
508
+ # scripts call
509
+ call_p = scripts_sub.add_parser("call", help="Call a NEXO MCP tool")
510
+ call_p.add_argument("tool", help="MCP tool name")
511
+ call_p.add_argument("--input", default="{}", help="JSON input payload")
512
+ call_p.add_argument("--json-output", action="store_true", help="Force JSON output")
513
+
514
+ # -- update --
515
+ update_parser = sub.add_parser("update", help="Sync all repo files to NEXO_HOME")
516
+ update_parser.add_argument("--json", action="store_true", help="JSON output")
517
+
518
+ # -- doctor --
519
+ doctor_parser = sub.add_parser("doctor", help="Unified diagnostics")
520
+ doctor_parser.add_argument("--tier", default="boot", choices=["boot", "runtime", "deep", "all"],
521
+ help="Diagnostic tier (default: boot)")
522
+ doctor_parser.add_argument("--json", action="store_true", help="JSON output")
523
+ doctor_parser.add_argument("--fix", action="store_true", help="Apply deterministic fixes")
524
+
525
+ # -- skills --
526
+ skills_parser = sub.add_parser("skills", help="Skills v2 runtime")
527
+ skills_sub = skills_parser.add_subparsers(dest="skills_command")
528
+
529
+ skills_list_p = skills_sub.add_parser("list", help="List skills")
530
+ skills_list_p.add_argument("--level", default="", help="Filter by level")
531
+ skills_list_p.add_argument("--tag", default="", help="Filter by tag")
532
+ skills_list_p.add_argument("--source-kind", default="", help="Filter by source kind")
533
+ skills_list_p.add_argument("--json", action="store_true", help="JSON output")
534
+
535
+ skills_get_p = skills_sub.add_parser("get", help="Get skill")
536
+ skills_get_p.add_argument("id", help="Skill ID")
537
+ skills_get_p.add_argument("--json", action="store_true", help="JSON output")
538
+
539
+ skills_apply_p = skills_sub.add_parser("apply", help="Apply a skill")
540
+ skills_apply_p.add_argument("id", help="Skill ID")
541
+ skills_apply_p.add_argument("--params", default="{}", help="JSON parameters")
542
+ skills_apply_p.add_argument("--mode", default="auto", choices=["auto", "guide", "execute", "hybrid"])
543
+ skills_apply_p.add_argument("--dry-run", action="store_true", help="Render without executing")
544
+ skills_apply_p.add_argument("--context", default="", help="Usage context for feedback loop")
545
+ skills_apply_p.add_argument("--json", action="store_true", help="JSON output")
546
+
547
+ skills_sync_p = skills_sub.add_parser("sync", help="Sync filesystem skills")
548
+ skills_sync_p.add_argument("--json", action="store_true", help="JSON output")
549
+
550
+ skills_approve_p = skills_sub.add_parser("approve", help="Approve an executable skill")
551
+ skills_approve_p.add_argument("id", help="Skill ID")
552
+ skills_approve_p.add_argument("--execution-level", default="", choices=["", "read-only", "local", "remote"])
553
+ skills_approve_p.add_argument("--approved-by", default="", help="Approver name")
554
+ skills_approve_p.add_argument("--json", action="store_true", help="JSON output")
555
+
556
+ skills_featured_p = skills_sub.add_parser("featured", help="Featured startup skills")
557
+ skills_featured_p.add_argument("--limit", type=int, default=5)
558
+ skills_featured_p.add_argument("--json", action="store_true", help="JSON output")
559
+
560
+ skills_evolution_p = skills_sub.add_parser("evolution", help="Evolution candidates")
561
+ skills_evolution_p.add_argument("--json", action="store_true", help="JSON output")
562
+
563
+ args = parser.parse_args()
564
+
565
+ if args.command == "scripts":
566
+ if args.scripts_command == "list":
567
+ return _scripts_list(args)
568
+ elif args.scripts_command == "run":
569
+ return _scripts_run(args)
570
+ elif args.scripts_command == "doctor":
571
+ return _scripts_doctor(args)
572
+ elif args.scripts_command == "call":
573
+ return _scripts_call(args)
574
+ else:
575
+ scripts_parser.print_help()
576
+ return 0
577
+ elif args.command == "update":
578
+ return _update(args)
579
+ elif args.command == "doctor":
580
+ return _doctor(args)
581
+ elif args.command == "skills":
582
+ if args.skills_command == "list":
583
+ return _skills_list(args)
584
+ elif args.skills_command == "get":
585
+ return _skills_get(args)
586
+ elif args.skills_command == "apply":
587
+ return _skills_apply(args)
588
+ elif args.skills_command == "sync":
589
+ return _skills_sync(args)
590
+ elif args.skills_command == "approve":
591
+ return _skills_approve(args)
592
+ elif args.skills_command == "featured":
593
+ return _skills_featured(args)
594
+ elif args.skills_command == "evolution":
595
+ return _skills_evolution(args)
596
+ else:
597
+ skills_parser.print_help()
598
+ return 0
599
+ else:
600
+ parser.print_help()
601
+ return 0
602
+
603
+
604
+ if __name__ == "__main__":
605
+ raise SystemExit(main())
@@ -79,7 +79,7 @@ def ingest(
79
79
 
80
80
  # Auto-pin: corrections and blocking learnings get pinned (zero decay, +0.2 boost)
81
81
  # This ensures user's corrections NEVER fade away
82
- _pin_lifecycle = None
82
+ _pin_lifecycle = 'active'
83
83
  if auto_pin or (source_type in ('learning', 'feedback') and
84
84
  any(kw in content.upper() for kw in ('BLOCKING', 'CRÍTICO', 'CRITICAL', 'NUNCA', 'NEVER', 'PROHIBIDO'))):
85
85
  _pin_lifecycle = 'pinned'
@@ -398,20 +398,20 @@ def get_stats() -> dict:
398
398
  """Return statistics about the cognitive memory system."""
399
399
  db = _get_db()
400
400
 
401
- stm_active = db.execute("SELECT COUNT(*) FROM stm_memories WHERE lifecycle_state = 'active' AND promoted_to_ltm = 0").fetchone()[0]
401
+ stm_active = db.execute("SELECT COUNT(*) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0").fetchone()[0]
402
402
  stm_promoted = db.execute("SELECT COUNT(*) FROM stm_memories WHERE promoted_to_ltm = 1").fetchone()[0]
403
- stm_total = db.execute("SELECT COUNT(*) FROM stm_memories WHERE lifecycle_state = 'active'").fetchone()[0]
403
+ stm_total = db.execute("SELECT COUNT(*) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned')").fetchone()[0]
404
404
  ltm_active = db.execute("SELECT COUNT(*) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0]
405
405
  ltm_dormant = db.execute("SELECT COUNT(*) FROM ltm_memories WHERE is_dormant = 1").fetchone()[0]
406
406
 
407
- avg_stm = db.execute("SELECT AVG(strength) FROM stm_memories WHERE lifecycle_state = 'active' AND promoted_to_ltm = 0").fetchone()[0] or 0.0
407
+ avg_stm = db.execute("SELECT AVG(strength) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0").fetchone()[0] or 0.0
408
408
  avg_ltm = db.execute("SELECT AVG(strength) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0] or 0.0
409
409
 
410
410
  total_retrievals = db.execute("SELECT COUNT(*) FROM retrieval_log").fetchone()[0]
411
411
  avg_retrieval_score = db.execute("SELECT AVG(top_score) FROM retrieval_log").fetchone()[0] or 0.0
412
412
 
413
413
  top_domains_stm = db.execute(
414
- "SELECT domain, COUNT(*) as cnt FROM stm_memories WHERE lifecycle_state = 'active' AND promoted_to_ltm = 0 AND domain != '' GROUP BY domain ORDER BY cnt DESC LIMIT 5"
414
+ "SELECT domain, COUNT(*) as cnt FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0 AND domain != '' GROUP BY domain ORDER BY cnt DESC LIMIT 5"
415
415
  ).fetchall()
416
416
  top_domains_ltm = db.execute(
417
417
  "SELECT domain, COUNT(*) as cnt FROM ltm_memories WHERE is_dormant = 0 AND domain != '' GROUP BY domain ORDER BY cnt DESC LIMIT 5"
@@ -94,6 +94,14 @@
94
94
  "run_at_load": true,
95
95
  "description": "Morning catchup briefing for the user",
96
96
  "core": true
97
+ },
98
+ {
99
+ "id": "day-orchestrator",
100
+ "script": "scripts/nexo-day-orchestrator.sh",
101
+ "type": "shell",
102
+ "description": "Autonomous NEXO cycle — checks followups, emails, infra every 15 min (8:00-23:00)",
103
+ "core": true,
104
+ "optional": "orchestrator"
97
105
  }
98
106
  ]
99
107
  }