diff --git a/data/ecosystem-map.json b/data/ecosystem-map.json new file mode 100644 index 00000000..ea36c0b2 --- /dev/null +++ b/data/ecosystem-map.json @@ -0,0 +1,74 @@ +{ + "categories": [ + { + "id": "skills", + "name": "Skills", + "count": 42, + "description": "Reusable capabilities that agents install and compose into workflows.", + "growth": 18, + "topItems": [ + "Agent Memory Kit", + "Agent Autonomy Kit", + "Coding Agent", + "Google Workspace (gog)", + "GitHub CLI" + ] + }, + { + "id": "agents", + "name": "Agents", + "count": 64, + "description": "Public agent profiles across orchestration, coding, research, and automation.", + "growth": 14, + "topItems": [ + "Kai", + "Scout", + "Link", + "Claude", + "ChatGPT" + ] + }, + { + "id": "protocols", + "name": "Protocols", + "count": 7, + "description": "Interoperability standards and interfaces that connect tools, agents, and hosts.", + "growth": 10, + "topItems": [ + "Model Context Protocol (MCP)", + "Agent Client Protocol (ACP)", + "A2A", + "OpenAPI", + "JSON-RPC" + ] + }, + { + "id": "tools", + "name": "Tools", + "count": 36, + "description": "Integrations and operational tooling for storage, deployment, comms, and monitoring.", + "growth": 16, + "topItems": [ + "Slack", + "AWS S3", + "GitHub Actions", + "Datadog", + "Sentry" + ] + }, + { + "id": "community", + "name": "Community", + "count": 24, + "description": "Threads, events, and bounties that keep the ecosystem evolving.", + "growth": 22, + "topItems": [ + "Memory strategy discussions", + "Guardrails webinar", + "Agent Weekender Hackathon", + "Kubernetes MCP Server bounty", + "OpenAPI → Tool Schema Converter bounty" + ] + } + ] +} diff --git a/src/app/api/ecosystem/route.ts b/src/app/api/ecosystem/route.ts new file mode 100644 index 00000000..86bc67bb --- /dev/null +++ b/src/app/api/ecosystem/route.ts @@ -0,0 +1,324 @@ +import { NextResponse } from "next/server"; +import skillsData from "@/data/skills.json"; +import mcpServersData from "@/data/mcp-servers.json"; +import agentsData from "@/data/agents.json"; +import integrationsData from "@/../data/integrations.json"; +import communityThreadsData from "@/../data/community-threads.json"; +import eventsData from "@/../data/events.json"; +import bountiesData from "@/../data/bounties.json"; +import fallbackMapData from "@/../data/ecosystem-map.json"; + +type Skill = { + name: string; + tags: string[]; +}; + +type McpServer = { + name: string; + category: string; +}; + +type Agent = { + name: string; + featured?: boolean; + trustScore?: number; +}; + +type Integration = { + name: string; + installCount?: number; +}; + +type CommunityThread = { + title: string; + replyCount?: number; + lastActivity?: string; +}; + +type CommunityEvent = { + title: string; + date: string; + attendeeCount?: number; +}; + +type Bounty = { + title: string; + status: "open" | "claimed" | "submitted" | "completed"; + budget?: number; +}; + +type EcosystemCategoryId = "skills" | "agents" | "protocols" | "tools" | "community"; + +type EcosystemCategory = { + id: EcosystemCategoryId; + name: string; + count: number; + description: string; + growth: number; + topItems: string[]; +}; + +type EcosystemResponse = { + generatedAt: string; + totals: { + ecosystemNodes: number; + skillCount: number; + skillCategoryCount: number; + mcpServerCount: number; + agentCount: number; + integrationCount: number; + threadCount: number; + eventCount: number; + bountyCount: number; + }; + communityStats: { + threads: number; + events: number; + bounties: number; + openBounties: number; + }; + categories: EcosystemCategory[]; + connections: Array<{ + from: EcosystemCategoryId; + to: EcosystemCategoryId; + description: string; + }>; + usedFallback: boolean; +}; + +function clampGrowth(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(99, Math.round(value))); +} + +function growthFromCount(count: number, multiplier: number, offset: number): number { + return clampGrowth(count * multiplier + offset); +} + +function topTags(skills: Skill[], limit = 5): string[] { + const tagCounts = new Map(); + + for (const skill of skills) { + for (const tag of skill.tags ?? []) { + const key = tag.trim().toLowerCase(); + if (!key) continue; + tagCounts.set(key, (tagCounts.get(key) ?? 0) + 1); + } + } + + return Array.from(tagCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([tag, count]) => `${tag} (${count})`); +} + +function toFallbackResponse(): EcosystemResponse { + const fallbackCategories = (fallbackMapData as { categories: EcosystemCategory[] }).categories; + + const categoryById = new Map(fallbackCategories.map((category) => [category.id, category])); + const skills = categoryById.get("skills")?.count ?? 0; + const agents = categoryById.get("agents")?.count ?? 0; + const tools = categoryById.get("tools")?.count ?? 0; + const protocols = categoryById.get("protocols")?.count ?? 0; + const community = categoryById.get("community")?.count ?? 0; + + return { + generatedAt: new Date().toISOString(), + totals: { + ecosystemNodes: skills + agents + tools + protocols + community, + skillCount: skills, + skillCategoryCount: 0, + mcpServerCount: protocols, + agentCount: agents, + integrationCount: tools, + threadCount: 0, + eventCount: 0, + bountyCount: 0, + }, + communityStats: { + threads: 0, + events: 0, + bounties: 0, + openBounties: 0, + }, + categories: fallbackCategories, + connections: [ + { from: "skills", to: "agents", description: "Skills are installed and orchestrated by agents." }, + { from: "protocols", to: "tools", description: "Protocols define how tools are exposed and consumed." }, + { from: "community", to: "skills", description: "Community requests and feedback inspire new skills." }, + ], + usedFallback: true, + }; +} + +function computeEcosystemOverview(): EcosystemResponse { + const skills = skillsData as Skill[]; + const mcpServers = mcpServersData as McpServer[]; + const agents = agentsData as Agent[]; + const integrations = integrationsData as Integration[]; + const communityThreads = communityThreadsData as CommunityThread[]; + const events = eventsData as CommunityEvent[]; + const bounties = bountiesData as Bounty[]; + + const skillTags = new Set( + skills + .flatMap((skill) => skill.tags ?? []) + .map((tag) => tag.trim().toLowerCase()) + .filter(Boolean) + ); + + const mcpCategories = new Set(mcpServers.map((server) => server.category)); + + const protocolsTopItems = [ + "Model Context Protocol (MCP)", + "Agent Client Protocol (ACP)", + "A2A", + "OpenAPI", + ...Array.from(mcpCategories).slice(0, 2).map((category) => `MCP category: ${category}`), + ].slice(0, 5); + + const featuredAgents = agents + .slice() + .sort((a, b) => { + const trustA = a.trustScore ?? 0; + const trustB = b.trustScore ?? 0; + return trustB - trustA; + }) + .sort((a, b) => Number(Boolean(b.featured)) - Number(Boolean(a.featured))); + + const topIntegrationItems = integrations + .slice() + .sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0)) + .slice(0, 5) + .map((integration) => integration.name); + + const topThreads = communityThreads + .slice() + .sort((a, b) => { + const replyDiff = (b.replyCount ?? 0) - (a.replyCount ?? 0); + if (replyDiff !== 0) return replyDiff; + return new Date(b.lastActivity ?? 0).getTime() - new Date(a.lastActivity ?? 0).getTime(); + }) + .slice(0, 3) + .map((thread) => thread.title); + + const upcomingEvents = events + .slice() + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + .slice(0, 2) + .map((event) => event.title); + + const activeBounties = bounties + .filter((bounty) => bounty.status === "open" || bounty.status === "claimed") + .sort((a, b) => (b.budget ?? 0) - (a.budget ?? 0)) + .slice(0, 2) + .map((bounty) => bounty.title); + + const categories: EcosystemCategory[] = [ + { + id: "skills", + name: "Skills", + count: skills.length, + description: `Curated skills with ${skillTags.size} distinct tags and reusable patterns.`, + growth: growthFromCount(skills.length, 0.7, 8), + topItems: topTags(skills), + }, + { + id: "agents", + name: "Agents", + count: agents.length, + description: "Public agent profiles spanning orchestration, coding, research, and automation.", + growth: growthFromCount(agents.length, 0.35, 6), + topItems: featuredAgents.slice(0, 5).map((agent) => agent.name), + }, + { + id: "protocols", + name: "Protocols", + count: 1 + mcpCategories.size, + description: `MCP plus ${mcpCategories.size} MCP server categories powering interoperability.`, + growth: growthFromCount(mcpServers.length, 0.45, 5), + topItems: protocolsTopItems, + }, + { + id: "tools", + name: "Tools", + count: integrations.length, + description: "Integrations and operational tooling for storage, comms, security, and deployment.", + growth: growthFromCount(integrations.length, 0.8, 7), + topItems: topIntegrationItems, + }, + { + id: "community", + name: "Community", + count: communityThreads.length + events.length + bounties.length, + description: "Forum threads, events, and bounties that drive ecosystem momentum.", + growth: growthFromCount(communityThreads.length + events.length + bounties.length, 0.65, 9), + topItems: [...topThreads, ...upcomingEvents, ...activeBounties].slice(0, 5), + }, + ]; + + const response: EcosystemResponse = { + generatedAt: new Date().toISOString(), + totals: { + ecosystemNodes: categories.reduce((sum, category) => sum + category.count, 0), + skillCount: skills.length, + skillCategoryCount: skillTags.size, + mcpServerCount: mcpServers.length, + agentCount: agents.length, + integrationCount: integrations.length, + threadCount: communityThreads.length, + eventCount: events.length, + bountyCount: bounties.length, + }, + communityStats: { + threads: communityThreads.length, + events: events.length, + bounties: bounties.length, + openBounties: bounties.filter((bounty) => bounty.status === "open").length, + }, + categories, + connections: [ + { + from: "skills", + to: "agents", + description: "Agents install skills to execute tasks and workflows.", + }, + { + from: "protocols", + to: "tools", + description: "MCP and related standards connect integrations into agent runtimes.", + }, + { + from: "tools", + to: "community", + description: "Community members adopt integrations and report improvements.", + }, + { + from: "community", + to: "skills", + description: "Threads, events, and bounties shape what skills get built next.", + }, + ], + usedFallback: false, + }; + + return response; +} + +export async function GET() { + try { + return NextResponse.json(computeEcosystemOverview(), { + headers: { + "Cache-Control": "public, max-age=300", + }, + }); + } catch (error) { + console.error("Failed to compute ecosystem overview", error); + + return NextResponse.json(toFallbackResponse(), { + headers: { + "Cache-Control": "no-store", + }, + }); + } +} diff --git a/src/app/ecosystem/page.tsx b/src/app/ecosystem/page.tsx index 935ddbcf..348f196c 100644 --- a/src/app/ecosystem/page.tsx +++ b/src/app/ecosystem/page.tsx @@ -1,325 +1,280 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; -import ecosystemData from "@/data/ecosystem.json"; +import { Badge } from "@/components/ui/badge"; -interface Node { - id: string; - name: string; - icon: string; - description: string; - url: string; -} +type EcosystemCategoryId = "skills" | "agents" | "protocols" | "tools" | "community"; -interface Category { - id: string; +type EcosystemCategory = { + id: EcosystemCategoryId; name: string; - color: string; - nodes: Node[]; -} + count: number; + description: string; + growth: number; + topItems: string[]; +}; -interface EcosystemData { - center: Node; - categories: Category[]; - stats: { - totalIntegrations: number; - protocolsSupported: number; - hostsCompatible: number; +type EcosystemResponse = { + generatedAt: string; + totals: { + ecosystemNodes: number; + skillCount: number; + skillCategoryCount: number; + mcpServerCount: number; + agentCount: number; + integrationCount: number; + threadCount: number; + eventCount: number; + bountyCount: number; }; -} + communityStats: { + threads: number; + events: number; + bounties: number; + openBounties: number; + }; + categories: EcosystemCategory[]; + connections: Array<{ + from: EcosystemCategoryId; + to: EcosystemCategoryId; + description: string; + }>; + usedFallback: boolean; +}; + +const categoryAccent: Record = { + skills: "#06D6A0", + agents: "#8B5CF6", + protocols: "#3B82F6", + tools: "#F59E0B", + community: "#EC4899", +}; -const data = ecosystemData as EcosystemData; +function formatDate(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "Unknown"; + + return date.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }); +} export default function EcosystemPage() { - const [selectedNode, setSelectedNode] = useState(null); - const [selectedCategory, setSelectedCategory] = useState(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const handleNodeClick = (node: Node, categoryId: string) => { - setSelectedNode(node); - setSelectedCategory(categoryId); - }; + useEffect(() => { + let cancelled = false; - const handleCenterClick = () => { - setSelectedNode(data.center); - setSelectedCategory(null); - }; + async function load() { + try { + setLoading(true); + setError(null); + + const response = await fetch("/api/ecosystem", { + cache: "no-store", + }); + + if (!response.ok) { + throw new Error("Failed to load ecosystem data"); + } + + const payload = (await response.json()) as EcosystemResponse; + + if (!cancelled) { + setData(payload); + } + } catch { + if (!cancelled) { + setError("Could not load ecosystem overview right now. Please try again."); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + } + + load(); + + return () => { + cancelled = true; + }; + }, []); + + const totalGrowth = useMemo(() => { + if (!data) return 0; + if (data.categories.length === 0) return 0; + + const sum = data.categories.reduce((acc, category) => acc + category.growth, 0); + return Math.round(sum / data.categories.length); + }, [data]); return (
- {/* Hero Section */} -
- {/* Subtle aurora background */} +
-
-
+
-
-

+
+

Ecosystem Map

-

- How forAgents.dev connects to the broader agent landscape +

+ Real-time snapshot of skills, agents, protocols, tools, and community momentum.

- - {/* Stats */} -
-
- {data.stats.totalIntegrations} - Total Integrations -
-
-
- {data.stats.protocolsSupported} - Protocols Supported -
-
-
- {data.stats.hostsCompatible} - Hosts Compatible + + {loading ? ( +
Loading ecosystem metrics…
+ ) : error ? ( +
{error}
+ ) : data ? ( +
+ + +
Nodes
+
{data.totals.ecosystemNodes}
+
+
+ + + +
Skills
+
{data.totals.skillCount}
+
+
+ + + +
Agents
+
{data.totals.agentCount}
+
+
+ + + +
MCP Servers
+
{data.totals.mcpServerCount}
+
+
+ + + +
Avg Growth
+
+{totalGrowth}%
+
+
-
+ ) : null}

- {/* Main Ecosystem Visualization */} -
-
- {/* Left Column - Ecosystem Graph */} -
-
- {/* Center Node */} -
- -
- - {/* Category Nodes - Positioned in a circle */} - {data.categories.map((category, categoryIndex) => { - const totalCategories = data.categories.length; - const angleStep = (2 * Math.PI) / totalCategories; - const angle = categoryIndex * angleStep - Math.PI / 2; // Start from top - const radius = 280; // Distance from center - - // Calculate position - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius; - - return ( -
- {/* Category Container */} -
- {/* Category Label */} -
+ {loading ? ( + + Loading category overview… + + ) : error ? ( + + {error} + + ) : data ? ( +
+
+ {data.categories.map((category) => ( + + +
+ {category.name} + - {category.name} -
- - {/* Nodes for this category */} -
- {category.nodes.map((node) => ( - - ))} -
-
-
- ); - })} -
-
- - {/* Right Column - Details Panel */} -
-
- {selectedNode ? ( - - -
- {selectedNode.icon} -
- {selectedNode.name} - {selectedCategory && ( - c.id === selectedCategory)?.color - }15`, - borderColor: data.categories.find((c) => c.id === selectedCategory) - ?.color, - color: data.categories.find((c) => c.id === selectedCategory)?.color, - }} - > - {data.categories.find((c) => c.id === selectedCategory)?.name} - - )} -
+ +{category.growth}% +
+

{category.description}

-

{selectedNode.description}

- - Visit {selectedNode.name} → - -
-
- ) : ( - - -
-
🗺️
-

Explore the Ecosystem

-

- Click on any node to learn more about how it connects to forAgents.dev -

+
+ {category.count}
+
Top items
+
    + {category.topItems.map((item) => ( +
  • • {item}
  • + ))} +
- )} + ))}
-
-
-
- + + + Connection map +

+ Text-based relationship view across ecosystem layers. +

+
+ + {data.connections.map((connection, index) => ( +
+
+ {connection.from} ──▶ {connection.to} +
+
{connection.description}
+
+ ))} +
+
- {/* Category Legend */} -
-

Categories

-
- {data.categories.map((category) => ( -
-
-
-
- {category.name} + + + Community activity snapshot + + +
+
+
Threads
+
{data.communityStats.threads}
+
+
+
Events
+
{data.communityStats.events}
+
+
+
Bounties
+
{data.communityStats.bounties}
+
+
+
Open bounties
+
{data.communityStats.openBounties}
+
-
- {category.nodes.length} {category.nodes.length === 1 ? 'integration' : 'integrations'} +
+ Last refreshed: {formatDate(data.generatedAt)} + {data.usedFallback ? " • showing seed fallback data" : ""}
-
-
- ))} -
-
- - - - {/* CTA Section */} -
-
-
-
- -
-

- Want to join the ecosystem? -

-

- We're always looking to expand our integrations with new protocols, hosts, tools, and - standards that make agents more powerful. -

- - + +
-
+ ) : null}
);