nexo-brain 2.6.11 → 2.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +22 -12
- package/bin/nexo-brain.js +483 -56
- package/package.json +4 -1
- package/src/agent_runner.py +322 -0
- package/src/auto_update.py +12 -3
- package/src/cli.py +22 -10
- package/src/client_preferences.py +394 -0
- package/src/client_sync.py +78 -0
- package/src/cron_recovery.py +8 -1
- package/src/crons/manifest.json +6 -0
- package/src/crons/sync.py +14 -1
- package/src/doctor/providers/runtime.py +109 -1
- package/src/plugins/schedule.py +69 -12
- package/src/plugins/update.py +5 -1
- package/src/runtime_power.py +23 -0
- package/src/script_registry.py +62 -1
- package/src/scripts/check-context.py +102 -100
- package/src/scripts/deep-sleep/extract.py +29 -54
- package/src/scripts/deep-sleep/synthesize.py +14 -38
- package/src/scripts/nexo-agent-run.py +73 -0
- package/src/scripts/nexo-catchup.py +15 -19
- package/src/scripts/nexo-daily-self-audit.py +17 -14
- package/src/scripts/nexo-evolution-run.py +25 -55
- package/src/scripts/nexo-immune.py +17 -15
- package/src/scripts/nexo-learning-validator.py +90 -58
- package/src/scripts/nexo-postmortem-consolidator.py +15 -14
- package/src/scripts/nexo-sleep.py +20 -14
- package/src/scripts/nexo-synthesis.py +19 -12
- package/src/scripts/nexo-update.sh +28 -2
- package/src/scripts/nexo-watchdog.sh +34 -10
- package/templates/nexo_helper.py +45 -0
- package/templates/plugin-template.py +4 -0
- package/templates/script-template.py +13 -2
- package/templates/skill-script-template.py +8 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Terminal client launchers and headless automation backend runner."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from client_preferences import (
|
|
12
|
+
BACKEND_NONE,
|
|
13
|
+
CLIENT_CLAUDE_CODE,
|
|
14
|
+
CLIENT_CODEX,
|
|
15
|
+
TERMINAL_CLIENT_KEYS,
|
|
16
|
+
load_client_preferences,
|
|
17
|
+
resolve_automation_backend,
|
|
18
|
+
resolve_client_runtime_profile,
|
|
19
|
+
resolve_terminal_client,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
24
|
+
CLAUDE_LEGACY_MODEL_HINTS = {"opus", "sonnet"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentRunnerError(RuntimeError):
|
|
28
|
+
"""Base exception for runner failures."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TerminalClientUnavailableError(AgentRunnerError):
|
|
32
|
+
"""Raised when the requested interactive client cannot be launched."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AutomationBackendUnavailableError(AgentRunnerError):
|
|
36
|
+
"""Raised when the configured automation backend is unavailable."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_claude_cli() -> str:
|
|
40
|
+
saved = NEXO_HOME / "config" / "claude-cli-path"
|
|
41
|
+
if saved.exists():
|
|
42
|
+
candidate = saved.read_text().strip()
|
|
43
|
+
if candidate and Path(candidate).exists():
|
|
44
|
+
return candidate
|
|
45
|
+
env_path = os.environ.get("CLAUDE_BIN", "").strip()
|
|
46
|
+
if env_path and Path(env_path).exists():
|
|
47
|
+
return env_path
|
|
48
|
+
discovered = shutil.which("claude")
|
|
49
|
+
if discovered:
|
|
50
|
+
return discovered
|
|
51
|
+
for candidate in (
|
|
52
|
+
Path.home() / ".local" / "bin" / "claude",
|
|
53
|
+
Path.home() / ".npm-global" / "bin" / "claude",
|
|
54
|
+
Path("/usr/local/bin/claude"),
|
|
55
|
+
):
|
|
56
|
+
if candidate.exists():
|
|
57
|
+
return str(candidate)
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resolve_codex_cli() -> str:
|
|
62
|
+
env_path = os.environ.get("CODEX_BIN", "").strip()
|
|
63
|
+
if env_path and Path(env_path).exists():
|
|
64
|
+
return env_path
|
|
65
|
+
return shutil.which("codex") or ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _headless_env(env: dict | None = None) -> dict:
|
|
69
|
+
merged = os.environ.copy()
|
|
70
|
+
if env:
|
|
71
|
+
merged.update(env)
|
|
72
|
+
merged["NEXO_HEADLESS"] = "1"
|
|
73
|
+
merged.pop("CLAUDECODE", None)
|
|
74
|
+
merged.pop("CLAUDE_CODE", None)
|
|
75
|
+
return merged
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_interactive_client_command(
|
|
79
|
+
*,
|
|
80
|
+
target: str | os.PathLike[str],
|
|
81
|
+
client: str | None = None,
|
|
82
|
+
preferences: dict | None = None,
|
|
83
|
+
) -> tuple[str, list[str]]:
|
|
84
|
+
prefs = preferences or load_client_preferences()
|
|
85
|
+
selected = resolve_terminal_client(client, preferences=prefs)
|
|
86
|
+
target_path = str(Path(target).expanduser())
|
|
87
|
+
profile = resolve_client_runtime_profile(selected, preferences=prefs)
|
|
88
|
+
|
|
89
|
+
if selected == CLIENT_CLAUDE_CODE:
|
|
90
|
+
claude_bin = _resolve_claude_cli()
|
|
91
|
+
if not claude_bin:
|
|
92
|
+
raise TerminalClientUnavailableError(
|
|
93
|
+
"Claude Code launcher not found in PATH. Install `claude` first."
|
|
94
|
+
)
|
|
95
|
+
cmd = [claude_bin]
|
|
96
|
+
if profile["model"]:
|
|
97
|
+
cmd.extend(["--model", profile["model"]])
|
|
98
|
+
if profile["reasoning_effort"]:
|
|
99
|
+
cmd.extend(["--effort", profile["reasoning_effort"]])
|
|
100
|
+
cmd.extend(["--dangerously-skip-permissions", target_path])
|
|
101
|
+
return selected, cmd
|
|
102
|
+
|
|
103
|
+
if selected == CLIENT_CODEX:
|
|
104
|
+
codex_bin = _resolve_codex_cli()
|
|
105
|
+
if not codex_bin:
|
|
106
|
+
raise TerminalClientUnavailableError(
|
|
107
|
+
"Codex launcher not found in PATH. Install `codex` first or reconfigure NEXO."
|
|
108
|
+
)
|
|
109
|
+
cmd = [codex_bin]
|
|
110
|
+
if profile["model"]:
|
|
111
|
+
cmd.extend(["-m", profile["model"]])
|
|
112
|
+
if profile["reasoning_effort"]:
|
|
113
|
+
cmd.extend(["-c", f'model_reasoning_effort="{profile["reasoning_effort"]}"'])
|
|
114
|
+
cmd.extend(["-C", target_path])
|
|
115
|
+
return selected, cmd
|
|
116
|
+
|
|
117
|
+
raise TerminalClientUnavailableError(f"Unsupported terminal client: {selected}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def launch_interactive_client(
|
|
121
|
+
*,
|
|
122
|
+
target: str | os.PathLike[str],
|
|
123
|
+
client: str | None = None,
|
|
124
|
+
env: dict | None = None,
|
|
125
|
+
preferences: dict | None = None,
|
|
126
|
+
) -> subprocess.CompletedProcess:
|
|
127
|
+
_, cmd = build_interactive_client_command(target=target, client=client, preferences=preferences)
|
|
128
|
+
launch_env = os.environ.copy()
|
|
129
|
+
if env:
|
|
130
|
+
launch_env.update(env)
|
|
131
|
+
return subprocess.run(cmd, env=launch_env)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _resolve_runtime_model_and_effort(
|
|
135
|
+
client: str,
|
|
136
|
+
*,
|
|
137
|
+
model: str | None = None,
|
|
138
|
+
reasoning_effort: str | None = None,
|
|
139
|
+
preferences: dict | None = None,
|
|
140
|
+
) -> tuple[str, str]:
|
|
141
|
+
profile = resolve_client_runtime_profile(client, preferences=preferences)
|
|
142
|
+
requested_model = str(model or "").strip()
|
|
143
|
+
requested_effort = str(reasoning_effort or "").strip().lower()
|
|
144
|
+
|
|
145
|
+
if client == CLIENT_CODEX:
|
|
146
|
+
if not requested_model or requested_model.lower() in CLAUDE_LEGACY_MODEL_HINTS:
|
|
147
|
+
requested_model = profile["model"]
|
|
148
|
+
elif not requested_model:
|
|
149
|
+
requested_model = profile["model"]
|
|
150
|
+
|
|
151
|
+
if not requested_effort:
|
|
152
|
+
requested_effort = profile["reasoning_effort"]
|
|
153
|
+
|
|
154
|
+
return requested_model, requested_effort
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_codex_prompt(
|
|
158
|
+
prompt: str,
|
|
159
|
+
*,
|
|
160
|
+
output_format: str = "",
|
|
161
|
+
append_system_prompt: str = "",
|
|
162
|
+
allowed_tools: str = "",
|
|
163
|
+
) -> str:
|
|
164
|
+
instructions: list[str] = []
|
|
165
|
+
if append_system_prompt:
|
|
166
|
+
instructions.append(f"SYSTEM INSTRUCTIONS:\n{append_system_prompt}")
|
|
167
|
+
if output_format and output_format.lower() == "text":
|
|
168
|
+
instructions.append("FINAL RESPONSE FORMAT: plain text only.")
|
|
169
|
+
elif output_format:
|
|
170
|
+
instructions.append(f"FINAL RESPONSE FORMAT: {output_format}.")
|
|
171
|
+
if allowed_tools:
|
|
172
|
+
instructions.append(
|
|
173
|
+
"TOOLING SCOPE: Prefer to stay within capabilities equivalent to "
|
|
174
|
+
f"{allowed_tools} unless that would make the task fail."
|
|
175
|
+
)
|
|
176
|
+
if instructions:
|
|
177
|
+
return "\n\n".join([*instructions, prompt])
|
|
178
|
+
return prompt
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_automation_prompt(
|
|
182
|
+
prompt: str,
|
|
183
|
+
*,
|
|
184
|
+
backend: str | None = None,
|
|
185
|
+
cwd: str | os.PathLike[str] | None = None,
|
|
186
|
+
env: dict | None = None,
|
|
187
|
+
model: str = "",
|
|
188
|
+
reasoning_effort: str = "",
|
|
189
|
+
timeout: int = 300,
|
|
190
|
+
output_format: str = "",
|
|
191
|
+
append_system_prompt: str = "",
|
|
192
|
+
allowed_tools: str = "",
|
|
193
|
+
extra_args: list[str] | tuple[str, ...] | None = None,
|
|
194
|
+
) -> subprocess.CompletedProcess:
|
|
195
|
+
prefs = load_client_preferences()
|
|
196
|
+
selected_backend = backend or resolve_automation_backend(preferences=prefs)
|
|
197
|
+
if selected_backend == BACKEND_NONE:
|
|
198
|
+
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
199
|
+
|
|
200
|
+
cwd_path = Path(cwd).expanduser().resolve() if cwd else Path.cwd()
|
|
201
|
+
run_env = _headless_env(env)
|
|
202
|
+
extra_args = list(extra_args or [])
|
|
203
|
+
resolved_model, resolved_effort = _resolve_runtime_model_and_effort(
|
|
204
|
+
selected_backend,
|
|
205
|
+
model=model,
|
|
206
|
+
reasoning_effort=reasoning_effort,
|
|
207
|
+
preferences=prefs,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if selected_backend == CLIENT_CLAUDE_CODE:
|
|
211
|
+
claude_bin = _resolve_claude_cli()
|
|
212
|
+
if not claude_bin:
|
|
213
|
+
raise AutomationBackendUnavailableError(
|
|
214
|
+
"Claude Code automation backend selected but `claude` is not installed."
|
|
215
|
+
)
|
|
216
|
+
cmd = [claude_bin, "-p", prompt]
|
|
217
|
+
if resolved_model:
|
|
218
|
+
cmd.extend(["--model", resolved_model])
|
|
219
|
+
if resolved_effort:
|
|
220
|
+
cmd.extend(["--effort", resolved_effort])
|
|
221
|
+
if output_format:
|
|
222
|
+
cmd.extend(["--output-format", output_format])
|
|
223
|
+
if append_system_prompt:
|
|
224
|
+
cmd.extend(["--append-system-prompt", append_system_prompt])
|
|
225
|
+
if allowed_tools:
|
|
226
|
+
cmd.extend(["--allowedTools", allowed_tools])
|
|
227
|
+
cmd.extend(extra_args)
|
|
228
|
+
return subprocess.run(
|
|
229
|
+
cmd,
|
|
230
|
+
cwd=str(cwd_path),
|
|
231
|
+
capture_output=True,
|
|
232
|
+
text=True,
|
|
233
|
+
timeout=timeout,
|
|
234
|
+
env=run_env,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if selected_backend == CLIENT_CODEX:
|
|
238
|
+
codex_bin = _resolve_codex_cli()
|
|
239
|
+
if not codex_bin:
|
|
240
|
+
raise AutomationBackendUnavailableError(
|
|
241
|
+
"Codex automation backend selected but `codex` is not installed."
|
|
242
|
+
)
|
|
243
|
+
with tempfile.TemporaryDirectory(prefix="nexo-codex-") as tmpdir:
|
|
244
|
+
output_path = Path(tmpdir) / "last-message.txt"
|
|
245
|
+
cmd = [
|
|
246
|
+
codex_bin,
|
|
247
|
+
"exec",
|
|
248
|
+
"--skip-git-repo-check",
|
|
249
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
250
|
+
"--ephemeral",
|
|
251
|
+
"-C",
|
|
252
|
+
str(cwd_path),
|
|
253
|
+
"-o",
|
|
254
|
+
str(output_path),
|
|
255
|
+
]
|
|
256
|
+
if resolved_model:
|
|
257
|
+
cmd.extend(["-m", resolved_model])
|
|
258
|
+
if resolved_effort:
|
|
259
|
+
cmd.extend(["-c", f'model_reasoning_effort="{resolved_effort}"'])
|
|
260
|
+
cmd.extend(extra_args)
|
|
261
|
+
cmd.append(
|
|
262
|
+
_build_codex_prompt(
|
|
263
|
+
prompt,
|
|
264
|
+
output_format=output_format,
|
|
265
|
+
append_system_prompt=append_system_prompt,
|
|
266
|
+
allowed_tools=allowed_tools,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
cmd,
|
|
271
|
+
cwd=str(cwd_path),
|
|
272
|
+
capture_output=True,
|
|
273
|
+
text=True,
|
|
274
|
+
timeout=timeout,
|
|
275
|
+
env=run_env,
|
|
276
|
+
)
|
|
277
|
+
stdout = output_path.read_text() if output_path.exists() else (result.stdout or "")
|
|
278
|
+
return subprocess.CompletedProcess(
|
|
279
|
+
cmd,
|
|
280
|
+
result.returncode,
|
|
281
|
+
stdout,
|
|
282
|
+
result.stderr,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
raise AutomationBackendUnavailableError(f"Unsupported automation backend: {selected_backend}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def probe_automation_backend(
|
|
289
|
+
*,
|
|
290
|
+
backend: str | None = None,
|
|
291
|
+
cwd: str | os.PathLike[str] | None = None,
|
|
292
|
+
timeout: int = 60,
|
|
293
|
+
) -> dict:
|
|
294
|
+
selected_backend = backend or resolve_automation_backend()
|
|
295
|
+
if selected_backend == BACKEND_NONE:
|
|
296
|
+
return {
|
|
297
|
+
"ok": False,
|
|
298
|
+
"backend": BACKEND_NONE,
|
|
299
|
+
"reason": "automation disabled in config",
|
|
300
|
+
}
|
|
301
|
+
try:
|
|
302
|
+
result = run_automation_prompt(
|
|
303
|
+
"Reply exactly OK.",
|
|
304
|
+
backend=selected_backend,
|
|
305
|
+
cwd=cwd,
|
|
306
|
+
timeout=timeout,
|
|
307
|
+
output_format="text",
|
|
308
|
+
)
|
|
309
|
+
except AutomationBackendUnavailableError as exc:
|
|
310
|
+
return {
|
|
311
|
+
"ok": False,
|
|
312
|
+
"backend": selected_backend,
|
|
313
|
+
"reason": str(exc),
|
|
314
|
+
}
|
|
315
|
+
output = (result.stdout or "").strip()
|
|
316
|
+
return {
|
|
317
|
+
"ok": result.returncode == 0 and "OK" in output,
|
|
318
|
+
"backend": selected_backend,
|
|
319
|
+
"returncode": result.returncode,
|
|
320
|
+
"stdout": output,
|
|
321
|
+
"stderr": (result.stderr or "").strip(),
|
|
322
|
+
}
|
package/src/auto_update.py
CHANGED
|
@@ -933,7 +933,7 @@ def auto_update_check() -> dict:
|
|
|
933
933
|
|
|
934
934
|
# Backfill runtime CLI modules for existing installs
|
|
935
935
|
try:
|
|
936
|
-
for fname in ("cli.py", "script_registry.py", "skills_runtime.py", "cron_recovery.py"):
|
|
936
|
+
for fname in ("cli.py", "script_registry.py", "skills_runtime.py", "cron_recovery.py", "client_preferences.py", "agent_runner.py"):
|
|
937
937
|
src_file = SRC_DIR / fname
|
|
938
938
|
dest_file = NEXO_HOME / fname
|
|
939
939
|
if src_file.is_file() and (not dest_file.exists() or src_file.stat().st_mtime > dest_file.stat().st_mtime):
|
|
@@ -1198,6 +1198,7 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1198
1198
|
"maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
|
|
1199
1199
|
"evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
|
|
1200
1200
|
"client_sync.py",
|
|
1201
|
+
"client_preferences.py", "agent_runner.py",
|
|
1201
1202
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1202
1203
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1203
1204
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
@@ -1247,6 +1248,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1247
1248
|
"maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
|
|
1248
1249
|
"evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
|
|
1249
1250
|
"client_sync.py",
|
|
1251
|
+
"client_preferences.py", "agent_runner.py",
|
|
1250
1252
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1251
1253
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1252
1254
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
@@ -1446,8 +1448,15 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
1446
1448
|
_emit_progress(progress_fn, "Refreshing shared client configs...")
|
|
1447
1449
|
try:
|
|
1448
1450
|
from client_sync import sync_all_clients
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
+
from client_preferences import normalize_client_preferences
|
|
1452
|
+
|
|
1453
|
+
schedule_path = dest / "config" / "schedule.json"
|
|
1454
|
+
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
1455
|
+
client_sync_result = sync_all_clients(
|
|
1456
|
+
nexo_home=dest,
|
|
1457
|
+
runtime_root=dest,
|
|
1458
|
+
preferences=normalize_client_preferences(schedule_payload),
|
|
1459
|
+
)
|
|
1451
1460
|
if client_sync_result.get("ok"):
|
|
1452
1461
|
actions.append("client-sync")
|
|
1453
1462
|
else:
|
package/src/cli.py
CHANGED
|
@@ -846,10 +846,6 @@ def _dashboard(args):
|
|
|
846
846
|
|
|
847
847
|
def _chat(args):
|
|
848
848
|
target = args.path or "."
|
|
849
|
-
claude_bin = os.environ.get("CLAUDE_BIN") or shutil.which("claude")
|
|
850
|
-
if not claude_bin:
|
|
851
|
-
print("Claude Code launcher not found in PATH. Install `claude` first.", file=sys.stderr)
|
|
852
|
-
return 1
|
|
853
849
|
|
|
854
850
|
try:
|
|
855
851
|
from auto_update import startup_preflight
|
|
@@ -868,10 +864,21 @@ def _chat(args):
|
|
|
868
864
|
except Exception:
|
|
869
865
|
pass
|
|
870
866
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
867
|
+
try:
|
|
868
|
+
from agent_runner import TerminalClientUnavailableError, launch_interactive_client
|
|
869
|
+
except ImportError:
|
|
870
|
+
print("Agent runner module not found. Ensure NEXO is properly installed.", file=sys.stderr)
|
|
871
|
+
return 1
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
result = launch_interactive_client(
|
|
875
|
+
target=target,
|
|
876
|
+
client=getattr(args, "client", None),
|
|
877
|
+
env=os.environ.copy(),
|
|
878
|
+
)
|
|
879
|
+
except TerminalClientUnavailableError as exc:
|
|
880
|
+
print(str(exc), file=sys.stderr)
|
|
881
|
+
return 1
|
|
875
882
|
return int(result.returncode)
|
|
876
883
|
|
|
877
884
|
|
|
@@ -1005,7 +1012,7 @@ def _print_help():
|
|
|
1005
1012
|
print(f"""NEXO Runtime CLI v{v}
|
|
1006
1013
|
|
|
1007
1014
|
Commands:
|
|
1008
|
-
nexo chat [path]
|
|
1015
|
+
nexo chat [path] [--client claude_code|codex] Launch the selected terminal client
|
|
1009
1016
|
nexo doctor [--tier boot|runtime|deep|all] [--fix] System diagnostics
|
|
1010
1017
|
nexo scripts list|create|classify|sync|reconcile|ensure-schedules|schedules|run|doctor|call|unschedule|remove
|
|
1011
1018
|
Personal scripts
|
|
@@ -1027,8 +1034,13 @@ def main():
|
|
|
1027
1034
|
sub = parser.add_subparsers(dest="command")
|
|
1028
1035
|
|
|
1029
1036
|
# -- chat --
|
|
1030
|
-
chat_parser = sub.add_parser("chat", help="Launch
|
|
1037
|
+
chat_parser = sub.add_parser("chat", help="Launch the selected terminal client")
|
|
1031
1038
|
chat_parser.add_argument("path", nargs="?", default=".", help="Working directory (default: current directory)")
|
|
1039
|
+
chat_parser.add_argument(
|
|
1040
|
+
"--client",
|
|
1041
|
+
choices=["claude_code", "codex"],
|
|
1042
|
+
help="Override the configured default terminal client",
|
|
1043
|
+
)
|
|
1032
1044
|
|
|
1033
1045
|
# -- scripts --
|
|
1034
1046
|
scripts_parser = sub.add_parser("scripts", help="Manage personal scripts")
|