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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"description": "A Model Context Protocol (MCP) server for Grafana providing access to dashboards, datasources, and querying capabilities",
"source": {
"source": "github",
"repo": "amondnet/grafana-plugin"
"repo": "amondnet/mcp-grafana"
}
}
]
Expand Down
4 changes: 4 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,19 @@ The marketplace website is built with:
cd apps/web

# Install dependencies
npm install
bun install

# Development server
npm run dev
bun run dev

# Build for production
npm run build
bun run build

# Preview production build
npm run preview
bun run preview

# Generate static site
npm run generate
bun run generate
```

### Plugin Development
Expand Down Expand Up @@ -134,10 +134,10 @@ Users can add this marketplace and install plugins:
```bash
# For plugins with tests
cd plugins/<plugin-name>
npm run test
bun run test

# Type checking across plugins
npm run typecheck
bun run typecheck
```

## Key Files
Expand Down
21 changes: 17 additions & 4 deletions apps/web/app/components/InstallModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
interface Props {
pluginName: string
marketplaceJsonName?: string
marketplaceRepo?: string
open?: boolean
}

Expand All @@ -19,8 +21,19 @@ const isOpen = computed({
set: value => emit('update:open', value),
})

const marketplaceCommand = '/plugin marketplace add pleaseai/claude-code-plugins'
const installCommand = computed(() => `/plugin install ${props.pluginName}@pleaseai`)
// Determine marketplace command and install command based on marketplace info
const marketplaceCommand = computed(() => {
// Use org/repo format for marketplace add
const repo = props.marketplaceRepo || 'pleaseai/claude-code-plugins'
return `/plugin marketplace add ${repo}`
})

const installCommand = computed(() => {
// Use pluginName@marketplaceJsonName format for plugin install
// marketplaceJsonName comes from the "name" field in marketplace.json
const marketplaceJsonName = props.marketplaceJsonName || 'claude-code-plugins'
return `/plugin install ${props.pluginName}@${marketplaceJsonName}`
})

const copiedStates = reactive({
marketplace: false,
Expand Down Expand Up @@ -74,7 +87,7 @@ async function copyCommand(command: string, type: keyof typeof copiedStates) {
}

async function copyAllCommands() {
const allCommands = `${marketplaceCommand}\n${installCommand.value}`
const allCommands = `${marketplaceCommand.value}\n${installCommand.value}`

try {
// Check if clipboard API is available
Expand All @@ -92,7 +105,7 @@ async function copyAllCommands() {
catch (err) {
// Log error with full context for debugging
console.error('Failed to copy all commands to clipboard:', {
marketplaceCommand,
marketplaceCommand: marketplaceCommand.value,
installCommand: installCommand.value,
error: err instanceof Error ? err.message : String(err),
clipboardAvailable: isClipboardSupported.value,
Expand Down
78 changes: 68 additions & 10 deletions apps/web/app/components/PluginCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ interface Plugin {
name: string
description: string
version?: string
author?: string
source: PluginSource
author?: string | { name?: string, email?: string }
source: PluginSource | string
marketplaceRepo?: string
marketplaceJsonName?: string
}

interface PluginMetadata {
Expand All @@ -35,13 +37,19 @@ const isModalOpen = ref(false)
const pluginMetadata = ref<PluginMetadata | null>(null)
const loading = ref(false)

// Fetch plugin metadata from GitHub
// Fetch plugin metadata from GitHub (only for plugins without version in marketplace.json)
async function fetchPluginMetadata() {
// If version already exists in marketplace.json, don't fetch
if (props.plugin.version) {
// If version already exists in marketplace.json, don't fetch
return
}

// Local plugins should have metadata in marketplace.json - skip fetch
if (typeof props.plugin.source === 'string') {
return
}

// GitHub plugin - fetch from GitHub
loading.value = true
try {
const url = `https://raw.githubusercontent.com/${props.plugin.source.repo}/main/.claude-plugin/plugin.json`
Expand Down Expand Up @@ -104,8 +112,22 @@ const displayDescription = computed(() => {

// Extract author name from either marketplace.json or fetched metadata
// Handles both string and object formats
function getAuthorName(author: string | { name?: string } | undefined): string | undefined {
return typeof author === 'string' ? author : author?.name
// If author email is @anthropic.com, display "Anthropic" instead of individual name
function getAuthorName(author: string | { name?: string, email?: string } | undefined): string | undefined {
if (!author)
return undefined

// Handle string format
if (typeof author === 'string') {
return author
}

// Handle object format - check email domain
if (author.email?.endsWith('@anthropic.com')) {
return 'Anthropic'
}

return author.name
}

// Computed author - prefer marketplace.json, fallback to metadata
Expand All @@ -129,6 +151,27 @@ const hasContext = computed(() => {
return !!pluginMetadata.value?.contextFileName
})

// Computed GitHub source URL
const githubSourceUrl = computed(() => {
if (typeof props.plugin.source === 'string') {
// Local plugin - construct GitHub URL from marketplace repository
if (!props.plugin.marketplaceRepo) {
console.warn(`[PluginCard] No marketplace repository for local plugin: ${props.plugin.name}`)
return undefined
}

// marketplaceRepo is already in "org/repo" format
const repoPath = props.plugin.marketplaceRepo
// Convert local path to GitHub tree path (e.g., "./plugins/agent-sdk-dev" -> "tree/main/plugins/agent-sdk-dev")
const treePath = props.plugin.source.replace(/^\.\//, 'tree/main/')
const url = `https://github.com/${repoPath}/${treePath}`

return url
}
// GitHub plugin
return `https://github.com/${props.plugin.source.repo}`
})

// Consolidated badges configuration
interface Badge {
key: string
Expand All @@ -141,6 +184,18 @@ interface Badge {
const badges = computed<Badge[]>(() => {
const badgeList: Badge[] = []

// Marketplace badge (shown first)
if (props.plugin.marketplaceJsonName) {
const isOfficial = props.plugin.marketplaceJsonName === 'anthropic'
badgeList.push({
key: 'marketplace',
icon: isOfficial ? 'i-heroicons-shield-check' : 'i-heroicons-building-storefront',
label: props.plugin.marketplaceJsonName,
color: isOfficial ? 'primary' : 'success',
title: `From ${props.plugin.marketplaceJsonName} marketplace`,
})
}

// Author badges
if (displayAuthor.value === 'Google') {
badgeList.push({
Expand Down Expand Up @@ -232,7 +287,7 @@ watch(() => props.autoOpenModal, (shouldOpen) => {
</div>
<div class="flex items-center gap-2 text-xs text-muted">
<UIcon name="i-heroicons-code-bracket" class="shrink-0" />
<span class="truncate">{{ plugin.source.repo }}</span>
<span class="truncate">{{ typeof plugin.source === 'string' ? plugin.source : plugin.source.repo }}</span>
</div>
</div>
<div class="shrink-0">
Expand Down Expand Up @@ -277,7 +332,7 @@ watch(() => props.autoOpenModal, (shouldOpen) => {

<!-- Metadata -->
<div v-if="displayAuthor || displayLicense" class="flex flex-wrap gap-2 text-xs">
<div v-if="displayAuthor" class="flex items-center gap-1 text-muted">
<div v-if="displayAuthor && displayAuthor !== 'Anthropic' && displayAuthor !== 'Google'" class="flex items-center gap-1 text-muted">
<UIcon name="i-heroicons-user-circle" class="shrink-0" />
<span>{{ displayAuthor }}</span>
</div>
Expand All @@ -291,7 +346,8 @@ watch(() => props.autoOpenModal, (shouldOpen) => {
<template #footer>
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<UButton
:to="`https://github.com/${plugin.source.repo}`"
v-if="githubSourceUrl"
:to="githubSourceUrl"
target="_blank"
external
variant="outline"
Expand All @@ -307,7 +363,7 @@ watch(() => props.autoOpenModal, (shouldOpen) => {
color="primary"
size="sm"
icon="i-heroicons-arrow-down-tray"
class="flex-1 justify-center"
:class="githubSourceUrl ? 'flex-1 justify-center' : 'w-full justify-center'"
title="View installation instructions"
@click="openInstallModal"
>
Expand All @@ -321,5 +377,7 @@ watch(() => props.autoOpenModal, (shouldOpen) => {
<InstallModal
v-model:open="isModalOpen"
:plugin-name="plugin.name"
:marketplace-json-name="plugin.marketplaceJsonName"
:marketplace-repo="plugin.marketplaceRepo"
/>
</template>
Loading