diff --git a/docs/adapters/overview.md b/docs/adapters/overview.md index 55fd4d4e..dfb4b21f 100644 --- a/docs/adapters/overview.md +++ b/docs/adapters/overview.md @@ -24,6 +24,7 @@ When a heartbeat fires, Paperclip: | OpenCode Local | `opencode_local` | Runs OpenCode CLI locally (multi-provider `provider/model`) | | Cursor | `cursor` | Runs Cursor in background mode | | Pi Local | `pi_local` | Runs an embedded Pi agent locally | +| Hermes Local | `hermes_local` | Runs Hermes CLI locally (`hermes-paperclip-adapter`) | | OpenClaw Gateway | `openclaw_gateway` | Connects to an OpenClaw gateway endpoint | | [Process](/adapters/process) | `process` | Executes arbitrary shell commands | | [HTTP](/adapters/http) | `http` | Sends webhooks to external agents | @@ -35,7 +36,6 @@ These adapters ship as standalone npm packages and are installed via the plugin | Adapter | Package | Type Key | Description | |---------|---------|----------|-------------| | Droid Local | `@henkey/droid-paperclip-adapter` | `droid_local` | Runs Factory Droid locally | -| Hermes Local | `@henkey/hermes-paperclip-adapter` | `hermes_local` | Runs Hermes CLI locally | ## External Adapters @@ -78,7 +78,7 @@ my-adapter/ ## Choosing an Adapter -- **Need a coding agent?** Use `claude_local`, `codex_local`, `opencode_local`, or install `droid_local` / `hermes_local` as external plugins +- **Need a coding agent?** Use `claude_local`, `codex_local`, `opencode_local`, `hermes_local`, or install `droid_local` as an external plugin - **Need to run a script or command?** Use `process` - **Need to call an external service?** Use `http` - **Need something custom?** [Create your own adapter](/adapters/creating-an-adapter) or [build an external adapter plugin](/adapters/external-adapters) diff --git a/docs/agents-runtime.md b/docs/agents-runtime.md index dffb6fe2..cafc39e9 100644 --- a/docs/agents-runtime.md +++ b/docs/agents-runtime.md @@ -39,6 +39,7 @@ Built-in adapters: - `opencode_local`: runs your local `opencode` CLI - `cursor`: runs Cursor in background mode - `pi_local`: runs an embedded Pi agent locally +- `hermes_local`: runs your local `hermes` CLI (`hermes-paperclip-adapter`) - `openclaw_gateway`: connects to an OpenClaw gateway endpoint - `process`: generic shell command adapter - `http`: calls an external HTTP endpoint @@ -46,9 +47,8 @@ Built-in adapters: External plugin adapters (install via the adapter manager or API): - `droid_local`: runs your local Factory Droid CLI (`@henkey/droid-paperclip-adapter`) -- `hermes_local`: runs your local `hermes` CLI (`@henkey/hermes-paperclip-adapter`) -For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `droid_local`, `hermes_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine. +For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `droid_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine. ## 3.2 Runtime behavior @@ -177,7 +177,7 @@ Start with least privilege where possible, and avoid exposing secrets in broad r ## 10. Minimal setup checklist -1. Choose adapter (e.g. `claude_local`, `codex_local`, `opencode_local`, `cursor`, or `openclaw_gateway`). External plugins like `droid_local` and `hermes_local` are also available via the adapter manager. +1. Choose adapter (e.g. `claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `cursor`, or `openclaw_gateway`). External plugins like `droid_local` are also available via the adapter manager. 2. Set `cwd` to the target workspace (for local adapters). 3. Optionally add a prompt template (`promptTemplate`) or use the managed instructions bundle. 4. Configure heartbeat policy (timer and/or assignment wakeups). diff --git a/server/package.json b/server/package.json index b519ed1a..4b95606a 100644 --- a/server/package.json +++ b/server/package.json @@ -66,6 +66,7 @@ "drizzle-orm": "^0.38.4", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", + "hermes-paperclip-adapter": "^0.2.0", "jsdom": "^28.1.0", "multer": "^2.0.2", "open": "^11.0.0", diff --git a/server/src/adapters/builtin-adapter-types.ts b/server/src/adapters/builtin-adapter-types.ts new file mode 100644 index 00000000..463a5694 --- /dev/null +++ b/server/src/adapters/builtin-adapter-types.ts @@ -0,0 +1,15 @@ +/** + * Adapter types shipped with Paperclip. External plugins must not replace these. + */ +export const BUILTIN_ADAPTER_TYPES = new Set([ + "claude_local", + "codex_local", + "cursor", + "gemini_local", + "openclaw_gateway", + "opencode_local", + "pi_local", + "hermes_local", + "process", + "http", +]); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 35ae45c5..aa8ddeb4 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -67,6 +67,21 @@ import { import { agentConfigurationDoc as piAgentConfigurationDoc, } from "@paperclipai/adapter-pi-local"; +import { + execute as hermesExecute, + testEnvironment as hermesTestEnvironment, + sessionCodec as hermesSessionCodec, + listSkills as hermesListSkills, + syncSkills as hermesSyncSkills, + detectModel as detectModelFromHermes, +} from "hermes-paperclip-adapter/server"; +import { + agentConfigurationDoc as hermesAgentConfigurationDoc, + models as hermesModels, +} from "hermes-paperclip-adapter"; +import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js"; +import { buildExternalAdapters } from "./plugin-loader.js"; +import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; @@ -163,6 +178,19 @@ const piLocalAdapter: ServerAdapterModule = { agentConfigurationDoc: piAgentConfigurationDoc, }; +const hermesLocalAdapter: ServerAdapterModule = { + type: "hermes_local", + execute: hermesExecute, + testEnvironment: hermesTestEnvironment, + sessionCodec: hermesSessionCodec, + listSkills: hermesListSkills, + syncSkills: hermesSyncSkills, + models: hermesModels, + supportsLocalAgentJwt: true, + agentConfigurationDoc: hermesAgentConfigurationDoc, + detectModel: () => detectModelFromHermes(), +}; + const adaptersByType = new Map(); function registerBuiltInAdapters() { @@ -174,6 +202,7 @@ function registerBuiltInAdapters() { cursorLocalAdapter, geminiLocalAdapter, openclawGatewayAdapter, + hermesLocalAdapter, processAdapter, httpAdapter, ]) { @@ -184,15 +213,12 @@ function registerBuiltInAdapters() { registerBuiltInAdapters(); // --------------------------------------------------------------------------- -// Load external adapter plugins (droid, hermes, etc.) +// Load external adapter plugins (e.g. droid_local) // // External adapter packages export createServerAdapter() which returns a // ServerAdapterModule. The host fills in sessionManagement. // --------------------------------------------------------------------------- -import { buildExternalAdapters } from "./plugin-loader.js"; -import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js"; - /** Cached sync wrapper — the store is a simple JSON file read, safe to call frequently. */ function getDisabledAdapterTypesFromStore(): string[] { return getDisabledAdapterTypes(); @@ -208,6 +234,12 @@ const externalAdaptersReady: Promise = (async () => { try { const externalAdapters = await buildExternalAdapters(); for (const externalAdapter of externalAdapters) { + if (BUILTIN_ADAPTER_TYPES.has(externalAdapter.type)) { + console.warn( + `[paperclip] Skipping external adapter "${externalAdapter.type}" — conflicts with built-in adapter`, + ); + continue; + } adaptersByType.set( externalAdapter.type, { diff --git a/server/src/routes/adapters.ts b/server/src/routes/adapters.ts index c50d5581..579ed31d 100644 --- a/server/src/routes/adapters.ts +++ b/server/src/routes/adapters.ts @@ -39,25 +39,10 @@ import type { ServerAdapterModule } from "../adapters/types.js"; import { loadExternalAdapterPackage, getUiParserSource, getOrExtractUiParserSource, reloadExternalAdapter } from "../adapters/plugin-loader.js"; import { logger } from "../middleware/logger.js"; import { assertBoard } from "./authz.js"; +import { BUILTIN_ADAPTER_TYPES } from "../adapters/builtin-adapter-types.js"; const execFileAsync = promisify(execFile); -// --------------------------------------------------------------------------- -// Known built-in adapter types (cannot be removed via the API) -// --------------------------------------------------------------------------- - -const BUILTIN_ADAPTER_TYPES = new Set([ - "claude_local", - "codex_local", - "cursor", - "gemini_local", - "openclaw_gateway", - "opencode_local", - "pi_local", - "process", - "http", -]); - // --------------------------------------------------------------------------- // Request / Response types // --------------------------------------------------------------------------- diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 36a87d63..8bf2a104 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -76,6 +76,7 @@ export function agentRoutes(db: Db) { codex_local: "instructionsFilePath", droid_local: "instructionsFilePath", gemini_local: "instructionsFilePath", + hermes_local: "instructionsFilePath", opencode_local: "instructionsFilePath", cursor: "instructionsFilePath", pi_local: "instructionsFilePath", diff --git a/ui/src/adapters/adapter-display-registry.ts b/ui/src/adapters/adapter-display-registry.ts index cbaae011..fe809273 100644 --- a/ui/src/adapters/adapter-display-registry.ts +++ b/ui/src/adapters/adapter-display-registry.ts @@ -16,6 +16,7 @@ import { Cpu, } from "lucide-react"; import { OpenCodeLogoIcon } from "@/components/OpenCodeLogoIcon"; +import { HermesIcon } from "@/components/HermesIcon"; // --------------------------------------------------------------------------- // Type suffix parsing @@ -73,6 +74,11 @@ const adapterDisplayMap: Record = { description: "Local multi-provider agent", icon: OpenCodeLogoIcon, }, + hermes_local: { + label: "Hermes Agent", + description: "Local Hermes CLI agent", + icon: HermesIcon, + }, pi_local: { label: "Pi", description: "Local Pi agent", diff --git a/ui/src/adapters/hermes-local/config-fields.tsx b/ui/src/adapters/hermes-local/config-fields.tsx new file mode 100644 index 00000000..4b807043 --- /dev/null +++ b/ui/src/adapters/hermes-local/config-fields.tsx @@ -0,0 +1,49 @@ +import type { AdapterConfigFieldsProps } from "../types"; +import { + Field, + DraftInput, +} from "../../components/agent-config-primitives"; +import { ChoosePathButton } from "../../components/PathInstructionsModal"; + +const inputClass = + "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; +const instructionsFileHint = + "Absolute path to a markdown file (e.g. AGENTS.md) that defines this agent's behavior. Injected into the system prompt at runtime."; + +export function HermesLocalConfigFields({ + isCreate, + values, + set, + config, + eff, + mark, + hideInstructionsFile, +}: AdapterConfigFieldsProps) { + if (hideInstructionsFile) return null; + return ( + +
+ + isCreate + ? set!({ instructionsFilePath: v }) + : mark("adapterConfig", "instructionsFilePath", v || undefined) + } + immediate + className={inputClass} + placeholder="/absolute/path/to/AGENTS.md" + /> + +
+
+ ); +} diff --git a/ui/src/adapters/hermes-local/index.ts b/ui/src/adapters/hermes-local/index.ts new file mode 100644 index 00000000..93d9ed6a --- /dev/null +++ b/ui/src/adapters/hermes-local/index.ts @@ -0,0 +1,12 @@ +import type { UIAdapterModule } from "../types"; +import { parseHermesStdoutLine } from "hermes-paperclip-adapter/ui"; +import { HermesLocalConfigFields } from "./config-fields"; +import { buildHermesConfig } from "hermes-paperclip-adapter/ui"; + +export const hermesLocalUIAdapter: UIAdapterModule = { + type: "hermes_local", + label: "Hermes Agent", + parseStdoutLine: parseHermesStdoutLine, + ConfigFields: HermesLocalConfigFields, + buildAdapterConfig: buildHermesConfig, +}; diff --git a/ui/src/adapters/registry.ts b/ui/src/adapters/registry.ts index a27e7316..8c48ecbc 100644 --- a/ui/src/adapters/registry.ts +++ b/ui/src/adapters/registry.ts @@ -6,6 +6,7 @@ import { geminiLocalUIAdapter } from "./gemini-local"; import { openCodeLocalUIAdapter } from "./opencode-local"; import { piLocalUIAdapter } from "./pi-local"; import { openClawGatewayUIAdapter } from "./openclaw-gateway"; +import { hermesLocalUIAdapter } from "./hermes-local"; import { processUIAdapter } from "./process"; import { httpUIAdapter } from "./http"; import { loadDynamicParser } from "./dynamic-loader"; @@ -18,6 +19,7 @@ function registerBuiltInUIAdapters() { claudeLocalUIAdapter, codexLocalUIAdapter, geminiLocalUIAdapter, + hermesLocalUIAdapter, openCodeLocalUIAdapter, piLocalUIAdapter, cursorLocalUIAdapter,