nexo-brain 7.1.0 → 7.1.2
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 -2
- package/bin/nexo-brain.js +198 -92
- package/package.json +1 -1
- package/src/agent_runner.py +10 -8
- package/src/auto_close_sessions.py +19 -2
- package/src/auto_update.py +305 -42
- package/src/autonomy_mandate.py +260 -0
- package/src/bootstrap_docs.py +22 -1
- package/src/cli.py +181 -1
- package/src/cli_email.py +104 -73
- package/src/client_sync.py +22 -1
- package/src/cognitive/_core.py +5 -3
- package/src/core_prompts.py +50 -0
- package/src/cron_recovery.py +81 -7
- package/src/crons/manifest.json +57 -0
- package/src/crons/sync.py +95 -26
- package/src/dashboard/app.py +59 -0
- package/src/dashboard/templates/base.html +2 -0
- package/src/dashboard/templates/feature-disabled.html +27 -0
- package/src/db/_email_accounts.py +67 -18
- package/src/db/_fts.py +5 -5
- package/src/db/_personal_scripts.py +1 -1
- package/src/db/_skills.py +3 -3
- package/src/doctor/providers/runtime.py +35 -20
- package/src/email_config.py +18 -9
- package/src/enforcement_classifier.py +3 -12
- package/src/evolution_cycle.py +37 -149
- package/src/guardian_telemetry.py +3 -2
- package/src/hook_guardrails.py +61 -0
- package/src/hooks/capture-tool-logs.sh +11 -3
- package/src/hooks/daily-briefing-check.sh +7 -2
- package/src/hooks/heartbeat-enforcement.py +14 -1
- package/src/hooks/heartbeat-posttool.sh +2 -0
- package/src/hooks/heartbeat-user-msg.sh +2 -0
- package/src/hooks/inbox-hook.sh +6 -2
- package/src/hooks/post-compact.sh +12 -4
- package/src/hooks/pre-compact.sh +12 -4
- package/src/migrate_embeddings.py +5 -3
- package/src/nexo_migrate.py +3 -1
- package/src/plugin_loader.py +14 -5
- package/src/plugins/adaptive_mode.py +4 -1
- package/src/plugins/backup.py +32 -20
- package/src/plugins/evolution.py +2 -0
- package/src/plugins/memory_export.py +6 -1
- package/src/plugins/personal_plugins.py +17 -7
- package/src/plugins/personal_scripts.py +64 -3
- package/src/presets/entities_universal.json +67 -4
- package/src/product_mode.py +201 -0
- package/src/r14_correction_learning.py +5 -20
- package/src/r15_project_context.py +4 -10
- package/src/r16_declared_done.py +3 -16
- package/src/r17_promise_debt.py +3 -16
- package/src/r18_followup_autocomplete.py +5 -7
- package/src/r19_project_grep.py +5 -8
- package/src/r20_constant_change.py +5 -15
- package/src/r21_legacy_path.py +5 -7
- package/src/r22_personal_script.py +4 -8
- package/src/r23_ssh_without_atlas.py +4 -11
- package/src/r23b_deploy_vhost.py +7 -6
- package/src/r23c_cwd_mismatch.py +7 -6
- package/src/r23d_chown_chmod_recursive.py +6 -6
- package/src/r23e_force_push_main.py +5 -6
- package/src/r23f_db_no_where.py +5 -6
- package/src/r23g_secrets_in_output.py +5 -5
- package/src/r23h_shebang_mismatch.py +6 -5
- package/src/r23i_auto_deploy_ignored.py +5 -6
- package/src/r23j_global_install.py +5 -6
- package/src/r23k_script_duplicates_skill.py +7 -6
- package/src/r23l_resource_collision.py +7 -6
- package/src/r23m_message_duplicate.py +6 -5
- package/src/r24_stale_memory.py +4 -9
- package/src/r25_nora_maria_read_only.py +5 -10
- package/src/r34_identity_coherence.py +6 -13
- package/src/r_catalog.py +3 -7
- package/src/resonance_map.py +13 -13
- package/src/runtime_power.py +29 -80
- package/src/script_registry.py +236 -6
- package/src/scripts/check-context.py +8 -25
- package/src/scripts/deep-sleep/extract.py +6 -10
- package/src/scripts/nexo-auto-update.py +27 -4
- package/src/scripts/nexo-catchup.py +9 -19
- package/src/scripts/nexo-cognitive-decay.py +26 -3
- package/src/scripts/nexo-daily-self-audit.py +50 -51
- package/src/scripts/nexo-email-migrate-config.py +30 -11
- package/src/scripts/nexo-email-monitor.py +97 -238
- package/src/scripts/nexo-followup-runner.py +70 -133
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-immune.py +6 -31
- package/src/scripts/nexo-impact-scorer.py +27 -4
- package/src/scripts/nexo-learning-housekeep.py +26 -3
- package/src/scripts/nexo-learning-validator.py +34 -32
- package/src/scripts/nexo-migrate.py +28 -12
- package/src/scripts/nexo-morning-agent.py +9 -23
- package/src/scripts/nexo-outcome-checker.py +27 -4
- package/src/scripts/nexo-postmortem-consolidator.py +30 -62
- package/src/scripts/nexo-pre-commit.py +28 -0
- package/src/scripts/nexo-proactive-dashboard.py +27 -0
- package/src/scripts/nexo-reflection.py +33 -3
- package/src/scripts/nexo-runtime-preflight.py +27 -2
- package/src/scripts/nexo-send-reply.py +10 -8
- package/src/scripts/nexo-sleep.py +11 -25
- package/src/scripts/nexo-synthesis.py +7 -40
- package/src/scripts/nexo-watchdog-smoke.py +30 -1
- package/src/scripts/nexo-watchdog.sh +23 -17
- package/src/scripts/phase_guardian_analysis.py +27 -4
- package/src/server.py +14 -3
- package/src/storage_router.py +8 -6
- package/src/tools_drive.py +5 -13
- package/src/tools_guardian.py +3 -4
- package/src/tools_menu.py +2 -2
- package/src/tools_reminders_crud.py +17 -0
- package/src/tools_sessions.py +1 -4
- package/src/user_context.py +3 -6
- package/src/user_data_portability.py +31 -23
- package/templates/CLAUDE.md.template +11 -3
- package/templates/CODEX.AGENTS.md.template +11 -3
- package/templates/core-prompts/catchup-assessment.md +19 -0
- package/templates/core-prompts/check-context.md +24 -0
- package/templates/core-prompts/daily-self-audit.md +42 -0
- package/templates/core-prompts/daily-synthesis.md +40 -0
- package/templates/core-prompts/deep-sleep-extract-json-output.md +8 -0
- package/templates/core-prompts/drive-signal-classifier-system.md +4 -0
- package/templates/core-prompts/drive-signal-classifier-user.md +6 -0
- package/templates/core-prompts/email-monitor.md +202 -0
- package/templates/core-prompts/enforcement-classifier-retry.md +1 -0
- package/templates/core-prompts/enforcement-classifier-strict.md +1 -0
- package/templates/core-prompts/evolution-public-contribution.md +32 -0
- package/templates/core-prompts/evolution-public-pr-review.md +38 -0
- package/templates/core-prompts/evolution-weekly.md +71 -0
- package/templates/core-prompts/followup-runner-operator-attention-context.md +4 -0
- package/templates/core-prompts/followup-runner-operator-attention-question.md +1 -0
- package/templates/core-prompts/followup-runner.md +74 -0
- package/templates/core-prompts/immune-triage.md +31 -0
- package/templates/core-prompts/interactive-startup.md +1 -0
- package/templates/core-prompts/json-object-only.md +1 -0
- package/templates/core-prompts/learning-validator.md +25 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -0
- package/templates/core-prompts/morning-agent.md +23 -0
- package/templates/core-prompts/postmortem-consolidator.md +60 -0
- package/templates/core-prompts/r-catalog.md +1 -0
- package/templates/core-prompts/r14-correction-learning-injection.md +1 -0
- package/templates/core-prompts/r14-correction-learning-question.md +1 -0
- package/templates/core-prompts/r15-project-context-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-question.md +1 -0
- package/templates/core-prompts/r17-promise-debt-injection.md +1 -0
- package/templates/core-prompts/r17-promise-debt-question.md +1 -0
- package/templates/core-prompts/r18-followup-autocomplete-injection.md +3 -0
- package/templates/core-prompts/r19-project-grep-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-question.md +1 -0
- package/templates/core-prompts/r21-legacy-path-injection.md +1 -0
- package/templates/core-prompts/r22-personal-script-injection.md +1 -0
- package/templates/core-prompts/r23-ssh-without-atlas-injection.md +1 -0
- package/templates/core-prompts/r23b-deploy-vhost-injection.md +1 -0
- package/templates/core-prompts/r23c-cwd-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23d-chown-chmod-recursive-injection.md +1 -0
- package/templates/core-prompts/r23e-force-push-main-injection.md +1 -0
- package/templates/core-prompts/r23f-db-no-where-injection.md +1 -0
- package/templates/core-prompts/r23g-secrets-in-output-injection.md +1 -0
- package/templates/core-prompts/r23h-shebang-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23i-auto-deploy-ignored-injection.md +1 -0
- package/templates/core-prompts/r23j-global-install-injection.md +1 -0
- package/templates/core-prompts/r23k-script-duplicates-skill-injection.md +1 -0
- package/templates/core-prompts/r23l-resource-collision-injection.md +1 -0
- package/templates/core-prompts/r23m-message-duplicate-injection.md +1 -0
- package/templates/core-prompts/r24-stale-memory-injection.md +1 -0
- package/templates/core-prompts/r25-read-only-host-injection.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-probe.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-question.md +1 -0
- package/templates/core-prompts/sleep.md +25 -0
- package/templates/email-template.md +55 -0
- package/templates/nexo_helper.py +31 -13
- package/templates/plugin-template.py +3 -3
- package/templates/skill-template.md +2 -1
package/src/cli_email.py
CHANGED
|
@@ -41,7 +41,7 @@ def _prompt_int(msg: str, default: int) -> int:
|
|
|
41
41
|
try:
|
|
42
42
|
return int(raw)
|
|
43
43
|
except ValueError:
|
|
44
|
-
print(f" ✗ '{raw}'
|
|
44
|
+
print(f" ✗ '{raw}' is not a valid number. Try again.")
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def _prompt_yes_no(msg: str, default: bool = True) -> bool:
|
|
@@ -54,12 +54,12 @@ def _prompt_yes_no(msg: str, default: bool = True) -> bool:
|
|
|
54
54
|
return True
|
|
55
55
|
if raw in ("n", "no"):
|
|
56
56
|
return False
|
|
57
|
-
print(" ✗
|
|
57
|
+
print(" ✗ Answer with y or n.")
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _mask_password(pw: str) -> str:
|
|
61
61
|
if not pw:
|
|
62
|
-
return "(
|
|
62
|
+
return "(empty)"
|
|
63
63
|
if len(pw) <= 4:
|
|
64
64
|
return "•" * len(pw)
|
|
65
65
|
return pw[0] + "•" * (len(pw) - 2) + pw[-1]
|
|
@@ -103,61 +103,61 @@ def _delete_credential(service: str, key: str) -> None:
|
|
|
103
103
|
def cmd_email_setup(args) -> int:
|
|
104
104
|
"""Interactive wizard for the primary agent mailbox."""
|
|
105
105
|
print("━" * 60)
|
|
106
|
-
print("NEXO ·
|
|
106
|
+
print("NEXO · Email setup wizard")
|
|
107
107
|
print("━" * 60)
|
|
108
|
-
print("
|
|
109
|
-
print("
|
|
110
|
-
print("
|
|
108
|
+
print("I will ask for the mailbox details NEXO should use to")
|
|
109
|
+
print("read and reply. If you make a mistake, just run")
|
|
110
|
+
print("`nexo email setup` again at any time.\n")
|
|
111
111
|
|
|
112
112
|
from db import init_db
|
|
113
113
|
from db._email_accounts import add_email_account, get_email_account
|
|
114
114
|
|
|
115
115
|
init_db()
|
|
116
116
|
|
|
117
|
-
label = _prompt("
|
|
117
|
+
label = _prompt("Account label (example: 'primary', 'wazion')", "primary")
|
|
118
118
|
|
|
119
119
|
existing = get_email_account(label)
|
|
120
120
|
if existing:
|
|
121
121
|
if not _prompt_yes_no(
|
|
122
|
-
f"
|
|
122
|
+
f"An account named '{label}' already exists ({existing.get('email')}). Overwrite it?",
|
|
123
123
|
default=False,
|
|
124
124
|
):
|
|
125
|
-
print("
|
|
125
|
+
print("Cancelled.")
|
|
126
126
|
return 1
|
|
127
127
|
|
|
128
|
-
email = _prompt("
|
|
128
|
+
email = _prompt("Email address (example: nexo@yourdomain.com)")
|
|
129
129
|
if not email or "@" not in email:
|
|
130
|
-
print(f" ✗ '{email}'
|
|
130
|
+
print(f" ✗ '{email}' does not look like a valid email address.")
|
|
131
131
|
return 1
|
|
132
132
|
|
|
133
|
-
imap_host = _prompt("
|
|
134
|
-
imap_port = _prompt_int("
|
|
135
|
-
smtp_host = _prompt("
|
|
136
|
-
smtp_port = _prompt_int("
|
|
133
|
+
imap_host = _prompt("IMAP server (incoming mail)", "imap.gmail.com")
|
|
134
|
+
imap_port = _prompt_int("IMAP port", 993)
|
|
135
|
+
smtp_host = _prompt("SMTP server (outgoing mail)", imap_host.replace("imap", "smtp"))
|
|
136
|
+
smtp_port = _prompt_int("SMTP port", 465)
|
|
137
137
|
|
|
138
138
|
try:
|
|
139
|
-
pwd = getpass.getpass("
|
|
139
|
+
pwd = getpass.getpass("Password (hidden input): ")
|
|
140
140
|
except (EOFError, KeyboardInterrupt):
|
|
141
|
-
print("\n(
|
|
141
|
+
print("\n(cancelled)")
|
|
142
142
|
return 1
|
|
143
143
|
if not pwd:
|
|
144
|
-
print(" ✗
|
|
144
|
+
print(" ✗ A password is required.")
|
|
145
145
|
return 1
|
|
146
146
|
|
|
147
147
|
operator_email = _prompt(
|
|
148
|
-
"
|
|
148
|
+
"Operator email for the daily briefing",
|
|
149
149
|
email,
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
trusted_raw = _prompt(
|
|
153
|
-
"
|
|
153
|
+
"Trusted domains (comma-separated, optional)",
|
|
154
154
|
"",
|
|
155
155
|
)
|
|
156
156
|
trusted = [d.strip() for d in trusted_raw.split(",") if d.strip()]
|
|
157
|
-
sent_folder = _prompt("
|
|
157
|
+
sent_folder = _prompt("IMAP sent folder", "INBOX.Sent").strip() or "INBOX.Sent"
|
|
158
158
|
|
|
159
159
|
role = _prompt(
|
|
160
|
-
"
|
|
160
|
+
"Account role: inbox (read only) / outbox (send only) / both",
|
|
161
161
|
"both",
|
|
162
162
|
)
|
|
163
163
|
if role not in ("inbox", "outbox", "both"):
|
|
@@ -188,20 +188,20 @@ def cmd_email_setup(args) -> int:
|
|
|
188
188
|
)
|
|
189
189
|
|
|
190
190
|
print()
|
|
191
|
-
print("✓
|
|
191
|
+
print("✓ Account saved:")
|
|
192
192
|
print(f" label: {account.get('label')}")
|
|
193
193
|
print(f" email: {account.get('email')}")
|
|
194
194
|
print(f" IMAP: {account.get('imap_host')}:{account.get('imap_port')}")
|
|
195
195
|
print(f" SMTP: {account.get('smtp_host')}:{account.get('smtp_port')}")
|
|
196
196
|
print(f" operator_email: {account.get('operator_email')}")
|
|
197
|
-
print(f" trusted: {account.get('trusted_domains') or '(
|
|
197
|
+
print(f" trusted: {account.get('trusted_domains') or '(none)'}")
|
|
198
198
|
print(f" role: {account.get('role')}")
|
|
199
199
|
print(f" sent_folder: {_sent_folder_from_account(account)}")
|
|
200
|
-
print(f" password: {_mask_password(pwd)} (
|
|
200
|
+
print(f" password: {_mask_password(pwd)} (stored in credentials)")
|
|
201
201
|
print()
|
|
202
|
-
if _prompt_yes_no("
|
|
202
|
+
if _prompt_yes_no("Test the connection now?", default=True):
|
|
203
203
|
return cmd_email_test(type("Args", (), {"label": label})())
|
|
204
|
-
print("
|
|
204
|
+
print("You can test it later with: nexo email test " + label)
|
|
205
205
|
return 0
|
|
206
206
|
|
|
207
207
|
|
|
@@ -218,11 +218,18 @@ def _account_to_public_dict(account: dict) -> dict:
|
|
|
218
218
|
stored so the UI can show a 'no password yet' marker."""
|
|
219
219
|
if not account:
|
|
220
220
|
return {}
|
|
221
|
+
metadata = account.get("metadata") or {}
|
|
222
|
+
if not isinstance(metadata, dict):
|
|
223
|
+
metadata = {}
|
|
224
|
+
legacy_migrated = bool(metadata.get("migrated_from_legacy_email_config"))
|
|
221
225
|
return {
|
|
226
|
+
"id": account.get("id"),
|
|
222
227
|
"label": account.get("label"),
|
|
223
228
|
"email": account.get("email"),
|
|
224
229
|
"account_type": account.get("account_type", "agent"),
|
|
225
230
|
"description": account.get("description", ""),
|
|
231
|
+
"description_source": "legacy_migration" if legacy_migrated else "",
|
|
232
|
+
"legacy_migrated": legacy_migrated,
|
|
226
233
|
"imap_host": account.get("imap_host"),
|
|
227
234
|
"imap_port": account.get("imap_port"),
|
|
228
235
|
"smtp_host": account.get("smtp_host"),
|
|
@@ -240,6 +247,22 @@ def _account_to_public_dict(account: dict) -> dict:
|
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
|
|
250
|
+
def _selector_from_args(args) -> tuple[int | None, str]:
|
|
251
|
+
raw_id = getattr(args, "account_id", None)
|
|
252
|
+
label = str(getattr(args, "label", None) or getattr(args, "label_pos", None) or "").strip()
|
|
253
|
+
try:
|
|
254
|
+
account_id = int(raw_id) if raw_id not in (None, "") else None
|
|
255
|
+
except Exception:
|
|
256
|
+
account_id = None
|
|
257
|
+
if account_id is not None and account_id <= 0:
|
|
258
|
+
account_id = None
|
|
259
|
+
return account_id, label
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _selector_usage(command: str) -> str:
|
|
263
|
+
return f"usage: nexo email {command} <label> [--id ACCOUNT_ID]"
|
|
264
|
+
|
|
265
|
+
|
|
243
266
|
def cmd_email_list(args) -> int:
|
|
244
267
|
from db import init_db
|
|
245
268
|
from db._email_accounts import list_email_accounts
|
|
@@ -253,7 +276,7 @@ def cmd_email_list(args) -> int:
|
|
|
253
276
|
})
|
|
254
277
|
return 0
|
|
255
278
|
if not accounts:
|
|
256
|
-
print("(
|
|
279
|
+
print("(no accounts configured — run `nexo email setup`)")
|
|
257
280
|
return 0
|
|
258
281
|
print(f"{'LABEL':<18} {'TYPE':<9} {'EMAIL':<34} {'PERMS':<7} {'DEF':<4} IMAP")
|
|
259
282
|
for a in accounts:
|
|
@@ -315,7 +338,7 @@ def cmd_email_add(args) -> int:
|
|
|
315
338
|
print(f"✗ {msg}")
|
|
316
339
|
return 1
|
|
317
340
|
if "@" not in email:
|
|
318
|
-
msg = f"'{email}'
|
|
341
|
+
msg = f"'{email}' does not look like a valid email address."
|
|
319
342
|
if json_mode:
|
|
320
343
|
_emit_json({"ok": False, "message": msg})
|
|
321
344
|
else:
|
|
@@ -443,16 +466,15 @@ def cmd_email_add(args) -> int:
|
|
|
443
466
|
if json_mode:
|
|
444
467
|
_emit_json({"ok": True, "account": public})
|
|
445
468
|
else:
|
|
446
|
-
print(f"✓
|
|
469
|
+
print(f"✓ Account '{label}' saved.")
|
|
447
470
|
return 0
|
|
448
471
|
|
|
449
472
|
|
|
450
473
|
def cmd_email_test(args) -> int:
|
|
451
474
|
json_mode = getattr(args, "json", False)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
msg = "usage: nexo email test <label>"
|
|
475
|
+
account_id, label = _selector_from_args(args)
|
|
476
|
+
if account_id is None and not label:
|
|
477
|
+
msg = _selector_usage("test")
|
|
456
478
|
if json_mode:
|
|
457
479
|
_emit_json({"ok": False, "message": msg})
|
|
458
480
|
else:
|
|
@@ -462,9 +484,10 @@ def cmd_email_test(args) -> int:
|
|
|
462
484
|
from email_config import load_email_config
|
|
463
485
|
|
|
464
486
|
init_db()
|
|
465
|
-
cfg = load_email_config(label=label)
|
|
487
|
+
cfg = load_email_config(label=label or None, account_id=account_id)
|
|
466
488
|
if cfg is None:
|
|
467
|
-
|
|
489
|
+
selector = f"id={account_id}" if account_id is not None else label
|
|
490
|
+
msg = f"Account '{selector}' not found."
|
|
468
491
|
if json_mode:
|
|
469
492
|
_emit_json({"ok": False, "message": msg})
|
|
470
493
|
else:
|
|
@@ -496,7 +519,8 @@ def cmd_email_test(args) -> int:
|
|
|
496
519
|
if json_mode:
|
|
497
520
|
_emit_json({
|
|
498
521
|
"ok": ok_imap and ok_smtp,
|
|
499
|
-
"
|
|
522
|
+
"id": cfg.get("id"),
|
|
523
|
+
"label": cfg.get("label") or label,
|
|
500
524
|
"imap": {"ok": ok_imap, "host": cfg["imap_host"], "port": cfg["imap_port"], "error": err_imap},
|
|
501
525
|
"smtp": {"ok": ok_smtp, "host": cfg["smtp_host"], "port": cfg["smtp_port"], "error": err_smtp},
|
|
502
526
|
"message": "Login OK" if (ok_imap and ok_smtp) else (err_imap or err_smtp or "test failed"),
|
|
@@ -515,21 +539,22 @@ def cmd_email_test(args) -> int:
|
|
|
515
539
|
|
|
516
540
|
def cmd_email_remove(args) -> int:
|
|
517
541
|
json_mode = getattr(args, "json", False)
|
|
518
|
-
|
|
519
|
-
if not label:
|
|
520
|
-
msg = "
|
|
542
|
+
account_id, label = _selector_from_args(args)
|
|
543
|
+
if account_id is None and not label:
|
|
544
|
+
msg = _selector_usage("remove")
|
|
521
545
|
if json_mode:
|
|
522
546
|
_emit_json({"ok": False, "message": msg})
|
|
523
547
|
else:
|
|
524
548
|
print(msg)
|
|
525
549
|
return 1
|
|
526
550
|
from db import init_db
|
|
527
|
-
from db._email_accounts import get_email_account, remove_email_account
|
|
551
|
+
from db._email_accounts import get_email_account, get_email_account_by_id, remove_email_account
|
|
528
552
|
|
|
529
553
|
init_db()
|
|
530
|
-
acc = get_email_account(label)
|
|
554
|
+
acc = get_email_account_by_id(account_id) if account_id is not None else get_email_account(label)
|
|
531
555
|
if not acc:
|
|
532
|
-
|
|
556
|
+
selector = f"id={account_id}" if account_id is not None else label
|
|
557
|
+
msg = f"Account '{selector}' not found."
|
|
533
558
|
if json_mode:
|
|
534
559
|
_emit_json({"ok": False, "message": msg})
|
|
535
560
|
else:
|
|
@@ -539,53 +564,55 @@ def cmd_email_remove(args) -> int:
|
|
|
539
564
|
if json_mode:
|
|
540
565
|
_emit_json({"ok": False, "message": "missing --yes (interactive confirmation required)"})
|
|
541
566
|
return 1
|
|
542
|
-
if not _prompt_yes_no(f"
|
|
543
|
-
print("
|
|
567
|
+
if not _prompt_yes_no(f"Delete account '{label}' ({acc.get('email')})?", default=False):
|
|
568
|
+
print("Cancelled.")
|
|
544
569
|
return 0
|
|
545
570
|
_delete_credential(acc.get("credential_service", ""), acc.get("credential_key", ""))
|
|
546
|
-
remove_email_account(
|
|
571
|
+
remove_email_account(account_id=acc.get("id"))
|
|
547
572
|
if json_mode:
|
|
548
|
-
_emit_json({"ok": True, "label": label, "message": "removed"})
|
|
573
|
+
_emit_json({"ok": True, "id": acc.get("id"), "label": acc.get("label"), "message": "removed"})
|
|
549
574
|
else:
|
|
550
|
-
print(f"✓
|
|
575
|
+
print(f"✓ Account '{acc.get('label')}' removed.")
|
|
551
576
|
return 0
|
|
552
577
|
|
|
553
578
|
|
|
554
579
|
def cmd_email_set_enabled(args) -> int:
|
|
555
|
-
|
|
580
|
+
account_id, label = _selector_from_args(args)
|
|
556
581
|
json_mode = bool(getattr(args, "json", False))
|
|
557
582
|
enabled = bool(getattr(args, "enabled", True))
|
|
558
|
-
if not label:
|
|
559
|
-
msg = "
|
|
583
|
+
if account_id is None and not label:
|
|
584
|
+
msg = _selector_usage("enable|disable")
|
|
560
585
|
if json_mode:
|
|
561
586
|
_emit_json({"ok": False, "message": msg})
|
|
562
587
|
else:
|
|
563
588
|
print(msg)
|
|
564
589
|
return 1
|
|
565
590
|
from db import init_db
|
|
566
|
-
from db._email_accounts import get_email_account, set_email_account_enabled
|
|
591
|
+
from db._email_accounts import get_email_account, get_email_account_by_id, set_email_account_enabled
|
|
567
592
|
|
|
568
593
|
init_db()
|
|
569
|
-
acc = get_email_account(label)
|
|
594
|
+
acc = get_email_account_by_id(account_id) if account_id is not None else get_email_account(label)
|
|
570
595
|
if not acc:
|
|
571
|
-
|
|
596
|
+
selector = f"id={account_id}" if account_id is not None else label
|
|
597
|
+
msg = f"Account '{selector}' not found."
|
|
572
598
|
if json_mode:
|
|
573
599
|
_emit_json({"ok": False, "message": msg})
|
|
574
600
|
else:
|
|
575
601
|
print(f"✗ {msg}")
|
|
576
602
|
return 1
|
|
577
|
-
changed = set_email_account_enabled(
|
|
603
|
+
changed = set_email_account_enabled(account_id=acc.get("id"), enabled=enabled)
|
|
578
604
|
if not changed:
|
|
579
|
-
msg = f"
|
|
605
|
+
msg = f"Could not update account '{acc.get('label')}'."
|
|
580
606
|
if json_mode:
|
|
581
607
|
_emit_json({"ok": False, "message": msg})
|
|
582
608
|
else:
|
|
583
609
|
print(f"✗ {msg}")
|
|
584
610
|
return 1
|
|
585
|
-
updated =
|
|
611
|
+
updated = get_email_account_by_id(acc.get("id")) or {}
|
|
586
612
|
payload = {
|
|
587
613
|
"ok": True,
|
|
588
|
-
"
|
|
614
|
+
"id": updated.get("id"),
|
|
615
|
+
"label": updated.get("label") or label,
|
|
589
616
|
"enabled": bool(updated.get("enabled", enabled)),
|
|
590
617
|
"message": "enabled" if enabled else "disabled",
|
|
591
618
|
}
|
|
@@ -593,22 +620,22 @@ def cmd_email_set_enabled(args) -> int:
|
|
|
593
620
|
_emit_json(payload)
|
|
594
621
|
else:
|
|
595
622
|
print(
|
|
596
|
-
f"✓
|
|
597
|
-
+ ("
|
|
623
|
+
f"✓ Account '{payload['label']}' "
|
|
624
|
+
+ ("enabled." if payload["enabled"] else "disabled.")
|
|
598
625
|
)
|
|
599
626
|
return 0
|
|
600
627
|
|
|
601
628
|
|
|
602
629
|
def register_email_parser(subparsers) -> None:
|
|
603
630
|
"""Hook called by cli.py to add the `email` subcommand tree."""
|
|
604
|
-
p = subparsers.add_parser("email", help="
|
|
631
|
+
p = subparsers.add_parser("email", help="Manage NEXO email accounts")
|
|
605
632
|
p.set_defaults(func=lambda a: p.print_help() or 0)
|
|
606
633
|
sub = p.add_subparsers(dest="email_action")
|
|
607
634
|
|
|
608
|
-
s = sub.add_parser("setup", help="
|
|
635
|
+
s = sub.add_parser("setup", help="Interactive wizard to add or reconfigure an account")
|
|
609
636
|
s.set_defaults(func=cmd_email_setup)
|
|
610
637
|
|
|
611
|
-
s = sub.add_parser("add", help="
|
|
638
|
+
s = sub.add_parser("add", help="Add an account non-interactively (Desktop / scripts)")
|
|
612
639
|
s.add_argument("--label", required=True)
|
|
613
640
|
s.add_argument("--email", required=True)
|
|
614
641
|
s.add_argument("--imap-host", dest="imap_host", default="")
|
|
@@ -639,37 +666,41 @@ def register_email_parser(subparsers) -> None:
|
|
|
639
666
|
s.add_argument("--json", dest="json", action="store_true")
|
|
640
667
|
s.set_defaults(func=cmd_email_add)
|
|
641
668
|
|
|
642
|
-
s = sub.add_parser("list", help="
|
|
669
|
+
s = sub.add_parser("list", help="List configured accounts")
|
|
643
670
|
s.add_argument("--json", dest="json", action="store_true")
|
|
644
671
|
s.set_defaults(func=cmd_email_list)
|
|
645
672
|
|
|
646
|
-
s = sub.add_parser("test", help="
|
|
673
|
+
s = sub.add_parser("test", help="Test IMAP + SMTP for an account")
|
|
647
674
|
s.add_argument("label_pos", nargs="?", default=None,
|
|
648
|
-
help="
|
|
675
|
+
help="Account label (legacy positional)")
|
|
649
676
|
s.add_argument("--label", dest="label", default=None)
|
|
677
|
+
s.add_argument("--id", dest="account_id", type=int, default=None)
|
|
650
678
|
s.add_argument("--json", dest="json", action="store_true")
|
|
651
679
|
s.set_defaults(func=cmd_email_test)
|
|
652
680
|
|
|
653
|
-
s = sub.add_parser("remove", help="
|
|
681
|
+
s = sub.add_parser("remove", help="Remove an account")
|
|
654
682
|
s.add_argument("label_pos", nargs="?", default=None,
|
|
655
|
-
help="
|
|
683
|
+
help="Account label (legacy positional)")
|
|
656
684
|
s.add_argument("--label", dest="label", default=None)
|
|
685
|
+
s.add_argument("--id", dest="account_id", type=int, default=None)
|
|
657
686
|
s.add_argument("--yes", dest="yes", action="store_true",
|
|
658
687
|
help="Skip the interactive confirmation (required for --json).")
|
|
659
688
|
s.add_argument("--json", dest="json", action="store_true")
|
|
660
689
|
s.set_defaults(func=cmd_email_remove)
|
|
661
690
|
|
|
662
|
-
s = sub.add_parser("enable", help="
|
|
691
|
+
s = sub.add_parser("enable", help="Enable an account without deleting it")
|
|
663
692
|
s.add_argument("label_pos", nargs="?", default=None,
|
|
664
|
-
help="
|
|
693
|
+
help="Account label (legacy positional)")
|
|
665
694
|
s.add_argument("--label", dest="label", default=None)
|
|
695
|
+
s.add_argument("--id", dest="account_id", type=int, default=None)
|
|
666
696
|
s.add_argument("--json", dest="json", action="store_true")
|
|
667
697
|
s.set_defaults(func=cmd_email_set_enabled, enabled=True)
|
|
668
698
|
|
|
669
|
-
s = sub.add_parser("disable", help="
|
|
699
|
+
s = sub.add_parser("disable", help="Disable an account without deleting it")
|
|
670
700
|
s.add_argument("label_pos", nargs="?", default=None,
|
|
671
|
-
help="
|
|
701
|
+
help="Account label (legacy positional)")
|
|
672
702
|
s.add_argument("--label", dest="label", default=None)
|
|
703
|
+
s.add_argument("--id", dest="account_id", type=int, default=None)
|
|
673
704
|
s.add_argument("--json", dest="json", action="store_true")
|
|
674
705
|
s.set_defaults(func=cmd_email_set_enabled, enabled=False)
|
|
675
706
|
|
package/src/client_sync.py
CHANGED
|
@@ -78,6 +78,7 @@ except Exception:
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code"
|
|
81
|
+
DEFAULT_ASSISTANT_NAME = "Nova"
|
|
81
82
|
HOOK_TIMEOUTS_BY_EVENT = {
|
|
82
83
|
"SessionStart": 40,
|
|
83
84
|
"Stop": 15,
|
|
@@ -99,6 +100,16 @@ def _default_nexo_home() -> Path:
|
|
|
99
100
|
return resolve_nexo_home(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo")))
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
def _read_json_file(path: Path) -> dict:
|
|
104
|
+
if not path.is_file():
|
|
105
|
+
return {}
|
|
106
|
+
try:
|
|
107
|
+
payload = json.loads(path.read_text())
|
|
108
|
+
except Exception:
|
|
109
|
+
return {}
|
|
110
|
+
return payload if isinstance(payload, dict) else {}
|
|
111
|
+
|
|
112
|
+
|
|
102
113
|
def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
103
114
|
explicit = (explicit or "").strip()
|
|
104
115
|
if explicit:
|
|
@@ -106,6 +117,16 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
|
106
117
|
env_name = os.environ.get("NEXO_NAME", "").strip()
|
|
107
118
|
if env_name:
|
|
108
119
|
return env_name
|
|
120
|
+
calibration = _read_json_file(nexo_home / "personal" / "brain" / "calibration.json")
|
|
121
|
+
user_payload = calibration.get("user")
|
|
122
|
+
if isinstance(user_payload, dict):
|
|
123
|
+
candidate = str(user_payload.get("assistant_name", "")).strip()
|
|
124
|
+
if candidate:
|
|
125
|
+
return candidate
|
|
126
|
+
for key in ("assistant_name", "identity", "operator_name"):
|
|
127
|
+
candidate = str(calibration.get(key, "")).strip()
|
|
128
|
+
if candidate:
|
|
129
|
+
return candidate
|
|
109
130
|
version_file = nexo_home / "version.json"
|
|
110
131
|
if version_file.is_file():
|
|
111
132
|
try:
|
|
@@ -114,7 +135,7 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
|
114
135
|
return candidate
|
|
115
136
|
except Exception:
|
|
116
137
|
pass
|
|
117
|
-
return
|
|
138
|
+
return DEFAULT_ASSISTANT_NAME
|
|
118
139
|
|
|
119
140
|
|
|
120
141
|
def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str] | None = None) -> Path:
|
package/src/cognitive/_core.py
CHANGED
|
@@ -11,11 +11,13 @@ from datetime import datetime, timedelta
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
+
import paths
|
|
15
|
+
|
|
14
16
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
_cognitive_dir = paths.cognitive_dir()
|
|
18
|
+
_cognitive_dir.mkdir(parents=True, exist_ok=True)
|
|
17
19
|
|
|
18
|
-
COGNITIVE_DB =
|
|
20
|
+
COGNITIVE_DB = str(_cognitive_dir / "cognitive.db")
|
|
19
21
|
EMBEDDING_DIM = 768
|
|
20
22
|
LAMBDA_STM = 0.004126 # half-life = ln(2) / (7 * 24) ≈ 7 days
|
|
21
23
|
LAMBDA_LTM = 0.000481 # half-life = ln(2) / (60 * 24) ≈ 60 days
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Shared prompt catalog for productized NEXO core automations."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_TOKEN_RE = re.compile(r"\[\[([a-zA-Z0-9_]+)\]\]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _resolve_repo_root() -> Path:
|
|
14
|
+
candidate = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent))).expanduser().resolve()
|
|
15
|
+
if candidate.name == "src":
|
|
16
|
+
return candidate.parent
|
|
17
|
+
if (candidate / "templates").is_dir():
|
|
18
|
+
return candidate
|
|
19
|
+
if (candidate.parent / "templates").is_dir():
|
|
20
|
+
return candidate.parent
|
|
21
|
+
return Path(__file__).resolve().parents[1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PROMPTS_DIR = _resolve_repo_root() / "templates" / "core-prompts"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=None)
|
|
28
|
+
def load_core_prompt_template(name: str) -> str:
|
|
29
|
+
path = PROMPTS_DIR / f"{name}.md"
|
|
30
|
+
if not path.is_file():
|
|
31
|
+
raise FileNotFoundError(f"Core prompt template not found: {path}")
|
|
32
|
+
return path.read_text(encoding="utf-8")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_core_prompt(name: str, /, **values: object) -> str:
|
|
36
|
+
template = load_core_prompt_template(name)
|
|
37
|
+
required = {match.group(1) for match in _TOKEN_RE.finditer(template)}
|
|
38
|
+
missing = sorted(key for key in required if key not in values)
|
|
39
|
+
if missing:
|
|
40
|
+
raise KeyError(f"Missing values for core prompt '{name}': {', '.join(missing)}")
|
|
41
|
+
|
|
42
|
+
rendered = _TOKEN_RE.sub(lambda match: str(values[match.group(1)]), template)
|
|
43
|
+
return rendered.rstrip("\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"PROMPTS_DIR",
|
|
48
|
+
"load_core_prompt_template",
|
|
49
|
+
"render_core_prompt",
|
|
50
|
+
]
|
package/src/cron_recovery.py
CHANGED
|
@@ -8,6 +8,7 @@ import plistlib
|
|
|
8
8
|
import sqlite3
|
|
9
9
|
import contextlib
|
|
10
10
|
import hashlib
|
|
11
|
+
import platform
|
|
11
12
|
import socket
|
|
12
13
|
from datetime import datetime, timedelta, timezone
|
|
13
14
|
from pathlib import Path
|
|
@@ -34,6 +35,68 @@ def _load_json(path: Path, default):
|
|
|
34
35
|
return default
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def _normalize_platform_name(system: str | None = None) -> str:
|
|
39
|
+
value = str(system or os.environ.get("NEXO_PLATFORM") or platform.system()).strip().lower()
|
|
40
|
+
if value.startswith("darwin") or value.startswith("mac"):
|
|
41
|
+
return "darwin"
|
|
42
|
+
if value.startswith("linux"):
|
|
43
|
+
return "linux"
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_power_policy_name(value: str | None) -> str:
|
|
48
|
+
return str(value or "unset").strip().lower().replace("-", "_")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _cron_platform_enabled(cron: dict, *, system: str | None = None) -> bool:
|
|
52
|
+
allowed = cron.get("platforms")
|
|
53
|
+
if allowed is None:
|
|
54
|
+
allowed = cron.get("platform")
|
|
55
|
+
if not allowed:
|
|
56
|
+
return True
|
|
57
|
+
if isinstance(allowed, str):
|
|
58
|
+
values = [allowed]
|
|
59
|
+
elif isinstance(allowed, (list, tuple, set)):
|
|
60
|
+
values = list(allowed)
|
|
61
|
+
else:
|
|
62
|
+
return True
|
|
63
|
+
current = _normalize_platform_name(system)
|
|
64
|
+
normalized = {_normalize_platform_name(str(item)) for item in values if str(item).strip()}
|
|
65
|
+
return not normalized or current in normalized
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_cron_enabled(
|
|
69
|
+
cron: dict,
|
|
70
|
+
*,
|
|
71
|
+
optionals: dict[str, bool] | None = None,
|
|
72
|
+
schedule_data: dict | None = None,
|
|
73
|
+
system: str | None = None,
|
|
74
|
+
) -> bool:
|
|
75
|
+
optionals = optionals or {}
|
|
76
|
+
schedule_data = schedule_data or {}
|
|
77
|
+
|
|
78
|
+
optional_key = cron.get("optional")
|
|
79
|
+
automation_default = bool(schedule_data.get("automation_enabled", True))
|
|
80
|
+
if optional_key == "automation":
|
|
81
|
+
optional_enabled = optionals.get(optional_key, automation_default)
|
|
82
|
+
else:
|
|
83
|
+
optional_enabled = optionals.get(optional_key, False)
|
|
84
|
+
if optional_key and not optional_enabled:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
if not _cron_platform_enabled(cron, system=system):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
required_power_policy = cron.get("requires_power_policy")
|
|
91
|
+
if required_power_policy:
|
|
92
|
+
current_policy = _normalize_power_policy_name(schedule_data.get("power_policy"))
|
|
93
|
+
expected_policy = _normalize_power_policy_name(required_power_policy)
|
|
94
|
+
if current_policy != expected_policy:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
37
100
|
def _schedule_machine_id() -> str:
|
|
38
101
|
schedule = _load_json(SCHEDULE_FILE, {})
|
|
39
102
|
if isinstance(schedule, dict):
|
|
@@ -88,6 +151,10 @@ def load_enabled_crons() -> list[dict]:
|
|
|
88
151
|
from automation_controls import apply_core_automation_overrides
|
|
89
152
|
except Exception:
|
|
90
153
|
apply_core_automation_overrides = None
|
|
154
|
+
try:
|
|
155
|
+
from product_mode import filter_blocked_crons
|
|
156
|
+
except Exception:
|
|
157
|
+
filter_blocked_crons = None
|
|
91
158
|
|
|
92
159
|
manifest_candidates = [
|
|
93
160
|
paths.crons_dir() / "manifest.json",
|
|
@@ -97,7 +164,9 @@ def load_enabled_crons() -> list[dict]:
|
|
|
97
164
|
if not isinstance(optionals, dict):
|
|
98
165
|
optionals = {}
|
|
99
166
|
schedule_data = _load_json(SCHEDULE_FILE, {})
|
|
100
|
-
|
|
167
|
+
if not isinstance(schedule_data, dict):
|
|
168
|
+
schedule_data = {}
|
|
169
|
+
platform_name = _normalize_platform_name()
|
|
101
170
|
|
|
102
171
|
for manifest_path in manifest_candidates:
|
|
103
172
|
if not manifest_path.is_file():
|
|
@@ -109,14 +178,19 @@ def load_enabled_crons() -> list[dict]:
|
|
|
109
178
|
|
|
110
179
|
enabled = []
|
|
111
180
|
for cron in data.get("crons", []):
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
181
|
+
if not is_cron_enabled(
|
|
182
|
+
cron,
|
|
183
|
+
optionals=optionals,
|
|
184
|
+
schedule_data=schedule_data,
|
|
185
|
+
system=platform_name,
|
|
186
|
+
):
|
|
118
187
|
continue
|
|
119
188
|
enabled.append(dict(cron))
|
|
189
|
+
if callable(filter_blocked_crons):
|
|
190
|
+
try:
|
|
191
|
+
enabled = filter_blocked_crons(enabled)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
120
194
|
if callable(apply_core_automation_overrides):
|
|
121
195
|
try:
|
|
122
196
|
return apply_core_automation_overrides(enabled)
|