Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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/` | | | |
Expand All @@ -69,15 +75,15 @@ 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?

See [What it does](#what-it-does) above. Full feature list and screenshots: [agentfiles.crafter.run/docs](https://agentfiles.crafter.run/docs).

### 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?

Expand Down
29 changes: 29 additions & 0 deletions scripts/check-tool-count.ts
Original file line number Diff line number Diff line change
@@ -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}).`);
31 changes: 31 additions & 0 deletions src/tools-meta.ts
Original file line number Diff line number Diff line change
@@ -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";
150 changes: 18 additions & 132 deletions vscode/src/rich-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<string, string[]>;
favorites: string[];
Expand All @@ -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" }
Expand Down Expand Up @@ -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<string> {
const names = new Set<string>();
if (!existsSync(LOCK_PATH)) return names;
Expand All @@ -279,48 +226,17 @@ function getInstalledNames(): Set<string> {
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<MarketplaceSkill[]> {
// 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;
}
Expand All @@ -330,31 +246,16 @@ async function marketplacePopular(): Promise<MarketplaceSkill[]> {
}

async function refreshPopularCache(): Promise<MarketplaceSkill[]> {
const queries = ["react", "next", "clerk", "stripe", "ai"];
const seen = new Set<string>();
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<MarketplaceSkill[]> {
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(
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
app/tools-meta.generated.ts
Loading
Loading