from fastcore.test import test,test_eq,expect_failpyskills API
Overview
A plugin system allowing Python packages to register “skills” — units of LLM-usable functionality — via standard Python entry points. An LLM host (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context.
Entry Point Convention
Packages register pyskills under the group pyskills:
[project.entry-points.pyskills]
my_skill = "mypackage.skill"The value is a module path (no :attribute needed). The module’s docstring first paragraph serves as the pyskill description.
Skill Module Contract
A pyskill module MUST have:
- Docstring — first paragraph is the short description shown to the LLM for pyskill selection; remaining will be read by the LLM to get full details on the pyskill.
A pyskill module MAY have:
__all__— the available symbols imported.
Discovery API
def list_pyskills() -> dict[str, str]Returns {name: description} for all registered pyskills, using find_spec + AST parsing — no imports.
import mypackage.skillStandard python native import
doc(mypackage.skill) # module overview: classes, functions, submodules
doc(SomeClass) # class detail: bases, __init__, methods, properties
doc(some_func) # function detail: full signature with docments
xdir(mypackage.skill) # filtered names for public symbolsInspect at increasing detail — works on any Python module, not just pyskills.
Host Integration
The host (e.g. solveit, claude code, codex, …) would:
- Call
list_pyskills()at startup to build a pyskill catalogue - Include the list with each prompt
- Call
import {module}followed bydoc({module})for chosen pyskills
Listing pyskills
ep = entry_points()
es = first(ep.select(group='pyskills', name='pyskills.skill'))
esEntryPoint(name='pyskills.skill', value='pyskills.skill', group='pyskills')
ep_desc
def ep_desc(
ep
):
First paragraph of docstring for entry point ep, without importing it
print(ep_desc(es))Pyskills is a plugin system allowing Python packages to register "skills" (units of LLM-usable functionality) via standard Python entry points. An LLM host (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context using standard imports.
list_pyskills
def list_pyskills(
):
Returns {module: description} for all pyskills. To load a module, use import {module} then view `doc({module}). NB: pyskills are THE critical way to extend functionality. ALWAYS check for pyskills to help with tasks. If unsure whether a particular pyskill might help, load it and grabs its docs to see!
list_pyskills(){'dialoghelper.solveitskill': 'Read, search, edit, and manage Solveit dialogs using dialoghelper.core, including dialog/message addressing, line-numbered inspection, targeted message edits, add/update/delete/copy/paste workflows, and safe editing patterns.',
'dialoghelper.termskill': 'Read and edit Solveit dialog (or Jupyter) .ipynb files from a CLI / script. Solveit is an online notebook application (like Jupyter with AI integration) where each notebook is called a "dialog" and is stored as an `.ipynb` file containing `code`, `note` (markdown), and `prompt` (markdown with a special delimiter) messages (aka "cells"). The `dialoghelper` package provides tools for reading, searching, adding, updating, and deleting those messages.',
'exhash.skill': 'Universal hash-verified text editing for local files. Use this when an LLM needs one safe editing interface for reading, previewing, and modifying text files.',
'cordslite.skill': 'Load this skill when an agent needs to search, summarize, or find information in Discord using cordslite. It covers read-only workflows for connecting to Discord, opening a guild, orienting through channels, searching messages, reading threads, and fetching attachments.',
'pyskills.edit': 'Functions for modifying files. Each editing operation returns unified diffs showing what changed, or `"none: No changes."`, or `"error: ..."`.',
'pyskills.skill': 'Pyskills is a plugin system allowing Python packages to register "skills" (units of LLM-usable functionality) via standard Python entry points. An LLM host (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context using standard imports.',
'repo.skill': 'Use this skill for repository investigation tasks involving local git history, GitHub PRs/issues/notifications, diffs, blame, code explanation, and source debugging.'}
allow
allow
def allow(
c:VAR_POSITIONAL, allow_policy:NoneType=None, # Callable that raises if call not allowed
):
Add all items in c to __pytools__, optionally constrained by allow_policy
__pytools__ is a defaultdict(set) mapping classes/modules to their allowed method/function names (or ... for all public methods). Values can be plain strings or (name, AllowPolicy) tuples for allow-checked methods. allow registers entries — callables are added under their module, dicts go directly into __pytools__.
def _test_fn(): pass
_test_fn.__module__ = '__main__'
_test_fn.__name__ = 'my_test_func'
allow(_test_fn)
assert 'my_test_func' in __pytools__[sys.modules['__main__']]
allow({str: ['zfill']})
assert 'zfill' in __pytools__[str]
allow({list: ...})
assert ... in __pytools__[list]
__pytools__[sys.modules['__main__']].discard('my_test_func')
allow(collections.Counter.most_common)
assert 'most_common' in __pytools__[collections.Counter]
__pytools__[collections.Counter].discard('most_common')import httpxdef chk_url(url, *args, **kwargs):
if not url.startswith('https://'): raise PermissionError()
allow({httpx.get: chk_url, httpx.post: chk_url})
assert ('get', chk_url) in __pytools__[httpx._api]
assert ('post', chk_url) in __pytools__[httpx._api]from fastcore.xtras import timed_cache@timed_cache(60)
def wrapped_tool(): return "ok"
allow(wrapped_tool)
assert 'wrapped_tool' in __pytools__[sys.modules['__main__']]Allow policies
chk_dest
def chk_dest(
p, ok_dests
):
Call self as a function.
chk_dest resolves a path and verifies it falls under one of the allowed destination prefixes. Raises PermissionError if not. Used by all AllowPolicy subclasses.
chk_dest('/tmp/foo.txt', ['/tmp'])
chk_dest('~/tmp/foo.txt', [Path.home()/'tmp'])
try: chk_dest('/etc/passwd', ['/tmp'])
except PermissionError: print("Correctly blocked /etc/passwd")Correctly blocked /etc/passwd
OpenWritePolicy
def OpenWritePolicy(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Check open() only when mode is writable
PathWritePolicy
def PathWritePolicy(
target_pos:NoneType=None, target_kw:NoneType=None
):
Check resolved Path self, optionally also target args
PosAllowPolicy
def PosAllowPolicy(
pos:int=0, kw:NoneType=None
):
Check positional/keyword arg is an allowed destination
AllowPolicy
def AllowPolicy(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Base for allow destination policies
Three AllowPolicy subclasses handle different allow-checking patterns.
pp = PosAllowPolicy(1, 'dst')
pp(None, ['src', '/tmp/ok'], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): pp(None, ['src', '/root/bad'], {}, {'ok_dests': ['/tmp']})
pwp = PathWritePolicy()
pwp(Path('/tmp/f.txt'), [], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): pwp(Path('/etc/f.txt'), [], {}, {'ok_dests': ['/tmp']})
owp = OpenWritePolicy()
owp(None, ['/tmp/f.txt', 'w'], {}, {'ok_dests': ['/tmp']})
owp(None, ['/etc/passwd', 'r'], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): owp(None, ['/root/f.txt', 'w'], {}, {'ok_dests': ['/tmp']})Allow policies are stored as (name, AllowPolicy) tuples directly inside __pytools__ sets.
doc / xdir
resolve
def resolve(
sym_nm:str, # Dotted symbol path, with optional [n] indexing, e.g. "module.attr.subattr[1]"
):
Resolve a dotted symbol string to its Python object, with optional [n] indexing
SymbolNotFound
def SymbolNotFound(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Common base class for all non-exit exceptions.
with expect_fail(SymbolNotFound): resolve('abc')
resolve('resolve'), resolve('len')(<function __main__.resolve(sym_nm: str)>, <function len(obj, /)>)
import pyskills.skillassert _is_own(pyskills.skill, 'skill_test_func')
assert _is_own(pyskills.skill, 'SkillTestClass')
assert not _is_own(pyskills.skill, 'inspect')
assert not _is_own(pyskills.skill, '_private')xdir returns filtered (name, obj) pairs for a module’s public symbols (respecting __all__, skipping private names), or a class’s __init__ and public methods. For modules, it also includes sibling submodules explicitly imported by the module.
xdir
def xdir(
sym:str | object
):
Filtered names for public symbols of a module or class (or anything with __dir__)
x = xdir(pyskills.skill)
test_eq(x, xdir('pyskills.skill'))
x['SkillTestClass',
'async_skill_test_func',
'skill_test_func',
'pyskills.createskill']
doc
def doc(
sym:str | object
)->str:
Docstring of a module, class, function, instance or any other Python object.
For a function, doc renders the docstring and full signature with docments (parameter comments):
d = doc(pyskills.skill.skill_test_func)
test_eq(d, doc('pyskills.skill.skill_test_func'))
print(d)def skill_test_func(
x:int=0, # the input
)->str: # the output
"""A test function"""
fmt_sig
def fmt_sig(
f
):
Call self as a function.
This is a simple helper that removes module names from str(signature(...)):
def f(a:Path|str = "aa"): ...
print(fmt_sig(f))(a: Path | str = 'aa')
For a class, doc shows the class hierarchy, docstring, __init__ signature, and all public methods/properties with their first docstring line:
print(doc(pyskills.skill.SkillTestClass))class SkillTestClass(str):
"""Some class.
More info about it."""
def __init__(self): ...
def f(self, x: int = 0) -> str: ... # A test method
@property
def g(self) -> str: ... # A test prop
For a module, doc shows the docstring, all public classes and functions with their signatures and first docstring line, submodules, and any allow() calls:
print(doc(pyskills.skill)[-450:])from pyskills import createskill; doc(createskill)` for how to build and register your own pyskill modules, including the allow/policy system.
"""
## types:
- class SkillTestClass(str): ... # Some class.
## functions:
- async def async_skill_test_func(x: int = 0) -> str: ... # A test function
- def skill_test_func(x: int = 0) -> str: ... # A test function
## submodules:
pyskills.createskill: ... # How to create a pyskills pyskill module.
class Pet:
def __init__(self, name, sound):
self.name,self.sound = name,sound
def speak(self):
"Make the pet's sound"
return f'{self.name} says {self.sound}!'
def __dir__(self): return ['name', 'sound', 'speak']
p = Pet('Rex', 'woof')
print(doc(p))Instance of type Pet:
- name: str = 'Rex'
- sound: str = 'woof'
- speak() # Make the pet's sound
docfind
def docfind(
o:str | object, q:str, n:int=2, _pre:str=''
):
Search doc() recursively through xdir(o), looking at submodules, classes, and functions, to depth n
docfind(pyskills.skill, 'test')[' // # module pyskills.skill:',
'SkillTestClass // class SkillTestClass(str):',
'SkillTestClass.f // def f(',
'SkillTestClass.g // def g(',
'async_skill_test_func // async def async_skill_test_func(',
'skill_test_func // def skill_test_func(']
import pyskills.editprint(doc(pyskills.edit))# module pyskills.edit:
"""Functions for creating, viewing, and modifying files. Each editing operation returns unified diffs showing what changed.
## File viewing, creating, and editing
File tools take a filesystem path as the first argument, e.g:
file_view('~/a/b.py', 3)
file_create('~/a/b/c.py', 'content here')
file_str_replace('myfile.py', 'old_name', 'new_name')
file_del_lines('myfile.py', 2, 4)
## Line filtering
`file_str_replace`, `file_strs_replace`, and `file_del_lines` support `re_filter` and `invert_filter` for targeting only lines matching (or not matching) a regex, like ex's `g//` and `g!//`. Combined with `start_line`/`end_line` to restrict to a region, e.g:
file_del_lines('myfile.py', 1, -1, re_filter=r'^\s*#') # delete all comment lines
Docs: https://AnswerDotAI.github.io/pyskillsedit.html.md
"""
## functions:
- def file_insert_line(path: str, insert_line: int, new_str: str): ... # Insert new_str at specified line number
- def file_str_replace(path: str, old_str: str, new_str: str, start_line: int = None, end_line: int = None, n_matches: int = None, re_filter: str = None, invert_filter: bool = False): ... # Replace occurrence(s) of old_str with new_str
- def file_strs_replace(path: str, old_strs: list[str], new_strs: list[str], start_line: int = None, end_line: int = None, n_matches: int = None, re_filter: str = None, invert_filter: bool = False): ... # Replace multiple strings simultaneously
- def file_replace_lines(path: str, start_line: int, end_line: int = None, new_content: str = ''): ... # Replace line range with new content
- def file_del_lines(path: str, start_line: int, end_line: int = None, re_filter: str = None, invert_filter: bool = False): ... # Delete line range
- def file_view(path: str, startline: int = 1, endline: int = None): ... # Read file contents, optionally limited to 1-based line range
- def file_create(path: str, contents: str): ... # Create a new file with contents. Error if file exists.
- def file_edit(f, name=None)
- def insert_line(text: str, insert_line: int, new_str: str): ... # Insert new_str at specified line number
- def str_replace(text: str, old_str: str, new_str: str, start_line: int = None, end_line: int = None, n_matches: int = None, re_filter: str = None, invert_filter: bool = False): ... # Replace occurrence(s) of old_str with new_str
- def strs_replace(text: str, old_strs: list[str], new_strs: list[str], start_line: int = None, end_line: int = None, n_matches: int = None, re_filter: str = None, invert_filter: bool = False): ... # Replace multiple strings simultaneously
- def replace_lines(text: str, start_line: int, end_line: int = None, new_content: str = ''): ... # Replace line range with new content
- def del_lines(text: str, start_line: int, end_line: int = None, re_filter: str = None, invert_filter: bool = False): ... # Delete line range
print(doc(pyskills.edit.file_create))def file_create(
path:str, # Path to create (expands `~` if needed)
contents:str, # Contents of file to create
):
"""Create a new file with contents. Error if file exists."""
Skills registration
Pyskills can be added as standard modules with pyproject entrypoints. But for convenience, they can also be added to a custom pyskills XDG directory, which is automatically added to sys.path.
ensure_pyskills_dir
def ensure_pyskills_dir(
):
Create xdg pyskills dir and .pth file if needed
pyskills_dir
def pyskills_dir(
):
Directory for user pyskills
pyskills_dir returns the XDG data home path for user pyskills. ensure_pyskills_dir creates that directory if needed and writes a .pth file into site-packages so Python automatically adds it to sys.path. You can drop pyskill modules there without manual path configuration and which can be available across venvs.
clear_mod
def clear_mod(
prefix
):
Clear modules starting with prefix from python caches
clear_mod purges all cached modules matching a prefix from sys.modules and invalidates import caches, ensuring a fresh import on next access. Used after enabling/disabling pyskills so changes take effect immediately.
register_pyskill
def register_pyskill(
name, docstr, code:str=''
):
Register a pyskill module name in the xdg pyskills dir
enable_pyskill
def enable_pyskill(
name
):
Enable pyskill name by creating its dist-info entry point
enable_pyskill creates a minimal dist-info directory with an entry point so the pyskill is listed. register_pyskill also writes an actual module file (with docstring and code) into the pyskills directory, and creates any needed __init__.py files for nested packages.
This lets you programmatically create and register a pyskill without a full package install.
disable_pyskill
def disable_pyskill(
name
):
Disable pyskill name by removing its dist-info entry point
disable_pyskill removes the dist-info directory for a pyskill, so it no longer appears in entry point discovery. It also clears the module cache so the pyskill is fully unloaded.
delete_pyskill
def delete_pyskill(
name
):
Delete pyskill name module files and dist-info