nexo-brain 7.25.6 → 7.27.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/nexo-brain.js +235 -31
- package/codex/openai-codex-0.133.0.tgz +0 -0
- package/package.json +7 -1
- package/src/agent_runner.py +99 -32
- package/src/call_model_raw.py +1 -1
- package/src/cli.py +117 -4
- package/src/client_preferences.py +296 -1
- package/src/client_sync.py +343 -6
- package/src/db/_schema.py +23 -0
- package/src/db/_sessions.py +75 -24
- package/src/enforcement_classifier.py +1 -1
- package/src/local_context/api.py +58 -45
- package/src/model_defaults.json +4 -4
- package/src/model_defaults.py +4 -4
- package/src/provider_runtime.py +39 -0
- package/src/resonance_tiers.json +5 -5
- package/src/scripts/deep-sleep/extract.py +2 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-cron-wrapper.sh +108 -25
- package/src/scripts/nexo-morning-agent.py +1 -1
- package/src/server.py +3 -1
- package/src/tools_automation_sessions.py +2 -1
- package/src/tools_sessions.py +13 -8
- package/templates/launchagents/README.md +2 -2
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
- package/templates/launchagents/com.nexo.catchup.plist +3 -3
- package/templates/launchagents/com.nexo.cognitive-decay.plist +3 -3
- package/templates/launchagents/com.nexo.deep-sleep.plist +3 -3
- package/templates/launchagents/com.nexo.evolution.plist +3 -3
- package/templates/launchagents/com.nexo.followup-hygiene.plist +3 -3
- package/templates/launchagents/com.nexo.immune.plist +1 -1
- package/templates/launchagents/com.nexo.postmortem.plist +3 -3
- package/templates/launchagents/com.nexo.self-audit.plist +3 -3
- package/templates/launchagents/com.nexo.synthesis.plist +1 -1
- package/templates/launchagents/com.nexo.watchdog.plist +3 -3
package/src/agent_runner.py
CHANGED
|
@@ -24,8 +24,12 @@ from client_preferences import (
|
|
|
24
24
|
BACKEND_NONE,
|
|
25
25
|
CLIENT_CLAUDE_CODE,
|
|
26
26
|
CLIENT_CODEX,
|
|
27
|
+
client_to_provider,
|
|
27
28
|
TERMINAL_CLIENT_KEYS,
|
|
28
29
|
load_client_preferences,
|
|
30
|
+
_desktop_product_requested,
|
|
31
|
+
_managed_codex_binary,
|
|
32
|
+
_managed_codex_vendor_present,
|
|
29
33
|
normalize_client_key,
|
|
30
34
|
resolve_automation_backend,
|
|
31
35
|
resolve_client_runtime_profile,
|
|
@@ -40,6 +44,7 @@ CLAUDE_LEGACY_MODEL_HINTS = {"opus", "sonnet"}
|
|
|
40
44
|
MODEL_PRICING_USD_PER_1M = {
|
|
41
45
|
# Pricing snapshot used only when the backend does not return explicit cost.
|
|
42
46
|
# Codex model names map to the current GPT-5 family pricing.
|
|
47
|
+
"gpt-5.5": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
|
|
43
48
|
"gpt-5.4": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
|
|
44
49
|
"gpt-5.4-mini": {"input": 0.25, "cached_input": 0.025, "output": 2.0},
|
|
45
50
|
}
|
|
@@ -54,11 +59,16 @@ class TerminalClientUnavailableError(AgentRunnerError):
|
|
|
54
59
|
class AutomationBackendUnavailableError(AgentRunnerError):
|
|
55
60
|
"""Raised when the configured automation backend is unavailable."""
|
|
56
61
|
|
|
62
|
+
|
|
63
|
+
def _automation_provider_for_backend(backend: str) -> str:
|
|
64
|
+
return client_to_provider(backend) or ""
|
|
65
|
+
|
|
57
66
|
def _canonical_pricing_model(model: str) -> str:
|
|
58
67
|
lowered = str(model or "").strip().lower()
|
|
59
68
|
lowered = lowered.split("[", 1)[0]
|
|
60
69
|
aliases = {
|
|
61
|
-
"gpt-5": "gpt-5.
|
|
70
|
+
"gpt-5": "gpt-5.5",
|
|
71
|
+
"gpt-5.5": "gpt-5.5",
|
|
62
72
|
"gpt-5.4": "gpt-5.4",
|
|
63
73
|
"gpt-5-mini": "gpt-5.4-mini",
|
|
64
74
|
"gpt-5.4-mini": "gpt-5.4-mini",
|
|
@@ -199,6 +209,7 @@ def _record_automation_start(
|
|
|
199
209
|
*,
|
|
200
210
|
caller: str,
|
|
201
211
|
backend: str,
|
|
212
|
+
provider: str = "",
|
|
202
213
|
session_type: str,
|
|
203
214
|
task_profile: str,
|
|
204
215
|
model: str,
|
|
@@ -225,14 +236,15 @@ def _record_automation_start(
|
|
|
225
236
|
cur = conn.execute(
|
|
226
237
|
"""
|
|
227
238
|
INSERT INTO automation_runs (
|
|
228
|
-
caller, backend, session_type, task_profile, model,
|
|
239
|
+
caller, backend, provider, session_type, task_profile, model,
|
|
229
240
|
reasoning_effort, resonance_tier, cwd, output_format,
|
|
230
241
|
prompt_chars, status, started_at, pid
|
|
231
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', datetime('now'), ?)
|
|
242
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', datetime('now'), ?)
|
|
232
243
|
""",
|
|
233
244
|
(
|
|
234
245
|
caller or "",
|
|
235
246
|
backend,
|
|
247
|
+
provider or _automation_provider_for_backend(backend),
|
|
236
248
|
session_type or "headless",
|
|
237
249
|
task_profile or "default",
|
|
238
250
|
model or "",
|
|
@@ -339,6 +351,7 @@ def _record_automation_end(
|
|
|
339
351
|
def _record_automation_run(
|
|
340
352
|
*,
|
|
341
353
|
backend: str,
|
|
354
|
+
provider: str = "",
|
|
342
355
|
task_profile: str,
|
|
343
356
|
model: str,
|
|
344
357
|
reasoning_effort: str,
|
|
@@ -359,6 +372,7 @@ def _record_automation_run(
|
|
|
359
372
|
row_id, err = _record_automation_start(
|
|
360
373
|
caller=caller,
|
|
361
374
|
backend=backend,
|
|
375
|
+
provider=provider or _automation_provider_for_backend(backend),
|
|
362
376
|
session_type=session_type,
|
|
363
377
|
task_profile=task_profile,
|
|
364
378
|
model=model,
|
|
@@ -378,14 +392,57 @@ def _record_automation_run(
|
|
|
378
392
|
)
|
|
379
393
|
|
|
380
394
|
|
|
395
|
+
def _record_backend_unavailable(
|
|
396
|
+
*,
|
|
397
|
+
backend: str,
|
|
398
|
+
caller: str,
|
|
399
|
+
cwd: Path,
|
|
400
|
+
prompt: str,
|
|
401
|
+
) -> None:
|
|
402
|
+
profile = resolve_client_runtime_profile(backend)
|
|
403
|
+
_record_automation_run(
|
|
404
|
+
backend=backend,
|
|
405
|
+
provider=_automation_provider_for_backend(backend),
|
|
406
|
+
task_profile="default",
|
|
407
|
+
model=profile.get("model", ""),
|
|
408
|
+
reasoning_effort=profile.get("reasoning_effort", ""),
|
|
409
|
+
cwd=cwd,
|
|
410
|
+
output_format="text",
|
|
411
|
+
prompt=prompt,
|
|
412
|
+
returncode=2,
|
|
413
|
+
duration_ms=0,
|
|
414
|
+
telemetry={
|
|
415
|
+
"telemetry_source": "backend_unavailable",
|
|
416
|
+
"cost_source": "missing",
|
|
417
|
+
"usage": {},
|
|
418
|
+
"warnings": ["backend_unavailable", "fallback_blocked"],
|
|
419
|
+
"raw": {
|
|
420
|
+
"event": "backend_unavailable",
|
|
421
|
+
"backend": backend,
|
|
422
|
+
"provider": _automation_provider_for_backend(backend),
|
|
423
|
+
"fallback_policy": "fail_closed",
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
caller=caller,
|
|
427
|
+
session_type="headless",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
381
431
|
def _resolve_claude_cli() -> str:
|
|
382
432
|
return _shared_resolve_claude_cli()
|
|
383
433
|
|
|
384
434
|
|
|
385
435
|
def _resolve_codex_cli() -> str:
|
|
436
|
+
home = Path.home()
|
|
437
|
+
if _desktop_product_requested(home):
|
|
438
|
+
managed_path = _managed_codex_binary(home)
|
|
439
|
+
return managed_path if managed_path and _managed_codex_vendor_present(home) else ""
|
|
386
440
|
env_path = os.environ.get("CODEX_BIN", "").strip()
|
|
387
441
|
if env_path and Path(env_path).exists():
|
|
388
442
|
return env_path
|
|
443
|
+
managed_path = _managed_codex_binary(home)
|
|
444
|
+
if managed_path and _managed_codex_vendor_present(home):
|
|
445
|
+
return managed_path
|
|
389
446
|
return shutil.which("codex") or ""
|
|
390
447
|
|
|
391
448
|
|
|
@@ -790,6 +847,7 @@ def run_automation_interactive(
|
|
|
790
847
|
row_id, _record_err = _record_automation_start(
|
|
791
848
|
caller=caller,
|
|
792
849
|
backend=resolved_client,
|
|
850
|
+
provider=_automation_provider_for_backend(resolved_client),
|
|
793
851
|
session_type=session_type,
|
|
794
852
|
task_profile="",
|
|
795
853
|
model="",
|
|
@@ -905,15 +963,6 @@ def _backend_is_available(backend: str) -> bool:
|
|
|
905
963
|
|
|
906
964
|
|
|
907
965
|
def _resolve_available_backend(selected_backend: str, *, preferences: dict | None = None) -> str:
|
|
908
|
-
if _backend_is_available(selected_backend):
|
|
909
|
-
return selected_backend
|
|
910
|
-
prefs = preferences or load_client_preferences()
|
|
911
|
-
preferred = resolve_automation_backend(preferences=prefs)
|
|
912
|
-
for candidate in (preferred, CLIENT_CLAUDE_CODE, CLIENT_CODEX):
|
|
913
|
-
if candidate == selected_backend or candidate == BACKEND_NONE:
|
|
914
|
-
continue
|
|
915
|
-
if _backend_is_available(candidate):
|
|
916
|
-
return candidate
|
|
917
966
|
return selected_backend
|
|
918
967
|
|
|
919
968
|
|
|
@@ -1011,7 +1060,7 @@ _ANTHROPIC_API_KEY_SEARCH_PATHS = (
|
|
|
1011
1060
|
|
|
1012
1061
|
|
|
1013
1062
|
def _resolve_anthropic_api_key() -> str:
|
|
1014
|
-
"""Locate
|
|
1063
|
+
"""Locate a legacy Anthropic API key for opt-in bare-mode invocations.
|
|
1015
1064
|
|
|
1016
1065
|
``claude --bare`` skips macOS Keychain auth entirely, so the child
|
|
1017
1066
|
must find the API key in ``ANTHROPIC_API_KEY`` or via ``apiKeyHelper``.
|
|
@@ -1039,15 +1088,16 @@ def _resolve_anthropic_api_key() -> str:
|
|
|
1039
1088
|
return ""
|
|
1040
1089
|
|
|
1041
1090
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1091
|
+
def _allow_anthropic_api_key_bare_mode() -> bool:
|
|
1092
|
+
value = os.environ.get("NEXO_ALLOW_ANTHROPIC_API_BARE", "").strip().lower()
|
|
1093
|
+
return value in {"1", "true", "yes", "on"}
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
# v7.26 provider-runtime contract: Desktop-managed automation must use the
|
|
1097
|
+
# selected account runtime (Claude Code or Codex), not an Anthropic API key
|
|
1098
|
+
# side path. Bare mode remains a hidden legacy opt-in for controlled local
|
|
1099
|
+
# experiments only via NEXO_ALLOW_ANTHROPIC_API_BARE=1.
|
|
1100
|
+
BARE_MODE_SAFE_CALLERS: frozenset[str] = frozenset()
|
|
1051
1101
|
|
|
1052
1102
|
# Execution contracts keep background agents disciplined without polluting
|
|
1053
1103
|
# machine-only child calls that must return strict JSON.
|
|
@@ -1153,6 +1203,17 @@ def run_automation_prompt(
|
|
|
1153
1203
|
if selected_backend == BACKEND_NONE:
|
|
1154
1204
|
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
1155
1205
|
selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
|
|
1206
|
+
if not _backend_is_available(selected_backend):
|
|
1207
|
+
cwd_path = Path(cwd).expanduser().resolve() if cwd else Path.cwd()
|
|
1208
|
+
_record_backend_unavailable(
|
|
1209
|
+
backend=selected_backend,
|
|
1210
|
+
caller=caller,
|
|
1211
|
+
cwd=cwd_path,
|
|
1212
|
+
prompt=prompt,
|
|
1213
|
+
)
|
|
1214
|
+
raise AutomationBackendUnavailableError(
|
|
1215
|
+
f"{selected_backend} automation backend selected but launcher is not installed; fallback blocked."
|
|
1216
|
+
)
|
|
1156
1217
|
|
|
1157
1218
|
# Resonance map decides (model, effort) for every call. ``caller`` is
|
|
1158
1219
|
# MANDATORY — every script that invokes the automation backend must be
|
|
@@ -1256,13 +1317,13 @@ def run_automation_prompt(
|
|
|
1256
1317
|
# completes in seconds.
|
|
1257
1318
|
#
|
|
1258
1319
|
# Selection rules:
|
|
1259
|
-
# - bare_mode=True explicit →
|
|
1320
|
+
# - bare_mode=True explicit → allow only when the legacy API-key
|
|
1321
|
+
# escape hatch is enabled.
|
|
1260
1322
|
# - bare_mode=None (default) + caller in BARE_MODE_SAFE_CALLERS
|
|
1261
|
-
# → auto-enable.
|
|
1323
|
+
# → auto-enable. v7.26 keeps this set empty for provider parity.
|
|
1262
1324
|
# - bare_mode=False → never.
|
|
1263
|
-
# - --bare disables keychain auth, so
|
|
1264
|
-
# ANTHROPIC_API_KEY. If
|
|
1265
|
-
# normal mode with a warning on stderr rather than failing.
|
|
1325
|
+
# - --bare disables keychain auth, so the legacy path requires
|
|
1326
|
+
# ANTHROPIC_API_KEY. If disabled or missing, use normal account auth.
|
|
1266
1327
|
resolved_bare = False
|
|
1267
1328
|
if bare_mode is True:
|
|
1268
1329
|
resolved_bare = True
|
|
@@ -1271,12 +1332,14 @@ def run_automation_prompt(
|
|
|
1271
1332
|
|
|
1272
1333
|
bare_api_key = ""
|
|
1273
1334
|
if resolved_bare:
|
|
1274
|
-
|
|
1275
|
-
if not bare_api_key:
|
|
1276
|
-
# Silent fallback: we would rather take the slower path
|
|
1277
|
-
# than force the caller to fail-closed on an env quirk.
|
|
1335
|
+
if not _allow_anthropic_api_key_bare_mode():
|
|
1278
1336
|
resolved_bare = False
|
|
1279
|
-
|
|
1337
|
+
else:
|
|
1338
|
+
bare_api_key = _resolve_anthropic_api_key()
|
|
1339
|
+
if not bare_api_key:
|
|
1340
|
+
# Silent fallback: we would rather take the slower path
|
|
1341
|
+
# than force the caller to fail-closed on an env quirk.
|
|
1342
|
+
resolved_bare = False
|
|
1280
1343
|
# Headless claude -p does NOT reliably honour permissions.allow from
|
|
1281
1344
|
# settings.json for MCP tool calls — it can stall waiting for an
|
|
1282
1345
|
# approval that will never come in non-interactive mode. All NEXO
|
|
@@ -1293,6 +1356,8 @@ def run_automation_prompt(
|
|
|
1293
1356
|
run_env = dict(run_env)
|
|
1294
1357
|
run_env["ANTHROPIC_API_KEY"] = bare_api_key
|
|
1295
1358
|
else:
|
|
1359
|
+
run_env = dict(run_env)
|
|
1360
|
+
run_env.pop("ANTHROPIC_API_KEY", None)
|
|
1296
1361
|
cmd.append("--dangerously-skip-permissions")
|
|
1297
1362
|
if resolved_model:
|
|
1298
1363
|
cmd.extend(["--model", resolved_model])
|
|
@@ -1342,6 +1407,7 @@ def run_automation_prompt(
|
|
|
1342
1407
|
telemetry["automation_contract"] = automation_contract
|
|
1343
1408
|
recorded, record_error = _record_automation_run(
|
|
1344
1409
|
backend=selected_backend,
|
|
1410
|
+
provider=_automation_provider_for_backend(selected_backend),
|
|
1345
1411
|
task_profile=task_profile,
|
|
1346
1412
|
model=resolved_model,
|
|
1347
1413
|
reasoning_effort=resolved_effort,
|
|
@@ -1420,6 +1486,7 @@ def run_automation_prompt(
|
|
|
1420
1486
|
telemetry["automation_contract"] = automation_contract
|
|
1421
1487
|
recorded, record_error = _record_automation_run(
|
|
1422
1488
|
backend=selected_backend,
|
|
1489
|
+
provider=_automation_provider_for_backend(selected_backend),
|
|
1423
1490
|
task_profile=task_profile,
|
|
1424
1491
|
model=resolved_model,
|
|
1425
1492
|
reasoning_effort=resolved_effort,
|
package/src/call_model_raw.py
CHANGED
|
@@ -260,7 +260,7 @@ def call_model_raw(
|
|
|
260
260
|
Parameters follow the Fase 2 plan doc 1 spec:
|
|
261
261
|
|
|
262
262
|
prompt — the user-role text (English or the model's default).
|
|
263
|
-
tier — resonance tier; default "muy_bajo" → Haiku / gpt-5.
|
|
263
|
+
tier — resonance tier; default "muy_bajo" → Haiku / gpt-5.5 low.
|
|
264
264
|
caller — resonance caller label. Must be registered in
|
|
265
265
|
resonance_map.SYSTEM_OWNED_CALLERS. Default
|
|
266
266
|
"enforcer_classifier".
|
package/src/cli.py
CHANGED
|
@@ -2470,6 +2470,7 @@ def _clients_sync(args):
|
|
|
2470
2470
|
enabled_clients=("claude_code", "claude_desktop", "codex"),
|
|
2471
2471
|
preferences=load_client_preferences(),
|
|
2472
2472
|
auto_install_missing_claude=bool(getattr(args, "auto_install_missing_claude", False)),
|
|
2473
|
+
auto_install_missing_codex=bool(getattr(args, "auto_install_missing_codex", False)),
|
|
2473
2474
|
)
|
|
2474
2475
|
if args.json:
|
|
2475
2476
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -2515,6 +2516,8 @@ def _preferences(args):
|
|
|
2515
2516
|
from client_preferences import (
|
|
2516
2517
|
BACKEND_NONE,
|
|
2517
2518
|
AUTOMATION_BACKEND_KEYS,
|
|
2519
|
+
PROVIDER_KEYS,
|
|
2520
|
+
PROVIDER_NONE,
|
|
2518
2521
|
load_client_preferences,
|
|
2519
2522
|
normalize_default_terminal_client,
|
|
2520
2523
|
save_client_preferences,
|
|
@@ -2536,7 +2539,21 @@ def _preferences(args):
|
|
|
2536
2539
|
automation_enabled = False
|
|
2537
2540
|
|
|
2538
2541
|
automation_backend = getattr(args, "automation_backend", None)
|
|
2539
|
-
|
|
2542
|
+
chat_provider = getattr(args, "chat_provider", None)
|
|
2543
|
+
automation_provider = getattr(args, "automation_provider", None)
|
|
2544
|
+
automation_changed = (
|
|
2545
|
+
automation_enabled is not None
|
|
2546
|
+
or automation_backend is not None
|
|
2547
|
+
or automation_provider is not None
|
|
2548
|
+
)
|
|
2549
|
+
provider_changed = chat_provider is not None or automation_provider is not None
|
|
2550
|
+
|
|
2551
|
+
if automation_backend and automation_provider:
|
|
2552
|
+
print(
|
|
2553
|
+
"[NEXO] Use either --automation-provider or --automation-backend, not both.",
|
|
2554
|
+
file=sys.stderr,
|
|
2555
|
+
)
|
|
2556
|
+
return 2
|
|
2540
2557
|
|
|
2541
2558
|
if args.resonance:
|
|
2542
2559
|
tier = args.resonance.lower()
|
|
@@ -2560,11 +2577,13 @@ def _preferences(args):
|
|
|
2560
2577
|
interactive_clients=prefs.get("interactive_clients"),
|
|
2561
2578
|
) or "claude_code"
|
|
2562
2579
|
|
|
2563
|
-
if tier or automation_changed:
|
|
2580
|
+
if tier or automation_changed or provider_changed:
|
|
2564
2581
|
save_client_preferences(
|
|
2565
2582
|
default_resonance=tier,
|
|
2566
2583
|
automation_enabled=automation_enabled,
|
|
2567
2584
|
automation_backend=automation_backend,
|
|
2585
|
+
selected_chat_provider=chat_provider,
|
|
2586
|
+
automation_provider=automation_provider,
|
|
2568
2587
|
automation_user_override=True if automation_changed else None,
|
|
2569
2588
|
)
|
|
2570
2589
|
if tier:
|
|
@@ -2579,8 +2598,9 @@ def _preferences(args):
|
|
|
2579
2598
|
).strip().lower()
|
|
2580
2599
|
current_resonance = calibration_value or schedule_value or DEFAULT_RESONANCE
|
|
2581
2600
|
|
|
2582
|
-
if args.show or tier or automation_changed:
|
|
2601
|
+
if args.show or tier or automation_changed or provider_changed:
|
|
2583
2602
|
is_explicit = bool(calibration_value or schedule_value)
|
|
2603
|
+
provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
|
|
2584
2604
|
payload = {
|
|
2585
2605
|
"default_resonance": current_resonance,
|
|
2586
2606
|
"default_resonance_is_explicit": is_explicit,
|
|
@@ -2594,6 +2614,10 @@ def _preferences(args):
|
|
|
2594
2614
|
"automation_user_override": bool(prefs.get("automation_user_override", False)),
|
|
2595
2615
|
"available_automation_backends": list(AUTOMATION_BACKEND_KEYS),
|
|
2596
2616
|
"default_terminal_client": str(prefs.get("default_terminal_client") or "claude_code"),
|
|
2617
|
+
"selected_chat_provider": str(provider_runtime.get("selected_chat_provider") or "anthropic"),
|
|
2618
|
+
"automation_provider": str(provider_runtime.get("automation_provider") or PROVIDER_NONE),
|
|
2619
|
+
"available_providers": list(PROVIDER_KEYS),
|
|
2620
|
+
"provider_runtime": provider_runtime,
|
|
2597
2621
|
}
|
|
2598
2622
|
if args.json:
|
|
2599
2623
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
@@ -2605,6 +2629,10 @@ def _preferences(args):
|
|
|
2605
2629
|
+ ("enabled" if payload["automation_enabled"] else "disabled")
|
|
2606
2630
|
+ f" · backend={payload['automation_backend']}"
|
|
2607
2631
|
)
|
|
2632
|
+
print(
|
|
2633
|
+
"provider = "
|
|
2634
|
+
+ f"chat:{payload['selected_chat_provider']} · automation:{payload['automation_provider']}"
|
|
2635
|
+
)
|
|
2608
2636
|
if not is_explicit:
|
|
2609
2637
|
print(f" (inherited from DEFAULT_RESONANCE; run "
|
|
2610
2638
|
f"`nexo preferences --resonance alto` to set explicitly)")
|
|
@@ -2614,13 +2642,69 @@ def _preferences(args):
|
|
|
2614
2642
|
print(
|
|
2615
2643
|
"Usage: nexo preferences [--resonance TIER] "
|
|
2616
2644
|
"[--automation-enabled|--automation-disabled] "
|
|
2617
|
-
"[--automation-backend BACKEND
|
|
2645
|
+
"[--automation-backend BACKEND|--automation-provider PROVIDER] "
|
|
2646
|
+
"[--chat-provider PROVIDER] [--show] [--json]"
|
|
2618
2647
|
)
|
|
2619
2648
|
print(f" resonance tiers: {', '.join(TIERS)}")
|
|
2620
2649
|
print(f" current default: {current_resonance}")
|
|
2621
2650
|
return 0
|
|
2622
2651
|
|
|
2623
2652
|
|
|
2653
|
+
def _provider(args):
|
|
2654
|
+
"""Provider runtime status/selection for Desktop and terminal parity."""
|
|
2655
|
+
from client_preferences import (
|
|
2656
|
+
PROVIDER_KEYS,
|
|
2657
|
+
detect_installed_clients,
|
|
2658
|
+
load_client_preferences,
|
|
2659
|
+
provider_to_client,
|
|
2660
|
+
save_client_preferences,
|
|
2661
|
+
)
|
|
2662
|
+
|
|
2663
|
+
command = getattr(args, "provider_command", "status") or "status"
|
|
2664
|
+
prefs = load_client_preferences()
|
|
2665
|
+
provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
|
|
2666
|
+
if command == "select":
|
|
2667
|
+
target = str(args.provider or "").strip().lower()
|
|
2668
|
+
if target not in PROVIDER_KEYS:
|
|
2669
|
+
print(f"[NEXO] Unknown provider '{target}'. Valid values: {', '.join(PROVIDER_KEYS)}.", file=sys.stderr)
|
|
2670
|
+
return 2
|
|
2671
|
+
chat_provider = None if getattr(args, "automation_only", False) else target
|
|
2672
|
+
automation_provider = None if getattr(args, "chat_only", False) else target
|
|
2673
|
+
save_client_preferences(
|
|
2674
|
+
selected_chat_provider=chat_provider,
|
|
2675
|
+
automation_provider=automation_provider,
|
|
2676
|
+
automation_user_override=True if automation_provider is not None else None,
|
|
2677
|
+
)
|
|
2678
|
+
prefs = load_client_preferences()
|
|
2679
|
+
provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
|
|
2680
|
+
|
|
2681
|
+
detected = detect_installed_clients()
|
|
2682
|
+
payload = {
|
|
2683
|
+
"selected_chat_provider": str(provider_runtime.get("selected_chat_provider") or "anthropic"),
|
|
2684
|
+
"automation_provider": str(provider_runtime.get("automation_provider") or "none"),
|
|
2685
|
+
"automation_backend": str(prefs.get("automation_backend") or "none"),
|
|
2686
|
+
"default_terminal_client": str(prefs.get("default_terminal_client") or "claude_code"),
|
|
2687
|
+
"providers": provider_runtime.get("providers") or {},
|
|
2688
|
+
"installed_clients": detected,
|
|
2689
|
+
}
|
|
2690
|
+
for provider in PROVIDER_KEYS:
|
|
2691
|
+
client = provider_to_client(provider)
|
|
2692
|
+
payload["providers"].setdefault(provider, {})
|
|
2693
|
+
payload["providers"][provider]["client"] = client
|
|
2694
|
+
payload["providers"][provider]["detected"] = detected.get(client, {})
|
|
2695
|
+
|
|
2696
|
+
if getattr(args, "json", False):
|
|
2697
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
2698
|
+
else:
|
|
2699
|
+
print(f"chat_provider = {payload['selected_chat_provider']}")
|
|
2700
|
+
print(f"automation_provider = {payload['automation_provider']} · backend={payload['automation_backend']}")
|
|
2701
|
+
for provider in PROVIDER_KEYS:
|
|
2702
|
+
detected_provider = payload["providers"].get(provider, {}).get("detected", {})
|
|
2703
|
+
state = "installed" if detected_provider.get("installed") else "missing"
|
|
2704
|
+
print(f" {provider}: {state}")
|
|
2705
|
+
return 0
|
|
2706
|
+
|
|
2707
|
+
|
|
2624
2708
|
def _contributor_status(args):
|
|
2625
2709
|
public_contribution = _load_public_contribution_support()
|
|
2626
2710
|
config = public_contribution["refresh_public_contribution_state"](
|
|
@@ -4058,6 +4142,11 @@ def main():
|
|
|
4058
4142
|
action="store_true",
|
|
4059
4143
|
help="Install Claude Code automatically when the selected runtime needs it and it is missing.",
|
|
4060
4144
|
)
|
|
4145
|
+
clients_sync_p.add_argument(
|
|
4146
|
+
"--auto-install-missing-codex",
|
|
4147
|
+
action="store_true",
|
|
4148
|
+
help="Install Codex automatically when the selected runtime needs it and it is missing.",
|
|
4149
|
+
)
|
|
4061
4150
|
|
|
4062
4151
|
# -- preferences --
|
|
4063
4152
|
preferences_parser = sub.add_parser(
|
|
@@ -4093,8 +4182,30 @@ def main():
|
|
|
4093
4182
|
choices=["claude_code", "codex", "none"],
|
|
4094
4183
|
help="Backend used by background automations when they are enabled.",
|
|
4095
4184
|
)
|
|
4185
|
+
preferences_parser.add_argument(
|
|
4186
|
+
"--chat-provider",
|
|
4187
|
+
choices=["anthropic", "openai"],
|
|
4188
|
+
help="Provider used for new chat sessions: Anthropic runs Claude Code, OpenAI runs Codex.",
|
|
4189
|
+
)
|
|
4190
|
+
preferences_parser.add_argument(
|
|
4191
|
+
"--automation-provider",
|
|
4192
|
+
choices=["anthropic", "openai", "none"],
|
|
4193
|
+
help="Provider used by background automations. 'none' pauses the runtime instead of falling back.",
|
|
4194
|
+
)
|
|
4096
4195
|
preferences_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
4097
4196
|
|
|
4197
|
+
# -- provider --
|
|
4198
|
+
provider_parser = sub.add_parser("provider", help="Provider runtime status and selection")
|
|
4199
|
+
provider_sub = provider_parser.add_subparsers(dest="provider_command")
|
|
4200
|
+
provider_status_p = provider_sub.add_parser("status", help="Show selected Anthropic/OpenAI runtime")
|
|
4201
|
+
provider_status_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4202
|
+
provider_select_p = provider_sub.add_parser("select", help="Select Anthropic or OpenAI runtime")
|
|
4203
|
+
provider_select_p.add_argument("provider", choices=["anthropic", "openai"], help="Provider to select")
|
|
4204
|
+
provider_scope = provider_select_p.add_mutually_exclusive_group()
|
|
4205
|
+
provider_scope.add_argument("--chat-only", action="store_true", help="Only switch new chat sessions")
|
|
4206
|
+
provider_scope.add_argument("--automation-only", action="store_true", help="Only switch background automation")
|
|
4207
|
+
provider_select_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4208
|
+
|
|
4098
4209
|
# -- doctor --
|
|
4099
4210
|
doctor_parser = sub.add_parser("doctor", help="Unified diagnostics")
|
|
4100
4211
|
doctor_parser.add_argument("--tier", default="boot", choices=["boot", "runtime", "deep", "all"],
|
|
@@ -4585,6 +4696,8 @@ def main():
|
|
|
4585
4696
|
return 0
|
|
4586
4697
|
elif args.command == "preferences":
|
|
4587
4698
|
return _preferences(args)
|
|
4699
|
+
elif args.command == "provider":
|
|
4700
|
+
return _provider(args)
|
|
4588
4701
|
elif args.command == "doctor":
|
|
4589
4702
|
return _doctor(args)
|
|
4590
4703
|
elif args.command == "support-snapshot":
|