feat(hermes): upgrade hermes-paperclip-adapter + UI adapter + skills + detectModel

Upgrades hermes-paperclip-adapter from 0.1.1 to ^0.2.0 and wires in all new
capabilities introduced in v0.2.0:

Server
- Upgrade hermes-paperclip-adapter 0.1.1 -> ^0.2.0 (pending PR#10 merge)
- Wire listSkills + syncSkills from hermes-paperclip-adapter/server
- Add detectModel to hermesLocalAdapter (reads ~/.hermes/config.yaml)
- Add detectAdapterModel() function + /adapters/:type/detect-model route
- Export detectAdapterModel from server/src/adapters/index.ts

Types
- Add optional detectModel? to ServerAdapterModule in adapter-utils

UI
- Add hermes-paperclip-adapter ^0.2.0 to ui/package.json (for /ui exports)
- New ui/src/adapters/hermes-local/ — config fields + UI adapter module
- Register hermesLocalUIAdapter in UI adapter registry
- New HermesIcon (caduceus SVG) for adapter pickers
- AgentConfigForm: detect-model button, creatable model input, preserve
  adapter-agnostic fields (env, promptTemplate) when switching adapter type
- NewAgentDialog + OnboardingWizard: add Hermes to adapter picker
- Agents, OrgChart, InviteLanding, NewAgent, agent-config-primitives: add
  hermes_local label + enable in adapter sets
- AgentDetail: smarter run summary excerpt extraction
- RunTranscriptView: improved Hermes stdout rendering

NOTE: requires hermes-paperclip-adapter@0.2.0 on npm.
      Blocked on NousResearch/hermes-paperclip-adapter#10 merging.
This commit is contained in:
HenkDz 2026-03-28 01:34:48 +01:00
parent 0ac01a04e5
commit 1583a2d65a
22 changed files with 634 additions and 33 deletions

View file

@ -1075,10 +1075,28 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
const isLive = run.status === "running" || run.status === "queued";
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
const StatusIcon = statusInfo.icon;
const summary = run.resultJson
const summaryRaw = run.resultJson
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
: run.error ?? "";
// Extract a clean 2-3 line excerpt: first non-empty, non-header, non-list-mark lines
const summary = useMemo(() => {
if (!summaryRaw) return "";
const lines = summaryRaw
.replace(/^#{1,6}\s+/gm, "")
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0 && !l.startsWith("---") && !l.startsWith("|") && !l.startsWith("```"));
const excerpt: string[] = [];
let chars = 0;
for (const line of lines) {
if (excerpt.length >= 3 || chars + line.length > 280) break;
excerpt.push(line);
chars += line.length;
}
return excerpt.join(" ");
}, [summaryRaw]);
return (
<div className="space-y-3">
<div className="flex w-full items-center justify-between">
@ -2351,6 +2369,7 @@ function AgentSkillsTab({
const queryClient = useQueryClient();
const [skillDraft, setSkillDraft] = useState<string[]>([]);
const [lastSavedSkills, setLastSavedSkills] = useState<string[]>([]);
const [unmanagedOpen, setUnmanagedOpen] = useState(false);
const lastSavedSkillsRef = useRef<string[]>([]);
const hasHydratedSkillSnapshotRef = useRef(false);
const skipNextSkillAutosaveRef = useRef(true);
@ -2680,12 +2699,19 @@ function AgentSkillsTab({
{unmanagedSkillRows.length > 0 && (
<section className="border-y border-border">
<div className="border-b border-border bg-muted/40 px-3 py-2">
<div
role="button"
tabIndex={0}
className="flex cursor-pointer items-center gap-2 border-b border-border bg-muted/40 px-3 py-2 select-none"
onClick={() => setUnmanagedOpen((v) => !v)}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setUnmanagedOpen((v) => !v); } }}
>
<span className="text-xs font-medium text-muted-foreground">
User-installed skills, not managed by Paperclip
({unmanagedSkillRows.length}) User-installed skills, not managed by Paperclip
</span>
{unmanagedOpen ? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" /> : <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />}
</div>
{unmanagedSkillRows.map(renderSkillRow)}
{unmanagedOpen && unmanagedSkillRows.map(renderSkillRow)}
</section>
)}
</>

View file

@ -26,6 +26,7 @@ const adapterLabels: Record<string, string> = {
gemini_local: "Gemini",
opencode_local: "OpenCode",
cursor: "Cursor",
hermes_local: "Hermes",
openclaw_gateway: "OpenClaw Gateway",
process: "Process",
http: "HTTP",

View file

@ -20,11 +20,12 @@ const adapterLabels: Record<string, string> = {
pi_local: "Pi (local)",
openclaw_gateway: "OpenClaw Gateway",
cursor: "Cursor (local)",
hermes_local: "Hermes Agent",
process: "Process",
http: "HTTP",
};
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor"]);
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor", "hermes_local"]);
function dateTime(value: string) {
return new Date(value).toLocaleString();

View file

@ -35,6 +35,7 @@ const SUPPORTED_ADVANCED_ADAPTER_TYPES = new Set<CreateConfigValues["adapterType
"opencode_local",
"pi_local",
"cursor",
"hermes_local",
"openclaw_gateway",
]);

View file

@ -122,6 +122,7 @@ const adapterLabels: Record<string, string> = {
gemini_local: "Gemini",
opencode_local: "OpenCode",
cursor: "Cursor",
hermes_local: "Hermes",
openclaw_gateway: "OpenClaw Gateway",
process: "Process",
http: "HTTP",