nexo-brain 5.3.26 → 5.3.28

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.
Files changed (212) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/hook_guardrails.py +44 -0
  4. package/src/server.py +3 -0
  5. package/src/tools_sessions.py +6 -1
  6. package/src/dashboard/static/favicon 2.svg +0 -32
  7. package/src/dashboard/static/nexo-logo 2.png +0 -0
  8. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  9. package/src/dashboard/static/style 2.css +0 -2458
  10. package/src/dashboard/templates/adaptive 2.html +0 -118
  11. package/src/dashboard/templates/artifacts 2.html +0 -133
  12. package/src/dashboard/templates/backups 2.html +0 -136
  13. package/src/dashboard/templates/base 2.html +0 -417
  14. package/src/dashboard/templates/calendar 2.html +0 -591
  15. package/src/dashboard/templates/chat 2.html +0 -356
  16. package/src/dashboard/templates/claims 2.html +0 -259
  17. package/src/dashboard/templates/cortex 2.html +0 -321
  18. package/src/dashboard/templates/credentials 2.html +0 -128
  19. package/src/dashboard/templates/crons 2.html +0 -370
  20. package/src/dashboard/templates/dashboard 2.html +0 -494
  21. package/src/dashboard/templates/dreams 2.html +0 -252
  22. package/src/dashboard/templates/email 2.html +0 -160
  23. package/src/dashboard/templates/evolution 2.html +0 -189
  24. package/src/dashboard/templates/feed 2.html +0 -249
  25. package/src/dashboard/templates/followup_health 2.html +0 -170
  26. package/src/dashboard/templates/graph 2.html +0 -201
  27. package/src/dashboard/templates/guard 2.html +0 -259
  28. package/src/dashboard/templates/inbox 2.html +0 -251
  29. package/src/dashboard/templates/memory 2.html +0 -420
  30. package/src/dashboard/templates/operations 2.html +0 -608
  31. package/src/dashboard/templates/plugins 2.html +0 -185
  32. package/src/dashboard/templates/protocol 2.html +0 -199
  33. package/src/dashboard/templates/rules 2.html +0 -246
  34. package/src/dashboard/templates/sentiment 2.html +0 -247
  35. package/src/dashboard/templates/sessions 2.html +0 -218
  36. package/src/dashboard/templates/skills 2.html +0 -329
  37. package/src/dashboard/templates/somatic 2.html +0 -73
  38. package/src/dashboard/templates/triggers 2.html +0 -133
  39. package/src/dashboard/templates/trust 2.html +0 -360
  40. package/src/db/__init__ 2.py +0 -259
  41. package/src/db/_core 2.py +0 -437
  42. package/src/db/_credentials 2.py +0 -124
  43. package/src/db/_episodic 2.py +0 -762
  44. package/src/db/_evolution 2.py +0 -54
  45. package/src/db/_fts 2.py +0 -406
  46. package/src/db/_goal_profiles 2.py +0 -376
  47. package/src/db/_hot_context 2.py +0 -660
  48. package/src/db/_outcomes 2.py +0 -800
  49. package/src/db/_personal_scripts 2.py +0 -582
  50. package/src/db/_sessions 2.py +0 -330
  51. package/src/db/_tasks 2.py +0 -91
  52. package/src/db/_watchers 2.py +0 -173
  53. package/src/doctor/formatters 2.py +0 -52
  54. package/src/doctor/models 2.py +0 -69
  55. package/src/doctor/planes 2.py +0 -87
  56. package/src/doctor/providers/__init__ 2.py +0 -1
  57. package/src/doctor/providers/deep 2.py +0 -367
  58. package/src/evolution_cycle 2.py +0 -519
  59. package/src/hooks/auto_capture 2.py +0 -208
  60. package/src/hooks/caffeinate-guard 2.sh +0 -8
  61. package/src/hooks/capture-session 2.sh +0 -21
  62. package/src/hooks/capture-tool-logs 2.sh +0 -158
  63. package/src/hooks/daily-briefing-check 2.sh +0 -33
  64. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  65. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  66. package/src/hooks/inbox-hook 2.sh +0 -76
  67. package/src/hooks/post-compact 2.sh +0 -152
  68. package/src/hooks/pre-compact 2.sh +0 -169
  69. package/src/hooks/protocol-guardrail 2.sh +0 -10
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  71. package/src/hooks/session-stop 2.sh +0 -52
  72. package/src/kg_populate 2.py +0 -292
  73. package/src/maintenance 2.py +0 -53
  74. package/src/memory_backends 2.py +0 -71
  75. package/src/migrate_embeddings 2.py +0 -124
  76. package/src/nexo_sdk 2.py +0 -103
  77. package/src/observability 2.py +0 -199
  78. package/src/plugin_loader 2.py +0 -217
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/artifact_registry 2.py +0 -450
  81. package/src/plugins/backup 2.py +0 -127
  82. package/src/plugins/claims_tools 2.py +0 -119
  83. package/src/plugins/cognitive_memory 2.py +0 -609
  84. package/src/plugins/core_rules 2.py +0 -252
  85. package/src/plugins/cortex 2.py +0 -1155
  86. package/src/plugins/entities 2.py +0 -67
  87. package/src/plugins/episodic_memory 2.py +0 -560
  88. package/src/plugins/evolution 2.py +0 -167
  89. package/src/plugins/goal_engine 2.py +0 -142
  90. package/src/plugins/guard 2.py +0 -862
  91. package/src/plugins/impact 2.py +0 -29
  92. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  93. package/src/plugins/media_memory_tools 2.py +0 -98
  94. package/src/plugins/memory_export 2.py +0 -196
  95. package/src/plugins/outcomes 2.py +0 -130
  96. package/src/plugins/personal_scripts 2.py +0 -117
  97. package/src/plugins/preferences 2.py +0 -47
  98. package/src/plugins/protocol 2.py +0 -1449
  99. package/src/plugins/simple_api 2.py +0 -106
  100. package/src/plugins/skills 2.py +0 -341
  101. package/src/plugins/state_watchers 2.py +0 -79
  102. package/src/plugins/update 2.py +0 -986
  103. package/src/plugins/user_state_tools 2.py +0 -43
  104. package/src/plugins/workflow 2.py +0 -588
  105. package/src/protocol_settings 2.py +0 -59
  106. package/src/public_contribution 2.py +0 -466
  107. package/src/public_evolution_queue 2.py +0 -241
  108. package/src/requirements 2.txt +0 -14
  109. package/src/retroactive_learnings 2.py +0 -373
  110. package/src/rules/__init__ 2.py +0 -0
  111. package/src/rules/core-rules 2.json +0 -331
  112. package/src/rules/migrate 2.py +0 -207
  113. package/src/runtime_power 2.py +0 -874
  114. package/src/script_registry 2.py +0 -1559
  115. package/src/scripts/check-context 2.py +0 -272
  116. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  117. package/src/scripts/deep-sleep/collect 2.py +0 -928
  118. package/src/scripts/deep-sleep/extract 2.py +0 -330
  119. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  120. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  121. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  122. package/src/scripts/nexo-agent-run 2.py +0 -75
  123. package/src/scripts/nexo-auto-update 2.py +0 -6
  124. package/src/scripts/nexo-backup 2.sh +0 -25
  125. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  126. package/src/scripts/nexo-catchup 2.py +0 -300
  127. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  128. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  129. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  130. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  131. package/src/scripts/nexo-dashboard 2.sh +0 -29
  132. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  133. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  134. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  135. package/src/scripts/nexo-hook-record 2.py +0 -42
  136. package/src/scripts/nexo-immune 2.py +0 -936
  137. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  138. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  139. package/src/scripts/nexo-install 2.py +0 -6
  140. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  141. package/src/scripts/nexo-learning-validator 2.py +0 -266
  142. package/src/scripts/nexo-migrate 2.py +0 -260
  143. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  144. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  145. package/src/scripts/nexo-pre-commit 2.py +0 -120
  146. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  147. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  148. package/src/scripts/nexo-reflection 2.py +0 -256
  149. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  150. package/src/scripts/nexo-sleep 2.py +0 -631
  151. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  152. package/src/scripts/nexo-sync-clients 2.py +0 -16
  153. package/src/scripts/nexo-synthesis 2.py +0 -475
  154. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  155. package/src/scripts/nexo-update 2.sh +0 -306
  156. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  157. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  158. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  159. package/src/server 2.py +0 -1296
  160. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  161. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  162. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  163. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  164. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  165. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  166. package/src/skills/run-release-final-audit/script 2.py +0 -259
  167. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  168. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  169. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  170. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  171. package/src/skills_runtime 2.py +0 -932
  172. package/src/state_watchers_runtime 2.py +0 -475
  173. package/src/storage_router 2.py +0 -32
  174. package/src/system_catalog 2.py +0 -786
  175. package/src/tools_coordination 2.py +0 -103
  176. package/src/tools_credentials 2.py +0 -68
  177. package/src/tools_drive 2.py +0 -487
  178. package/src/tools_hot_context 2.py +0 -163
  179. package/src/tools_learnings 2.py +0 -612
  180. package/src/tools_menu 2.py +0 -229
  181. package/src/tools_reminders 2.py +0 -88
  182. package/src/tools_reminders_crud 2.py +0 -363
  183. package/src/tools_sessions 2.py +0 -1054
  184. package/src/tools_system_catalog 2.py +0 -19
  185. package/src/tools_task_history 2.py +0 -57
  186. package/src/tools_transcripts 2.py +0 -98
  187. package/src/transcript_utils 2.py +0 -412
  188. package/src/user_context 2.py +0 -46
  189. package/src/user_data_portability 2.py +0 -328
  190. package/src/user_state_model 2.py +0 -170
  191. package/templates/CLAUDE.md 2.template +0 -108
  192. package/templates/CODEX.AGENTS.md 2.template +0 -66
  193. package/templates/launchagents/README 2.md +0 -132
  194. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  196. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  197. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  199. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  200. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  201. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  202. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  203. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  204. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  205. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  206. package/templates/nexo_helper 2.py +0 -301
  207. package/templates/openclaw 2.json +0 -13
  208. package/templates/plugin-template 2.py +0 -40
  209. package/templates/script-template 2.py +0 -59
  210. package/templates/script-template 2.sh +0 -13
  211. package/templates/skill-script-template 2.py +0 -48
  212. package/templates/skill-template 2.md +0 -33
@@ -1,59 +0,0 @@
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)
@@ -1,466 +0,0 @@
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