nexo-brain 5.3.26 → 5.3.27

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 (211) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/server.py +3 -0
  4. package/src/tools_sessions.py +6 -1
  5. package/src/dashboard/static/favicon 2.svg +0 -32
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  8. package/src/dashboard/static/style 2.css +0 -2458
  9. package/src/dashboard/templates/adaptive 2.html +0 -118
  10. package/src/dashboard/templates/artifacts 2.html +0 -133
  11. package/src/dashboard/templates/backups 2.html +0 -136
  12. package/src/dashboard/templates/base 2.html +0 -417
  13. package/src/dashboard/templates/calendar 2.html +0 -591
  14. package/src/dashboard/templates/chat 2.html +0 -356
  15. package/src/dashboard/templates/claims 2.html +0 -259
  16. package/src/dashboard/templates/cortex 2.html +0 -321
  17. package/src/dashboard/templates/credentials 2.html +0 -128
  18. package/src/dashboard/templates/crons 2.html +0 -370
  19. package/src/dashboard/templates/dashboard 2.html +0 -494
  20. package/src/dashboard/templates/dreams 2.html +0 -252
  21. package/src/dashboard/templates/email 2.html +0 -160
  22. package/src/dashboard/templates/evolution 2.html +0 -189
  23. package/src/dashboard/templates/feed 2.html +0 -249
  24. package/src/dashboard/templates/followup_health 2.html +0 -170
  25. package/src/dashboard/templates/graph 2.html +0 -201
  26. package/src/dashboard/templates/guard 2.html +0 -259
  27. package/src/dashboard/templates/inbox 2.html +0 -251
  28. package/src/dashboard/templates/memory 2.html +0 -420
  29. package/src/dashboard/templates/operations 2.html +0 -608
  30. package/src/dashboard/templates/plugins 2.html +0 -185
  31. package/src/dashboard/templates/protocol 2.html +0 -199
  32. package/src/dashboard/templates/rules 2.html +0 -246
  33. package/src/dashboard/templates/sentiment 2.html +0 -247
  34. package/src/dashboard/templates/sessions 2.html +0 -218
  35. package/src/dashboard/templates/skills 2.html +0 -329
  36. package/src/dashboard/templates/somatic 2.html +0 -73
  37. package/src/dashboard/templates/triggers 2.html +0 -133
  38. package/src/dashboard/templates/trust 2.html +0 -360
  39. package/src/db/__init__ 2.py +0 -259
  40. package/src/db/_core 2.py +0 -437
  41. package/src/db/_credentials 2.py +0 -124
  42. package/src/db/_episodic 2.py +0 -762
  43. package/src/db/_evolution 2.py +0 -54
  44. package/src/db/_fts 2.py +0 -406
  45. package/src/db/_goal_profiles 2.py +0 -376
  46. package/src/db/_hot_context 2.py +0 -660
  47. package/src/db/_outcomes 2.py +0 -800
  48. package/src/db/_personal_scripts 2.py +0 -582
  49. package/src/db/_sessions 2.py +0 -330
  50. package/src/db/_tasks 2.py +0 -91
  51. package/src/db/_watchers 2.py +0 -173
  52. package/src/doctor/formatters 2.py +0 -52
  53. package/src/doctor/models 2.py +0 -69
  54. package/src/doctor/planes 2.py +0 -87
  55. package/src/doctor/providers/__init__ 2.py +0 -1
  56. package/src/doctor/providers/deep 2.py +0 -367
  57. package/src/evolution_cycle 2.py +0 -519
  58. package/src/hooks/auto_capture 2.py +0 -208
  59. package/src/hooks/caffeinate-guard 2.sh +0 -8
  60. package/src/hooks/capture-session 2.sh +0 -21
  61. package/src/hooks/capture-tool-logs 2.sh +0 -158
  62. package/src/hooks/daily-briefing-check 2.sh +0 -33
  63. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  64. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  65. package/src/hooks/inbox-hook 2.sh +0 -76
  66. package/src/hooks/post-compact 2.sh +0 -152
  67. package/src/hooks/pre-compact 2.sh +0 -169
  68. package/src/hooks/protocol-guardrail 2.sh +0 -10
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  70. package/src/hooks/session-stop 2.sh +0 -52
  71. package/src/kg_populate 2.py +0 -292
  72. package/src/maintenance 2.py +0 -53
  73. package/src/memory_backends 2.py +0 -71
  74. package/src/migrate_embeddings 2.py +0 -124
  75. package/src/nexo_sdk 2.py +0 -103
  76. package/src/observability 2.py +0 -199
  77. package/src/plugin_loader 2.py +0 -217
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +0 -450
  80. package/src/plugins/backup 2.py +0 -127
  81. package/src/plugins/claims_tools 2.py +0 -119
  82. package/src/plugins/cognitive_memory 2.py +0 -609
  83. package/src/plugins/core_rules 2.py +0 -252
  84. package/src/plugins/cortex 2.py +0 -1155
  85. package/src/plugins/entities 2.py +0 -67
  86. package/src/plugins/episodic_memory 2.py +0 -560
  87. package/src/plugins/evolution 2.py +0 -167
  88. package/src/plugins/goal_engine 2.py +0 -142
  89. package/src/plugins/guard 2.py +0 -862
  90. package/src/plugins/impact 2.py +0 -29
  91. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  92. package/src/plugins/media_memory_tools 2.py +0 -98
  93. package/src/plugins/memory_export 2.py +0 -196
  94. package/src/plugins/outcomes 2.py +0 -130
  95. package/src/plugins/personal_scripts 2.py +0 -117
  96. package/src/plugins/preferences 2.py +0 -47
  97. package/src/plugins/protocol 2.py +0 -1449
  98. package/src/plugins/simple_api 2.py +0 -106
  99. package/src/plugins/skills 2.py +0 -341
  100. package/src/plugins/state_watchers 2.py +0 -79
  101. package/src/plugins/update 2.py +0 -986
  102. package/src/plugins/user_state_tools 2.py +0 -43
  103. package/src/plugins/workflow 2.py +0 -588
  104. package/src/protocol_settings 2.py +0 -59
  105. package/src/public_contribution 2.py +0 -466
  106. package/src/public_evolution_queue 2.py +0 -241
  107. package/src/requirements 2.txt +0 -14
  108. package/src/retroactive_learnings 2.py +0 -373
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +0 -331
  111. package/src/rules/migrate 2.py +0 -207
  112. package/src/runtime_power 2.py +0 -874
  113. package/src/script_registry 2.py +0 -1559
  114. package/src/scripts/check-context 2.py +0 -272
  115. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  116. package/src/scripts/deep-sleep/collect 2.py +0 -928
  117. package/src/scripts/deep-sleep/extract 2.py +0 -330
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  119. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  121. package/src/scripts/nexo-agent-run 2.py +0 -75
  122. package/src/scripts/nexo-auto-update 2.py +0 -6
  123. package/src/scripts/nexo-backup 2.sh +0 -25
  124. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  125. package/src/scripts/nexo-catchup 2.py +0 -300
  126. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  127. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  128. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  129. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  130. package/src/scripts/nexo-dashboard 2.sh +0 -29
  131. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  132. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  133. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  134. package/src/scripts/nexo-hook-record 2.py +0 -42
  135. package/src/scripts/nexo-immune 2.py +0 -936
  136. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  137. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  138. package/src/scripts/nexo-install 2.py +0 -6
  139. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  140. package/src/scripts/nexo-learning-validator 2.py +0 -266
  141. package/src/scripts/nexo-migrate 2.py +0 -260
  142. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  144. package/src/scripts/nexo-pre-commit 2.py +0 -120
  145. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  146. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  147. package/src/scripts/nexo-reflection 2.py +0 -256
  148. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  149. package/src/scripts/nexo-sleep 2.py +0 -631
  150. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  151. package/src/scripts/nexo-sync-clients 2.py +0 -16
  152. package/src/scripts/nexo-synthesis 2.py +0 -475
  153. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  154. package/src/scripts/nexo-update 2.sh +0 -306
  155. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  156. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  158. package/src/server 2.py +0 -1296
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  164. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  165. package/src/skills/run-release-final-audit/script 2.py +0 -259
  166. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  167. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  168. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  169. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  170. package/src/skills_runtime 2.py +0 -932
  171. package/src/state_watchers_runtime 2.py +0 -475
  172. package/src/storage_router 2.py +0 -32
  173. package/src/system_catalog 2.py +0 -786
  174. package/src/tools_coordination 2.py +0 -103
  175. package/src/tools_credentials 2.py +0 -68
  176. package/src/tools_drive 2.py +0 -487
  177. package/src/tools_hot_context 2.py +0 -163
  178. package/src/tools_learnings 2.py +0 -612
  179. package/src/tools_menu 2.py +0 -229
  180. package/src/tools_reminders 2.py +0 -88
  181. package/src/tools_reminders_crud 2.py +0 -363
  182. package/src/tools_sessions 2.py +0 -1054
  183. package/src/tools_system_catalog 2.py +0 -19
  184. package/src/tools_task_history 2.py +0 -57
  185. package/src/tools_transcripts 2.py +0 -98
  186. package/src/transcript_utils 2.py +0 -412
  187. package/src/user_context 2.py +0 -46
  188. package/src/user_data_portability 2.py +0 -328
  189. package/src/user_state_model 2.py +0 -170
  190. package/templates/CLAUDE.md 2.template +0 -108
  191. package/templates/CODEX.AGENTS.md 2.template +0 -66
  192. package/templates/launchagents/README 2.md +0 -132
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  194. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  200. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  205. package/templates/nexo_helper 2.py +0 -301
  206. package/templates/openclaw 2.json +0 -13
  207. package/templates/plugin-template 2.py +0 -40
  208. package/templates/script-template 2.py +0 -59
  209. package/templates/script-template 2.sh +0 -13
  210. package/templates/skill-script-template 2.py +0 -48
  211. package/templates/skill-template 2.md +0 -33
