Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7c68586
feat(ui): add Hermes Agent adapter to frontend
HenkDz Mar 25, 2026
17fca05
chore: switch to scoped @henkey/hermes-paperclip-adapter v0.1.3
HenkDz Mar 25, 2026
f7833f8
feat(ui): Hermes adapter polish - tool grouping, stderr accordion, cl…
HenkDz Mar 25, 2026
d2c3a04
feat: add skill sync + unmanaged skills accordion
HenkDz Mar 25, 2026
e0c668c
feat(ui): replace Zap icon with custom HermesIcon caduceus for hermes…
HenkDz Mar 26, 2026
8064e40
feat: add Hermes detectModel, creatable model input, and UI improvements
HenkDz Mar 26, 2026
27859fd
fix(ui): show 'hermes' placeholder in command input for hermes_local …
HenkDz Mar 26, 2026
d47e4c7
feat: migrate Hermes adapter in-tree as @paperclipai/adapter-hermes-l…
HenkDz Mar 26, 2026
8f6b60f
fix: address PR #1867 review comments
HenkDz Mar 27, 2026
ab7fc7c
fix: revert packageManager to pnpm@9.15.4 to match CI config
HenkDz Mar 27, 2026
b16c293
fix: add hermes-local to Dockerfile deps stage
HenkDz Mar 27, 2026
3aa1e9f
fix(ui): preserve adapter-agnostic fields when switching adapter type
HenkDz Mar 27, 2026
a2b77f4
fix(hermes-local): check config.env for API keys in test environment
HenkDz Mar 27, 2026
93dd1b6
feat: improve Hermes model selection UX
HenkDz Mar 27, 2026
0098b84
fix: mark Hermes model dropdown icons decorative
HenkDz Mar 27, 2026
76b9d19
fix(hermes-local): remove DEFAULT_MODEL fallback, honour empty model …
HenkDz Mar 27, 2026
d9facca
feat(adapter-utils): add detectModel? to ServerAdapterModule, clean u…
HenkDz Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ COPY packages/adapters/claude-local/package.json packages/adapters/claude-local/
COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/
COPY packages/adapters/hermes-local/package.json packages/adapters/hermes-local/
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
Expand Down
6 changes: 6 additions & 0 deletions packages/adapter-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ export interface ServerAdapterModule {
* without knowing provider-specific credential paths or API shapes.
*/
getQuotaWindows?: () => Promise<ProviderQuotaResult>;
/**
* Optional: detect the currently configured model from local config files.
* Returns the detected model/provider and the config source, or null if
* the adapter does not support detection or no config is found.
*/
detectModel?: () => Promise<{ model: string; provider: string; source: string } | null>;
}

// ---------------------------------------------------------------------------
Expand Down
61 changes: 61 additions & 0 deletions packages/adapters/hermes-local/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@paperclipai/adapter-hermes-local",
"version": "0.3.1",
"license": "MIT",
"homepage": "https://github.com/paperclipai/paperclip",
"bugs": {
"url": "https://github.com/paperclipai/paperclip/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/paperclipai/paperclip",
"directory": "packages/adapters/hermes-local"
},
"type": "module",
"exports": {
".": "./src/index.ts",
"./server": "./src/server/index.ts",
"./ui": "./src/ui/index.ts",
"./cli": "./src/cli/index.ts"
},
"publishConfig": {
"access": "public",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
},
"./ui": {
"types": "./dist/ui/index.d.ts",
"import": "./dist/ui/index.js"
},
"./cli": {
"types": "./dist/cli/index.d.ts",
"import": "./dist/cli/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"files": [
"dist",
"skills"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@paperclipai/adapter-utils": "workspace:*",
"picocolors": "^1.1.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.3"
}
}
61 changes: 61 additions & 0 deletions packages/adapters/hermes-local/src/cli/format-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* CLI output formatting for Hermes Agent adapter.
*
* Pretty-prints Hermes output lines in the terminal when running
* Paperclip's CLI tools.
*/

import pc from "picocolors";

/**
* Format a Hermes Agent stdout event for terminal display.
*
* @param raw Raw stdout line from Hermes
* @param debug If true, show extra metadata with color coding
*/
export function printHermesStreamEvent(raw: string, debug: boolean): void {
const line = raw.trim();
if (!line) return;

if (!debug) {
console.log(line);
return;
}

// Adapter log lines
if (line.startsWith("[hermes]")) {
console.log(pc.blue(line));
return;
}

// Tool output (┊ prefix)
if (line.startsWith("┊")) {
console.log(pc.cyan(line));
return;
}

// Thinking
if (line.includes("💭") || line.startsWith("<thinking>")) {
console.log(pc.dim(line));
return;
}

// Errors
if (
line.startsWith("Error:") ||
line.startsWith("ERROR:") ||
line.startsWith("Traceback")
) {
console.log(pc.red(line));
return;
}

// Session info
if (/session/i.test(line) && /id|saved|resumed/i.test(line)) {
console.log(pc.green(line));
return;
}

// Default: gray in debug mode
console.log(pc.gray(line));
}
5 changes: 5 additions & 0 deletions packages/adapters/hermes-local/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* CLI module exports — used by Paperclip's CLI for terminal formatting.
*/

