nexo-brain 7.29.0 → 7.30.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/package.json +1 -1
- package/src/auto_update.py +72 -0
- package/src/automation_controls.py +180 -10
- package/src/automation_preferences.py +64 -20
- package/src/cli.py +37 -0
- package/src/cron_recovery.py +58 -3
- package/src/crons/sync.py +47 -14
- package/src/model_defaults.json +4 -4
- package/src/model_defaults.py +9 -10
- package/src/plugins/desktop_preferences.py +63 -0
- package/src/plugins/personal_scripts.py +2 -0
- package/src/plugins/update.py +4 -0
- package/src/preference_catalog.py +438 -0
- package/src/resonance_tiers.json +4 -4
- package/src/script_registry.py +6 -0
- package/src/scripts/nexo-morning-agent.py +263 -3
- package/src/scripts/nexo-send-reply.py +29 -1
- package/src/server.py +1 -0
- package/tool-enforcement-map.json +40 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Unified preference catalog for agent-facing read/explain/set operations."""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
AUTOMATION_ITEM_ES = {
|
|
13
|
+
"priorities": (
|
|
14
|
+
"Prioridades",
|
|
15
|
+
"Incluye las cosas más importantes del día: lo urgente, lo atrasado y lo que conviene resolver primero.",
|
|
16
|
+
),
|
|
17
|
+
"agenda": (
|
|
18
|
+
"Agenda",
|
|
19
|
+
"Añade citas, eventos o bloques de calendario que NEXO conozca para que sepas qué viene hoy.",
|
|
20
|
+
),
|
|
21
|
+
"reminders": (
|
|
22
|
+
"Recordatorios y tareas",
|
|
23
|
+
"Muestra recordatorios y tareas pendientes que pueden requerir acción durante el día.",
|
|
24
|
+
),
|
|
25
|
+
"followups": (
|
|
26
|
+
"Seguimientos",
|
|
27
|
+
"Incluye asuntos abiertos que NEXO debe empujar hasta que haya respuesta o decisión.",
|
|
28
|
+
),
|
|
29
|
+
"decisions": (
|
|
30
|
+
"Decisiones recientes",
|
|
31
|
+
"Resume decisiones recientes guardadas para no perder cambios importantes de criterio.",
|
|
32
|
+
),
|
|
33
|
+
"email_activity": (
|
|
34
|
+
"Emails enviados recientes",
|
|
35
|
+
"Añade emails enviados recientemente para dar contexto de conversaciones activas.",
|
|
36
|
+
),
|
|
37
|
+
"blockers": (
|
|
38
|
+
"Bloqueos y riesgos",
|
|
39
|
+
"Señala bloqueos, riesgos o asuntos que pueden frenarte si no los atiendes.",
|
|
40
|
+
),
|
|
41
|
+
"internal_refs": (
|
|
42
|
+
"Referencias internas",
|
|
43
|
+
"Incluye referencias internas útiles. Déjalo desactivado si prefieres un resumen más limpio.",
|
|
44
|
+
),
|
|
45
|
+
"news": (
|
|
46
|
+
"Noticias",
|
|
47
|
+
"Añade titulares públicos. Si internet o la fuente de noticias falla, NEXO seguirá preparando el resumen.",
|
|
48
|
+
),
|
|
49
|
+
"weather": (
|
|
50
|
+
"Tiempo",
|
|
51
|
+
"Añade el tiempo usando tu ubicación guardada en el perfil o en Desktop.",
|
|
52
|
+
),
|
|
53
|
+
"length": (
|
|
54
|
+
"Longitud",
|
|
55
|
+
"Controla cuánto detalle quieres: corto, normal o más completo.",
|
|
56
|
+
),
|
|
57
|
+
"tone": (
|
|
58
|
+
"Tono",
|
|
59
|
+
"Cambia cómo escribe el resumen: directo, cercano, ejecutivo o personal.",
|
|
60
|
+
),
|
|
61
|
+
"format": (
|
|
62
|
+
"Formato",
|
|
63
|
+
"Elige la estructura visual del resumen: secciones, lista de puntos o texto narrativo.",
|
|
64
|
+
),
|
|
65
|
+
"quiet_days": (
|
|
66
|
+
"Días tranquilos",
|
|
67
|
+
"Decide qué hacer cuando no hay novedades importantes.",
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fold(value: str) -> str:
|
|
73
|
+
return re.sub(r"\s+", " ", str(value or "").strip().lower())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _label(value: Any, locale: str = "es") -> str:
|
|
77
|
+
if isinstance(value, dict):
|
|
78
|
+
return str(value.get(locale) or value.get("es") or value.get("en") or "").strip()
|
|
79
|
+
return str(value or "").strip()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
83
|
+
try:
|
|
84
|
+
if path.is_file():
|
|
85
|
+
payload = json.loads(path.read_text())
|
|
86
|
+
if isinstance(payload, dict):
|
|
87
|
+
return payload
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
94
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_path(payload: dict[str, Any], dotted: str) -> Any:
|
|
99
|
+
current: Any = payload
|
|
100
|
+
for part in dotted.split("."):
|
|
101
|
+
if not isinstance(current, dict):
|
|
102
|
+
return None
|
|
103
|
+
current = current.get(part)
|
|
104
|
+
return current
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _set_path(payload: dict[str, Any], dotted: str, value: Any) -> dict[str, Any]:
|
|
108
|
+
next_payload = copy.deepcopy(payload)
|
|
109
|
+
current = next_payload
|
|
110
|
+
parts = dotted.split(".")
|
|
111
|
+
for part in parts[:-1]:
|
|
112
|
+
child = current.get(part)
|
|
113
|
+
if not isinstance(child, dict):
|
|
114
|
+
child = {}
|
|
115
|
+
current[part] = child
|
|
116
|
+
current = child
|
|
117
|
+
current[parts[-1]] = value
|
|
118
|
+
return next_payload
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _coerce_value(value: Any, entry: dict[str, Any]) -> Any:
|
|
122
|
+
kind = str(entry.get("type") or "").lower()
|
|
123
|
+
if kind in {"boolean", "toggle"}:
|
|
124
|
+
raw = str(value).strip().lower()
|
|
125
|
+
return raw in {"1", "true", "yes", "on", "si", "sí", "activar", "enabled"}
|
|
126
|
+
options = [str(option.get("value") if isinstance(option, dict) else option) for option in entry.get("options") or []]
|
|
127
|
+
if options:
|
|
128
|
+
raw = str(value).strip()
|
|
129
|
+
if raw not in options:
|
|
130
|
+
raise ValueError(f"Invalid value '{raw}'. Valid values: {', '.join(options)}")
|
|
131
|
+
return raw
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _brain_paths() -> dict[str, Path]:
|
|
136
|
+
import paths
|
|
137
|
+
|
|
138
|
+
brain = paths.brain_dir()
|
|
139
|
+
return {
|
|
140
|
+
"calibration.json": brain / "calibration.json",
|
|
141
|
+
"profile.json": brain / "profile.json",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _brain_schema_entries(*, include_values: bool, locale: str) -> list[dict[str, Any]]:
|
|
146
|
+
from desktop_bridge import _schema_fields
|
|
147
|
+
|
|
148
|
+
paths = _brain_paths()
|
|
149
|
+
cache = {name: _read_json(path) for name, path in paths.items()}
|
|
150
|
+
entries: list[dict[str, Any]] = []
|
|
151
|
+
for field in _schema_fields():
|
|
152
|
+
dotted = str(field.get("path") or field.get("writes") or "").strip()
|
|
153
|
+
file_name = str(field.get("file") or "").strip()
|
|
154
|
+
if not dotted or file_name not in paths:
|
|
155
|
+
continue
|
|
156
|
+
entry = {
|
|
157
|
+
"id": dotted,
|
|
158
|
+
"aliases": [field.get("id") or "", dotted.replace("user.", "assistant.")],
|
|
159
|
+
"section": "assistant" if dotted.startswith("user.") else "profile",
|
|
160
|
+
"label": _label(field.get("label") or field.get("prompt") or dotted, locale),
|
|
161
|
+
"help": _label(field.get("hint") or "", locale),
|
|
162
|
+
"type": str(field.get("type") or "text"),
|
|
163
|
+
"options": field.get("options") or [],
|
|
164
|
+
"writable": True,
|
|
165
|
+
"storage": file_name,
|
|
166
|
+
"path": dotted,
|
|
167
|
+
}
|
|
168
|
+
if include_values:
|
|
169
|
+
entry["current_value"] = _get_path(cache[file_name], dotted)
|
|
170
|
+
entries.append(entry)
|
|
171
|
+
return entries
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _automation_entries(*, include_values: bool, locale: str) -> list[dict[str, Any]]:
|
|
175
|
+
from automation_controls import get_core_automation_schedule_state
|
|
176
|
+
from automation_preferences import get_automation_preferences
|
|
177
|
+
|
|
178
|
+
contract = get_automation_preferences("morning-agent")
|
|
179
|
+
schema = contract.get("schema") or {}
|
|
180
|
+
values = ((contract.get("preferences") or {}).get("values") or {}) if include_values else {}
|
|
181
|
+
entries: list[dict[str, Any]] = []
|
|
182
|
+
for group in list(schema.get("groups") or []):
|
|
183
|
+
for item in list(group.get("items") or []):
|
|
184
|
+
item_id = str(item.get("id") or "").strip()
|
|
185
|
+
if not item_id:
|
|
186
|
+
continue
|
|
187
|
+
localized = AUTOMATION_ITEM_ES.get(item_id) if locale == "es" else None
|
|
188
|
+
label = (localized[0] if localized else str(item.get("label") or item_id))
|
|
189
|
+
help_text = (localized[1] if localized else str(item.get("help") or item.get("disabled_reason") or ""))
|
|
190
|
+
entry = {
|
|
191
|
+
"id": f"automation.morning-agent.{item_id}",
|
|
192
|
+
"aliases": [
|
|
193
|
+
f"morning-agent.{item_id}",
|
|
194
|
+
f"resumen.{item_id}",
|
|
195
|
+
str(item.get("label") or ""),
|
|
196
|
+
item_id.replace("_", " "),
|
|
197
|
+
label,
|
|
198
|
+
],
|
|
199
|
+
"section": "automation.morning-agent",
|
|
200
|
+
"group": str(group.get("id") or ""),
|
|
201
|
+
"label": label,
|
|
202
|
+
"help": help_text,
|
|
203
|
+
"type": str(item.get("type") or "text"),
|
|
204
|
+
"options": list(item.get("options") or []),
|
|
205
|
+
"writable": not bool(item.get("disabled")),
|
|
206
|
+
"storage": "personal_scripts.metadata.automation_preferences",
|
|
207
|
+
"path": item_id,
|
|
208
|
+
}
|
|
209
|
+
if include_values:
|
|
210
|
+
entry["current_value"] = values.get(item_id, item.get("default"))
|
|
211
|
+
entries.append(entry)
|
|
212
|
+
|
|
213
|
+
schedule_state = get_core_automation_schedule_state("morning-agent")
|
|
214
|
+
schedule_entry = {
|
|
215
|
+
"id": "automation.morning-agent.schedule",
|
|
216
|
+
"aliases": ["resumen horario", "resumen frecuencia", "morning-agent schedule"],
|
|
217
|
+
"section": "automation.morning-agent",
|
|
218
|
+
"group": "schedule",
|
|
219
|
+
"label": "Horario del resumen de la mañana",
|
|
220
|
+
"help": "Hora y días en que se ejecuta el resumen. Acepta valores como 07:00 Mon-Fri o {\"daily_at\":\"07:00\",\"weekdays\":\"Tue,Sat\"}.",
|
|
221
|
+
"type": "calendar",
|
|
222
|
+
"writable": True,
|
|
223
|
+
"storage": "schedule.json.core_automation_overrides",
|
|
224
|
+
"path": "morning-agent.schedule",
|
|
225
|
+
}
|
|
226
|
+
if include_values:
|
|
227
|
+
schedule_entry["current_value"] = {
|
|
228
|
+
"label": schedule_state.get("effective_schedule_label"),
|
|
229
|
+
"schedule": schedule_state.get("schedule"),
|
|
230
|
+
"source": schedule_state.get("schedule_source"),
|
|
231
|
+
}
|
|
232
|
+
entries.append(schedule_entry)
|
|
233
|
+
return entries
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _client_entries(*, include_values: bool) -> list[dict[str, Any]]:
|
|
237
|
+
from client_preferences import load_client_preferences
|
|
238
|
+
from resonance_map import TIERS, _load_user_default_resonance
|
|
239
|
+
|
|
240
|
+
prefs = load_client_preferences()
|
|
241
|
+
provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
|
|
242
|
+
definitions = [
|
|
243
|
+
("client.default_terminal_client", "Cliente de chat por defecto", "codex o claude_code", ["codex", "claude_code"]),
|
|
244
|
+
("client.automation_enabled", "Automatizaciones activadas", "Activa o pausa rutinas de fondo.", []),
|
|
245
|
+
("client.automation_backend", "Motor de automatizaciones", "Backend usado por automatizaciones.", ["codex", "claude_code", "none"]),
|
|
246
|
+
("client.selected_chat_provider", "Proveedor de chat", "Proveedor seleccionado para conversaciones.", ["openai", "anthropic"]),
|
|
247
|
+
("client.default_resonance", "Nivel de razonamiento", "Nivel bajo/medio/alto/maximo usado por NEXO.", list(TIERS)),
|
|
248
|
+
]
|
|
249
|
+
current = {
|
|
250
|
+
"client.default_terminal_client": prefs.get("default_terminal_client"),
|
|
251
|
+
"client.automation_enabled": prefs.get("automation_enabled"),
|
|
252
|
+
"client.automation_backend": prefs.get("automation_backend"),
|
|
253
|
+
"client.selected_chat_provider": provider_runtime.get("selected_chat_provider"),
|
|
254
|
+
"client.default_resonance": _load_user_default_resonance() or prefs.get("default_resonance"),
|
|
255
|
+
}
|
|
256
|
+
entries = []
|
|
257
|
+
for pref_id, label, help_text, options in definitions:
|
|
258
|
+
entry = {
|
|
259
|
+
"id": pref_id,
|
|
260
|
+
"aliases": [pref_id.replace("client.", "")],
|
|
261
|
+
"section": "client",
|
|
262
|
+
"label": label,
|
|
263
|
+
"help": help_text,
|
|
264
|
+
"type": "boolean" if pref_id.endswith("automation_enabled") else "choice",
|
|
265
|
+
"options": options,
|
|
266
|
+
"writable": True,
|
|
267
|
+
"storage": "schedule.json",
|
|
268
|
+
"path": pref_id,
|
|
269
|
+
}
|
|
270
|
+
if include_values:
|
|
271
|
+
entry["current_value"] = current.get(pref_id)
|
|
272
|
+
entries.append(entry)
|
|
273
|
+
return entries
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _desktop_app_entries() -> list[dict[str, Any]]:
|
|
277
|
+
return [{
|
|
278
|
+
"id": "desktop.app",
|
|
279
|
+
"aliases": ["app settings", "preferencias de app", "desktop preferences"],
|
|
280
|
+
"section": "desktop.app",
|
|
281
|
+
"label": "Preferencias visuales y locales de Desktop",
|
|
282
|
+
"help": "Estas opciones viven en Desktop y deben cambiarse desde Desktop para disparar guardado, mirrors y refrescos de UI.",
|
|
283
|
+
"type": "catalog",
|
|
284
|
+
"writable": False,
|
|
285
|
+
"storage": "desktop.app-settings.json",
|
|
286
|
+
"path": "app.*",
|
|
287
|
+
}]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def build_preference_catalog(*, include_values: bool = False, query: str | None = None, locale: str = "es") -> dict[str, Any]:
|
|
291
|
+
entries = [
|
|
292
|
+
*_brain_schema_entries(include_values=include_values, locale=locale),
|
|
293
|
+
*_automation_entries(include_values=include_values, locale=locale),
|
|
294
|
+
*_client_entries(include_values=include_values),
|
|
295
|
+
*_desktop_app_entries(),
|
|
296
|
+
]
|
|
297
|
+
needle = _fold(query or "")
|
|
298
|
+
if needle:
|
|
299
|
+
entries = [
|
|
300
|
+
entry for entry in entries
|
|
301
|
+
if needle in _fold(" ".join([
|
|
302
|
+
str(entry.get("id") or ""),
|
|
303
|
+
str(entry.get("label") or ""),
|
|
304
|
+
str(entry.get("help") or ""),
|
|
305
|
+
" ".join(str(alias) for alias in entry.get("aliases") or []),
|
|
306
|
+
]))
|
|
307
|
+
]
|
|
308
|
+
return {"ok": True, "count": len(entries), "preferences": entries}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def find_preference(id_or_alias: str, *, include_values: bool = True) -> dict[str, Any] | None:
|
|
312
|
+
target = _fold(id_or_alias)
|
|
313
|
+
if not target:
|
|
314
|
+
return None
|
|
315
|
+
catalog = build_preference_catalog(include_values=include_values)
|
|
316
|
+
for entry in catalog["preferences"]:
|
|
317
|
+
keys = [entry.get("id"), *(entry.get("aliases") or [])]
|
|
318
|
+
if any(_fold(key) == target for key in keys):
|
|
319
|
+
return entry
|
|
320
|
+
matches = build_preference_catalog(include_values=include_values, query=id_or_alias)["preferences"]
|
|
321
|
+
return matches[0] if len(matches) == 1 else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def explain_preference(id_or_alias: str) -> dict[str, Any]:
|
|
325
|
+
entry = find_preference(id_or_alias, include_values=True)
|
|
326
|
+
if not entry:
|
|
327
|
+
return {"ok": False, "error": f"Preference not found: {id_or_alias}"}
|
|
328
|
+
return {"ok": True, "preference": entry}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _set_brain_entry(entry: dict[str, Any], value: Any, *, dry_run: bool) -> dict[str, Any]:
|
|
332
|
+
file_name = str(entry.get("storage") or "")
|
|
333
|
+
dotted = str(entry.get("path") or "")
|
|
334
|
+
paths = _brain_paths()
|
|
335
|
+
path = paths.get(file_name)
|
|
336
|
+
if not path:
|
|
337
|
+
return {"ok": False, "error": f"Unsupported storage: {file_name}"}
|
|
338
|
+
current = _read_json(path)
|
|
339
|
+
coerced = _coerce_value(value, entry)
|
|
340
|
+
next_payload = _set_path(current, dotted, coerced)
|
|
341
|
+
if not dry_run:
|
|
342
|
+
_write_json(path, next_payload)
|
|
343
|
+
return {"ok": True, "dry_run": dry_run, "id": entry["id"], "value": coerced, "storage": file_name}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _set_automation_entry(entry: dict[str, Any], value: Any, *, dry_run: bool) -> dict[str, Any]:
|
|
347
|
+
from automation_preferences import get_automation_preferences, set_automation_preferences
|
|
348
|
+
|
|
349
|
+
key = str(entry.get("path") or "")
|
|
350
|
+
contract = get_automation_preferences("morning-agent")
|
|
351
|
+
values = dict(((contract.get("preferences") or {}).get("values") or {}))
|
|
352
|
+
coerced = _coerce_value(value, entry)
|
|
353
|
+
values[key] = coerced
|
|
354
|
+
if not dry_run:
|
|
355
|
+
result = set_automation_preferences("morning-agent", {"values": values})
|
|
356
|
+
if not result.get("ok"):
|
|
357
|
+
return result
|
|
358
|
+
return {"ok": True, "dry_run": dry_run, "id": entry["id"], "value": coerced}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_schedule_value(value: Any) -> tuple[str, Any]:
|
|
362
|
+
if isinstance(value, dict):
|
|
363
|
+
return str(value.get("daily_at") or value.get("time") or "").strip(), value.get("weekdays")
|
|
364
|
+
text = str(value or "").strip()
|
|
365
|
+
if text.startswith("{"):
|
|
366
|
+
payload = json.loads(text)
|
|
367
|
+
if not isinstance(payload, dict):
|
|
368
|
+
raise ValueError("Schedule JSON must be an object.")
|
|
369
|
+
return _parse_schedule_value(payload)
|
|
370
|
+
match = re.match(r"^(\d{1,2}:\d{2})(?:\s+(.+))?$", text)
|
|
371
|
+
if not match:
|
|
372
|
+
raise ValueError("Use HH:MM, optionally followed by days, e.g. 07:00 Mon-Fri.")
|
|
373
|
+
return match.group(1), (match.group(2) or "")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _set_schedule_entry(entry: dict[str, Any], value: Any, *, dry_run: bool) -> dict[str, Any]:
|
|
377
|
+
from script_registry import set_automation_schedule
|
|
378
|
+
|
|
379
|
+
daily_at, weekdays = _parse_schedule_value(value)
|
|
380
|
+
if not daily_at:
|
|
381
|
+
return {"ok": False, "error": "daily_at is required."}
|
|
382
|
+
if dry_run:
|
|
383
|
+
return {"ok": True, "dry_run": True, "id": entry["id"], "daily_at": daily_at, "weekdays": weekdays}
|
|
384
|
+
result = set_automation_schedule("morning-agent", daily_at=daily_at, weekdays=weekdays)
|
|
385
|
+
result["dry_run"] = False
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _set_client_entry(entry: dict[str, Any], value: Any, *, dry_run: bool) -> dict[str, Any]:
|
|
390
|
+
from client_preferences import load_client_preferences, save_client_preferences
|
|
391
|
+
|
|
392
|
+
pref_id = str(entry.get("id") or "")
|
|
393
|
+
coerced = _coerce_value(value, entry)
|
|
394
|
+
if dry_run:
|
|
395
|
+
return {"ok": True, "dry_run": True, "id": pref_id, "value": coerced}
|
|
396
|
+
if pref_id == "client.default_terminal_client":
|
|
397
|
+
save_client_preferences(default_terminal_client=str(coerced))
|
|
398
|
+
elif pref_id == "client.automation_enabled":
|
|
399
|
+
save_client_preferences(automation_enabled=bool(coerced), automation_user_override=True)
|
|
400
|
+
elif pref_id == "client.automation_backend":
|
|
401
|
+
save_client_preferences(automation_backend=str(coerced), automation_user_override=True)
|
|
402
|
+
elif pref_id == "client.selected_chat_provider":
|
|
403
|
+
save_client_preferences(selected_chat_provider=str(coerced))
|
|
404
|
+
elif pref_id == "client.default_resonance":
|
|
405
|
+
save_client_preferences(default_resonance=str(coerced))
|
|
406
|
+
try:
|
|
407
|
+
from cli import _write_calibration_default_resonance
|
|
408
|
+
|
|
409
|
+
_write_calibration_default_resonance(str(coerced))
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
else:
|
|
413
|
+
return {"ok": False, "error": f"Unsupported client preference: {pref_id}"}
|
|
414
|
+
return {"ok": True, "dry_run": False, "id": pref_id, "value": coerced, "preferences": load_client_preferences()}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def set_preference(id_or_alias: str, value: Any, *, dry_run: bool = False) -> dict[str, Any]:
|
|
418
|
+
entry = find_preference(id_or_alias, include_values=False)
|
|
419
|
+
if not entry:
|
|
420
|
+
return {"ok": False, "error": f"Preference not found: {id_or_alias}"}
|
|
421
|
+
if not entry.get("writable"):
|
|
422
|
+
return {"ok": False, "error": "This preference is read-only from Brain; use Desktop for app.* preferences.", "preference": entry}
|
|
423
|
+
pref_id = str(entry.get("id") or "")
|
|
424
|
+
if pref_id == "automation.morning-agent.schedule":
|
|
425
|
+
return _set_schedule_entry(entry, value, dry_run=dry_run)
|
|
426
|
+
if pref_id.startswith("automation.morning-agent."):
|
|
427
|
+
return _set_automation_entry(entry, value, dry_run=dry_run)
|
|
428
|
+
if pref_id.startswith("client."):
|
|
429
|
+
return _set_client_entry(entry, value, dry_run=dry_run)
|
|
430
|
+
return _set_brain_entry(entry, value, dry_run=dry_run)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
__all__ = [
|
|
434
|
+
"build_preference_catalog",
|
|
435
|
+
"explain_preference",
|
|
436
|
+
"find_preference",
|
|
437
|
+
"set_preference",
|
|
438
|
+
]
|
package/src/resonance_tiers.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"tiers": {
|
|
3
3
|
"maximo": {
|
|
4
|
-
"claude_code": { "model": "claude-opus-4-
|
|
4
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "max" },
|
|
5
5
|
"codex": { "model": "gpt-5.5", "effort": "xhigh" }
|
|
6
6
|
},
|
|
7
7
|
"alto": {
|
|
8
|
-
"claude_code": { "model": "claude-opus-4-
|
|
8
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "xhigh" },
|
|
9
9
|
"codex": { "model": "gpt-5.5", "effort": "high" }
|
|
10
10
|
},
|
|
11
11
|
"medio": {
|
|
12
|
-
"claude_code": { "model": "claude-opus-4-
|
|
12
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "high" },
|
|
13
13
|
"codex": { "model": "gpt-5.5", "effort": "medium" }
|
|
14
14
|
},
|
|
15
15
|
"bajo": {
|
|
16
|
-
"claude_code": { "model": "claude-opus-4-
|
|
16
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "medium" },
|
|
17
17
|
"codex": { "model": "gpt-5.5", "effort": "low" }
|
|
18
18
|
},
|
|
19
19
|
"muy_bajo": {
|
package/src/script_registry.py
CHANGED
|
@@ -946,6 +946,8 @@ def list_scripts(include_core: bool = False) -> list[dict]:
|
|
|
946
946
|
entry["schedule_type"] = str(contract.get("schedule_type") or "")
|
|
947
947
|
entry["schedule_source"] = str(contract.get("schedule_source") or "")
|
|
948
948
|
entry["effective_schedule_label"] = str(contract.get("effective_schedule_label") or "")
|
|
949
|
+
entry["schedule"] = contract.get("schedule")
|
|
950
|
+
entry["default_schedule"] = contract.get("default_schedule")
|
|
949
951
|
entry["interval_seconds"] = int(contract.get("interval_seconds", 0) or 0)
|
|
950
952
|
entry["default_interval_seconds"] = int(contract.get("default_interval_seconds", 0) or 0)
|
|
951
953
|
entry["minimum_interval_seconds"] = int(contract.get("minimum_interval_seconds", 0) or 0)
|
|
@@ -2950,6 +2952,7 @@ def set_script_schedule_override(
|
|
|
2950
2952
|
*,
|
|
2951
2953
|
interval_seconds: int | None = None,
|
|
2952
2954
|
daily_at: str | None = None,
|
|
2955
|
+
weekdays=None,
|
|
2953
2956
|
clear: bool = False,
|
|
2954
2957
|
) -> dict:
|
|
2955
2958
|
from automation_controls import set_core_automation_schedule
|
|
@@ -2964,6 +2967,7 @@ def set_script_schedule_override(
|
|
|
2964
2967
|
script.get("name", name_or_path),
|
|
2965
2968
|
interval_seconds=interval_seconds,
|
|
2966
2969
|
daily_at=daily_at,
|
|
2970
|
+
weekdays=weekdays,
|
|
2967
2971
|
clear=clear,
|
|
2968
2972
|
)
|
|
2969
2973
|
|
|
@@ -2973,6 +2977,7 @@ def set_automation_schedule(
|
|
|
2973
2977
|
*,
|
|
2974
2978
|
interval_seconds: int | None = None,
|
|
2975
2979
|
daily_at: str | None = None,
|
|
2980
|
+
weekdays=None,
|
|
2976
2981
|
clear: bool = False,
|
|
2977
2982
|
) -> dict:
|
|
2978
2983
|
"""Stable contract wrapper for automation cadence overrides."""
|
|
@@ -2980,6 +2985,7 @@ def set_automation_schedule(
|
|
|
2980
2985
|
name_or_path,
|
|
2981
2986
|
interval_seconds=interval_seconds,
|
|
2982
2987
|
daily_at=daily_at,
|
|
2988
|
+
weekdays=weekdays,
|
|
2983
2989
|
clear=clear,
|
|
2984
2990
|
)
|
|
2985
2991
|
|