__init__.py
"""
inspecting-skills: Discover and import Python code across skills.
Solves the dash-underscore naming mismatch between skill directories
(e.g., browsing-bluesky) and Python imports (e.g., browsing_bluesky).
Quick Start:
from inspecting_skills import setup_skill_path, skill_import
# Enable transparent imports
setup_skill_path("/home/user/claude-skills")
from browsing_bluesky import search_posts
# Or import explicitly
bsky = skill_import("browsing-bluesky")
posts = bsky.search_posts("python")
"""
from .scripts import (
ModuleIndex,
SkillIndex,
# Discovery
SkillLayout,
# Indexing
Symbol,
discover_all_skills,
discover_skill,
extract_symbols,
find_skill_by_name,
generate_registry,
# Importing
get_skills_root,
index_all_skills,
index_skill,
list_importable_skills,
module_to_skill_name,
register_skill,
set_skills_root,
setup_skill_path,
skill_import,
skill_name_to_module,
)
__all__ = [
# Discovery
"SkillLayout",
"discover_skill",
"discover_all_skills",
"skill_name_to_module",
"module_to_skill_name",
"find_skill_by_name",
# Indexing
"Symbol",
"ModuleIndex",
"SkillIndex",
"extract_symbols",
"index_skill",
"index_all_skills",
"generate_registry",
# Importing
"get_skills_root",
"set_skills_root",
"setup_skill_path",
"skill_import",
"register_skill",
"list_importable_skills",
]
CHANGELOG.md
# inspecting-skills - Changelog
All notable changes to the `inspecting-skills` skill are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [1.0.2] - 2026-01-26
### Fixed
- Remove circular symlink causing zip issues
## [1.0.1] - 2026-01-26
### Fixed
- Remove circular symlink causing zip issues
## [1.0.0] - 2026-01-26
### Added
- Add skill for cross-skill Python imports (#217)
## 1.0.3 — 2026-07-26
- Repointed the integration section from mapping-codebases (deprecated) to tree-sitting.
scripts/__init__.py
"""Scripts for skill inspection and import utilities."""
from .discover import (
SkillLayout,
discover_all_skills,
discover_skill,
find_skill_by_name,
module_to_skill_name,
skill_name_to_module,
)
from .index import (
ModuleIndex,
SkillIndex,
Symbol,
extract_symbols,
generate_registry,
index_all_skills,
index_skill,
)
from .skill_imports import (
get_skills_root,
list_importable_skills,
register_skill,
set_skills_root,
setup_skill_path,
skill_import,
)
__all__ = [
# Discovery
"SkillLayout",
"discover_skill",
"discover_all_skills",
"skill_name_to_module",
"module_to_skill_name",
"find_skill_by_name",
# Indexing
"Symbol",
"ModuleIndex",
"SkillIndex",
"extract_symbols",
"index_skill",
"index_all_skills",
"generate_registry",
# Importing
"get_skills_root",
"set_skills_root",
"setup_skill_path",
"skill_import",
"register_skill",
"list_importable_skills",
]
scripts/discover.py
"""
discover.py - Scan skill directories for Python code.
Detects three common layouts:
1. scripts/ subdirectory (e.g., browsing-bluesky/scripts/*.py)
2. Root-level .py files (e.g., remembering/*.py)
3. Full packages with __init__.py
"""
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class SkillLayout:
"""Describes a skill's code structure."""
name: str # e.g., "browsing-bluesky"
path: Path # e.g., /home/user/claude-skills/browsing-bluesky
layout_type: str # "scripts" | "root" | "package" | "none"
python_files: list[Path] = field(default_factory=list)
has_init: bool = False # Has __init__.py (importable as package)
entry_module: str | None = None # Primary module name for import
def discover_skill(skill_path: Path) -> SkillLayout | None:
"""
Analyze a skill directory to determine its code layout.
Args:
skill_path: Path to a skill directory (must contain SKILL.md)
Returns:
SkillLayout describing the skill's code structure, or None if invalid
"""
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
return None
name = skill_path.name
python_files = []
layout_type = "none"
has_init = False
entry_module = None
# Check for __init__.py at root (makes it a package)
root_init = skill_path / "__init__.py"
if root_init.exists():
has_init = True
entry_module = name.replace("-", "_") # browsing-bluesky -> browsing_bluesky
# Check for scripts/ subdirectory
scripts_dir = skill_path / "scripts"
if scripts_dir.is_dir():
scripts_py_files = list(scripts_dir.glob("*.py"))
if scripts_py_files:
layout_type = "scripts"
python_files.extend(scripts_py_files)
# Check for scripts/__init__.py
if (scripts_dir / "__init__.py").exists():
has_init = True
if not entry_module:
entry_module = name.replace("-", "_")
# Check for root-level .py files (excluding __init__.py)
root_py_files = [
f for f in skill_path.glob("*.py")
if f.name != "__init__.py"
]
if root_py_files:
if layout_type == "none":
layout_type = "root" if not has_init else "package"
elif layout_type == "scripts":
# Has both scripts/ and root-level - prefer "package" if has __init__
if has_init:
layout_type = "package"
python_files.extend(root_py_files)
# Determine entry module from __init__.py exports if not set
if has_init and not entry_module:
entry_module = name.replace("-", "_")
return SkillLayout(
name=name,
path=skill_path,
layout_type=layout_type,
python_files=python_files,
has_init=has_init,
entry_module=entry_module
)
def discover_all_skills(
skills_root: Path,
exclude: set[str] | None = None
) -> list[SkillLayout]:
"""
Discover all skills with Python code in a skills repository.
Args:
skills_root: Root directory containing skill directories
exclude: Set of skill names to exclude (e.g., {"templates", ".uploads"})
Returns:
List of SkillLayout objects for skills with Python code
"""
exclude = exclude or {"templates", "uploads", ".uploads", "scripts", ".github", ".git"}
skills = []
for entry in sorted(skills_root.iterdir()):
if not entry.is_dir():
continue
if entry.name.startswith(".") or entry.name.startswith("_"):
continue
if entry.name in exclude:
continue
layout = discover_skill(entry)
if layout and layout.layout_type != "none":
skills.append(layout)
return skills
def skill_name_to_module(skill_name: str) -> str:
"""
Convert a skill name (dash-delimited) to a valid Python module name.
Examples:
browsing-bluesky -> browsing_bluesky
creating-mcp-servers -> creating_mcp_servers
"""
return skill_name.replace("-", "_")
def module_to_skill_name(module_name: str) -> str:
"""
Convert a Python module name back to the skill name.
Examples:
browsing_bluesky -> browsing-bluesky
creating_mcp_servers -> creating-mcp-servers
Note: This is lossy - underscores could have been dashes OR underscores.
Use skill discovery to find the actual skill directory.
"""
return module_name.replace("_", "-")
def find_skill_by_name(
name: str,
skills_root: Path
) -> SkillLayout | None:
"""
Find a skill by name or module name.
Handles both forms:
- "browsing-bluesky" (directory name)
- "browsing_bluesky" (Python module name)
Args:
name: Skill name (either form)
skills_root: Root directory containing skill directories
Returns:
SkillLayout if found, None otherwise
"""
# Try exact match first
skill_path = skills_root / name
if skill_path.is_dir():
return discover_skill(skill_path)
# Try converting underscore to dash
dash_name = name.replace("_", "-")
skill_path = skills_root / dash_name
if skill_path.is_dir():
return discover_skill(skill_path)
# Try converting dash to underscore (less common)
underscore_name = name.replace("-", "_")
skill_path = skills_root / underscore_name
if skill_path.is_dir():
return discover_skill(skill_path)
return None
scripts/index.py
"""
index.py - Extract callable symbols from Python files using AST parsing.
Uses Python's stdlib ast module for reliable parsing. Tree-sitter is available
via tree-sitting for multi-language support, but stdlib ast is preferred
for Python-only indexing due to zero external dependencies.
"""
import ast
import json
from dataclasses import dataclass, field
from pathlib import Path
from .discover import SkillLayout, discover_all_skills, skill_name_to_module
@dataclass
class Symbol:
"""A callable symbol (function, class, method) in a module."""
name: str
kind: str # "function" | "class" | "method" | "variable"
signature: str | None = None # e.g., "(self, x: int, y: str = 'default')"
line: int | None = None # 1-indexed line number
docstring: str | None = None # First line of docstring
children: list["Symbol"] = field(default_factory=list) # Methods for classes
@dataclass
class ModuleIndex:
"""Index of a single Python module."""
file_path: str # Relative path from skill root
module_name: str # Python import name
symbols: list[Symbol] = field(default_factory=list)
imports: list[str] = field(default_factory=list)
@dataclass
class SkillIndex:
"""Complete index of a skill's exportable code."""
name: str # e.g., "browsing-bluesky"
module_name: str # e.g., "browsing_bluesky"
layout_type: str # "scripts" | "root" | "package" | "none"
modules: list[ModuleIndex] = field(default_factory=list)
exports: list[str] = field(default_factory=list) # From __all__ if present
def get_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
"""Extract function signature as a string."""
args = node.args
parts = []
# Positional-only args (Python 3.8+)
for arg in args.posonlyargs:
parts.append(format_arg(arg))
if args.posonlyargs:
parts.append("/")
# Regular args
defaults_offset = len(args.args) - len(args.defaults)
for i, arg in enumerate(args.args):
default_idx = i - defaults_offset
if default_idx >= 0:
parts.append(format_arg(arg, args.defaults[default_idx]))
else:
parts.append(format_arg(arg))
# *args
if args.vararg:
parts.append(f"*{args.vararg.arg}")
elif args.kwonlyargs:
parts.append("*")
# Keyword-only args
for i, arg in enumerate(args.kwonlyargs):
default = args.kw_defaults[i]
parts.append(format_arg(arg, default))
# **kwargs
if args.kwarg:
parts.append(f"**{args.kwarg.arg}")
return f"({', '.join(parts)})"
def format_arg(arg: ast.arg, default: ast.expr | None = None) -> str:
"""Format a single function argument."""
name = arg.arg
# Add type annotation if present
if arg.annotation:
name += f": {ast.unparse(arg.annotation)}"
# Add default value if present
if default is not None:
try:
default_str = ast.unparse(default)
# Truncate long defaults
if len(default_str) > 20:
default_str = default_str[:17] + "..."
name += f" = {default_str}"
except:
name += " = ..."
return name
def get_docstring_first_line(node: ast.AST) -> str | None:
"""Extract first line of docstring if present."""
docstring = ast.get_docstring(node)
if docstring:
first_line = docstring.split("\n")[0].strip()
if len(first_line) > 80:
first_line = first_line[:77] + "..."
return first_line
return None
def extract_symbols(source: str, module_name: str) -> ModuleIndex:
"""
Extract all exportable symbols from Python source code.
Args:
source: Python source code
module_name: Name of the module (for import reference)
Returns:
ModuleIndex with symbols and imports
"""
try:
tree = ast.parse(source)
except SyntaxError:
return ModuleIndex(
file_path="",
module_name=module_name,
symbols=[],
imports=[]
)
symbols = []
imports = []
exports = [] # From __all__
for node in ast.walk(tree):
# Collect imports
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.module:
imports.append(node.module)
# Only process top-level definitions
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Skip private functions
if node.name.startswith("_"):
continue
symbols.append(Symbol(
name=node.name,
kind="function",
signature=get_signature(node),
line=node.lineno,
docstring=get_docstring_first_line(node)
))
elif isinstance(node, ast.ClassDef):
# Skip private classes
if node.name.startswith("_"):
continue
methods = []
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Include public methods
if not item.name.startswith("_") or item.name in ("__init__", "__call__"):
methods.append(Symbol(
name=item.name,
kind="method",
signature=get_signature(item),
line=item.lineno,
docstring=get_docstring_first_line(item)
))
symbols.append(Symbol(
name=node.name,
kind="class",
line=node.lineno,
docstring=get_docstring_first_line(node),
children=methods
))
# Look for __all__ definition
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "__all__":
if isinstance(node.value, (ast.List, ast.Tuple)):
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
exports.append(elt.value)
index = ModuleIndex(
file_path="",
module_name=module_name,
symbols=symbols,
imports=imports
)
return index
# @lat: [[skill-lifecycle#Skill Inspection]]
def index_skill(layout: SkillLayout) -> SkillIndex:
"""
Create a complete index of a skill's exportable code.
Args:
layout: SkillLayout from discover module
Returns:
SkillIndex with all modules and symbols
"""
modules = []
skill_exports = []
# Check for __init__.py exports
init_path = layout.path / "__init__.py"
if init_path.exists():
source = init_path.read_text()
init_index = extract_symbols(source, layout.entry_module or layout.name)
init_index.file_path = "__init__.py"
# Extract __all__ from __init__.py
try:
tree = ast.parse(source)
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "__all__":
if isinstance(node.value, (ast.List, ast.Tuple)):
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
skill_exports.append(elt.value)
except:
pass
# Index all Python files
for py_file in layout.python_files:
try:
source = py_file.read_text()
relative_path = py_file.relative_to(layout.path)
# Determine module name
if py_file.parent.name == "scripts":
module_name = f"{skill_name_to_module(layout.name)}.scripts.{py_file.stem}"
else:
module_name = f"{skill_name_to_module(layout.name)}.{py_file.stem}"
index = extract_symbols(source, module_name)
index.file_path = str(relative_path)
modules.append(index)
except Exception:
# Skip files that can't be read/parsed
continue
return SkillIndex(
name=layout.name,
module_name=skill_name_to_module(layout.name),
layout_type=layout.layout_type,
modules=modules,
exports=skill_exports
)
def index_all_skills(skills_root: Path) -> dict[str, SkillIndex]:
"""
Index all skills in a repository.
Args:
skills_root: Root directory containing skill directories
Returns:
Dict mapping skill names to their SkillIndex
"""
layouts = discover_all_skills(skills_root)
return {layout.name: index_skill(layout) for layout in layouts}
def generate_registry(skills_root: Path, output_path: Path | None = None) -> dict:
"""
Generate a registry.json file mapping skill names to their exports.
Args:
skills_root: Root directory containing skill directories
output_path: Optional path to write registry.json
Returns:
Registry dict suitable for JSON serialization
"""
indices = index_all_skills(skills_root)
registry = {
"version": "1.0.0",
"skills_root": str(skills_root),
"skills": {}
}
for name, index in indices.items():
skill_entry = {
"module_name": index.module_name,
"layout_type": index.layout_type,
"exports": index.exports,
"modules": []
}
for module in index.modules:
module_entry = {
"file": module.file_path,
"module": module.module_name,
"symbols": []
}
for symbol in module.symbols:
symbol_entry = {
"name": symbol.name,
"kind": symbol.kind,
"line": symbol.line
}
if symbol.signature:
symbol_entry["signature"] = symbol.signature
if symbol.docstring:
symbol_entry["doc"] = symbol.docstring
if symbol.children:
symbol_entry["methods"] = [
{"name": m.name, "signature": m.signature, "line": m.line}
for m in symbol.children
]
module_entry["symbols"].append(symbol_entry)
skill_entry["modules"].append(module_entry)
registry["skills"][name] = skill_entry
if output_path:
output_path.write_text(json.dumps(registry, indent=2))
return registry
scripts/skill_imports.py
"""
skill_imports.py - Universal import mechanism for cross-skill code reuse.
Handles the dash-to-underscore naming mismatch between skill directories
(dash-delimited) and Python imports (underscore-required).
Usage:
from inspecting_skills import skill_import, setup_skill_path
# Simple import
bsky = skill_import("browsing-bluesky")
bsky.search_posts("python")
# Import specific functions
search_posts, get_profile = skill_import("browsing-bluesky", ["search_posts", "get_profile"])
# Or setup path once and use normal imports
setup_skill_path("/home/user/claude-skills")
from browsing_bluesky import search_posts # Works!
"""
import importlib
import importlib.abc
import importlib.util
import sys
from pathlib import Path
from typing import Any
from .discover import find_skill_by_name, skill_name_to_module
# Default skills root - can be overridden
_skills_root: Path | None = None
_registered_skills: dict[str, Path] = {}
def get_skills_root() -> Path | None:
"""Get the currently configured skills root directory."""
global _skills_root
return _skills_root
def set_skills_root(path: str | Path) -> None:
"""
Set the skills root directory.
Args:
path: Path to the directory containing skill directories
"""
global _skills_root
_skills_root = Path(path).resolve()
def setup_skill_path(skills_root: str | Path | None = None) -> Path:
"""
Add skills root to sys.path and enable skill imports.
This sets up the Python path so that skills can be imported directly:
setup_skill_path("/home/user/claude-skills")
from browsing_bluesky import search_posts
Args:
skills_root: Path to skills directory. If None, attempts auto-detection.
Returns:
The skills root path that was configured
Raises:
ValueError: If skills_root cannot be determined
"""
global _skills_root
if skills_root:
_skills_root = Path(skills_root).resolve()
elif _skills_root is None:
# Try to auto-detect from common locations
candidates = [
Path("/home/user/claude-skills"),
Path("/home/claude/claude-skills"),
Path.home() / "claude-skills",
Path.cwd(),
]
for candidate in candidates:
if candidate.is_dir() and (candidate / ".git").exists():
# Verify it looks like a skills repo
skill_dirs = [d for d in candidate.iterdir()
if d.is_dir() and (d / "SKILL.md").exists()]
if skill_dirs:
_skills_root = candidate
break
if _skills_root is None:
raise ValueError(
"Could not auto-detect skills root. "
"Please provide skills_root parameter."
)
# Add to sys.path if not already there
path_str = str(_skills_root)
if path_str not in sys.path:
sys.path.insert(0, path_str)
# Register the meta path finder for transparent imports
_ensure_finder_installed()
return _skills_root
# @lat: [[skill-lifecycle#Cross-Skill Imports]]
def skill_import(
skill_name: str,
symbols: list[str] | None = None
) -> Any:
"""
Import a skill module or specific symbols from it.
Handles dash-to-underscore conversion automatically.
Args:
skill_name: Skill name (e.g., "browsing-bluesky" or "browsing_bluesky")
symbols: Optional list of symbol names to import. If None, returns module.
Returns:
If symbols is None: The imported module
If symbols is a list: Tuple of the requested symbols
Examples:
# Import the whole module
bsky = skill_import("browsing-bluesky")
bsky.search_posts("python")
# Import specific functions
search_posts, get_profile = skill_import("browsing-bluesky",
["search_posts", "get_profile"])
# Single symbol
(search_posts,) = skill_import("browsing-bluesky", ["search_posts"])
"""
# Ensure skills root is configured
if _skills_root is None:
setup_skill_path()
# Find the skill
layout = find_skill_by_name(skill_name, _skills_root)
if layout is None:
raise ImportError(f"Skill not found: {skill_name}")
# Determine the module name
module_name = skill_name_to_module(layout.name)
# Ensure the skill path is importable
skill_path_str = str(layout.path.parent)
if skill_path_str not in sys.path:
sys.path.insert(0, skill_path_str)
# Import the module
try:
module = importlib.import_module(module_name)
except ImportError as e:
# Try importing directly from the skill directory
init_path = layout.path / "__init__.py"
if init_path.exists():
spec = importlib.util.spec_from_file_location(module_name, init_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
else:
raise ImportError(f"Cannot load skill {skill_name}: {e}")
else:
raise ImportError(f"Skill {skill_name} has no __init__.py: {e}")
# Return module or specific symbols
if symbols is None:
return module
result = []
for sym in symbols:
if hasattr(module, sym):
result.append(getattr(module, sym))
else:
raise ImportError(f"Symbol '{sym}' not found in skill '{skill_name}'")
return tuple(result)
def register_skill(skill_name: str, skill_path: str | Path) -> None:
"""
Register a skill at a custom path for importing.
Useful when skills are not in the standard skills root.
Args:
skill_name: Name to register the skill under
skill_path: Path to the skill directory
"""
global _registered_skills
_registered_skills[skill_name] = Path(skill_path).resolve()
_registered_skills[skill_name_to_module(skill_name)] = Path(skill_path).resolve()
# @lat: [[skill-lifecycle#Cross-Skill Imports]]
class SkillImportFinder(importlib.abc.MetaPathFinder):
"""
Meta path finder that enables importing skills with underscore names.
Installed by setup_skill_path() to enable:
from browsing_bluesky import search_posts
"""
def find_spec(self, fullname: str, path, target=None):
if _skills_root is None:
return None
# Only handle top-level imports that might be skills
parts = fullname.split(".")
top_level = parts[0]
# Check registered skills first
if top_level in _registered_skills:
skill_path = _registered_skills[top_level]
return self._create_spec(fullname, skill_path, parts)
# Try to find as a skill (convert underscore to dash)
dash_name = top_level.replace("_", "-")
skill_path = _skills_root / dash_name
if skill_path.is_dir() and (skill_path / "SKILL.md").exists():
return self._create_spec(fullname, skill_path, parts)
return None
def _create_spec(self, fullname: str, skill_path: Path, parts: list[str]):
"""Create a module spec for the skill."""
if len(parts) == 1:
# Top-level skill import
init_path = skill_path / "__init__.py"
if init_path.exists():
return importlib.util.spec_from_file_location(
fullname,
init_path,
submodule_search_locations=[str(skill_path)]
)
else:
# Submodule import (e.g., browsing_bluesky.scripts.bsky)
subpath = skill_path
for part in parts[1:]:
subpath = subpath / part
# Try as package
init_path = subpath / "__init__.py"
if init_path.exists():
return importlib.util.spec_from_file_location(
fullname,
init_path,
submodule_search_locations=[str(subpath)]
)
# Try as module
module_path = subpath.with_suffix(".py")
if module_path.exists():
return importlib.util.spec_from_file_location(
fullname,
module_path
)
return None
_finder_installed = False
def _ensure_finder_installed() -> None:
"""Install the SkillImportFinder if not already installed."""
global _finder_installed
if not _finder_installed:
sys.meta_path.insert(0, SkillImportFinder())
_finder_installed = True
def list_importable_skills() -> list[dict]:
"""
List all skills that can be imported.
Returns:
List of dicts with skill info: name, module_name, has_init, layout_type
"""
if _skills_root is None:
try:
setup_skill_path()
except ValueError:
return []
from .discover import discover_all_skills
layouts = discover_all_skills(_skills_root)
return [
{
"name": layout.name,
"module_name": layout.entry_module or skill_name_to_module(layout.name),
"has_init": layout.has_init,
"layout_type": layout.layout_type,
"path": str(layout.path)
}
for layout in layouts
if layout.has_init # Only show skills that can actually be imported
]
SKILL.md
---
name: inspecting-skills
description: Discovers and indexes Python code in skills, enabling cross-skill imports. Use when importing functions from other skills or analyzing skill codebases.
metadata:
version: 1.0.3
---
# Inspecting Skills
Discover Python code across skills and enable universal imports. Solves the dash-underscore naming mismatch between skill directories (e.g., `browsing-bluesky`) and Python imports (e.g., `browsing_bluesky`).
## Installation
```python
import sys
sys.path.insert(0, '/home/user/claude-skills')
from inspecting_skills import setup_skill_path, skill_import
```
## Quick Start
### Import a Skill
```python
from inspecting_skills import skill_import
# Import by skill name (dash or underscore form)
bsky = skill_import("browsing-bluesky")
posts = bsky.search_posts("python")
# Import specific functions
search, profile = skill_import("browsing-bluesky", ["search_posts", "get_profile"])
```
### Enable Transparent Imports
```python
from inspecting_skills import setup_skill_path
# Configure once at session start
setup_skill_path("/home/user/claude-skills")
# Now import skills directly (underscore form)
from browsing_bluesky import search_posts, get_profile
from remembering import remember, recall
```
### Discover Available Skills
```python
from inspecting_skills import list_importable_skills
skills = list_importable_skills()
for s in skills:
print(f"{s['name']} -> import {s['module_name']}")
```
## Core Functions
### Discovery
| Function | Purpose |
|----------|---------|
| `discover_skill(path)` | Analyze a single skill directory |
| `discover_all_skills(root)` | Find all skills with Python code |
| `find_skill_by_name(name, root)` | Find skill by name (either form) |
| `skill_name_to_module(name)` | Convert "browsing-bluesky" to "browsing_bluesky" |
### Indexing
| Function | Purpose |
|----------|---------|
| `index_skill(layout)` | Extract symbols from a discovered skill |
| `index_all_skills(root)` | Index all skills in repository |
| `generate_registry(root, output)` | Create registry.json manifest |
### Importing
| Function | Purpose |
|----------|---------|
| `setup_skill_path(root)` | Enable transparent skill imports |
| `skill_import(name, symbols)` | Import skill or specific symbols |
| `register_skill(name, path)` | Register skill at custom path |
| `list_importable_skills()` | List all importable skills |
## Skill Layouts
Skills organize Python code in three patterns:
### 1. Scripts Directory
```
browsing-bluesky/
SKILL.md
__init__.py # Re-exports from scripts/
scripts/
__init__.py
bsky.py # Main implementation
```
### 2. Root-Level Modules
```
remembering/
SKILL.md
__init__.py # Re-exports functions
memory.py # Core functionality
boot.py
config.py
```
### 3. Simple Package
```
simple-skill/
SKILL.md
__init__.py # Contains all code
```
## Generating a Registry
Create a `registry.json` for offline symbol lookup:
```python
from inspecting_skills import generate_registry
from pathlib import Path
registry = generate_registry(
Path("/home/user/claude-skills"),
output_path=Path("registry.json")
)
# Registry structure:
# {
# "version": "1.0.0",
# "skills": {
# "browsing-bluesky": {
# "module_name": "browsing_bluesky",
# "exports": ["search_posts", "get_profile", ...],
# "modules": [...]
# }
# }
# }
```
## Indexing a Single Skill
```python
from inspecting_skills import discover_skill, index_skill
from pathlib import Path
# Discover the skill layout
layout = discover_skill(Path("/home/user/claude-skills/remembering"))
print(f"Layout: {layout.layout_type}")
print(f"Has __init__.py: {layout.has_init}")
print(f"Python files: {[f.name for f in layout.python_files]}")
# Index symbols
index = index_skill(layout)
for module in index.modules:
print(f"\n{module.file_path}:")
for sym in module.symbols:
print(f" {sym.kind} {sym.name}{sym.signature or ''}")
```
## Integration with tree-sitting
This skill complements `tree-sitting`, which extracts code structure at runtime:
- **tree-sitting**: AST-derived structure via tree-sitter, multi-language
- **inspecting-skills**: Runtime import support, Python-focused, dynamic discovery
Use both together:
1. `tree-sitting` for navigation and code review
2. `inspecting-skills` for actual code imports and execution
## Troubleshooting
### Import Errors
```python
# If skill_import fails, check:
# 1. Skill exists and has __init__.py
from inspecting_skills import discover_skill
layout = discover_skill(Path("/path/to/skill"))
print(layout.has_init) # Must be True for importing
# 2. Skills root is configured
from inspecting_skills import get_skills_root
print(get_skills_root())
# 3. Symbol is exported in __all__
import ast
init_code = open("/path/to/skill/__init__.py").read()
# Check for __all__ definition
```
### Path Not Found
```python
# Manually set skills root
from inspecting_skills import set_skills_root
set_skills_root("/home/user/claude-skills")
```
## API Reference
### SkillLayout
```python
@dataclass
class SkillLayout:
name: str # "browsing-bluesky"
path: Path # Full path to skill directory
layout_type: str # "scripts" | "root" | "package" | "none"
python_files: list[Path]
has_init: bool # Can be imported as package
entry_module: str # "browsing_bluesky"
```
### SkillIndex
```python
@dataclass
class SkillIndex:
name: str # "browsing-bluesky"
module_name: str # "browsing_bluesky"
layout_type: str
modules: list[ModuleIndex]
exports: list[str] # From __all__
```
### Symbol
```python
@dataclass
class Symbol:
name: str # Function/class name
kind: str # "function" | "class" | "method"
signature: str | None # "(self, x: int)"
line: int | None # 1-indexed
docstring: str | None # First line
children: list[Symbol] # Methods for classes
```