1ba81a7cd0
Plugin `tools[]` overlapped with MCP, kandev's established mechanism for exposing tools to agents (`internal/mcp/`), and the `InvokeTool` path was never wired into agent tool sets — the `listTools` handler was listing-only. Rather than maintain a half-built parallel surface, remove it. If plugins ever contribute agent tools, they should feed through MCP, not a separate `InvokeTool` RPC. Removes: - proto `InvokeTool` RPC + `ToolRequest`/`ToolResponse`/`ToolContext` messages (regenerated `*.pb.go` / `*_grpc.pb.go`) - pkg/pluginsdk `InvokeTool` from the `Plugin` interface and `UnimplementedPlugin`, the Tool* Go-native types, and the `RemotePlugin`/`GRPCServer` dispatch - internal/plugins `Service.InvokeTool`, the `listTools` handler + `GET /api/plugins/tools` route, `PluginToolDTO`, and the manifest `Tools` field + tool-name uniqueness validation - frontend `PluginTool`/`PluginToolDTO` types, `listPluginTools`, and the manifest-card tools list - fixture/e2e `InvokeTool` echo impl and the `tools:` manifest block Test doubles that used the "crash" tool to exercise crash-restart now use a "crash" webhook key instead. Internal specs (GRPC-CONTRACT, spec) drop the tool RPC/feature; docs/public was already updated in #1774. Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Badge } from "@kandev/ui/badge";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
|
|
import type { PluginRecord } from "@/lib/types/plugins";
|
|
|
|
/**
|
|
* Read-only view of the plugin's manifest: identity, capabilities, declared
|
|
* webhooks, and a collapsible raw JSON dump for anything the summary
|
|
* rows don't surface.
|
|
*/
|
|
export function PluginManifestCard({ plugin }: { plugin: PluginRecord }) {
|
|
const [showRaw, setShowRaw] = useState(false);
|
|
|
|
return (
|
|
<Card data-testid="plugin-manifest-card">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle className="text-base">Manifest</CardTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="cursor-pointer"
|
|
onClick={() => setShowRaw((v) => !v)}
|
|
>
|
|
{showRaw ? "Hide raw" : "View raw"}
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
|
<ManifestRow label="ID" value={plugin.id} mono />
|
|
<ManifestRow label="Version" value={plugin.version} mono />
|
|
<ManifestRow label="API version" value={String(plugin.api_version)} />
|
|
<ManifestRow label="Author" value={plugin.author || "—"} />
|
|
<ManifestRow label="Signed" value={plugin.signed ? "yes" : "no"} />
|
|
<ManifestRow label="Installed" value={formatInstalledAt(plugin.installed_at)} />
|
|
</div>
|
|
|
|
<CapabilityBadges plugin={plugin} />
|
|
<DeclarationList
|
|
label="Webhooks"
|
|
items={(plugin.webhooks ?? []).map((w) => ({ key: w.key, text: w.key }))}
|
|
/>
|
|
|
|
{showRaw && (
|
|
<pre
|
|
data-testid="plugin-manifest-raw"
|
|
className="rounded-md border border-border/70 bg-muted/40 p-3 text-xs overflow-x-auto"
|
|
>
|
|
{JSON.stringify(manifestOnly(plugin), null, 2)}
|
|
</pre>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function ManifestRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
return (
|
|
<div className="flex items-baseline justify-between gap-3 sm:justify-start">
|
|
<span className="text-muted-foreground w-28 shrink-0">{label}</span>
|
|
<span className={mono ? "font-mono text-xs truncate" : "truncate"}>{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CapabilityBadges({ plugin }: { plugin: PluginRecord }) {
|
|
const caps = plugin.capabilities ?? {};
|
|
const badges: string[] = [
|
|
...(caps.events ?? []).map((e) => `events:${e}`),
|
|
...(caps.api_read ?? []).map((r) => `read:${r}`),
|
|
...(caps.api_write ?? []).map((w) => `write:${w}`),
|
|
...(caps.state ? ["state"] : []),
|
|
...(caps.secrets ? ["secrets"] : []),
|
|
];
|
|
if (badges.length === 0) return null;
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<div className="text-sm text-muted-foreground">Capabilities</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{badges.map((badge) => (
|
|
<Badge key={badge} variant="secondary" className="text-[11px] font-mono">
|
|
{badge}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DeclarationList({
|
|
label,
|
|
items,
|
|
}: {
|
|
label: string;
|
|
items: { key: string; text: string }[];
|
|
}) {
|
|
if (items.length === 0) return null;
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<div className="text-sm text-muted-foreground">{label}</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{items.map((item) => (
|
|
<Badge key={item.key} variant="outline" className="text-[11px]">
|
|
{item.text}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** The manifest-declared fields only — runtime bookkeeping (status, install
|
|
* path, restart counter) is shown elsewhere and just noise here. */
|
|
function manifestOnly(plugin: PluginRecord): Record<string, unknown> {
|
|
const { status: _s, install_path: _i, restart_count: _r, installed_at: _a, ...manifest } = plugin;
|
|
return manifest;
|
|
}
|
|
|
|
function formatInstalledAt(installedAt: string): string {
|
|
const date = new Date(installedAt);
|
|
return Number.isNaN(date.getTime()) ? installedAt : date.toLocaleString();
|
|
}
|