nexo-brain 7.25.6 → 7.26.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 +3 -1
- package/bin/nexo-brain.js +233 -29
- package/codex/openai-codex-0.133.0.tgz +0 -0
- package/package.json +7 -1
- package/src/agent_runner.py +96 -31
- package/src/cli.py +117 -4
- package/src/client_preferences.py +293 -1
- package/src/client_sync.py +327 -1
- package/src/db/_schema.py +23 -0
- package/src/db/_sessions.py +75 -24
- package/src/provider_runtime.py +39 -0
- 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/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":
|
|
@@ -20,6 +20,22 @@ CLIENT_CLAUDE_CODE = "claude_code"
|
|
|
20
20
|
CLIENT_CODEX = "codex"
|
|
21
21
|
CLIENT_CLAUDE_DESKTOP = "claude_desktop"
|
|
22
22
|
BACKEND_NONE = "none"
|
|
23
|
+
PROVIDER_ANTHROPIC = "anthropic"
|
|
24
|
+
PROVIDER_OPENAI = "openai"
|
|
25
|
+
PROVIDER_NONE = "none"
|
|
26
|
+
|
|
27
|
+
PROVIDER_TO_CLIENT = {
|
|
28
|
+
PROVIDER_ANTHROPIC: CLIENT_CLAUDE_CODE,
|
|
29
|
+
PROVIDER_OPENAI: CLIENT_CODEX,
|
|
30
|
+
}
|
|
31
|
+
CLIENT_TO_PROVIDER = {
|
|
32
|
+
CLIENT_CLAUDE_CODE: PROVIDER_ANTHROPIC,
|
|
33
|
+
CLIENT_CODEX: PROVIDER_OPENAI,
|
|
34
|
+
}
|
|
35
|
+
PROVIDER_KEYS = (
|
|
36
|
+
PROVIDER_ANTHROPIC,
|
|
37
|
+
PROVIDER_OPENAI,
|
|
38
|
+
)
|
|
23
39
|
|
|
24
40
|
INTERACTIVE_CLIENT_KEYS = (
|
|
25
41
|
CLIENT_CLAUDE_CODE,
|
|
@@ -67,6 +83,14 @@ AUTOMATION_TASK_PROFILE_TIERS = {
|
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
|
|
86
|
+
def provider_to_client(provider: str | None) -> str:
|
|
87
|
+
return PROVIDER_TO_CLIENT.get(normalize_provider_key(provider), "")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def client_to_provider(client: str | None) -> str:
|
|
91
|
+
return CLIENT_TO_PROVIDER.get(normalize_client_key(client), "")
|
|
92
|
+
|
|
93
|
+
|
|
70
94
|
def _user_home() -> Path:
|
|
71
95
|
return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
72
96
|
|
|
@@ -158,6 +182,38 @@ def _managed_claude_binary(home: Path | None = None) -> str:
|
|
|
158
182
|
return ""
|
|
159
183
|
|
|
160
184
|
|
|
185
|
+
def _managed_codex_binary(home: Path | None = None) -> str:
|
|
186
|
+
base = (home or _user_home()).expanduser()
|
|
187
|
+
managed_prefix = _managed_claude_prefix(base)
|
|
188
|
+
candidates = [managed_prefix / "bin" / "codex"]
|
|
189
|
+
for candidate in candidates:
|
|
190
|
+
try:
|
|
191
|
+
if candidate.exists() and _path_within(candidate, managed_prefix):
|
|
192
|
+
return str(candidate)
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
return ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _managed_codex_vendor_present(home: Path | None = None) -> bool:
|
|
199
|
+
base = (home or _user_home()).expanduser()
|
|
200
|
+
managed_prefix = _managed_claude_prefix(base)
|
|
201
|
+
package_roots = (
|
|
202
|
+
managed_prefix / "lib" / "node_modules" / "@openai" / "codex",
|
|
203
|
+
managed_prefix / "node_modules" / "@openai" / "codex",
|
|
204
|
+
)
|
|
205
|
+
for package_root in package_roots:
|
|
206
|
+
vendor_root = package_root / "vendor"
|
|
207
|
+
if not vendor_root.exists():
|
|
208
|
+
continue
|
|
209
|
+
try:
|
|
210
|
+
if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
|
|
211
|
+
return True
|
|
212
|
+
except Exception:
|
|
213
|
+
continue
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
161
217
|
def _coerce_bool(value, default: bool) -> bool:
|
|
162
218
|
if isinstance(value, bool):
|
|
163
219
|
return value
|
|
@@ -182,6 +238,7 @@ def default_client_preferences() -> dict:
|
|
|
182
238
|
"last_terminal_client": "",
|
|
183
239
|
"automation_enabled": True,
|
|
184
240
|
"automation_backend": CLIENT_CLAUDE_CODE,
|
|
241
|
+
"provider_runtime": default_provider_runtime(),
|
|
185
242
|
# True iff the user has EXPLICITLY changed automation_backend from its
|
|
186
243
|
# installer-chosen value. Installer/update flows (Fase E) only rewrite
|
|
187
244
|
# automation_backend if this flag is False — respects user opt-out.
|
|
@@ -204,6 +261,59 @@ def default_client_preferences() -> dict:
|
|
|
204
261
|
}
|
|
205
262
|
|
|
206
263
|
|
|
264
|
+
def default_provider_runtime() -> dict:
|
|
265
|
+
return {
|
|
266
|
+
"schema_version": 1,
|
|
267
|
+
"selected_chat_provider": PROVIDER_ANTHROPIC,
|
|
268
|
+
"automation_provider": PROVIDER_ANTHROPIC,
|
|
269
|
+
"automation_backend": CLIENT_CLAUDE_CODE,
|
|
270
|
+
"providers": {
|
|
271
|
+
PROVIDER_ANTHROPIC: {
|
|
272
|
+
"client": CLIENT_CLAUDE_CODE,
|
|
273
|
+
"runtime_account_status": {
|
|
274
|
+
"surface": "desktop_login",
|
|
275
|
+
"status": "unknown",
|
|
276
|
+
"plan": None,
|
|
277
|
+
"last_checked_at": None,
|
|
278
|
+
"detail": None,
|
|
279
|
+
},
|
|
280
|
+
"install_status": {
|
|
281
|
+
"installed": False,
|
|
282
|
+
"managed": True,
|
|
283
|
+
"binary_path": None,
|
|
284
|
+
"version": None,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
PROVIDER_OPENAI: {
|
|
288
|
+
"client": CLIENT_CODEX,
|
|
289
|
+
"runtime_account_status": {
|
|
290
|
+
"surface": "desktop_login",
|
|
291
|
+
"status": "unknown",
|
|
292
|
+
"plan": None,
|
|
293
|
+
"last_checked_at": None,
|
|
294
|
+
"detail": None,
|
|
295
|
+
},
|
|
296
|
+
"install_status": {
|
|
297
|
+
"installed": False,
|
|
298
|
+
"managed": True,
|
|
299
|
+
"binary_path": None,
|
|
300
|
+
"version": None,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
"fallback_policy": {
|
|
305
|
+
"chat": "ask",
|
|
306
|
+
"automation": "fail_closed",
|
|
307
|
+
},
|
|
308
|
+
"last_provider_change": {
|
|
309
|
+
"changed_at": None,
|
|
310
|
+
"from_provider": None,
|
|
311
|
+
"to_provider": None,
|
|
312
|
+
"source": None,
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
207
317
|
def normalize_client_key(value: str | None) -> str:
|
|
208
318
|
candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
209
319
|
aliases = {
|
|
@@ -221,6 +331,27 @@ def normalize_client_key(value: str | None) -> str:
|
|
|
221
331
|
return aliases.get(candidate, "")
|
|
222
332
|
|
|
223
333
|
|
|
334
|
+
def normalize_provider_key(value: str | None) -> str:
|
|
335
|
+
candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
336
|
+
aliases = {
|
|
337
|
+
"": "",
|
|
338
|
+
"none": PROVIDER_NONE,
|
|
339
|
+
"off": PROVIDER_NONE,
|
|
340
|
+
"disabled": PROVIDER_NONE,
|
|
341
|
+
"false": PROVIDER_NONE,
|
|
342
|
+
"0": PROVIDER_NONE,
|
|
343
|
+
"anthropic": PROVIDER_ANTHROPIC,
|
|
344
|
+
"anthropic_claude": PROVIDER_ANTHROPIC,
|
|
345
|
+
"anthropic_claude_code": PROVIDER_ANTHROPIC,
|
|
346
|
+
"claude": PROVIDER_ANTHROPIC,
|
|
347
|
+
"claude_code": PROVIDER_ANTHROPIC,
|
|
348
|
+
"openai": PROVIDER_OPENAI,
|
|
349
|
+
"openai_codex": PROVIDER_OPENAI,
|
|
350
|
+
"codex": PROVIDER_OPENAI,
|
|
351
|
+
}
|
|
352
|
+
return aliases.get(candidate, "")
|
|
353
|
+
|
|
354
|
+
|
|
224
355
|
def normalize_backend_key(value: str | None) -> str:
|
|
225
356
|
candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
226
357
|
if candidate in {"", "none", "off", "disabled", "false", "0"}:
|
|
@@ -231,6 +362,75 @@ def normalize_backend_key(value: str | None) -> str:
|
|
|
231
362
|
return ""
|
|
232
363
|
|
|
233
364
|
|
|
365
|
+
def _provider_from_runtime_payload(value, key: str, fallback_client: str) -> str:
|
|
366
|
+
if isinstance(value, dict):
|
|
367
|
+
provider = normalize_provider_key(value.get(key))
|
|
368
|
+
if provider in PROVIDER_KEYS:
|
|
369
|
+
return provider
|
|
370
|
+
return client_to_provider(fallback_client) or PROVIDER_ANTHROPIC
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def normalize_provider_runtime(
|
|
374
|
+
value,
|
|
375
|
+
*,
|
|
376
|
+
default_terminal_client: str = CLIENT_CLAUDE_CODE,
|
|
377
|
+
automation_backend: str = CLIENT_CLAUDE_CODE,
|
|
378
|
+
automation_enabled: bool = True,
|
|
379
|
+
) -> dict:
|
|
380
|
+
defaults = default_provider_runtime()
|
|
381
|
+
raw = value if isinstance(value, dict) else {}
|
|
382
|
+
selected_provider = _provider_from_runtime_payload(
|
|
383
|
+
raw,
|
|
384
|
+
"selected_chat_provider",
|
|
385
|
+
default_terminal_client,
|
|
386
|
+
)
|
|
387
|
+
raw_automation_provider = normalize_provider_key(raw.get("automation_provider")) if isinstance(raw, dict) else ""
|
|
388
|
+
automation_provider = (
|
|
389
|
+
PROVIDER_NONE
|
|
390
|
+
if raw_automation_provider == PROVIDER_NONE
|
|
391
|
+
else _provider_from_runtime_payload(
|
|
392
|
+
raw,
|
|
393
|
+
"automation_provider",
|
|
394
|
+
automation_backend if automation_enabled else BACKEND_NONE,
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
if not automation_enabled or automation_backend == BACKEND_NONE:
|
|
398
|
+
automation_provider = PROVIDER_NONE
|
|
399
|
+
elif automation_provider not in PROVIDER_KEYS:
|
|
400
|
+
automation_provider = client_to_provider(automation_backend) or PROVIDER_ANTHROPIC
|
|
401
|
+
|
|
402
|
+
providers = defaults["providers"]
|
|
403
|
+
raw_providers = raw.get("providers") if isinstance(raw.get("providers"), dict) else {}
|
|
404
|
+
normalized_providers = {}
|
|
405
|
+
for provider, expected_client in PROVIDER_TO_CLIENT.items():
|
|
406
|
+
raw_provider = raw_providers.get(provider) if isinstance(raw_providers.get(provider), dict) else {}
|
|
407
|
+
merged = dict(providers[provider])
|
|
408
|
+
merged.update({k: v for k, v in raw_provider.items() if k not in {"client"}})
|
|
409
|
+
merged["client"] = expected_client
|
|
410
|
+
normalized_providers[provider] = merged
|
|
411
|
+
|
|
412
|
+
fallback_policy = raw.get("fallback_policy") if isinstance(raw.get("fallback_policy"), dict) else {}
|
|
413
|
+
last_provider_change = raw.get("last_provider_change") if isinstance(raw.get("last_provider_change"), dict) else {}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
"schema_version": 1,
|
|
417
|
+
"selected_chat_provider": selected_provider,
|
|
418
|
+
"automation_provider": automation_provider,
|
|
419
|
+
"automation_backend": provider_to_client(automation_provider) if automation_provider in PROVIDER_KEYS else BACKEND_NONE,
|
|
420
|
+
"providers": normalized_providers,
|
|
421
|
+
"fallback_policy": {
|
|
422
|
+
"chat": str(fallback_policy.get("chat") or defaults["fallback_policy"]["chat"]),
|
|
423
|
+
"automation": "fail_closed",
|
|
424
|
+
},
|
|
425
|
+
"last_provider_change": {
|
|
426
|
+
"changed_at": last_provider_change.get("changed_at"),
|
|
427
|
+
"from_provider": normalize_provider_key(last_provider_change.get("from_provider")) or None,
|
|
428
|
+
"to_provider": normalize_provider_key(last_provider_change.get("to_provider")) or None,
|
|
429
|
+
"source": last_provider_change.get("source"),
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
234
434
|
def normalize_interactive_clients(value) -> dict[str, bool]:
|
|
235
435
|
if not isinstance(value, dict):
|
|
236
436
|
return dict(default_client_preferences()["interactive_clients"])
|
|
@@ -339,7 +539,10 @@ def normalize_automation_user_override(value) -> bool:
|
|
|
339
539
|
def normalize_automation_backend(value, *, automation_enabled: bool = True) -> str:
|
|
340
540
|
if not automation_enabled:
|
|
341
541
|
return BACKEND_NONE
|
|
542
|
+
raw = str(value or "").strip().lower()
|
|
342
543
|
candidate = normalize_backend_key(value)
|
|
544
|
+
if candidate == BACKEND_NONE and raw in {"none", "off", "disabled", "false", "0"}:
|
|
545
|
+
return BACKEND_NONE
|
|
343
546
|
if candidate in TERMINAL_CLIENT_KEYS:
|
|
344
547
|
return candidate
|
|
345
548
|
return CLIENT_CLAUDE_CODE
|
|
@@ -486,12 +689,27 @@ def normalize_client_preferences(
|
|
|
486
689
|
runtime_profiles = normalize_client_runtime_profiles(
|
|
487
690
|
schedule.get("client_runtime_profiles")
|
|
488
691
|
)
|
|
692
|
+
provider_runtime = normalize_provider_runtime(
|
|
693
|
+
schedule.get("provider_runtime"),
|
|
694
|
+
default_terminal_client=default_terminal_client,
|
|
695
|
+
automation_backend=automation_backend,
|
|
696
|
+
automation_enabled=automation_enabled,
|
|
697
|
+
)
|
|
698
|
+
selected_client = provider_to_client(provider_runtime.get("selected_chat_provider"))
|
|
699
|
+
if selected_client in TERMINAL_CLIENT_KEYS:
|
|
700
|
+
interactive_clients[selected_client] = True
|
|
701
|
+
default_terminal_client = normalize_default_terminal_client(
|
|
702
|
+
selected_client,
|
|
703
|
+
interactive_clients=interactive_clients,
|
|
704
|
+
)
|
|
705
|
+
automation_backend = provider_runtime["automation_backend"]
|
|
489
706
|
return {
|
|
490
707
|
"interactive_clients": interactive_clients,
|
|
491
708
|
"default_terminal_client": default_terminal_client,
|
|
492
709
|
"last_terminal_client": last_terminal_client,
|
|
493
710
|
"automation_enabled": automation_enabled,
|
|
494
711
|
"automation_backend": automation_backend,
|
|
712
|
+
"provider_runtime": provider_runtime,
|
|
495
713
|
"automation_user_override": automation_user_override,
|
|
496
714
|
"client_runtime_profiles": runtime_profiles,
|
|
497
715
|
"automation_task_profiles": normalize_automation_task_profiles(
|
|
@@ -530,6 +748,9 @@ def apply_client_preferences(
|
|
|
530
748
|
last_terminal_client: str | None = None,
|
|
531
749
|
automation_enabled=None,
|
|
532
750
|
automation_backend: str | None = None,
|
|
751
|
+
provider_runtime: dict | None = None,
|
|
752
|
+
selected_chat_provider: str | None = None,
|
|
753
|
+
automation_provider: str | None = None,
|
|
533
754
|
automation_user_override: bool | None = None,
|
|
534
755
|
client_runtime_profiles: dict | None = None,
|
|
535
756
|
automation_task_profiles: dict | None = None,
|
|
@@ -556,6 +777,45 @@ def apply_client_preferences(
|
|
|
556
777
|
automation_backend if automation_backend is not None else current["automation_backend"],
|
|
557
778
|
automation_enabled=merged["automation_enabled"],
|
|
558
779
|
)
|
|
780
|
+
raw_provider_runtime = (
|
|
781
|
+
provider_runtime
|
|
782
|
+
if provider_runtime is not None
|
|
783
|
+
else current.get("provider_runtime")
|
|
784
|
+
)
|
|
785
|
+
raw_provider_runtime = dict(raw_provider_runtime or {})
|
|
786
|
+
if selected_chat_provider is not None:
|
|
787
|
+
raw_provider_runtime["selected_chat_provider"] = selected_chat_provider
|
|
788
|
+
derived_chat_client = provider_to_client(selected_chat_provider)
|
|
789
|
+
if derived_chat_client:
|
|
790
|
+
merged["interactive_clients"][derived_chat_client] = True
|
|
791
|
+
merged["default_terminal_client"] = derived_chat_client
|
|
792
|
+
elif default_terminal_client is not None:
|
|
793
|
+
raw_provider_runtime["selected_chat_provider"] = client_to_provider(
|
|
794
|
+
merged["default_terminal_client"]
|
|
795
|
+
)
|
|
796
|
+
if automation_provider is not None:
|
|
797
|
+
raw_provider_runtime["automation_provider"] = automation_provider
|
|
798
|
+
derived_backend = provider_to_client(automation_provider)
|
|
799
|
+
if derived_backend:
|
|
800
|
+
merged["interactive_clients"][derived_backend] = True
|
|
801
|
+
merged["automation_backend"] = (
|
|
802
|
+
derived_backend if derived_backend else BACKEND_NONE
|
|
803
|
+
)
|
|
804
|
+
elif automation_backend is not None or automation_enabled is not None:
|
|
805
|
+
raw_provider_runtime["automation_provider"] = client_to_provider(
|
|
806
|
+
merged["automation_backend"]
|
|
807
|
+
) if merged["automation_backend"] != BACKEND_NONE else PROVIDER_NONE
|
|
808
|
+
merged["provider_runtime"] = normalize_provider_runtime(
|
|
809
|
+
raw_provider_runtime,
|
|
810
|
+
default_terminal_client=merged["default_terminal_client"],
|
|
811
|
+
automation_backend=merged["automation_backend"],
|
|
812
|
+
automation_enabled=merged["automation_enabled"],
|
|
813
|
+
)
|
|
814
|
+
merged["automation_backend"] = merged["provider_runtime"]["automation_backend"]
|
|
815
|
+
selected_client = provider_to_client(merged["provider_runtime"].get("selected_chat_provider"))
|
|
816
|
+
if selected_client in TERMINAL_CLIENT_KEYS:
|
|
817
|
+
merged["interactive_clients"][selected_client] = True
|
|
818
|
+
merged["default_terminal_client"] = selected_client
|
|
559
819
|
merged["automation_user_override"] = normalize_automation_user_override(
|
|
560
820
|
automation_user_override
|
|
561
821
|
if automation_user_override is not None
|
|
@@ -597,6 +857,9 @@ def save_client_preferences(
|
|
|
597
857
|
last_terminal_client: str | None = None,
|
|
598
858
|
automation_enabled=None,
|
|
599
859
|
automation_backend: str | None = None,
|
|
860
|
+
provider_runtime: dict | None = None,
|
|
861
|
+
selected_chat_provider: str | None = None,
|
|
862
|
+
automation_provider: str | None = None,
|
|
600
863
|
automation_user_override: bool | None = None,
|
|
601
864
|
client_runtime_profiles: dict | None = None,
|
|
602
865
|
automation_task_profiles: dict | None = None,
|
|
@@ -611,6 +874,9 @@ def save_client_preferences(
|
|
|
611
874
|
last_terminal_client=last_terminal_client,
|
|
612
875
|
automation_enabled=automation_enabled,
|
|
613
876
|
automation_backend=automation_backend,
|
|
877
|
+
provider_runtime=provider_runtime,
|
|
878
|
+
selected_chat_provider=selected_chat_provider,
|
|
879
|
+
automation_provider=automation_provider,
|
|
614
880
|
automation_user_override=automation_user_override,
|
|
615
881
|
client_runtime_profiles=client_runtime_profiles,
|
|
616
882
|
automation_task_profiles=automation_task_profiles,
|
|
@@ -666,7 +932,11 @@ def detect_installed_clients(user_home: str | os.PathLike[str] | None = None) ->
|
|
|
666
932
|
claude_bin = _managed_claude_binary(home)
|
|
667
933
|
else:
|
|
668
934
|
claude_bin = os.environ.get("CLAUDE_BIN", "").strip() or _which_with_nvm("claude", home)
|
|
669
|
-
|
|
935
|
+
if _desktop_product_requested(home):
|
|
936
|
+
managed_codex_bin = _managed_codex_binary(home)
|
|
937
|
+
codex_bin = managed_codex_bin if managed_codex_bin and _managed_codex_vendor_present(home) else ""
|
|
938
|
+
else:
|
|
939
|
+
codex_bin = os.environ.get("CODEX_BIN", "").strip() or _which_with_nvm("codex", home)
|
|
670
940
|
|
|
671
941
|
if sys.platform == "darwin":
|
|
672
942
|
desktop_app = next(
|
|
@@ -714,6 +984,28 @@ def resolve_terminal_client(requested: str | None = None, *, preferences: dict |
|
|
|
714
984
|
)
|
|
715
985
|
|
|
716
986
|
|
|
987
|
+
def resolve_selected_chat_provider(preferences: dict | None = None) -> str:
|
|
988
|
+
normalized = preferences or load_client_preferences()
|
|
989
|
+
provider_runtime = normalize_provider_runtime(
|
|
990
|
+
normalized.get("provider_runtime"),
|
|
991
|
+
default_terminal_client=normalized.get("default_terminal_client", CLIENT_CLAUDE_CODE),
|
|
992
|
+
automation_backend=normalized.get("automation_backend", CLIENT_CLAUDE_CODE),
|
|
993
|
+
automation_enabled=normalized.get("automation_enabled", True),
|
|
994
|
+
)
|
|
995
|
+
return provider_runtime["selected_chat_provider"]
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def resolve_automation_provider(preferences: dict | None = None) -> str:
|
|
999
|
+
normalized = preferences or load_client_preferences()
|
|
1000
|
+
provider_runtime = normalize_provider_runtime(
|
|
1001
|
+
normalized.get("provider_runtime"),
|
|
1002
|
+
default_terminal_client=normalized.get("default_terminal_client", CLIENT_CLAUDE_CODE),
|
|
1003
|
+
automation_backend=normalized.get("automation_backend", CLIENT_CLAUDE_CODE),
|
|
1004
|
+
automation_enabled=normalized.get("automation_enabled", True),
|
|
1005
|
+
)
|
|
1006
|
+
return provider_runtime["automation_provider"]
|
|
1007
|
+
|
|
1008
|
+
|
|
717
1009
|
def resolve_user_model(preferences: dict | None = None) -> str:
|
|
718
1010
|
"""Return the single model the user has configured.
|
|
719
1011
|
|