How Skills Are Listed in Pi Web: Discovery, Annotation, and Runtime Visibility

Pi Web discovers skills through the Pi SDK's DefaultResourceLoader, enriches them with installation metadata from lock files, and exposes them at runtime via an API endpoint and React components that render the live skill list in the UI.

The agegr/pi-web repository implements a multi-layered system for listing available agent skills. Understanding how skills are listed in Pi Web—including their runtime visibility—requires examining the interaction between the Pi SDK's resource loading mechanism, lock-file based annotation, and the frontend components that consume this data.

Skill Discovery via the Pi SDK

Pi Web relies on the Pi SDK's DefaultResourceLoader to discover available skills across multiple filesystem roots. The loader is instantiated with the current working directory (cwd) and the agent's global directory (agentDir), then initialized to scan skill directories.

const loader = new DefaultResourceLoader({ cwd, agentDir });
await loader.reload(projectTrustReloadOptions(cwd, agentDir));
const { skills } = loader.getSkills();

The loader.getSkills() method walks several roots and returns an array of SkillInfo objects sourced from:

  • The global skills folder at ~/.agents/skills (symlinked into the agent's skills directory)
  • The project-specific folder .pi/skills inside the repository
  • Package-installed skill directories defined by the SDK's extensions system

Annotating Installation Metadata

After discovery, raw skill data requires enrichment to reflect installation state. The annotateSkillsWithInstallInfo function in lib/skill-lock.ts (lines 21-46) merges each skill with metadata from two lock files:

  • Global lock: $XDG_STATE_HOME/skills/.skill-lock.json for globally installed skills
  • Project lock: <cwd>/skills-lock.json for project-scoped installations

The function determines whether a skill's filePath lives under the global root (~/.agents/skills) or project root (.pi/skills), then attaches the corresponding lock entry.

export function annotateSkillsWithInstallInfo(
  skills: SkillInfo[],
  { cwd, agentDir }: AnnotateSkillOptions,
): SkillInfo[] {
  const globalEntries = readSkillLock(getGlobalSkillsLockPath());
  const projectEntries = readSkillLock(join(cwd, 'skills-lock.json'));
  const globalRoot = join(agentDir, 'skills');
  const projectRoot = join(cwd, '.pi', 'skills');

  return skills.map(skill => {
    if (!existsSync(skill.filePath)) return skill;
    const install = isWithin(skill.filePath, globalRoot)
      ? getInstallInfo(globalEntries, skill.name, 'global')
      : isWithin(skill.filePath, projectRoot)
      ? getInstallInfo(projectEntries, skill.name, 'project')
      : undefined;
    return install ? { ...skill, install } : skill;
  });
}

This produces a final SkillInfo object that may include an install property containing scope, package name, version hash, and URL.

The Skills API Endpoint

The server-side route at app/api/skills/route.ts exposes the annotated skill list via HTTP. The endpoint calls loadSkillsWithInstallInfo(cwd) and returns a JSON payload containing:

  • skills: The annotated array of SkillInfo objects
  • projectResourcesLoaded: A boolean indicating whether project trust status allowed loading of local skills
import { loadSkillsWithInstallInfo } from '@/lib/skills-service';

export async function GET(req: Request) {
  const url = new URL(req.url);
  const cwd = url.searchParams.get('cwd') ?? process.cwd();
  const data = await loadSkillsWithInstallInfo(cwd);
  return new Response(JSON.stringify(data), { status: 200 });
}

Runtime Visibility in the UI

Runtime visibility operates through two mechanisms: the RPC manager for active sessions and the React frontend for user interaction.

In lib/rpc-manager.ts (line 537), the RPC manager accesses inner.resourceLoader.getSkills().skills when an AgentSession is active, ensuring the current session can query the live skill list.

The UI component components/SkillsConfig.tsx fetches the /api/skills endpoint on mount and stores the results in component state:

const loadSkills = useCallback(async () => {
  const res = await fetch(`/api/skills?cwd=${encodeURIComponent(cwd)}`);
  const { skills, projectResourcesLoaded } = await res.json();
  setSkills(skills);
  setProjectResourcesLoaded(projectResourcesLoaded);
}, [cwd]);

The UI renders each skill with:

  • Name and description from SkillInfo
  • Install count and skills.sh link (for public registry skills)
  • Action buttons for install, update, and model-invocation toggling based on the install metadata

The component also respects the projectResourcesLoaded flag to display warnings when the project is not trusted.

Summary

  • Discovery: The DefaultResourceLoader scans global (~/.agents/skills) and project (.pi/skills) directories to collect SkillInfo objects
  • Annotation: annotateSkillsWithInstallInfo in lib/skill-lock.ts enriches skills with installation metadata from global and project lock files
  • API Exposure: The /api/skills endpoint in app/api/skills/route.ts serves annotated skills with trust status
  • Runtime Visibility: The RPC manager exposes skills to active sessions, while SkillsConfig.tsx fetches and renders the live list with installation controls

Frequently Asked Questions

How does Pi Web distinguish between global and project-specific skills?

Pi Web checks each skill's filePath against the global root (join(agentDir, 'skills')) and project root (join(cwd, '.pi', 'skills')). Skills existing under the global directory receive metadata from $XDG_STATE_HOME/skills/.skill-lock.json, while project-scoped skills pull from <cwd>/skills-lock.json.

What happens when a project is not trusted?

When project trust checks fail, the projectResourcesLoaded flag returns false from the /api/skills endpoint. The UI (components/SkillsConfig.tsx) detects this state and displays a warning that project-local skills are unavailable, while still showing globally installed skills.

How does the runtime UI stay synchronized with the skill list?

The SkillsConfig component fetches fresh data from /api/skills?cwd=... on component mount and during refresh operations. Additionally, the RPC manager in lib/rpc-manager.ts maintains access to inner.resourceLoader.getSkills().skills for active AgentSessions, ensuring the backend state remains consistent with the filesystem.

Where does Pi Web store skill installation metadata?

Installation metadata resides in two JSON lock files: the global lock at $XDG_STATE_HOME/skills/.skill-lock.json and the project lock at <cwd>/skills-lock.json. The lib/skill-lock.ts utility reads these files to determine installation scope, version hashes, and package URLs for each discovered skill.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →