mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
feat(site): built-in SDK integrations and maintainer tiers in the catalog (#3766)
Co-authored-by: strandly-the-agent <strandly-the-agent@users.noreply.github.com>
This commit is contained in:
co-authored by
strandly-the-agent
parent
d2567ce5ec
commit
766e4f4e3d
@@ -188,10 +188,15 @@ jobs:
|
||||
// parses yes/on as strings, which the schema's z.boolean()
|
||||
// rejects at the build gate — only true booleans can reach main.
|
||||
function editorialState(data) {
|
||||
if (data === null) return { featured: true, badges: true };
|
||||
if (data === null) return { featured: true, badges: true, teamTier: true, partnerTier: true };
|
||||
const tier = typeof data.maintainedBy === 'string' ? data.maintainedBy : 'community';
|
||||
return {
|
||||
featured: data.featured === true,
|
||||
badges: Array.isArray(data.badges) && data.badges.length > 0,
|
||||
// `strands` and `aws` are team-granted; `partner` needs a
|
||||
// maintainer to verify the vendor relationship.
|
||||
teamTier: tier === 'strands' || tier === 'aws',
|
||||
partnerTier: tier === 'partner',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,6 +220,7 @@ jobs:
|
||||
// entry is flagged whenever its head state differs from its
|
||||
// base state (a brand-new file compares against empty).
|
||||
const flagged = [];
|
||||
const partnerFlagged = [];
|
||||
const changedPackages = [];
|
||||
for (const file of catalogFiles) {
|
||||
// A renamed file lives at previous_filename on the base ref;
|
||||
@@ -228,9 +234,16 @@ jobs:
|
||||
const headData = parseEntry(headText);
|
||||
const head = editorialState(headData);
|
||||
const base = editorialState(parseEntry(baseText));
|
||||
if (head.featured !== base.featured || head.badges !== base.badges) {
|
||||
if (
|
||||
head.featured !== base.featured ||
|
||||
head.badges !== base.badges ||
|
||||
head.teamTier !== base.teamTier
|
||||
) {
|
||||
flagged.push(file.filename);
|
||||
}
|
||||
if (head.partnerTier !== base.partnerTier) {
|
||||
partnerFlagged.push(file.filename);
|
||||
}
|
||||
if (headText) {
|
||||
const { github: entryGithub, pkgs } = extractEntryData(headData);
|
||||
for (const p of pkgs) {
|
||||
@@ -239,7 +252,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
if (flagged.length === 0 && changedPackages.length === 0) {
|
||||
if (flagged.length === 0 && partnerFlagged.length === 0 && changedPackages.length === 0) {
|
||||
console.log('No editorial changes or packages in touched entries — nothing to flag.');
|
||||
await clearStaleFlag();
|
||||
return;
|
||||
@@ -325,7 +338,7 @@ jobs:
|
||||
if (status !== 'MATCH') provenance.push({ ...p, status, detail });
|
||||
}
|
||||
|
||||
if (flagged.length === 0 && provenance.length === 0) {
|
||||
if (flagged.length === 0 && partnerFlagged.length === 0 && provenance.length === 0) {
|
||||
console.log('Provenance verified and no editorial changes — nothing to flag.');
|
||||
await clearStaleFlag();
|
||||
return;
|
||||
@@ -334,6 +347,9 @@ jobs:
|
||||
if (flagged.length > 0) {
|
||||
console.log(`Editorial fields changed by non-maintainer "${author}" in: ${flagged.join(', ')}`);
|
||||
}
|
||||
if (partnerFlagged.length > 0) {
|
||||
console.log(`Partner tier changed by non-maintainer "${author}" in: ${partnerFlagged.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!(await headStillCurrent())) return;
|
||||
|
||||
@@ -343,7 +359,7 @@ jobs:
|
||||
// exist in the repo, so create it on first use. A label failure
|
||||
// must not prevent the sticky comment below — the comment is the
|
||||
// primary signal.
|
||||
if (flagged.length > 0) {
|
||||
if (flagged.length > 0 || partnerFlagged.length > 0) {
|
||||
try {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
@@ -383,7 +399,7 @@ jobs:
|
||||
'',
|
||||
'#### Editorial catalog fields detected',
|
||||
'',
|
||||
`This PR changes \`featured\` and/or \`badges\` on: ${flagged.map(f => `\`${f}\``).join(', ')}.`,
|
||||
`This PR changes \`featured\`, \`badges\`, and/or a team-granted \`maintainedBy\` tier (\`strands\`/\`aws\`) on: ${flagged.map(f => `\`${f}\``).join(', ')}.`,
|
||||
'',
|
||||
'Those fields are granted by the Strands team, not set by submitters',
|
||||
'(see the [submission guide](https://strandsagents.com/docs/integrations/get-featured/)).',
|
||||
@@ -392,6 +408,20 @@ jobs:
|
||||
'submitter to revert it** before merging.'
|
||||
);
|
||||
}
|
||||
if (partnerFlagged.length > 0) {
|
||||
sections.push(
|
||||
'',
|
||||
'#### Partner maintainer tier claimed',
|
||||
'',
|
||||
`This PR adds or removes \`maintainedBy: partner\` on: ${partnerFlagged.map(f => `\`${f}\``).join(', ')}.`,
|
||||
'',
|
||||
'That tier means the integration is vended and supported by the company',
|
||||
"behind it. Before merging, a maintainer must verify the entry's `github`",
|
||||
'org is the official org of that company (and its packages are published',
|
||||
'by them) — an integration published from a personal account stays',
|
||||
'`community` regardless of what it integrates.'
|
||||
);
|
||||
}
|
||||
if (provenance.length > 0) {
|
||||
sections.push(
|
||||
'',
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
import type { CatalogCardModel } from '../../util/catalog'
|
||||
import { pathWithBase } from '../../util/links'
|
||||
import { DOWNLOAD_ICON, GITHUB_ICON, NPM_ICON, PYPI_ICON, STAR_ICON } from './icons'
|
||||
import { TYPE_ICONS, TYPE_LABELS, VERIFIED_ICON } from './types'
|
||||
import { AWS_ICON, DOWNLOAD_ICON, GITHUB_ICON, NPM_ICON, PYPI_ICON, STAR_ICON } from './icons'
|
||||
import { AWS_MARK_VIEWBOX, MAINTAINED_BY_FLAGS, MAINTAINED_BY_TITLES, TYPE_ICONS, TYPE_LABELS, VERIFIED_ICON } from './types'
|
||||
|
||||
interface Props {
|
||||
card: CatalogCardModel
|
||||
@@ -32,13 +32,26 @@ const href = card.external ? card.href : pathWithBase(card.href)
|
||||
---
|
||||
|
||||
<article
|
||||
class:list={['cat-card', { 'cat-card--featured': card.featured }]}
|
||||
class:list={['cat-card', { 'cat-card--featured': card.featured, 'cat-card--builtin': card.maintainedBy === 'strands' }]}
|
||||
data-type={card.integrationType}
|
||||
data-langs={card.languages.join(' ')}
|
||||
data-badges={[...card.badges, ...(card.featured ? ['featured'] : [])].join(' ')}
|
||||
data-maintained={card.maintainedBy}
|
||||
data-sdk={card.sdk}
|
||||
data-search={`${card.name} ${card.description} by ${card.maintainer}`.toLowerCase()}
|
||||
>
|
||||
{
|
||||
(card.featured || card.badges.includes('new')) && (
|
||||
<span class="cat-corner">
|
||||
{card.featured && (
|
||||
<span class="cat-tab cat-tab--featured" title="Featured by the Strands team">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
{card.badges.includes('new') && <span class="cat-tab cat-tab--new">New</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<span class="cat-tile" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d={TYPE_ICONS[card.integrationType]} />
|
||||
@@ -58,20 +71,8 @@ const href = card.external ? card.href : pathWithBase(card.href)
|
||||
title={card.name}
|
||||
>
|
||||
{card.name}
|
||||
{card.external && <span class="cat-external" aria-hidden="true">↗</span>}
|
||||
</a>
|
||||
{card.external && <span class="cat-external" aria-hidden="true">↗</span>}
|
||||
{card.featured && <span class="cat-flag cat-flag--featured" title="Featured by the Strands team">Featured</span>}
|
||||
{
|
||||
card.badges.includes('verified') && (
|
||||
<span class="cat-flag cat-flag--verified" title="Verified by the Strands team">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d={VERIFIED_ICON} />
|
||||
</svg>
|
||||
Verified
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{card.badges.includes('new') && <span class="cat-flag cat-flag--new">New</span>}
|
||||
<span class="cat-card-stats">
|
||||
{
|
||||
card.stars !== undefined && (
|
||||
@@ -94,6 +95,35 @@ const href = card.external ? card.href : pathWithBase(card.href)
|
||||
)
|
||||
}
|
||||
</span>
|
||||
{/* Badges hold the fixed right slot: every non-community card has one,
|
||||
while stats are sparse and flow after the title instead. */}
|
||||
{(card.badges.includes('verified') || card.maintainedBy !== 'community') && (
|
||||
<span class="cat-card-flags">
|
||||
{card.badges.includes('verified') && (
|
||||
<span class="cat-flag cat-flag--verified" title="Verified by the Strands team">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d={VERIFIED_ICON} />
|
||||
</svg>
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
{card.maintainedBy !== 'community' && (
|
||||
<span
|
||||
class:list={['cat-flag', `cat-flag--${card.maintainedBy}`]}
|
||||
title={MAINTAINED_BY_TITLES[card.maintainedBy]}
|
||||
aria-label={MAINTAINED_BY_FLAGS[card.maintainedBy]}
|
||||
>
|
||||
{card.maintainedBy === 'aws' ? (
|
||||
<svg width="31" height="19" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
|
||||
<path fill="currentColor" d={AWS_ICON} />
|
||||
</svg>
|
||||
) : (
|
||||
MAINTAINED_BY_FLAGS[card.maintainedBy]
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p class="cat-card-desc">{card.description}</p>
|
||||
<div class="cat-card-foot">
|
||||
|
||||
@@ -6,17 +6,18 @@
|
||||
* segments and the left rail's type/badge chips.
|
||||
*/
|
||||
import CatalogSegments from './CatalogSegments.astro'
|
||||
import { TYPE_ICONS, VERIFIED_ICON, type FacetOption } from './types'
|
||||
import { AWS_MARK_VIEWBOX, MAINTAINED_BY_ICONS, TYPE_ICONS, VERIFIED_ICON, type FacetOption } from './types'
|
||||
|
||||
interface Props {
|
||||
types: FacetOption[]
|
||||
maintainedBy: FacetOption[]
|
||||
showSdkFacet: boolean
|
||||
hasVerified: boolean
|
||||
hasFeatured: boolean
|
||||
hasNew: boolean
|
||||
}
|
||||
|
||||
const { types, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
|
||||
const { types, maintainedBy, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
|
||||
---
|
||||
|
||||
<div class="cat-facet-row">
|
||||
@@ -46,6 +47,22 @@ const { types, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="cat-type-rail" role="group" aria-label="Maintained by">
|
||||
{
|
||||
maintainedBy.map((t) => (
|
||||
<button class="cat-chip" type="button" data-cat-maintained={t.value} aria-label={t.label} aria-pressed="false">
|
||||
{MAINTAINED_BY_ICONS[t.value] ? (
|
||||
<svg width="29" height="18" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
|
||||
<path fill="currentColor" d={MAINTAINED_BY_ICONS[t.value]} />
|
||||
</svg>
|
||||
) : (
|
||||
t.label
|
||||
)}
|
||||
<span class="cat-count">{t.count}</span>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="cat-type-rail" role="group" aria-label="Type">
|
||||
{
|
||||
types.map((t) => (
|
||||
|
||||
@@ -25,3 +25,7 @@ export const PYPI_ICON: readonly string[] = ICONS.python
|
||||
export const NPM_ICON: readonly string[] = [
|
||||
'M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z',
|
||||
]
|
||||
|
||||
/** AWS smile mark (24x24, fill=currentColor) for aws-maintained entries. */
|
||||
export const AWS_ICON =
|
||||
'M6.763 10.036c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 0 1-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 0 1-.287-.375 6.18 6.18 0 0 1-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.391-.384-.59-.894-.59-1.533 0-.678.239-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272a2.287 2.287 0 0 1-.28.104.488.488 0 0 1-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 0 1 .224-.167c.279-.144.614-.264 1.005-.36a4.84 4.84 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647.439.43.662 1.085.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144.287-.096.543-.271.758-.51.128-.152.224-.32.272-.512.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 0 0-.735-.136 6.02 6.02 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 5.55a1.398 1.398 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 0 1-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 0 1-.303.08h-.687c-.151 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32l-1.238-5.148-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.319.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.777.777 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 0 1-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .359.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 0 1 .24.2.43.43 0 0 1 .071.263v.375c0 .168-.064.256-.184.256a.83.83 0 0 1-.303-.096 3.652 3.652 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.159.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926-.144.272-.336.511-.583.703-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167zM21.698 16.207c-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351 3.384 1.963 7.559 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.439-.2.814.287.383.607zM22.792 14.961c-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151.32-.79 1.03-2.57.695-2.994z'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AWS_ICON } from './icons'
|
||||
|
||||
// Canonical registry of catalog integration types: filter value, display
|
||||
// labels, and a 24x24 single-path glyph (fill=currentColor) rendered in cards
|
||||
// and facet chips. The zod enum in src/content.config.ts lists the same
|
||||
@@ -72,6 +74,29 @@ export const CATALOG_TYPES = [
|
||||
export const TYPE_LABELS: Record<string, string> = Object.fromEntries(CATALOG_TYPES.map((t) => [t.value, t.label]))
|
||||
export const TYPE_ICONS: Record<string, string> = Object.fromEntries(CATALOG_TYPES.map((t) => [t.value, t.icon]))
|
||||
|
||||
// Maintainer tiers, highest commitment first: who stands behind an
|
||||
// integration and what support users can infer. `label` names the facet chip;
|
||||
// `flag` names the card badge (an SDK entry's badge reads "Built-in", not the
|
||||
// team name). Values match the zod enum in src/content.config.ts — keep the
|
||||
// two in sync.
|
||||
export const MAINTAINED_BY_TIERS = [
|
||||
{ value: 'strands', label: 'Strands', flag: 'Built-in', title: 'Built into the Strands SDK' },
|
||||
{ value: 'aws', label: 'AWS', flag: 'AWS', title: 'Maintained by AWS' },
|
||||
{ value: 'partner', label: 'Partner', flag: 'Partner', title: 'Maintained by the partner behind the integration' },
|
||||
{ value: 'community', label: 'Community', flag: 'Community', title: 'Maintained by a community developer' },
|
||||
] as const
|
||||
|
||||
export const MAINTAINED_BY_FLAGS: Record<string, string> = Object.fromEntries(
|
||||
MAINTAINED_BY_TIERS.map((t) => [t.value, t.flag])
|
||||
)
|
||||
export const MAINTAINED_BY_TITLES: Record<string, string> = Object.fromEntries(
|
||||
MAINTAINED_BY_TIERS.map((t) => [t.value, t.title])
|
||||
)
|
||||
/** Tier glyphs for facet chips; only AWS carries a brand mark. */
|
||||
export const MAINTAINED_BY_ICONS: Record<string, string> = { aws: AWS_ICON }
|
||||
/** Crops AWS_ICON's square canvas to the mark's measured bounds (y 4.8-19.2). */
|
||||
export const AWS_MARK_VIEWBOX = '0 4.6 24 14.8'
|
||||
|
||||
/** Shield-check glyph for the verified badge (cards and the verified facet). */
|
||||
export const VERIFIED_ICON =
|
||||
'M12 1 3 5v6c0 5.6 3.8 10.7 9 12 5.2-1.3 9-6.4 9-12V5l-9-4zm-1.5 15L7 12.5 8.4 11l2.1 2.1 5.1-5.1L17 9.4 10.5 16z'
|
||||
|
||||
@@ -150,6 +150,11 @@ export const catalogEntrySchema = z
|
||||
// on-site docsPage, the card's primary link prefers this over the bare
|
||||
// GitHub repo so users land on usage instructions.
|
||||
docsUrl: z.string().url().startsWith('https://', 'docsUrl must be https').optional(),
|
||||
// Who stands behind the integration, shown and filtered on the catalog.
|
||||
// `strands` (SDK built-ins) and `aws` are granted by the Strands team;
|
||||
// `partner` requires a maintainer to verify the entry is vended by the
|
||||
// integrator's official org; submitters leave it unset (community).
|
||||
maintainedBy: z.enum(['strands', 'aws', 'partner', 'community']).default('community'),
|
||||
// Editorial fields — maintainer-granted only; submitters leave them unset.
|
||||
featured: z.boolean().default(false),
|
||||
badges: z.array(z.enum(['verified'])).default([]),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: ag-ui
|
||||
description: Connect agents to rich clients like CopilotKit through the AG-UI protocol, with streaming chat and tool-based generative UI
|
||||
integrationType: integration
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: ag-ui-strands
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: agent-control
|
||||
description: Runtime control plane that evaluates every agent input and output against configurable policies to block prompt injection, PII leakage, and rogue tool calls.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: agent-control-sdk[strands-agents]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Agent Skills
|
||||
description: Give agents on-demand access to specialized instructions with progressive disclosure that keeps the system prompt lean.
|
||||
integrationType: plugin
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/skills
|
||||
docsPage: docs/user-guide/concepts/plugins/skills
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: AgentCore Memory Store
|
||||
description: Amazon Bedrock AgentCore Memory as a Strands MemoryStore, with long-term recall and server-side memory extraction from conversation turns.
|
||||
integrationType: memory-store
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
typescript:
|
||||
package: bedrock-agentcore
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: agentcore-memory
|
||||
description: Amazon Bedrock AgentCore Memory session manager providing short-term conversation persistence and long-term memory strategies for Strands Agents.
|
||||
integrationType: session-manager
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: bedrock-agentcore[strands-agents]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: agentcore-payments
|
||||
description: Amazon Bedrock AgentCore plugin that intercepts HTTP 402 responses and processes x402 micropayments to access paid APIs and MCP servers.
|
||||
integrationType: plugin
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: bedrock-agentcore[strands-agents]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: agentcore-rl-toolkit
|
||||
description: AWS SDK for reinforcement learning training loops on Amazon Bedrock AgentCore, with built-in Strands Agents support via a decorator swap.
|
||||
integrationType: integration
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: agentcore-rl-toolkit
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: agentcore-tool-search
|
||||
description: Amazon Bedrock AgentCore Gateway plugin that loads only the tools relevant to each invocation via semantic search over hundreds of registered tools.
|
||||
integrationType: plugin
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: bedrock-agentcore[strands-agents]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Amazon Bedrock
|
||||
description: Run Strands agents on Amazon Bedrock models, with guardrails, prompt caching, and multi-modal input.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/bedrock.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/amazon-bedrock
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Amazon SageMaker
|
||||
description: Run Strands agents on models deployed to Amazon SageMaker inference endpoints, including JumpStart and fine-tuned models.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/sagemaker.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/sagemaker
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Anthropic
|
||||
description: Run Strands agents on Claude models through the Anthropic API, with structured output and prompt caching.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/anthropic.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/anthropic
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Bash
|
||||
description: Execute shell commands, with persistent bash sessions in TypeScript and sandboxed stateless shells in Python.
|
||||
integrationType: tool
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/shell
|
||||
docsPage: docs/user-guide/concepts/tools/vended-tools
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Bedrock Knowledge Base
|
||||
description: 'Back agent memory with Amazon Bedrock Knowledge Bases: semantic search over a managed vector store with document ingestion.'
|
||||
integrationType: memory-store
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_memory_stores/bedrock_knowledge_base
|
||||
docsPage: docs/user-guide/concepts/memory/bedrock-knowledge-base
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Cedar Authorization
|
||||
description: 'Control which tools an agent can invoke with Cedar policies: identity-aware access control, role-based permissions, and rate limits.'
|
||||
integrationType: intervention
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_interventions/cedar
|
||||
docsPage: docs/user-guide/concepts/agents/interventions/cedar-authorization
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: coinbase-agentkit-strands-agents
|
||||
description: Coinbase AgentKit extension for Strands Agents that provides onchain action tools for interacting with blockchain networks and wallets.
|
||||
integrationType: tool
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: coinbase-agentkit-strands-agents
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Context Injector
|
||||
description: 'Fold real-time text into the model input before each call: a clock, environment facts, or a lookup, without storing it in history.'
|
||||
integrationType: plugin
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/context_injector
|
||||
docsPage: docs/user-guide/concepts/plugins/context-injector
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Context Offloader
|
||||
description: Keep large tool results from consuming the context window by storing them externally and leaving retrievable references.
|
||||
integrationType: plugin
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/context_offloader
|
||||
docsPage: docs/user-guide/concepts/plugins/context-offloader
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: datadog-ai-guard
|
||||
description: Datadog AI Guard plugin that inspects and blocks prompt injection, jailbreaking, data exfiltration, and destructive tool calls in real time.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: ddtrace
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: File Editor
|
||||
description: View, create, and edit files in the workspace from agent tool calls.
|
||||
integrationType: tool
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/file_editor
|
||||
docsPage: docs/user-guide/concepts/tools/vended-tools
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: File Session Manager
|
||||
description: Persist agent conversations, tool state, and context to the local filesystem across sessions.
|
||||
integrationType: session-manager
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/session/file_session_manager.py
|
||||
docsPage: docs/user-guide/concepts/agents/session-management
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: GoalLoop
|
||||
description: Validate agent responses against a goal and loop with feedback until it is satisfied.
|
||||
integrationType: plugin
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/goal
|
||||
docsPage: docs/user-guide/concepts/plugins/goal-loop
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Google Gemini
|
||||
description: Run Strands agents on Google Gemini models, with built-in tools, structured output, and image and video input.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/gemini.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/google
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: headroom-ai
|
||||
description: Framework-agnostic context compression library that cuts LLM token usage 60–95% on JSON and 15–20% on code, with Strands support built in.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: headroom-ai[strands]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: hiddenlayer-strands
|
||||
description: HiddenLayer AI security integration for Strands Agents that detects and blocks threats such as prompt injection and model abuse in real time.
|
||||
integrationType: intervention
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: hiddenlayer-strands
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: hindsight-strands
|
||||
description: Persistent long-term memory and semantic search tools for Strands agents, powered by Vectorize's Hindsight memory service.
|
||||
integrationType: memory-store
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: hindsight-strands
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: HTTP Request
|
||||
description: Make HTTP requests to external APIs, with configurable clients for authentication, timeouts, and proxies.
|
||||
integrationType: tool
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/http_request
|
||||
docsPage: docs/user-guide/concepts/tools/vended-tools
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Human in the Loop
|
||||
description: Gate agent tool execution behind human approval, with modes for CLI, web, and custom UIs.
|
||||
integrationType: intervention
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_interventions/hitl
|
||||
docsPage: docs/user-guide/concepts/agents/interventions/human-in-the-loop
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: langfuse
|
||||
description: Langfuse LLM observability for Strands agents — traces, metrics, and evaluations via the SDK's built-in OpenTelemetry export (strands-agents[otel]).
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/langfuse/langfuse
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name: LiteLLM
|
||||
description: Run Strands agents on models from OpenAI, Anthropic, Amazon, and more through LiteLLM's unified API.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/litellm.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/litellm
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Llama API
|
||||
description: Run Strands agents on Meta-hosted Llama models without managing your own inference infrastructure.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/llamaapi.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/llamaapi
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,9 @@
|
||||
name: llama.cpp
|
||||
description: Run Strands agents on local quantized models served by llama.cpp, with grammar constraints and advanced sampling.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/llamacpp.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/llamacpp
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: llm-tracekit-strands
|
||||
description: OpenTelemetry instrumentation for Strands Agents that captures spans, traces, and metrics compatible with Coralogix and other OTel backends.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: llm-tracekit-strands
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Mistral AI
|
||||
description: Run Strands agents on Mistral AI models, with tool calling and streaming.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/mistral.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/mistral
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Notebook
|
||||
description: Manage persistent text notebooks that carry plans, notes, and scratch space across agent turns.
|
||||
integrationType: tool
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-ts/src/vended-tools/notebook
|
||||
docsPage: docs/user-guide/concepts/tools/vended-tools
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: nvidia-nat-strands
|
||||
description: NVIDIA NeMo Agent Toolkit integration adding enterprise-grade instrumentation, observability, and optimization to Strands Agents.
|
||||
integrationType: integration
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: nvidia-nat-strands
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Ollama
|
||||
description: 'Run Strands agents on local models with Ollama: text generation, image understanding, tool calling, and streaming.'
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/ollama.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/ollama
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: OpenAI Responses
|
||||
description: Run Strands agents on the OpenAI Responses API, with built-in tools like web search and server-side conversation state.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/openai_responses.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/openai-responses
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: OpenAI
|
||||
description: Run Strands agents on OpenAI models or any OpenAI-compatible API, with vision and structured output.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/openai.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/openai
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: openinference-instrumentation-strands-agents
|
||||
description: Arize AI OpenInference span processor for Strands Agents — transforms native OTel spans to OpenInference format for Arize AX and Phoenix observability.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: openinference-instrumentation-strands-agents
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: portkey
|
||||
description: Routes Strands model calls through Portkey's AI gateway via the OpenAI-compatible endpoint, adding guardrails, fallbacks, and observability.
|
||||
integrationType: model-provider
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/Portkey-AI/gateway
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: "respan"
|
||||
description: "OpenTelemetry instrumentation plugin that normalizes Strands Agents TS spans into the Respan observability platform."
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
typescript:
|
||||
package: "@respan/instrumentation-strands-agents"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: S3 Session Manager
|
||||
description: Persist agent conversations, tool state, and context to Amazon S3 across sessions.
|
||||
integrationType: session-manager
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/session/s3_session_manager.py
|
||||
docsPage: docs/user-guide/concepts/agents/session-management
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: s3-vectors-memory
|
||||
description: Long-term semantic memory for Strands Agents via Amazon S3 Vectors, storing conversation summaries as vectors and recalling them on later runs.
|
||||
integrationType: plugin
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: strands-s3-vectors-memory
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: sigil-sdk-strands
|
||||
description: Grafana Cloud AI Observability (Sigil) hooks for Strands Agents — sends agent, model, and tool lifecycle events to Grafana Sigil via SigilStrandsHookProvider.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: sigil-sdk-strands
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Sleep
|
||||
description: Pause agent execution for a bounded, cancellable duration.
|
||||
integrationType: tool
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/sleep
|
||||
docsPage: docs/user-guide/concepts/tools/vended-tools
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Steering
|
||||
description: Guide agents with just-in-time contextual feedback at key moments instead of front-loaded monolithic prompts.
|
||||
integrationType: intervention
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/steering
|
||||
docsPage: docs/user-guide/concepts/agents/interventions/steering
|
||||
addedDate: 2025-12-01
|
||||
@@ -1,6 +1,7 @@
|
||||
name: strands-apify
|
||||
description: Apify platform integration giving Strands Agents access to thousands of pre-built Actors for web scraping, search, and e-commerce crawling.
|
||||
integrationType: tool
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: strands-apify
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: strands-code-agent
|
||||
description: A coding agent that replaces tool-calling with code generation in a persistent Python REPL
|
||||
integrationType: agent-extension
|
||||
maintainedBy: aws
|
||||
languages:
|
||||
python:
|
||||
package: strands-code-agent
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: strands-dakera
|
||||
description: Persistent, decay-weighted MemoryStore backed by a self-hosted Dakera server, plus a full-CRUD memory tool sharing the same backend.
|
||||
integrationType: memory-store
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: strands-dakera
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: strands-token-telemetry
|
||||
description: Emits Strands agent token usage as CloudWatch EMF metrics for cost visibility and capacity planning.
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: strands-token-telemetry
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: temporal
|
||||
description: Durable execution for Strands agents via Temporal workflows, so every model call and tool invocation survives crashes and restarts.
|
||||
integrationType: integration
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: temporalio[strands-agents]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: "traceai"
|
||||
description: "OpenTelemetry instrumentation for the Strands Agents TypeScript SDK, part of the Future AGI traceAI observability framework."
|
||||
integrationType: plugin
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
typescript:
|
||||
package: "@traceai/strands"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: utcp
|
||||
description: Universal Tool Calling Protocol adapter enabling tool discovery and invocation across HTTP, WebSocket, and other transports without wrapper servers.
|
||||
integrationType: tool
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: strands-utcp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: valyu-agentcore
|
||||
description: Valyu search tools for AWS Bedrock AgentCore providing web, financial, academic, biomedical, patent, and regulatory data search.
|
||||
integrationType: tool
|
||||
maintainedBy: partner
|
||||
languages:
|
||||
python:
|
||||
package: valyu-agentcore
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Vercel AI SDK
|
||||
description: Run Strands agents on any Vercel AI SDK provider through the VercelModel adapter, using @ai-sdk packages.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
typescript: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-ts/src/models/vercel.ts
|
||||
docsPage: docs/user-guide/concepts/model-providers/vercel
|
||||
addedDate: 2025-12-01
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Writer
|
||||
description: Run Strands agents on Writer Palmyra models, with tool calling, structured output, and enterprise tasks.
|
||||
integrationType: model-provider
|
||||
maintainedBy: strands
|
||||
languages:
|
||||
python: {}
|
||||
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/writer.py
|
||||
docsPage: docs/user-guide/concepts/model-providers/writer
|
||||
addedDate: 2025-12-01
|
||||
@@ -59,6 +59,7 @@ addedDate: 2026-07-31 # the date you open the PR
|
||||
| `addedDate` | ✅ | Date of submission (drives the "New" badge) |
|
||||
| `docsUrl` | Recommended | Link to your own docs page about the Strands integration — your card's primary link. Must start with `https://`. |
|
||||
| `docsPage` | Legacy | On-site docs page reference — existing entries only; new submissions link their own docs via `docsUrl` |
|
||||
| `maintainedBy` | Optional | Who stands behind the integration, shown and filtered on the catalog. Defaults to `community` (maintained by an individual developer). Set `partner` only if you represent the company behind the integration and the entry's `github` org is that company's official org — a maintainer verifies this before merging. `strands` and `aws` are granted by the Strands team. |
|
||||
|
||||
The maintainer shown on your card and the PyPI/npm links are derived automatically — the
|
||||
maintainer from your `github` URL's owner, the registry links from your `package` names.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
---
|
||||
/**
|
||||
* Community integration catalog: searchable, filterable grid of community
|
||||
* packages. Entries come from the `catalog` content collection; popularity
|
||||
* stats from the bot-maintained src/data/catalog-stats.json. Everything is
|
||||
* rendered statically; the inline script only toggles card visibility.
|
||||
* Integration catalog: searchable, filterable grid of SDK built-ins and
|
||||
* community packages. Entries come from the `catalog` content collection;
|
||||
* popularity stats from the bot-maintained src/data/catalog-stats.json.
|
||||
* Everything is rendered statically; the inline script only toggles card
|
||||
* visibility.
|
||||
*/
|
||||
import { getCollection } from 'astro:content'
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'
|
||||
import CatalogCard from '../components/catalog/CatalogCard.astro'
|
||||
import CatalogFilterBar from '../components/catalog/CatalogFilterBar.astro'
|
||||
import CatalogFacets from '../components/catalog/CatalogFacets.astro'
|
||||
import { CATALOG_TYPES, TYPE_ICONS } from '../components/catalog/types'
|
||||
import { CATALOG_TYPES, AWS_MARK_VIEWBOX, MAINTAINED_BY_ICONS, MAINTAINED_BY_TIERS, TYPE_ICONS } from '../components/catalog/types'
|
||||
import { toCardModel, sortEntries, type CatalogStatsFile } from '../util/catalog'
|
||||
import { pathWithBase } from '../util/links'
|
||||
import statsFile from '../data/catalog-stats.json'
|
||||
@@ -28,16 +29,22 @@ const hasVerified = cards.some((c) => c.badges.includes('verified'))
|
||||
const hasFeatured = cards.some((c) => c.featured)
|
||||
const hasNew = cards.some((c) => c.badges.includes('new'))
|
||||
|
||||
// Counts span both SDKs while filtering is per-SDK; with no evals entries the
|
||||
// two coincide, so if/when evals entries land these need client-side recompute.
|
||||
// Build-time counts are the initial paint only; apply() recomputes every
|
||||
// chip count client-side against the active filters.
|
||||
const typeFacets = CATALOG_TYPES.map(({ value, labelPlural }) => ({
|
||||
value,
|
||||
label: labelPlural,
|
||||
count: cards.filter((c) => c.integrationType === value).length,
|
||||
})).filter((t) => t.count > 0)
|
||||
|
||||
const maintainedByFacets = MAINTAINED_BY_TIERS.map(({ value, label }) => ({
|
||||
value,
|
||||
label,
|
||||
count: cards.filter((c) => c.maintainedBy === value).length,
|
||||
})).filter((t) => t.count > 0)
|
||||
|
||||
const title = 'Integrations'
|
||||
const description = 'Community-built tools, model providers, plugins, and integrations for Strands Agents.'
|
||||
const description = 'Built-in and community tools, model providers, plugins, and integrations for Strands Agents.'
|
||||
---
|
||||
|
||||
<StarlightPage frontmatter={{ title, description, template: 'splash', pagefind: false, hero: { actions: [] } }} hasSidebar={false}>
|
||||
@@ -45,7 +52,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
<div class="cat-head">
|
||||
<div>
|
||||
<h1 class="cat-title">Integrations</h1>
|
||||
<p class="cat-subtitle">Tools, model providers, and integrations built by the Strands community</p>
|
||||
<p class="cat-subtitle">Tools, model providers, and integrations, built into the SDK and by the Strands community</p>
|
||||
</div>
|
||||
<div class="cat-head-actions">
|
||||
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
|
||||
@@ -56,22 +63,6 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
<CatalogFilterBar showSdkFacet={showSdkFacet} />
|
||||
<div class="cat-body">
|
||||
<nav class="cat-rail" aria-label="Filter catalog">
|
||||
<p class="cat-rail-label" id="cat-type-label">Type</p>
|
||||
<ul class="cat-rail-list" aria-labelledby="cat-type-label">
|
||||
{
|
||||
typeFacets.map((t) => (
|
||||
<li class="cat-rail-item">
|
||||
<button class="cat-chip" type="button" data-cat-type={t.value} aria-pressed="false">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d={TYPE_ICONS[t.value]} />
|
||||
</svg>
|
||||
{t.label}
|
||||
<span class="cat-count">{t.count}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
(hasFeatured || hasNew || hasVerified) && (
|
||||
<>
|
||||
@@ -105,11 +96,54 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
</>
|
||||
)
|
||||
}
|
||||
<p class="cat-rail-label" id="cat-type-label">Type</p>
|
||||
<ul class="cat-rail-list" aria-labelledby="cat-type-label">
|
||||
{
|
||||
typeFacets.map((t) => (
|
||||
<li class="cat-rail-item">
|
||||
<button class="cat-chip" type="button" data-cat-type={t.value} aria-pressed="false">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d={TYPE_ICONS[t.value]} />
|
||||
</svg>
|
||||
{t.label}
|
||||
<span class="cat-count">{t.count}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<p class="cat-rail-label" id="cat-maintained-label">Maintained by</p>
|
||||
<ul class="cat-rail-list" aria-labelledby="cat-maintained-label">
|
||||
{
|
||||
maintainedByFacets.map((t) => (
|
||||
<li class="cat-rail-item">
|
||||
<button class="cat-chip" type="button" data-cat-maintained={t.value} aria-label={t.label} aria-pressed="false">
|
||||
{MAINTAINED_BY_ICONS[t.value] ? (
|
||||
<svg width="29" height="18" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
|
||||
<path fill="currentColor" d={MAINTAINED_BY_ICONS[t.value]} />
|
||||
</svg>
|
||||
) : (
|
||||
t.label
|
||||
)}
|
||||
<span class="cat-count">{t.count}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="cat-grid" data-cat-grid>
|
||||
{cards.map((card) => <CatalogCard card={card} />)}
|
||||
{/* Not a .cat-card: the filter script must never hide this tile. */}
|
||||
<div class="cat-cta">
|
||||
<div class="cat-empty" data-cat-empty hidden>
|
||||
<p class="cat-empty-title">No integrations match</p>
|
||||
<p class="cat-empty-body">Try different filters, or build the missing piece and list it here.</p>
|
||||
<div class="cat-empty-actions">
|
||||
<button class="cat-btn-ghost" type="button" data-cat-clear>Clear filters</button>
|
||||
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* Not a .cat-card: hidden only when the empty panel takes over. */}
|
||||
<div class="cat-cta" data-cat-cta>
|
||||
<h2 class="cat-cta-title">Add your integration</h2>
|
||||
<p class="cat-cta-body">Built a tool, model provider, or plugin for Strands? List it here.</p>
|
||||
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
|
||||
@@ -137,6 +171,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
<div class="cat-dialog-facets">
|
||||
<CatalogFacets
|
||||
types={typeFacets}
|
||||
maintainedBy={maintainedByFacets}
|
||||
showSdkFacet={showSdkFacet}
|
||||
hasVerified={hasVerified}
|
||||
hasFeatured={hasFeatured}
|
||||
@@ -150,6 +185,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
data-type={card.integrationType}
|
||||
data-langs={card.languages.join(' ')}
|
||||
data-badges={[...card.badges, ...(card.featured ? ['featured'] : [])].join(' ')}
|
||||
data-maintained={card.maintainedBy}
|
||||
data-sdk={card.sdk}
|
||||
data-search={`${card.name} ${card.description} by ${card.maintainer}`.toLowerCase()}
|
||||
>
|
||||
@@ -179,7 +215,6 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
<iframe class="cat-drawer-frame" data-cat-drawer-frame title="Integration documentation" src="about:blank"
|
||||
></iframe>
|
||||
</dialog>
|
||||
<p class="cat-empty" data-cat-empty hidden>No integrations match the current filters.</p>
|
||||
|
||||
<p class="cat-disclaimer">Community packages are maintained by their authors — review before production use.</p>
|
||||
</div>
|
||||
@@ -189,15 +224,24 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
// Client-side faceted filtering over the statically-rendered cards. The
|
||||
// matching + URL-state logic lives in util/catalog-filter (unit-tested);
|
||||
// this script only reads the DOM and toggles [hidden].
|
||||
import { matchesFilters, stateToQuery, queryToState, type CardFilterData } from '../util/catalog-filter'
|
||||
import {
|
||||
countIfSelected,
|
||||
matchesFilters,
|
||||
selectOnly,
|
||||
stateToQuery,
|
||||
queryToState,
|
||||
type CardFilterData,
|
||||
} from '../util/catalog-filter'
|
||||
|
||||
const cards = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-grid] .cat-card'))
|
||||
const searchInput = document.querySelector<HTMLInputElement>('[data-cat-search]')
|
||||
const typeBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-type]'))
|
||||
const langBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-lang]'))
|
||||
const badgeBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-badge]'))
|
||||
const maintainedBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-maintained]'))
|
||||
const sdkBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-sdk]'))
|
||||
const emptyEl = document.querySelector<HTMLElement>('[data-cat-empty]')
|
||||
const ctaEl = document.querySelector<HTMLElement>('[data-cat-cta]')
|
||||
const countEl = document.querySelector<HTMLElement>('[data-cat-count]')
|
||||
const dialog = document.querySelector<HTMLDialogElement>('[data-cat-dialog]')
|
||||
const dialogItems = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-dialog-list] .cat-dialog-item'))
|
||||
@@ -214,6 +258,8 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
for (const badge of state.badges) if (!renderedBadges.has(badge)) state.badges.delete(badge)
|
||||
const renderedTypes = new Set(typeBtns.map((btn) => btn.dataset.catType))
|
||||
for (const t of state.types) if (!renderedTypes.has(t)) state.types.delete(t)
|
||||
const renderedMaintained = new Set(maintainedBtns.map((btn) => btn.dataset.catMaintained))
|
||||
for (const t of state.maintainedBy) if (!renderedMaintained.has(t)) state.maintainedBy.delete(t)
|
||||
|
||||
function cardData(el: HTMLElement): CardFilterData {
|
||||
return {
|
||||
@@ -221,10 +267,22 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
type: el.dataset.type || '',
|
||||
languages: (el.dataset.langs || '').split(' ').filter(Boolean),
|
||||
badges: (el.dataset.badges || '').split(' ').filter(Boolean),
|
||||
maintainedBy: el.dataset.maintained || 'community',
|
||||
sdk: el.dataset.sdk || 'agents',
|
||||
}
|
||||
}
|
||||
|
||||
// Facet data never changes after render; cache it for the count updates.
|
||||
const facetData = cards.map(cardData)
|
||||
|
||||
function updateChipCount(btn: HTMLElement, facet: 'types' | 'badges' | 'maintainedBy', value: string) {
|
||||
const el = btn.querySelector('.cat-count')
|
||||
if (!el) return
|
||||
const count = countIfSelected(facetData, state, facet, value)
|
||||
el.textContent = String(count)
|
||||
btn.classList.toggle('cat-chip--zero', count === 0)
|
||||
}
|
||||
|
||||
function apply() {
|
||||
let visible = 0
|
||||
for (const el of cards) {
|
||||
@@ -233,6 +291,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
if (show) visible++
|
||||
}
|
||||
if (emptyEl) emptyEl.hidden = visible > 0
|
||||
if (ctaEl) ctaEl.hidden = visible === 0
|
||||
if (countEl) countEl.textContent = String(visible)
|
||||
if (searchLabel) searchLabel.textContent = state.search.trim() || 'Search integrations…'
|
||||
if (searchOpenBtn) searchOpenBtn.classList.toggle('cat-search-trigger--active', Boolean(state.search.trim()))
|
||||
@@ -250,6 +309,12 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
for (const btn of typeBtns) btn.setAttribute('aria-pressed', String(state.types.has(btn.dataset.catType || '')))
|
||||
for (const btn of langBtns) btn.setAttribute('aria-pressed', String(state.languages.has(btn.dataset.catLang || '')))
|
||||
for (const btn of badgeBtns) btn.setAttribute('aria-pressed', String(state.badges.has(btn.dataset.catBadge || '')))
|
||||
for (const btn of maintainedBtns)
|
||||
btn.setAttribute('aria-pressed', String(state.maintainedBy.has(btn.dataset.catMaintained || '')))
|
||||
|
||||
for (const btn of typeBtns) updateChipCount(btn, 'types', btn.dataset.catType || '')
|
||||
for (const btn of badgeBtns) updateChipCount(btn, 'badges', btn.dataset.catBadge || '')
|
||||
for (const btn of maintainedBtns) updateChipCount(btn, 'maintainedBy', btn.dataset.catMaintained || '')
|
||||
for (const btn of sdkBtns) btn.setAttribute('aria-checked', String(state.sdk === btn.dataset.catSdk))
|
||||
|
||||
// `entry` (the open docs drawer) is not filter state; carry the current
|
||||
@@ -302,7 +367,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
})
|
||||
for (const btn of typeBtns)
|
||||
btn.addEventListener('click', () => {
|
||||
toggle(state.types, btn.dataset.catType || '')
|
||||
selectOnly(state.types, btn.dataset.catType || '')
|
||||
apply()
|
||||
})
|
||||
for (const btn of langBtns)
|
||||
@@ -310,11 +375,29 @@ const description = 'Community-built tools, model providers, plugins, and integr
|
||||
toggle(state.languages, btn.dataset.catLang || '')
|
||||
apply()
|
||||
})
|
||||
// Highlights are cross-cutting flags (an entry can be Featured and New), so
|
||||
// they multi-select; Type and Maintained-by hold one value per entry, so
|
||||
// selecting within them replaces the previous choice.
|
||||
for (const btn of badgeBtns)
|
||||
btn.addEventListener('click', () => {
|
||||
toggle(state.badges, btn.dataset.catBadge || '')
|
||||
apply()
|
||||
})
|
||||
for (const btn of maintainedBtns)
|
||||
btn.addEventListener('click', () => {
|
||||
selectOnly(state.maintainedBy, btn.dataset.catMaintained || '')
|
||||
apply()
|
||||
})
|
||||
document.querySelector('[data-cat-clear]')?.addEventListener('click', () => {
|
||||
state.search = ''
|
||||
if (searchInput) searchInput.value = ''
|
||||
state.types.clear()
|
||||
state.languages.clear()
|
||||
state.badges.clear()
|
||||
state.maintainedBy.clear()
|
||||
state.sdk = 'agents'
|
||||
apply()
|
||||
})
|
||||
for (const btn of sdkBtns)
|
||||
btn.addEventListener('click', () => {
|
||||
state.sdk = btn.dataset.catSdk || 'agents'
|
||||
|
||||
+153
-22
@@ -108,9 +108,9 @@
|
||||
margin: 0 0 0.25rem;
|
||||
padding-left: 0.65rem;
|
||||
}
|
||||
/* Space between rail sections (Type, Highlights). */
|
||||
/* Space between rail sections (Highlights, Type, Maintained by). */
|
||||
.cat-rail-list + .cat-rail-label {
|
||||
margin-top: 1.1rem;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
.cat-rail-list {
|
||||
list-style: none;
|
||||
@@ -118,7 +118,7 @@
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.cat-rail-item {
|
||||
margin: 0;
|
||||
@@ -127,11 +127,18 @@
|
||||
.cat-rail .cat-chip {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-top: 0.22rem;
|
||||
padding-bottom: 0.22rem;
|
||||
/* Starlight's content styles give buttons a bottom margin the :where reset
|
||||
(margin-top only) doesn't touch; without this the rail rows drift apart. */
|
||||
margin: 0;
|
||||
padding-top: 0.14rem;
|
||||
padding-bottom: 0.14rem;
|
||||
}
|
||||
.cat-rail .cat-count {
|
||||
margin-left: auto;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
/* A chip whose selection would match nothing stays clickable but reads muted. */
|
||||
.cat-chip--zero {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.cat-grid {
|
||||
@@ -151,13 +158,16 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 0.85rem;
|
||||
align-items: flex-start;
|
||||
/* Stretch so the content column fills the uniform card height and the
|
||||
footer's margin-top auto can push it to the bottom edge. */
|
||||
align-items: stretch;
|
||||
/* Grid items default to min-width auto; without this a card's nowrap
|
||||
footer would widen its track past the container on narrow screens. */
|
||||
min-width: 0;
|
||||
border: 1px solid var(--sl-color-hairline, var(--sl-color-gray-5));
|
||||
border-radius: var(--cat-radius-card);
|
||||
padding: 1.4rem;
|
||||
/* Lighter bottom padding so the footer line hugs the card's lower edge. */
|
||||
padding: 1.4rem 1.4rem 0.95rem;
|
||||
background: var(--sl-color-bg-nav);
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
@@ -169,12 +179,77 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The CTA tile sets its own display, which outranks the [hidden] UA style
|
||||
(same trap as .cat-card[hidden]). */
|
||||
.cat-cta[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Zero-results panel: spans the grid, offers a reset and the submission CTA. */
|
||||
.cat-empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
border: 1px dashed color-mix(in srgb, var(--sl-color-gray-4) 60%, transparent);
|
||||
border-radius: var(--cat-radius-card);
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
.cat-empty[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.cat-empty-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
color: var(--sl-color-white);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.cat-empty-body {
|
||||
color: var(--sl-color-gray-3);
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.cat-empty-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cat-btn-ghost {
|
||||
font-size: 0.92rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border: 1px solid var(--sl-color-gray-4);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--sl-color-gray-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.cat-btn-ghost:hover {
|
||||
border-color: var(--sl-color-gray-3);
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
|
||||
/* Featured entries live in the same grid (sorted first) and are marked by a
|
||||
Featured chip in the card head plus a light accent border tint. */
|
||||
.cat-card--featured {
|
||||
border-color: color-mix(in srgb, var(--sl-color-accent) 25%, var(--sl-color-gray-5));
|
||||
}
|
||||
|
||||
/* Built-in (SDK) cards carry the badge's note-blue on the border and type
|
||||
tile so first-party entries scan apart from the rest of the catalog. */
|
||||
.cat-card--builtin {
|
||||
border-color: color-mix(in srgb, var(--sl-badge-note-border) 45%, var(--sl-color-gray-5));
|
||||
}
|
||||
.cat-card--builtin:hover {
|
||||
border-color: color-mix(in srgb, var(--sl-badge-note-border) 75%, var(--sl-color-gray-5));
|
||||
background: color-mix(in srgb, var(--sl-badge-note-bg) 12%, var(--sl-color-bg-nav));
|
||||
}
|
||||
.cat-card--builtin .cat-tile {
|
||||
color: var(--sl-badge-note-text);
|
||||
background: color-mix(in srgb, var(--sl-badge-note-bg) 30%, transparent);
|
||||
border-color: color-mix(in srgb, var(--sl-badge-note-border) 35%, transparent);
|
||||
}
|
||||
.cat-card--builtin:hover .cat-card-title {
|
||||
color: var(--sl-badge-note-text);
|
||||
}
|
||||
|
||||
/* Whole-card click: the title link stretches over the card; secondary links
|
||||
re-raise themselves above it. */
|
||||
.cat-card-title::after {
|
||||
@@ -236,17 +311,24 @@
|
||||
.cat-card-stats {
|
||||
display: inline-flex;
|
||||
gap: 0.55rem;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
font-size: var(--cat-text-meta);
|
||||
color: var(--sl-color-gray-3);
|
||||
}
|
||||
.cat-card-flags {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cat-card:hover .cat-card-title {
|
||||
color: var(--cat-accent-text);
|
||||
}
|
||||
.cat-external {
|
||||
font-size: 0.7rem;
|
||||
color: var(--sl-color-gray-3);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Card flags mirror Starlight's sidebar .sl-badge treatment (see Badge.astro):
|
||||
@@ -264,28 +346,77 @@
|
||||
border: 1px solid;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
/* Variant colors follow the docs sidebar badges but lightened: outline-only
|
||||
(no fill) so the flags read as quiet metadata rather than solid chips. */
|
||||
.cat-flag--new {
|
||||
color: var(--sl-badge-tip-text);
|
||||
background: color-mix(in srgb, var(--sl-badge-tip-bg) 35%, transparent);
|
||||
border-color: color-mix(in srgb, var(--sl-badge-tip-border) 55%, transparent);
|
||||
}
|
||||
/* Featured/Verified are maintainer-granted: brand-green outline keeps them
|
||||
distinct from the informational New without a solid fill. */
|
||||
.cat-flag--verified,
|
||||
.cat-flag--featured {
|
||||
/* Verified is maintainer-granted: brand-green outline, no solid fill. */
|
||||
.cat-flag--verified {
|
||||
color: var(--cat-accent-text);
|
||||
background: color-mix(in srgb, var(--sl-color-accent) 7%, transparent);
|
||||
border-color: color-mix(in srgb, var(--sl-color-accent) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Featured/New render as tabs seated on the card's top-right border rather
|
||||
than chips in the title row, keeping the head to name + tier. The fill
|
||||
matches the featured border tint so tab and border read as one shape. */
|
||||
.cat-corner {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 14px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
z-index: 1;
|
||||
/* Sits above the stretched title link; without this the tabs would be a
|
||||
click-dead zone on the card. */
|
||||
pointer-events: none;
|
||||
}
|
||||
.cat-tab {
|
||||
font-family: var(--sl-font-system-mono);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
padding: 0.22rem 0.5rem;
|
||||
border-radius: 0 0 5px 5px;
|
||||
color: var(--sl-color-black);
|
||||
}
|
||||
.cat-tab--featured {
|
||||
background: var(--sl-color-accent);
|
||||
}
|
||||
.cat-tab--new {
|
||||
background: var(--sl-badge-tip-border);
|
||||
}
|
||||
/* Maintainer-tier flags. Built-in (Strands) is note-blue so it reads apart
|
||||
from the maintainer-granted green (Verified/Featured) and the informational
|
||||
New; AWS is caution-orange; Partner stays a neutral outline. Community
|
||||
renders no flag: it is the default tier. */
|
||||
.cat-flag--strands {
|
||||
color: var(--sl-badge-note-text);
|
||||
background: color-mix(in srgb, var(--sl-badge-note-bg) 35%, transparent);
|
||||
border-color: color-mix(in srgb, var(--sl-badge-note-border) 55%, transparent);
|
||||
}
|
||||
/* Inline svg baseline-aligns and rides high; block lets the flag's flex
|
||||
centering align marks with the neighbor chips' text. */
|
||||
.cat-flag svg {
|
||||
display: block;
|
||||
}
|
||||
/* The AWS flag is the bare mark, no chip box; --sl-color-white flips with
|
||||
the theme so the logo stays full-contrast in light and dark. */
|
||||
.cat-flag--aws {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
.cat-flag--partner {
|
||||
color: var(--sl-color-gray-2);
|
||||
background: color-mix(in srgb, var(--sl-color-gray-4) 20%, transparent);
|
||||
border-color: color-mix(in srgb, var(--sl-color-gray-4) 70%, transparent);
|
||||
}
|
||||
|
||||
.cat-card-desc {
|
||||
color: var(--sl-color-gray-2);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
/* Uniform card heights: every description occupies exactly two lines —
|
||||
longer text clamps, shorter text still reserves the space. */
|
||||
display: -webkit-box;
|
||||
@@ -298,9 +429,9 @@
|
||||
.cat-card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
justify-content: space-between;
|
||||
gap: 0.7rem;
|
||||
margin-top: 0.15rem;
|
||||
font-size: var(--cat-text-meta);
|
||||
color: var(--sl-color-gray-3);
|
||||
min-width: 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface CatalogFilterState {
|
||||
types: Set<string>
|
||||
languages: Set<string>
|
||||
badges: Set<string>
|
||||
maintainedBy: Set<string>
|
||||
sdk: string
|
||||
}
|
||||
|
||||
@@ -18,12 +19,14 @@ export interface CardFilterData {
|
||||
type: string
|
||||
languages: string[]
|
||||
badges: string[]
|
||||
maintainedBy: string
|
||||
sdk: string
|
||||
}
|
||||
|
||||
const KNOWN_TYPES = new Set<string>(CATALOG_TYPES.map((t) => t.value))
|
||||
const KNOWN_LANGUAGES = new Set(['python', 'typescript'])
|
||||
const KNOWN_BADGES = new Set(['verified', 'featured', 'new'])
|
||||
export const KNOWN_MAINTAINED_BY = new Set(['strands', 'aws', 'partner', 'community'])
|
||||
const KNOWN_SDKS = new Set(['agents', 'evals'])
|
||||
const DEFAULT_SDK = 'agents'
|
||||
|
||||
@@ -35,9 +38,39 @@ export function matchesFilters(card: CardFilterData, state: CatalogFilterState):
|
||||
if (state.types.size > 0 && !state.types.has(card.type)) return false
|
||||
if (state.languages.size > 0 && !card.languages.some((l) => state.languages.has(l))) return false
|
||||
if (state.badges.size > 0 && !card.badges.some((b) => state.badges.has(b))) return false
|
||||
if (state.maintainedBy.size > 0 && !state.maintainedBy.has(card.maintainedBy)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards a facet value would match if it were the sole selection in its facet,
|
||||
* holding every other facet at the current state. Drives the live chip counts
|
||||
* so no chip invites a zero-result click.
|
||||
*/
|
||||
export function countIfSelected(
|
||||
cards: CardFilterData[],
|
||||
state: CatalogFilterState,
|
||||
facet: 'types' | 'languages' | 'badges' | 'maintainedBy',
|
||||
value: string
|
||||
): number {
|
||||
const probe: CatalogFilterState = {
|
||||
...state,
|
||||
types: new Set(state.types),
|
||||
languages: new Set(state.languages),
|
||||
badges: new Set(state.badges),
|
||||
maintainedBy: new Set(state.maintainedBy),
|
||||
}
|
||||
probe[facet] = new Set([value])
|
||||
return cards.filter((card) => matchesFilters(card, probe)).length
|
||||
}
|
||||
|
||||
/** Exclusive facet selection: picking a value replaces the set; re-picking clears it. */
|
||||
export function selectOnly(set: Set<string>, value: string): void {
|
||||
const had = set.has(value)
|
||||
set.clear()
|
||||
if (!had) set.add(value)
|
||||
}
|
||||
|
||||
/** Serialize non-default state to a query string ('' when everything is default). */
|
||||
export function stateToQuery(state: CatalogFilterState): string {
|
||||
const params = new URLSearchParams()
|
||||
@@ -45,6 +78,7 @@ export function stateToQuery(state: CatalogFilterState): string {
|
||||
if (state.types.size > 0) params.set('type', [...state.types].sort().join(','))
|
||||
if (state.languages.size > 0) params.set('lang', [...state.languages].sort().join(','))
|
||||
if (state.badges.size > 0) params.set('badge', [...state.badges].sort().join(','))
|
||||
if (state.maintainedBy.size > 0) params.set('by', [...state.maintainedBy].sort().join(','))
|
||||
if (state.sdk !== DEFAULT_SDK) params.set('sdk', state.sdk)
|
||||
return params.toString()
|
||||
}
|
||||
@@ -60,6 +94,7 @@ export function queryToState(query: string): CatalogFilterState {
|
||||
types: pick('type', KNOWN_TYPES),
|
||||
languages: pick('lang', KNOWN_LANGUAGES),
|
||||
badges: pick('badge', KNOWN_BADGES),
|
||||
maintainedBy: pick('by', KNOWN_MAINTAINED_BY),
|
||||
sdk: KNOWN_SDKS.has(sdkParam) ? sdkParam : DEFAULT_SDK,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface CatalogCardModel {
|
||||
name: string
|
||||
description: string
|
||||
integrationType: CatalogEntryData['integrationType']
|
||||
maintainedBy: CatalogEntryData['maintainedBy']
|
||||
sdk: 'agents' | 'evals'
|
||||
languages: ('python' | 'typescript')[]
|
||||
href: string
|
||||
@@ -81,6 +82,7 @@ export function toCardModel(
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
integrationType: data.integrationType,
|
||||
maintainedBy: data.maintainedBy,
|
||||
sdk: data.sdk,
|
||||
languages,
|
||||
href: docsHref ?? data.docsUrl ?? data.github,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getCollection } from 'astro:content'
|
||||
import { catalogEntrySchema } from '../src/content.config'
|
||||
@@ -69,6 +71,24 @@ describe('catalog content collection', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts every maintainer tier and defaults to community', () => {
|
||||
const base = {
|
||||
name: 'tiered-entry',
|
||||
description: 'maintainer tier probe',
|
||||
integrationType: 'model-provider',
|
||||
languages: { python: {} },
|
||||
github: 'https://github.com/strands-agents/harness-sdk',
|
||||
addedDate: '2025-12-01',
|
||||
}
|
||||
for (const maintainedBy of ['strands', 'aws', 'partner', 'community']) {
|
||||
expect(catalogEntrySchema.safeParse({ ...base, maintainedBy }).success, maintainedBy).toBe(true)
|
||||
}
|
||||
const parsed = catalogEntrySchema.safeParse(base)
|
||||
expect(parsed.success).toBe(true)
|
||||
if (parsed.success) expect(parsed.data.maintainedBy).toBe('community')
|
||||
expect(catalogEntrySchema.safeParse({ ...base, maintainedBy: 'official' }).success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an unknown language key', () => {
|
||||
// The languages container is .strict() so a misspelled key
|
||||
// (`typeScript:`) fails the build instead of silently dropping the
|
||||
@@ -144,6 +164,38 @@ describe('catalog content collection', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('loads built-in SDK entries', async () => {
|
||||
const entries = await getCollection('catalog')
|
||||
const anthropic = entries.find((e) => e.id === 'anthropic')
|
||||
expect(anthropic).toBeDefined()
|
||||
expect(anthropic!.data.maintainedBy).toBe('strands')
|
||||
expect(anthropic!.data.docsPage).toBe('docs/user-guide/concepts/model-providers/anthropic')
|
||||
// The content layer silently drops entries whose YAML fails to parse;
|
||||
// pinned counts catch that. Update them when granting or removing tiers.
|
||||
const byTier = Map.groupBy(entries, (e) => e.data.maintainedBy)
|
||||
expect(byTier.get('strands')?.length).toBe(28)
|
||||
expect(byTier.get('aws')?.length).toBe(7)
|
||||
expect(byTier.get('partner')?.length).toBe(21)
|
||||
// Built-ins point at their source path in the SDK monorepo and always
|
||||
// have on-site docs.
|
||||
for (const e of byTier.get('strands') ?? []) {
|
||||
expect(e.data.github, e.id).toMatch(/^https:\/\/github\.com\/strands-agents\/harness-sdk\//)
|
||||
expect(e.data.docsPage, e.id).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('built-in github links resolve to real monorepo paths', async () => {
|
||||
// Built-ins deep-link their source (blob/tree under main); unlike docsPage
|
||||
// nothing else validates them, so a repo restructure would rot the links
|
||||
// silently without this.
|
||||
const entries = await getCollection('catalog')
|
||||
for (const e of entries.filter((x) => x.data.maintainedBy === 'strands')) {
|
||||
const m = /^https:\/\/github\.com\/strands-agents\/harness-sdk\/(?:blob|tree)\/main\/(.+)$/.exec(e.data.github)
|
||||
expect(m, `${e.id}: ${e.data.github}`).not.toBeNull()
|
||||
expect(existsSync(join('..', m![1])), `${e.id}: ${m![1]} missing from the repo`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('every docsPage points at a real docs collection entry', async () => {
|
||||
const [entries, docs] = await Promise.all([getCollection('catalog'), getCollection('docs')])
|
||||
const docIds = new Set(docs.map((d) => d.id))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
countIfSelected,
|
||||
matchesFilters,
|
||||
selectOnly,
|
||||
stateToQuery,
|
||||
queryToState,
|
||||
type CatalogFilterState,
|
||||
@@ -8,7 +10,15 @@ import {
|
||||
} from '../src/util/catalog-filter'
|
||||
|
||||
function state(overrides: Partial<CatalogFilterState> = {}): CatalogFilterState {
|
||||
return { search: '', types: new Set(), languages: new Set(), badges: new Set(), sdk: 'agents', ...overrides }
|
||||
return {
|
||||
search: '',
|
||||
types: new Set(),
|
||||
languages: new Set(),
|
||||
badges: new Set(),
|
||||
maintainedBy: new Set(),
|
||||
sdk: 'agents',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const card: CardFilterData = {
|
||||
@@ -16,6 +26,7 @@ const card: CardFilterData = {
|
||||
type: 'tool',
|
||||
languages: ['python'],
|
||||
badges: ['verified'],
|
||||
maintainedBy: 'community',
|
||||
sdk: 'agents',
|
||||
}
|
||||
|
||||
@@ -69,6 +80,7 @@ describe('URL state round-trip', () => {
|
||||
types: new Set(['tool', 'plugin']),
|
||||
languages: new Set(['python']),
|
||||
badges: new Set(['verified']),
|
||||
maintainedBy: new Set(['partner']),
|
||||
sdk: 'evals',
|
||||
})
|
||||
const back = queryToState(stateToQuery(s))
|
||||
@@ -76,9 +88,41 @@ describe('URL state round-trip', () => {
|
||||
expect(back.types).toEqual(new Set(['tool', 'plugin']))
|
||||
expect(back.languages).toEqual(new Set(['python']))
|
||||
expect(back.badges).toEqual(new Set(['verified']))
|
||||
expect(back.maintainedBy).toEqual(new Set(['partner']))
|
||||
expect(back.sdk).toBe('evals')
|
||||
})
|
||||
|
||||
it('filters by maintainer tier', () => {
|
||||
expect(matchesFilters(card, state({ maintainedBy: new Set(['community', 'aws']) }))).toBe(true)
|
||||
expect(matchesFilters(card, state({ maintainedBy: new Set(['strands']) }))).toBe(false)
|
||||
})
|
||||
|
||||
it('parses maintainer tiers from a deep link', () => {
|
||||
expect(queryToState('by=strands,aws').maintainedBy).toEqual(new Set(['strands', 'aws']))
|
||||
expect(queryToState('by=official').maintainedBy).toEqual(new Set())
|
||||
})
|
||||
|
||||
it('countIfSelected holds other facets and swaps in the probed value', () => {
|
||||
const cards = [
|
||||
card,
|
||||
{ ...card, type: 'plugin', maintainedBy: 'aws' },
|
||||
{ ...card, type: 'tool', maintainedBy: 'aws' },
|
||||
]
|
||||
const s = state({ maintainedBy: new Set(['aws']) })
|
||||
expect(countIfSelected(cards, s, 'types', 'tool')).toBe(1)
|
||||
expect(countIfSelected(cards, s, 'types', 'session-manager')).toBe(0)
|
||||
// probing within the same facet replaces the current selection
|
||||
expect(countIfSelected(cards, s, 'maintainedBy', 'community')).toBe(1)
|
||||
})
|
||||
|
||||
it('selectOnly replaces the selection and re-picking clears it', () => {
|
||||
const set = new Set(['model-provider'])
|
||||
selectOnly(set, 'tool')
|
||||
expect(set).toEqual(new Set(['tool']))
|
||||
selectOnly(set, 'tool')
|
||||
expect(set).toEqual(new Set())
|
||||
})
|
||||
|
||||
it('drops unknown values when parsing', () => {
|
||||
const back = queryToState('type=widget&lang=rust&sdk=nope')
|
||||
expect(back.types.size).toBe(0)
|
||||
|
||||
@@ -11,6 +11,7 @@ function entry(overrides: Partial<CatalogEntryData> = {}): CatalogEntryData {
|
||||
description: 'Example integration',
|
||||
integrationType: 'tool',
|
||||
sdk: 'agents',
|
||||
maintainedBy: 'community',
|
||||
languages: { python: { package: 'strands-example' } },
|
||||
github: 'https://github.com/example/strands-example',
|
||||
featured: false,
|
||||
|
||||
Reference in New Issue
Block a user