nexo-brain 7.31.2 → 7.31.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 +2 -2
- package/bin/nexo-brain.js +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +2 -2
- package/src/client_sync.py +1 -1
- package/src/db/_memory_v2.py +27 -0
- package/src/managed_mcp/lock.json +3 -3
- package/src/memory_retrieval.py +40 -0
- package/src/model_defaults.json +12 -5
- package/src/model_defaults.py +2 -2
- package/src/provider_circuit_breaker.py +2 -1
- package/src/resonance_tiers.json +4 -4
- package/src/hooks/heartbeat-enforcement.py +0 -103
- package/src/hooks/heartbeat-posttool.sh +0 -20
- package/src/hooks/heartbeat-user-msg.sh +0 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.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
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.31.
|
|
21
|
+
Version `7.31.4` is the current packaged-runtime line. Patch release over v7.31.3 - memory recall honours absolute time ranges (ISO dates, start..end ranges, datetimes, epochs) and enforces the window in SQL, so asking about a specific past day returns that day.
|
|
22
22
|
|
|
23
|
-
Previously in `7.31.
|
|
23
|
+
Previously in `7.31.3`: patch release over v7.31.2 - the recommended Claude Code model returns to Opus 4.8 with max reasoning (installs riding NEXO defaults migrate back automatically; customized models untouched), and the dead heartbeat-enforcement hook trio is removed from the source tree.
|
|
24
24
|
|
|
25
25
|
Previously in `7.30.33`: patch release over v7.30.32 - personal agent/script status now keeps the newest real run between manual executions and cron history, so a successful manual agent run cannot be hidden behind an older scheduled failure.
|
|
26
26
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -115,7 +115,7 @@ const PUBLIC_CONTRIBUTION_UPSTREAM = "wazionapps/nexo";
|
|
|
115
115
|
const MODEL_DEFAULTS_PATH = path.join(__dirname, "..", "src", "model_defaults.json");
|
|
116
116
|
function _loadModelDefaults() {
|
|
117
117
|
const fallback = {
|
|
118
|
-
claude_code: { model: "claude-
|
|
118
|
+
claude_code: { model: "claude-opus-4-8", reasoning_effort: "max", display_name: "Opus 4.8 with max reasoning" },
|
|
119
119
|
codex: { model: "gpt-5.5", reasoning_effort: "xhigh", display_name: "GPT-5.5 with max reasoning" },
|
|
120
120
|
};
|
|
121
121
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.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",
|
package/src/auto_update.py
CHANGED
|
@@ -2058,7 +2058,7 @@ def _refresh_resonance_tiers_model_defaults(dest: Path = NEXO_HOME) -> list[str]
|
|
|
2058
2058
|
dest / "personal" / "brain" / "resonance_tiers.json",
|
|
2059
2059
|
dest / "brain" / "resonance_tiers.json",
|
|
2060
2060
|
]
|
|
2061
|
-
old_prefixes = ("claude-opus-4-6", "claude-opus-4-7", "claude-
|
|
2061
|
+
old_prefixes = ("claude-opus-4-6", "claude-opus-4-7", "claude-fable-5")
|
|
2062
2062
|
|
|
2063
2063
|
for target_path in target_paths:
|
|
2064
2064
|
try:
|
|
@@ -2089,7 +2089,7 @@ def _refresh_resonance_tiers_model_defaults(dest: Path = NEXO_HOME) -> list[str]
|
|
|
2089
2089
|
model = str(claude.get("model") or "").strip()
|
|
2090
2090
|
tier_changed = False
|
|
2091
2091
|
if model and model.startswith(old_prefixes):
|
|
2092
|
-
claude["model"] = str(source_claude.get("model") or "claude-
|
|
2092
|
+
claude["model"] = str(source_claude.get("model") or "claude-opus-4-8")
|
|
2093
2093
|
tier_changed = True
|
|
2094
2094
|
if tier_changed and not str(claude.get("effort") or "").strip() and source_claude.get("effort"):
|
|
2095
2095
|
claude["effort"] = str(source_claude.get("effort"))
|
package/src/client_sync.py
CHANGED
|
@@ -83,7 +83,7 @@ except Exception:
|
|
|
83
83
|
|
|
84
84
|
def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
|
|
85
85
|
defaults = {
|
|
86
|
-
"claude_code": {"model": "claude-
|
|
86
|
+
"claude_code": {"model": "claude-opus-4-8", "reasoning_effort": "max"},
|
|
87
87
|
"codex": {"model": "gpt-5.5", "reasoning_effort": "xhigh"},
|
|
88
88
|
}
|
|
89
89
|
return dict(defaults.get(client, {}))
|
package/src/db/_memory_v2.py
CHANGED
|
@@ -750,12 +750,23 @@ def list_memory_events(
|
|
|
750
750
|
session_id: str = "",
|
|
751
751
|
project_key: str = "",
|
|
752
752
|
limit: int = 20,
|
|
753
|
+
start_ts: float | None = None,
|
|
754
|
+
end_ts: float | None = None,
|
|
753
755
|
) -> list[dict]:
|
|
754
756
|
conn = _core().get_db()
|
|
755
757
|
if not _table_exists(conn, "memory_events"):
|
|
756
758
|
return []
|
|
757
759
|
clauses = ["1=1"]
|
|
758
760
|
params: list[Any] = []
|
|
761
|
+
# time_range push-down (ff78ff94): bounds must constrain the SQL fetch
|
|
762
|
+
# itself — Python-side filtering after a recency-truncated fetch made old
|
|
763
|
+
# windows return nothing.
|
|
764
|
+
if start_ts is not None:
|
|
765
|
+
clauses.append("created_at >= ?")
|
|
766
|
+
params.append(float(start_ts))
|
|
767
|
+
if end_ts is not None:
|
|
768
|
+
clauses.append("created_at < ?")
|
|
769
|
+
params.append(float(end_ts))
|
|
759
770
|
if event_type.strip():
|
|
760
771
|
clauses.append("event_type = ?")
|
|
761
772
|
params.append(event_type.strip().lower())
|
|
@@ -798,12 +809,20 @@ def list_memory_observations(
|
|
|
798
809
|
project_key: str = "",
|
|
799
810
|
status: str = "",
|
|
800
811
|
limit: int = 20,
|
|
812
|
+
start_ts: float | None = None,
|
|
813
|
+
end_ts: float | None = None,
|
|
801
814
|
) -> list[dict]:
|
|
802
815
|
conn = _core().get_db()
|
|
803
816
|
if not _table_exists(conn, "memory_observations"):
|
|
804
817
|
return []
|
|
805
818
|
clauses = ["1=1"]
|
|
806
819
|
params: list[Any] = []
|
|
820
|
+
if start_ts is not None:
|
|
821
|
+
clauses.append("created_at >= ?")
|
|
822
|
+
params.append(float(start_ts))
|
|
823
|
+
if end_ts is not None:
|
|
824
|
+
clauses.append("created_at < ?")
|
|
825
|
+
params.append(float(end_ts))
|
|
807
826
|
if observation_type.strip():
|
|
808
827
|
clauses.append("observation_type = ?")
|
|
809
828
|
params.append(observation_type.strip().lower())
|
|
@@ -845,6 +864,8 @@ def search_memory_observations_fts(
|
|
|
845
864
|
*,
|
|
846
865
|
project_key: str = "",
|
|
847
866
|
limit: int = 20,
|
|
867
|
+
start_ts: float | None = None,
|
|
868
|
+
end_ts: float | None = None,
|
|
848
869
|
) -> list[dict]:
|
|
849
870
|
conn = _core().get_db()
|
|
850
871
|
if not _table_exists(conn, "memory_observations_fts"):
|
|
@@ -859,6 +880,12 @@ def search_memory_observations_fts(
|
|
|
859
880
|
WHERE memory_observations_fts MATCH ?
|
|
860
881
|
"""
|
|
861
882
|
params: list[Any] = [fts]
|
|
883
|
+
if start_ts is not None:
|
|
884
|
+
sql += " AND o.created_at >= ?"
|
|
885
|
+
params.append(float(start_ts))
|
|
886
|
+
if end_ts is not None:
|
|
887
|
+
sql += " AND o.created_at < ?"
|
|
888
|
+
params.append(float(end_ts))
|
|
862
889
|
if project_key.strip():
|
|
863
890
|
sql += " AND o.project_key = ?"
|
|
864
891
|
params.append(project_key.strip())
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"open-computer-use": {
|
|
40
40
|
"source_type": "npm",
|
|
41
41
|
"package": "open-computer-use",
|
|
42
|
-
"version": "0.1.
|
|
43
|
-
"integrity": "sha512-
|
|
44
|
-
"tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.
|
|
42
|
+
"version": "0.1.53",
|
|
43
|
+
"integrity": "sha512-5qwCPl7Gm4Wk2i/wFkq2dVLN2SzNRQSJTd95zXdGF+u5ZsUXkFx1IFVdNbYelWOpc4fgy8Z8/gYrbacj/2chig==",
|
|
44
|
+
"tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.53.tgz",
|
|
45
45
|
"bin": "open-computer-use-mcp",
|
|
46
46
|
"engines": {}
|
|
47
47
|
},
|
package/src/memory_retrieval.py
CHANGED
|
@@ -82,6 +82,40 @@ def _parse_time_range(value: str = "") -> tuple[float | None, float | None, str]
|
|
|
82
82
|
unit = match.group(2)
|
|
83
83
|
delta = timedelta(hours=amount) if unit.startswith("h") else timedelta(days=amount)
|
|
84
84
|
return (now - delta).timestamp(), now.timestamp(), clean
|
|
85
|
+
|
|
86
|
+
# Operator bug (session ff78ff94, 11-jun): absolute values silently fell
|
|
87
|
+
# through to (None, None, "") which DISABLED the filter — asking for a
|
|
88
|
+
# specific past day returned the most recent events instead. Support ISO
|
|
89
|
+
# dates, ISO ranges (date end is inclusive: bound = next midnight), ISO
|
|
90
|
+
# datetimes, and epoch seconds / epoch ranges.
|
|
91
|
+
def _point(text, *, end_of_day=False):
|
|
92
|
+
text = text.strip()
|
|
93
|
+
if re.fullmatch(r"\d{9,}(\.\d+)?", text):
|
|
94
|
+
return float(text), False
|
|
95
|
+
try:
|
|
96
|
+
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
|
|
97
|
+
day = datetime.fromisoformat(text)
|
|
98
|
+
if end_of_day:
|
|
99
|
+
return (day + timedelta(days=1)).timestamp(), True
|
|
100
|
+
return day.timestamp(), True
|
|
101
|
+
return datetime.fromisoformat(text).timestamp(), False
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None, False
|
|
104
|
+
|
|
105
|
+
if ".." in clean:
|
|
106
|
+
left, _, right = clean.partition("..")
|
|
107
|
+
start_ts, _ = _point(left)
|
|
108
|
+
end_ts, _ = _point(right, end_of_day=True)
|
|
109
|
+
if start_ts is not None and end_ts is not None and end_ts > start_ts:
|
|
110
|
+
return start_ts, end_ts, f"range:{clean}"
|
|
111
|
+
return None, None, ""
|
|
112
|
+
|
|
113
|
+
point_ts, is_date = _point(clean)
|
|
114
|
+
if point_ts is not None:
|
|
115
|
+
if is_date:
|
|
116
|
+
return point_ts, point_ts + 86400, f"day:{clean}"
|
|
117
|
+
# Single datetime/epoch: a one-hour window centred forward.
|
|
118
|
+
return point_ts, point_ts + 3600, f"at:{clean}"
|
|
85
119
|
return None, None, ""
|
|
86
120
|
|
|
87
121
|
|
|
@@ -177,6 +211,8 @@ def memory_search(
|
|
|
177
211
|
clean_query,
|
|
178
212
|
project_key="",
|
|
179
213
|
limit=max_items * 3,
|
|
214
|
+
start_ts=start,
|
|
215
|
+
end_ts=end,
|
|
180
216
|
):
|
|
181
217
|
uid = item.get("observation_uid") or f"id:{item.get('id')}"
|
|
182
218
|
observations_by_uid[uid] = item
|
|
@@ -184,6 +220,8 @@ def memory_search(
|
|
|
184
220
|
query=clean_query,
|
|
185
221
|
project_key="",
|
|
186
222
|
limit=max_items * 3,
|
|
223
|
+
start_ts=start,
|
|
224
|
+
end_ts=end,
|
|
187
225
|
):
|
|
188
226
|
uid = item.get("observation_uid") or f"id:{item.get('id')}"
|
|
189
227
|
observations_by_uid.setdefault(uid, item)
|
|
@@ -192,6 +230,8 @@ def memory_search(
|
|
|
192
230
|
query=clean_query,
|
|
193
231
|
project_key="",
|
|
194
232
|
limit=max_items * 3,
|
|
233
|
+
start_ts=start,
|
|
234
|
+
end_ts=end,
|
|
195
235
|
)
|
|
196
236
|
|
|
197
237
|
candidates = [
|
package/src/model_defaults.json
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"claude_code": {
|
|
4
|
-
"model": "claude-
|
|
4
|
+
"model": "claude-opus-4-8",
|
|
5
5
|
"reasoning_effort": "max",
|
|
6
|
-
"display_name": "
|
|
7
|
-
"recommendation_version":
|
|
8
|
-
"previous_defaults": [
|
|
6
|
+
"display_name": "Opus 4.8 with max reasoning",
|
|
7
|
+
"recommendation_version": 5,
|
|
8
|
+
"previous_defaults": [
|
|
9
|
+
"claude-fable-5",
|
|
10
|
+
"claude-opus-4-7[1m]",
|
|
11
|
+
"claude-opus-4-7",
|
|
12
|
+
"claude-opus-4-6[1m]"
|
|
13
|
+
]
|
|
9
14
|
},
|
|
10
15
|
"codex": {
|
|
11
16
|
"model": "gpt-5.5",
|
|
12
17
|
"reasoning_effort": "xhigh",
|
|
13
18
|
"display_name": "GPT-5.5 with max reasoning",
|
|
14
19
|
"recommendation_version": 2,
|
|
15
|
-
"previous_defaults": [
|
|
20
|
+
"previous_defaults": [
|
|
21
|
+
"gpt-5.4"
|
|
22
|
+
]
|
|
16
23
|
}
|
|
17
24
|
}
|
package/src/model_defaults.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import Any
|
|
|
20
20
|
_FALLBACK: dict[str, Any] = {
|
|
21
21
|
"schema_version": 1,
|
|
22
22
|
"claude_code": {
|
|
23
|
-
"model": "claude-
|
|
23
|
+
"model": "claude-opus-4-8",
|
|
24
24
|
"reasoning_effort": "max",
|
|
25
25
|
"display_name": "Fable 5 with max reasoning",
|
|
26
26
|
"recommendation_version": 4,
|
|
@@ -99,7 +99,7 @@ def looks_like_claude_model(model: str) -> bool:
|
|
|
99
99
|
return str(model or "").strip().lower().startswith(_CLAUDE_MODEL_PREFIXES)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
_CLAUDE_DEFAULT_PREFIXES = ("claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8")
|
|
102
|
+
_CLAUDE_DEFAULT_PREFIXES = ("claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-fable-5")
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
|
|
@@ -51,7 +51,8 @@ DEFAULT_RETRY_AFTER_S = {
|
|
|
51
51
|
_FAILURE_PATTERNS = (
|
|
52
52
|
("credits", re.compile(
|
|
53
53
|
r"credit balance is too low|insufficient[_ ]quota|exceeded your current quota"
|
|
54
|
-
r"|billing hard limit|out of credits|usage limit reached|
|
|
54
|
+
r"|billing hard limit|out of credits|usage limit reached|hit your usage limit"
|
|
55
|
+
r"|purchase more credits|plan limits",
|
|
55
56
|
re.I)),
|
|
56
57
|
("rate_limit", re.compile(
|
|
57
58
|
r"rate[_ -]?limit|too many requests|\b429\b|overloaded[_ ]error|\b529\b"
|
package/src/resonance_tiers.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"tiers": {
|
|
3
3
|
"maximo": {
|
|
4
|
-
"claude_code": { "model": "claude-
|
|
4
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "max" },
|
|
5
5
|
"codex": { "model": "gpt-5.5", "effort": "xhigh" }
|
|
6
6
|
},
|
|
7
7
|
"alto": {
|
|
8
|
-
"claude_code": { "model": "claude-
|
|
8
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "xhigh" },
|
|
9
9
|
"codex": { "model": "gpt-5.5", "effort": "high" }
|
|
10
10
|
},
|
|
11
11
|
"medio": {
|
|
12
|
-
"claude_code": { "model": "claude-
|
|
12
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "high" },
|
|
13
13
|
"codex": { "model": "gpt-5.5", "effort": "medium" }
|
|
14
14
|
},
|
|
15
15
|
"bajo": {
|
|
16
|
-
"claude_code": { "model": "claude-
|
|
16
|
+
"claude_code": { "model": "claude-opus-4-8", "effort": "medium" },
|
|
17
17
|
"codex": { "model": "gpt-5.5", "effort": "low" }
|
|
18
18
|
},
|
|
19
19
|
"muy_bajo": {
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Heartbeat enforcement for NEXO sessions.
|
|
3
|
-
|
|
4
|
-
Tracks user messages vs heartbeat calls. Emits a warning when more than two
|
|
5
|
-
user messages pass without a heartbeat call.
|
|
6
|
-
|
|
7
|
-
Modes:
|
|
8
|
-
- HEARTBEAT_MODE=user_msg: increment counter on UserPromptSubmit
|
|
9
|
-
- HEARTBEAT_MODE=post_tool: inspect PostToolUse payload, reset on heartbeat,
|
|
10
|
-
warn when other tools keep running without one
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
import sys
|
|
18
|
-
import time
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
import paths
|
|
23
|
-
except ModuleNotFoundError as exc:
|
|
24
|
-
if getattr(exc, "name", "") != "paths":
|
|
25
|
-
raise
|
|
26
|
-
|
|
27
|
-
class _PathsFallback:
|
|
28
|
-
@staticmethod
|
|
29
|
-
def operations_dir():
|
|
30
|
-
return Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo")) / "operations"
|
|
31
|
-
|
|
32
|
-
paths = _PathsFallback()
|
|
33
|
-
|
|
34
|
-
STATE_FILE = paths.operations_dir() / ".heartbeat-state.json"
|
|
35
|
-
THRESHOLD = 2
|
|
36
|
-
HEARTBEAT_TOOL = "nexo_heartbeat"
|
|
37
|
-
SKIP_TOOLS = {"nexo_startup", "nexo_stop", "nexo_smart_startup"}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _read_state() -> dict:
|
|
41
|
-
try:
|
|
42
|
-
return json.loads(STATE_FILE.read_text())
|
|
43
|
-
except Exception:
|
|
44
|
-
return {"user_msgs": 0, "last_heartbeat_ts": 0.0, "last_user_msg_ts": 0.0}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _write_state(state: dict) -> None:
|
|
48
|
-
try:
|
|
49
|
-
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
-
STATE_FILE.write_text(json.dumps(state))
|
|
51
|
-
except Exception:
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def handle_user_message() -> int:
|
|
56
|
-
state = _read_state()
|
|
57
|
-
state["user_msgs"] = state.get("user_msgs", 0) + 1
|
|
58
|
-
state["last_user_msg_ts"] = time.time()
|
|
59
|
-
_write_state(state)
|
|
60
|
-
return 0
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def handle_post_tool(payload: dict) -> int:
|
|
64
|
-
tool_name = str(payload.get("tool_name", "")).strip()
|
|
65
|
-
short_name = tool_name.rsplit("__", 1)[-1] if "__" in tool_name else tool_name
|
|
66
|
-
state = _read_state()
|
|
67
|
-
|
|
68
|
-
if short_name == HEARTBEAT_TOOL:
|
|
69
|
-
state["user_msgs"] = 0
|
|
70
|
-
state["last_heartbeat_ts"] = time.time()
|
|
71
|
-
_write_state(state)
|
|
72
|
-
return 0
|
|
73
|
-
|
|
74
|
-
if short_name in SKIP_TOOLS:
|
|
75
|
-
return 0
|
|
76
|
-
|
|
77
|
-
user_msgs = state.get("user_msgs", 0)
|
|
78
|
-
if user_msgs > THRESHOLD:
|
|
79
|
-
print(
|
|
80
|
-
f"\nWARNING: HEARTBEAT OVERDUE ({user_msgs} user messages without nexo_heartbeat). "
|
|
81
|
-
"Call nexo_heartbeat(sid=SID, task='...') before continuing."
|
|
82
|
-
)
|
|
83
|
-
return 0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def main() -> int:
|
|
87
|
-
mode = os.environ.get("HEARTBEAT_MODE", "").strip()
|
|
88
|
-
if mode == "user_msg":
|
|
89
|
-
return handle_user_message()
|
|
90
|
-
if mode == "post_tool":
|
|
91
|
-
raw = sys.stdin.read()
|
|
92
|
-
if not raw.strip():
|
|
93
|
-
return 0
|
|
94
|
-
try:
|
|
95
|
-
payload = json.loads(raw)
|
|
96
|
-
except Exception:
|
|
97
|
-
return 0
|
|
98
|
-
return handle_post_tool(payload)
|
|
99
|
-
return 0
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if __name__ == "__main__":
|
|
103
|
-
raise SystemExit(main())
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# NEXO PostToolUse hook — heartbeat enforcement checker
|
|
3
|
-
set -uo pipefail
|
|
4
|
-
|
|
5
|
-
INPUT=$(cat || true)
|
|
6
|
-
[ -z "$INPUT" ] && exit 0
|
|
7
|
-
|
|
8
|
-
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
9
|
-
HELPER=""
|
|
10
|
-
if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
|
|
11
|
-
HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
|
|
12
|
-
elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
|
|
13
|
-
HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
|
|
14
|
-
elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
|
|
15
|
-
HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
[ -z "$HELPER" ] && exit 0
|
|
19
|
-
HEARTBEAT_MODE=post_tool python3 "$HELPER" <<< "$INPUT" 2>/dev/null || true
|
|
20
|
-
exit 0
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# NEXO UserPromptSubmit hook — track user messages for heartbeat enforcement
|
|
3
|
-
set -uo pipefail
|
|
4
|
-
|
|
5
|
-
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
6
|
-
HELPER=""
|
|
7
|
-
if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
|
|
8
|
-
HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
|
|
9
|
-
elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
|
|
10
|
-
HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
|
|
11
|
-
elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
|
|
12
|
-
HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
[ -z "$HELPER" ] && exit 0
|
|
16
|
-
HEARTBEAT_MODE=user_msg python3 "$HELPER" 2>/dev/null || true
|
|
17
|
-
exit 0
|