feat: Enhance tool management and documentation in Castle

- Introduced category tools support in the `castle create` command.
- Added detailed guides for creating components in CLAUDE.md.
- Implemented new API endpoints for listing and retrieving tool details.
- Updated component and tool models to include additional metadata.
- Improved error handling and response structures in service actions.
- Enhanced documentation for component registry and web APIs.
This commit is contained in:
2026-02-21 00:09:34 -08:00
parent f39a551aad
commit 08c6f3fa83
34 changed files with 1748 additions and 296 deletions

View File

@@ -2,6 +2,7 @@ import { ExternalLink, Play, RefreshCw, Server, Square, Terminal } from "lucide-
import { Link } from "react-router-dom"
import type { ComponentSummary, HealthStatus } from "@/types"
import { useServiceAction } from "@/services/api/hooks"
import { runnerLabel } from "@/lib/labels"
import { HealthBadge } from "./HealthBadge"
import { RoleBadge } from "./RoleBadge"
@@ -56,7 +57,7 @@ export function ComponentCard({ component, health }: ComponentCardProps) {
{component.runner && (
<span className="flex items-center gap-1">
<Terminal size={12} />
{component.runner}
{runnerLabel(component.runner)}
</span>
)}
{component.proxy_path && (

View File

@@ -1,6 +1,7 @@
import { useMemo, useState } from "react"
import { Check, Loader2, Save, Trash2 } from "lucide-react"
import type { ComponentDetail } from "@/types"
import { runnerLabel } from "@/lib/labels"
import { SecretsEditor } from "./SecretsEditor"
interface ComponentFieldsProps {
@@ -113,7 +114,7 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
{runner && (
<Field label="Runner">
<span className="text-sm font-mono text-[var(--muted)]">
{runner}
{runnerLabel(runner)}
{(m.run as Record<string, string>)?.tool && (
<> &middot; {(m.run as Record<string, string>).tool}</>
)}

View File

@@ -0,0 +1,333 @@
import { useMemo, useState } from "react"
import { Link } from "react-router-dom"
import { ArrowDown, ArrowUp, ArrowUpDown, Download, Play, RefreshCw, Square, Trash2 } from "lucide-react"
import type { ComponentSummary, HealthStatus } from "@/types"
import { useServiceAction, useToolAction } from "@/services/api/hooks"
import { runnerLabel, toolTypeLabel } from "@/lib/labels"
import { HealthBadge } from "./HealthBadge"
import { RoleBadge } from "./RoleBadge"
interface ComponentTableProps {
components: ComponentSummary[]
statuses: HealthStatus[]
}
type SortKey = "id" | "role" | "runner" | "tool_type" | "category" | "schedule" | "port" | "status"
type SortDir = "asc" | "desc"
function statusRank(s: HealthStatus | undefined, installed: boolean | null): number {
// Health takes priority for services
if (s) {
if (s.status === "down") return 0
if (s.status === "up") return 3
return 2
}
// Installed state for tools
if (installed === false) return 1
if (installed === true) return 3
return 2
}
export function ComponentTable({ components, statuses }: ComponentTableProps) {
const statusMap = useMemo(() => new Map(statuses.map((s) => [s.id, s])), [statuses])
const allRoles = useMemo(() => {
const set = new Set<string>()
for (const c of components) for (const r of c.roles) set.add(r)
return Array.from(set).sort()
}, [components])
const [roleFilter, setRoleFilter] = useState<string | null>(null)
const [search, setSearch] = useState("")
const [sortKey, setSortKey] = useState<SortKey>("id")
const [sortDir, setSortDir] = useState<SortDir>("asc")
const toggleSort = (key: SortKey) => {
if (sortKey === key) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"))
} else {
setSortKey(key)
setSortDir("asc")
}
}
const filtered = useMemo(() => {
let list = components
if (roleFilter) {
list = list.filter((c) => c.roles.includes(roleFilter))
}
if (search) {
const q = search.toLowerCase()
list = list.filter(
(c) =>
c.id.toLowerCase().includes(q) ||
(c.description?.toLowerCase().includes(q) ?? false),
)
}
return list
}, [components, roleFilter, search])
const sorted = useMemo(() => {
const dir = sortDir === "asc" ? 1 : -1
return [...filtered].sort((a, b) => {
switch (sortKey) {
case "id":
return dir * a.id.localeCompare(b.id)
case "role":
return dir * (a.roles[0] ?? "").localeCompare(b.roles[0] ?? "")
case "runner":
return dir * (a.runner ?? "").localeCompare(b.runner ?? "")
case "tool_type":
return dir * (a.tool_type ?? "").localeCompare(b.tool_type ?? "")
case "category":
return dir * (a.category ?? "").localeCompare(b.category ?? "")
case "schedule":
return dir * (a.schedule ?? "").localeCompare(b.schedule ?? "")
case "port":
return dir * ((a.port ?? 0) - (b.port ?? 0))
case "status":
return dir * (statusRank(statusMap.get(a.id), a.installed) - statusRank(statusMap.get(b.id), b.installed))
default:
return 0
}
})
}, [filtered, sortKey, sortDir, statusMap])
return (
<div>
{/* Filters */}
<div className="flex items-center gap-3 mb-4 flex-wrap">
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter components..."
className="bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)] w-56"
/>
<div className="flex items-center gap-1.5">
<button
onClick={() => setRoleFilter(null)}
className={`text-xs px-2 py-1 rounded transition-colors ${
roleFilter === null
? "bg-[var(--primary)] text-white"
: "bg-[var(--border)] text-[var(--muted)] hover:text-[var(--foreground)]"
}`}
>
All
</button>
{allRoles.map((role) => (
<button
key={role}
onClick={() => setRoleFilter(roleFilter === role ? null : role)}
className={`text-xs px-2 py-1 rounded transition-colors ${
roleFilter === role
? "bg-[var(--primary)] text-white"
: "bg-[var(--border)] text-[var(--muted)] hover:text-[var(--foreground)]"
}`}
>
{role}
</button>
))}
</div>
</div>
{/* Table */}
<div className="border border-[var(--border)] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-[var(--card)] border-b border-[var(--border)] text-left">
<SortHeader label="Name" sortKey="id" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Roles" sortKey="role" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Runner" sortKey="runner" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Package" sortKey="tool_type" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Category" sortKey="category" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Schedule" sortKey="schedule" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Port" sortKey="port" current={sortKey} dir={sortDir} onSort={toggleSort} />
<SortHeader label="Status" sortKey="status" current={sortKey} dir={sortDir} onSort={toggleSort} />
<th className="px-3 py-2 font-medium text-[var(--muted)]">Actions</th>
</tr>
</thead>
<tbody>
{sorted.map((comp) => (
<ComponentRow
key={comp.id}
component={comp}
health={statusMap.get(comp.id)}
/>
))}
{sorted.length === 0 && (
<tr>
<td colSpan={9} className="px-3 py-6 text-center text-[var(--muted)]">
No components match.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
function SortHeader({
label,
sortKey,
current,
dir,
onSort,
}: {
label: string
sortKey: SortKey
current: SortKey
dir: SortDir
onSort: (key: SortKey) => void
}) {
const active = current === sortKey
const Icon = active ? (dir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown
return (
<th className="px-3 py-2 font-medium text-[var(--muted)]">
<button
onClick={() => onSort(sortKey)}
className="flex items-center gap-1 hover:text-[var(--foreground)] transition-colors"
>
{label}
<Icon size={12} className={active ? "text-[var(--foreground)]" : ""} />
</button>
</th>
)
}
function InstalledBadge({ installed }: { installed: boolean }) {
return installed ? (
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-green-900/40 text-green-400 border border-green-800/50">
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
installed
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-zinc-800/40 text-[var(--muted)] border border-[var(--border)]">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-500" />
not installed
</span>
)
}
function ComponentRow({
component,
health,
}: {
component: ComponentSummary
health?: HealthStatus
}) {
const hasHttp = component.port != null
const isTool = component.installed !== null
const { mutate: serviceAction, isPending: servicePending } = useServiceAction()
const { mutate: toolAction, isPending: toolPending } = useToolAction()
const isDown = health?.status === "down"
return (
<tr className="border-b border-[var(--border)] last:border-b-0 hover:bg-[var(--card)]/50 transition-colors">
<td className="px-3 py-2.5">
<Link
to={`/${component.id}`}
className="font-medium hover:text-[var(--primary)] transition-colors"
>
{component.id}
</Link>
{component.description && (
<p className="text-xs text-[var(--muted)] mt-0.5 truncate max-w-xs">
{component.description}
</p>
)}
</td>
<td className="px-3 py-2.5">
<div className="flex gap-1 flex-wrap">
{component.roles.map((role) => (
<RoleBadge key={role} role={role} />
))}
</div>
</td>
<td className="px-3 py-2.5 text-[var(--muted)]">
{component.runner ? runnerLabel(component.runner) : "—"}
</td>
<td className="px-3 py-2.5 text-[var(--muted)]">
{component.tool_type ? toolTypeLabel(component.tool_type) : "—"}
</td>
<td className="px-3 py-2.5 text-[var(--muted)]">
{component.category ?? "—"}
</td>
<td className="px-3 py-2.5 font-mono text-[var(--muted)]">
{component.schedule ?? "—"}
</td>
<td className="px-3 py-2.5 font-mono text-[var(--muted)]">
{component.port ?? "—"}
</td>
<td className="px-3 py-2.5">
{health ? (
<HealthBadge status={health.status} latency={health.latency_ms} />
) : hasHttp ? (
<HealthBadge status="unknown" />
) : isTool ? (
<InstalledBadge installed={component.installed!} />
) : (
<span className="text-[var(--muted)]"></span>
)}
</td>
<td className="px-3 py-2.5">
{component.managed ? (
<div className="flex items-center gap-1">
{isDown && (
<button
onClick={() => serviceAction({ name: component.id, action: "start" })}
disabled={servicePending}
className="p-1 rounded hover:bg-green-800/30 text-green-400 transition-colors disabled:opacity-40"
title="Start"
>
<Play size={14} />
</button>
)}
<button
onClick={() => serviceAction({ name: component.id, action: "restart" })}
disabled={servicePending}
className="p-1 rounded hover:bg-blue-800/30 text-blue-400 transition-colors disabled:opacity-40"
title="Restart"
>
<RefreshCw size={14} />
</button>
{!isDown && (
<button
onClick={() => serviceAction({ name: component.id, action: "stop" })}
disabled={servicePending}
className="p-1 rounded hover:bg-red-800/30 text-red-400 transition-colors disabled:opacity-40"
title="Stop"
>
<Square size={14} />
</button>
)}
</div>
) : isTool ? (
<div className="flex items-center gap-1">
{component.installed ? (
<button
onClick={() => toolAction({ name: component.id, action: "uninstall" })}
disabled={toolPending}
className="p-1 rounded hover:bg-red-800/30 text-red-400 transition-colors disabled:opacity-40"
title="Uninstall from PATH"
>
<Trash2 size={14} />
</button>
) : (
<button
onClick={() => toolAction({ name: component.id, action: "install" })}
disabled={toolPending}
className="p-1 rounded hover:bg-green-800/30 text-green-400 transition-colors disabled:opacity-40"
title="Install to PATH"
>
<Download size={14} />
</button>
)}
</div>
) : (
<span className="text-[var(--muted)]"></span>
)}
</td>
</tr>
)
}

View File

@@ -1,4 +1,5 @@
import { cn } from "@/lib/utils"
import { ROLE_DESCRIPTIONS } from "@/lib/labels"
const roleColors: Record<string, string> = {
service: "bg-green-700 text-white",
@@ -17,6 +18,7 @@ export function RoleBadge({ role }: { role: string }) {
"inline-block text-[0.65rem] font-semibold uppercase px-1.5 py-0.5 rounded",
roleColors[role] ?? "bg-gray-600 text-gray-200",
)}
title={ROLE_DESCRIPTIONS[role]}
>
{role}
</span>

View File

@@ -0,0 +1,57 @@
import { Link } from "react-router-dom"
import { Terminal } from "lucide-react"
import type { ToolSummary } from "@/types"
import { runnerLabel } from "@/lib/labels"
interface ToolCardProps {
tool: ToolSummary
}
export function ToolCard({ tool }: ToolCardProps) {
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5">
<div className="flex items-start justify-between mb-2">
<Link
to={`/${tool.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors"
>
{tool.id}
</Link>
{tool.installed && (
<span className="text-xs px-2 py-0.5 rounded bg-green-900/30 text-green-400 border border-green-800">
installed
</span>
)}
</div>
{tool.description && (
<p className="text-sm text-[var(--muted)] mb-3">{tool.description}</p>
)}
<div className="flex items-center gap-3 text-xs text-[var(--muted)] mb-3">
{tool.runner && (
<span className="flex items-center gap-1">
<Terminal size={12} />
{runnerLabel(tool.runner)}
</span>
)}
{tool.version && (
<span className="font-mono">v{tool.version}</span>
)}
</div>
{tool.system_dependencies.length > 0 && (
<div className="flex flex-wrap gap-1">
{tool.system_dependencies.map((dep) => (
<span
key={dep}
className="text-xs px-2 py-0.5 rounded bg-amber-900/30 text-amber-400 border border-amber-800"
>
{dep}
</span>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,31 @@
export const RUNNER_LABELS: Record<string, string> = {
python_uv_tool: "Python (uv)",
command: "Command",
python_module: "Python module",
container: "Container",
node: "Node.js",
remote: "Remote",
}
export const TOOL_TYPE_LABELS: Record<string, string> = {
python_standalone: "Python package",
script: "Shell script",
}
export const ROLE_DESCRIPTIONS: Record<string, string> = {
service: "Exposes HTTP endpoints",
tool: "CLI utility installed to PATH",
worker: "Background process (no HTTP)",
job: "Runs on a schedule",
frontend: "Built static assets",
remote: "Hosted externally",
containerized: "Runs in a container",
}
export function runnerLabel(runner: string): string {
return RUNNER_LABELS[runner] ?? runner
}
export function toolTypeLabel(toolType: string): string {
return TOOL_TYPE_LABELS[toolType] ?? toolType
}

View File

@@ -3,7 +3,8 @@ import { useParams, Link, useNavigate } from "react-router-dom"
import { ArrowLeft, Check, Play, RefreshCw, Square } from "lucide-react"
import { useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/services/api/client"
import { useComponent, useStatus, useServiceAction, useEventStream } from "@/services/api/hooks"
import { useComponent, useStatus, useServiceAction, useEventStream, useToolDetail } from "@/services/api/hooks"
import { runnerLabel, toolTypeLabel } from "@/lib/labels"
import { ComponentFields } from "@/components/ComponentFields"
import { HealthBadge } from "@/components/HealthBadge"
import { LogViewer } from "@/components/LogViewer"
@@ -19,6 +20,8 @@ export function ComponentDetailPage() {
const { mutate, isPending } = useServiceAction()
const health = statusResp?.statuses.find((s) => s.id === name)
const isDown = health?.status === "down"
const isTool = component?.roles.includes("tool") ?? false
const { data: toolDetail } = useToolDetail(isTool ? (name ?? "") : "")
const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null)
const handleSave = async (compName: string, config: Record<string, unknown>) => {
@@ -130,6 +133,72 @@ export function ComponentDetailPage() {
</div>
)}
{toolDetail && (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
Tool Info
</h2>
<p className="text-xs text-[var(--muted)] mb-4">
How this tool is packaged and what it depends on.
</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
{toolDetail.category && (
<>
<span className="text-[var(--muted)]">Category</span>
<span>{toolDetail.category}</span>
</>
)}
{toolDetail.source && (
<>
<span className="text-[var(--muted)]">Source</span>
<span className="font-mono">{toolDetail.source}</span>
</>
)}
{toolDetail.version && (
<>
<span className="text-[var(--muted)]">Version</span>
<span>{toolDetail.version}</span>
</>
)}
{toolDetail.tool_type && (
<>
<span className="text-[var(--muted)]">Type</span>
<span>{toolTypeLabel(toolDetail.tool_type)}</span>
</>
)}
{toolDetail.runner && (
<>
<span className="text-[var(--muted)]">Runner</span>
<span>{runnerLabel(toolDetail.runner)}</span>
</>
)}
</div>
{toolDetail.system_dependencies.length > 0 && (
<div className="mb-4">
<span className="text-sm text-[var(--muted)] block mb-1">System Dependencies</span>
<div className="flex flex-wrap gap-1">
{toolDetail.system_dependencies.map((dep) => (
<span
key={dep}
className="text-xs px-2 py-0.5 rounded bg-amber-900/30 text-amber-400 border border-amber-800"
>
{dep}
</span>
))}
</div>
</div>
)}
{toolDetail.docs && (
<div>
<span className="text-sm text-[var(--muted)] block mb-1">Documentation</span>
<pre className="text-sm whitespace-pre-wrap bg-[var(--background)] rounded p-3 border border-[var(--border)]">
{toolDetail.docs}
</pre>
</div>
)}
</div>
)}
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-4">
Configuration

View File

@@ -1,6 +1,4 @@
import { Link } from "react-router-dom"
import { Settings } from "lucide-react"
import { ComponentGrid } from "@/components/ComponentGrid"
import { ComponentTable } from "@/components/ComponentTable"
import { useComponents, useStatus, useGateway, useEventStream } from "@/services/api/hooks"
export function Dashboard() {
@@ -11,30 +9,22 @@ export function Dashboard() {
return (
<div className="max-w-6xl mx-auto px-6 py-8">
<div className="flex items-start justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">Castle</h1>
<p className="text-[var(--muted)] mt-1">
Personal software platform
{gateway && (
<span className="ml-2 text-sm">
&middot; {gateway.component_count} components &middot; port {gateway.port}
</span>
)}
</p>
</div>
<Link
to="/config"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded bg-[var(--border)] hover:bg-gray-600 text-[var(--foreground)] transition-colors"
>
<Settings size={14} /> Config
</Link>
<div className="mb-8">
<h1 className="text-3xl font-bold">Castle</h1>
<p className="text-[var(--muted)] mt-1">
Personal software platform
{gateway && (
<span className="ml-2 text-sm">
&middot; {gateway.component_count} components &middot; port {gateway.port}
</span>
)}
</p>
</div>
{isLoading ? (
<p className="text-[var(--muted)]">Loading components...</p>
) : components ? (
<ComponentGrid
<ComponentTable
components={components}
statuses={statusResp?.statuses ?? []}
/>

View File

@@ -0,0 +1,42 @@
import { Link } from "react-router-dom"
import { ArrowLeft } from "lucide-react"
import { useTools } from "@/services/api/hooks"
import { ToolCard } from "@/components/ToolCard"
export function ToolsPage() {
const { data: categories, isLoading } = useTools()
return (
<div className="max-w-6xl mx-auto px-6 py-8">
<Link to="/" className="text-[var(--primary)] hover:underline flex items-center gap-1 mb-6">
<ArrowLeft size={16} /> Back
</Link>
<h1 className="text-3xl font-bold mb-2">Tools</h1>
<p className="text-sm text-[var(--muted)] mb-8">
CLI utilities grouped by category. Each tool is installed to PATH via castle and run with uv.
</p>
{isLoading ? (
<p className="text-[var(--muted)]">Loading tools...</p>
) : categories?.length ? (
<div className="space-y-8">
{categories.map((cat) => (
<section key={cat.name}>
<h2 className="text-lg font-semibold mb-3 text-[var(--muted)]">
{cat.name}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{cat.tools.map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
</section>
))}
</div>
) : (
<p className="text-[var(--muted)]">No tools registered.</p>
)}
</div>
)
}

View File

@@ -1,17 +1,12 @@
import { createBrowserRouter } from "react-router-dom"
import { Dashboard } from "@/pages/Dashboard"
import { ComponentDetailPage } from "@/pages/ComponentDetail"
import { ConfigEditorPage } from "@/pages/ConfigEditor"
export const router = createBrowserRouter([
{
path: "/",
element: <Dashboard />,
},
{
path: "/config",
element: <ConfigEditorPage />,
},
{
path: "/:name",
element: <ComponentDetailPage />,

View File

@@ -8,6 +8,8 @@ import type {
GatewayInfo,
ServiceActionResponse,
SSEHealthEvent,
ToolCategory,
ToolDetail,
} from "@/types"
export function useComponents() {
@@ -41,11 +43,66 @@ export function useGateway() {
})
}
async function waitForApi(attempts = 20, interval = 1000): Promise<void> {
for (let i = 0; i < attempts; i++) {
try {
await apiClient.get<{ status: string }>("/health")
return
} catch {
await new Promise((r) => setTimeout(r, interval))
}
}
}
export function useServiceAction() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ name, action }: { name: string; action: string }) =>
apiClient.post<ServiceActionResponse>(`/services/${name}/${action}`),
// SSE health event handles the UI update; no need to refetch here
mutationFn: async ({ name, action }: { name: string; action: string }) => {
try {
return await apiClient.post<ServiceActionResponse>(`/services/${name}/${action}`)
} catch (err) {
// Network error from self-restart killing the connection — expected
if (err instanceof TypeError) {
return { component: name, action, status: "accepted" } as ServiceActionResponse
}
throw err
}
},
onSuccess: async (data) => {
if (data.status === "accepted") {
// API is restarting itself — poll until it's back, then refresh everything
await waitForApi()
qc.invalidateQueries()
}
},
})
}
export function useToolAction() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ name, action }: { name: string; action: "install" | "uninstall" }) =>
apiClient.post<{ component: string; action: string; status: string }>(
`/tools/${name}/${action}`,
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["components"] })
},
})
}
export function useTools() {
return useQuery({
queryKey: ["tools"],
queryFn: () => apiClient.get<ToolCategory[]>("/tools"),
})
}
export function useToolDetail(name: string) {
return useQuery({
queryKey: ["tools", name],
queryFn: () => apiClient.get<ToolDetail>(`/tools/${name}`),
enabled: !!name,
})
}

View File

@@ -7,6 +7,13 @@ export interface ComponentSummary {
health_path: string | null
proxy_path: string | null
managed: boolean
category: string | null
version: string | null
tool_type: string | null
source: string | null
system_dependencies: string[]
schedule: string | null
installed: boolean | null
}
export interface ComponentDetail extends ComponentSummary {
@@ -47,11 +54,23 @@ export interface SSEServiceActionEvent {
status: string
}
export interface ToolInfo {
command: string
description: string
category: string
version: string
export interface ToolSummary {
id: string
description: string | null
category: string | null
source: string | null
tool_type: string | null
version: string | null
runner: string | null
system_dependencies: string[]
script: string
installed: boolean
}
export interface ToolCategory {
name: string
tools: ToolSummary[]
}
export interface ToolDetail extends ToolSummary {
docs: string | null
}