export { printHermesStreamEvent } from "./format-event.js";
83 changes: 83 additions & 0 deletions packages/adapters/hermes-local/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Hermes Agent adapter for Paperclip.
*
* Runs Hermes Agent (https://github.com/NousResearch/hermes-agent)
* as a managed employee in a Paperclip company. Hermes Agent is a
* full-featured AI agent with 30+ native tools, persistent memory,
* skills, session persistence, and MCP support.
*
* @packageDocumentation
*/

import { ADAPTER_TYPE, ADAPTER_LABEL } from "./shared/constants.js";

export const type = ADAPTER_TYPE;
export const label = ADAPTER_LABEL;

/**
* Models available through Hermes Agent.
*
* Hermes supports any model via any provider — the list is empty
* and the UI uses detectModel() + free-text input instead.
*/
export const models: { id: string; label: string }[] = [];

/**
* Documentation shown in the Paperclip UI when configuring a Hermes agent.
*/
export const agentConfigurationDoc = `# Hermes Agent Configuration

Hermes Agent is a full-featured AI agent by Nous Research with 30+ native
tools, persistent memory, session persistence, skills, and MCP support.

## Prerequisites

- Python 3.10+ installed
- Hermes Agent installed: \`pip install hermes-agent\`
- At least one LLM API key configured in ~/.hermes/.env

## Core Configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| model | string | anthropic/claude-sonnet-4 | Model to use (provider/model format) |
| provider | string | (auto) | API provider: auto, openrouter, nous, openai-codex, zai, kimi-coding, minimax, minimax-cn. Usually not needed — Hermes auto-detects from model name. |
| timeoutSec | number | 300 | Execution timeout in seconds |
| graceSec | number | 10 | Grace period after SIGTERM before SIGKILL |

## Tool Configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| toolsets | string | (all) | Comma-separated toolsets to enable (e.g. "terminal,file,web") |

## Session & Workspace

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| persistSession | boolean | true | Resume sessions across heartbeats |
| worktreeMode | boolean | false | Use git worktree for isolated changes |
| checkpoints | boolean | false | Enable filesystem checkpoints |

## Advanced

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| hermesCommand | string | hermes | Path to hermes CLI binary |
| verbose | boolean | false | Enable verbose output |
| extraArgs | string[] | [] | Additional CLI arguments |
| env | object | {} | Extra environment variables |
| promptTemplate | string | (default) | Custom prompt template with {{variable}} placeholders |

## Available Template Variables

- \`{{agentId}}\` — Paperclip agent ID
- \`{{agentName}}\` — Agent display name
- \`{{companyId}}\` — Paperclip company ID
- \`{{companyName}}\` — Company display name
- \`{{runId}}\` — Current heartbeat run ID
- \`{{taskId}}\` — Current task/issue ID (if assigned)
- \`{{taskTitle}}\` — Task title (if assigned)
- \`{{taskBody}}\` — Task description (if assigned)
- \`{{projectName}}\` — Project name (if scoped to a project)
`;
77 changes: 77 additions & 0 deletions packages/adapters/hermes-local/src/server/detect-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Detect the current model from the user's Hermes config.
*
* Reads ~/.hermes/config.yaml and extracts the default model
* and provider settings.
*/

import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { homedir } from "node:os";

export interface DetectedModel {
model: string;
provider: string;
source: "config";
}

/**
* Read the Hermes config file and extract the default model.
*/
export async function detectModel(
configPath?: string,
): Promise<DetectedModel | null> {
const filePath = configPath ?? join(homedir(), ".hermes", "config.yaml");

let content: string;
try {
content = await readFile(filePath, "utf-8");
} catch {
return null;
}

return parseModelFromConfig(content);
}

/**
* Parse model.default and model.provider from raw YAML content.
* Uses simple regex parsing to avoid a YAML dependency.
*/
export function parseModelFromConfig(content: string): DetectedModel | null {
const lines = content.split("\n");
let model = "";
let provider = "";
let inModelSection = false;
let modelSectionIndent = 0;

for (const line of lines) {
const trimmed = line.trimEnd();
const indent = line.length - line.trimStart().length;

// Track model: section (indent 0)
if (/^model:\s*$/.test(trimmed) && indent === 0) {
inModelSection = true;
modelSectionIndent = 0;
continue;
}

// We left the model section if indent drops back to the section level or below
if (inModelSection && indent <= modelSectionIndent && trimmed && !trimmed.startsWith("#")) {
inModelSection = false;
}

if (inModelSection) {
const match = trimmed.match(/^\s*(\w+)\s*:\s*(.+)$/);
if (match) {
const key = match[1];
const val = match[2].trim().replace(/#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
if (key === "default") model = val;
if (key === "provider") provider = val;
}
}
}

if (!model) return null;

return { model, provider, source: "config" };
}
Loading
Loading