Imported from Crosery/runai (
src/core/market/AGENTS.md). Install upstream withnpx skills add Crosery/runai --skill market. Copyright stays with the author.
market
This folder (
src/core/market/). One-liner: market source list + skill index cache + skills.sh SSR/sitemap aggregator + install.
Purpose
Marketplace skill browsing + installation. Manages a list of SourceEntry (one per repo/index), caches the per-source skill index on disk with 1h TTL, provides search/filter, and installs single skills. The runai Market is a thin layer over the skills.sh ecosystem.
Public surface (stable — external code depends on these exact paths)
All re-exported from mod.rs; consumers reach them at crate::core::market::X.
crate::core::market::SourceEntry— market source row;from_input(input),repo_id(&self),is_skillshub(&self).crate::core::market::SKILLSHUB_SENTINEL(&str = "*skills-hub*") — sentinelowner/repoflag for the skills.sh aggregator.crate::core::market::load_sources(data_dir) -> Vec<SourceEntry>/save_sources(data_dir, &sources).crate::core::market::MarketSkill— a single installable skill row with popularity signals (installs/trending_installs/hot_score/weekly_installs/is_official).crate::core::market::load_cache(data_dir, source)/save_cache(data_dir, source, skills).crate::core::market::save_plugin_marker/is_plugin_source— opt-in plugin vs vanilla source distinction.crate::core::market::find_skill_in_sources(data_dir, &sources, name, source_filter?) -> Option<MarketSkill>.crate::core::market::Market— the orchestrator. Inherent methods (reached through the re-exportedMarkettype, no separate re-export):Market::install_single(&skill, install_root) -> Result<()>(async,pub).Market::mark_installed(&mut skills, &installed_names)(pub).Market::fetch(source)/fetch_skillshub()/extract_skills/get_skill_files/collect_download_tasks/execute_downloads/install_single_with_tree(allpub(crate)).
crate::core::market::is_root_skill_payload(path) -> bool(pub(crate)) — whitelist filter for root-skill repos; also called byserver.rs.
Submodule map
| File | Responsibility | Key items |
|---|---|---|
mod.rs |
re-exports only, no logic | pub use of public + pub(crate) use is_root_skill_payload |
types.rs |
core domain types | MarketSkill, Market (unit struct) |
sources.rs |
source list (built-in + user) | SourceEntry, SKILLSHUB_SENTINEL, builtin_sources, load_sources, save_sources |
github_mirror.rs |
raw-download + REST-API URL shaping | mirror_base (private), raw_url_for, github_api_base |
cache.rs |
on-disk index cache + plugin markers + lookup | load_cache, save_cache, save_plugin_marker, is_plugin_source, find_skill_in_sources, cache_key |
leaderboard.rs |
skills.sh SSR leaderboard parser | LeaderboardRow, parse_leaderboard, extract_quoted_field/extract_field/extract_array_field (private) |
sitemap.rs |
sitemap XML + root-skill filter | extract_sitemap_locs (pub(super)), is_root_skill_payload |
skillshub.rs |
skills.sh aggregator pipeline | Market::fetch_skillshub |
extract.rs |
git-tree → skills + plugin detect | ExtractResult, GitTree, GitTreeNode, Market::extract_skills |
fetch.rs |
source dispatch (GitHub tree vs aggregator) | Market::fetch |
download.rs |
concurrent download task build/exec | DownloadTask, Market::get_skill_files, collect_download_tasks, execute_downloads |
install.rs |
single-skill install + Contents API fallback | Market::install_single[_with_tree], download_directory_recursive, GitHubContentItem, mark_installed |
Key invariants
mod.rsis thin — declarations +pub useonly. No business logic.- Public path is frozen — every item formerly at
crate::core::market::Xis re-exported frommod.rsat the same path. InherentMarketmethods are reached through the re-exportedMarkettype, not via separate re-exports. - Brittle SSR/sitemap parsers stay next to their tests —
parse_leaderboard(inleaderboard.rs) is regex-free and stdlib-only; do not "improve" it.extract_sitemap_locs(insitemap.rs) is likewise stdlib-only. Each parser's#[cfg(test)] mod testslives in the same file. - Cache lives at
~/.runai/market-cache/<source_id>.json, TTL 1 hour (checked via file mtime). The skills.sh aggregator cache key is*skills-hub*_*skills-hub*.json(~3 MB for ~20K skills). - UI always displays from cache, never blocks on network. Cache refresh is background via
server.rs::refresh_all_sources(concurrent per source viatokio::spawn). install_singledownloads the full skill directory — asset files included, not justSKILL.md.- skills.sh install hops through
manager::install_github_repo_filtered_for—MarketSkill.repo_pathis intentionally empty for aggregator entries because the path inside the real GitHub repo is resolved on install via the repo's git tree, not stored up-front (avoids ~2.6K extra tree fetches at sitemap time).
Cross-module dependencies
- Upstream consumers: TUI Market tab,
cli::MarketInstall, MCPsm_market/sm_market_install,manager::install_github_repo_filtered_for,server.rs(refresh_all_sources, market handlers,is_root_skill_payload). - Downstream:
reqwest,serde_json,tokio,crate::core::paths::AppPaths(only indownload.rstests).
Gotchas / where bodies are buried
extract_sitemap_locsispub(super)(notpub(crate)) — visible to siblings viasuper::sitemap::extract_sitemap_locs; it is NOT re-exported frommod.rsbecause onlyskillshub.rsuses it.Marketis a unit struct intypes.rs; itsimpl Marketblocks are spread acrossextract.rs/fetch.rs/skillshub.rs/download.rs/install.rs. Rust auto-collects all inherent impls — adding a method to any submodule'simpl Marketworks without touchingtypes.rs.SourceEntry::from_inputaccepts multiple URL forms (raw GitHub,user/repo,owner/repo@branch). Adding a new source provider? Update thesources.rsparser andrepo_id.- Cache invalidation is file mtime only — if the file is touched (e.g.
git clone), TTL restarts. Don't assume content age tracks file age. - Plugin markers (
is_plugin_source) change how installed skills are laid out — plugin sources put the whole repo underplugins/marketplaces/, which scanner then filters out. - The fetch/install path creates a nested
tokioruntime insidespawn_blockingin the server handlers — legal, do not relocate the runtime creation across an async boundary. - Test-only base-URL overrides (default off, production behavior byte-identical):
github_mirror::github_api_base()readsRUNAI_GITHUB_API_BASE(used byfetch.rsgit-trees +install.rsContents API), andgithub_mirror::raw_url_forreadsRUNAI_GITHUB_RAW_BASE(raw-file downloads, raw.githubusercontent path shape, takes precedence overRUNAI_GH_MIRROR). Both default to the real GitHub hosts when unset. They exist sotests/install_fixture_e2e.rscan point the whole install pipeline (tree → download → 落盘 → DB) at a local axum fixture server and run offline in default CI.installer.rshas a siblingRUNAI_GITHUB_ARCHIVE_BASEfor its branch-tarball URL. Do NOT set any of these in shipped code paths.
Tests
Each submodule carries its own #[cfg(test)] mod tests next to the code under test (12 tests total, no platform gating):
sources.rs— builtin-source shape, sentinel presence, enabled-toggle persistence (3).cache.rs—find_skill_in_sourcesby label/repo_id/no-filter (1).leaderboard.rs—parse_leaderboardinstall/weekly extraction + empty/garbage (2).sitemap.rs—extract_sitemap_locsparse + empty/malformed,is_root_skill_payloadkeep/drop (3).extract.rs—.claude-plugindetection (1).download.rs—get_skill_filestree extraction,collect_download_tasksmapping + mirror URL shape (2).