feat: implement skills marketplace API with CRUD operations and gateway integration
This commit is contained in:
committed by
Abhimanyu Saharan
parent
db510a8612
commit
e7b5df0bce
313
backend/app/api/skills_marketplace.py
Normal file
313
backend/app/api/skills_marketplace.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""Skills marketplace API for catalog management and gateway install actions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import unquote, urlparse
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import col
|
||||
|
||||
from app.api.deps import require_org_admin
|
||||
from app.core.time import utcnow
|
||||
from app.db.session import get_session
|
||||
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.marketplace_skills import MarketplaceSkill
|
||||
from app.schemas.common import OkResponse
|
||||
from app.schemas.skills_marketplace import (
|
||||
MarketplaceSkillActionResponse,
|
||||
MarketplaceSkillCardRead,
|
||||
MarketplaceSkillCreate,
|
||||
MarketplaceSkillRead,
|
||||
)
|
||||
from app.services.openclaw.gateway_dispatch import GatewayDispatchService
|
||||
from app.services.openclaw.gateway_resolver import gateway_client_config, require_gateway_workspace_root
|
||||
from app.services.openclaw.gateway_rpc import OpenClawGatewayError
|
||||
from app.services.openclaw.shared import GatewayAgentIdentity
|
||||
from app.services.organizations import OrganizationContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/skills", tags=["skills"])
|
||||
SESSION_DEP = Depends(get_session)
|
||||
ORG_ADMIN_DEP = Depends(require_org_admin)
|
||||
GATEWAY_ID_QUERY = Query(...)
|
||||
|
||||
|
||||
def _skills_install_dir(workspace_root: str) -> str:
|
||||
normalized = workspace_root.rstrip("/\\")
|
||||
if not normalized:
|
||||
return "skills"
|
||||
return f"{normalized}/skills"
|
||||
|
||||
|
||||
def _infer_skill_name(source_url: str) -> str:
|
||||
parsed = urlparse(source_url)
|
||||
path = parsed.path.rstrip("/")
|
||||
candidate = path.rsplit("/", maxsplit=1)[-1] if path else parsed.netloc
|
||||
candidate = unquote(candidate).removesuffix(".git").replace("-", " ").replace("_", " ")
|
||||
if candidate.strip():
|
||||
return candidate.strip()
|
||||
return "Skill"
|
||||
|
||||
|
||||
def _install_instruction(*, skill: MarketplaceSkill, gateway: Gateway) -> str:
|
||||
install_dir = _skills_install_dir(gateway.workspace_root)
|
||||
return (
|
||||
"MISSION CONTROL SKILL INSTALL REQUEST\n"
|
||||
f"Skill name: {skill.name}\n"
|
||||
f"Skill source URL: {skill.source_url}\n"
|
||||
f"Install destination: {install_dir}\n\n"
|
||||
"Actions:\n"
|
||||
"1. Ensure the install destination exists.\n"
|
||||
"2. Install or update the skill from the source URL into the destination.\n"
|
||||
"3. Verify the skill is discoverable by the runtime.\n"
|
||||
"4. Reply with success or failure details."
|
||||
)
|
||||
|
||||
|
||||
def _uninstall_instruction(*, skill: MarketplaceSkill, gateway: Gateway) -> str:
|
||||
install_dir = _skills_install_dir(gateway.workspace_root)
|
||||
return (
|
||||
"MISSION CONTROL SKILL UNINSTALL REQUEST\n"
|
||||
f"Skill name: {skill.name}\n"
|
||||
f"Skill source URL: {skill.source_url}\n"
|
||||
f"Install destination: {install_dir}\n\n"
|
||||
"Actions:\n"
|
||||
"1. Remove the skill assets previously installed from this source URL.\n"
|
||||
"2. Ensure the skill is no longer discoverable by the runtime.\n"
|
||||
"3. Reply with success or failure details."
|
||||
)
|
||||
|
||||
|
||||
def _as_card(
|
||||
*,
|
||||
skill: MarketplaceSkill,
|
||||
installation: GatewayInstalledSkill | None,
|
||||
) -> MarketplaceSkillCardRead:
|
||||
return MarketplaceSkillCardRead(
|
||||
id=skill.id,
|
||||
organization_id=skill.organization_id,
|
||||
name=skill.name,
|
||||
description=skill.description,
|
||||
source_url=skill.source_url,
|
||||
created_at=skill.created_at,
|
||||
updated_at=skill.updated_at,
|
||||
installed=installation is not None,
|
||||
installed_at=installation.created_at if installation is not None else None,
|
||||
)
|
||||
|
||||
|
||||
async def _require_gateway_for_org(
|
||||
*,
|
||||
gateway_id: UUID,
|
||||
session: AsyncSession,
|
||||
ctx: OrganizationContext,
|
||||
) -> Gateway:
|
||||
gateway = await Gateway.objects.by_id(gateway_id).first(session)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found",
|
||||
)
|
||||
return gateway
|
||||
|
||||
|
||||
async def _require_marketplace_skill_for_org(
|
||||
*,
|
||||
skill_id: UUID,
|
||||
session: AsyncSession,
|
||||
ctx: OrganizationContext,
|
||||
) -> MarketplaceSkill:
|
||||
skill = await MarketplaceSkill.objects.by_id(skill_id).first(session)
|
||||
if skill is None or skill.organization_id != ctx.organization.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Marketplace skill not found",
|
||||
)
|
||||
return skill
|
||||
|
||||
|
||||
async def _dispatch_gateway_instruction(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
gateway: Gateway,
|
||||
message: str,
|
||||
) -> None:
|
||||
dispatch = GatewayDispatchService(session)
|
||||
config = gateway_client_config(gateway)
|
||||
session_key = GatewayAgentIdentity.session_key(gateway)
|
||||
await dispatch.send_agent_message(
|
||||
session_key=session_key,
|
||||
config=config,
|
||||
agent_name="Gateway Agent",
|
||||
message=message,
|
||||
deliver=True,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/marketplace", response_model=list[MarketplaceSkillCardRead])
|
||||
async def list_marketplace_skills(
|
||||
gateway_id: UUID = GATEWAY_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> list[MarketplaceSkillCardRead]:
|
||||
"""List marketplace cards for an org and annotate install state for a gateway."""
|
||||
gateway = await _require_gateway_for_org(gateway_id=gateway_id, session=session, ctx=ctx)
|
||||
skills = (
|
||||
await MarketplaceSkill.objects.filter_by(organization_id=ctx.organization.id)
|
||||
.order_by(col(MarketplaceSkill.created_at).desc())
|
||||
.all(session)
|
||||
)
|
||||
installations = await GatewayInstalledSkill.objects.filter_by(gateway_id=gateway.id).all(session)
|
||||
installed_by_skill_id = {record.skill_id: record for record in installations}
|
||||
return [
|
||||
_as_card(skill=skill, installation=installed_by_skill_id.get(skill.id))
|
||||
for skill in skills
|
||||
]
|
||||
|
||||
|
||||
@router.post("/marketplace", response_model=MarketplaceSkillRead)
|
||||
async def create_marketplace_skill(
|
||||
payload: MarketplaceSkillCreate,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> MarketplaceSkill:
|
||||
"""Register a skill source URL in the organization's marketplace catalog."""
|
||||
source_url = str(payload.source_url).strip()
|
||||
existing = await MarketplaceSkill.objects.filter_by(
|
||||
organization_id=ctx.organization.id,
|
||||
source_url=source_url,
|
||||
).first(session)
|
||||
if existing is not None:
|
||||
changed = False
|
||||
if payload.name and existing.name != payload.name:
|
||||
existing.name = payload.name
|
||||
changed = True
|
||||
if payload.description is not None and existing.description != payload.description:
|
||||
existing.description = payload.description
|
||||
changed = True
|
||||
if changed:
|
||||
existing.updated_at = utcnow()
|
||||
session.add(existing)
|
||||
await session.commit()
|
||||
await session.refresh(existing)
|
||||
return existing
|
||||
|
||||
skill = MarketplaceSkill(
|
||||
organization_id=ctx.organization.id,
|
||||
source_url=source_url,
|
||||
name=payload.name or _infer_skill_name(source_url),
|
||||
description=payload.description,
|
||||
)
|
||||
session.add(skill)
|
||||
await session.commit()
|
||||
await session.refresh(skill)
|
||||
return skill
|
||||
|
||||
|
||||
@router.delete("/marketplace/{skill_id}", response_model=OkResponse)
|
||||
async def delete_marketplace_skill(
|
||||
skill_id: UUID,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> OkResponse:
|
||||
"""Delete a marketplace catalog entry and any install records that reference it."""
|
||||
skill = await _require_marketplace_skill_for_org(skill_id=skill_id, session=session, ctx=ctx)
|
||||
installations = await GatewayInstalledSkill.objects.filter_by(skill_id=skill.id).all(session)
|
||||
for installation in installations:
|
||||
await session.delete(installation)
|
||||
await session.delete(skill)
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/marketplace/{skill_id}/install",
|
||||
response_model=MarketplaceSkillActionResponse,
|
||||
)
|
||||
async def install_marketplace_skill(
|
||||
skill_id: UUID,
|
||||
gateway_id: UUID = GATEWAY_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> MarketplaceSkillActionResponse:
|
||||
"""Install a marketplace skill by dispatching instructions to the gateway agent."""
|
||||
gateway = await _require_gateway_for_org(gateway_id=gateway_id, session=session, ctx=ctx)
|
||||
require_gateway_workspace_root(gateway)
|
||||
skill = await _require_marketplace_skill_for_org(skill_id=skill_id, session=session, ctx=ctx)
|
||||
try:
|
||||
await _dispatch_gateway_instruction(
|
||||
session=session,
|
||||
gateway=gateway,
|
||||
message=_install_instruction(skill=skill, gateway=gateway),
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
installation = await GatewayInstalledSkill.objects.filter_by(
|
||||
gateway_id=gateway.id,
|
||||
skill_id=skill.id,
|
||||
).first(session)
|
||||
if installation is None:
|
||||
session.add(
|
||||
GatewayInstalledSkill(
|
||||
gateway_id=gateway.id,
|
||||
skill_id=skill.id,
|
||||
),
|
||||
)
|
||||
else:
|
||||
installation.updated_at = utcnow()
|
||||
session.add(installation)
|
||||
await session.commit()
|
||||
return MarketplaceSkillActionResponse(
|
||||
skill_id=skill.id,
|
||||
gateway_id=gateway.id,
|
||||
installed=True,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/marketplace/{skill_id}/uninstall",
|
||||
response_model=MarketplaceSkillActionResponse,
|
||||
)
|
||||
async def uninstall_marketplace_skill(
|
||||
skill_id: UUID,
|
||||
gateway_id: UUID = GATEWAY_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> MarketplaceSkillActionResponse:
|
||||
"""Uninstall a marketplace skill by dispatching instructions to the gateway agent."""
|
||||
gateway = await _require_gateway_for_org(gateway_id=gateway_id, session=session, ctx=ctx)
|
||||
require_gateway_workspace_root(gateway)
|
||||
skill = await _require_marketplace_skill_for_org(skill_id=skill_id, session=session, ctx=ctx)
|
||||
try:
|
||||
await _dispatch_gateway_instruction(
|
||||
session=session,
|
||||
gateway=gateway,
|
||||
message=_uninstall_instruction(skill=skill, gateway=gateway),
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
installation = await GatewayInstalledSkill.objects.filter_by(
|
||||
gateway_id=gateway.id,
|
||||
skill_id=skill.id,
|
||||
).first(session)
|
||||
if installation is not None:
|
||||
await session.delete(installation)
|
||||
await session.commit()
|
||||
return MarketplaceSkillActionResponse(
|
||||
skill_id=skill.id,
|
||||
gateway_id=gateway.id,
|
||||
installed=False,
|
||||
)
|
||||
@@ -25,6 +25,7 @@ from app.api.gateways import router as gateways_router
|
||||
from app.api.metrics import router as metrics_router
|
||||
from app.api.organizations import router as organizations_router
|
||||
from app.api.souls_directory import router as souls_directory_router
|
||||
from app.api.skills_marketplace import router as skills_marketplace_router
|
||||
from app.api.tags import router as tags_router
|
||||
from app.api.task_custom_fields import router as task_custom_fields_router
|
||||
from app.api.tasks import router as tasks_router
|
||||
@@ -138,6 +139,7 @@ api_v1.include_router(gateways_router)
|
||||
api_v1.include_router(metrics_router)
|
||||
api_v1.include_router(organizations_router)
|
||||
api_v1.include_router(souls_directory_router)
|
||||
api_v1.include_router(skills_marketplace_router)
|
||||
api_v1.include_router(board_groups_router)
|
||||
api_v1.include_router(board_group_memory_router)
|
||||
api_v1.include_router(boards_router)
|
||||
|
||||
@@ -12,6 +12,8 @@ from app.models.board_webhook_payloads import BoardWebhookPayload
|
||||
from app.models.board_webhooks import BoardWebhook
|
||||
from app.models.boards import Board
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||
from app.models.marketplace_skills import MarketplaceSkill
|
||||
from app.models.organization_board_access import OrganizationBoardAccess
|
||||
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
|
||||
from app.models.organization_invites import OrganizationInvite
|
||||
@@ -42,6 +44,8 @@ __all__ = [
|
||||
"BoardGroup",
|
||||
"Board",
|
||||
"Gateway",
|
||||
"GatewayInstalledSkill",
|
||||
"MarketplaceSkill",
|
||||
"Organization",
|
||||
"BoardTaskCustomField",
|
||||
"TaskCustomFieldDefinition",
|
||||
|
||||
33
backend/app/models/gateway_installed_skills.py
Normal file
33
backend/app/models/gateway_installed_skills.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Gateway-to-skill installation state records."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlmodel import Field
|
||||
|
||||
from app.core.time import utcnow
|
||||
from app.models.base import QueryModel
|
||||
|
||||
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||
|
||||
|
||||
class GatewayInstalledSkill(QueryModel, table=True):
|
||||
"""Marks that a marketplace skill is installed for a specific gateway."""
|
||||
|
||||
__tablename__ = "gateway_installed_skills" # pyright: ignore[reportAssignmentType]
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"gateway_id",
|
||||
"skill_id",
|
||||
name="uq_gateway_installed_skills_gateway_id_skill_id",
|
||||
),
|
||||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
gateway_id: UUID = Field(foreign_key="gateways.id", index=True)
|
||||
skill_id: UUID = Field(foreign_key="marketplace_skills.id", index=True)
|
||||
created_at: datetime = Field(default_factory=utcnow)
|
||||
updated_at: datetime = Field(default_factory=utcnow)
|
||||
35
backend/app/models/marketplace_skills.py
Normal file
35
backend/app/models/marketplace_skills.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Organization-scoped skill catalog entries for the skills marketplace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlmodel import Field
|
||||
|
||||
from app.core.time import utcnow
|
||||
from app.models.tenancy import TenantScoped
|
||||
|
||||
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||
|
||||
|
||||
class MarketplaceSkill(TenantScoped, table=True):
|
||||
"""A marketplace skill entry that can be installed onto one or more gateways."""
|
||||
|
||||
__tablename__ = "marketplace_skills" # pyright: ignore[reportAssignmentType]
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"organization_id",
|
||||
"source_url",
|
||||
name="uq_marketplace_skills_org_source_url",
|
||||
),
|
||||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
organization_id: UUID = Field(foreign_key="organizations.id", index=True)
|
||||
name: str
|
||||
description: str | None = Field(default=None)
|
||||
source_url: str
|
||||
created_at: datetime = Field(default_factory=utcnow)
|
||||
updated_at: datetime = Field(default_factory=utcnow)
|
||||
@@ -33,6 +33,12 @@ from app.schemas.organizations import (
|
||||
OrganizationMemberUpdate,
|
||||
OrganizationRead,
|
||||
)
|
||||
from app.schemas.skills_marketplace import (
|
||||
MarketplaceSkillActionResponse,
|
||||
MarketplaceSkillCardRead,
|
||||
MarketplaceSkillCreate,
|
||||
MarketplaceSkillRead,
|
||||
)
|
||||
from app.schemas.souls_directory import (
|
||||
SoulsDirectoryMarkdownResponse,
|
||||
SoulsDirectorySearchResponse,
|
||||
@@ -83,6 +89,10 @@ __all__ = [
|
||||
"SoulsDirectoryMarkdownResponse",
|
||||
"SoulsDirectorySearchResponse",
|
||||
"SoulsDirectorySoulRef",
|
||||
"MarketplaceSkillActionResponse",
|
||||
"MarketplaceSkillCardRead",
|
||||
"MarketplaceSkillCreate",
|
||||
"MarketplaceSkillRead",
|
||||
"TagCreate",
|
||||
"TagRead",
|
||||
"TagRef",
|
||||
|
||||
49
backend/app/schemas/skills_marketplace.py
Normal file
49
backend/app/schemas/skills_marketplace.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Schemas for skills marketplace listing and install/uninstall actions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import AnyHttpUrl
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.schemas.common import NonEmptyStr
|
||||
|
||||
RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr)
|
||||
|
||||
|
||||
class MarketplaceSkillCreate(SQLModel):
|
||||
"""Payload used to register a skill URL in the organization marketplace."""
|
||||
|
||||
source_url: AnyHttpUrl
|
||||
name: NonEmptyStr | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class MarketplaceSkillRead(SQLModel):
|
||||
"""Serialized marketplace skill catalog record."""
|
||||
|
||||
id: UUID
|
||||
organization_id: UUID
|
||||
name: str
|
||||
description: str | None = None
|
||||
source_url: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class MarketplaceSkillCardRead(MarketplaceSkillRead):
|
||||
"""Marketplace card payload with gateway-specific install state."""
|
||||
|
||||
installed: bool
|
||||
installed_at: datetime | None = None
|
||||
|
||||
|
||||
class MarketplaceSkillActionResponse(SQLModel):
|
||||
"""Install/uninstall action response payload."""
|
||||
|
||||
ok: bool = True
|
||||
skill_id: UUID
|
||||
gateway_id: UUID
|
||||
installed: bool
|
||||
@@ -0,0 +1,99 @@
|
||||
"""add skills marketplace tables
|
||||
|
||||
Revision ID: c9d7e9b6a4f2
|
||||
Revises: b6f4c7d9e1a2
|
||||
Create Date: 2026-02-13 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c9d7e9b6a4f2"
|
||||
down_revision = "b6f4c7d9e1a2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"marketplace_skills",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("source_url", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_id",
|
||||
"source_url",
|
||||
name="uq_marketplace_skills_org_source_url",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_marketplace_skills_organization_id"),
|
||||
"marketplace_skills",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"gateway_installed_skills",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("gateway_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("skill_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["gateway_id"],
|
||||
["gateways.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["skill_id"],
|
||||
["marketplace_skills.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"gateway_id",
|
||||
"skill_id",
|
||||
name="uq_gateway_installed_skills_gateway_id_skill_id",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_gateway_installed_skills_gateway_id"),
|
||||
"gateway_installed_skills",
|
||||
["gateway_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_gateway_installed_skills_skill_id"),
|
||||
"gateway_installed_skills",
|
||||
["skill_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(
|
||||
op.f("ix_gateway_installed_skills_skill_id"),
|
||||
table_name="gateway_installed_skills",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_gateway_installed_skills_gateway_id"),
|
||||
table_name="gateway_installed_skills",
|
||||
)
|
||||
op.drop_table("gateway_installed_skills")
|
||||
op.drop_index(
|
||||
op.f("ix_marketplace_skills_organization_id"),
|
||||
table_name="marketplace_skills",
|
||||
)
|
||||
op.drop_table("marketplace_skills")
|
||||
221
backend/tests/test_skills_marketplace_api.py
Normal file
221
backend/tests/test_skills_marketplace_api.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# ruff: noqa: INP001
|
||||
"""Integration tests for skills marketplace APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
||||
from sqlmodel import SQLModel, col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.api.deps import require_org_admin
|
||||
from app.api.skills_marketplace import router as skills_marketplace_router
|
||||
from app.db.session import get_session
|
||||
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.marketplace_skills import MarketplaceSkill
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
from app.services.organizations import OrganizationContext
|
||||
|
||||
|
||||
async def _make_engine() -> AsyncEngine:
|
||||
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||
async with engine.connect() as conn, conn.begin():
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
return engine
|
||||
|
||||
|
||||
def _build_test_app(
|
||||
session_maker: async_sessionmaker[AsyncSession],
|
||||
*,
|
||||
organization: Organization,
|
||||
) -> FastAPI:
|
||||
app = FastAPI()
|
||||
api_v1 = APIRouter(prefix="/api/v1")
|
||||
api_v1.include_router(skills_marketplace_router)
|
||||
app.include_router(api_v1)
|
||||
|
||||
async def _override_get_session() -> AsyncSession:
|
||||
async with session_maker() as session:
|
||||
yield session
|
||||
|
||||
async def _override_require_org_admin() -> OrganizationContext:
|
||||
return OrganizationContext(
|
||||
organization=organization,
|
||||
member=OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=uuid4(),
|
||||
role="owner",
|
||||
all_boards_read=True,
|
||||
all_boards_write=True,
|
||||
),
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_session] = _override_get_session
|
||||
app.dependency_overrides[require_org_admin] = _override_require_org_admin
|
||||
return app
|
||||
|
||||
|
||||
async def _seed_base(
|
||||
session: AsyncSession,
|
||||
) -> tuple[Organization, Gateway]:
|
||||
organization = Organization(id=uuid4(), name="Org One")
|
||||
gateway = Gateway(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
name="Gateway One",
|
||||
url="https://gateway.example.local",
|
||||
workspace_root="/workspace/openclaw",
|
||||
)
|
||||
session.add(organization)
|
||||
session.add(gateway)
|
||||
await session.commit()
|
||||
return organization, gateway
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_skill_dispatches_instruction_and_persists_installation(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
engine = await _make_engine()
|
||||
session_maker = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
try:
|
||||
async with session_maker() as session:
|
||||
organization, gateway = await _seed_base(session)
|
||||
skill = MarketplaceSkill(
|
||||
organization_id=organization.id,
|
||||
name="Deploy Helper",
|
||||
source_url="https://example.com/skills/deploy-helper.git",
|
||||
description="Handles deploy workflow checks.",
|
||||
)
|
||||
session.add(skill)
|
||||
await session.commit()
|
||||
await session.refresh(skill)
|
||||
|
||||
app = _build_test_app(session_maker, organization=organization)
|
||||
sent_messages: list[dict[str, str | bool]] = []
|
||||
|
||||
async def _fake_send_agent_message(
|
||||
_self: object,
|
||||
*,
|
||||
session_key: str,
|
||||
config: object,
|
||||
agent_name: str,
|
||||
message: str,
|
||||
deliver: bool = False,
|
||||
) -> None:
|
||||
del config
|
||||
sent_messages.append(
|
||||
{
|
||||
"session_key": session_key,
|
||||
"agent_name": agent_name,
|
||||
"message": message,
|
||||
"deliver": deliver,
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.api.skills_marketplace.GatewayDispatchService.send_agent_message",
|
||||
_fake_send_agent_message,
|
||||
)
|
||||
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
response = await client.post(
|
||||
f"/api/v1/skills/marketplace/{skill.id}/install",
|
||||
params={"gateway_id": str(gateway.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["installed"] is True
|
||||
assert body["gateway_id"] == str(gateway.id)
|
||||
assert len(sent_messages) == 1
|
||||
assert sent_messages[0]["agent_name"] == "Gateway Agent"
|
||||
assert sent_messages[0]["deliver"] is True
|
||||
assert sent_messages[0]["session_key"] == f"agent:mc-gateway-{gateway.id}:main"
|
||||
message = str(sent_messages[0]["message"])
|
||||
assert "SKILL INSTALL REQUEST" in message
|
||||
assert str(skill.source_url) in message
|
||||
assert "/workspace/openclaw/skills" in message
|
||||
|
||||
async with session_maker() as session:
|
||||
installed_rows = (
|
||||
await session.exec(
|
||||
select(GatewayInstalledSkill).where(
|
||||
col(GatewayInstalledSkill.gateway_id) == gateway.id,
|
||||
col(GatewayInstalledSkill.skill_id) == skill.id,
|
||||
),
|
||||
)
|
||||
).all()
|
||||
assert len(installed_rows) == 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_marketplace_skills_marks_installed_cards() -> None:
|
||||
engine = await _make_engine()
|
||||
session_maker = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
try:
|
||||
async with session_maker() as session:
|
||||
organization, gateway = await _seed_base(session)
|
||||
first = MarketplaceSkill(
|
||||
organization_id=organization.id,
|
||||
name="First Skill",
|
||||
source_url="https://example.com/skills/first",
|
||||
)
|
||||
second = MarketplaceSkill(
|
||||
organization_id=organization.id,
|
||||
name="Second Skill",
|
||||
source_url="https://example.com/skills/second",
|
||||
)
|
||||
session.add(first)
|
||||
session.add(second)
|
||||
await session.commit()
|
||||
await session.refresh(first)
|
||||
await session.refresh(second)
|
||||
|
||||
session.add(
|
||||
GatewayInstalledSkill(
|
||||
gateway_id=gateway.id,
|
||||
skill_id=first.id,
|
||||
),
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
app = _build_test_app(session_maker, organization=organization)
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
response = await client.get(
|
||||
"/api/v1/skills/marketplace",
|
||||
params={"gateway_id": str(gateway.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
cards = response.json()
|
||||
assert len(cards) == 2
|
||||
cards_by_id = {item["id"]: item for item in cards}
|
||||
assert cards_by_id[str(first.id)]["installed"] is True
|
||||
assert cards_by_id[str(first.id)]["installed_at"] is not None
|
||||
assert cards_by_id[str(second.id)]["installed"] is False
|
||||
assert cards_by_id[str(second.id)]["installed_at"] is None
|
||||
finally:
|
||||
await engine.dispose()
|
||||
@@ -19,6 +19,7 @@ export interface ApprovalCreate {
|
||||
* @maximum 100
|
||||
*/
|
||||
confidence: number;
|
||||
lead_reasoning?: string | null;
|
||||
payload?: ApprovalCreatePayload;
|
||||
rubric_scores?: ApprovalCreateRubricScores;
|
||||
status?: ApprovalCreateStatus;
|
||||
|
||||
@@ -114,6 +114,7 @@ export * from "./getSessionHistoryApiV1GatewaysSessionsSessionIdHistoryGetParams
|
||||
export * from "./healthHealthGet200";
|
||||
export * from "./healthzHealthzGet200";
|
||||
export * from "./hTTPValidationError";
|
||||
export * from "./installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams";
|
||||
export * from "./limitOffsetPageTypeVarCustomizedActivityEventRead";
|
||||
export * from "./limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead";
|
||||
export * from "./limitOffsetPageTypeVarCustomizedAgentRead";
|
||||
@@ -146,6 +147,7 @@ export * from "./listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayl
|
||||
export * from "./listBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams";
|
||||
export * from "./listGatewaysApiV1GatewaysGetParams";
|
||||
export * from "./listGatewaySessionsApiV1GatewaysSessionsGetParams";
|
||||
export * from "./listMarketplaceSkillsApiV1SkillsMarketplaceGetParams";
|
||||
export * from "./listOrgInvitesApiV1OrganizationsMeInvitesGetParams";
|
||||
export * from "./listOrgMembersApiV1OrganizationsMeMembersGetParams";
|
||||
export * from "./listTagsApiV1TagsGetParams";
|
||||
@@ -154,6 +156,10 @@ export * from "./listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetPa
|
||||
export * from "./listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams";
|
||||
export * from "./listTasksApiV1AgentBoardsBoardIdTasksGetParams";
|
||||
export * from "./listTasksApiV1BoardsBoardIdTasksGetParams";
|
||||
export * from "./marketplaceSkillActionResponse";
|
||||
export * from "./marketplaceSkillCardRead";
|
||||
export * from "./marketplaceSkillCreate";
|
||||
export * from "./marketplaceSkillRead";
|
||||
export * from "./okResponse";
|
||||
export * from "./organizationActiveUpdate";
|
||||
export * from "./organizationBoardAccessRead";
|
||||
@@ -207,6 +213,7 @@ export * from "./taskReadCustomFieldValues";
|
||||
export * from "./taskReadStatus";
|
||||
export * from "./taskUpdate";
|
||||
export * from "./taskUpdateCustomFieldValues";
|
||||
export * from "./uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams";
|
||||
export * from "./updateAgentApiV1AgentsAgentIdPatchParams";
|
||||
export * from "./userRead";
|
||||
export * from "./userUpdate";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export type InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams =
|
||||
{
|
||||
gateway_id: string;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export type ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams = {
|
||||
gateway_id: string;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Install/uninstall action response payload.
|
||||
*/
|
||||
export interface MarketplaceSkillActionResponse {
|
||||
gateway_id: string;
|
||||
installed: boolean;
|
||||
ok?: boolean;
|
||||
skill_id: string;
|
||||
}
|
||||
21
frontend/src/api/generated/model/marketplaceSkillCardRead.ts
Normal file
21
frontend/src/api/generated/model/marketplaceSkillCardRead.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Marketplace card payload with gateway-specific install state.
|
||||
*/
|
||||
export interface MarketplaceSkillCardRead {
|
||||
created_at: string;
|
||||
description?: string | null;
|
||||
id: string;
|
||||
installed: boolean;
|
||||
installed_at?: string | null;
|
||||
name: string;
|
||||
organization_id: string;
|
||||
source_url: string;
|
||||
updated_at: string;
|
||||
}
|
||||
16
frontend/src/api/generated/model/marketplaceSkillCreate.ts
Normal file
16
frontend/src/api/generated/model/marketplaceSkillCreate.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payload used to register a skill URL in the organization marketplace.
|
||||
*/
|
||||
export interface MarketplaceSkillCreate {
|
||||
description?: string | null;
|
||||
name?: string | null;
|
||||
/** @minLength 1 */
|
||||
source_url: string;
|
||||
}
|
||||
19
frontend/src/api/generated/model/marketplaceSkillRead.ts
Normal file
19
frontend/src/api/generated/model/marketplaceSkillRead.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialized marketplace skill catalog record.
|
||||
*/
|
||||
export interface MarketplaceSkillRead {
|
||||
created_at: string;
|
||||
description?: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
organization_id: string;
|
||||
source_url: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export type UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams =
|
||||
{
|
||||
gateway_id: string;
|
||||
};
|
||||
@@ -0,0 +1,939 @@
|
||||
/**
|
||||
* Generated by orval v8.3.0 🍺
|
||||
* Do not edit manually.
|
||||
* Mission Control API
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
DataTag,
|
||||
DefinedInitialDataOptions,
|
||||
DefinedUseQueryResult,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UndefinedInitialDataOptions,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
HTTPValidationError,
|
||||
InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams,
|
||||
ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
MarketplaceSkillActionResponse,
|
||||
MarketplaceSkillCardRead,
|
||||
MarketplaceSkillCreate,
|
||||
MarketplaceSkillRead,
|
||||
OkResponse,
|
||||
UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams,
|
||||
} from ".././model";
|
||||
|
||||
import { customFetch } from "../../mutator";
|
||||
|
||||
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
||||
|
||||
/**
|
||||
* List marketplace cards for an org and annotate install state for a gateway.
|
||||
* @summary List Marketplace Skills
|
||||
*/
|
||||
export type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse200 = {
|
||||
data: MarketplaceSkillCardRead[];
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse422 = {
|
||||
data: HTTPValidationError;
|
||||
status: 422;
|
||||
};
|
||||
|
||||
export type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponseSuccess =
|
||||
listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponseError =
|
||||
listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse422 & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse =
|
||||
| listMarketplaceSkillsApiV1SkillsMarketplaceGetResponseSuccess
|
||||
| listMarketplaceSkillsApiV1SkillsMarketplaceGetResponseError;
|
||||
|
||||
export const getListMarketplaceSkillsApiV1SkillsMarketplaceGetUrl = (
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(key, value === null ? "null" : value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0
|
||||
? `/api/v1/skills/marketplace?${stringifiedParams}`
|
||||
: `/api/v1/skills/marketplace`;
|
||||
};
|
||||
|
||||
export const listMarketplaceSkillsApiV1SkillsMarketplaceGet = async (
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options?: RequestInit,
|
||||
): Promise<listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse> => {
|
||||
return customFetch<listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse>(
|
||||
getListMarketplaceSkillsApiV1SkillsMarketplaceGetUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryKey = (
|
||||
params?: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
) => {
|
||||
return [`/api/v1/skills/marketplace`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryOptions = <
|
||||
TData = Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError = HTTPValidationError,
|
||||
>(
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>>
|
||||
> = ({ signal }) =>
|
||||
listMarketplaceSkillsApiV1SkillsMarketplaceGet(params, {
|
||||
signal,
|
||||
...requestOptions,
|
||||
});
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
};
|
||||
|
||||
export type ListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryResult =
|
||||
NonNullable<
|
||||
Awaited<ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>>
|
||||
>;
|
||||
export type ListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryError =
|
||||
HTTPValidationError;
|
||||
|
||||
export function useListMarketplaceSkillsApiV1SkillsMarketplaceGet<
|
||||
TData = Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError = HTTPValidationError,
|
||||
>(
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options: {
|
||||
query: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useListMarketplaceSkillsApiV1SkillsMarketplaceGet<
|
||||
TData = Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError = HTTPValidationError,
|
||||
>(
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useListMarketplaceSkillsApiV1SkillsMarketplaceGet<
|
||||
TData = Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError = HTTPValidationError,
|
||||
>(
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
/**
|
||||
* @summary List Marketplace Skills
|
||||
*/
|
||||
|
||||
export function useListMarketplaceSkillsApiV1SkillsMarketplaceGet<
|
||||
TData = Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError = HTTPValidationError,
|
||||
>(
|
||||
params: ListMarketplaceSkillsApiV1SkillsMarketplaceGetParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof listMarketplaceSkillsApiV1SkillsMarketplaceGet>
|
||||
>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
} {
|
||||
const queryOptions =
|
||||
getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryOptions(
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
|
||||
TData,
|
||||
TError
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a skill source URL in the organization's marketplace catalog.
|
||||
* @summary Create Marketplace Skill
|
||||
*/
|
||||
export type createMarketplaceSkillApiV1SkillsMarketplacePostResponse200 = {
|
||||
data: MarketplaceSkillRead;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type createMarketplaceSkillApiV1SkillsMarketplacePostResponse422 = {
|
||||
data: HTTPValidationError;
|
||||
status: 422;
|
||||
};
|
||||
|
||||
export type createMarketplaceSkillApiV1SkillsMarketplacePostResponseSuccess =
|
||||
createMarketplaceSkillApiV1SkillsMarketplacePostResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type createMarketplaceSkillApiV1SkillsMarketplacePostResponseError =
|
||||
createMarketplaceSkillApiV1SkillsMarketplacePostResponse422 & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type createMarketplaceSkillApiV1SkillsMarketplacePostResponse =
|
||||
| createMarketplaceSkillApiV1SkillsMarketplacePostResponseSuccess
|
||||
| createMarketplaceSkillApiV1SkillsMarketplacePostResponseError;
|
||||
|
||||
export const getCreateMarketplaceSkillApiV1SkillsMarketplacePostUrl = () => {
|
||||
return `/api/v1/skills/marketplace`;
|
||||
};
|
||||
|
||||
export const createMarketplaceSkillApiV1SkillsMarketplacePost = async (
|
||||
marketplaceSkillCreate: MarketplaceSkillCreate,
|
||||
options?: RequestInit,
|
||||
): Promise<createMarketplaceSkillApiV1SkillsMarketplacePostResponse> => {
|
||||
return customFetch<createMarketplaceSkillApiV1SkillsMarketplacePostResponse>(
|
||||
getCreateMarketplaceSkillApiV1SkillsMarketplacePostUrl(),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(marketplaceSkillCreate),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getCreateMarketplaceSkillApiV1SkillsMarketplacePostMutationOptions =
|
||||
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>
|
||||
>,
|
||||
TError,
|
||||
{ data: MarketplaceSkillCreate },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>
|
||||
>,
|
||||
TError,
|
||||
{ data: MarketplaceSkillCreate },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["createMarketplaceSkillApiV1SkillsMarketplacePost"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<
|
||||
ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>
|
||||
>,
|
||||
{ data: MarketplaceSkillCreate }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createMarketplaceSkillApiV1SkillsMarketplacePost(
|
||||
data,
|
||||
requestOptions,
|
||||
);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateMarketplaceSkillApiV1SkillsMarketplacePostMutationResult =
|
||||
NonNullable<
|
||||
Awaited<ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>>
|
||||
>;
|
||||
export type CreateMarketplaceSkillApiV1SkillsMarketplacePostMutationBody =
|
||||
MarketplaceSkillCreate;
|
||||
export type CreateMarketplaceSkillApiV1SkillsMarketplacePostMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary Create Marketplace Skill
|
||||
*/
|
||||
export const useCreateMarketplaceSkillApiV1SkillsMarketplacePost = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>
|
||||
>,
|
||||
TError,
|
||||
{ data: MarketplaceSkillCreate },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createMarketplaceSkillApiV1SkillsMarketplacePost>>,
|
||||
TError,
|
||||
{ data: MarketplaceSkillCreate },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getCreateMarketplaceSkillApiV1SkillsMarketplacePostMutationOptions(options),
|
||||
queryClient,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Delete a marketplace catalog entry and any install records that reference it.
|
||||
* @summary Delete Marketplace Skill
|
||||
*/
|
||||
export type deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse200 =
|
||||
{
|
||||
data: OkResponse;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse422 =
|
||||
{
|
||||
data: HTTPValidationError;
|
||||
status: 422;
|
||||
};
|
||||
|
||||
export type deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponseSuccess =
|
||||
deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponseError =
|
||||
deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse422 & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse =
|
||||
| deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponseSuccess
|
||||
| deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponseError;
|
||||
|
||||
export const getDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteUrl = (
|
||||
skillId: string,
|
||||
) => {
|
||||
return `/api/v1/skills/marketplace/${skillId}`;
|
||||
};
|
||||
|
||||
export const deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete = async (
|
||||
skillId: string,
|
||||
options?: RequestInit,
|
||||
): Promise<deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse> => {
|
||||
return customFetch<deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteResponse>(
|
||||
getDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteUrl(skillId),
|
||||
{
|
||||
...options,
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteMutationOptions =
|
||||
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{ skillId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{ skillId: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = [
|
||||
"deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete",
|
||||
];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete
|
||||
>
|
||||
>,
|
||||
{ skillId: string }
|
||||
> = (props) => {
|
||||
const { skillId } = props ?? {};
|
||||
|
||||
return deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete(
|
||||
skillId,
|
||||
requestOptions,
|
||||
);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteMutationResult =
|
||||
NonNullable<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
export type DeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary Delete Marketplace Skill
|
||||
*/
|
||||
export const useDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{ skillId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<
|
||||
ReturnType<typeof deleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete>
|
||||
>,
|
||||
TError,
|
||||
{ skillId: string },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDeleteMutationOptions(
|
||||
options,
|
||||
),
|
||||
queryClient,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Install a marketplace skill by dispatching instructions to the gateway agent.
|
||||
* @summary Install Marketplace Skill
|
||||
*/
|
||||
export type installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse200 =
|
||||
{
|
||||
data: MarketplaceSkillActionResponse;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse422 =
|
||||
{
|
||||
data: HTTPValidationError;
|
||||
status: 422;
|
||||
};
|
||||
|
||||
export type installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponseSuccess =
|
||||
installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponseError =
|
||||
installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse422 & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse =
|
||||
|
||||
| installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponseSuccess
|
||||
| installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponseError;
|
||||
|
||||
export const getInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostUrl =
|
||||
(
|
||||
skillId: string,
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams,
|
||||
) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(
|
||||
key,
|
||||
value === null ? "null" : value.toString(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0
|
||||
? `/api/v1/skills/marketplace/${skillId}/install?${stringifiedParams}`
|
||||
: `/api/v1/skills/marketplace/${skillId}/install`;
|
||||
};
|
||||
|
||||
export const installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost =
|
||||
async (
|
||||
skillId: string,
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams,
|
||||
options?: RequestInit,
|
||||
): Promise<installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse> => {
|
||||
return customFetch<installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostResponse>(
|
||||
getInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostUrl(
|
||||
skillId,
|
||||
params,
|
||||
),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostMutationOptions =
|
||||
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = [
|
||||
"installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost",
|
||||
];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>,
|
||||
{
|
||||
skillId: string;
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { skillId, params } = props ?? {};
|
||||
|
||||
return installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost(
|
||||
skillId,
|
||||
params,
|
||||
requestOptions,
|
||||
);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostMutationResult =
|
||||
NonNullable<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
export type InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary Install Marketplace Skill
|
||||
*/
|
||||
export const useInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost =
|
||||
<TError = HTTPValidationError, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof installMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: InstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPostMutationOptions(
|
||||
options,
|
||||
),
|
||||
queryClient,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Uninstall a marketplace skill by dispatching instructions to the gateway agent.
|
||||
* @summary Uninstall Marketplace Skill
|
||||
*/
|
||||
export type uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse200 =
|
||||
{
|
||||
data: MarketplaceSkillActionResponse;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse422 =
|
||||
{
|
||||
data: HTTPValidationError;
|
||||
status: 422;
|
||||
};
|
||||
|
||||
export type uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponseSuccess =
|
||||
uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponseError =
|
||||
uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse422 & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse =
|
||||
|
||||
| uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponseSuccess
|
||||
| uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponseError;
|
||||
|
||||
export const getUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostUrl =
|
||||
(
|
||||
skillId: string,
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams,
|
||||
) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(
|
||||
key,
|
||||
value === null ? "null" : value.toString(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0
|
||||
? `/api/v1/skills/marketplace/${skillId}/uninstall?${stringifiedParams}`
|
||||
: `/api/v1/skills/marketplace/${skillId}/uninstall`;
|
||||
};
|
||||
|
||||
export const uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost =
|
||||
async (
|
||||
skillId: string,
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams,
|
||||
options?: RequestInit,
|
||||
): Promise<uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse> => {
|
||||
return customFetch<uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostResponse>(
|
||||
getUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostUrl(
|
||||
skillId,
|
||||
params,
|
||||
),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostMutationOptions =
|
||||
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = [
|
||||
"uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost",
|
||||
];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>,
|
||||
{
|
||||
skillId: string;
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { skillId, params } = props ?? {};
|
||||
|
||||
return uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost(
|
||||
skillId,
|
||||
params,
|
||||
requestOptions,
|
||||
);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostMutationResult =
|
||||
NonNullable<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
export type UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary Uninstall Marketplace Skill
|
||||
*/
|
||||
export const useUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost =
|
||||
<TError = HTTPValidationError, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<
|
||||
ReturnType<
|
||||
typeof uninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost
|
||||
>
|
||||
>,
|
||||
TError,
|
||||
{
|
||||
skillId: string;
|
||||
params: UninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPostMutationOptions(
|
||||
options,
|
||||
),
|
||||
queryClient,
|
||||
);
|
||||
};
|
||||
412
frontend/src/app/skills/page.tsx
Normal file
412
frontend/src/app/skills/page.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import Link from "next/link";
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
|
||||
import { useAuth } from "@/auth/clerk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ExternalLink, Package, PlusCircle, Trash2 } from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
type listGatewaysApiV1GatewaysGetResponse,
|
||||
useListGatewaysApiV1GatewaysGet,
|
||||
} from "@/api/generated/gateways/gateways";
|
||||
import type { MarketplaceSkillCardRead } from "@/api/generated/model";
|
||||
import {
|
||||
getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryKey,
|
||||
type listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse,
|
||||
useCreateMarketplaceSkillApiV1SkillsMarketplacePost,
|
||||
useDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete,
|
||||
useInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost,
|
||||
useListMarketplaceSkillsApiV1SkillsMarketplaceGet,
|
||||
useUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost,
|
||||
} from "@/api/generated/skills-marketplace/skills-marketplace";
|
||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { formatRelativeTimestamp } from "@/lib/formatters";
|
||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||
|
||||
export default function SkillsMarketplacePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { isSignedIn } = useAuth();
|
||||
const { isAdmin } = useOrganizationMembership(isSignedIn);
|
||||
const [selectedGatewayId, setSelectedGatewayId] = useState("");
|
||||
const [sourceUrl, setSourceUrl] = useState("");
|
||||
const [skillName, setSkillName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const gatewaysQuery = useListGatewaysApiV1GatewaysGet<
|
||||
listGatewaysApiV1GatewaysGetResponse,
|
||||
ApiError
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn && isAdmin),
|
||||
refetchOnMount: "always",
|
||||
refetchInterval: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
const gateways = useMemo(
|
||||
() =>
|
||||
gatewaysQuery.data?.status === 200
|
||||
? (gatewaysQuery.data.data.items ?? [])
|
||||
: [],
|
||||
[gatewaysQuery.data],
|
||||
);
|
||||
|
||||
const resolvedGatewayId = useMemo(() => {
|
||||
if (selectedGatewayId && gateways.some((gateway) => gateway.id === selectedGatewayId)) {
|
||||
return selectedGatewayId;
|
||||
}
|
||||
return gateways[0]?.id ?? "";
|
||||
}, [gateways, selectedGatewayId]);
|
||||
|
||||
const skillsQueryKey = getListMarketplaceSkillsApiV1SkillsMarketplaceGetQueryKey(
|
||||
resolvedGatewayId ? { gateway_id: resolvedGatewayId } : undefined,
|
||||
);
|
||||
|
||||
const skillsQuery = useListMarketplaceSkillsApiV1SkillsMarketplaceGet<
|
||||
listMarketplaceSkillsApiV1SkillsMarketplaceGetResponse,
|
||||
ApiError
|
||||
>(
|
||||
{ gateway_id: resolvedGatewayId },
|
||||
{
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn && isAdmin && resolvedGatewayId),
|
||||
refetchOnMount: "always",
|
||||
refetchInterval: 15_000,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const skills = useMemo<MarketplaceSkillCardRead[]>(
|
||||
() => (skillsQuery.data?.status === 200 ? skillsQuery.data.data : []),
|
||||
[skillsQuery.data],
|
||||
);
|
||||
|
||||
const createMutation =
|
||||
useCreateMarketplaceSkillApiV1SkillsMarketplacePost<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
setSourceUrl("");
|
||||
setSkillName("");
|
||||
setDescription("");
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: skillsQueryKey,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const installMutation =
|
||||
useInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: skillsQueryKey,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const uninstallMutation =
|
||||
useUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: skillsQueryKey,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const deleteMutation =
|
||||
useDeleteMarketplaceSkillApiV1SkillsMarketplaceSkillIdDelete<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: skillsQueryKey,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const mutationError =
|
||||
createMutation.error?.message ??
|
||||
installMutation.error?.message ??
|
||||
uninstallMutation.error?.message ??
|
||||
deleteMutation.error?.message;
|
||||
|
||||
const handleAddSkill = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const normalizedUrl = sourceUrl.trim();
|
||||
if (!normalizedUrl) return;
|
||||
createMutation.mutate({
|
||||
data: {
|
||||
source_url: normalizedUrl,
|
||||
name: skillName.trim() || undefined,
|
||||
description: description.trim() || undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isMutating =
|
||||
createMutation.isPending ||
|
||||
installMutation.isPending ||
|
||||
uninstallMutation.isPending ||
|
||||
deleteMutation.isPending;
|
||||
|
||||
return (
|
||||
<DashboardPageLayout
|
||||
signedOut={{
|
||||
message: "Sign in to manage marketplace skills.",
|
||||
forceRedirectUrl: "/skills",
|
||||
}}
|
||||
title="Skills Marketplace"
|
||||
description="Register skill links and install or uninstall them per gateway."
|
||||
isAdmin={isAdmin}
|
||||
adminOnlyMessage="Only organization owners and admins can manage skills."
|
||||
stickyHeader
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{gateways.length === 0 ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-600 shadow-sm">
|
||||
<p className="font-medium text-slate-900">No gateways available yet.</p>
|
||||
<p className="mt-2">
|
||||
Create a gateway first, then return here to install skills.
|
||||
</p>
|
||||
<Link
|
||||
href="/gateways/new"
|
||||
className={`${buttonVariants({ variant: "primary", size: "md" })} mt-4`}
|
||||
>
|
||||
Create gateway
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<h2 className="font-heading text-lg font-semibold text-slate-900">
|
||||
Add skill source
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
Add a URL once, then install or uninstall the skill for the selected gateway.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-5">
|
||||
<form className="space-y-4" onSubmit={handleAddSkill}>
|
||||
<div className="grid gap-4 md:grid-cols-[260px_1fr]">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
Gateway
|
||||
</label>
|
||||
<Select value={resolvedGatewayId} onValueChange={setSelectedGatewayId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select gateway" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gateways.map((gateway) => (
|
||||
<SelectItem key={gateway.id} value={gateway.id}>
|
||||
{gateway.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="skill-url"
|
||||
className="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
Skill URL
|
||||
</label>
|
||||
<Input
|
||||
id="skill-url"
|
||||
type="url"
|
||||
value={sourceUrl}
|
||||
onChange={(event) => setSourceUrl(event.target.value)}
|
||||
placeholder="https://github.com/org/skill-repo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="skill-name"
|
||||
className="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
Display name (optional)
|
||||
</label>
|
||||
<Input
|
||||
id="skill-name"
|
||||
value={skillName}
|
||||
onChange={(event) => setSkillName(event.target.value)}
|
||||
placeholder="Deploy Helper"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="skill-description"
|
||||
className="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
Description (optional)
|
||||
</label>
|
||||
<Textarea
|
||||
id="skill-description"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
placeholder="Short summary shown on the marketplace card."
|
||||
className="min-h-[44px] py-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || !resolvedGatewayId}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
{createMutation.isPending ? "Adding…" : "Add skill"}
|
||||
</Button>
|
||||
{createMutation.error ? (
|
||||
<p className="text-sm text-rose-600">
|
||||
{createMutation.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{mutationError ? <p className="text-sm text-rose-600">{mutationError}</p> : null}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-heading text-lg font-semibold text-slate-900">
|
||||
Marketplace skills
|
||||
</h2>
|
||||
{skillsQuery.isLoading ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||
Loading skills…
|
||||
</div>
|
||||
) : skillsQuery.error ? (
|
||||
<div className="rounded-xl border border-rose-200 bg-rose-50 p-6 text-sm text-rose-700">
|
||||
{skillsQuery.error.message}
|
||||
</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-600 shadow-sm">
|
||||
No skill links added yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{skills.map((skill) => (
|
||||
<Card key={skill.id}>
|
||||
<CardHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-slate-900">
|
||||
{skill.name}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
{skill.description || "No description provided."}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={skill.installed ? "success" : "outline"}>
|
||||
{skill.installed ? "Installed" : "Not installed"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-5">
|
||||
<a
|
||||
href={skill.source_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-[color:var(--accent)] hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Open source link
|
||||
</a>
|
||||
<p className="text-xs text-slate-500">
|
||||
{skill.installed && skill.installed_at
|
||||
? `Installed ${formatRelativeTimestamp(skill.installed_at)}`
|
||||
: "Not installed on selected gateway"}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{skill.installed ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
uninstallMutation.mutate({
|
||||
skillId: skill.id,
|
||||
params: { gateway_id: resolvedGatewayId },
|
||||
})
|
||||
}
|
||||
disabled={isMutating || !resolvedGatewayId}
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Uninstall
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
installMutation.mutate({
|
||||
skillId: skill.id,
|
||||
params: { gateway_id: resolvedGatewayId },
|
||||
})
|
||||
}
|
||||
disabled={isMutating || !resolvedGatewayId}
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Install
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => deleteMutation.mutate({ skillId: skill.id })}
|
||||
disabled={isMutating}
|
||||
aria-label={`Delete ${skill.name}`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Building2,
|
||||
LayoutGrid,
|
||||
Network,
|
||||
Package,
|
||||
Settings,
|
||||
Tags,
|
||||
} from "lucide-react";
|
||||
@@ -195,6 +196,20 @@ export function DashboardSidebar() {
|
||||
Gateways
|
||||
</Link>
|
||||
) : null}
|
||||
{isAdmin ? (
|
||||
<Link
|
||||
href="/skills"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/skills")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Skills
|
||||
</Link>
|
||||
) : null}
|
||||
{isAdmin ? (
|
||||
<Link
|
||||
href="/agents"
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ChevronDown,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Package,
|
||||
Plus,
|
||||
Server,
|
||||
Settings,
|
||||
@@ -155,6 +156,7 @@ export function UserMenu({
|
||||
{ href: "/activity", label: "Activity", icon: Activity },
|
||||
{ href: "/agents", label: "Agents", icon: Bot },
|
||||
{ href: "/gateways", label: "Gateways", icon: Server },
|
||||
{ href: "/skills", label: "Skills", icon: Package },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
] as const
|
||||
).map((item) => (
|
||||
|
||||
Reference in New Issue
Block a user