nexo-brain 5.0.3 → 5.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/doctor/providers/deep.py +0 -3
- package/src/doctor/providers/runtime.py +5 -2
- package/templates/CLAUDE.md.template +2 -0
- package/templates/CODEX.AGENTS.md.template +2 -0
- package/templates/nexo_helper.py +163 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -87,6 +87,12 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
87
87
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
88
88
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
89
89
|
|
|
90
|
+
Version `5.0.4` tightens the local runtime bridge and trims false-positive doctor noise:
|
|
91
|
+
|
|
92
|
+
- vendorable `nexo_helper.py` now resolves `NEXO_HOME` and the `nexo` CLI path robustly, so personal scripts and subprocess flows stop depending on a lucky PATH
|
|
93
|
+
- doctor no longer degrades because of advisory-only self-audit warnings or a single missing usage-telemetry row
|
|
94
|
+
- managed Claude Code and Codex bootstraps now force an immediate first answer after simple email/diary/reminder/followup reads instead of feeling hung while chaining extra lookups
|
|
95
|
+
|
|
90
96
|
Version `5.0.3` closes the next post-5.0 runtime gap:
|
|
91
97
|
|
|
92
98
|
- `nexo chat` now boots Claude Code and Codex with an explicit NEXO startup prompt instead of opening cold or leaking the target path as a fake prompt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -2491,7 +2491,7 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2491
2491
|
if str(row["opened_at"] or "") >= first_cortex_eval_at
|
|
2492
2492
|
]
|
|
2493
2493
|
decision_ok = [row for row in decision_eligible if row["task_id"] in covered_tasks]
|
|
2494
|
-
decision_metric_ready = len(decision_eligible) >= 3
|
|
2494
|
+
decision_metric_ready = bool(first_cortex_eval_at) and len(decision_eligible) >= 3
|
|
2495
2495
|
|
|
2496
2496
|
score_parts = []
|
|
2497
2497
|
if verify_required:
|
|
@@ -2922,6 +2922,7 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
2922
2922
|
pricing_gaps = int((row["pricing_gaps"] if row else 0) or 0)
|
|
2923
2923
|
usage_denominator = successful_runs or total_runs
|
|
2924
2924
|
cost_denominator = successful_runs or total_runs
|
|
2925
|
+
missing_usage_runs = max(0, usage_denominator - usage_runs) if usage_denominator else 0
|
|
2925
2926
|
usage_coverage = round((usage_runs / usage_denominator) * 100, 1) if usage_denominator else 100.0
|
|
2926
2927
|
cost_coverage = round((cost_runs / cost_denominator) * 100, 1) if cost_denominator else 100.0
|
|
2927
2928
|
evidence = [
|
|
@@ -2933,6 +2934,8 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
2933
2934
|
f"cost_coverage={cost_coverage}%",
|
|
2934
2935
|
f"pricing_gaps={pricing_gaps}",
|
|
2935
2936
|
]
|
|
2937
|
+
if missing_usage_runs:
|
|
2938
|
+
evidence.append(f"missing_usage_runs={missing_usage_runs}")
|
|
2936
2939
|
backends = str((row["backends"] if row else "") or "").strip()
|
|
2937
2940
|
if backends:
|
|
2938
2941
|
evidence.append(f"backends={backends}")
|
|
@@ -2940,7 +2943,7 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
2940
2943
|
status = "healthy"
|
|
2941
2944
|
severity = "info"
|
|
2942
2945
|
repair_plan: list[str] = []
|
|
2943
|
-
if usage_coverage < 100.0:
|
|
2946
|
+
if usage_coverage < 100.0 and missing_usage_runs > 1:
|
|
2944
2947
|
status = "degraded"
|
|
2945
2948
|
severity = "warn"
|
|
2946
2949
|
repair_plan.append("Restore backend usage parsing so automation runs always emit token telemetry")
|
|
@@ -32,6 +32,8 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
32
32
|
- After the first relevant tool or artifact result, reply with the answer immediately instead of silently chaining more investigation.
|
|
33
33
|
- Only continue into deeper investigation before the first visible answer if the user explicitly asked for a deep investigation or the situation is urgent/high-risk.
|
|
34
34
|
- For single-artifact asks (email, message, diary item, reminder, prior fact), retrieve the artifact, summarize it, then decide whether further action is needed.
|
|
35
|
+
- For single-artifact asks, the default cap before the first visible answer is one lookup plus one detail read. Do not keep chaining tools before answering unless the user explicitly asked for more depth.
|
|
36
|
+
- After `nexo_email_read`, `nexo_session_diary_read`, `nexo_reminders`, `nexo_followups`, or equivalent single-artifact retrieval, answer immediately. Do not launch another search, another read, or background analysis before the first user-visible answer unless the user explicitly asked for it.
|
|
35
37
|
|
|
36
38
|
<!-- nexo:start:profile -->
|
|
37
39
|
## User Profile
|
|
@@ -29,6 +29,8 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
29
29
|
- After the first relevant tool or artifact result, answer immediately instead of silently chaining more investigation.
|
|
30
30
|
- Only keep investigating before the first visible answer if the user explicitly requested deep investigation or the situation is urgent/high-risk.
|
|
31
31
|
- For single-artifact asks (email, message, diary item, reminder, prior fact), retrieve it, summarize it, then decide if more work is needed.
|
|
32
|
+
- For single-artifact asks, the default cap before the first visible answer is one lookup plus one detail read. Do not keep chaining tools before answering unless the user explicitly asked for more depth.
|
|
33
|
+
- After `nexo_email_read`, `nexo_session_diary_read`, `nexo_reminders`, `nexo_followups`, or equivalent single-artifact retrieval, answer immediately. Do not launch another search, another read, or background analysis before the first visible answer unless the user explicitly asked for it.
|
|
32
34
|
|
|
33
35
|
## Codex Runtime Notes
|
|
34
36
|
- Codex does not provide Claude Code hooks, so protocol discipline must be explicit.
|
package/templates/nexo_helper.py
CHANGED
|
@@ -10,12 +10,34 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
|
+
import shutil
|
|
13
14
|
import subprocess
|
|
14
15
|
import sys
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
def _detect_nexo_home() -> Path:
|
|
20
|
+
env_home = os.environ.get("NEXO_HOME", "").strip()
|
|
21
|
+
if env_home:
|
|
22
|
+
return Path(env_home).expanduser()
|
|
23
|
+
|
|
24
|
+
helper_path = Path(__file__).resolve()
|
|
25
|
+
inferred_home = helper_path.parent.parent
|
|
26
|
+
if (
|
|
27
|
+
helper_path.parent.name == "templates"
|
|
28
|
+
and (inferred_home / "scripts").is_dir()
|
|
29
|
+
and (inferred_home / "config").is_dir()
|
|
30
|
+
):
|
|
31
|
+
return inferred_home
|
|
32
|
+
|
|
33
|
+
claude_home = Path.home() / "claude"
|
|
34
|
+
if claude_home.is_dir():
|
|
35
|
+
return claude_home
|
|
36
|
+
|
|
37
|
+
return Path.home() / ".nexo"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
NEXO_HOME = _detect_nexo_home()
|
|
19
41
|
DEFAULT_ALLOWED_TOOLS = "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"
|
|
20
42
|
DEFAULT_NEXO_TIMEOUT_SECONDS = max(15, int(os.environ.get("NEXO_HELPER_TIMEOUT", "90")))
|
|
21
43
|
|
|
@@ -49,16 +71,44 @@ def _load_bootstrap_prompt() -> str:
|
|
|
49
71
|
return ""
|
|
50
72
|
|
|
51
73
|
|
|
74
|
+
def _resolve_nexo_cli() -> str:
|
|
75
|
+
candidates = []
|
|
76
|
+
|
|
77
|
+
env_cli = os.environ.get("NEXO_BIN", "").strip()
|
|
78
|
+
if env_cli:
|
|
79
|
+
candidates.append(Path(env_cli).expanduser())
|
|
80
|
+
|
|
81
|
+
candidates.extend(
|
|
82
|
+
[
|
|
83
|
+
NEXO_HOME / "bin" / "nexo",
|
|
84
|
+
Path.home() / ".local" / "bin" / "nexo",
|
|
85
|
+
Path.home() / "bin" / "nexo",
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for candidate in candidates:
|
|
90
|
+
try:
|
|
91
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
92
|
+
return str(candidate)
|
|
93
|
+
except OSError:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
return shutil.which("nexo") or "nexo"
|
|
97
|
+
|
|
98
|
+
|
|
52
99
|
def run_nexo(args: list[str]) -> str:
|
|
53
100
|
"""Run a nexo CLI command and return stdout.
|
|
54
101
|
|
|
55
102
|
Raises RuntimeError on non-zero exit.
|
|
56
103
|
"""
|
|
104
|
+
env = os.environ.copy()
|
|
105
|
+
env.setdefault("NEXO_HOME", str(NEXO_HOME))
|
|
57
106
|
result = subprocess.run(
|
|
58
|
-
[
|
|
107
|
+
[_resolve_nexo_cli(), *args],
|
|
59
108
|
capture_output=True,
|
|
60
109
|
text=True,
|
|
61
110
|
timeout=DEFAULT_NEXO_TIMEOUT_SECONDS,
|
|
111
|
+
env=env,
|
|
62
112
|
)
|
|
63
113
|
if result.returncode != 0:
|
|
64
114
|
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"nexo exited {result.returncode}")
|
|
@@ -83,12 +133,72 @@ def call_tool_json(name: str, payload: dict | None = None) -> dict:
|
|
|
83
133
|
return json.loads(out)
|
|
84
134
|
|
|
85
135
|
|
|
136
|
+
def _extract_json_object(text: str) -> dict:
|
|
137
|
+
raw = str(text or "").strip()
|
|
138
|
+
if not raw:
|
|
139
|
+
raise RuntimeError("Automation backend returned empty output.")
|
|
140
|
+
|
|
141
|
+
if raw.startswith("```"):
|
|
142
|
+
lines = raw.splitlines()
|
|
143
|
+
if len(lines) >= 2:
|
|
144
|
+
end = len(lines)
|
|
145
|
+
for i in range(len(lines) - 1, 0, -1):
|
|
146
|
+
if lines[i].strip() == "```":
|
|
147
|
+
end = i
|
|
148
|
+
break
|
|
149
|
+
raw = "\n".join(lines[1:end]).strip()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
parsed = json.loads(raw)
|
|
153
|
+
if isinstance(parsed, dict):
|
|
154
|
+
return parsed
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
start = raw.find("{")
|
|
159
|
+
if start < 0:
|
|
160
|
+
raise RuntimeError("Automation backend did not return a JSON object.")
|
|
161
|
+
|
|
162
|
+
depth = 0
|
|
163
|
+
in_string = False
|
|
164
|
+
escape = False
|
|
165
|
+
for idx in range(start, len(raw)):
|
|
166
|
+
ch = raw[idx]
|
|
167
|
+
if in_string:
|
|
168
|
+
if escape:
|
|
169
|
+
escape = False
|
|
170
|
+
elif ch == "\\":
|
|
171
|
+
escape = True
|
|
172
|
+
elif ch == '"':
|
|
173
|
+
in_string = False
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if ch == '"':
|
|
177
|
+
in_string = True
|
|
178
|
+
elif ch == "{":
|
|
179
|
+
depth += 1
|
|
180
|
+
elif ch == "}":
|
|
181
|
+
depth -= 1
|
|
182
|
+
if depth == 0:
|
|
183
|
+
candidate = raw[start : idx + 1]
|
|
184
|
+
try:
|
|
185
|
+
parsed = json.loads(candidate)
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
raise RuntimeError(f"Automation backend returned invalid JSON object: {exc}") from exc
|
|
188
|
+
if isinstance(parsed, dict):
|
|
189
|
+
return parsed
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
raise RuntimeError("Automation backend did not return a parseable JSON object.")
|
|
193
|
+
|
|
194
|
+
|
|
86
195
|
def run_automation_text(
|
|
87
196
|
prompt: str,
|
|
88
197
|
*,
|
|
89
198
|
model: str = "",
|
|
90
199
|
reasoning_effort: str = "",
|
|
91
200
|
cwd: str = "",
|
|
201
|
+
timeout: int | None = None,
|
|
92
202
|
allowed_tools: str = DEFAULT_ALLOWED_TOOLS,
|
|
93
203
|
append_system_prompt: str = "",
|
|
94
204
|
include_bootstrap: bool = True,
|
|
@@ -130,7 +240,58 @@ def run_automation_text(
|
|
|
130
240
|
capture_output=True,
|
|
131
241
|
text=True,
|
|
132
242
|
env=env,
|
|
243
|
+
timeout=(int(timeout) if timeout else None),
|
|
133
244
|
)
|
|
134
245
|
if result.returncode != 0:
|
|
135
246
|
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"automation backend exited {result.returncode}")
|
|
136
247
|
return result.stdout
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run_automation_json(
|
|
251
|
+
prompt: str,
|
|
252
|
+
*,
|
|
253
|
+
model: str = "",
|
|
254
|
+
reasoning_effort: str = "",
|
|
255
|
+
cwd: str = "",
|
|
256
|
+
timeout: int | None = None,
|
|
257
|
+
allowed_tools: str = DEFAULT_ALLOWED_TOOLS,
|
|
258
|
+
append_system_prompt: str = "",
|
|
259
|
+
include_bootstrap: bool = True,
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""Run the configured backend and return a parsed JSON object."""
|
|
262
|
+
runner = NEXO_HOME / "scripts" / "nexo-agent-run.py"
|
|
263
|
+
if not runner.exists():
|
|
264
|
+
raise RuntimeError(f"Automation runner not found: {runner}")
|
|
265
|
+
|
|
266
|
+
cmd = [sys.executable, str(runner), "--prompt", prompt, "--output-format", "json"]
|
|
267
|
+
if model:
|
|
268
|
+
cmd.extend(["--model", model])
|
|
269
|
+
if reasoning_effort:
|
|
270
|
+
cmd.extend(["--reasoning-effort", reasoning_effort])
|
|
271
|
+
if cwd:
|
|
272
|
+
cmd.extend(["--cwd", cwd])
|
|
273
|
+
merged_system_prompt = []
|
|
274
|
+
if include_bootstrap:
|
|
275
|
+
bootstrap = _load_bootstrap_prompt()
|
|
276
|
+
if bootstrap:
|
|
277
|
+
merged_system_prompt.append(bootstrap)
|
|
278
|
+
if append_system_prompt:
|
|
279
|
+
merged_system_prompt.append(append_system_prompt)
|
|
280
|
+
if merged_system_prompt:
|
|
281
|
+
cmd.extend(["--append-system-prompt", "\n\n".join(merged_system_prompt)])
|
|
282
|
+
if allowed_tools:
|
|
283
|
+
cmd.extend(["--allowed-tools", allowed_tools])
|
|
284
|
+
|
|
285
|
+
env = os.environ.copy()
|
|
286
|
+
env.setdefault("NEXO_HOME", str(NEXO_HOME))
|
|
287
|
+
env.setdefault("NEXO_CODE", env.get("NEXO_CODE", str(NEXO_HOME)))
|
|
288
|
+
result = subprocess.run(
|
|
289
|
+
cmd,
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
env=env,
|
|
293
|
+
timeout=(int(timeout) if timeout else None),
|
|
294
|
+
)
|
|
295
|
+
if result.returncode != 0:
|
|
296
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"automation backend exited {result.returncode}")
|
|
297
|
+
return _extract_json_object(result.stdout)
|