nexo-brain 5.3.19 → 5.3.21
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 +52 -10
- package/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Shared protocol-discipline settings loaded from calibration.json."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_PROTOCOL_STRICTNESS = "lenient"
|
|
11
|
+
VALID_PROTOCOL_STRICTNESS = {"lenient", "strict", "learning"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _nexo_home() -> Path:
|
|
15
|
+
return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _calibration_path() -> Path:
|
|
19
|
+
return _nexo_home() / "brain" / "calibration.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normalize_protocol_strictness(value: str | None) -> str:
|
|
23
|
+
candidate = str(value or "").strip().lower()
|
|
24
|
+
aliases = {
|
|
25
|
+
"default": "lenient",
|
|
26
|
+
"normal": "lenient",
|
|
27
|
+
"off": "lenient",
|
|
28
|
+
"warn": "lenient",
|
|
29
|
+
"soft": "lenient",
|
|
30
|
+
"hard": "strict",
|
|
31
|
+
"guided": "learning",
|
|
32
|
+
}
|
|
33
|
+
candidate = aliases.get(candidate, candidate)
|
|
34
|
+
if candidate in VALID_PROTOCOL_STRICTNESS:
|
|
35
|
+
return candidate
|
|
36
|
+
return DEFAULT_PROTOCOL_STRICTNESS
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_protocol_strictness() -> str:
|
|
40
|
+
env_override = os.environ.get("NEXO_PROTOCOL_STRICTNESS", "").strip()
|
|
41
|
+
if env_override:
|
|
42
|
+
return normalize_protocol_strictness(env_override)
|
|
43
|
+
|
|
44
|
+
cal_path = _calibration_path()
|
|
45
|
+
if not cal_path.is_file():
|
|
46
|
+
return DEFAULT_PROTOCOL_STRICTNESS
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
payload = json.loads(cal_path.read_text())
|
|
50
|
+
except Exception:
|
|
51
|
+
return DEFAULT_PROTOCOL_STRICTNESS
|
|
52
|
+
|
|
53
|
+
preferences = payload.get("preferences") if isinstance(payload, dict) else {}
|
|
54
|
+
candidate = ""
|
|
55
|
+
if isinstance(preferences, dict):
|
|
56
|
+
candidate = str(preferences.get("protocol_strictness", "") or "").strip()
|
|
57
|
+
if not candidate and isinstance(payload, dict):
|
|
58
|
+
candidate = str(payload.get("protocol_strictness", "") or "").strip()
|
|
59
|
+
return normalize_protocol_strictness(candidate)
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""Public contribution preferences and GitHub PR workflow helpers.
|
|
3
|
+
|
|
4
|
+
This module manages the opt-in "public core evolution" mode:
|
|
5
|
+
- user consent and persisted config in schedule.json
|
|
6
|
+
- GitHub auth/fork detection
|
|
7
|
+
- active Draft PR pause/resume lifecycle
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import socket
|
|
16
|
+
import subprocess
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from runtime_power import load_schedule_config, save_schedule_config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
UPSTREAM_REPO = "wazionapps/nexo"
|
|
24
|
+
CONFIG_KEY = "public_contribution"
|
|
25
|
+
CONSENT_VERSION = 1
|
|
26
|
+
MODE_UNSET = "unset"
|
|
27
|
+
MODE_OFF = "off"
|
|
28
|
+
MODE_DRAFT_PRS = "draft_prs"
|
|
29
|
+
MODE_PENDING_AUTH = "pending_auth"
|
|
30
|
+
STATUS_UNSET = "unset"
|
|
31
|
+
STATUS_ACTIVE = "active"
|
|
32
|
+
STATUS_PENDING_AUTH = "pending_auth"
|
|
33
|
+
STATUS_PAUSED_OPEN_PR = "paused_open_pr"
|
|
34
|
+
STATUS_COOLDOWN = "cooldown"
|
|
35
|
+
STATUS_OFF = "off"
|
|
36
|
+
VALID_MODES = {MODE_UNSET, MODE_OFF, MODE_DRAFT_PRS, MODE_PENDING_AUTH}
|
|
37
|
+
VALID_STATUSES = {
|
|
38
|
+
STATUS_UNSET,
|
|
39
|
+
STATUS_ACTIVE,
|
|
40
|
+
STATUS_PENDING_AUTH,
|
|
41
|
+
STATUS_PAUSED_OPEN_PR,
|
|
42
|
+
STATUS_COOLDOWN,
|
|
43
|
+
STATUS_OFF,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
47
|
+
CONTRIB_ROOT = NEXO_HOME / "contrib" / "public-core"
|
|
48
|
+
CONTRIB_REPO_DIR = CONTRIB_ROOT / "repo"
|
|
49
|
+
CONTRIB_WORKTREES_DIR = CONTRIB_ROOT / "worktrees"
|
|
50
|
+
CONTRIB_ARTIFACTS_DIR = NEXO_HOME / "operations" / "public-contrib"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _utcnow() -> datetime:
|
|
54
|
+
return datetime.now(timezone.utc)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _machine_id() -> str:
|
|
58
|
+
raw = socket.gethostname().strip().lower() or "nexo-machine"
|
|
59
|
+
return re.sub(r"[^a-z0-9._-]+", "-", raw).strip("-") or "nexo-machine"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _default_public_contribution() -> dict:
|
|
63
|
+
return {
|
|
64
|
+
"enabled": False,
|
|
65
|
+
"mode": MODE_UNSET,
|
|
66
|
+
"consent_version": CONSENT_VERSION,
|
|
67
|
+
"github_user": "",
|
|
68
|
+
"upstream_repo": UPSTREAM_REPO,
|
|
69
|
+
"fork_repo": "",
|
|
70
|
+
"machine_id": _machine_id(),
|
|
71
|
+
"active_pr_url": "",
|
|
72
|
+
"active_pr_number": None,
|
|
73
|
+
"active_branch": "",
|
|
74
|
+
"status": STATUS_UNSET,
|
|
75
|
+
"cooldown_until": "",
|
|
76
|
+
"last_run_at": "",
|
|
77
|
+
"last_result": "",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_public_contribution_config(config: dict | None) -> dict:
|
|
82
|
+
merged = dict(_default_public_contribution())
|
|
83
|
+
if isinstance(config, dict):
|
|
84
|
+
merged.update(config)
|
|
85
|
+
merged["mode"] = str(merged.get("mode") or MODE_UNSET).strip().lower()
|
|
86
|
+
if merged["mode"] not in VALID_MODES:
|
|
87
|
+
merged["mode"] = MODE_UNSET
|
|
88
|
+
merged["status"] = str(merged.get("status") or STATUS_UNSET).strip().lower()
|
|
89
|
+
if merged["status"] not in VALID_STATUSES:
|
|
90
|
+
merged["status"] = STATUS_UNSET
|
|
91
|
+
merged["enabled"] = bool(merged.get("enabled", False))
|
|
92
|
+
merged["consent_version"] = CONSENT_VERSION
|
|
93
|
+
merged["upstream_repo"] = str(merged.get("upstream_repo") or UPSTREAM_REPO)
|
|
94
|
+
merged["github_user"] = str(merged.get("github_user") or "").strip()
|
|
95
|
+
merged["fork_repo"] = str(merged.get("fork_repo") or "").strip()
|
|
96
|
+
merged["machine_id"] = str(merged.get("machine_id") or _machine_id()).strip() or _machine_id()
|
|
97
|
+
merged["active_pr_url"] = str(merged.get("active_pr_url") or "").strip()
|
|
98
|
+
merged["active_pr_number"] = merged.get("active_pr_number")
|
|
99
|
+
if merged["active_pr_number"] in {"", 0, "0"}:
|
|
100
|
+
merged["active_pr_number"] = None
|
|
101
|
+
merged["active_branch"] = str(merged.get("active_branch") or "").strip()
|
|
102
|
+
merged["cooldown_until"] = str(merged.get("cooldown_until") or "").strip()
|
|
103
|
+
merged["last_run_at"] = str(merged.get("last_run_at") or "").strip()
|
|
104
|
+
merged["last_result"] = str(merged.get("last_result") or "").strip()
|
|
105
|
+
return merged
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_public_contribution_config(schedule: dict | None = None) -> dict:
|
|
109
|
+
schedule = schedule or load_schedule_config()
|
|
110
|
+
return normalize_public_contribution_config(schedule.get(CONFIG_KEY))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_public_contribution_config(config: dict) -> dict:
|
|
114
|
+
schedule = load_schedule_config()
|
|
115
|
+
schedule[CONFIG_KEY] = normalize_public_contribution_config(config)
|
|
116
|
+
save_schedule_config(schedule)
|
|
117
|
+
return schedule[CONFIG_KEY]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _gh(*args: str, cwd: Path | None = None, timeout: int = 20) -> subprocess.CompletedProcess:
|
|
121
|
+
env = os.environ.copy()
|
|
122
|
+
token = (
|
|
123
|
+
str(env.get("GH_TOKEN") or env.get("GITHUB_TOKEN") or "").strip()
|
|
124
|
+
or _github_token_from_credentials()
|
|
125
|
+
)
|
|
126
|
+
if token:
|
|
127
|
+
env["GH_TOKEN"] = token
|
|
128
|
+
return subprocess.run(
|
|
129
|
+
["gh", *args],
|
|
130
|
+
cwd=str(cwd) if cwd else None,
|
|
131
|
+
capture_output=True,
|
|
132
|
+
text=True,
|
|
133
|
+
timeout=timeout,
|
|
134
|
+
env=env,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _github_token_from_credentials() -> str:
|
|
139
|
+
try:
|
|
140
|
+
from db import get_credential
|
|
141
|
+
except Exception:
|
|
142
|
+
return ""
|
|
143
|
+
for key in ("token", "gh_token", "github_token"):
|
|
144
|
+
try:
|
|
145
|
+
matches = get_credential("github", key)
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
for item in matches or []:
|
|
149
|
+
value = str(item.get("value") or "").strip()
|
|
150
|
+
if value:
|
|
151
|
+
return value
|
|
152
|
+
return ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def github_auth_status() -> dict:
|
|
156
|
+
if not shutil.which("gh"):
|
|
157
|
+
return {"ok": False, "message": "GitHub CLI not found.", "login": "", "code": "gh_missing"}
|
|
158
|
+
try:
|
|
159
|
+
result = _gh("api", "user", timeout=20)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return {"ok": False, "message": str(e), "login": "", "code": "gh_error"}
|
|
162
|
+
if result.returncode != 0:
|
|
163
|
+
message = (result.stderr or result.stdout).strip()
|
|
164
|
+
lowered = message.lower()
|
|
165
|
+
code = "auth_missing"
|
|
166
|
+
if "keychain" in lowered:
|
|
167
|
+
code = "keychain_blocked"
|
|
168
|
+
elif "token" in lowered or "authentication" in lowered or "login" in lowered:
|
|
169
|
+
code = "auth_missing"
|
|
170
|
+
return {"ok": False, "message": message, "login": "", "code": code}
|
|
171
|
+
try:
|
|
172
|
+
payload = json.loads(result.stdout or "{}")
|
|
173
|
+
login = str(payload.get("login") or "").strip()
|
|
174
|
+
except Exception:
|
|
175
|
+
login = ""
|
|
176
|
+
return {"ok": bool(login), "message": "", "login": login, "code": "ok" if login else "auth_missing"}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def ensure_fork(login: str) -> dict:
|
|
180
|
+
if not login:
|
|
181
|
+
return {"ok": False, "message": "Missing GitHub login.", "fork_repo": "", "code": "missing_login"}
|
|
182
|
+
fork_repo = f"{login}/nexo"
|
|
183
|
+
if not shutil.which("gh"):
|
|
184
|
+
return {"ok": False, "message": "GitHub CLI not found.", "fork_repo": "", "code": "gh_missing"}
|
|
185
|
+
try:
|
|
186
|
+
check = _gh("repo", "view", fork_repo, "--json", "nameWithOwner", timeout=20)
|
|
187
|
+
if check.returncode == 0:
|
|
188
|
+
return {"ok": True, "message": "", "fork_repo": fork_repo, "code": "ok"}
|
|
189
|
+
create = _gh("repo", "fork", UPSTREAM_REPO, "--clone=false", "--remote=false", timeout=60)
|
|
190
|
+
if create.returncode == 0:
|
|
191
|
+
return {"ok": True, "message": "", "fork_repo": fork_repo, "code": "ok"}
|
|
192
|
+
return {
|
|
193
|
+
"ok": False,
|
|
194
|
+
"message": (create.stderr or create.stdout or check.stderr or check.stdout).strip(),
|
|
195
|
+
"fork_repo": "",
|
|
196
|
+
"code": "fork_unavailable",
|
|
197
|
+
}
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return {"ok": False, "message": str(e), "fork_repo": "", "code": "fork_error"}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _set_pending_auth(config: dict, message: str) -> dict:
|
|
203
|
+
config["status"] = STATUS_PENDING_AUTH
|
|
204
|
+
config["last_result"] = f"pending_auth:{message}"
|
|
205
|
+
save_public_contribution_config(config)
|
|
206
|
+
config["message"] = message
|
|
207
|
+
return config
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_iso(ts: str | None) -> datetime | None:
|
|
211
|
+
value = str(ts or "").strip()
|
|
212
|
+
if not value:
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
if value.endswith("Z"):
|
|
216
|
+
value = value[:-1] + "+00:00"
|
|
217
|
+
dt = datetime.fromisoformat(value)
|
|
218
|
+
if dt.tzinfo is None:
|
|
219
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
220
|
+
return dt.astimezone(timezone.utc)
|
|
221
|
+
except Exception:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def format_public_contribution_label(config: dict | None = None) -> str:
|
|
226
|
+
cfg = normalize_public_contribution_config(config)
|
|
227
|
+
if cfg["mode"] == MODE_DRAFT_PRS:
|
|
228
|
+
return f"draft_prs ({cfg['status']})"
|
|
229
|
+
return cfg["mode"]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def prompt_for_public_contribution(
|
|
233
|
+
*,
|
|
234
|
+
reason: str = "update",
|
|
235
|
+
input_fn=input,
|
|
236
|
+
output_fn=print,
|
|
237
|
+
) -> dict:
|
|
238
|
+
output_fn("[NEXO] Public contribution mode is optional and opt-in.")
|
|
239
|
+
output_fn(
|
|
240
|
+
"[NEXO] If enabled, this machine may prepare core improvements in an isolated checkout "
|
|
241
|
+
"and open a Draft PR to the public NEXO repository."
|
|
242
|
+
)
|
|
243
|
+
output_fn("[NEXO] It never auto-merges, and it stays paused while that PR remains open.")
|
|
244
|
+
output_fn("[NEXO] It must never publish personal scripts, local runtime data, logs, prompts, or secrets.")
|
|
245
|
+
|
|
246
|
+
while True:
|
|
247
|
+
answer = str(
|
|
248
|
+
input_fn("[NEXO] Enable public contribution via Draft PRs on this machine? [y]es / [n]o / [l]ater: ")
|
|
249
|
+
).strip().lower()
|
|
250
|
+
if answer in {"y", "yes"}:
|
|
251
|
+
auth = github_auth_status()
|
|
252
|
+
if not auth.get("ok"):
|
|
253
|
+
return {
|
|
254
|
+
"mode": MODE_PENDING_AUTH,
|
|
255
|
+
"status": STATUS_PENDING_AUTH,
|
|
256
|
+
"enabled": False,
|
|
257
|
+
"message": auth.get("message") or "GitHub authentication is missing.",
|
|
258
|
+
"github_user": "",
|
|
259
|
+
"fork_repo": "",
|
|
260
|
+
"prompted": True,
|
|
261
|
+
}
|
|
262
|
+
fork = ensure_fork(auth.get("login", ""))
|
|
263
|
+
if not fork.get("ok"):
|
|
264
|
+
return {
|
|
265
|
+
"mode": MODE_PENDING_AUTH,
|
|
266
|
+
"status": STATUS_PENDING_AUTH,
|
|
267
|
+
"enabled": False,
|
|
268
|
+
"message": fork.get("message") or "Could not ensure a GitHub fork.",
|
|
269
|
+
"github_user": auth.get("login", ""),
|
|
270
|
+
"fork_repo": "",
|
|
271
|
+
"prompted": True,
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
"mode": MODE_DRAFT_PRS,
|
|
275
|
+
"status": STATUS_ACTIVE,
|
|
276
|
+
"enabled": True,
|
|
277
|
+
"message": "",
|
|
278
|
+
"github_user": auth.get("login", ""),
|
|
279
|
+
"fork_repo": fork.get("fork_repo", ""),
|
|
280
|
+
"prompted": True,
|
|
281
|
+
}
|
|
282
|
+
if answer in {"n", "no"}:
|
|
283
|
+
return {
|
|
284
|
+
"mode": MODE_OFF,
|
|
285
|
+
"status": STATUS_OFF,
|
|
286
|
+
"enabled": False,
|
|
287
|
+
"message": "",
|
|
288
|
+
"github_user": "",
|
|
289
|
+
"fork_repo": "",
|
|
290
|
+
"prompted": True,
|
|
291
|
+
}
|
|
292
|
+
if answer in {"l", "later", ""}:
|
|
293
|
+
return {
|
|
294
|
+
"mode": MODE_UNSET,
|
|
295
|
+
"status": STATUS_UNSET,
|
|
296
|
+
"enabled": False,
|
|
297
|
+
"message": "",
|
|
298
|
+
"github_user": "",
|
|
299
|
+
"fork_repo": "",
|
|
300
|
+
"prompted": True,
|
|
301
|
+
}
|
|
302
|
+
output_fn("[NEXO] Reply with yes, no, or later.")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def ensure_public_contribution_choice(
|
|
306
|
+
*,
|
|
307
|
+
interactive: bool,
|
|
308
|
+
reason: str = "update",
|
|
309
|
+
input_fn=input,
|
|
310
|
+
output_fn=print,
|
|
311
|
+
force_prompt: bool = False,
|
|
312
|
+
) -> dict:
|
|
313
|
+
config = load_public_contribution_config()
|
|
314
|
+
prompted = False
|
|
315
|
+
if interactive and (force_prompt or config["mode"] == MODE_UNSET):
|
|
316
|
+
prompted = True
|
|
317
|
+
result = prompt_for_public_contribution(reason=reason, input_fn=input_fn, output_fn=output_fn)
|
|
318
|
+
config.update({
|
|
319
|
+
"enabled": result["enabled"],
|
|
320
|
+
"mode": result["mode"],
|
|
321
|
+
"status": result["status"],
|
|
322
|
+
"github_user": result["github_user"],
|
|
323
|
+
"fork_repo": result["fork_repo"],
|
|
324
|
+
"machine_id": config.get("machine_id") or _machine_id(),
|
|
325
|
+
})
|
|
326
|
+
if result["mode"] != MODE_DRAFT_PRS:
|
|
327
|
+
config["active_pr_url"] = ""
|
|
328
|
+
config["active_pr_number"] = None
|
|
329
|
+
config["active_branch"] = ""
|
|
330
|
+
save_public_contribution_config(config)
|
|
331
|
+
config = load_public_contribution_config()
|
|
332
|
+
config["message"] = result.get("message", "")
|
|
333
|
+
else:
|
|
334
|
+
config["message"] = ""
|
|
335
|
+
config["prompted"] = prompted
|
|
336
|
+
return config
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def refresh_public_contribution_state(config: dict | None = None) -> dict:
|
|
340
|
+
config = normalize_public_contribution_config(config or load_public_contribution_config())
|
|
341
|
+
if config["mode"] != MODE_DRAFT_PRS:
|
|
342
|
+
return config
|
|
343
|
+
|
|
344
|
+
if config.get("active_pr_number") and config.get("active_pr_url"):
|
|
345
|
+
try:
|
|
346
|
+
result = _gh(
|
|
347
|
+
"pr",
|
|
348
|
+
"view",
|
|
349
|
+
str(config["active_pr_number"]),
|
|
350
|
+
"--repo",
|
|
351
|
+
config["upstream_repo"],
|
|
352
|
+
"--json",
|
|
353
|
+
"state,isDraft,url,mergedAt,closed",
|
|
354
|
+
timeout=20,
|
|
355
|
+
)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
config["last_result"] = f"pr_status_error:{e}"
|
|
358
|
+
save_public_contribution_config(config)
|
|
359
|
+
return config
|
|
360
|
+
if result.returncode == 0:
|
|
361
|
+
payload = json.loads(result.stdout or "{}")
|
|
362
|
+
if payload.get("state") == "OPEN" and payload.get("isDraft", False):
|
|
363
|
+
config["status"] = STATUS_PAUSED_OPEN_PR
|
|
364
|
+
save_public_contribution_config(config)
|
|
365
|
+
return config
|
|
366
|
+
resolution = "merged" if payload.get("mergedAt") else "closed"
|
|
367
|
+
config["active_pr_url"] = ""
|
|
368
|
+
config["active_pr_number"] = None
|
|
369
|
+
config["active_branch"] = ""
|
|
370
|
+
config["cooldown_until"] = ""
|
|
371
|
+
config["status"] = STATUS_ACTIVE
|
|
372
|
+
config["last_result"] = f"resolved_pr:{resolution}:{payload.get('url') or ''}".rstrip(":")
|
|
373
|
+
save_public_contribution_config(config)
|
|
374
|
+
return config
|
|
375
|
+
return _set_pending_auth(
|
|
376
|
+
config,
|
|
377
|
+
f"GitHub Draft PR status check failed: {(result.stderr or result.stdout).strip() or 'unknown gh error'}",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
cooldown_until = _parse_iso(config.get("cooldown_until"))
|
|
381
|
+
if cooldown_until and cooldown_until > _utcnow():
|
|
382
|
+
# Legacy installs used a post-merge/close cooldown that blocked the next
|
|
383
|
+
# public contribution cycle even after maintainers resolved the Draft PR.
|
|
384
|
+
# Public contribution should pause only while the PR is still open.
|
|
385
|
+
config["cooldown_until"] = ""
|
|
386
|
+
config["status"] = STATUS_ACTIVE
|
|
387
|
+
save_public_contribution_config(config)
|
|
388
|
+
return config
|
|
389
|
+
|
|
390
|
+
auth = github_auth_status()
|
|
391
|
+
if not auth.get("ok"):
|
|
392
|
+
return _set_pending_auth(
|
|
393
|
+
config,
|
|
394
|
+
auth.get("message") or "GitHub authentication is missing for public contribution.",
|
|
395
|
+
)
|
|
396
|
+
login = str(auth.get("login") or "").strip()
|
|
397
|
+
configured_login = str(config.get("github_user") or "").strip()
|
|
398
|
+
if configured_login and login and configured_login.lower() != login.lower():
|
|
399
|
+
return _set_pending_auth(
|
|
400
|
+
config,
|
|
401
|
+
f"GitHub login drift detected: configured {configured_login}, current {login}. Reconfirm public contribution credentials.",
|
|
402
|
+
)
|
|
403
|
+
if login and not configured_login:
|
|
404
|
+
config["github_user"] = login
|
|
405
|
+
|
|
406
|
+
if not str(config.get("fork_repo") or "").strip():
|
|
407
|
+
fork = ensure_fork(login)
|
|
408
|
+
if not fork.get("ok"):
|
|
409
|
+
return _set_pending_auth(
|
|
410
|
+
config,
|
|
411
|
+
fork.get("message") or "GitHub fork setup is missing for public contribution.",
|
|
412
|
+
)
|
|
413
|
+
config["fork_repo"] = str(fork.get("fork_repo") or "").strip()
|
|
414
|
+
|
|
415
|
+
if config["mode"] == MODE_PENDING_AUTH:
|
|
416
|
+
config["status"] = STATUS_PENDING_AUTH
|
|
417
|
+
else:
|
|
418
|
+
config["status"] = STATUS_ACTIVE
|
|
419
|
+
save_public_contribution_config(config)
|
|
420
|
+
return config
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def can_run_public_contribution(config: dict | None = None) -> tuple[bool, str, dict]:
|
|
424
|
+
config = refresh_public_contribution_state(config)
|
|
425
|
+
if config["mode"] == MODE_PENDING_AUTH or config["status"] == STATUS_PENDING_AUTH:
|
|
426
|
+
detail = str(config.get("message") or config.get("last_result") or "").strip()
|
|
427
|
+
return False, detail or "github authentication or fork setup is pending", config
|
|
428
|
+
if config["mode"] != MODE_DRAFT_PRS or not config.get("enabled"):
|
|
429
|
+
return False, "public contribution is disabled", config
|
|
430
|
+
if config["status"] == STATUS_PAUSED_OPEN_PR:
|
|
431
|
+
return False, "an active Draft PR is already open for this machine", config
|
|
432
|
+
return True, "", config
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def mark_public_contribution_result(*, result: str, config: dict | None = None) -> dict:
|
|
436
|
+
config = normalize_public_contribution_config(config or load_public_contribution_config())
|
|
437
|
+
config["last_run_at"] = _utcnow().isoformat()
|
|
438
|
+
config["last_result"] = str(result or "")
|
|
439
|
+
save_public_contribution_config(config)
|
|
440
|
+
return config
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def mark_active_pr(*, pr_url: str, pr_number: int | None, branch: str, config: dict | None = None) -> dict:
|
|
444
|
+
config = normalize_public_contribution_config(config or load_public_contribution_config())
|
|
445
|
+
config["active_pr_url"] = pr_url
|
|
446
|
+
config["active_pr_number"] = pr_number
|
|
447
|
+
config["active_branch"] = branch
|
|
448
|
+
config["status"] = STATUS_PAUSED_OPEN_PR
|
|
449
|
+
config["last_run_at"] = _utcnow().isoformat()
|
|
450
|
+
config["last_result"] = "draft_pr_created"
|
|
451
|
+
save_public_contribution_config(config)
|
|
452
|
+
return config
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def disable_public_contribution() -> dict:
|
|
456
|
+
config = load_public_contribution_config()
|
|
457
|
+
config.update({
|
|
458
|
+
"enabled": False,
|
|
459
|
+
"mode": MODE_OFF,
|
|
460
|
+
"status": STATUS_OFF,
|
|
461
|
+
"active_pr_url": "",
|
|
462
|
+
"active_pr_number": None,
|
|
463
|
+
"active_branch": "",
|
|
464
|
+
})
|
|
465
|
+
save_public_contribution_config(config)
|
|
466
|
+
return config
|