mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
cli/skills: open the skill directory instead of printing SKILL.md
This commit is contained in:
+25
-13
@@ -112,11 +112,17 @@ def _run_blocking_editor(editor_cmd: str, path: Path) -> None:
|
||||
raise click.ClickException(msg)
|
||||
|
||||
|
||||
def _open_with_system_default(path: Path) -> None:
|
||||
def _open_failure(reason: str, hint: str) -> str:
|
||||
"""Compose an open failure message, appending the caller's fallback hint."""
|
||||
return f"{reason}; {hint}" if hint else reason
|
||||
|
||||
|
||||
def _open_with_system_default(path: Path, *, hint: str = "") -> None:
|
||||
"""Open *path* with the OS file association (non-blocking).
|
||||
|
||||
Linux: ``xdg-open``; macOS: ``open``; Windows: ``os.startfile``.
|
||||
Does not wait for the application to exit.
|
||||
Does not wait for the application to exit. *hint* names the caller's own
|
||||
fallback (an editor, a printed path) and is appended to failures.
|
||||
"""
|
||||
resolved = str(path.resolve())
|
||||
if sys.platform == "win32":
|
||||
@@ -141,19 +147,22 @@ def _open_with_system_default(path: Path) -> None:
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
cmd = argv[0]
|
||||
msg = (
|
||||
f"{cmd} not found and neither VISUAL nor EDITOR is set; "
|
||||
"set VISUAL or EDITOR to a blocking editor (e.g. nano, vim, codium --wait)"
|
||||
)
|
||||
raise click.ClickException(msg) from exc
|
||||
raise click.ClickException(_open_failure(f"{argv[0]} not found", hint)) from exc
|
||||
except OSError as exc:
|
||||
msg = f"failed to open with system default: {exc}"
|
||||
raise click.ClickException(msg) from exc
|
||||
raise click.ClickException(_open_failure(f"failed to open with system default: {exc}", hint)) from exc
|
||||
|
||||
if completed.returncode != 0:
|
||||
msg = f"system open failed (exit {completed.returncode}); set VISUAL or EDITOR to a blocking editor"
|
||||
raise click.ClickException(msg)
|
||||
raise click.ClickException(_open_failure(f"system open failed (exit {completed.returncode})", hint))
|
||||
|
||||
|
||||
def open_directory(path: Path) -> None:
|
||||
"""Reveal *path* (a directory) in the OS file manager (non-blocking).
|
||||
|
||||
A folder is not an editor target, so ``$VISUAL`` / ``$EDITOR`` are never
|
||||
consulted: the platform opener decides. The caller prints *path* too, which
|
||||
is the way in when the host has no opener at all.
|
||||
"""
|
||||
_open_with_system_default(path, hint="open the printed path manually")
|
||||
|
||||
|
||||
def open_in_editor(
|
||||
@@ -189,7 +198,10 @@ def open_in_editor(
|
||||
msg = "neither VISUAL nor EDITOR is set"
|
||||
raise click.ClickException(msg)
|
||||
|
||||
_open_with_system_default(path)
|
||||
_open_with_system_default(
|
||||
path,
|
||||
hint="set VISUAL or EDITOR to a blocking editor (e.g. nano, vim, codium --wait)",
|
||||
)
|
||||
return "system"
|
||||
|
||||
|
||||
|
||||
+33
-12
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from plyngent.agent import ToolRegistry
|
||||
from plyngent.cli.state import ReplState, YoloMode
|
||||
from plyngent.skills import SkillStore
|
||||
|
||||
_COMPACT_PREVIEW = 400
|
||||
_ON_OFF_CHOICES = ("on", "off")
|
||||
@@ -1244,21 +1245,41 @@ def mcp_slash_cmd(state: ReplState, action: str | None) -> None:
|
||||
_print_mcp_status(state)
|
||||
|
||||
|
||||
def _open_skill_directory(store: SkillStore, name: str) -> None:
|
||||
"""Reveal a skill's directory in the host file manager (``/skills open``).
|
||||
|
||||
Reading a skill is a file-manager job, not a render of the model-facing
|
||||
``skill_read`` output; the printed path is the fallback on a headless host.
|
||||
"""
|
||||
from plyngent.cli.editor import open_directory
|
||||
|
||||
skill = store.find(name)
|
||||
if skill is None:
|
||||
click.echo(f"error: unknown skill {name!r}; /skills lists the discovered ones")
|
||||
return
|
||||
try:
|
||||
open_directory(skill.path)
|
||||
except click.ClickException as exc:
|
||||
click.echo(f"error: {exc}")
|
||||
click.echo(f"skill directory: {skill.path}")
|
||||
return
|
||||
click.echo(f"opened {skill.path}")
|
||||
|
||||
|
||||
@slash.command("skills")
|
||||
@click.argument("action", required=False, type=click.Choice(["list", "search", "read", "reload"]))
|
||||
@click.argument("action", required=False, type=click.Choice(["list", "search", "open", "reload"]))
|
||||
@click.argument("target", required=False)
|
||||
@click.option("--file", "file", default="", help="File inside the skill (with ``read``).")
|
||||
@click.pass_obj
|
||||
def skills_slash_cmd(state: ReplState, action: str | None, target: str | None, file: str) -> None:
|
||||
"""List skills, search or read them, or rescan the roots.
|
||||
def skills_slash_cmd(state: ReplState, action: str | None, target: str | None) -> None:
|
||||
"""List skills, search them, open one's directory, or rescan the roots.
|
||||
|
||||
``/skills`` or ``/skills list`` — discovered skills with their source root.
|
||||
``/skills search PATTERN`` — regex over every skill's contents.
|
||||
``/skills read NAME [--file F]`` — print a skill's SKILL.md (or one file).
|
||||
``/skills open NAME`` — reveal the skill's directory in the file manager.
|
||||
``/skills reload`` — rescan the roots after an external edit.
|
||||
"""
|
||||
from plyngent.tools.context import bind_tool_context
|
||||
from plyngent.tools.skills import SKILL_TOOL_NAMES, format_skill_table, skill_read, skill_search
|
||||
from plyngent.tools.skills import SKILL_TOOL_NAMES, format_skill_table, skill_search
|
||||
|
||||
if state.skills is None:
|
||||
click.echo("skills: disabled (set [skills].enabled = true)")
|
||||
@@ -1269,6 +1290,12 @@ def skills_slash_cmd(state: ReplState, action: str | None, target: str | None, f
|
||||
_slash_rebuild_tools_if_on(state)
|
||||
click.echo(f"skills reloaded: {len(state.skills.skills)} visible; tools rebuilt")
|
||||
return
|
||||
if act == "open":
|
||||
if not target:
|
||||
msg = "/skills open requires a skill name"
|
||||
raise click.UsageError(msg)
|
||||
_open_skill_directory(state.skills, target)
|
||||
return
|
||||
# The tool handlers read the bound instance (the registry binds it for a
|
||||
# model call; a slash command has to do it itself).
|
||||
with bind_tool_context(instance=state.instance_state, session=state.session_state):
|
||||
@@ -1278,12 +1305,6 @@ def skills_slash_cmd(state: ReplState, action: str | None, target: str | None, f
|
||||
raise click.UsageError(msg)
|
||||
click.echo(_await(skill_search.handler(target)))
|
||||
return
|
||||
if act == "read":
|
||||
if not target:
|
||||
msg = "/skills read requires a skill name"
|
||||
raise click.UsageError(msg)
|
||||
click.echo(_await(skill_read.handler(target, file or "SKILL.md")))
|
||||
return
|
||||
click.echo(format_skill_table(state.skills))
|
||||
if not state.tools_enabled:
|
||||
click.secho(
|
||||
|
||||
Reference in New Issue
Block a user