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
58 changes: 30 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

# CLI to install and manage agent skills

**agent-skill-manager** (`asm`) is a scriptable CLI built for AI agents and automation — install, search, audit, and organize skills across Claude Code, Codex, Cursor, and 16 more tools. Every command supports `--json` and `--yes` for non-interactive use. An optional TUI (`asm`) is available for local browsing.
**agent-skill-manager** (`asm`) is a scriptable CLI built for AI agents and automation — install, search, audit, and organize skills across Claude Code, Codex, Cursor, and 18 more tools. Every command supports `--json` and `--yes` for non-interactive use. An optional TUI (`asm`) is available for local browsing.

[**Get Started**](#getting-started) · [**Browse 4,300+ skills**](https://luongnv.com/asm/#/skills) · [**Full docs**](#documentation)

## Problems `asm` solves

| Pain | Without `asm` | With `asm` |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Scattered installs | Same skill copied into `~/.claude/skills/`, `~/.codex/skills/`, `~/.cursor/rules/` — different versions, no single view | One `asm list` across all 19 providers and scopes |
| Scattered installs | Same skill copied into `~/.claude/skills/`, `~/.codex/skills/`, `~/.cursor/rules/` — different versions, no single view | One `asm list` across all 21 providers and scopes |
| No inventory | `ls` through hidden dirs; no idea what is installed, duplicated, or outdated | `asm search`, `asm inspect`, `asm stats`, `asm audit` |
| Invisible context cost | Every installed skill's description is resident in the agent's prompt on every message, whether or not it ever fires | `asm stats --tokens`, `asm audit residency` |
| Install just to read once | Installing is the only way to get a skill in front of an agent — and it stays resident forever afterwards | `asm get <skill>` prints the body and installs nothing |
Expand All @@ -38,7 +38,7 @@
| | |
| ------------------- | ------------------------------------------------------------------------------ |
| **Catalog** | 4,672 skills from 58 repos — [browse online](https://luongnv.com/asm/#/skills) |
| **Providers** | 19 agents (Claude, Codex, Cursor, Windsurf, Copilot, …) |
| **Providers** | 21 providers (Agents, Claude Code, Pi, OpenCode, Codex, …) |
| **Agent-ready CLI** | `--json`, `--yes`, `--machine` on list, search, install, audit, eval |
| **Security** | Pre-install scan for shell exec, network access, credential exposure |
| **License** | MIT — no accounts, no telemetry |
Expand Down Expand Up @@ -281,7 +281,7 @@ MIT licensed. No accounts or paywalls.
Yes. Commands return structured JSON (`--json`), accept non-interactive flags (`--yes`, `--machine`), and map to discrete actions an agent can chain — list inventory, search catalog, install, audit, uninstall.

**Which agents are supported?**
19 providers: Claude Code, Codex, OpenClaw, Cursor, Windsurf, Cline, Roo Code, Continue, GitHub Copilot, Aider, OpenCode, Zed, Augment, Amp, Gemini CLI, Google Antigravity, Pi, Hermes, and a generic Agents provider. Disable any via `asm config edit`.
21 providers: a generic Agents provider (supported by most harnesses except Claude Code), Claude Code, Pi, OpenCode, Codex, Oh My Pi, Grok CLI, Hermes, OpenClaw, Cursor, GitHub Copilot, Windsurf, Google Antigravity, Gemini CLI, Cline, Roo Code, Continue, Aider, Zed, Augment, and Amp. Disable any via `asm config edit`.

**What about the TUI?**
Run `asm` with no args for an optional local browser. The CLI is the primary interface for scripts, CI, and agent tool calls.
Expand Down Expand Up @@ -509,29 +509,31 @@ asm install github:anthropics/skills --all
<details>
<summary><strong>Supported agent tools</strong></summary>

19 built-in providers, all enabled by default. Disable via `asm config edit`.

| Tool | Global Path | Project Path |
| ------------------ | --------------------------------- | ----------------------- |
| Claude Code | `~/.claude/skills/` | `.claude/skills/` |
| Codex | `~/.codex/skills/` | `.codex/skills/` |
| OpenClaw | `~/.openclaw/skills/` | `.openclaw/skills/` |
| Agents (generic) | `~/.agents/skills/` | `.agents/skills/` |
| Cursor | `~/.cursor/rules/` | `.cursor/rules/` |
| Windsurf | `~/.windsurf/rules/` | `.windsurf/rules/` |
| Cline | `~/Documents/Cline/Rules/` | `.clinerules/` |
| Roo Code | `~/.roo/rules/` | `.roo/rules/` |
| Continue | `~/.continue/rules/` | `.continue/rules/` |
| GitHub Copilot | `~/.github/instructions/` | `.github/instructions/` |
| Aider | `~/.aider/skills/` | `.aider/skills/` |
| OpenCode | `~/.config/opencode/skills/` | `.opencode/skills/` |
| Zed | `~/.config/zed/prompt_overrides/` | `.zed/rules/` |
| Augment | `~/.augment/rules/` | `.augment/rules/` |
| Amp | `~/.amp/skills/` | `.amp/skills/` |
| Gemini CLI | `~/.gemini/skills/` | `.gemini/skills/` |
| Google Antigravity | `~/.antigravity/skills/` | `.antigravity/skills/` |
| Pi | `~/.pi/skills/` | `.pi/skills/` |
| Hermes | `~/.hermes/skills/` | `.hermes/skills/` |
21 built-in providers, all enabled by default. Disable via `asm config edit`.

| Tool | Global Path | Project Path |
| ------------------------------------------ | --------------------------------- | ----------------------- |
| Agents (most harnesses except Claude Code) | `~/.agents/skills/` | `.agents/skills/` |
| Claude Code | `~/.claude/skills/` | `.claude/skills/` |
| Pi | `~/.pi/skills/` | `.pi/skills/` |
| OpenCode | `~/.config/opencode/skills/` | `.opencode/skills/` |
| Codex | `~/.codex/skills/` | `.codex/skills/` |
| Oh My Pi | `~/.omp/agent/skills/` | `.omp/skills/` |
| Grok CLI | `~/.grok/skills/` | `.grok/skills/` |
| Hermes | `~/.hermes/skills/` | `.hermes/skills/` |
| OpenClaw | `~/.openclaw/skills/` | `.openclaw/skills/` |
| Cursor | `~/.cursor/rules/` | `.cursor/rules/` |
| GitHub Copilot | `~/.github/instructions/` | `.github/instructions/` |
| Windsurf | `~/.windsurf/rules/` | `.windsurf/rules/` |
| Google Antigravity | `~/.antigravity/skills/` | `.antigravity/skills/` |
| Gemini CLI | `~/.gemini/skills/` | `.gemini/skills/` |
| Cline | `~/Documents/Cline/Rules/` | `.clinerules/` |
| Roo Code | `~/.roo/rules/` | `.roo/rules/` |
| Continue | `~/.continue/rules/` | `.continue/rules/` |
| Aider | `~/.aider/skills/` | `.aider/skills/` |
| Zed | `~/.config/zed/prompt_overrides/` | `.zed/rules/` |
| Augment | `~/.augment/rules/` | `.augment/rules/` |
| Amp | `~/.amp/skills/` | `.amp/skills/` |

Add custom providers in config.

Expand Down Expand Up @@ -964,7 +966,7 @@ On first run, config is created at `~/.config/agent-skill-manager/config.json`:
}
```

- All 19 providers are enabled by default
- All 21 providers are enabled by default
- Set `"enabled": false` to skip a provider you don't use
- Add arbitrary directories via `customPaths`
- Manage via `asm config show|path|reset|edit` or press `c` in the TUI
Expand Down
83 changes: 70 additions & 13 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ describe("getDefaultConfig", () => {
expect(config.version).toBe(1);
});

it("returns 19 default providers", () => {
it("returns 21 default providers", () => {
const config = getDefaultConfig();
expect(config.providers).toHaveLength(19);
expect(config.providers).toHaveLength(21);
});

it("includes all 19 default providers in priority order", () => {
it("includes all 21 default providers in priority order", () => {
const config = getDefaultConfig();
const names = config.providers.map((p) => p.name);
expect(names).toEqual([
"agents",
"claude",
"codex",
"opencode",
"pi",
"opencode",
"codex",
"omp",
"grok",
"hermes",
"openclaw",
"agents",
"cursor",
"copilot",
"windsurf",
Expand All @@ -55,9 +57,9 @@ describe("getDefaultConfig", () => {
]);
});

it("all 19 providers are enabled by default", () => {
it("all 21 providers are enabled by default", () => {
const config = getDefaultConfig();
expect(config.providers).toHaveLength(19);
expect(config.providers).toHaveLength(21);
expect(config.providers.every((p) => p.enabled)).toBe(true);
});

Expand All @@ -76,7 +78,7 @@ describe("getDefaultConfig", () => {
const a = getDefaultConfig();
const b = getDefaultConfig();
a.providers[0].name = "mutated";
expect(b.providers[0].name).toBe("claude");
expect(b.providers[0].name).toBe("agents");
});
});

Expand Down Expand Up @@ -202,7 +204,7 @@ describe("config backup on corruption", () => {

// Should return defaults
expect(config.version).toBe(1);
expect(config.providers).toHaveLength(19);
expect(config.providers).toHaveLength(21);

// Should have created backup
const backup = await readFile(backupPath, "utf-8");
Expand Down Expand Up @@ -446,13 +448,68 @@ describe("mergeWithDefaults priority-order insertion", () => {

const config = await loadConfig();
const piIdx = config.providers.findIndex((p) => p.name === "pi");
const claudeIdx = config.providers.findIndex((p) => p.name === "claude");
const opencodeIdx = config.providers.findIndex(
(p) => p.name === "opencode",
);
const hermesIdx = config.providers.findIndex((p) => p.name === "hermes");

expect(piIdx).toBeGreaterThan(opencodeIdx);
expect(piIdx).toBeLessThan(hermesIdx);
// Post-#617 canonical slot: pi sits between claude and opencode.
expect(piIdx).toBeGreaterThan(claudeIdx);
expect(piIdx).toBeLessThan(opencodeIdx);
});

it("canonicalizes a saved config that predates the #617 reorder", async () => {
await mkdir(dirname(configPath), { recursive: true });
// Pre-#617 saved order: claude first, agents seventh, no omp/grok.
const legacyNames = [
"claude",
"codex",
"opencode",
"pi",
"hermes",
"openclaw",
"agents",
"cursor",
"copilot",
"windsurf",
"antigravity",
"gemini",
"cline",
"roocode",
"continue",
"aider",
"zed",
"augment",
"amp",
];
const partial = {
version: 1,
providers: legacyNames.map((name) => ({
name,
label: name,
global: `~/.${name}/skills`,
project: `.${name}/skills`,
enabled: true,
})),
preferences: { defaultScope: "both", defaultSort: "name" },
};
await writeFile(configPath, JSON.stringify(partial), "utf-8");

const config = await loadConfig();
const names = config.providers.map((p) => p.name);

expect(names.slice(0, 7)).toEqual([
"agents",
"claude",
"pi",
"opencode",
"codex",
"omp",
"grok",
]);
expect(names).toHaveLength(21);
// Nothing the user had is dropped by the reorder.
for (const name of legacyNames) expect(names).toContain(name);
});

it("preserves user-added custom providers in place when adding new defaults", async () => {
Expand Down
70 changes: 53 additions & 17 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ export function getConfigDir(): string {
}

const DEFAULT_PROVIDERS: ProviderConfig[] = [
// ── Priority providers (ordered by user preference) ──
// ── Priority providers (ordered by user preference, #617) ──
{
name: "agents",
label: "Agents",
global: "~/.agents/skills",
project: ".agents/skills",
enabled: true,
},
{
name: "claude",
label: "Claude Code",
Expand All @@ -33,10 +40,10 @@ const DEFAULT_PROVIDERS: ProviderConfig[] = [
enabled: true,
},
{
name: "codex",
label: "Codex",
global: "~/.codex/skills",
project: ".codex/skills",
name: "pi",
label: "Pi",
global: "~/.pi/skills",
project: ".pi/skills",
enabled: true,
},
{
Expand All @@ -47,12 +54,27 @@ const DEFAULT_PROVIDERS: ProviderConfig[] = [
enabled: true,
},
{
name: "pi",
label: "Pi",
global: "~/.pi/skills",
project: ".pi/skills",
name: "codex",
label: "Codex",
global: "~/.codex/skills",
project: ".codex/skills",
enabled: true,
},
{
name: "omp",
label: "Oh My Pi",
global: "~/.omp/agent/skills",
project: ".omp/skills",
enabled: true,
},
{
name: "grok",
label: "Grok CLI",
global: "~/.grok/skills",
project: ".grok/skills",
enabled: true,
},
// ── Additional providers ──
{
name: "hermes",
label: "Hermes",
Expand All @@ -67,14 +89,6 @@ const DEFAULT_PROVIDERS: ProviderConfig[] = [
project: ".openclaw/skills",
enabled: true,
},
// ── Additional providers ──
{
name: "agents",
label: "Agents",
global: "~/.agents/skills",
project: ".agents/skills",
enabled: true,
},
{
name: "cursor",
label: "Cursor",
Expand Down Expand Up @@ -264,6 +278,28 @@ function mergeWithDefaults(config: Partial<AppConfig>): AppConfig {
existingNames.add(defaultProvider.name);
}

// Canonicalize order so DEFAULT_PROVIDERS ordering wins for built-ins (#617).
// Reordering the defaults must reach users who already have a saved config,
// not just fresh installs. A user-added provider sorts just after the
// built-in that preceded it at this point, which is after the new defaults
// the insertion loop above already slotted in ahead of it.
const defaultRank = new Map(
defaults.providers.map((p, i) => [p.name, i] as const),
);
const sortKey = new Map<ProviderConfig, number>();
let lastRank = -1;
for (const p of providers) {
const rank = defaultRank.get(p.name);
if (rank === undefined) {
sortKey.set(p, lastRank + 0.5);
} else {
lastRank = rank;
sortKey.set(p, rank);
}
}
// Stable sort keeps consecutive user-added providers in their saved order.
providers.sort((a, b) => sortKey.get(a)! - sortKey.get(b)!);

return {
version: config.version ?? defaults.version,
providers,
Expand Down
13 changes: 11 additions & 2 deletions src/installer-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,17 @@ export async function resolveProvider(

const pickerItems = config.providers.map((p) => ({
label: `${p.label} (${p.name})`,
hint: p.global,
checked: savedSet ? savedSet.has(p.name) : p.name === "agents",
// The Claude Code exception leads so it survives hint truncation on
// narrow terminals (#617) — a 15–25 col hint still names the exception.
hint:
p.name === "agents"
? `except Claude Code; most harnesses — ${p.global}`
: p.global,
// First-time setup pre-checks Agents + Claude Code (#617), but never a
// provider the user has disabled — a pre-checked row installs on Enter.
checked: savedSet
? savedSet.has(p.name)
: p.enabled && (p.name === "agents" || p.name === "claude"),
}));

const selectedIndices = await checkboxPicker({ items: pickerItems });
Expand Down
Loading
Loading