Files
nearxos 0375b20bb4 Enhance MCP development features and introduce skills management
- Added configuration options for requiring human approval before applying LLM-generated MCP patches.
- Updated Docker setup to include skills directory.
- Integrated skills management into the backend, allowing for procedural guides and skill matching.
- Refactored database initialization to apply Alembic migrations.
- Enhanced task approval process to handle MCP patch applications with optional approval.
- Introduced new schemas for skills and updated existing APIs to support skills functionality.

This commit lays the groundwork for improved agent capabilities and better management of MCP development processes.
2026-06-14 22:27:24 +03:00

200 lines
6.6 KiB
TypeScript

"use client";
import { useState } from "react";
import useSWR from "swr";
import AppShell from "@/components/AppShell";
import { PageHeader } from "@/components/ui";
import { api, fetcher } from "@/lib/api";
import { useAuth } from "@/lib/auth";
type SkillSummary = {
id: string;
name: string;
description: string;
priority: number;
rule_ids: string[];
path_hint: string;
};
type SkillsListResponse = {
skills: SkillSummary[];
path_hint: string;
};
type SkillDetail = SkillSummary & {
markdown: string;
match: Record<string, unknown>;
};
export default function SkillsPage() {
const { user } = useAuth();
const isAdmin = user?.role === "admin";
const { data: listData } = useSWR<SkillsListResponse>("/api/skills", fetcher);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [markdown, setMarkdown] = useState<string | null>(null);
const [previewTitle, setPreviewTitle] = useState("VoIP issue");
const [previewIssue, setPreviewIssue] = useState("one-way audio on SIP calls from crew phones");
const [previewRuleId, setPreviewRuleId] = useState("");
const [preview, setPreview] = useState<{ matched: Record<string, unknown>[] } | null>(null);
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [msgKind, setMsgKind] = useState<"ok" | "err">("ok");
const skills = listData?.skills ?? [];
const activeId = selectedId ?? skills[0]?.id ?? null;
const { data: skillData, mutate } = useSWR<SkillDetail>(
activeId ? `/api/skills/${activeId}` : null,
fetcher,
);
const content = markdown ?? skillData?.markdown ?? "";
const selectSkill = (id: string) => {
setSelectedId(id);
setMarkdown(null);
setMsg(null);
};
const save = async () => {
if (!activeId) return;
setSaving(true);
setMsg(null);
try {
await api(`/api/skills/${activeId}`, {
method: "PUT",
body: JSON.stringify({ markdown: content }),
});
setMsgKind("ok");
setMsg("Skill saved — next task run will use it.");
setMarkdown(null);
mutate();
} catch (err: unknown) {
setMsgKind("err");
setMsg(err instanceof Error ? err.message : "Save failed");
} finally {
setSaving(false);
}
};
const runPreview = async () => {
setPreview(null);
try {
const body: Record<string, string> = {
title: previewTitle,
issue: previewIssue,
};
if (previewRuleId.trim()) body.matched_rule_id = previewRuleId.trim();
const res = await api<{ matched: Record<string, unknown>[] }>("/api/skills/preview", {
method: "POST",
body: JSON.stringify(body),
});
setPreview(res);
} catch (err: unknown) {
setMsgKind("err");
setMsg(err instanceof Error ? err.message : "Preview failed");
}
};
return (
<AppShell>
<PageHeader
title="Agent skills"
subtitle="Procedural guides injected into triage and reasoning — complements troubleshooting rules"
/>
<div className="mb-6 grid gap-4 lg:grid-cols-2">
<div className="card">
<h3 className="mb-2 text-sm font-semibold">Skill preview</h3>
<p className="mb-3 text-xs text-muted">Test which skills match a task title + issue.</p>
<input
className="input mb-2 w-full"
value={previewTitle}
onChange={(e) => setPreviewTitle(e.target.value)}
placeholder="Task title"
/>
<textarea
className="input mb-2 min-h-[80px] w-full"
value={previewIssue}
onChange={(e) => setPreviewIssue(e.target.value)}
placeholder="Issue description"
/>
<input
className="input mb-2 w-full"
value={previewRuleId}
onChange={(e) => setPreviewRuleId(e.target.value)}
placeholder="Optional matched rule id (e.g. one-way-audio)"
/>
<button type="button" className="btn-secondary" onClick={runPreview}>
Preview match
</button>
{preview?.matched && preview.matched.length > 0 && (
<pre className="mt-3 overflow-x-auto rounded-lg bg-panel2 p-3 text-xs">
{JSON.stringify(preview.matched, null, 2)}
</pre>
)}
{preview && preview.matched.length === 0 && (
<p className="mt-3 text-sm text-muted">No skills matched.</p>
)}
</div>
<div className="card">
<h3 className="mb-2 text-sm font-semibold">Defined skills</h3>
<ul className="space-y-2 text-sm">
{skills.map((s) => (
<li key={s.id}>
<button
type="button"
onClick={() => selectSkill(s.id)}
className={`flex w-full justify-between gap-2 border-b border-edge/50 py-2 text-left transition ${
activeId === s.id ? "text-accent2" : "hover:text-white"
}`}
>
<span>{s.name}</span>
<span className="text-xs text-muted">priority {s.priority}</span>
</button>
{s.rule_ids.length > 0 && (
<p className="pb-2 text-[11px] text-muted">Rules: {s.rule_ids.join(", ")}</p>
)}
</li>
))}
</ul>
<p className="mt-3 text-xs text-muted">Directory: {listData?.path_hint}</p>
</div>
</div>
{activeId && (
<div className="card">
<div className="mb-2 flex items-center justify-between">
<h3 className="text-sm font-semibold">
{skillData?.name ?? activeId}{" "}
<span className="font-normal text-muted">({skillData?.path_hint})</span>
</h3>
{!isAdmin && <span className="text-xs text-muted">Read-only (admin can edit)</span>}
</div>
{skillData?.description && (
<p className="mb-3 text-xs text-muted">{skillData.description}</p>
)}
<textarea
className="input min-h-[420px] w-full font-mono text-xs leading-relaxed"
value={content}
onChange={(e) => isAdmin && setMarkdown(e.target.value)}
readOnly={!isAdmin}
spellCheck={false}
/>
{msg && (
<p className={`mt-2 text-sm ${msgKind === "ok" ? "text-good" : "text-bad"}`}>{msg}</p>
)}
{isAdmin && (
<button type="button" className="btn-primary mt-4" disabled={saving} onClick={save}>
{saving ? "Saving…" : "Save skill"}
</button>
)}
</div>
)}
</AppShell>
);
}