nexo-brain 2.5.1 → 2.6.1

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