tools/skills: list, read, and search skills

skill_list shows what was discovered (name, source root, directory, description)
together with the copies a name collision shadowed and the notes from repaired
frontmatter, so a third-party skill never disappears quietly. skill_read
addresses a skill by name and defaults to its SKILL.md, reusing read_file for
ranges, line numbers, and the continuation token; a bundled file is resolved
inside the skill directory only, symlinks included. skill_search greps every
visible skill (or one) and rebases each hit on its skill, which makes a match
attributable without absolute paths.

Reads need no grant: the host registers the roots as static read-only roots on
the workspace policy, so read_file/grep_files see skill directories too while
write access stays denied — skill writes will go through their own confirm.
Grep used to drop every file outside the primary workspace root, which also hid
hits under a granted directory; those now come back as absolute paths.
This commit is contained in:
2026-09-22 22:42:45 +08:00
parent a9f58428bf
commit 1d05da62c2
12 changed files with 599 additions and 3 deletions
+47
View File
@@ -821,6 +821,53 @@ def test_every_builtin_tool_has_a_pretty_line() -> None:
assert missing == []
async def test_pretty_skill_tools(capsys: pytest.CaptureFixture[str]) -> None:
await render_events(
_aiter(
[_pretty_call("skill_list", "{}"), _result("skills: 3\npdf-processing [plyngent-user] /x — Extract PDFs")]
)
)
out = capsys.readouterr().out
assert "* Skills (3 skills)" in out
await render_events(
_aiter(
[
_pretty_call("skill_read", '{"name": "pdf-processing"}'),
_result("L1-40\n# PDF processing\n"),
]
)
)
out = capsys.readouterr().out
assert "* Skill 'pdf-processing' L1-40 (done)" in out
await render_events(
_aiter(
[
_pretty_call("skill_read", '{"name": "pdf-processing", "file": "scripts/run.sh"}'),
_result("L1-3\n#!/bin/sh\n"),
]
)
)
out = capsys.readouterr().out
assert "* Skill 'pdf-processing' scripts/run.sh L1-3 (done)" in out
await render_events(
_aiter(
[
_pretty_call("skill_search", '{"pattern": "pdftotext"}'),
_result("alpha/SKILL.md:3: Use pdftotext.\nbeta/notes.md:1: pdftotext again"),
]
)
)
out = capsys.readouterr().out
assert "* Skill Search 'pdftotext' (2 matches in 2 skills)" in out
await render_events(_aiter([_pretty_call("skill_search", '{"pattern": "nope"}'), _result("(no matches)")]))
out = capsys.readouterr().out
assert "* Skill Search 'nope' (no matches)" in out
async def test_pretty_prefix_shown_while_call_runs(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],