@@ -1,786 +0,0 @@
1
- from __future__ import annotations
2
- """Live system catalog / ontology derived from canonical NEXO sources."""
3
-
4
- import ast
5
- import importlib.util
6
- import inspect
7
- import json
8
- import os
9
- import re
10
- import sys
11
- from pathlib import Path
12
-
13
- from db import get_db, list_skills, sync_skill_directories
14
- from plugin_loader import PERSONAL_PLUGINS_DIR, PLUGINS_DIR, list_plugins
15
- from script_registry import list_scripts
16
-
17
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
- NEXO_CODE = Path(__file__).resolve().parent
19
- SERVER_PATH = NEXO_CODE / "server.py"
20
- MANIFEST_PATHS = [NEXO_CODE / "crons" / "manifest.json", NEXO_HOME / "crons" / "manifest.json"]
21
- ATLAS_PATH = NEXO_HOME / "brain" / "project-atlas.json"
22
-
23
- SECTION_ORDER = (
24
- "core_tools",
25
- "plugin_tools",
26
- "skills",
27
- "scripts",
28
- "crons",
29
- "projects",
30
- "artifacts",
31
- )
32
-
33
- _DOC_ARG_LINE_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$")
34
-
35
-
36
- def _normalize_text(text: str | None) -> str:
37
- return str(text or "").strip().lower()
38
-
39
-
40
- def _tokenize(text: str | None) -> set[str]:
41
- import re
42
- normalized = _normalize_text(text)
43
- return {
44
- token
45
- for token in re.findall(r"[a-z0-9][a-z0-9._:-]{1,}", normalized)
46
- if len(token) >= 3
47
- }
48
-
49
-
50
- def _score(query_tokens: set[str], haystack: str) -> float:
51
- if not query_tokens:
52
- return 0.0
53
- haystack_tokens = _tokenize(haystack)
54
- if not haystack_tokens:
55
- return 0.0
56
- overlap = query_tokens & haystack_tokens
57
- if not overlap:
58
- return 0.0
59
- return len(overlap) / max(1, min(len(query_tokens), len(haystack_tokens)))
60
-
61
-
62
- def _truncate(text: str | None, limit: int = 180) -> str:
63
- clean = str(text or "").strip()
64
- if len(clean) <= limit:
65
- return clean
66
- return clean[: limit - 3] + "..."
67
-
68
-
69
- def _tool_category(name: str) -> str:
70
- if name.startswith("nexo_recent_context") or name.startswith("nexo_pre_action_context") or name.startswith("nexo_hot_context"):
71
- return "recent_memory"
72
- if name.startswith("nexo_transcript"):
73
- return "transcripts"
74
- if name.startswith("nexo_session") or name.startswith("nexo_checkpoint"):
75
- return "sessions"
76
- if name.startswith("nexo_followup") or name.startswith("nexo_reminder"):
77
- return "reminders"
78
- if name.startswith("nexo_skill"):
79
- return "skills"
80
- if name.startswith("nexo_plugin"):
81
- return "plugins"
82
- if name.startswith("nexo_goal") or name.startswith("nexo_workflow"):
83
- return "workflow"
84
- if name.startswith("nexo_learning"):
85
- return "learnings"
86
- if name.startswith("nexo_guard") or name.startswith("nexo_task") or name.startswith("nexo_cortex"):
87
- return "protocol"
88
- return "general"
89
-
90
-
91
- def _annotation_text_from_ast(node: ast.AST | None) -> str:
92
- if node is None:
93
- return ""
94
- try:
95
- return ast.unparse(node)
96
- except Exception:
97
- return ""
98
-
99
-
100
- def _literal_text(value) -> str:
101
- if value is inspect._empty:
102
- return ""
103
- if isinstance(value, str):
104
- return json.dumps(value, ensure_ascii=False)
105
- if value is None:
106
- return "None"
107
- return repr(value)
108
-
109
-
110
- def _default_text_from_ast(node: ast.AST | None) -> str:
111
- if node is None:
112
- return ""
113
- try:
114
- return _literal_text(ast.literal_eval(node))
115
- except Exception:
116
- try:
117
- return ast.unparse(node)
118
- except Exception:
119
- return "..."
120
-
121
-
122
- def _annotation_text(annotation) -> str:
123
- if annotation is inspect._empty:
124
- return ""
125
- if isinstance(annotation, str):
126
- return annotation
127
- if getattr(annotation, "__module__", "") == "builtins" and hasattr(annotation, "__name__"):
128
- return str(annotation.__name__)
129
- text = str(annotation)
130
- return text.replace("typing.", "")
131
-
132
-
133
- def _parse_arg_docs(doc: str) -> dict[str, str]:
134
- docs: dict[str, str] = {}
135
- if not doc.strip():
136
- return docs
137
- in_args = False
138
- current_arg = ""
139
- for raw_line in doc.splitlines():
140
- line = raw_line.rstrip()
141
- stripped = line.strip()
142
- if stripped in {"Args:", "Arguments:"}:
143
- in_args = True
144
- current_arg = ""
145
- continue
146
- if not in_args:
147
- continue
148
- if stripped and not raw_line.startswith((" ", "\t")) and stripped.endswith(":"):
149
- break
150
- if not stripped:
151
- current_arg = ""
152
- continue
153
- match = _DOC_ARG_LINE_RE.match(raw_line)
154
- if match:
155
- current_arg = match.group(1)
156
- docs[current_arg] = match.group(2).strip()
157
- continue
158
- if current_arg:
159
- docs[current_arg] = f"{docs[current_arg]} {stripped}".strip()
160
- return docs
161
-
162
-
163
- def _build_signature(name: str, params: list[dict], return_annotation: str = "") -> str:
164
- pieces: list[str] = []
165
- for param in params:
166
- part = param["name"]
167
- if param.get("annotation"):
168
- part += f": {param['annotation']}"
169
- if not param.get("required", False):
170
- part += f" = {param.get('default', '')}"
171
- pieces.append(part)
172
- signature = f"{name}({', '.join(pieces)})"
173
- if return_annotation:
174
- signature += f" -> {return_annotation}"
175
- return signature
176
-
177
-
178
- def _example_value_for_param(param: dict) -> str:
179
- name = str(param.get("name", "value"))
180
- annotation = str(param.get("annotation", "")).lower()
181
- if name == "id" or name.endswith("_id"):
182
- return '"..."'
183
- if name.endswith("_token") or name == "read_token":
184
- return '"TOKEN"'
185
- if "bool" in annotation:
186
- return "True"
187
- if "int" in annotation:
188
- return "1"
189
- if "float" in annotation:
190
- return "1.0"
191
- if "list" in annotation or name.endswith("s"):
192
- return '["..."]'
193
- if "dict" in annotation or "object" in annotation:
194
- return '{"key": "value"}'
195
- return '"..."'
196
-
197
-
198
- def _generic_example(name: str, params: list[dict]) -> str:
199
- required = [param for param in params if param.get("required", False)]
200
- if not required:
201
- return f"{name}()"
202
- pieces = [f"{param['name']}={_example_value_for_param(param)}" for param in required]
203
- return f"{name}({', '.join(pieces)})"
204
-
205
-
206
- def _ast_params_for_node(node: ast.FunctionDef, arg_docs: dict[str, str]) -> list[dict]:
207
- params: list[dict] = []
208
- positional = list(node.args.posonlyargs) + list(node.args.args)
209
- positional_defaults = [None] * (len(positional) - len(node.args.defaults)) + list(node.args.defaults)
210
- for arg_node, default_node in zip(positional, positional_defaults):
211
- if arg_node.arg in {"self", "cls"}:
212
- continue
213
- params.append(
214
- {
215
- "name": arg_node.arg,
216
- "annotation": _annotation_text_from_ast(arg_node.annotation),
217
- "required": default_node is None,
218
- "default": "" if default_node is None else _default_text_from_ast(default_node),
219
- "description": arg_docs.get(arg_node.arg, ""),
220
- }
221
- )
222
- for arg_node, default_node in zip(node.args.kwonlyargs, node.args.kw_defaults):
223
- if arg_node.arg in {"self", "cls"}:
224
- continue
225
- params.append(
226
- {
227
- "name": arg_node.arg,
228
- "annotation": _annotation_text_from_ast(arg_node.annotation),
229
- "required": default_node is None,
230
- "default": "" if default_node is None else _default_text_from_ast(default_node),
231
- "description": arg_docs.get(arg_node.arg, ""),
232
- }
233
- )
234
- return params
235
-
236
-
237
- def _callable_params(func, arg_docs: dict[str, str]) -> list[dict]:
238
- params: list[dict] = []
239
- try:
240
- signature = inspect.signature(func)
241
- except (TypeError, ValueError):
242
- return params
243
- for name, param in signature.parameters.items():
244
- if name in {"self", "cls"}:
245
- continue
246
- required = param.default is inspect._empty
247
- params.append(
248
- {
249
- "name": name,
250
- "annotation": _annotation_text(param.annotation),
251
- "required": required,
252
- "default": "" if required else _literal_text(param.default),
253
- "description": arg_docs.get(name, ""),
254
- }
255
- )
256
- return params
257
-
258
-
259
- def _guide_for_tool(name: str) -> dict[str, list]:
260
- if name == "nexo_learning_add":
261
- return {
262
- "workflow": [
263
- "Usa `applies_to` si quieres que el guard recuerde este learning antes de tocar un archivo, directorio o patrón concreto.",
264
- "Usa `priority` (`critical`, `high`, `medium`, `low`) para marcar severidad operativa.",
265
- ],
266
- "examples": [
267
- {
268
- "title": "Learning mínimo",
269
- "code": 'nexo_learning_add(category="shopify", title="Hacer pull antes de editar", content="Siempre sincronizar antes de editar el tema live.")',
270
- },
271
- {
272
- "title": "Learning ligado a archivo o patrón",
273
- "code": 'nexo_learning_add(category="recambios-bmw", title="Pull antes de editar theme", content="El admin puede tocar JSONs live.", applies_to="/abs/path/templates/product.json,templates/*.json,sections/*.liquid", prevention="Ejecutar `shopify theme pull` antes de editar.", priority="high")',
274
- },
275
- ],
276
- "common_errors": [
277
- "Usar `severity` en vez de `priority`.",
278
- "Olvidar `title`, que es obligatorio.",
279
- "No poner `applies_to` cuando quieres que el warning salte antes de tocar archivos concretos.",
280
- ],
281
- }
282
- if name == "nexo_learning_update":
283
- return {
284
- "workflow": [
285
- "Úsalo para completar o endurecer un learning existente cuando descubres nuevos archivos afectados, mejor `prevention` o prioridad distinta.",
286
- ],
287
- "examples": [
288
- {
289
- "title": "Añadir alcance a un learning existente",
290
- "code": 'nexo_learning_update(id=57, applies_to="/abs/path/file.py,src/plugins/*.py", prevention="Leer schema antes del primer uso", priority="high")',
291
- },
292
- ],
293
- "common_errors": [
294
- "Intentar recrear el learning desde cero cuando basta con actualizar el existente.",
295
- ],
296
- }
297
- if name == "nexo_reminder_get":
298
- return {
299
- "workflow": [
300
- "Devuelve el `READ_TOKEN` necesario para `update`, `delete`, `restore` y `note` sobre ese reminder.",
301
- ],
302
- "examples": [
303
- {
304
- "title": "Leer reminder y obtener token",
305
- "code": 'nexo_reminder_get(id="R87")',
306
- },
307
- ],
308
- "common_errors": [
309
- "Intentar editar o borrar un reminder sin llamar antes a `nexo_reminder_get`.",
310
- ],
311
- }
312
- if name in {"nexo_reminder_update", "nexo_reminder_delete", "nexo_reminder_restore", "nexo_reminder_note"}:
313
- return {
314
- "workflow": [
315
- "Primero llama `nexo_reminder_get(id=\"R87\")` para obtener `READ_TOKEN`.",
316
- f"Luego reutiliza ese `READ_TOKEN` en `{name}(...)`.",
317
- ],
318
- "examples": [
319
- {
320
- "title": "1. Obtener token",
321
- "code": 'nexo_reminder_get(id="R87")',
322
- },
323
- {
324
- "title": "2. Reutilizar READ_TOKEN",
325
- "code": f'{name}(id="R87", read_token="TOKEN")',
326
- },
327
- ],
328
- "common_errors": [
329
- "Llamar a esta tool sin `READ_TOKEN` válido.",
330
- "Usar un `READ_TOKEN` de otro reminder o de una lectura antigua.",
331
- ],
332
- }
333
- if name == "nexo_followup_get":
334
- return {
335
- "workflow": [
336
- "Devuelve el `READ_TOKEN` necesario para `update`, `delete`, `restore` y `note` sobre ese followup.",
337
- ],
338
- "examples": [
339
- {
340
- "title": "Leer followup y obtener token",
341
- "code": 'nexo_followup_get(id="NF45")',
342
- },
343
- ],
344
- "common_errors": [
345
- "Intentar editar o borrar un followup sin llamar antes a `nexo_followup_get`.",
346
- ],
347
- }
348
- if name in {"nexo_followup_update", "nexo_followup_delete", "nexo_followup_restore", "nexo_followup_note"}:
349
- return {
350
- "workflow": [
351
- "Primero llama `nexo_followup_get(id=\"NF45\")` para obtener `READ_TOKEN`.",
352
- f"Luego reutiliza ese `READ_TOKEN` en `{name}(...)`.",
353
- ],
354
- "examples": [
355
- {
356
- "title": "1. Obtener token",
357
- "code": 'nexo_followup_get(id="NF45")',
358
- },
359
- {
360
- "title": "2. Reutilizar READ_TOKEN",
361
- "code": f'{name}(id="NF45", read_token="TOKEN")',
362
- },
363
- ],
364
- "common_errors": [
365
- "Llamar a esta tool sin `READ_TOKEN` válido.",
366
- "Usar un `READ_TOKEN` de otro followup o de una lectura antigua.",
367
- ],
368
- }
369
- return {}
370
-
371
-
372
- def _parse_core_tools() -> list[dict]:
373
- if not SERVER_PATH.is_file():
374
- return []
375
- try:
376
- tree = ast.parse(SERVER_PATH.read_text())
377
- except Exception:
378
- return []
379
-
380
- entries: list[dict] = []
381
- for node in tree.body:
382
- if not isinstance(node, ast.FunctionDef):
383
- continue
384
- if not any(
385
- isinstance(dec, ast.Attribute) and getattr(dec.value, "id", "") == "mcp" and dec.attr == "tool"
386
- for dec in node.decorator_list
387
- ):
388
- continue
389
- doc = ast.get_docstring(node) or ""
390
- first_line = doc.strip().splitlines()[0].strip() if doc.strip() else ""
391
- arg_docs = _parse_arg_docs(doc)
392
- params = _ast_params_for_node(node, arg_docs)
393
- entries.append(
394
- {
395
- "kind": "core_tool",
396
- "name": node.name,
397
- "description": first_line,
398
- "doc": doc,
399
- "category": _tool_category(node.name),
400
- "path": str(SERVER_PATH),
401
- "line": int(getattr(node, "lineno", 0) or 0),
402
- "params": params,
403
- "signature": _build_signature(
404
- node.name,
405
- params,
406
- _annotation_text_from_ast(node.returns),
407
- ),
408
- "quick_example": _generic_example(node.name, params),
409
- "source": "core",
410
- }
411
- )
412
- return entries
413
-
414
-
415
- def _plugin_module_tools(filename: str, created_by: str) -> list[dict]:
416
- module_name = f"plugins.{filename[:-3]}"
417
- module = sys.modules.get(module_name)
418
- if module is None:
419
- plugin_dir = PLUGINS_DIR if created_by == "repo" else PERSONAL_PLUGINS_DIR
420
- path = Path(plugin_dir) / filename
421
- if not path.is_file():
422
- return []
423
- try:
424
- spec = importlib.util.spec_from_file_location(module_name, path)
425
- if spec is None or spec.loader is None:
426
- return []
427
- module = importlib.util.module_from_spec(spec)
428
- spec.loader.exec_module(module)
429
- except Exception:
430
- return []
431
- tools = getattr(module, "TOOLS", []) or []
432
- result: list[dict] = []
433
- for item in tools:
434
- try:
435
- func, name, description = item
436
- except Exception:
437
- continue
438
- doc = inspect.getdoc(func) or ""
439
- arg_docs = _parse_arg_docs(doc)
440
- params = _callable_params(func, arg_docs)
441
- try:
442
- return_annotation = _annotation_text(inspect.signature(func).return_annotation)
443
- except (TypeError, ValueError):
444
- return_annotation = ""
445
- result.append(
446
- {
447
- "kind": "plugin_tool",
448
- "name": str(name),
449
- "description": str(description or ""),
450
- "doc": doc,
451
- "params": params,
452
- "signature": _build_signature(
453
- str(name),
454
- params,
455
- return_annotation,
456
- ),
457
- "quick_example": _generic_example(str(name), params),
458
- "plugin": filename,
459
- "source": created_by,
460
- "category": _tool_category(str(name)),
461
- }
462
- )
463
- return result
464
-
465
-
466
- def _plugin_entries() -> list[dict]:
467
- rows = list_plugins()
468
- entries: list[dict] = []
469
- for row in rows:
470
- filename = str(row.get("filename") or "")
471
- created_by = str(row.get("created_by") or row.get("source") or "repo")
472
- plugin_tools = _plugin_module_tools(filename, created_by)
473
- if plugin_tools:
474
- entries.extend(plugin_tools)
475
- continue
476
- names = str(row.get("tool_names") or "").split(",")
477
- for name in [n.strip() for n in names if n.strip()]:
478
- entries.append(
479
- {
480
- "kind": "plugin_tool",
481
- "name": name,
482
- "description": "",
483
- "plugin": filename,
484
- "source": created_by,
485
- "category": _tool_category(name),
486
- }
487
- )
488
- return entries
489
-
490
-
491
- def _skill_entries() -> list[dict]:
492
- try:
493
- sync_skill_directories()
494
- except Exception:
495
- pass
496
- entries: list[dict] = []
497
- for row in list_skills():
498
- entries.append(
499
- {
500
- "kind": "skill",
501
- "name": row.get("id", ""),
502
- "display_name": row.get("name", ""),
503
- "description": row.get("description", "") or "",
504
- "source": row.get("source_kind", "") or "",
505
- "level": row.get("level", "") or "",
506
- "mode": row.get("mode", "") or "",
507
- "execution_level": row.get("execution_level", "") or "",
508
- "trust_score": row.get("trust_score", 0),
509
- "tags": row.get("tags", "[]"),
510
- }
511
- )
512
- return entries
513
-
514
-
515
- def _script_entries() -> list[dict]:
516
- entries: list[dict] = []
517
- for row in list_scripts(include_core=True):
518
- entries.append(
519
- {
520
- "kind": "script",
521
- "name": row.get("name", ""),
522
- "description": row.get("description", "") or "",
523
- "runtime": row.get("runtime", "") or "",
524
- "path": row.get("path", "") or "",
525
- "source": "core" if row.get("core") else "personal",
526
- "classification": row.get("classification", "") or "",
527
- "declared_schedule": row.get("declared_schedule", {}) or {},
528
- }
529
- )
530
- return entries
531
-
532
-
533
- def _cron_entries() -> list[dict]:
534
- manifest = None
535
- for path in MANIFEST_PATHS:
536
- if path.is_file():
537
- try:
538
- manifest = json.loads(path.read_text())
539
- break
540
- except Exception:
541
- continue
542
- if not isinstance(manifest, dict):
543
- return []
544
- entries: list[dict] = []
545
- for cron in manifest.get("crons", []) or []:
546
- entries.append(
547
- {
548
- "kind": "cron",
549
- "name": cron.get("id", ""),
550
- "description": cron.get("description", "") or "",
551
- "script": cron.get("script", "") or "",
552
- "schedule": cron.get("schedule", {}) or {},
553
- "optional": bool(cron.get("optional", False)),
554
- }
555
- )
556
- return entries
557
-
558
-
559
- def _project_entries() -> list[dict]:
560
- if not ATLAS_PATH.is_file():
561
- return []
562
- try:
563
- payload = json.loads(ATLAS_PATH.read_text())
564
- except Exception:
565
- return []
566
- entries: list[dict] = []
567
- if isinstance(payload, dict):
568
- for key, value in payload.items():
569
- if str(key).startswith("_"):
570
- continue
571
- if not isinstance(value, dict):
572
- continue
573
- entries.append(
574
- {
575
- "kind": "project",
576
- "name": key,
577
- "path": value.get("path", "") or "",
578
- "domain": value.get("domain", "") or "",
579
- "aliases": value.get("aliases", []) or [],
580
- "services": value.get("services", {}) or {},
581
- "plugins": value.get("plugins", "") or value.get("plugin_path", "") or "",
582
- }
583
- )
584
- return entries
585
-
586
-
587
- def _artifact_entries() -> list[dict]:
588
- conn = get_db()
589
- try:
590
- rows = conn.execute(
591
- "SELECT canonical_name, kind, domain, state, uri, paths, ports, aliases FROM artifact_registry ORDER BY last_touched_at DESC LIMIT 100"
592
- ).fetchall()
593
- except Exception:
594
- return []
595
- return [
596
- {
597
- "kind": "artifact",
598
- "name": row["canonical_name"],
599
- "artifact_kind": row["kind"],
600
- "domain": row["domain"],
601
- "state": row["state"],
602
- "uri": row["uri"],
603
- "paths": row["paths"],
604
- "ports": row["ports"],
605
- "aliases": row["aliases"],
606
- }
607
- for row in rows
608
- ]
609
-
610
-
611
- def build_system_catalog() -> dict:
612
- catalog = {
613
- "core_tools": _parse_core_tools(),
614
- "plugin_tools": _plugin_entries(),
615
- "skills": _skill_entries(),
616
- "scripts": _script_entries(),
617
- "crons": _cron_entries(),
618
- "projects": _project_entries(),
619
- "artifacts": _artifact_entries(),
620
- }
621
- catalog["summary"] = {
622
- section: len(catalog.get(section) or [])
623
- for section in SECTION_ORDER
624
- }
625
- return catalog
626
-
627
-
628
- def search_system_catalog(query: str, *, section: str = "", limit: int = 20) -> list[dict]:
629
- catalog = build_system_catalog()
630
- query_tokens = _tokenize(query)
631
- sections = [section] if section in SECTION_ORDER else list(SECTION_ORDER)
632
- matches: list[dict] = []
633
- for section_name in sections:
634
- for entry in catalog.get(section_name) or []:
635
- haystack = " ".join(
636
- [
637
- section_name,
638
- str(entry.get("name", "") or ""),
639
- str(entry.get("display_name", "") or ""),
640
- str(entry.get("description", "") or ""),
641
- str(entry.get("source", "") or ""),
642
- str(entry.get("category", "") or ""),
643
- str(entry.get("plugin", "") or ""),
644
- str(entry.get("domain", "") or ""),
645
- str(entry.get("path", "") or ""),
646
- json.dumps(entry, ensure_ascii=False),
647
- ]
648
- )
649
- score = _score(query_tokens, haystack) if query_tokens else 0.5
650
- if query_tokens and score <= 0:
651
- continue
652
- row = dict(entry)
653
- row["_section"] = section_name
654
- row["_score"] = round(score, 4)
655
- matches.append(row)
656
- matches.sort(key=lambda row: (row["_score"], row.get("name", "")), reverse=True)
657
- return matches[: max(1, int(limit or 20))]
658
-
659
-
660
- def explain_tool(name: str) -> dict | None:
661
- clean = _normalize_text(name)
662
- if not clean:
663
- return None
664
- candidates = [clean]
665
- if clean.startswith("mcp__nexo__"):
666
- candidates.append(clean.split("mcp__nexo__", 1)[1])
667
- if "__" in clean:
668
- candidates.append(clean.split("__")[-1])
669
- seen: set[str] = set()
670
- for candidate in [item for item in candidates if item and not (item in seen or seen.add(item))]:
671
- exact = search_system_catalog(candidate, limit=200)
672
- for row in exact:
673
- if _normalize_text(row.get("name")) == candidate:
674
- return row
675
- for row in exact:
676
- if candidate in _normalize_text(row.get("name")):
677
- return row
678
- return None
679
-
680
-
681
- def format_catalog(catalog: dict, *, section: str = "", query: str = "", limit: int = 20) -> str:
682
- summary = catalog.get("summary") or {}
683
- if query:
684
- matches = search_system_catalog(query, section=section, limit=limit)
685
- if not matches:
686
- scope = section or "all sections"
687
- return f"No system-catalog matches for '{query}' in {scope}."
688
- lines = [f"SYSTEM CATALOG SEARCH — '{query}' ({len(matches)} match(es))"]
689
- for row in matches:
690
- label = row.get("_section", "")
691
- title = row.get("display_name") or row.get("name") or "(unnamed)"
692
- desc = _truncate(row.get("description") or row.get("path") or row.get("script") or "", 180)
693
- suffix = f" — {desc}" if desc else ""
694
- lines.append(f"- [{label}] {title}{suffix}")
695
- return "\n".join(lines)
696
-
697
- if section in SECTION_ORDER:
698
- entries = catalog.get(section) or []
699
- if not entries:
700
- return f"SYSTEM CATALOG — {section}: empty"
701
- lines = [f"SYSTEM CATALOG — {section} ({len(entries)})"]
702
- for row in entries[: max(1, int(limit or 20))]:
703
- title = row.get("display_name") or row.get("name") or "(unnamed)"
704
- desc = _truncate(row.get("description") or row.get("path") or row.get("script") or "", 180)
705
- suffix = f" — {desc}" if desc else ""
706
- lines.append(f"- {title}{suffix}")
707
- return "\n".join(lines)
708
-
709
- lines = ["SYSTEM CATALOG SUMMARY"]
710
- for name in SECTION_ORDER:
711
- lines.append(f"- {name}: {summary.get(name, 0)}")
712
- return "\n".join(lines)
713
-
714
-
715
- def format_tool_explanation(entry: dict | None) -> str:
716
- if not entry:
717
- return "Tool/capability not found in the live system catalog."
718
- params = entry.get("params") or []
719
- required = [param for param in params if param.get("required")]
720
- optional = [param for param in params if not param.get("required")]
721
- guide = _guide_for_tool(str(entry.get("name") or ""))
722
- examples = [{"title": "Quick example", "code": entry["quick_example"]}] if entry.get("quick_example") else []
723
- examples.extend(guide.get("examples", []))
724
- lines = [
725
- f"CATALOG ENTRY — {entry.get('name') or entry.get('display_name')}",
726
- f"Section: {entry.get('_section') or entry.get('kind')}",
727
- ]
728
- if entry.get("display_name"):
729
- lines.append(f"Display name: {entry['display_name']}")
730
- if entry.get("description"):
731
- lines.append(f"Description: {entry['description']}")
732
- if entry.get("category"):
733
- lines.append(f"Category: {entry['category']}")
734
- if entry.get("source"):
735
- lines.append(f"Source: {entry['source']}")
736
- if entry.get("plugin"):
737
- lines.append(f"Plugin: {entry['plugin']}")
738
- if entry.get("path"):
739
- lines.append(f"Path: {entry['path']}")
740
- if entry.get("line"):
741
- lines.append(f"Line: {entry['line']}")
742
- if entry.get("script"):
743
- lines.append(f"Script: {entry['script']}")
744
- if entry.get("runtime"):
745
- lines.append(f"Runtime: {entry['runtime']}")
746
- if entry.get("level"):
747
- lines.append(f"Level: {entry['level']}")
748
- if entry.get("mode"):
749
- lines.append(f"Mode: {entry['mode']}")
750
- if entry.get("execution_level"):
751
- lines.append(f"Execution level: {entry['execution_level']}")
752
- if entry.get("domain"):
753
- lines.append(f"Domain: {entry['domain']}")
754
- if entry.get("signature"):
755
- lines.append(f"Signature: {entry['signature']}")
756
- if required:
757
- lines.append("Required args:")
758
- for param in required:
759
- detail = param.get("description") or "No description."
760
- annotation = f" ({param['annotation']})" if param.get("annotation") else ""
761
- lines.append(f"- {param['name']}{annotation}: {detail}")
762
- if optional:
763
- lines.append("Optional args:")
764
- for param in optional:
765
- detail = param.get("description") or "Optional."
766
- annotation = f" ({param['annotation']})" if param.get("annotation") else ""
767
- default = f" Default: {param['default']}." if param.get("default", "") != "" else ""
768
- lines.append(f"- {param['name']}{annotation}: {detail}{default}")
769
- if guide.get("workflow"):
770
- lines.append("Workflow notes:")
771
- for item in guide["workflow"]:
772
- lines.append(f"- {item}")
773
- if examples:
774
- lines.append("Examples:")
775
- for example in examples:
776
- title = str(example.get("title") or "").strip()
777
- code = str(example.get("code") or "").strip()
778
- if title:
779
- lines.append(f"- {title}")
780
- if code:
781
- lines.append(f" {code}")
782
- if guide.get("common_errors"):
783
- lines.append("Common errors:")
784
- for item in guide["common_errors"]:
785
- lines.append(f"- {item}")
786
- return "\n".join(lines)