nexo-brain 2.6.10 → 2.6.11
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/bin/nexo-brain.js +42 -39
- package/package.json +1 -1
- package/src/auto_update.py +55 -2
- package/src/cli.py +175 -38
- package/src/scripts/nexo-catchup.py +25 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.11",
|
|
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/bin/nexo-brain.js
CHANGED
|
@@ -89,6 +89,44 @@ function syncWatchdogHashRegistry(nexoHome) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function getCoreRuntimeFlatFiles() {
|
|
93
|
+
return [
|
|
94
|
+
"server.py",
|
|
95
|
+
"plugin_loader.py",
|
|
96
|
+
"knowledge_graph.py",
|
|
97
|
+
"kg_populate.py",
|
|
98
|
+
"maintenance.py",
|
|
99
|
+
"storage_router.py",
|
|
100
|
+
"claim_graph.py",
|
|
101
|
+
"hnsw_index.py",
|
|
102
|
+
"evolution_cycle.py",
|
|
103
|
+
"migrate_embeddings.py",
|
|
104
|
+
"auto_close_sessions.py",
|
|
105
|
+
"client_sync.py",
|
|
106
|
+
"auto_update.py",
|
|
107
|
+
"tools_sessions.py",
|
|
108
|
+
"tools_coordination.py",
|
|
109
|
+
"tools_reminders.py",
|
|
110
|
+
"tools_reminders_crud.py",
|
|
111
|
+
"tools_learnings.py",
|
|
112
|
+
"tools_credentials.py",
|
|
113
|
+
"tools_task_history.py",
|
|
114
|
+
"tools_menu.py",
|
|
115
|
+
"cli.py",
|
|
116
|
+
"script_registry.py",
|
|
117
|
+
"skills_runtime.py",
|
|
118
|
+
"user_context.py",
|
|
119
|
+
"public_contribution.py",
|
|
120
|
+
"cron_recovery.py",
|
|
121
|
+
"runtime_power.py",
|
|
122
|
+
"requirements.txt",
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getCoreRuntimePackages() {
|
|
127
|
+
return ["db", "cognitive", "doctor"];
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
function isProtectedMacPath(candidate) {
|
|
93
131
|
if (process.platform !== "darwin" || !candidate) return false;
|
|
94
132
|
const homeDir = require("os").homedir();
|
|
@@ -981,16 +1019,7 @@ async function main() {
|
|
|
981
1019
|
log(" Hooks updated.");
|
|
982
1020
|
|
|
983
1021
|
// Update core Python files (flat .py files in src/)
|
|
984
|
-
const coreFlatFiles =
|
|
985
|
-
"server.py", "plugin_loader.py",
|
|
986
|
-
"knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
|
|
987
|
-
"claim_graph.py", "hnsw_index.py", "evolution_cycle.py", "migrate_embeddings.py",
|
|
988
|
-
"auto_close_sessions.py", "auto_update.py",
|
|
989
|
-
"tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
|
|
990
|
-
"tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
|
|
991
|
-
"tools_task_history.py", "tools_menu.py",
|
|
992
|
-
"requirements.txt",
|
|
993
|
-
];
|
|
1022
|
+
const coreFlatFiles = getCoreRuntimeFlatFiles();
|
|
994
1023
|
coreFlatFiles.forEach((f) => {
|
|
995
1024
|
const src = path.join(srcDir, f);
|
|
996
1025
|
if (fs.existsSync(src)) {
|
|
@@ -998,7 +1027,7 @@ async function main() {
|
|
|
998
1027
|
}
|
|
999
1028
|
});
|
|
1000
1029
|
// Update core packages (db/, cognitive/) — full directory copy
|
|
1001
|
-
|
|
1030
|
+
getCoreRuntimePackages().forEach(pkg => {
|
|
1002
1031
|
const pkgSrc = path.join(srcDir, pkg);
|
|
1003
1032
|
if (fs.existsSync(pkgSrc)) {
|
|
1004
1033
|
copyDirRec(pkgSrc, path.join(NEXO_HOME, pkg));
|
|
@@ -1752,33 +1781,7 @@ async function main() {
|
|
|
1752
1781
|
};
|
|
1753
1782
|
|
|
1754
1783
|
// Core flat files (single .py files in src/)
|
|
1755
|
-
const coreFiles =
|
|
1756
|
-
"server.py",
|
|
1757
|
-
"plugin_loader.py",
|
|
1758
|
-
"knowledge_graph.py",
|
|
1759
|
-
"kg_populate.py",
|
|
1760
|
-
"maintenance.py",
|
|
1761
|
-
"storage_router.py",
|
|
1762
|
-
"claim_graph.py",
|
|
1763
|
-
"hnsw_index.py",
|
|
1764
|
-
"evolution_cycle.py",
|
|
1765
|
-
"migrate_embeddings.py",
|
|
1766
|
-
"auto_close_sessions.py",
|
|
1767
|
-
"client_sync.py",
|
|
1768
|
-
"auto_update.py",
|
|
1769
|
-
"tools_sessions.py",
|
|
1770
|
-
"tools_coordination.py",
|
|
1771
|
-
"tools_reminders.py",
|
|
1772
|
-
"tools_reminders_crud.py",
|
|
1773
|
-
"tools_learnings.py",
|
|
1774
|
-
"tools_credentials.py",
|
|
1775
|
-
"tools_task_history.py",
|
|
1776
|
-
"tools_menu.py",
|
|
1777
|
-
"requirements.txt",
|
|
1778
|
-
"cli.py",
|
|
1779
|
-
"script_registry.py",
|
|
1780
|
-
"skills_runtime.py",
|
|
1781
|
-
];
|
|
1784
|
+
const coreFiles = getCoreRuntimeFlatFiles();
|
|
1782
1785
|
coreFiles.forEach((f) => {
|
|
1783
1786
|
const src = path.join(srcDir, f);
|
|
1784
1787
|
if (fs.existsSync(src)) {
|
|
@@ -1811,7 +1814,7 @@ async function main() {
|
|
|
1811
1814
|
|
|
1812
1815
|
log("Copying core packages...");
|
|
1813
1816
|
// Core packages (directories with __init__.py)
|
|
1814
|
-
|
|
1817
|
+
getCoreRuntimePackages().forEach(pkg => {
|
|
1815
1818
|
const pkgSrc = path.join(srcDir, pkg);
|
|
1816
1819
|
if (fs.existsSync(pkgSrc)) {
|
|
1817
1820
|
copyDirRecursive(pkgSrc, path.join(NEXO_HOME, pkg));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.11",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — local cognitive runtime for Claude Code. Persistent memory, overnight learning, recovery-aware crons, personal scripts, doctor diagnostics, startup preflight, and optional power helper.",
|
|
6
6
|
"bin": {
|
package/src/auto_update.py
CHANGED
|
@@ -1202,6 +1202,7 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1202
1202
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1203
1203
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
1204
1204
|
"cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
|
|
1205
|
+
"public_contribution.py",
|
|
1205
1206
|
"cron_recovery.py", "runtime_power.py", "requirements.txt", "package.json", "version.json",
|
|
1206
1207
|
]
|
|
1207
1208
|
for name in code_dirs:
|
|
@@ -1250,6 +1251,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1250
1251
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1251
1252
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
1252
1253
|
"cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
|
|
1254
|
+
"public_contribution.py",
|
|
1253
1255
|
"cron_recovery.py", "runtime_power.py", "requirements.txt",
|
|
1254
1256
|
]
|
|
1255
1257
|
copied_packages = 0
|
|
@@ -1383,12 +1385,14 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
1383
1385
|
sys.executable,
|
|
1384
1386
|
"-c",
|
|
1385
1387
|
(
|
|
1388
|
+
"import json; "
|
|
1386
1389
|
"import db; "
|
|
1387
1390
|
"init_db = getattr(db, 'init_db', None); "
|
|
1388
1391
|
"init_db() if callable(init_db) else None; "
|
|
1389
1392
|
"import script_registry; "
|
|
1390
1393
|
"reconcile_scripts = getattr(script_registry, 'reconcile_personal_scripts', None); "
|
|
1391
|
-
"reconcile_scripts(dry_run=False) if callable(reconcile_scripts) else
|
|
1394
|
+
"result = reconcile_scripts(dry_run=False) if callable(reconcile_scripts) else {}; "
|
|
1395
|
+
"print(json.dumps(result))"
|
|
1392
1396
|
),
|
|
1393
1397
|
],
|
|
1394
1398
|
cwd=str(dest),
|
|
@@ -1400,6 +1404,11 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
1400
1404
|
if init_result.returncode != 0:
|
|
1401
1405
|
return False, [init_result.stderr.strip() or init_result.stdout.strip() or "runtime init failed"]
|
|
1402
1406
|
actions.append("db+personal-sync")
|
|
1407
|
+
reconcile_payload = _parse_runtime_init_payload(init_result.stdout or "")
|
|
1408
|
+
extra_actions, reconcile_message = _personal_schedule_reconcile_summary(reconcile_payload)
|
|
1409
|
+
actions.extend(extra_actions)
|
|
1410
|
+
if reconcile_message:
|
|
1411
|
+
_emit_progress(progress_fn, reconcile_message)
|
|
1403
1412
|
except Exception as e:
|
|
1404
1413
|
return False, [f"runtime init error: {e}"]
|
|
1405
1414
|
|
|
@@ -1493,6 +1502,46 @@ def _emit_progress(progress_fn, message: str) -> None:
|
|
|
1493
1502
|
pass
|
|
1494
1503
|
|
|
1495
1504
|
|
|
1505
|
+
def _parse_runtime_init_payload(stdout: str) -> dict:
|
|
1506
|
+
"""Extract the JSON payload emitted by the runtime init helper."""
|
|
1507
|
+
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
1508
|
+
for line in reversed(lines):
|
|
1509
|
+
try:
|
|
1510
|
+
payload = json.loads(line)
|
|
1511
|
+
except Exception:
|
|
1512
|
+
continue
|
|
1513
|
+
if isinstance(payload, dict):
|
|
1514
|
+
return payload
|
|
1515
|
+
return {}
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
def _personal_schedule_reconcile_summary(reconcile_result: dict) -> tuple[list[str], str | None]:
|
|
1519
|
+
"""Turn reconcile_personal_scripts() output into stable update actions."""
|
|
1520
|
+
if not isinstance(reconcile_result, dict):
|
|
1521
|
+
return [], None
|
|
1522
|
+
|
|
1523
|
+
ensured = reconcile_result.get("ensure_schedules", {})
|
|
1524
|
+
if not isinstance(ensured, dict):
|
|
1525
|
+
return [], None
|
|
1526
|
+
|
|
1527
|
+
created = len(ensured.get("created", []) or [])
|
|
1528
|
+
repaired = len(ensured.get("repaired", []) or [])
|
|
1529
|
+
invalid = len(ensured.get("invalid", []) or [])
|
|
1530
|
+
|
|
1531
|
+
actions: list[str] = []
|
|
1532
|
+
parts: list[str] = []
|
|
1533
|
+
if created or repaired:
|
|
1534
|
+
actions.append(f"personal-schedules-healed:{created + repaired}")
|
|
1535
|
+
parts.append(f"{created} created")
|
|
1536
|
+
parts.append(f"{repaired} repaired")
|
|
1537
|
+
if invalid:
|
|
1538
|
+
actions.append(f"personal-schedules-invalid:{invalid}")
|
|
1539
|
+
parts.append(f"{invalid} invalid")
|
|
1540
|
+
if not parts:
|
|
1541
|
+
return [], None
|
|
1542
|
+
return actions, "Personal schedules: " + ", ".join(parts) + "."
|
|
1543
|
+
|
|
1544
|
+
|
|
1496
1545
|
def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = True, progress_fn=None) -> dict:
|
|
1497
1546
|
src_dir, repo_dir = _resolve_sync_source()
|
|
1498
1547
|
if src_dir is None or repo_dir is None:
|
|
@@ -1599,8 +1648,12 @@ def startup_preflight(*, entrypoint: str, interactive: bool = False) -> dict:
|
|
|
1599
1648
|
_ensure_runtime_cli_wrapper()
|
|
1600
1649
|
_ensure_runtime_cli_in_shell()
|
|
1601
1650
|
init_db()
|
|
1602
|
-
reconcile_personal_scripts(dry_run=False)
|
|
1651
|
+
reconcile_result = reconcile_personal_scripts(dry_run=False)
|
|
1603
1652
|
result["actions"].append("db+personal-sync")
|
|
1653
|
+
extra_actions, reconcile_message = _personal_schedule_reconcile_summary(reconcile_result)
|
|
1654
|
+
result["actions"].extend(extra_actions)
|
|
1655
|
+
if reconcile_message:
|
|
1656
|
+
_log(reconcile_message)
|
|
1604
1657
|
except Exception as e:
|
|
1605
1658
|
result["error"] = str(e)
|
|
1606
1659
|
_write_update_summary(result)
|
package/src/cli.py
CHANGED
|
@@ -64,6 +64,123 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
64
64
|
sys.path.insert(0, str(NEXO_CODE))
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def _missing_runtime_module_message(module_name: str, exc: Exception) -> str:
|
|
68
|
+
missing = getattr(exc, "name", None) or module_name
|
|
69
|
+
return (
|
|
70
|
+
f"{module_name} is unavailable in the current runtime ({missing}). "
|
|
71
|
+
"Continuing with safe defaults so `nexo update` can repair the installation."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_runtime_power_support() -> dict:
|
|
76
|
+
try:
|
|
77
|
+
from runtime_power import (
|
|
78
|
+
ensure_power_policy_choice,
|
|
79
|
+
apply_power_policy,
|
|
80
|
+
format_power_policy_label,
|
|
81
|
+
ensure_full_disk_access_choice,
|
|
82
|
+
format_full_disk_access_label,
|
|
83
|
+
)
|
|
84
|
+
return {
|
|
85
|
+
"available": True,
|
|
86
|
+
"message": "",
|
|
87
|
+
"ensure_power_policy_choice": ensure_power_policy_choice,
|
|
88
|
+
"apply_power_policy": apply_power_policy,
|
|
89
|
+
"format_power_policy_label": format_power_policy_label,
|
|
90
|
+
"ensure_full_disk_access_choice": ensure_full_disk_access_choice,
|
|
91
|
+
"format_full_disk_access_label": format_full_disk_access_label,
|
|
92
|
+
}
|
|
93
|
+
except ImportError as exc:
|
|
94
|
+
message = _missing_runtime_module_message("runtime_power", exc)
|
|
95
|
+
|
|
96
|
+
def ensure_power_policy_choice(**kwargs):
|
|
97
|
+
return {"policy": "disabled", "prompted": False, "message": message}
|
|
98
|
+
|
|
99
|
+
def apply_power_policy(policy=None):
|
|
100
|
+
return {"ok": True, "action": "skipped", "details": [], "message": message}
|
|
101
|
+
|
|
102
|
+
def format_power_policy_label(policy):
|
|
103
|
+
return policy or "disabled"
|
|
104
|
+
|
|
105
|
+
def ensure_full_disk_access_choice(**kwargs):
|
|
106
|
+
return {"status": "unset", "prompted": False, "reasons": [], "message": message}
|
|
107
|
+
|
|
108
|
+
def format_full_disk_access_label(status):
|
|
109
|
+
return status or "unset"
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"available": False,
|
|
113
|
+
"message": message,
|
|
114
|
+
"ensure_power_policy_choice": ensure_power_policy_choice,
|
|
115
|
+
"apply_power_policy": apply_power_policy,
|
|
116
|
+
"format_power_policy_label": format_power_policy_label,
|
|
117
|
+
"ensure_full_disk_access_choice": ensure_full_disk_access_choice,
|
|
118
|
+
"format_full_disk_access_label": format_full_disk_access_label,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_public_contribution_support() -> dict:
|
|
123
|
+
try:
|
|
124
|
+
from public_contribution import (
|
|
125
|
+
ensure_public_contribution_choice,
|
|
126
|
+
format_public_contribution_label,
|
|
127
|
+
load_public_contribution_config,
|
|
128
|
+
refresh_public_contribution_state,
|
|
129
|
+
disable_public_contribution,
|
|
130
|
+
)
|
|
131
|
+
return {
|
|
132
|
+
"available": True,
|
|
133
|
+
"message": "",
|
|
134
|
+
"ensure_public_contribution_choice": ensure_public_contribution_choice,
|
|
135
|
+
"format_public_contribution_label": format_public_contribution_label,
|
|
136
|
+
"load_public_contribution_config": load_public_contribution_config,
|
|
137
|
+
"refresh_public_contribution_state": refresh_public_contribution_state,
|
|
138
|
+
"disable_public_contribution": disable_public_contribution,
|
|
139
|
+
}
|
|
140
|
+
except ImportError as exc:
|
|
141
|
+
message = _missing_runtime_module_message("public_contribution", exc)
|
|
142
|
+
|
|
143
|
+
def _default_config(config=None):
|
|
144
|
+
payload = {
|
|
145
|
+
"enabled": False,
|
|
146
|
+
"mode": "disabled",
|
|
147
|
+
"status": "unavailable",
|
|
148
|
+
"prompted": False,
|
|
149
|
+
"message": message,
|
|
150
|
+
}
|
|
151
|
+
if isinstance(config, dict):
|
|
152
|
+
payload.update(config)
|
|
153
|
+
return payload
|
|
154
|
+
|
|
155
|
+
def ensure_public_contribution_choice(**kwargs):
|
|
156
|
+
return _default_config()
|
|
157
|
+
|
|
158
|
+
def format_public_contribution_label(config=None):
|
|
159
|
+
cfg = _default_config(config)
|
|
160
|
+
if cfg.get("status") == "unavailable":
|
|
161
|
+
return "disabled (runtime repair needed)"
|
|
162
|
+
return cfg.get("mode") or "disabled"
|
|
163
|
+
|
|
164
|
+
def load_public_contribution_config():
|
|
165
|
+
return _default_config()
|
|
166
|
+
|
|
167
|
+
def refresh_public_contribution_state(config=None):
|
|
168
|
+
return _default_config(config)
|
|
169
|
+
|
|
170
|
+
def disable_public_contribution():
|
|
171
|
+
return _default_config()
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"available": False,
|
|
175
|
+
"message": message,
|
|
176
|
+
"ensure_public_contribution_choice": ensure_public_contribution_choice,
|
|
177
|
+
"format_public_contribution_label": format_public_contribution_label,
|
|
178
|
+
"load_public_contribution_config": load_public_contribution_config,
|
|
179
|
+
"refresh_public_contribution_state": refresh_public_contribution_state,
|
|
180
|
+
"disable_public_contribution": disable_public_contribution,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
67
184
|
def _scripts_list(args):
|
|
68
185
|
from db import init_db, list_personal_scripts
|
|
69
186
|
from script_registry import list_scripts, sync_personal_scripts
|
|
@@ -453,17 +570,6 @@ def _update(args):
|
|
|
453
570
|
- Packaged/runtime-only install: delegate to plugins.update handle_update()
|
|
454
571
|
"""
|
|
455
572
|
from auto_update import manual_sync_update, _resolve_sync_source
|
|
456
|
-
from runtime_power import (
|
|
457
|
-
ensure_power_policy_choice,
|
|
458
|
-
apply_power_policy,
|
|
459
|
-
format_power_policy_label,
|
|
460
|
-
ensure_full_disk_access_choice,
|
|
461
|
-
format_full_disk_access_label,
|
|
462
|
-
)
|
|
463
|
-
from public_contribution import (
|
|
464
|
-
ensure_public_contribution_choice,
|
|
465
|
-
format_public_contribution_label,
|
|
466
|
-
)
|
|
467
573
|
|
|
468
574
|
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
469
575
|
progress_messages: list[str] = []
|
|
@@ -488,10 +594,12 @@ def _update(args):
|
|
|
488
594
|
return 1
|
|
489
595
|
|
|
490
596
|
result = handle_update(progress_fn=progress)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
597
|
+
runtime_power = _load_runtime_power_support()
|
|
598
|
+
public_contribution = _load_public_contribution_support()
|
|
599
|
+
choice = runtime_power["ensure_power_policy_choice"](interactive=interactive, reason="update")
|
|
600
|
+
power_result = runtime_power["apply_power_policy"](choice.get("policy"))
|
|
601
|
+
fda_choice = runtime_power["ensure_full_disk_access_choice"](interactive=interactive, reason="update")
|
|
602
|
+
contrib_choice = public_contribution["ensure_public_contribution_choice"](interactive=interactive, reason="update")
|
|
495
603
|
if args.json:
|
|
496
604
|
print(json.dumps({
|
|
497
605
|
"mode": "packaged",
|
|
@@ -510,24 +618,26 @@ def _update(args):
|
|
|
510
618
|
else:
|
|
511
619
|
print(result)
|
|
512
620
|
if choice.get("prompted"):
|
|
513
|
-
print(f"Power policy: {format_power_policy_label(choice.get('policy'))}")
|
|
621
|
+
print(f"Power policy: {runtime_power['format_power_policy_label'](choice.get('policy'))}")
|
|
514
622
|
if power_result.get("message"):
|
|
515
623
|
print(f"Power helper: {power_result.get('message')}")
|
|
516
624
|
if fda_choice.get("prompted"):
|
|
517
|
-
print(f"Full Disk Access: {format_full_disk_access_label(fda_choice.get('status'))}")
|
|
625
|
+
print(f"Full Disk Access: {runtime_power['format_full_disk_access_label'](fda_choice.get('status'))}")
|
|
518
626
|
if fda_choice.get("message"):
|
|
519
627
|
print(f"Full Disk Access: {fda_choice.get('message')}")
|
|
520
628
|
if contrib_choice.get("prompted"):
|
|
521
|
-
print(f"Contributor mode: {format_public_contribution_label(contrib_choice)}")
|
|
629
|
+
print(f"Contributor mode: {public_contribution['format_public_contribution_label'](contrib_choice)}")
|
|
522
630
|
if contrib_choice.get("message"):
|
|
523
631
|
print(f"Contributor mode: {contrib_choice.get('message')}")
|
|
524
632
|
return 0 if "UPDATE SUCCESSFUL" in result or "Already up to date" in result else 1
|
|
525
633
|
|
|
526
|
-
choice = ensure_power_policy_choice(interactive=interactive, reason="update")
|
|
527
|
-
power_result = apply_power_policy(choice.get("policy"))
|
|
528
|
-
fda_choice = ensure_full_disk_access_choice(interactive=interactive, reason="update")
|
|
529
|
-
contrib_choice = ensure_public_contribution_choice(interactive=interactive, reason="update")
|
|
530
634
|
result = manual_sync_update(interactive=interactive, allow_source_pull=True, progress_fn=progress)
|
|
635
|
+
runtime_power = _load_runtime_power_support()
|
|
636
|
+
public_contribution = _load_public_contribution_support()
|
|
637
|
+
choice = runtime_power["ensure_power_policy_choice"](interactive=interactive, reason="update")
|
|
638
|
+
power_result = runtime_power["apply_power_policy"](choice.get("policy"))
|
|
639
|
+
fda_choice = runtime_power["ensure_full_disk_access_choice"](interactive=interactive, reason="update")
|
|
640
|
+
contrib_choice = public_contribution["ensure_public_contribution_choice"](interactive=interactive, reason="update")
|
|
531
641
|
result["power_policy"] = choice.get("policy")
|
|
532
642
|
result["power_action"] = power_result.get("action")
|
|
533
643
|
result["power_details"] = power_result.get("details")
|
|
@@ -551,18 +661,35 @@ def _update(args):
|
|
|
551
661
|
f" {result.get('packages', 0)} packages, {result.get('files', 0)} files synced from "
|
|
552
662
|
f"{result.get('source', src_dir)}"
|
|
553
663
|
)
|
|
664
|
+
healed = 0
|
|
665
|
+
invalid = 0
|
|
666
|
+
for action in result.get("actions", []):
|
|
667
|
+
if action.startswith("personal-schedules-healed:"):
|
|
668
|
+
try:
|
|
669
|
+
healed += int(action.split(":", 1)[1])
|
|
670
|
+
except ValueError:
|
|
671
|
+
pass
|
|
672
|
+
elif action.startswith("personal-schedules-invalid:"):
|
|
673
|
+
try:
|
|
674
|
+
invalid += int(action.split(":", 1)[1])
|
|
675
|
+
except ValueError:
|
|
676
|
+
pass
|
|
677
|
+
if healed:
|
|
678
|
+
print(f" Personal schedules: self-healed {healed}")
|
|
679
|
+
if invalid:
|
|
680
|
+
print(f" Personal schedules: {invalid} declarations need review")
|
|
554
681
|
if result.get("pulled_source"):
|
|
555
682
|
print(" Source repo: pulled latest fast-forward before sync")
|
|
556
683
|
if choice.get("prompted"):
|
|
557
|
-
print(f" Power policy: {format_power_policy_label(choice.get('policy'))}")
|
|
684
|
+
print(f" Power policy: {runtime_power['format_power_policy_label'](choice.get('policy'))}")
|
|
558
685
|
if power_result.get("message"):
|
|
559
686
|
print(f" Power helper: {power_result.get('message')}")
|
|
560
687
|
if fda_choice.get("prompted"):
|
|
561
|
-
print(f" Full Disk Access: {format_full_disk_access_label(fda_choice.get('status'))}")
|
|
688
|
+
print(f" Full Disk Access: {runtime_power['format_full_disk_access_label'](fda_choice.get('status'))}")
|
|
562
689
|
if fda_choice.get("message"):
|
|
563
690
|
print(f" Full Disk Access: {fda_choice.get('message')}")
|
|
564
691
|
if contrib_choice.get("prompted"):
|
|
565
|
-
print(f" Contributor mode: {format_public_contribution_label(contrib_choice)}")
|
|
692
|
+
print(f" Contributor mode: {public_contribution['format_public_contribution_label'](contrib_choice)}")
|
|
566
693
|
if contrib_choice.get("message"):
|
|
567
694
|
print(f" Contributor mode: {contrib_choice.get('message')}")
|
|
568
695
|
else:
|
|
@@ -582,29 +709,29 @@ def _clients_sync(args):
|
|
|
582
709
|
|
|
583
710
|
|
|
584
711
|
def _contributor_status(args):
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
load_public_contribution_config
|
|
588
|
-
refresh_public_contribution_state,
|
|
712
|
+
public_contribution = _load_public_contribution_support()
|
|
713
|
+
config = public_contribution["refresh_public_contribution_state"](
|
|
714
|
+
public_contribution["load_public_contribution_config"]()
|
|
589
715
|
)
|
|
590
|
-
|
|
591
|
-
config = refresh_public_contribution_state(load_public_contribution_config())
|
|
592
716
|
payload = {
|
|
593
717
|
"enabled": bool(config.get("enabled")),
|
|
594
718
|
"mode": config.get("mode"),
|
|
595
719
|
"status": config.get("status"),
|
|
596
|
-
"label": format_public_contribution_label(config),
|
|
720
|
+
"label": public_contribution["format_public_contribution_label"](config),
|
|
597
721
|
"github_user": config.get("github_user"),
|
|
598
722
|
"fork_repo": config.get("fork_repo"),
|
|
599
723
|
"active_pr_url": config.get("active_pr_url"),
|
|
600
724
|
"active_branch": config.get("active_branch"),
|
|
601
725
|
"cooldown_until": config.get("cooldown_until"),
|
|
602
726
|
"last_result": config.get("last_result"),
|
|
727
|
+
"message": config.get("message") or public_contribution.get("message"),
|
|
603
728
|
}
|
|
604
729
|
if args.json:
|
|
605
730
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
606
731
|
else:
|
|
607
732
|
print(f"Contributor mode: {payload['label']}")
|
|
733
|
+
if payload["message"]:
|
|
734
|
+
print(f" {payload['message']}")
|
|
608
735
|
if payload["github_user"]:
|
|
609
736
|
print(f" GitHub user: {payload['github_user']}")
|
|
610
737
|
if payload["fork_repo"]:
|
|
@@ -619,30 +746,40 @@ def _contributor_status(args):
|
|
|
619
746
|
|
|
620
747
|
|
|
621
748
|
def _contributor_on(args):
|
|
622
|
-
|
|
749
|
+
public_contribution = _load_public_contribution_support()
|
|
623
750
|
|
|
624
751
|
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
625
752
|
if not interactive:
|
|
626
753
|
print("Contributor mode requires an interactive terminal to confirm GitHub Draft PR consent.", file=sys.stderr)
|
|
627
754
|
return 1
|
|
628
|
-
|
|
755
|
+
if not public_contribution["available"]:
|
|
756
|
+
print(public_contribution["message"], file=sys.stderr)
|
|
757
|
+
return 1
|
|
758
|
+
config = public_contribution["ensure_public_contribution_choice"](
|
|
759
|
+
interactive=True,
|
|
760
|
+
reason="contributor",
|
|
761
|
+
force_prompt=True,
|
|
762
|
+
)
|
|
629
763
|
if args.json:
|
|
630
764
|
print(json.dumps(config, indent=2, ensure_ascii=False))
|
|
631
765
|
else:
|
|
632
|
-
print(f"Contributor mode: {format_public_contribution_label(config)}")
|
|
766
|
+
print(f"Contributor mode: {public_contribution['format_public_contribution_label'](config)}")
|
|
633
767
|
if config.get("message"):
|
|
634
768
|
print(config.get("message"))
|
|
635
769
|
return 0 if config.get("mode") == "draft_prs" else 1
|
|
636
770
|
|
|
637
771
|
|
|
638
772
|
def _contributor_off(args):
|
|
639
|
-
|
|
773
|
+
public_contribution = _load_public_contribution_support()
|
|
640
774
|
|
|
641
|
-
|
|
775
|
+
if not public_contribution["available"]:
|
|
776
|
+
print(public_contribution["message"], file=sys.stderr)
|
|
777
|
+
return 1
|
|
778
|
+
config = public_contribution["disable_public_contribution"]()
|
|
642
779
|
if args.json:
|
|
643
780
|
print(json.dumps(config, indent=2, ensure_ascii=False))
|
|
644
781
|
else:
|
|
645
|
-
print(f"Contributor mode: {format_public_contribution_label(config)}")
|
|
782
|
+
print(f"Contributor mode: {public_contribution['format_public_contribution_label'](config)}")
|
|
646
783
|
return 0
|
|
647
784
|
|
|
648
785
|
|
|
@@ -120,6 +120,30 @@ def _acquire_lock():
|
|
|
120
120
|
return handle
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def _heal_personal_schedules() -> dict:
|
|
124
|
+
"""Recreate declared personal schedules before catch-up checks missed windows."""
|
|
125
|
+
summary = {"created": 0, "repaired": 0, "invalid": 0, "error": ""}
|
|
126
|
+
try:
|
|
127
|
+
from script_registry import reconcile_personal_scripts
|
|
128
|
+
|
|
129
|
+
result = reconcile_personal_scripts(dry_run=False)
|
|
130
|
+
ensured = result.get("ensure_schedules", {})
|
|
131
|
+
summary["created"] = len(ensured.get("created", []))
|
|
132
|
+
summary["repaired"] = len(ensured.get("repaired", []))
|
|
133
|
+
summary["invalid"] = len(ensured.get("invalid", []))
|
|
134
|
+
if summary["created"] or summary["repaired"]:
|
|
135
|
+
log(
|
|
136
|
+
"Repaired declared personal schedules before catch-up: "
|
|
137
|
+
f"{summary['created']} created, {summary['repaired']} repaired."
|
|
138
|
+
)
|
|
139
|
+
if summary["invalid"]:
|
|
140
|
+
log(f"WARNING: {summary['invalid']} declared personal schedules are invalid.")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
summary["error"] = str(e)
|
|
143
|
+
log(f"Personal schedule self-heal skipped: {e}")
|
|
144
|
+
return summary
|
|
145
|
+
|
|
146
|
+
|
|
123
147
|
def run_task(candidate: dict, state: dict) -> bool:
|
|
124
148
|
"""Execute a task and update state."""
|
|
125
149
|
name = candidate["cron_id"]
|
|
@@ -172,6 +196,7 @@ def main():
|
|
|
172
196
|
log("Catch-Up already running; skipping overlapping invocation.")
|
|
173
197
|
return
|
|
174
198
|
|
|
199
|
+
_heal_personal_schedules()
|
|
175
200
|
state = load_state()
|
|
176
201
|
tasks = catchup_candidates()
|
|
177
202
|
|