mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 21:38:40 +03:00
fix(apps): preserve legacy CLI app skills
This commit is contained in:
@@ -219,7 +219,13 @@ def _safe_skill_name(name: str) -> str:
|
||||
return f"cli-app-{clean or 'app'}"
|
||||
|
||||
|
||||
def _skill_relative_path(name: str) -> str:
|
||||
def _legacy_skill_name(name: str) -> str:
|
||||
"""Return the workspace skill name emitted before Agent Plugins support."""
|
||||
clean = _SAFE_NAME_RE.sub("-", name.lower()).strip("-")
|
||||
return f"cli-app-{clean or 'app'}"
|
||||
|
||||
|
||||
def _plugin_skill_relative_path(name: str) -> str:
|
||||
skill_name = _safe_skill_name(name)
|
||||
return f"plugins/{skill_name}/skills/{skill_name}/SKILL.md"
|
||||
|
||||
@@ -622,7 +628,7 @@ class CliAppManager:
|
||||
"name": installed_name,
|
||||
"entry_point": entry_point,
|
||||
"source": str(data.get("source") or ""),
|
||||
"skill": _skill_relative_path(installed_name),
|
||||
"skill": self.skill_relative_path(installed_name),
|
||||
"tool": "run_cli_app",
|
||||
}
|
||||
)
|
||||
@@ -653,7 +659,16 @@ class CliAppManager:
|
||||
return self.workspace / "plugins" / skill_name / "skills" / skill_name / "SKILL.md"
|
||||
|
||||
def _legacy_skill_path(self, name: str) -> Path:
|
||||
return self.workspace / "skills" / _safe_skill_name(name) / "SKILL.md"
|
||||
return self.workspace / "skills" / _legacy_skill_name(name) / "SKILL.md"
|
||||
|
||||
def _installed_skill_path(self, name: str) -> Path:
|
||||
path = self._skill_path(name)
|
||||
legacy_path = self._legacy_skill_path(name)
|
||||
return legacy_path if not path.is_file() and legacy_path.is_file() else path
|
||||
|
||||
def skill_relative_path(self, name: str) -> str:
|
||||
"""Return the existing skill path, falling back to the canonical plugin path."""
|
||||
return self._installed_skill_path(name).relative_to(self.workspace).as_posix()
|
||||
|
||||
def _app_payload(
|
||||
self,
|
||||
@@ -690,7 +705,7 @@ class CliAppManager:
|
||||
"status": status,
|
||||
"logo_url": logo_url,
|
||||
"brand_color": brand_color,
|
||||
"skill_installed": self._skill_path(name).is_file(),
|
||||
"skill_installed": self._installed_skill_path(name).is_file(),
|
||||
"manifest": self._manifest_payload(app, logo_url=logo_url, brand_color=brand_color),
|
||||
}
|
||||
|
||||
@@ -726,7 +741,7 @@ class CliAppManager:
|
||||
name = str(app["name"])
|
||||
entry_point = str(app.get("entry_point") or "")
|
||||
strategy = self._strategy(app)
|
||||
skill_path = _skill_relative_path(name)
|
||||
skill_path = _plugin_skill_relative_path(name)
|
||||
plugin_path = f"plugins/{_safe_skill_name(name)}"
|
||||
capabilities = [
|
||||
compact_dict({
|
||||
|
||||
@@ -29,6 +29,9 @@ def runtime_lines_for_request(
|
||||
"""Return CLI App annotations from an immutable request snapshot."""
|
||||
structured = metadata.get("cli_apps") if isinstance(metadata, Mapping) else None
|
||||
if isinstance(structured, list):
|
||||
from nanobot.apps.cli import CliAppManager
|
||||
|
||||
manager = CliAppManager(workspace=workspace)
|
||||
structured_items = cast(list[Any], structured)
|
||||
mentions = [
|
||||
cast(Mapping[str, Any], item) for item in structured_items
|
||||
@@ -41,8 +44,7 @@ def runtime_lines_for_request(
|
||||
f"@{str(item['name']).strip().lower()} "
|
||||
f"(installed; tool=run_cli_app; "
|
||||
f"entry_point={str(item.get('entry_point') or 'unknown')}; "
|
||||
f"skill=plugins/cli-app-{str(item['name']).strip().lower()}/skills/"
|
||||
f"cli-app-{str(item['name']).strip().lower()}/SKILL.md). "
|
||||
f"skill={manager.skill_relative_path(str(item['name']))}). "
|
||||
"Read the skill when useful, then run this app with `run_cli_app`; do not bypass it with shell."
|
||||
for item in mentions
|
||||
if str(item.get("name") or "").strip()
|
||||
|
||||
@@ -285,7 +285,7 @@ class Session:
|
||||
)
|
||||
cli_lines.append(
|
||||
f"[CLI App Attachment: @{name}; tool=run_cli_app; entry_point={entry_point}; "
|
||||
f"skill=plugins/cli-app-{name}/skills/cli-app-{name}/SKILL.md]"
|
||||
f"skill=skills/cli-app-{name}/SKILL.md]"
|
||||
)
|
||||
if cli_lines:
|
||||
breadcrumbs = "\n".join(cli_lines)
|
||||
|
||||
@@ -491,8 +491,7 @@ def test_get_history_synthesizes_cli_app_attachment_breadcrumb():
|
||||
"content": (
|
||||
"please use @drawio\n"
|
||||
"[CLI App Attachment: @drawio; tool=run_cli_app; "
|
||||
"entry_point=cli-anything-drawio; "
|
||||
"skill=plugins/cli-app-drawio/skills/cli-app-drawio/SKILL.md]"
|
||||
"entry_point=cli-anything-drawio; skill=skills/cli-app-drawio/SKILL.md]"
|
||||
),
|
||||
}]
|
||||
|
||||
|
||||
@@ -882,6 +882,34 @@ def test_mentioned_installed_apps_only_returns_installed_mentions(tmp_path: Path
|
||||
]
|
||||
|
||||
|
||||
def test_legacy_underscored_skill_remains_visible_and_removable(tmp_path: Path) -> None:
|
||||
manager = _manager(tmp_path)
|
||||
legacy = manager.workspace / "skills" / "cli-app-unimol_tools" / "SKILL.md"
|
||||
legacy.parent.mkdir(parents=True)
|
||||
legacy.write_text(
|
||||
"---\nname: cli-app-unimol_tools\ndescription: Legacy Uni-Mol app.\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager._save_installed(
|
||||
{"unimol_tools": {"entry_point": "cli-anything-unimol-tools", "source": "harness"}}
|
||||
)
|
||||
|
||||
app = {
|
||||
"name": "unimol_tools",
|
||||
"entry_point": "cli-anything-unimol-tools",
|
||||
"install_cmd": "pip install cli-anything-unimol-tools",
|
||||
}
|
||||
|
||||
assert manager._app_payload(app, manager._load_installed())["skill_installed"] is True
|
||||
assert manager.mentioned_installed_apps("use @unimol_tools")[0]["skill"] == (
|
||||
"skills/cli-app-unimol_tools/SKILL.md"
|
||||
)
|
||||
|
||||
manager.remove_skill("unimol_tools")
|
||||
|
||||
assert not legacy.exists()
|
||||
|
||||
|
||||
def test_install_rejects_unknown_and_script_strategy(tmp_path: Path) -> None:
|
||||
manager = _manager(tmp_path)
|
||||
_seed_catalog(manager)
|
||||
|
||||
@@ -62,3 +62,24 @@ def test_structured_cli_app_attachment_injects_runtime_metadata(tmp_path):
|
||||
assert "tool=run_cli_app" in joined
|
||||
assert "entry_point=cli-anything-zoom" in joined
|
||||
assert "skill=plugins/cli-app-zoom/skills/cli-app-zoom/SKILL.md" in joined
|
||||
|
||||
|
||||
def test_structured_cli_app_attachment_uses_existing_legacy_skill(tmp_path):
|
||||
legacy = tmp_path / "skills" / "cli-app-unimol_tools" / "SKILL.md"
|
||||
legacy.parent.mkdir(parents=True)
|
||||
legacy.write_text("# Legacy Uni-Mol\n", encoding="utf-8")
|
||||
|
||||
lines = runtime_lines(
|
||||
SimpleNamespace(
|
||||
content="please use @unimol_tools",
|
||||
metadata={
|
||||
"cli_apps": [{
|
||||
"name": "unimol_tools",
|
||||
"entry_point": "cli-anything-unimol-tools",
|
||||
}],
|
||||
},
|
||||
),
|
||||
tmp_path,
|
||||
)
|
||||
|
||||
assert "skill=skills/cli-app-unimol_tools/SKILL.md" in "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user