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,367 @@
|
|
|
1
|
+
"""Structured operator preferences for product automations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import unicodedata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
AUTOMATION_PREFERENCES_METADATA_KEY = "automation_preferences"
|
|
13
|
+
SUPPORTED_AUTOMATIONS = {"morning-agent"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MORNING_AGENT_SCHEMA: dict[str, Any] = {
|
|
17
|
+
"schema_version": 1,
|
|
18
|
+
"automation": "morning-agent",
|
|
19
|
+
"title": "Morning briefing content",
|
|
20
|
+
"groups": [
|
|
21
|
+
{
|
|
22
|
+
"id": "content",
|
|
23
|
+
"label": "Content",
|
|
24
|
+
"items": [
|
|
25
|
+
{
|
|
26
|
+
"id": "priorities",
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"label": "Priorities",
|
|
29
|
+
"default": True,
|
|
30
|
+
"help": "The most important things NEXO thinks you should look at first.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "agenda",
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"label": "Agenda",
|
|
36
|
+
"default": True,
|
|
37
|
+
"help": "Calendar-like items, dated work and events that affect today.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "reminders",
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"label": "Reminders and tasks",
|
|
43
|
+
"default": True,
|
|
44
|
+
"help": "Pending reminders and tasks saved in NEXO.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "followups",
|
|
48
|
+
"type": "boolean",
|
|
49
|
+
"label": "Follow-ups",
|
|
50
|
+
"default": True,
|
|
51
|
+
"help": "Open work that NEXO is tracking until it is resolved or clearly blocked.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "decisions",
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"label": "Recent decisions",
|
|
57
|
+
"default": True,
|
|
58
|
+
"help": "Important decisions recorded recently so they are not forgotten.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "email_activity",
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"label": "Recent sent email",
|
|
64
|
+
"default": True,
|
|
65
|
+
"help": "Emails NEXO sent recently, useful to know what moved while you were away.",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "blockers",
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"label": "Blockers and risks",
|
|
71
|
+
"default": True,
|
|
72
|
+
"help": "Things that may stop progress, need your decision or could become a problem.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "internal_refs",
|
|
76
|
+
"type": "boolean",
|
|
77
|
+
"label": "Internal references",
|
|
78
|
+
"default": False,
|
|
79
|
+
"help": "Technical file names, IDs or internal references. Keep this off for a cleaner human summary.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "news",
|
|
83
|
+
"type": "boolean",
|
|
84
|
+
"label": "News",
|
|
85
|
+
"default": False,
|
|
86
|
+
"help": "A short set of public headlines from the configured news feed. If the feed is unreachable, NEXO simply says it was unavailable.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "weather",
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"label": "Weather",
|
|
92
|
+
"default": True,
|
|
93
|
+
"help": "Today's weather from the location saved in Desktop or your residence in the profile.",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "style",
|
|
99
|
+
"label": "Style",
|
|
100
|
+
"items": [
|
|
101
|
+
{
|
|
102
|
+
"id": "length",
|
|
103
|
+
"type": "choice",
|
|
104
|
+
"label": "Length",
|
|
105
|
+
"default": "normal",
|
|
106
|
+
"options": ["short", "normal", "detailed"],
|
|
107
|
+
"help": "How much detail the briefing should include.",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "tone",
|
|
111
|
+
"type": "choice",
|
|
112
|
+
"label": "Tone",
|
|
113
|
+
"default": "direct",
|
|
114
|
+
"options": ["direct", "warm", "executive", "personal"],
|
|
115
|
+
"help": "How NEXO should write the summary.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "format",
|
|
119
|
+
"type": "choice",
|
|
120
|
+
"label": "Format",
|
|
121
|
+
"default": "sections",
|
|
122
|
+
"options": ["sections", "bullets", "narrative"],
|
|
123
|
+
"help": "How the briefing is organized visually.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "delivery",
|
|
129
|
+
"label": "Delivery",
|
|
130
|
+
"items": [
|
|
131
|
+
{
|
|
132
|
+
"id": "quiet_days",
|
|
133
|
+
"type": "choice",
|
|
134
|
+
"label": "Quiet days",
|
|
135
|
+
"default": "summary_if_anything_important",
|
|
136
|
+
"options": ["always_send", "summary_if_anything_important", "skip_if_empty"],
|
|
137
|
+
"help": "What NEXO should do on days with little or no important activity.",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _schema_for(name: str) -> dict[str, Any] | None:
|
|
146
|
+
clean = normalize_automation_name(name)
|
|
147
|
+
if clean == "morning-agent":
|
|
148
|
+
return copy.deepcopy(MORNING_AGENT_SCHEMA)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def normalize_automation_name(name: str) -> str:
|
|
153
|
+
return str(name or "").strip().lower().replace("_", "-")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def supports_automation_preferences(name: str) -> bool:
|
|
157
|
+
return normalize_automation_name(name) in SUPPORTED_AUTOMATIONS
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_automation_preference_schema(name: str) -> dict[str, Any]:
|
|
161
|
+
schema = _schema_for(name)
|
|
162
|
+
if not schema:
|
|
163
|
+
return {
|
|
164
|
+
"schema_version": 1,
|
|
165
|
+
"automation": normalize_automation_name(name),
|
|
166
|
+
"title": "Automation content",
|
|
167
|
+
"groups": [],
|
|
168
|
+
}
|
|
169
|
+
return schema
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _iter_schema_items(schema: dict[str, Any]):
|
|
173
|
+
for group in list(schema.get("groups") or []):
|
|
174
|
+
for item in list(group.get("items") or []):
|
|
175
|
+
if isinstance(item, dict) and item.get("id"):
|
|
176
|
+
yield item
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def default_automation_preferences(name: str) -> dict[str, Any]:
|
|
180
|
+
schema = get_automation_preference_schema(name)
|
|
181
|
+
values: dict[str, Any] = {}
|
|
182
|
+
for item in _iter_schema_items(schema):
|
|
183
|
+
values[str(item["id"])] = copy.deepcopy(item.get("default"))
|
|
184
|
+
return {
|
|
185
|
+
"schema_version": int(schema.get("schema_version") or 1),
|
|
186
|
+
"automation": normalize_automation_name(name),
|
|
187
|
+
"values": values,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def validate_automation_preferences(name: str, payload: dict[str, Any] | None) -> dict[str, Any]:
|
|
192
|
+
schema = get_automation_preference_schema(name)
|
|
193
|
+
defaults = default_automation_preferences(name)
|
|
194
|
+
source_values = {}
|
|
195
|
+
if isinstance(payload, dict):
|
|
196
|
+
if isinstance(payload.get("values"), dict):
|
|
197
|
+
source_values = payload.get("values") or {}
|
|
198
|
+
else:
|
|
199
|
+
source_values = payload
|
|
200
|
+
values = dict(defaults["values"])
|
|
201
|
+
warnings: list[str] = []
|
|
202
|
+
for item in _iter_schema_items(schema):
|
|
203
|
+
key = str(item["id"])
|
|
204
|
+
if key not in source_values:
|
|
205
|
+
continue
|
|
206
|
+
if item.get("disabled"):
|
|
207
|
+
values[key] = copy.deepcopy(item.get("default"))
|
|
208
|
+
warnings.append(f"{key}: disabled")
|
|
209
|
+
continue
|
|
210
|
+
kind = str(item.get("type") or "text")
|
|
211
|
+
raw_value = source_values.get(key)
|
|
212
|
+
if kind == "boolean":
|
|
213
|
+
values[key] = bool(raw_value)
|
|
214
|
+
elif kind == "choice":
|
|
215
|
+
options = [str(v) for v in list(item.get("options") or [])]
|
|
216
|
+
clean = str(raw_value or "").strip()
|
|
217
|
+
if clean in options:
|
|
218
|
+
values[key] = clean
|
|
219
|
+
else:
|
|
220
|
+
warnings.append(f"{key}: invalid choice")
|
|
221
|
+
elif kind == "number":
|
|
222
|
+
try:
|
|
223
|
+
values[key] = int(raw_value)
|
|
224
|
+
except Exception:
|
|
225
|
+
warnings.append(f"{key}: invalid number")
|
|
226
|
+
else:
|
|
227
|
+
values[key] = str(raw_value or "").strip()[:1000]
|
|
228
|
+
return {
|
|
229
|
+
"schema_version": int(schema.get("schema_version") or 1),
|
|
230
|
+
"automation": normalize_automation_name(name),
|
|
231
|
+
"values": values,
|
|
232
|
+
"warnings": warnings,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _script_row_for(name_or_path: str) -> tuple[dict, dict] | tuple[None, None]:
|
|
237
|
+
from db import init_db
|
|
238
|
+
from db._personal_scripts import get_personal_script
|
|
239
|
+
from script_registry import resolve_script, sync_personal_scripts
|
|
240
|
+
|
|
241
|
+
init_db()
|
|
242
|
+
sync_personal_scripts()
|
|
243
|
+
script = get_personal_script(name_or_path, include_core=True) or resolve_script(name_or_path)
|
|
244
|
+
if not script and normalize_automation_name(name_or_path) == "morning-agent":
|
|
245
|
+
script_path = Path(__file__).resolve().parent / "scripts" / "nexo-morning-agent.py"
|
|
246
|
+
script = {
|
|
247
|
+
"name": "morning-agent",
|
|
248
|
+
"path": str(script_path),
|
|
249
|
+
"description": "Generate and send the operator's daily morning briefing email.",
|
|
250
|
+
"runtime": "python",
|
|
251
|
+
"core": True,
|
|
252
|
+
"metadata": {},
|
|
253
|
+
"origin": "core",
|
|
254
|
+
}
|
|
255
|
+
if not script:
|
|
256
|
+
return None, None
|
|
257
|
+
existing = get_personal_script(script.get("path", ""), include_core=True) or script
|
|
258
|
+
return script, existing
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_automation_preferences(name_or_path: str) -> dict[str, Any]:
|
|
262
|
+
clean_name = normalize_automation_name(name_or_path)
|
|
263
|
+
script, existing = _script_row_for(name_or_path)
|
|
264
|
+
if script:
|
|
265
|
+
clean_name = normalize_automation_name(script.get("name") or clean_name)
|
|
266
|
+
metadata = (existing or {}).get("metadata") if isinstance((existing or {}).get("metadata"), dict) else {}
|
|
267
|
+
stored = metadata.get(AUTOMATION_PREFERENCES_METADATA_KEY) if isinstance(metadata, dict) else {}
|
|
268
|
+
validated = validate_automation_preferences(clean_name, stored if isinstance(stored, dict) else {})
|
|
269
|
+
return {
|
|
270
|
+
"ok": True,
|
|
271
|
+
"name": clean_name,
|
|
272
|
+
"schema": get_automation_preference_schema(clean_name),
|
|
273
|
+
"preferences": validated,
|
|
274
|
+
"supports_automation_preferences": supports_automation_preferences(clean_name),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def set_automation_preferences(name_or_path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
279
|
+
from db._personal_scripts import upsert_personal_script
|
|
280
|
+
|
|
281
|
+
script, existing = _script_row_for(name_or_path)
|
|
282
|
+
if not script:
|
|
283
|
+
return {"ok": False, "error": f"Automation not found: {name_or_path}"}
|
|
284
|
+
clean_name = normalize_automation_name(script.get("name") or name_or_path)
|
|
285
|
+
if not supports_automation_preferences(clean_name):
|
|
286
|
+
return {"ok": False, "error": "This automation does not support structured preferences."}
|
|
287
|
+
validated = validate_automation_preferences(clean_name, payload)
|
|
288
|
+
metadata = dict((existing or script).get("metadata") or {})
|
|
289
|
+
metadata[AUTOMATION_PREFERENCES_METADATA_KEY] = {
|
|
290
|
+
"schema_version": validated["schema_version"],
|
|
291
|
+
"values": validated["values"],
|
|
292
|
+
}
|
|
293
|
+
script_origin = "core" if (bool(script.get("core")) or str(script.get("origin") or "") == "core") else "user"
|
|
294
|
+
upsert_personal_script(
|
|
295
|
+
name=script.get("name", clean_name),
|
|
296
|
+
path=script.get("path", ""),
|
|
297
|
+
description=script.get("description", ""),
|
|
298
|
+
runtime=script.get("runtime", "unknown"),
|
|
299
|
+
metadata=metadata,
|
|
300
|
+
created_by="nexo-core" if script_origin == "core" else "manual",
|
|
301
|
+
source="core-toggle" if script_origin == "core" else "filesystem",
|
|
302
|
+
origin=script_origin,
|
|
303
|
+
enabled=bool((existing or script).get("enabled", True)),
|
|
304
|
+
has_inline_metadata=bool(script.get("metadata")),
|
|
305
|
+
)
|
|
306
|
+
return {
|
|
307
|
+
"ok": True,
|
|
308
|
+
"name": clean_name,
|
|
309
|
+
"preferences": validated,
|
|
310
|
+
"supports_automation_preferences": True,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def search_automation_preference_schema(name: str, query: str) -> list[dict[str, Any]]:
|
|
315
|
+
clean_query = _fold_text(query)
|
|
316
|
+
if not clean_query:
|
|
317
|
+
return []
|
|
318
|
+
matches: list[dict[str, Any]] = []
|
|
319
|
+
for group in list(get_automation_preference_schema(name).get("groups") or []):
|
|
320
|
+
for item in list(group.get("items") or []):
|
|
321
|
+
text = " ".join([
|
|
322
|
+
str(item.get("id") or ""),
|
|
323
|
+
str(item.get("label") or ""),
|
|
324
|
+
str(item.get("disabled_reason") or ""),
|
|
325
|
+
str(item.get("help") or ""),
|
|
326
|
+
str(group.get("label") or ""),
|
|
327
|
+
])
|
|
328
|
+
if clean_query in _fold_text(text):
|
|
329
|
+
matches.append({"group": group.get("id"), **item})
|
|
330
|
+
return matches
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _fold_text(value: str) -> str:
|
|
334
|
+
normalized = unicodedata.normalize("NFKD", str(value or ""))
|
|
335
|
+
asciiish = "".join(ch for ch in normalized if not unicodedata.combining(ch))
|
|
336
|
+
return asciiish.casefold()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def format_automation_preferences_prompt_block(name_or_path: str) -> str:
|
|
340
|
+
result = get_automation_preferences(name_or_path)
|
|
341
|
+
if not result.get("supports_automation_preferences"):
|
|
342
|
+
return ""
|
|
343
|
+
prefs = result.get("preferences") or {}
|
|
344
|
+
values = prefs.get("values") if isinstance(prefs, dict) else {}
|
|
345
|
+
if not isinstance(values, dict):
|
|
346
|
+
values = {}
|
|
347
|
+
compact = json.dumps(values, ensure_ascii=False, sort_keys=True)
|
|
348
|
+
return (
|
|
349
|
+
"\n== STRUCTURED CONTENT PREFERENCES FOR THIS AUTOMATION ==\n"
|
|
350
|
+
f"{compact}\n"
|
|
351
|
+
"Use these preferences to decide what to include, omit, and emphasize. "
|
|
352
|
+
"Disabled/unavailable data sources must not be invented.\n"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
__all__ = [
|
|
357
|
+
"AUTOMATION_PREFERENCES_METADATA_KEY",
|
|
358
|
+
"default_automation_preferences",
|
|
359
|
+
"format_automation_preferences_prompt_block",
|
|
360
|
+
"get_automation_preference_schema",
|
|
361
|
+
"get_automation_preferences",
|
|
362
|
+
"normalize_automation_name",
|
|
363
|
+
"search_automation_preference_schema",
|
|
364
|
+
"set_automation_preferences",
|
|
365
|
+
"supports_automation_preferences",
|
|
366
|
+
"validate_automation_preferences",
|
|
367
|
+
]
|
package/src/cli.py
CHANGED
|
@@ -1037,11 +1037,19 @@ def _automations_set_schedule(args):
|
|
|
1037
1037
|
interval_seconds = int(args.every_seconds)
|
|
1038
1038
|
elif getattr(args, "daily_at", None):
|
|
1039
1039
|
daily_at = str(args.daily_at).strip()
|
|
1040
|
+
if getattr(args, "weekdays", None) and not daily_at:
|
|
1041
|
+
result = {"ok": False, "error": "--weekdays can only be used with --daily-at"}
|
|
1042
|
+
if args.json:
|
|
1043
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1044
|
+
return 1
|
|
1045
|
+
print(result["error"], file=sys.stderr)
|
|
1046
|
+
return 1
|
|
1040
1047
|
|
|
1041
1048
|
result = set_automation_schedule(
|
|
1042
1049
|
args.name,
|
|
1043
1050
|
interval_seconds=interval_seconds,
|
|
1044
1051
|
daily_at=daily_at,
|
|
1052
|
+
weekdays=getattr(args, "weekdays", None),
|
|
1045
1053
|
clear=bool(getattr(args, "reset", False)),
|
|
1046
1054
|
)
|
|
1047
1055
|
if args.json:
|
|
@@ -1059,6 +1067,89 @@ def _automations_set_schedule(args):
|
|
|
1059
1067
|
return 0
|
|
1060
1068
|
|
|
1061
1069
|
|
|
1070
|
+
def _automations_preference_schema(args):
|
|
1071
|
+
from automation_preferences import get_automation_preference_schema, search_automation_preference_schema
|
|
1072
|
+
|
|
1073
|
+
query = str(getattr(args, "query", "") or "").strip()
|
|
1074
|
+
schema = get_automation_preference_schema(args.name)
|
|
1075
|
+
payload = {
|
|
1076
|
+
"ok": True,
|
|
1077
|
+
"name": args.name,
|
|
1078
|
+
"schema": schema,
|
|
1079
|
+
"matches": search_automation_preference_schema(args.name, query) if query else [],
|
|
1080
|
+
}
|
|
1081
|
+
if args.json:
|
|
1082
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
1083
|
+
return 0
|
|
1084
|
+
print(schema.get("title") or args.name)
|
|
1085
|
+
for group in list(schema.get("groups") or []):
|
|
1086
|
+
print(f"\n{group.get('label') or group.get('id')}")
|
|
1087
|
+
for item in list(group.get("items") or []):
|
|
1088
|
+
marker = " (unavailable)" if item.get("disabled") else ""
|
|
1089
|
+
print(f" - {item.get('label') or item.get('id')}{marker}")
|
|
1090
|
+
return 0
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _automations_preferences(args):
|
|
1094
|
+
from script_registry import get_automation_preference_contract, set_automation_preference_contract
|
|
1095
|
+
|
|
1096
|
+
has_payload = any([
|
|
1097
|
+
str(getattr(args, "payload", "") or "").strip(),
|
|
1098
|
+
str(getattr(args, "payload_file", "") or "").strip(),
|
|
1099
|
+
bool(getattr(args, "payload_stdin", False)),
|
|
1100
|
+
])
|
|
1101
|
+
if has_payload:
|
|
1102
|
+
payload, error_code = _load_json_payload_arg(args, command_name="automations preferences")
|
|
1103
|
+
if error_code is not None:
|
|
1104
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
1105
|
+
return error_code
|
|
1106
|
+
result = set_automation_preference_contract(args.name, payload)
|
|
1107
|
+
else:
|
|
1108
|
+
result = get_automation_preference_contract(args.name)
|
|
1109
|
+
if args.json:
|
|
1110
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1111
|
+
return 0 if result.get("ok", True) else 1
|
|
1112
|
+
if not result.get("ok", True):
|
|
1113
|
+
print(result.get("error", "Could not update automation preferences"), file=sys.stderr)
|
|
1114
|
+
return 1
|
|
1115
|
+
prefs = result.get("preferences") or {}
|
|
1116
|
+
values = prefs.get("values") if isinstance(prefs, dict) else {}
|
|
1117
|
+
print(f"Content preferences for {result.get('name') or args.name}:")
|
|
1118
|
+
for key, value in sorted((values or {}).items()):
|
|
1119
|
+
print(f" {key}: {value}")
|
|
1120
|
+
return 0
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _morning_briefing(args):
|
|
1124
|
+
from morning_briefing import latest_morning_briefing, mark_desktop_state
|
|
1125
|
+
|
|
1126
|
+
command = str(getattr(args, "morning_briefing_command", "") or "").strip()
|
|
1127
|
+
if command == "latest":
|
|
1128
|
+
result = latest_morning_briefing(include_non_sent=bool(getattr(args, "include_non_sent", False)))
|
|
1129
|
+
elif command in {"mark-shown", "mark-opened", "mark-dismissed"}:
|
|
1130
|
+
action = command.replace("mark-", "")
|
|
1131
|
+
result = mark_desktop_state(action, briefing_id=getattr(args, "briefing_id", None))
|
|
1132
|
+
else:
|
|
1133
|
+
result = {"ok": False, "error": "Unknown morning briefing command."}
|
|
1134
|
+
|
|
1135
|
+
if getattr(args, "json", False):
|
|
1136
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1137
|
+
return 0 if result.get("ok", True) else 1
|
|
1138
|
+
if not result.get("ok", True):
|
|
1139
|
+
print(result.get("error", "Could not read morning briefing"), file=sys.stderr)
|
|
1140
|
+
return 1
|
|
1141
|
+
briefing = result.get("briefing")
|
|
1142
|
+
if not briefing:
|
|
1143
|
+
print("No morning briefing is available yet.")
|
|
1144
|
+
return 0
|
|
1145
|
+
print(briefing.get("subject") or "Morning briefing")
|
|
1146
|
+
body = str(briefing.get("body_text") or "").strip()
|
|
1147
|
+
if body:
|
|
1148
|
+
print()
|
|
1149
|
+
print(body)
|
|
1150
|
+
return 0
|
|
1151
|
+
|
|
1152
|
+
|
|
1062
1153
|
def _agents_list(args):
|
|
1063
1154
|
from script_registry import list_agents
|
|
1064
1155
|
|
|
@@ -2529,6 +2620,28 @@ def _preferences(args):
|
|
|
2529
2620
|
_load_user_default_resonance,
|
|
2530
2621
|
)
|
|
2531
2622
|
|
|
2623
|
+
action = str(getattr(args, "preferences_action", "") or "").strip().lower()
|
|
2624
|
+
if action in {"catalog", "get", "set", "explain"}:
|
|
2625
|
+
from preference_catalog import build_preference_catalog, explain_preference, set_preference
|
|
2626
|
+
|
|
2627
|
+
if action == "catalog":
|
|
2628
|
+
payload = build_preference_catalog(
|
|
2629
|
+
include_values=bool(getattr(args, "include_values", False)),
|
|
2630
|
+
query=str(getattr(args, "query", "") or "").strip() or None,
|
|
2631
|
+
)
|
|
2632
|
+
elif action in {"get", "explain"}:
|
|
2633
|
+
pref_id = str(getattr(args, "preferences_id", "") or "").strip()
|
|
2634
|
+
payload = explain_preference(pref_id)
|
|
2635
|
+
else:
|
|
2636
|
+
pref_id = str(getattr(args, "preferences_id", "") or "").strip()
|
|
2637
|
+
value = getattr(args, "preferences_value", None)
|
|
2638
|
+
payload = set_preference(pref_id, value, dry_run=bool(getattr(args, "dry_run", False)))
|
|
2639
|
+
if getattr(args, "json", False):
|
|
2640
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
2641
|
+
else:
|
|
2642
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
2643
|
+
return 0 if payload.get("ok", True) else 1
|
|
2644
|
+
|
|
2532
2645
|
prefs = load_client_preferences()
|
|
2533
2646
|
if not isinstance(prefs, dict):
|
|
2534
2647
|
prefs = {}
|
|
@@ -4067,8 +4180,37 @@ def main():
|
|
|
4067
4180
|
automations_schedule_group.add_argument("--every-seconds", type=int, help="Run the automation every N seconds")
|
|
4068
4181
|
automations_schedule_group.add_argument("--daily-at", type=str, help="Run the automation every day at HH:MM (24h)")
|
|
4069
4182
|
automations_schedule_group.add_argument("--reset", action="store_true", help="Restore the shipped default cadence")
|
|
4183
|
+
automations_schedule_p.add_argument("--weekdays", default=None, help="Optional calendar days, e.g. Mon-Fri or Tue,Sat")
|
|
4070
4184
|
automations_schedule_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4071
4185
|
|
|
4186
|
+
automations_schema_p = automations_sub.add_parser(
|
|
4187
|
+
"preference-schema",
|
|
4188
|
+
help="Read structured content options for an automation",
|
|
4189
|
+
)
|
|
4190
|
+
automations_schema_p.add_argument("name", help="Automation name or path")
|
|
4191
|
+
automations_schema_p.add_argument("--query", default="", help="Local search inside the preference options")
|
|
4192
|
+
automations_schema_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4193
|
+
|
|
4194
|
+
automations_preferences_p = automations_sub.add_parser(
|
|
4195
|
+
"preferences",
|
|
4196
|
+
help="Read or update structured content preferences for an automation",
|
|
4197
|
+
)
|
|
4198
|
+
automations_preferences_p.add_argument("name", help="Automation name or path")
|
|
4199
|
+
automations_preferences_p.add_argument("--payload", default="", help="JSON object to persist")
|
|
4200
|
+
automations_preferences_p.add_argument("--payload-file", default="", help="Path to a JSON object to persist")
|
|
4201
|
+
automations_preferences_p.add_argument("--payload-stdin", action="store_true", help="Read JSON object from stdin")
|
|
4202
|
+
automations_preferences_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4203
|
+
|
|
4204
|
+
morning_briefing_parser = sub.add_parser("morning-briefing", help="Read the latest operator morning briefing")
|
|
4205
|
+
morning_briefing_sub = morning_briefing_parser.add_subparsers(dest="morning_briefing_command")
|
|
4206
|
+
morning_latest_p = morning_briefing_sub.add_parser("latest", help="Show the latest sent morning briefing")
|
|
4207
|
+
morning_latest_p.add_argument("--include-non-sent", action="store_true", help="Include failed/in-progress rows for diagnostics")
|
|
4208
|
+
morning_latest_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4209
|
+
for mark_command in ("mark-shown", "mark-opened", "mark-dismissed"):
|
|
4210
|
+
mark_p = morning_briefing_sub.add_parser(mark_command, help=f"{mark_command.replace('-', ' ')} for Desktop")
|
|
4211
|
+
mark_p.add_argument("--id", dest="briefing_id", type=int, default=None, help="Specific briefing id")
|
|
4212
|
+
mark_p.add_argument("--json", action="store_true", help="JSON output")
|
|
4213
|
+
|
|
4072
4214
|
core_schedules_parser = sub.add_parser("core-schedules", help="Manage structural core cron cadences")
|
|
4073
4215
|
core_schedules_sub = core_schedules_parser.add_subparsers(dest="core_schedules_command")
|
|
4074
4216
|
|
|
@@ -4170,6 +4312,12 @@ def main():
|
|
|
4170
4312
|
"preferences",
|
|
4171
4313
|
help="Read or change NEXO user preferences (resonance, default client, ...)",
|
|
4172
4314
|
)
|
|
4315
|
+
preferences_parser.add_argument("preferences_action", nargs="?", choices=["catalog", "get", "set", "explain"], help="Catalog operation")
|
|
4316
|
+
preferences_parser.add_argument("preferences_id", nargs="?", help="Preference id or alias")
|
|
4317
|
+
preferences_parser.add_argument("preferences_value", nargs="?", help="Value for `preferences set`")
|
|
4318
|
+
preferences_parser.add_argument("--query", default="", help="Filter catalog entries")
|
|
4319
|
+
preferences_parser.add_argument("--include-values", action="store_true", help="Include current values in catalog output")
|
|
4320
|
+
preferences_parser.add_argument("--dry-run", action="store_true", help="Preview a preference change")
|
|
4173
4321
|
preferences_parser.add_argument(
|
|
4174
4322
|
"--resonance",
|
|
4175
4323
|
choices=["maximo", "alto", "medio", "bajo"],
|
|
@@ -4658,8 +4806,17 @@ def main():
|
|
|
4658
4806
|
return _automations_set_instructions(args)
|
|
4659
4807
|
elif args.automations_command == "schedule":
|
|
4660
4808
|
return _automations_set_schedule(args)
|
|
4809
|
+
elif args.automations_command == "preference-schema":
|
|
4810
|
+
return _automations_preference_schema(args)
|
|
4811
|
+
elif args.automations_command == "preferences":
|
|
4812
|
+
return _automations_preferences(args)
|
|
4661
4813
|
automations_parser.print_help()
|
|
4662
4814
|
return 0
|
|
4815
|
+
elif args.command == "morning-briefing":
|
|
4816
|
+
if args.morning_briefing_command in {"latest", "mark-shown", "mark-opened", "mark-dismissed"}:
|
|
4817
|
+
return _morning_briefing(args)
|
|
4818
|
+
morning_briefing_parser.print_help()
|
|
4819
|
+
return 0
|
|
4663
4820
|
elif args.command == "core-schedules":
|
|
4664
4821
|
if args.core_schedules_command == "list":
|
|
4665
4822
|
return _core_schedules_list(args)
|