Skip to content

Skills

pycodeloop.core.skills.Skill dataclass

Source code in pycodeloop/core/skills.py
15
16
17
18
19
20
21
@dataclass
class Skill:
    name: str
    description: str
    source: str
    path: Path
    content: str

content instance-attribute

description instance-attribute

name instance-attribute

path instance-attribute

source instance-attribute

__init__(name, description, source, path, content)

pycodeloop.core.skills.discover_skills(cwd=None, home=None, sources=None, use_cache=True)

Scan well-known locations Claude Code, Cursor, and the AGENTS.md convention (Codex and others) use for skills/instructions.

Results are cached in ~/.pycodeloop/config.json under "skills_cache", keyed by cwd and invalidated automatically when any matched file's mtime changes — unchanged skills are served from cache, unread.

Parameters:

Name Type Description Default
cwd Path | None

Project directory to scan. Defaults to the current directory.

None
home Path | None

User home directory to scan. Defaults to Path.home().

None
sources set[str] | None

Limit to these sources ("claude-skill", "claude-memory", "cursor-rule", "agents-md"). Defaults to all of them.

None
use_cache bool

Set False to force a full rescan and refresh the cache.

True
Source code in pycodeloop/core/skills.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def discover_skills(
    cwd: Path | None = None,
    home: Path | None = None,
    sources: set[str] | None = None,
    use_cache: bool = True,
) -> list[Skill]:
    """Scan well-known locations Claude Code, Cursor, and the AGENTS.md
    convention (Codex and others) use for skills/instructions.

    Results are cached in `~/.pycodeloop/config.json` under "skills_cache",
    keyed by cwd and invalidated automatically when any matched file's
    mtime changes — unchanged skills are served from cache, unread.

    Args:
        cwd: Project directory to scan. Defaults to the current directory.
        home: User home directory to scan. Defaults to `Path.home()`.
        sources: Limit to these sources ("claude-skill", "claude-memory",
            "cursor-rule", "agents-md"). Defaults to all of them.
        use_cache: Set False to force a full rescan and refresh the cache.
    """
    cwd = cwd or Path.cwd()
    home = home or Path.home()
    jobs = _jobs(cwd, home, sources)
    fingerprint = _fingerprint(jobs)
    key = _cache_key(cwd, home, sources)

    cache = default_store.get_section(_CACHE_SECTION)
    cached_entry = cache.get(key)

    if use_cache and cached_entry and cached_entry.get("fingerprint") == fingerprint:
        return [_skill_from_dict(item) for item in cached_entry["skills"]]

    skills = _load_jobs(jobs)

    if use_cache:
        cache[key] = {
            "fingerprint": fingerprint,
            "skills": [_skill_to_dict(s) for s in skills],
        }
        default_store.set_section(_CACHE_SECTION, cache)

    return skills

pycodeloop.core.skills.render_skills_index(skills)

Source code in pycodeloop/core/skills.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def render_skills_index(skills: list[Skill]) -> str:
    if not skills:
        return ""

    lines = [
        "You have access to the following skills/instructions discovered on "
        "this machine (from Claude Code, Cursor, and the AGENTS.md "
        "convention). Each is a short index entry — call `read_skill` with "
        "its name to load the full content when it's relevant to the task.",
        "",
    ]
    lines.extend(
        f"- **{skill.name}** ({skill.source}): {skill.description}" for skill in skills
    )
    return "\n".join(lines)

pycodeloop.core.skills.ReadSkillTool

Bases: Tool

Source code in pycodeloop/core/skills.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
class ReadSkillTool(Tool):
    name = "read_skill"
    description = (
        "Read the full content of a discovered skill/instruction file by name."
    )
    parameters = {
        "type": "object",
        "properties": {"name": {"type": "string"}},
        "required": ["name"],
    }

    def __init__(self, skills: list[Skill]) -> None:
        self._skills = {skill.name: skill for skill in skills}

    def run(self, name: str) -> ToolResult:
        skill = self._skills.get(name)

        if skill is None:
            available = ", ".join(sorted(self._skills)) or "(none)"
            return ToolResult(
                output=f"Unknown skill '{name}'. Available: {available}",
                is_error=True,
            )

        return ToolResult(output=skill.content)

description = 'Read the full content of a discovered skill/instruction file by name.' class-attribute instance-attribute

name = 'read_skill' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'name': {'type': 'string'}}, 'required': ['name']} class-attribute instance-attribute

__init__(skills)

Source code in pycodeloop/core/skills.py
243
244
def __init__(self, skills: list[Skill]) -> None:
    self._skills = {skill.name: skill for skill in skills}

run(name)

Source code in pycodeloop/core/skills.py
246
247
248
249
250
251
252
253
254
255
256
def run(self, name: str) -> ToolResult:
    skill = self._skills.get(name)

    if skill is None:
        available = ", ".join(sorted(self._skills)) or "(none)"
        return ToolResult(
            output=f"Unknown skill '{name}'. Available: {available}",
            is_error=True,
        )

    return ToolResult(output=skill.content)