Skip to content

Commit 79b3e66

Browse files
authored
fix: plugin discovery with recursive directory scanning (#25)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 18085ee commit 79b3e66

19 files changed

+1293
-47
lines changed

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# aipm
22

3-
**A plugin manager for AI coding assistants.**
3+
**A Cursor plugin manager with Claude Code marketplace federation.**
44

5-
Install plugins once, use them everywhere. Manage curated plugin collections across Claude Code, Cursor, and other AI assistants from multiple marketplace sources.
5+
Manage Cursor plugins from multiple sources including AIPM marketplaces and **auto-discovered Claude Code marketplaces**. Install plugins to Cursor from Claude Code's ecosystem without manual configuration.
66

7-
Inspired by [Claude Code's plugin marketplace system](https://docs.claude.com/en/docs/claude-code/plugin-marketplaces), extended to work across multiple AI assistants.
7+
**Simple model**: AIPM manages Cursor's `.cursor/` directory and can read from Claude Code's `.claude/` marketplaces for plugin discovery.
8+
9+
Inspired by [Claude Code's plugin marketplace system](https://docs.claude.com/en/docs/claude-code/plugin-marketplaces), extended with marketplace federation and nested plugin structure support.
810

911
## Quick Start
1012

@@ -15,12 +17,37 @@ mise use -g ubi:TrogonStack/aipm
1517
# Or download pre-built binary for your platform
1618
# See INSTALLATION.md for platform-specific download commands
1719

18-
# Use
20+
# Initialize (auto-detects Cursor or Claude Code)
1921
aipm init
22+
23+
# Add a marketplace
2024
aipm marketplace add team https://github.com/your-org/plugins.git
25+
26+
# Install plugins (works with nested structures)
2127
aipm plugin install my-plugin@team
28+
aipm plugin install document-skills/docx@anthropic # nested plugin support
2229
```
2330

31+
### Claude Code Marketplace Federation
32+
33+
If you have Claude Code installed, **AIPM automatically discovers its marketplaces**:
34+
35+
```bash
36+
# AIPM scans ~/.claude/plugins/known_marketplaces.json automatically
37+
aipm list
38+
39+
📦 Marketplaces:
40+
• claude:anthropic-agent-skills (🤖 Claude Code auto-discovered)
41+
• claude:claude-code-workflows (🤖 Claude Code auto-discovered)
42+
43+
# Install Claude Code plugins to Cursor
44+
aipm plugin install algorithmic-art@claude:anthropic-agent-skills
45+
aipm plugin install document-skills/docx@claude:anthropic-agent-skills
46+
aipm sync # Installs to .cursor/ for Cursor to use
47+
```
48+
49+
**Federation Model**: AIPM reads from Claude Code's marketplaces but installs everything to `.cursor/` (for Cursor). This gives you access to Claude Code's plugin ecosystem in Cursor without manual configuration.
50+
2451
## Documentation
2552

2653
**See [docs/](./docs/) for complete documentation.**

docs/tutorials/getting-started.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Getting Started with aipm
22

3-
A hands-on tutorial to get you started with aipm in 15 minutes.
3+
A hands-on tutorial to get you started with aipm in 15 minutes. Works with both **Cursor** and **Claude Code**!
44

55
## Prerequisites
66

77
- `aipm` installed ([Installation Guide](../../INSTALLATION.md))
88
- A project directory (we'll create one for testing)
9+
- Either Cursor or Claude Code (aipm auto-detects which one you're using)
910

1011
## Part 1: Verify Installation
1112

@@ -21,6 +22,8 @@ aipm --help
2122

2223
Expected output: Version number (e.g., `0.1.0`)
2324

25+
> **Note for Claude Code users**: AIPM is a Cursor plugin manager. It can discover and install plugins from Claude Code's marketplaces, but always installs them to Cursor's `.cursor/` directory. See the "Working with Claude Code's Official Marketplaces" section below.
26+
2427
## Part 2: Create Your First Plugin
2528

2629
Let's create a test project and add a simple plugin:
@@ -153,6 +156,34 @@ When you run `aipm list`, you should see:
153156
✓ hello-world@my-marketplace
154157
```
155158

159+
## Working with Claude Code's Official Marketplaces
160+
161+
If you have Claude Code installed with official Anthropic marketplaces, **AIPM automatically discovers them**:
162+
163+
```bash
164+
# No configuration needed - Claude Code marketplaces are auto-discovered!
165+
aipm list
166+
# Shows: claude:anthropic-agent-skills (auto-discovered from Claude Code)
167+
168+
# Install Claude Code plugins to Cursor
169+
aipm plugin install algorithmic-art@claude:anthropic-agent-skills
170+
aipm plugin install document-skills/docx@claude:anthropic-agent-skills # nested plugin!
171+
172+
# Sync to .cursor/ directory (for Cursor to use)
173+
aipm sync
174+
175+
# View all available plugins from both AIPM and Claude Code
176+
aipm plugin search
177+
```
178+
179+
**How it works**:
180+
181+
- AIPM reads `~/.claude/plugins/known_marketplaces.json` to discover Claude Code's marketplaces
182+
- Plugins are installed to `.cursor/` (AIPM never modifies `.claude/`)
183+
- You get the best of both worlds: Claude Code's marketplaces work in Cursor!
184+
185+
**Nested Plugin Structure**: Claude Code's marketplaces use nested directories (like `document-skills/docx/`). AIPM automatically discovers all plugins regardless of nesting depth.
186+
156187
## Next Steps
157188

158189
Congratulations! You've successfully:

src/commands/list.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ export async function list(options: ListOptions = {}): Promise<void> {
3232
if (marketplaceCount > 0) {
3333
console.log('\n📦 Marketplaces:');
3434
for (const [name, marketplace] of Object.entries(config.marketplaces)) {
35+
const isClaudeMarketplace = name.startsWith('claude:');
36+
const sourceLabel = isClaudeMarketplace ? '🤖 Claude Code (auto-discovered)' : marketplace.source;
37+
3538
console.log(` • ${name}`);
36-
console.log(` Source: ${marketplace.source}`);
39+
console.log(` Source: ${sourceLabel}`);
3740

3841
if (marketplace.source === 'directory' && marketplace.path) {
3942
console.log(` Path: ${marketplace.path}`);

src/commands/plugin-install.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'node:path';
22
import { z } from 'zod';
33
import { getConfigPaths, loadPluginsConfig } from '../config/loader';
4+
import { DIR_CURSOR } from '../constants';
45
import { fileExists, writeJsonFile } from '../helpers/fs';
56
import { resolveMarketplacePath } from '../helpers/git';
67
import { defaultIO } from '../helpers/io';
@@ -119,7 +120,7 @@ export async function pluginInstall(options: unknown): Promise<void> {
119120
await writeJsonFile(targetPath, updatedConfig, PluginsConfigSchema);
120121
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
121122

122-
const cursorDir = join(cwd, '.cursor');
123+
const cursorDir = join(cwd, DIR_CURSOR);
123124
const syncResult = await syncPluginToCursor(pluginPath, marketplaceName, pluginName, cursorDir);
124125
const summary = formatSyncResult(syncResult);
125126

src/commands/plugin-uninstall.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { rm } from 'node:fs/promises';
22
import { join } from 'node:path';
33
import { z } from 'zod';
44
import { getConfigPaths, loadPluginsConfig } from '../config/loader';
5+
import { DIR_CURSOR, DIR_MARKETPLACE } from '../constants';
56
import { fileExists, writeJsonFile } from '../helpers/fs';
67
import { defaultIO } from '../helpers/io';
78
import { PluginsConfigSchema } from '../schema';
@@ -53,7 +54,7 @@ export async function pluginUninstall(options: unknown): Promise<void> {
5354
if (cmd.dryRun) {
5455
defaultIO.logInfo(`[DRY RUN] Would remove plugin '${cmd.pluginId}' from ${configName}`);
5556
if (cmd.removeFiles) {
56-
defaultIO.logInfo(`[DRY RUN] Would delete files from .cursor/marketplace/`);
57+
defaultIO.logInfo('[DRY RUN] Would delete files from .cursor/marketplace/');
5758
}
5859
} else {
5960
await writeJsonFile(targetPath, updatedConfig, PluginsConfigSchema);
@@ -63,7 +64,7 @@ export async function pluginUninstall(options: unknown): Promise<void> {
6364
const [pluginName, marketplaceName] = cmd.pluginId.split('@');
6465

6566
if (pluginName && marketplaceName) {
66-
const installedPath = join(cwd, '.cursor', 'marketplace', marketplaceName, pluginName);
67+
const installedPath = join(cwd, DIR_CURSOR, DIR_MARKETPLACE, marketplaceName, pluginName);
6768

6869
if (await fileExists(installedPath)) {
6970
await rm(installedPath, { recursive: true, force: true });

src/commands/plugin-update.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'node:path';
22
import { z } from 'zod';
33
import { getConfigPaths, loadPluginsConfig } from '../config/loader';
4+
import { DIR_CURSOR } from '../constants';
45
import { fileExists } from '../helpers/fs';
56
import { resolveMarketplacePath } from '../helpers/git';
67
import { defaultIO } from '../helpers/io';
@@ -98,7 +99,7 @@ export async function pluginUpdate(options: unknown): Promise<void> {
9899
return;
99100
}
100101

101-
const cursorDir = join(cwd, '.cursor');
102+
const cursorDir = join(cwd, DIR_CURSOR);
102103
const syncResult = await syncPluginToCursor(pluginPath, marketplaceName, pluginName, cursorDir);
103104
const summary = formatSyncResult(syncResult);
104105

src/commands/sync.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { rm, stat } from 'node:fs/promises';
22
import { join } from 'node:path';
33
import { z } from 'zod';
44
import { getConfigPaths, loadPluginsConfig } from '../config/loader';
5+
import { DIR_CURSOR, DIR_MARKETPLACE } from '../constants';
56
import { ensureDir, fileExists } from '../helpers/fs';
67
import { resolveMarketplacePath } from '../helpers/git';
78
import { defaultIO } from '../helpers/io';
@@ -20,6 +21,7 @@ export async function sync(options: SyncOptions = {}): Promise<void> {
2021

2122
const cwd = cmd.cwd || process.cwd();
2223
const paths = getConfigPaths(cwd);
24+
const cursorDir = join(cwd, DIR_CURSOR);
2325

2426
try {
2527
if (!(await fileExists(paths.plugins))) {
@@ -44,11 +46,9 @@ export async function sync(options: SyncOptions = {}): Promise<void> {
4446

4547
console.log(`\n🔄 Syncing ${enabledPlugins.length} enabled plugin(s)...\n`);
4648

47-
const cursorDir = join(cwd, '.cursor');
48-
4949
if (!cmd.dryRun) {
5050
// Clean up old marketplace directory if it exists
51-
const oldMarketplaceDir = join(cursorDir, 'marketplace');
51+
const oldMarketplaceDir = join(cursorDir, DIR_MARKETPLACE);
5252
if (await fileExists(oldMarketplaceDir)) {
5353
await rm(oldMarketplaceDir, { recursive: true, force: true });
5454
}

src/config/loader.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import {
77
FILE_PLUGINS_EXAMPLE,
88
FILE_PLUGINS_LOCAL,
99
} from '../constants';
10+
import {
11+
convertClaudeMarketplaceToAIPM,
12+
isClaudeCodeInstalled,
13+
readClaudeCodeMarketplaces,
14+
} from '../helpers/claude-code-config';
1015
import { fileExists } from '../helpers/fs';
1116
import { getGlobalDir } from '../helpers/paths';
1217
import type { PluginsConfig } from '../schema';
@@ -76,11 +81,30 @@ export async function loadPluginsConfig(baseDir: string): Promise<PluginsConfig
7681

7782
const { marketplaces: localMarketplaces, plugins: localPlugins } = await loadOptionalConfig(localConfigPath);
7883

84+
const claudeMarketplaces: Record<string, ReturnType<typeof convertClaudeMarketplaceToAIPM>> = {};
85+
if (await isClaudeCodeInstalled()) {
86+
const claudeCodeMarketplaces = await readClaudeCodeMarketplaces();
87+
88+
for (const marketplace of claudeCodeMarketplaces) {
89+
const prefixedName = `claude:${marketplace.name}`;
90+
91+
if (globalMarketplaces[prefixedName] || config.marketplaces[prefixedName] || localMarketplaces[prefixedName]) {
92+
console.warn(
93+
`⚠️ Skipping Claude Code marketplace '${prefixedName}' - name conflict with existing AIPM marketplace`,
94+
);
95+
continue;
96+
}
97+
98+
claudeMarketplaces[prefixedName] = convertClaudeMarketplaceToAIPM(marketplace);
99+
}
100+
}
101+
79102
return {
80103
marketplaces: {
81104
...globalMarketplaces,
82105
...config.marketplaces,
83106
...localMarketplaces,
107+
...claudeMarketplaces,
84108
},
85109
plugins: {
86110
...globalPlugins,

src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* File and directory names
33
*/
44
export const DIR_CURSOR = '.cursor';
5+
export const DIR_CLAUDE = '.claude';
56
export const DIR_CLAUDE_PLUGIN = '.claude-plugin';
67
export const DIR_MARKETPLACE = 'marketplace';
78
export const DIR_CACHE = 'cache';
@@ -17,6 +18,13 @@ export const FILE_MARKETPLACE_MANIFEST = 'marketplace.json';
1718
export const FILE_PLUGIN_MANIFEST = 'plugin.json';
1819
export const FILE_GITIGNORE = '.gitignore';
1920

21+
/**
22+
* Claude Code file names
23+
*/
24+
export const FILE_CLAUDE_KNOWN_MARKETPLACES = 'known_marketplaces.json';
25+
export const FILE_CLAUDE_INSTALLED_PLUGINS = 'installed_plugins.json';
26+
export const FILE_CLAUDE_CONFIG = 'config.json';
27+
2028
/**
2129
* Environment variables
2230
*/

0 commit comments

Comments
 (0)