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
|
@@ -47,6 +47,7 @@ if str(_repo_src) not in sys.path:
|
|
|
47
47
|
from paths import nexo_email_dir
|
|
48
48
|
from runtime_home import export_resolved_nexo_home
|
|
49
49
|
from email_sent_events import record_sent_email
|
|
50
|
+
from email_presentation import build_email_presentation, signature_from_config, text_to_html_fragment
|
|
50
51
|
|
|
51
52
|
NEXO_HOME = export_resolved_nexo_home()
|
|
52
53
|
EMAIL_BASE_DIR = nexo_email_dir()
|
|
@@ -159,6 +160,34 @@ def _message_id_domain(config: dict) -> str:
|
|
|
159
160
|
return "localhost"
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
def _make_smtp_ssl_context() -> ssl.SSLContext:
|
|
164
|
+
"""Build a verified TLS context that survives macOS Python CA quirks."""
|
|
165
|
+
candidates: list[str] = []
|
|
166
|
+
try:
|
|
167
|
+
import certifi # type: ignore
|
|
168
|
+
|
|
169
|
+
candidates.append(certifi.where())
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
candidates.extend([
|
|
173
|
+
os.environ.get("SSL_CERT_FILE", ""),
|
|
174
|
+
"/etc/ssl/cert.pem",
|
|
175
|
+
"/usr/local/etc/openssl/cert.pem",
|
|
176
|
+
"/usr/local/etc/openssl@3/cert.pem",
|
|
177
|
+
"/opt/homebrew/etc/openssl@3/cert.pem",
|
|
178
|
+
])
|
|
179
|
+
for cafile in candidates:
|
|
180
|
+
if not cafile:
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
path = Path(cafile)
|
|
184
|
+
if path.is_file():
|
|
185
|
+
return ssl.create_default_context(cafile=str(path))
|
|
186
|
+
except Exception:
|
|
187
|
+
continue
|
|
188
|
+
return ssl.create_default_context()
|
|
189
|
+
|
|
190
|
+
|
|
162
191
|
def classify_reply_event(body_text):
|
|
163
192
|
normalized = normalize_reply_text(body_text).lower()
|
|
164
193
|
if not normalized:
|
|
@@ -402,7 +431,7 @@ def send_email(config, to, cc, subject, body_text, body_html, in_reply_to, refer
|
|
|
402
431
|
else:
|
|
403
432
|
clean_recipients.append(r.strip())
|
|
404
433
|
|
|
405
|
-
context =
|
|
434
|
+
context = _make_smtp_ssl_context()
|
|
406
435
|
server = smtplib.SMTP_SSL(config["smtp_host"], config["smtp_port"], context=context)
|
|
407
436
|
server.login(config["email"], config["password"])
|
|
408
437
|
server.sendmail(config["email"], clean_recipients, msg.as_string())
|
|
@@ -440,6 +469,8 @@ def build_parser():
|
|
|
440
469
|
parser.add_argument("--references", default="")
|
|
441
470
|
parser.add_argument("--body-file", required=True, help="Plain text body file")
|
|
442
471
|
parser.add_argument("--html-file", default="", help="HTML body file (optional)")
|
|
472
|
+
parser.add_argument("--audience", default="", help="Message audience label for continuity metadata")
|
|
473
|
+
parser.add_argument("--message-kind", default="", help="Message kind label for continuity metadata")
|
|
443
474
|
parser.add_argument("--quote-file", default="")
|
|
444
475
|
parser.add_argument("--quote-from", default="")
|
|
445
476
|
parser.add_argument("--quote-date", default="")
|
|
@@ -465,35 +496,25 @@ def main(argv=None):
|
|
|
465
496
|
if thread:
|
|
466
497
|
body_text = f"{body_text}{thread}"
|
|
467
498
|
|
|
468
|
-
# HTML body
|
|
469
|
-
|
|
499
|
+
# HTML body. Any agent-provided HTML is treated as untrusted and normalized
|
|
500
|
+
# through email_presentation before SMTP or continuity records see it.
|
|
470
501
|
html_thread = build_html_thread(args.thread_file)
|
|
471
502
|
if args.html_file and Path(args.html_file).exists():
|
|
472
503
|
html_content = Path(args.html_file).read_text(encoding="utf-8").strip()
|
|
473
|
-
|
|
474
|
-
body_html = f"""<!DOCTYPE html>
|
|
475
|
-
<html><head><meta charset="utf-8"></head>
|
|
476
|
-
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;color:#333;line-height:1.6;">
|
|
477
|
-
{html_content}
|
|
478
|
-
{html_quote}
|
|
479
|
-
{html_thread}
|
|
480
|
-
</body></html>"""
|
|
504
|
+
html_fragment = html_content
|
|
481
505
|
else:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
body_html
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
<hr style="border:none;border-top:1px solid #ddd;margin:20px 0;">
|
|
495
|
-
<p style="color:#888;font-size:11px;">{signature}</p>
|
|
496
|
-
</body></html>"""
|
|
506
|
+
html_fragment = text_to_html_fragment(reply_body)
|
|
507
|
+
html_quote = build_html_quoted(args.quote_file, args.quote_from, args.quote_date)
|
|
508
|
+
html_fragment = f"{html_fragment}{html_quote}{html_thread}"
|
|
509
|
+
presentation = build_email_presentation(
|
|
510
|
+
subject=args.subject,
|
|
511
|
+
body_text=body_text,
|
|
512
|
+
body_html=html_fragment,
|
|
513
|
+
signature=signature_from_config(config, fallback=_signature_label(config)),
|
|
514
|
+
include_signature=True,
|
|
515
|
+
)
|
|
516
|
+
body_text = presentation.body_text
|
|
517
|
+
body_html = presentation.body_html
|
|
497
518
|
|
|
498
519
|
try:
|
|
499
520
|
msg_id, raw_message = send_email(
|
|
@@ -531,6 +552,8 @@ def main(argv=None):
|
|
|
531
552
|
"sent_copy_saved": sent_copy_saved,
|
|
532
553
|
"lifecycle_event": lifecycle_event,
|
|
533
554
|
"account_label": (args.account_label or "").strip(),
|
|
555
|
+
"audience": (args.audience or "").strip(),
|
|
556
|
+
"message_kind": (args.message_kind or "").strip(),
|
|
534
557
|
},
|
|
535
558
|
)
|
|
536
559
|
except Exception as sent_event_exc:
|
package/src/server.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Return raw JSON only. No markdown fences. No commentary. No tool calls unless absolutely unavoidable.
|
|
1
|
+
Return raw JSON only. Include subject and body_text. You may include body_html with simple safe email HTML. No markdown fences. No commentary. No tool calls unless absolutely unavoidable.
|
|
@@ -11,13 +11,16 @@ Hard rules:
|
|
|
11
11
|
- Prioritise what changed recently, what is due now, what is blocked, and what deserves focus today.
|
|
12
12
|
- If activity was quiet, say so plainly instead of padding.
|
|
13
13
|
- Mention operator decisions only when the context actually supports them.
|
|
14
|
-
- Keep the email concise: roughly 180-350 words.
|
|
14
|
+
- Keep the email concise unless structured preferences ask for more detail: roughly 180-350 words.
|
|
15
15
|
- Use short sections and bullets when useful.
|
|
16
16
|
[[extra_section]]Return ONLY a valid JSON object with this exact shape:
|
|
17
17
|
{
|
|
18
18
|
"subject": "string",
|
|
19
|
-
"
|
|
19
|
+
"body_text": "plain text email body",
|
|
20
|
+
"body_html": "optional simple HTML body using p, ul, ol, li, strong, em, h2, h3, blockquote, table"
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
Compatibility rule: if you cannot produce body_html, return body_text only. Older "body" is accepted but body_text is preferred.
|
|
24
|
+
|
|
22
25
|
Structured context:
|
|
23
26
|
[[context_json]]
|
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
"version": "2.2.0",
|
|
4
4
|
"description": "Canonical map of all NEXO Brain MCP tools with enforcement rules, dependency chains, and behavioral hints. Source of truth for Protocol Enforcer (Desktop + headless). v2.1 adds schema support for Phase 2 event-driven rules (R01-R33): server_side_rules at tool level, per-rule mode (shadow|soft|hard), core_rule flag, and new rule types (pre_tool_intent, post_user_message, on_output_classify, before_tool_strict_block). Backward-compatible: executors that only handle v2.0 rule types ignore the new fields.",
|
|
5
5
|
"tools": {
|
|
6
|
+
"nexo_desktop_preferences_catalog": {
|
|
7
|
+
"description": "List Desktop/Brain preferences the agent can explain or change",
|
|
8
|
+
"category": "preferences",
|
|
9
|
+
"source": "plugin:desktop_preferences",
|
|
10
|
+
"requires": [],
|
|
11
|
+
"provides": ["preference_catalog"],
|
|
12
|
+
"internal_calls": [],
|
|
13
|
+
"enforcement": {"level": "none", "rules": []},
|
|
14
|
+
"triggers_after": []
|
|
15
|
+
},
|
|
16
|
+
"nexo_desktop_preference_get": {
|
|
17
|
+
"description": "Read one Desktop/Brain preference by id or alias",
|
|
18
|
+
"category": "preferences",
|
|
19
|
+
"source": "plugin:desktop_preferences",
|
|
20
|
+
"requires": ["preference_catalog"],
|
|
21
|
+
"provides": ["preference_value"],
|
|
22
|
+
"internal_calls": [],
|
|
23
|
+
"enforcement": {"level": "none", "rules": []},
|
|
24
|
+
"triggers_after": []
|
|
25
|
+
},
|
|
26
|
+
"nexo_desktop_preference_explain": {
|
|
27
|
+
"description": "Explain a Desktop/Brain preference in operator-friendly terms",
|
|
28
|
+
"category": "preferences",
|
|
29
|
+
"source": "plugin:desktop_preferences",
|
|
30
|
+
"requires": ["preference_catalog"],
|
|
31
|
+
"provides": ["preference_explanation"],
|
|
32
|
+
"internal_calls": [],
|
|
33
|
+
"enforcement": {"level": "none", "rules": []},
|
|
34
|
+
"triggers_after": []
|
|
35
|
+
},
|
|
36
|
+
"nexo_desktop_preference_set": {
|
|
37
|
+
"description": "Set one supported Desktop/Brain preference or preview with dry_run",
|
|
38
|
+
"category": "preferences",
|
|
39
|
+
"source": "plugin:desktop_preferences",
|
|
40
|
+
"requires": ["preference_catalog"],
|
|
41
|
+
"provides": ["preference_value"],
|
|
42
|
+
"internal_calls": [],
|
|
43
|
+
"enforcement": {"level": "standard", "rules": []},
|
|
44
|
+
"triggers_after": []
|
|
45
|
+
},
|
|
6
46
|
"nexo_adaptive_decay": {
|
|
7
47
|
"description": "Trigger inter-session tension decay",
|
|
8
48
|
"category": "adaptive",
|