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,281 @@
|
|
|
1
|
+
"""Morning briefing persistence and Desktop-facing accessors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import db as nexo_db
|
|
11
|
+
from paths import operations_dir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LATEST_MARKDOWN_FILE = operations_dir() / "morning-briefing-latest.md"
|
|
15
|
+
LATEST_HTML_FILE = operations_dir() / "morning-briefing-latest.html"
|
|
16
|
+
LATEST_JSON_FILE = operations_dir() / "morning-briefing-latest.json"
|
|
17
|
+
|
|
18
|
+
PRESENTATION_COLUMNS = {
|
|
19
|
+
"body_text": "TEXT DEFAULT ''",
|
|
20
|
+
"body_html": "TEXT DEFAULT ''",
|
|
21
|
+
"artifact_json": "TEXT DEFAULT ''",
|
|
22
|
+
"desktop_shown_at": "TEXT DEFAULT NULL",
|
|
23
|
+
"desktop_opened_at": "TEXT DEFAULT NULL",
|
|
24
|
+
"desktop_dismissed_at": "TEXT DEFAULT NULL",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now() -> str:
|
|
29
|
+
return datetime.now().astimezone().isoformat()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _conn():
|
|
33
|
+
nexo_db.init_db()
|
|
34
|
+
return nexo_db.get_db()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_morning_briefing_runs_table(conn=None) -> None:
|
|
38
|
+
conn = conn or _conn()
|
|
39
|
+
conn.execute(
|
|
40
|
+
"""CREATE TABLE IF NOT EXISTS morning_briefing_runs (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
local_date TEXT NOT NULL,
|
|
43
|
+
recipient TEXT NOT NULL,
|
|
44
|
+
status TEXT NOT NULL DEFAULT 'in_progress',
|
|
45
|
+
subject TEXT DEFAULT '',
|
|
46
|
+
body_text TEXT DEFAULT '',
|
|
47
|
+
body_html TEXT DEFAULT '',
|
|
48
|
+
artifact_json TEXT DEFAULT '',
|
|
49
|
+
send_output TEXT DEFAULT '',
|
|
50
|
+
error TEXT DEFAULT '',
|
|
51
|
+
desktop_shown_at TEXT DEFAULT NULL,
|
|
52
|
+
desktop_opened_at TEXT DEFAULT NULL,
|
|
53
|
+
desktop_dismissed_at TEXT DEFAULT NULL,
|
|
54
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
55
|
+
finished_at TEXT DEFAULT NULL,
|
|
56
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
57
|
+
UNIQUE(local_date, recipient)
|
|
58
|
+
)"""
|
|
59
|
+
)
|
|
60
|
+
existing = {
|
|
61
|
+
str(row[1])
|
|
62
|
+
for row in conn.execute("PRAGMA table_info(morning_briefing_runs)").fetchall()
|
|
63
|
+
}
|
|
64
|
+
for name, ddl in PRESENTATION_COLUMNS.items():
|
|
65
|
+
if name not in existing:
|
|
66
|
+
conn.execute(f"ALTER TABLE morning_briefing_runs ADD COLUMN {name} {ddl}")
|
|
67
|
+
conn.execute(
|
|
68
|
+
"CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_date "
|
|
69
|
+
"ON morning_briefing_runs(local_date)"
|
|
70
|
+
)
|
|
71
|
+
conn.execute(
|
|
72
|
+
"CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_status "
|
|
73
|
+
"ON morning_briefing_runs(status)"
|
|
74
|
+
)
|
|
75
|
+
conn.execute(
|
|
76
|
+
"CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_desktop "
|
|
77
|
+
"ON morning_briefing_runs(status, desktop_shown_at, finished_at)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _row_to_dict(row) -> dict[str, Any]:
|
|
82
|
+
if row is None:
|
|
83
|
+
return {}
|
|
84
|
+
try:
|
|
85
|
+
return dict(row)
|
|
86
|
+
except Exception:
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _artifact_paths() -> dict[str, str]:
|
|
91
|
+
return {
|
|
92
|
+
"markdown": str(LATEST_MARKDOWN_FILE),
|
|
93
|
+
"html": str(LATEST_HTML_FILE),
|
|
94
|
+
"json": str(LATEST_JSON_FILE),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_latest_briefing_artifacts(
|
|
99
|
+
*,
|
|
100
|
+
recipient: str,
|
|
101
|
+
subject: str,
|
|
102
|
+
body_text: str,
|
|
103
|
+
body_html: str,
|
|
104
|
+
local_date: str = "",
|
|
105
|
+
run_id: int | None = None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
generated_at = _now()
|
|
108
|
+
LATEST_MARKDOWN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
payload = {
|
|
110
|
+
"schema": "nexo.morning_briefing.v1",
|
|
111
|
+
"generated_at": generated_at,
|
|
112
|
+
"local_date": local_date,
|
|
113
|
+
"run_id": run_id,
|
|
114
|
+
"recipient": recipient,
|
|
115
|
+
"subject": subject,
|
|
116
|
+
"body_text": body_text,
|
|
117
|
+
"body_html": body_html,
|
|
118
|
+
"artifacts": _artifact_paths(),
|
|
119
|
+
}
|
|
120
|
+
markdown = (
|
|
121
|
+
"# Morning briefing\n\n"
|
|
122
|
+
f"- Generated at: {generated_at}\n"
|
|
123
|
+
f"- To: {recipient}\n"
|
|
124
|
+
f"- Subject: {subject}\n\n"
|
|
125
|
+
f"{body_text}\n"
|
|
126
|
+
)
|
|
127
|
+
LATEST_MARKDOWN_FILE.write_text(markdown, encoding="utf-8")
|
|
128
|
+
LATEST_HTML_FILE.write_text(body_html, encoding="utf-8")
|
|
129
|
+
LATEST_JSON_FILE.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
130
|
+
return payload
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def mark_morning_briefing_sent(
|
|
134
|
+
*,
|
|
135
|
+
local_date: str,
|
|
136
|
+
recipient: str,
|
|
137
|
+
subject: str,
|
|
138
|
+
body_text: str,
|
|
139
|
+
body_html: str,
|
|
140
|
+
send_output: str = "",
|
|
141
|
+
artifact_payload: dict[str, Any] | None = None,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
conn = _conn()
|
|
144
|
+
ensure_morning_briefing_runs_table(conn)
|
|
145
|
+
now = _now()
|
|
146
|
+
artifact_json = json.dumps(artifact_payload or {}, ensure_ascii=False)
|
|
147
|
+
conn.execute(
|
|
148
|
+
"""
|
|
149
|
+
UPDATE morning_briefing_runs
|
|
150
|
+
SET status = 'sent',
|
|
151
|
+
subject = ?,
|
|
152
|
+
body_text = ?,
|
|
153
|
+
body_html = ?,
|
|
154
|
+
artifact_json = ?,
|
|
155
|
+
send_output = ?,
|
|
156
|
+
error = '',
|
|
157
|
+
finished_at = ?,
|
|
158
|
+
updated_at = ?
|
|
159
|
+
WHERE local_date = ? AND recipient = ?
|
|
160
|
+
""",
|
|
161
|
+
(
|
|
162
|
+
str(subject or ""),
|
|
163
|
+
str(body_text or ""),
|
|
164
|
+
str(body_html or ""),
|
|
165
|
+
artifact_json,
|
|
166
|
+
str(send_output or ""),
|
|
167
|
+
now,
|
|
168
|
+
now,
|
|
169
|
+
str(local_date or ""),
|
|
170
|
+
str(recipient or ""),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
conn.commit()
|
|
174
|
+
return latest_morning_briefing()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def latest_morning_briefing(*, include_non_sent: bool = False) -> dict[str, Any]:
|
|
178
|
+
conn = _conn()
|
|
179
|
+
ensure_morning_briefing_runs_table(conn)
|
|
180
|
+
if include_non_sent:
|
|
181
|
+
row = conn.execute(
|
|
182
|
+
"""
|
|
183
|
+
SELECT * FROM morning_briefing_runs
|
|
184
|
+
ORDER BY COALESCE(finished_at, updated_at, started_at) DESC, id DESC
|
|
185
|
+
LIMIT 1
|
|
186
|
+
"""
|
|
187
|
+
).fetchone()
|
|
188
|
+
else:
|
|
189
|
+
row = conn.execute(
|
|
190
|
+
"""
|
|
191
|
+
SELECT * FROM morning_briefing_runs
|
|
192
|
+
WHERE status = 'sent'
|
|
193
|
+
ORDER BY COALESCE(finished_at, updated_at, started_at) DESC, id DESC
|
|
194
|
+
LIMIT 1
|
|
195
|
+
"""
|
|
196
|
+
).fetchone()
|
|
197
|
+
payload = public_briefing_payload(_row_to_dict(row))
|
|
198
|
+
return {"ok": True, "briefing": payload}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def public_briefing_payload(row: dict[str, Any]) -> dict[str, Any] | None:
|
|
202
|
+
if not row:
|
|
203
|
+
return None
|
|
204
|
+
artifact_payload: dict[str, Any] = {}
|
|
205
|
+
try:
|
|
206
|
+
parsed = json.loads(row.get("artifact_json") or "{}")
|
|
207
|
+
if isinstance(parsed, dict):
|
|
208
|
+
artifact_payload = parsed
|
|
209
|
+
except Exception:
|
|
210
|
+
artifact_payload = {}
|
|
211
|
+
return {
|
|
212
|
+
"id": row.get("id"),
|
|
213
|
+
"local_date": row.get("local_date") or "",
|
|
214
|
+
"recipient": row.get("recipient") or "",
|
|
215
|
+
"status": row.get("status") or "",
|
|
216
|
+
"subject": row.get("subject") or "",
|
|
217
|
+
"body_text": row.get("body_text") or "",
|
|
218
|
+
"body_html": row.get("body_html") or "",
|
|
219
|
+
"send_output": row.get("send_output") or "",
|
|
220
|
+
"error": row.get("error") or "",
|
|
221
|
+
"started_at": row.get("started_at") or "",
|
|
222
|
+
"finished_at": row.get("finished_at") or "",
|
|
223
|
+
"updated_at": row.get("updated_at") or "",
|
|
224
|
+
"desktop_shown_at": row.get("desktop_shown_at") or "",
|
|
225
|
+
"desktop_opened_at": row.get("desktop_opened_at") or "",
|
|
226
|
+
"desktop_dismissed_at": row.get("desktop_dismissed_at") or "",
|
|
227
|
+
"unseen": not bool(row.get("desktop_shown_at")),
|
|
228
|
+
"artifacts": artifact_payload.get("artifacts") or _artifact_paths(),
|
|
229
|
+
"schema": "nexo.morning_briefing.v1",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def mark_desktop_state(action: str, *, briefing_id: int | None = None) -> dict[str, Any]:
|
|
234
|
+
field_by_action = {
|
|
235
|
+
"shown": "desktop_shown_at",
|
|
236
|
+
"opened": "desktop_opened_at",
|
|
237
|
+
"dismissed": "desktop_dismissed_at",
|
|
238
|
+
}
|
|
239
|
+
field = field_by_action.get(str(action or "").strip().lower())
|
|
240
|
+
if not field:
|
|
241
|
+
return {"ok": False, "error": f"Unknown briefing mark action: {action}"}
|
|
242
|
+
conn = _conn()
|
|
243
|
+
ensure_morning_briefing_runs_table(conn)
|
|
244
|
+
if briefing_id:
|
|
245
|
+
row = _row_to_dict(conn.execute(
|
|
246
|
+
"SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
|
|
247
|
+
(int(briefing_id),),
|
|
248
|
+
).fetchone())
|
|
249
|
+
else:
|
|
250
|
+
row = (latest_morning_briefing().get("briefing") or {})
|
|
251
|
+
if row:
|
|
252
|
+
row = _row_to_dict(conn.execute(
|
|
253
|
+
"SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
|
|
254
|
+
(int(row.get("id") or 0),),
|
|
255
|
+
).fetchone())
|
|
256
|
+
if not row:
|
|
257
|
+
return {"ok": False, "error": "No morning briefing found."}
|
|
258
|
+
now = _now()
|
|
259
|
+
conn.execute(
|
|
260
|
+
f"UPDATE morning_briefing_runs SET {field} = ?, updated_at = ? WHERE id = ?",
|
|
261
|
+
(now, now, int(row.get("id"))),
|
|
262
|
+
)
|
|
263
|
+
conn.commit()
|
|
264
|
+
updated = _row_to_dict(conn.execute(
|
|
265
|
+
"SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
|
|
266
|
+
(int(row.get("id")),),
|
|
267
|
+
).fetchone())
|
|
268
|
+
return {"ok": True, "briefing": public_briefing_payload(updated), "marked": action}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
__all__ = [
|
|
272
|
+
"LATEST_HTML_FILE",
|
|
273
|
+
"LATEST_JSON_FILE",
|
|
274
|
+
"LATEST_MARKDOWN_FILE",
|
|
275
|
+
"ensure_morning_briefing_runs_table",
|
|
276
|
+
"latest_morning_briefing",
|
|
277
|
+
"mark_desktop_state",
|
|
278
|
+
"mark_morning_briefing_sent",
|
|
279
|
+
"public_briefing_payload",
|
|
280
|
+
"write_latest_briefing_artifacts",
|
|
281
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Agent-facing catalog and mutation tools for NEXO Desktop preferences."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_desktop_preferences_catalog(query: str = "", include_values: bool = True, locale: str = "es") -> str:
|
|
9
|
+
from preference_catalog import build_preference_catalog
|
|
10
|
+
|
|
11
|
+
return json.dumps(
|
|
12
|
+
build_preference_catalog(
|
|
13
|
+
include_values=bool(include_values),
|
|
14
|
+
query=str(query or "").strip() or None,
|
|
15
|
+
locale=str(locale or "es"),
|
|
16
|
+
),
|
|
17
|
+
ensure_ascii=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_desktop_preference_get(id: str) -> str:
|
|
22
|
+
from preference_catalog import explain_preference
|
|
23
|
+
|
|
24
|
+
return json.dumps(explain_preference(id), ensure_ascii=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_desktop_preference_explain(id: str) -> str:
|
|
28
|
+
from preference_catalog import explain_preference
|
|
29
|
+
|
|
30
|
+
return json.dumps(explain_preference(id), ensure_ascii=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_desktop_preference_set(id: str, value: str, dry_run: bool = False) -> str:
|
|
34
|
+
from preference_catalog import set_preference
|
|
35
|
+
|
|
36
|
+
return json.dumps(
|
|
37
|
+
set_preference(id, value, dry_run=bool(dry_run)),
|
|
38
|
+
ensure_ascii=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
TOOLS = [
|
|
43
|
+
(
|
|
44
|
+
handle_desktop_preferences_catalog,
|
|
45
|
+
"nexo_desktop_preferences_catalog",
|
|
46
|
+
"List the settings and automation preferences NEXO can explain or change for the operator.",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
handle_desktop_preference_get,
|
|
50
|
+
"nexo_desktop_preference_get",
|
|
51
|
+
"Read one preference by id or alias, including its current value when available.",
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
handle_desktop_preference_explain,
|
|
55
|
+
"nexo_desktop_preference_explain",
|
|
56
|
+
"Explain what one Desktop/Brain preference means and where it is stored.",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
handle_desktop_preference_set,
|
|
60
|
+
"nexo_desktop_preference_set",
|
|
61
|
+
"Change one supported preference by id or alias. Use dry_run=true to preview.",
|
|
62
|
+
),
|
|
63
|
+
]
|
|
@@ -253,6 +253,7 @@ def handle_automation_schedule(
|
|
|
253
253
|
name: str,
|
|
254
254
|
every_seconds: int = 0,
|
|
255
255
|
daily_at: str = "",
|
|
256
|
+
weekdays: str = "",
|
|
256
257
|
clear: bool = False,
|
|
257
258
|
) -> str:
|
|
258
259
|
init_db()
|
|
@@ -262,6 +263,7 @@ def handle_automation_schedule(
|
|
|
262
263
|
name,
|
|
263
264
|
interval_seconds=interval_seconds,
|
|
264
265
|
daily_at=str(daily_at or "").strip() or None,
|
|
266
|
+
weekdays=str(weekdays or "").strip() or None,
|
|
265
267
|
clear=bool(clear),
|
|
266
268
|
),
|
|
267
269
|
ensure_ascii=False,
|
package/src/plugins/update.py
CHANGED
|
@@ -1829,9 +1829,13 @@ def handle_update(
|
|
|
1829
1829
|
from client_sync import sync_all_clients
|
|
1830
1830
|
from client_preferences import normalize_client_preferences
|
|
1831
1831
|
from model_defaults import heal_runtime_profiles
|
|
1832
|
+
from auto_update import _refresh_resonance_tiers_model_defaults
|
|
1832
1833
|
|
|
1833
1834
|
schedule_path = paths.config_dir() / "schedule.json"
|
|
1834
1835
|
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
1836
|
+
for action in _refresh_resonance_tiers_model_defaults(NEXO_HOME):
|
|
1837
|
+
_emit_progress(progress_fn, action)
|
|
1838
|
+
steps_done.append("resonance-default-refresh")
|
|
1835
1839
|
# Heal Claude-family models written into Codex profile by earlier
|
|
1836
1840
|
# buggy versions. Must run BEFORE normalize so healed values
|
|
1837
1841
|
# propagate into the saved preferences.
|