nexo-brain 7.30.10 → 7.30.12
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/bin/nexo-brain.js +55 -0
- package/bin/postinstall.js +33 -1
- package/package.json +1 -1
- package/src/cli_email.py +7 -22
- package/src/email_config.py +4 -17
- package/src/email_credentials.py +159 -0
- package/src/scripts/nexo-email-migrate-config.py +8 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.12",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.30.
|
|
21
|
+
Version `7.30.12` is the current packaged-runtime line. Patch release over v7.30.11 - Desktop-managed email credentials now resolve through the shared credential helper, so Email NEXO migrations and CLI reads use keychain-backed markers before legacy SQLite fallback.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.11`: patch release over v7.30.10 - the installer and npm postinstall path now stamp the verified repair baseline too, so the first update from older packaged installs is covered without a manual second run.
|
|
24
|
+
|
|
25
|
+
Previously in `7.30.10`: patch release over v7.30.9 - packaged `nexo update` now stamps the verified repair baseline after import verification, including same-version maintenance runs.
|
|
22
26
|
|
|
23
27
|
Previously in `7.30.9`: patch release over v7.30.8 - post-update self-heal now stamps a verified repair baseline, and doctor release gates distinguish current installation failures from historical operator/session drift.
|
|
24
28
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -949,6 +949,52 @@ function syncRuntimePackageMetadata(repoRoot = path.join(__dirname, ".."), runti
|
|
|
949
949
|
}
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
const REPAIR_BASELINE_FILE = "last-repair-baseline.json";
|
|
953
|
+
|
|
954
|
+
function _runtimeRepairBaselineDir(nexoHome = NEXO_HOME) {
|
|
955
|
+
const canonical = path.join(nexoHome, "runtime", "operations");
|
|
956
|
+
const legacy = path.join(nexoHome, "operations");
|
|
957
|
+
if (!fs.existsSync(path.join(nexoHome, "runtime")) && fs.existsSync(legacy)) {
|
|
958
|
+
return legacy;
|
|
959
|
+
}
|
|
960
|
+
return canonical;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function stampRuntimeRepairBaseline(nexoHome = NEXO_HOME, source = "bin.nexo-brain") {
|
|
964
|
+
try {
|
|
965
|
+
const operationsDir = _runtimeRepairBaselineDir(nexoHome);
|
|
966
|
+
fs.mkdirSync(operationsDir, { recursive: true });
|
|
967
|
+
const now = new Date();
|
|
968
|
+
const body = JSON.stringify({
|
|
969
|
+
last_repair_epoch: now.getTime() / 1000,
|
|
970
|
+
last_repair_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
971
|
+
source,
|
|
972
|
+
reason: "verified runtime repair baseline after installer/postinstall repair",
|
|
973
|
+
}, null, 2) + "\n";
|
|
974
|
+
const baselinePath = path.join(operationsDir, REPAIR_BASELINE_FILE);
|
|
975
|
+
fs.writeFileSync(baselinePath, body);
|
|
976
|
+
|
|
977
|
+
const legacyDir = path.join(nexoHome, "operations");
|
|
978
|
+
const legacyPath = path.join(legacyDir, REPAIR_BASELINE_FILE);
|
|
979
|
+
if (legacyPath !== baselinePath && fs.existsSync(legacyDir)) {
|
|
980
|
+
try {
|
|
981
|
+
fs.writeFileSync(legacyPath, body);
|
|
982
|
+
} catch (_) {}
|
|
983
|
+
}
|
|
984
|
+
return { ok: true, path: baselinePath };
|
|
985
|
+
} catch (err) {
|
|
986
|
+
return { ok: false, error: String((err && err.message) || err) };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function logRepairBaselineStatus(result) {
|
|
991
|
+
if (result && result.ok) {
|
|
992
|
+
log(" Repair baseline: updated.");
|
|
993
|
+
} else {
|
|
994
|
+
log(` Repair baseline warning: ${(result && result.error) || "unknown error"}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
952
998
|
function resolveRuntimeConfigDir(nexoHome) {
|
|
953
999
|
const canonical = path.join(nexoHome, "personal", "config");
|
|
954
1000
|
const legacy = path.join(nexoHome, "config");
|
|
@@ -3286,6 +3332,9 @@ async function runSetup() {
|
|
|
3286
3332
|
throw new Error(`Runtime activation failed: ${migActivation.error}`);
|
|
3287
3333
|
}
|
|
3288
3334
|
log(` Runtime activation: core/current -> versions/${currentVersion}`);
|
|
3335
|
+
logRepairBaselineStatus(
|
|
3336
|
+
stampRuntimeRepairBaseline(NEXO_HOME, "bin.nexo-brain.migration")
|
|
3337
|
+
);
|
|
3289
3338
|
|
|
3290
3339
|
// Keep the rendered template in-memory for version tracking, but do
|
|
3291
3340
|
// not drop a loose reference file in NEXO_HOME root.
|
|
@@ -3439,6 +3488,9 @@ async function runSetup() {
|
|
|
3439
3488
|
if (!syncLayoutFinalize.ok) {
|
|
3440
3489
|
throw new Error(`F0.6 layout finalization failed: ${syncLayoutFinalize.error}`);
|
|
3441
3490
|
}
|
|
3491
|
+
logRepairBaselineStatus(
|
|
3492
|
+
stampRuntimeRepairBaseline(NEXO_HOME, "bin.nexo-brain.same-version-repair")
|
|
3493
|
+
);
|
|
3442
3494
|
|
|
3443
3495
|
runDesktopAwareModelWarmup(syncPython, NEXO_HOME, { reason: "repair" });
|
|
3444
3496
|
logMacPermissionsNotice(NEXO_HOME, syncPython);
|
|
@@ -5029,6 +5081,9 @@ See ~/.nexo/ for configuration.
|
|
|
5029
5081
|
if (!layoutFinalize.ok) {
|
|
5030
5082
|
throw new Error(`F0.6 layout finalization failed: ${layoutFinalize.error}`);
|
|
5031
5083
|
}
|
|
5084
|
+
logRepairBaselineStatus(
|
|
5085
|
+
stampRuntimeRepairBaseline(NEXO_HOME, "bin.nexo-brain.install")
|
|
5086
|
+
);
|
|
5032
5087
|
|
|
5033
5088
|
console.log("");
|
|
5034
5089
|
const readyMsg = t.ready(operatorName, aliasName);
|
package/bin/postinstall.js
CHANGED
|
@@ -13,6 +13,7 @@ const { execFileSync } = require("child_process");
|
|
|
13
13
|
const NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
14
14
|
const VERSION_FILE = path.join(NEXO_HOME, "version.json");
|
|
15
15
|
const INSTALLER = path.join(__dirname, "nexo-brain.js");
|
|
16
|
+
const REPAIR_BASELINE_FILE = "last-repair-baseline.json";
|
|
16
17
|
|
|
17
18
|
if (process.env.NEXO_SKIP_POSTINSTALL === "1") {
|
|
18
19
|
// Called during rollback — skip migration to avoid loops
|
|
@@ -31,13 +32,44 @@ function runModelWarmup(reason) {
|
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function runtimeRepairBaselineDir(nexoHome = NEXO_HOME) {
|
|
36
|
+
const canonical = path.join(nexoHome, "runtime", "operations");
|
|
37
|
+
const legacy = path.join(nexoHome, "operations");
|
|
38
|
+
if (!fs.existsSync(path.join(nexoHome, "runtime")) && fs.existsSync(legacy)) {
|
|
39
|
+
return legacy;
|
|
40
|
+
}
|
|
41
|
+
return canonical;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stampRuntimeRepairBaseline(source = "bin.postinstall") {
|
|
45
|
+
const operationsDir = runtimeRepairBaselineDir(NEXO_HOME);
|
|
46
|
+
fs.mkdirSync(operationsDir, { recursive: true });
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const body = JSON.stringify({
|
|
49
|
+
last_repair_epoch: now.getTime() / 1000,
|
|
50
|
+
last_repair_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
51
|
+
source,
|
|
52
|
+
reason: "verified runtime repair baseline after installer/postinstall repair",
|
|
53
|
+
}, null, 2) + "\n";
|
|
54
|
+
const baselinePath = path.join(operationsDir, REPAIR_BASELINE_FILE);
|
|
55
|
+
fs.writeFileSync(baselinePath, body);
|
|
56
|
+
|
|
57
|
+
const legacyDir = path.join(NEXO_HOME, "operations");
|
|
58
|
+
const legacyPath = path.join(legacyDir, REPAIR_BASELINE_FILE);
|
|
59
|
+
if (legacyPath !== baselinePath && fs.existsSync(legacyDir)) {
|
|
60
|
+
try {
|
|
61
|
+
fs.writeFileSync(legacyPath, body);
|
|
62
|
+
} catch (_) {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
if (fs.existsSync(VERSION_FILE)) {
|
|
35
67
|
// Existing installation — run auto-migration silently
|
|
36
68
|
const installed = JSON.parse(fs.readFileSync(VERSION_FILE, "utf8"));
|
|
37
69
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
38
70
|
|
|
39
71
|
if (installed.version === pkg.version) {
|
|
40
|
-
|
|
72
|
+
stampRuntimeRepairBaseline("bin.postinstall.same-version");
|
|
41
73
|
process.exit(0);
|
|
42
74
|
}
|
|
43
75
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.12",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/cli_email.py
CHANGED
|
@@ -21,7 +21,6 @@ from __future__ import annotations
|
|
|
21
21
|
import getpass
|
|
22
22
|
import json
|
|
23
23
|
import sys
|
|
24
|
-
import time
|
|
25
24
|
from typing import Any
|
|
26
25
|
|
|
27
26
|
|
|
@@ -74,30 +73,16 @@ def _sent_folder_from_account(account: dict | None) -> str:
|
|
|
74
73
|
|
|
75
74
|
|
|
76
75
|
def _store_credential(service: str, key: str, value: str) -> None:
|
|
77
|
-
"""Write password
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
conn = get_db()
|
|
82
|
-
now = time.time()
|
|
83
|
-
conn.execute(
|
|
84
|
-
"""
|
|
85
|
-
INSERT INTO credentials (service, key, value, notes, created_at, updated_at)
|
|
86
|
-
VALUES (?, ?, ?, 'email account password (nexo email setup)', ?, ?)
|
|
87
|
-
ON CONFLICT(service, key) DO UPDATE SET
|
|
88
|
-
value = excluded.value,
|
|
89
|
-
updated_at = excluded.updated_at
|
|
90
|
-
""",
|
|
91
|
-
(service, key, value, now, now),
|
|
92
|
-
)
|
|
93
|
-
conn.commit()
|
|
76
|
+
"""Write password without echoing it back to stdout."""
|
|
77
|
+
from email_credentials import store_email_credential
|
|
78
|
+
|
|
79
|
+
store_email_credential(service, key, value, "email account password (nexo email setup)")
|
|
94
80
|
|
|
95
81
|
|
|
96
82
|
def _delete_credential(service: str, key: str) -> None:
|
|
97
|
-
from
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
conn.commit()
|
|
83
|
+
from email_credentials import delete_email_credential
|
|
84
|
+
|
|
85
|
+
delete_email_credential(service, key)
|
|
101
86
|
|
|
102
87
|
|
|
103
88
|
def cmd_email_setup(args) -> int:
|
package/src/email_config.py
CHANGED
|
@@ -41,24 +41,11 @@ LEGACY_CONFIG_PATH = _legacy_config_path()
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _get_credential(service: str, key: str) -> str:
|
|
44
|
-
"""Fetch a password from
|
|
45
|
-
on any miss so the caller can log-and-skip instead of crashing a cron.
|
|
46
|
-
"""
|
|
47
|
-
if not service or not key:
|
|
48
|
-
return ""
|
|
44
|
+
"""Fetch a password from keyring marker or legacy credentials table."""
|
|
49
45
|
try:
|
|
50
|
-
from
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
try:
|
|
54
|
-
conn = get_db()
|
|
55
|
-
row = conn.execute(
|
|
56
|
-
"SELECT value FROM credentials WHERE service = ? AND key = ?",
|
|
57
|
-
(service, key),
|
|
58
|
-
).fetchone()
|
|
59
|
-
if row is None:
|
|
60
|
-
return ""
|
|
61
|
-
return str(row[0] or "")
|
|
46
|
+
from email_credentials import read_email_credential
|
|
47
|
+
|
|
48
|
+
return read_email_credential(service, key)
|
|
62
49
|
except Exception as exc: # pragma: no cover
|
|
63
50
|
_logger.warning("credential lookup failed for %s/%s: %s", service, key, exc)
|
|
64
51
|
return ""
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Credential storage for email account passwords.
|
|
2
|
+
|
|
3
|
+
Email account rows keep only ``credential_service`` + ``credential_key``.
|
|
4
|
+
Historically the referenced ``credentials.value`` column stored the password
|
|
5
|
+
directly. New writes try to place the secret in the OS keyring and keep a
|
|
6
|
+
``keyring://...`` marker in SQLite. Legacy plaintext values remain readable so
|
|
7
|
+
older installs keep working until they are rotated or migrated.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from urllib.parse import quote, unquote
|
|
16
|
+
|
|
17
|
+
KEYRING_SERVICE = "com.nexo.email"
|
|
18
|
+
KEYRING_MARKER_PREFIX = "keyring://"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _db():
|
|
22
|
+
from db._core import get_db
|
|
23
|
+
|
|
24
|
+
return get_db()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _marker(service: str, key: str) -> str:
|
|
28
|
+
return f"{KEYRING_MARKER_PREFIX}{quote(service, safe='')}/{quote(key, safe='')}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_marker(value: str) -> tuple[str, str] | None:
|
|
32
|
+
if not value.startswith(KEYRING_MARKER_PREFIX):
|
|
33
|
+
return None
|
|
34
|
+
rest = value[len(KEYRING_MARKER_PREFIX):]
|
|
35
|
+
if "/" not in rest:
|
|
36
|
+
return None
|
|
37
|
+
service, key = rest.split("/", 1)
|
|
38
|
+
return unquote(service), unquote(key)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _account_name(service: str, key: str) -> str:
|
|
42
|
+
return f"{service}:{key}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _keyring_module():
|
|
46
|
+
try:
|
|
47
|
+
return importlib.import_module("keyring")
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_stored_value(service: str, key: str) -> str:
|
|
53
|
+
if not service or not key:
|
|
54
|
+
return ""
|
|
55
|
+
row = _db().execute(
|
|
56
|
+
"SELECT value FROM credentials WHERE service = ? AND key = ?",
|
|
57
|
+
(service, key),
|
|
58
|
+
).fetchone()
|
|
59
|
+
if row is None:
|
|
60
|
+
return ""
|
|
61
|
+
return str(row[0] or "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _write_db_value(service: str, key: str, value: str, notes: str) -> None:
|
|
65
|
+
conn = _db()
|
|
66
|
+
now = time.time()
|
|
67
|
+
conn.execute(
|
|
68
|
+
"""
|
|
69
|
+
INSERT INTO credentials (service, key, value, notes, created_at, updated_at)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
71
|
+
ON CONFLICT(service, key) DO UPDATE SET
|
|
72
|
+
value = excluded.value,
|
|
73
|
+
notes = excluded.notes,
|
|
74
|
+
updated_at = excluded.updated_at
|
|
75
|
+
""",
|
|
76
|
+
(service, key, value, notes, now, now),
|
|
77
|
+
)
|
|
78
|
+
conn.commit()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def store_email_credential(service: str, key: str, value: str, notes: str = "email account password") -> str:
|
|
82
|
+
"""Store an email password and return the SQLite value written.
|
|
83
|
+
|
|
84
|
+
Uses keyring when available. If the keyring backend is unavailable or
|
|
85
|
+
locked, the legacy SQLite plaintext path is used so existing automation
|
|
86
|
+
does not lose email access.
|
|
87
|
+
"""
|
|
88
|
+
service = str(service or "").strip()
|
|
89
|
+
key = str(key or "").strip()
|
|
90
|
+
value = str(value or "")
|
|
91
|
+
if not service or not key:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
mode = os.environ.get("NEXO_EMAIL_CREDENTIAL_STORE", "auto").strip().lower()
|
|
95
|
+
keyring = None if mode in {"sqlite", "legacy", "plain", "plaintext"} else _keyring_module()
|
|
96
|
+
if keyring is not None:
|
|
97
|
+
try:
|
|
98
|
+
keyring.set_password(KEYRING_SERVICE, _account_name(service, key), value)
|
|
99
|
+
stored = _marker(service, key)
|
|
100
|
+
_write_db_value(service, key, stored, notes + " (stored in system keyring)")
|
|
101
|
+
return stored
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
if mode == "keyring":
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
_write_db_value(service, key, value, notes + " (legacy sqlite fallback)")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_email_credential(service: str, key: str) -> str:
|
|
113
|
+
"""Resolve an email password from keyring marker or legacy plaintext."""
|
|
114
|
+
stored = _read_stored_value(str(service or "").strip(), str(key or "").strip())
|
|
115
|
+
parsed = _parse_marker(stored)
|
|
116
|
+
if parsed is None:
|
|
117
|
+
return stored
|
|
118
|
+
|
|
119
|
+
keyring = _keyring_module()
|
|
120
|
+
if keyring is None:
|
|
121
|
+
return ""
|
|
122
|
+
try:
|
|
123
|
+
value = keyring.get_password(KEYRING_SERVICE, _account_name(*parsed))
|
|
124
|
+
except Exception:
|
|
125
|
+
return ""
|
|
126
|
+
return str(value or "")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def delete_email_credential(service: str, key: str) -> None:
|
|
130
|
+
service = str(service or "").strip()
|
|
131
|
+
key = str(key or "").strip()
|
|
132
|
+
if not service or not key:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
parsed = _parse_marker(_read_stored_value(service, key))
|
|
136
|
+
keyring = _keyring_module()
|
|
137
|
+
if parsed is not None and keyring is not None:
|
|
138
|
+
try:
|
|
139
|
+
keyring.delete_password(KEYRING_SERVICE, _account_name(*parsed))
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
conn = _db()
|
|
144
|
+
conn.execute("DELETE FROM credentials WHERE service = ? AND key = ?", (service, key))
|
|
145
|
+
conn.commit()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_keyring_marker(value: str) -> bool:
|
|
149
|
+
return _parse_marker(str(value or "")) is not None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = [
|
|
153
|
+
"KEYRING_MARKER_PREFIX",
|
|
154
|
+
"KEYRING_SERVICE",
|
|
155
|
+
"delete_email_credential",
|
|
156
|
+
"is_keyring_marker",
|
|
157
|
+
"read_email_credential",
|
|
158
|
+
"store_email_credential",
|
|
159
|
+
]
|
|
@@ -26,7 +26,6 @@ import json
|
|
|
26
26
|
import os
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
|
-
import time
|
|
30
29
|
from pathlib import Path
|
|
31
30
|
|
|
32
31
|
|
|
@@ -121,20 +120,14 @@ def main(argv: list[str]) -> int:
|
|
|
121
120
|
print(f"[dry-run] credentials[{cred_service}/{cred_key}] = <password>")
|
|
122
121
|
return 0
|
|
123
122
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
VALUES (?, ?, ?, 'migrated from ~/.nexo/nexo-email/config.json (F1)', ?, ?)
|
|
132
|
-
ON CONFLICT(service, key) DO UPDATE SET
|
|
133
|
-
value = excluded.value, updated_at = excluded.updated_at
|
|
134
|
-
""",
|
|
135
|
-
(cred_service, cred_key, password, now, now),
|
|
123
|
+
from email_credentials import store_email_credential
|
|
124
|
+
|
|
125
|
+
store_email_credential(
|
|
126
|
+
cred_service,
|
|
127
|
+
cred_key,
|
|
128
|
+
password,
|
|
129
|
+
"migrated from ~/.nexo/nexo-email/config.json (F1)",
|
|
136
130
|
)
|
|
137
|
-
conn.commit()
|
|
138
131
|
|
|
139
132
|
if existing and not args.force:
|
|
140
133
|
existing_metadata = existing.get("metadata") if isinstance(existing.get("metadata"), dict) else {}
|
|
@@ -194,7 +187,7 @@ def main(argv: list[str]) -> int:
|
|
|
194
187
|
can_send=True,
|
|
195
188
|
)
|
|
196
189
|
print(f"✓ Cuenta agente '{args.label}' migrada ({account.get('email')}).")
|
|
197
|
-
print(f" Password guardada en
|
|
190
|
+
print(f" Password guardada en el almacen de credenciales[{cred_service}/{cred_key}].")
|
|
198
191
|
print(
|
|
199
192
|
" Metadata: "
|
|
200
193
|
f"operator_aliases={len(legacy_operator_aliases)}, trusted_domains={len(trusted)}."
|