diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..35feebc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Check tool count drift + run: bun run scripts/check-tool-count.ts + - name: Build + run: bun run build + - name: Lint + run: bun run lint diff --git a/README.md b/README.md index eafcf7c..312988c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Agentfiles -AI skills manager for Obsidian. Browse, create, and manage skills across Claude Code, Cursor, Codex, Windsurf, and 12 coding agents. +AI skills manager for Obsidian. Browse, create, and manage skills across Claude Code, Cursor, Codex, Windsurf, and 17 coding agents. **[Open in Obsidian →](obsidian://show-plugin?id=agentfiles)** · [View on Obsidian Community](https://community.obsidian.md/plugins/agentfiles) · [Website](https://agentfiles.crafter.run) · [Latest release](https://github.com/Railly/agentfiles/releases/latest) -![Browse skills, commands, and agents across 12 coding assistants](assets/browse.jpeg) +![Browse skills, commands, and agents across 17 coding assistants](assets/browse.jpeg) ![Dashboard with burn rate, context tax, and health metrics](assets/dashboard.jpeg) @@ -38,7 +38,7 @@ skillkit scan ## What it does -- **Browse** skills, commands, and agents from 12 tools in one place +- **Browse** skills, commands, and agents from 17 tools in one place - **Search** by name or file content with deep search toggle - **Create** new skills with a stepped wizard (pick tool, type, name) - **Edit** skills inline with markdown preview and Cmd+S save @@ -52,11 +52,17 @@ skillkit scan |------|--------|----------|-------------------|--------| | Claude Code | `~/.claude/skills/` | `~/.claude/commands/` | | `~/.claude/agents/` | | Cursor | `~/.cursor/skills/` | | `~/.cursor/rules/` | `~/.cursor/agents/` | -| Windsurf | | | `~/.codeium/windsurf/memories/`, `~/.windsurf/rules/` | | +| Windsurf | `~/.codeium/windsurf/skills/` | | `~/.codeium/windsurf/memories/`, `~/.windsurf/rules/` | | | Codex | `~/.codex/skills/` | `~/.codex/prompts/` | `~/.codex/memories/` | `~/.codex/agents/` | | Copilot | `~/.copilot/skills/` | | | | | Amp | `$XDG_CONFIG/amp/skills/` | | | | | OpenCode | `$XDG_CONFIG/opencode/skills/` | | | | +| Cline | detection only | | | | +| Roo Code | `~/.roo/skills/` | | | | +| Kilo Code | `~/.kilocode/skills/` | | | | +| Continue | `~/.continue/skills/` | | | | +| OpenHands | `~/.openhands/skills/` | | | | +| Goose | `$XDG_CONFIG/goose/skills/` | | | | | Pi | `~/.pi/agent/skills/` | | | | | Antigravity | `~/.gemini/antigravity/skills/` | | | | | Global | `~/.agents/skills/` | | | | @@ -69,7 +75,7 @@ Desktop only (macOS, Windows, Linux) — reads files outside your vault. ### What is Agentfiles? -An AI skills manager for Obsidian. Browse, create, and manage skills, commands, and agents across 12 coding agents, all from your vault. Full docs: [agentfiles.crafter.run/docs](https://agentfiles.crafter.run/docs). +An AI skills manager for Obsidian. Browse, create, and manage skills, commands, and agents across 17 coding agents, all from your vault. Full docs: [agentfiles.crafter.run/docs](https://agentfiles.crafter.run/docs). ### What can I do with Agentfiles? @@ -77,7 +83,7 @@ See [What it does](#what-it-does) above. Full feature list and screenshots: [age ### What tools are supported? -See [Supported tools](#supported-tools) above: 12 tools including Claude Code, Cursor, Codex, Windsurf, Copilot, Amp, OpenCode, Pi, Antigravity, Claude Desktop, Aider, and a global `~/.agents/skills/` directory. +See [Supported tools](#supported-tools) above: 17 tools including Claude Code, Cursor, Codex, Windsurf, Copilot, Amp, OpenCode, Cline, Roo Code, Kilo Code, Continue, OpenHands, Goose, Pi, Antigravity, Claude Desktop, Aider, and a global `~/.agents/skills/` directory. ### How do I install Agentfiles? diff --git a/scripts/check-tool-count.ts b/scripts/check-tool-count.ts new file mode 100644 index 0000000..fe9ed95 --- /dev/null +++ b/scripts/check-tool-count.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env bun +// Fails if README.md tool counts drift from TOOL_COUNT. README cannot import the +// constant, so this greps it and asserts. Run in CI on every PR. + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { TOOL_COUNT } from "../src/tools-meta"; + +const root = join(import.meta.dir, ".."); +const readme = readFileSync(join(root, "README.md"), "utf-8"); + +// Every standalone "N coding agents", "N tools", "N coding assistants" number in the README. +const pattern = /\b(\d+)\s+(?:coding agents|coding assistants|tools|AI coding tools|coding tools)\b/g; +const bad: string[] = []; +for (const m of readme.matchAll(pattern)) { + const n = Number(m[1]); + if (n !== TOOL_COUNT) { + bad.push(` "${m[0]}" (expected ${TOOL_COUNT})`); + } +} + +if (bad.length > 0) { + console.error(`README tool count drift (TOOL_COUNT is ${TOOL_COUNT}):`); + console.error(bad.join("\n")); + console.error("\nUpdate README.md, or update TOOL_COUNT in src/tools-meta.ts if a tool was added."); + process.exit(1); +} + +console.log(`README tool counts match TOOL_COUNT (${TOOL_COUNT}).`); diff --git a/src/tools-meta.ts b/src/tools-meta.ts new file mode 100644 index 0000000..8c2ae97 --- /dev/null +++ b/src/tools-meta.ts @@ -0,0 +1,31 @@ +// Single source of truth for the tool roster shown in docs, web copy, and OG metadata. +// The plugin's runtime tool list lives in tool-configs.ts; this file is the display-facing +// mirror that the web package (a separate tsconfig) can also import. TOOL_NAMES must stay +// in sync with the ids in tool-configs.ts. The "Global" ~/.agents/skills entry is a shared +// directory, not a coding tool, so TOOL_COUNT counts it separately. + +export const TOOL_NAMES = [ + "Claude Code", + "Cursor", + "Codex", + "Windsurf", + "Copilot", + "Amp", + "OpenCode", + "Cline", + "Roo Code", + "Kilo Code", + "Continue", + "OpenHands", + "Goose", + "Pi", + "Antigravity", + "Claude Desktop", + "Aider", +] as const; + +// Coding tools (excludes the shared global ~/.agents/skills directory). +export const TOOL_COUNT = TOOL_NAMES.length; + +// A short lead used across hero copy and README, e.g. "Claude Code, Cursor, Codex, Windsurf, and 17 tools". +export const TOOL_LEAD = "Claude Code, Cursor, Codex, Windsurf"; diff --git a/vscode/src/rich-extension.ts b/vscode/src/rich-extension.ts index c572d25..0880511 100644 --- a/vscode/src/rich-extension.ts +++ b/vscode/src/rich-extension.ts @@ -5,18 +5,24 @@ import { readFileSync, writeFileSync, mkdirSync, - rmSync, readdirSync, } from "fs"; import { join, delimiter, dirname } from "path"; import { homedir, platform } from "os"; -import { execSync, exec } from "child_process"; +import { execSync } from "child_process"; import { createHash } from "crypto"; import { scanAll, getInstalledTools } from "../../src/scanner"; import { clearInstallCache } from "../../src/tool-configs"; import { TOOL_CONFIGS } from "../../src/tool-configs"; import { parseAllConversationsSync } from "../../src/conversations/parser"; import { isSkillkitAvailable } from "../../src/skillkit"; +import { + getPopularSkills, + installSkillAsync, + type MarketplaceSkill, + removeSkillAsync, + searchSkills, +} from "../../src/marketplace"; import { DEFAULT_SETTINGS, type SkillItem, type ConversationItem, type ConversationMessage } from "../../src/types"; import { libraryHtml, sessionsHtml, dashboardHtml, marketplaceHtml } from "./panels"; @@ -137,8 +143,6 @@ const IS_WIN = platform() === "win32"; const TAG_FILE = join(HOME, ".claude", "agentfiles-conversations.json"); const ENRICH_CACHE = join(HOME, ".skillkit", "enrichment-cache.json"); const LOCK_PATH = join(HOME, ".agents", ".skill-lock.json"); -const API_BASE = "https://skills.sh/api"; - interface ConversationTagData { customTags: Record; favorites: string[]; @@ -153,16 +157,6 @@ interface EnrichmentCache { }; } -interface MarketplaceSkill { - id: string; - skillId: string; - name: string; - source: string; - installs: number; - description?: string; - content?: string; - installed?: boolean; -} type SidebarFilter = | { kind: "all" } @@ -218,53 +212,6 @@ function buildPath(): string { return [...extra, process.env.PATH || ""].join(delimiter); } -function detectRunner(): string { - const names = IS_WIN ? ["bunx.cmd", "bunx.exe", "bunx"] : ["bunx"]; - const dirs = IS_WIN - ? [join(HOME, ".bun", "bin"), join(process.env.APPDATA || join(HOME, "AppData", "Roaming"), "npm")] - : [join(HOME, ".bun", "bin"), "/usr/local/bin", "/opt/homebrew/bin"]; - for (const dir of dirs) { - for (const name of names) { - if (existsSync(join(dir, name))) return join(dir, name); - } - } - return "npx"; -} - -function getRunner(preference: "auto" | "npx" | "bunx" = "auto"): string { - if (preference === "npx") return "npx"; - if (preference === "bunx") return detectRunner(); - return detectRunner(); -} - -function execAsync(cmd: string, timeout = 120000): Promise<{ success: boolean; output: string }> { - return new Promise((resolve) => { - exec( - cmd, - { - encoding: "utf-8", - timeout, - env: { ...process.env, PATH: buildPath(), NO_COLOR: "1" }, - shell: IS_WIN ? "cmd.exe" : undefined, - }, - (error, stdout) => { - const out = String(stdout ?? ""); - if ( - !error || - out.includes("Done") || - out.includes("Installed") || - out.includes("Removed") || - out.includes("Updated") - ) { - resolve({ success: true, output: out }); - } else { - resolve({ success: false, output: error?.message ?? "Command failed" }); - } - }, - ); - }); -} - function getInstalledNames(): Set { const names = new Set(); if (!existsSync(LOCK_PATH)) return names; @@ -279,48 +226,17 @@ function getInstalledNames(): Set { return names; } -const AGENT_SKILL_DIRS = [ - join(HOME, ".claude", "skills"), - join(HOME, ".cursor", "skills"), - join(HOME, ".codex", "skills"), - join(HOME, ".codeium", "windsurf", "skills"), - join(HOME, ".config", "amp", "skills"), - join(HOME, ".config", "opencode", "skills"), - join(HOME, ".copilot", "skills"), - join(HOME, ".agents", "skills"), -]; - -function cleanupCopies(skillName: string): void { - for (const dir of AGENT_SKILL_DIRS) { - const skillPath = join(dir, skillName); - if (existsSync(skillPath)) { - try { - rmSync(skillPath, { recursive: true, force: true }); - } catch {} - } - } - const lockPath = join(HOME, ".agents", ".skill-lock.json"); - if (!existsSync(lockPath)) return; - try { - const data = JSON.parse(readFileSync(lockPath, "utf-8")); - if (data.skills && data.skills[skillName]) { - delete data.skills[skillName]; - writeFileSync(lockPath, JSON.stringify(data, null, 2) + "\n", "utf-8"); - } - } catch {} -} - const POPULAR_CACHE = join(HOME, ".skillkit", "marketplace-popular.json"); +// Disk-cached wrapper over src/marketplace getPopularSkills: serve the cache instantly, +// refresh in the background. The one-off fetch and slug logic live in src/marketplace. async function marketplacePopular(): Promise { - // Try disk cache first try { if (existsSync(POPULAR_CACHE)) { const cached = JSON.parse(readFileSync(POPULAR_CACHE, "utf-8")) as MarketplaceSkill[]; if (cached.length > 0) { const installed = getInstalledNames(); for (const s of cached) s.installed = installed.has(s.name); - // Refresh in background void refreshPopularCache(); return cached; } @@ -330,31 +246,16 @@ async function marketplacePopular(): Promise { } async function refreshPopularCache(): Promise { - const queries = ["react", "next", "clerk", "stripe", "ai"]; - const seen = new Set(); - const results: MarketplaceSkill[] = []; - for (const q of queries) { - const skills = await marketplaceSearch(q); - for (const s of skills) { - if (!seen.has(s.id)) { seen.add(s.id); results.push(s); } - } - } - const sorted = results.sort((a, b) => b.installs - a.installs).slice(0, 20); - try { writeFileSync(POPULAR_CACHE, JSON.stringify(sorted), "utf-8"); } catch {} + const sorted = await getPopularSkills(); + try { + writeFileSync(POPULAR_CACHE, JSON.stringify(sorted), "utf-8"); + } catch {} return sorted; } async function marketplaceSearch(query: string): Promise { if (query.length < 2) return []; - try { - const res = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}&limit=30`); - const data = (await res.json()) as { skills?: { id: string; skillId: string; name: string; installs: number; source: string }[] }; - if (!data.skills) return []; - const installed = getInstalledNames(); - return data.skills.map((s) => ({ ...s, installed: installed.has(s.name) })); - } catch { - return []; - } + return searchSkills(query); } async function marketplaceInstall( @@ -364,25 +265,14 @@ async function marketplaceInstall( global: boolean, skillName?: string, ): Promise<{ success: boolean; output: string }> { - const agentFlag = agents.length > 0 ? `-a ${agents.join(" ")}` : "-a '*'"; - const globalFlag = global ? "-g" : ""; - const skillFlag = skillName ? `-s ${skillName}` : ""; - const resolvedRunner = getRunner(runner); - const cmd = `${resolvedRunner} skills add ${source} ${agentFlag} ${globalFlag} ${skillFlag} -y` - .replace(/\s+/g, " ") - .trim(); - return execAsync(cmd); + return installSkillAsync(source, agents, { runner, globalInstall: global, skillName }); } async function marketplaceUninstall( skillName: string, runner: "auto" | "npx" | "bunx", ): Promise<{ success: boolean; output: string }> { - const resolvedRunner = getRunner(runner); - const cmd = `${resolvedRunner} skills remove ${skillName} -y`; - const result = await execAsync(cmd, 30000); - cleanupCopies(skillName); - return { success: true, output: result.output || `Cleaned ${skillName}` }; + return removeSkillAsync(skillName, runner); } function sanitizeFilename(name: string): string { @@ -1144,12 +1034,8 @@ class AgentfilesStore { if (!skill) break; const cfg = getConfig(); const runner = cfg.get<"auto" | "npx" | "bunx">("packageRunner") ?? "auto"; - const result = await execAsync( - `${getRunner(runner)} skills remove ${skill.name} -y`, - 30000, - ); + const result = await removeSkillAsync(skill.name, runner); if (result.success) { - cleanupCopies(skill.name); vscode.window.showInformationMessage(`Skill "${skill.name}" removed`); this._skills.delete(id); this._sendSkillsUpdate(); diff --git a/web/.gitignore b/web/.gitignore index 5ef6a52..51102d4 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +app/tools-meta.generated.ts diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 50c4775..8ddff0a 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -2,17 +2,16 @@ import { Analytics } from "@vercel/analytics/next"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; import type { Metadata } from "next"; +import { TOOL_COUNT, TOOL_LEAD } from "./tools-meta.generated"; import "./globals.css"; export const metadata: Metadata = { title: "Agentfiles — One plugin for all your agent files", - description: - "Browse, create, and edit skills, commands, and agents across Claude Code, Cursor, Codex, Windsurf, and 12 AI coding tools — from a single Obsidian panel.", + description: `Browse, create, and edit skills, commands, and agents across ${TOOL_LEAD}, and ${TOOL_COUNT} AI coding tools — from a single Obsidian panel.`, metadataBase: new URL("https://agentfiles.crafter.run"), openGraph: { title: "Agentfiles — One plugin for all your agent files", - description: - "Browse, create, and edit skills, commands, and agents across Claude Code, Cursor, Codex, Windsurf, and 12 AI coding tools — from a single Obsidian panel.", + description: `Browse, create, and edit skills, commands, and agents across ${TOOL_LEAD}, and ${TOOL_COUNT} AI coding tools — from a single Obsidian panel.`, url: "https://agentfiles.crafter.run", siteName: "Agentfiles", type: "website", @@ -21,8 +20,7 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title: "Agentfiles — One plugin for all your agent files", - description: - "Browse, create, and edit skills, commands, and agents across Claude Code, Cursor, Codex, Windsurf, and 12 AI coding tools — from a single Obsidian panel.", + description: `Browse, create, and edit skills, commands, and agents across ${TOOL_LEAD}, and ${TOOL_COUNT} AI coding tools — from a single Obsidian panel.`, images: ["/og-twitter.png"], }, }; diff --git a/web/app/page.tsx b/web/app/page.tsx index ccee344..6b093db 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { Footer } from "@/components/footer"; import { Nav } from "@/components/nav"; +import { TOOL_COUNT } from "./tools-meta.generated"; async function getStarCount(): Promise { const doFetch = globalThis.fetch; @@ -20,8 +21,7 @@ async function StarCount() { const features = [ { title: "Skill Scanner", - description: - "Auto-discovers skills, commands, and agents across Claude Code, Cursor, Codex, Windsurf, Copilot, and 12 tools from a single unified view.", + description: `Auto-discovers skills, commands, and agents across Claude Code, Cursor, Codex, Windsurf, Copilot, and ${TOOL_COUNT} tools from a single unified view.`, icon: ScanIcon, }, { @@ -100,8 +100,8 @@ function HeroSection() {

Browse, create, and edit skills, commands, and agents across Claude - Code, Cursor, Codex, Windsurf, and 12 AI coding tools — from a - single Obsidian panel. + Code, Cursor, Codex, Windsurf, and {TOOL_COUNT} AI coding tools + — from a single Obsidian panel.