feat(adapters): external adapter plugin system with dynamic UI parser

- Plugin loader: install/reload/remove/reinstall external adapters
  from npm packages or local directories
- Plugin store persisted at ~/.paperclip/adapter-plugins.json
- Self-healing UI parser resolution with version caching
- UI: Adapter Manager page, dynamic loader, display registry
  with humanized names for unknown adapter types
- Dev watch: exclude adapter-plugins dir from tsx watcher
  to prevent mid-request server restarts during reinstall
- All consumer fallbacks use getAdapterLabel() for consistent display
- AdapterTypeDropdown uses controlled open state for proper close behavior
- Remove hermes-local from built-in UI (externalized to plugin)
- Add docs for external adapters and UI parser contract
This commit is contained in:
HenkDz 2026-03-31 20:21:13 +01:00
parent f8452a4520
commit 14d59da316
72 changed files with 4102 additions and 585 deletions

View file

@ -7,6 +7,12 @@ function readNumericField(record: Record<string, unknown>, key: string) {
return key in record ? record[key] ?? null : undefined;
}
function readCommentText(value: unknown) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
export function summarizeHeartbeatRunResultJson(
resultJson: Record<string, unknown> | null | undefined,
): Record<string, unknown> | null {
@ -33,3 +39,18 @@ export function summarizeHeartbeatRunResultJson(
return Object.keys(summary).length > 0 ? summary : null;
}
export function buildHeartbeatRunIssueComment(
resultJson: Record<string, unknown> | null | undefined,
): string | null {
if (!resultJson || typeof resultJson !== "object" || Array.isArray(resultJson)) {
return null;
}
return (
readCommentText(resultJson.summary)
?? readCommentText(resultJson.result)
?? readCommentText(resultJson.message)
?? null
);
}