nexo-brain 7.28.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 +5 -1
- package/package.json +1 -1
- package/src/auto_update.py +72 -0
- package/src/automation_controls.py +187 -10
- package/src/automation_preferences.py +367 -0
- package/src/cli.py +157 -0
- package/src/cli_email.py +95 -0
- package/src/cron_recovery.py +58 -3
- package/src/crons/sync.py +47 -14
- package/src/db/_schema.py +18 -0
- package/src/email_presentation.py +243 -0
- package/src/model_defaults.json +4 -4
- package/src/model_defaults.py +9 -10
- package/src/morning_briefing.py +281 -0
- 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 +21 -0
- package/src/scripts/nexo-morning-agent.py +380 -71
- package/src/scripts/nexo-send-reply.py +49 -26
- package/src/server.py +1 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -1
- package/templates/core-prompts/morning-agent.md +5 -2
- 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
|
@@ -935,6 +935,7 @@ def list_scripts(include_core: bool = False) -> list[dict]:
|
|
|
935
935
|
or bool(contract.get("toggleable_core"))
|
|
936
936
|
)
|
|
937
937
|
entry["supports_extra_instructions"] = bool(contract.get("supports_extra_instructions"))
|
|
938
|
+
entry["supports_automation_preferences"] = bool(contract.get("supports_automation_preferences"))
|
|
938
939
|
entry["operator_extra_instructions"] = str(metadata.get("operator_extra_instructions") or "")
|
|
939
940
|
entry["runtime_contract"] = contract
|
|
940
941
|
entry["available"] = bool(contract.get("available", True))
|
|
@@ -945,6 +946,8 @@ def list_scripts(include_core: bool = False) -> list[dict]:
|
|
|
945
946
|
entry["schedule_type"] = str(contract.get("schedule_type") or "")
|
|
946
947
|
entry["schedule_source"] = str(contract.get("schedule_source") or "")
|
|
947
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")
|
|
948
951
|
entry["interval_seconds"] = int(contract.get("interval_seconds", 0) or 0)
|
|
949
952
|
entry["default_interval_seconds"] = int(contract.get("default_interval_seconds", 0) or 0)
|
|
950
953
|
entry["minimum_interval_seconds"] = int(contract.get("minimum_interval_seconds", 0) or 0)
|
|
@@ -2930,11 +2933,26 @@ def set_automation_instructions(name_or_path: str, instructions: str) -> dict:
|
|
|
2930
2933
|
return set_script_extra_instructions(name_or_path, instructions)
|
|
2931
2934
|
|
|
2932
2935
|
|
|
2936
|
+
def get_automation_preference_contract(name_or_path: str) -> dict:
|
|
2937
|
+
"""Return schema + current structured preferences for a product automation."""
|
|
2938
|
+
from automation_preferences import get_automation_preferences
|
|
2939
|
+
|
|
2940
|
+
return get_automation_preferences(name_or_path)
|
|
2941
|
+
|
|
2942
|
+
|
|
2943
|
+
def set_automation_preference_contract(name_or_path: str, payload: dict) -> dict:
|
|
2944
|
+
"""Persist structured preferences without touching extra instructions."""
|
|
2945
|
+
from automation_preferences import set_automation_preferences
|
|
2946
|
+
|
|
2947
|
+
return set_automation_preferences(name_or_path, payload)
|
|
2948
|
+
|
|
2949
|
+
|
|
2933
2950
|
def set_script_schedule_override(
|
|
2934
2951
|
name_or_path: str,
|
|
2935
2952
|
*,
|
|
2936
2953
|
interval_seconds: int | None = None,
|
|
2937
2954
|
daily_at: str | None = None,
|
|
2955
|
+
weekdays=None,
|
|
2938
2956
|
clear: bool = False,
|
|
2939
2957
|
) -> dict:
|
|
2940
2958
|
from automation_controls import set_core_automation_schedule
|
|
@@ -2949,6 +2967,7 @@ def set_script_schedule_override(
|
|
|
2949
2967
|
script.get("name", name_or_path),
|
|
2950
2968
|
interval_seconds=interval_seconds,
|
|
2951
2969
|
daily_at=daily_at,
|
|
2970
|
+
weekdays=weekdays,
|
|
2952
2971
|
clear=clear,
|
|
2953
2972
|
)
|
|
2954
2973
|
|
|
@@ -2958,6 +2977,7 @@ def set_automation_schedule(
|
|
|
2958
2977
|
*,
|
|
2959
2978
|
interval_seconds: int | None = None,
|
|
2960
2979
|
daily_at: str | None = None,
|
|
2980
|
+
weekdays=None,
|
|
2961
2981
|
clear: bool = False,
|
|
2962
2982
|
) -> dict:
|
|
2963
2983
|
"""Stable contract wrapper for automation cadence overrides."""
|
|
@@ -2965,6 +2985,7 @@ def set_automation_schedule(
|
|
|
2965
2985
|
name_or_path,
|
|
2966
2986
|
interval_seconds=interval_seconds,
|
|
2967
2987
|
daily_at=daily_at,
|
|
2988
|
+
weekdays=weekdays,
|
|
2968
2989
|
clear=clear,
|
|
2969
2990
|
)
|
|
2970
2991
|
|