nexo-brain 5.3.13 → 5.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/nexo-brain.js +52 -1
- package/package.json +1 -1
- package/src/crons/sync.py +18 -4
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_entities.py +1 -1
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/agents.py +10 -3
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/schedule.py +2 -1
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/requirements.txt +1 -1
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/runtime_power.py +18 -1
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-cron-wrapper.sh +7 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup.plist +1 -1
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.dashboard.plist +1 -1
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.evolution.plist +1 -1
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.immune.plist +1 -1
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.postmortem.plist +1 -1
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.self-audit.plist +1 -1
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.synthesis.plist +1 -1
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/launchagents/com.nexo.watchdog.plist +1 -1
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/script-template.py +5 -4
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-script-template.py +2 -1
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,786 @@
|
|
|
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)
|