feat: add Hermes integration

- Add new installHermesIntegration to write ~/.hermes/config.yaml
- Prepend model block with base_url/api_key/default model at top
- Append custom_providers block at the bottom
- Use 3rd model in daemon list as default
- Add Hermes to setup menu and registry
This commit is contained in:
redshift
2026-04-30 21:42:53 +05:30
parent 9a520efb24
commit 10937f851c
3 changed files with 105 additions and 3 deletions

View File

@@ -0,0 +1,87 @@
import { existsSync, mkdirSync } from "fs";
import { readFile, writeFile } from "fs/promises";
import { dirname } from "path";
import type { RoutstrdConfig } from "../utils/config";
import { logger } from "../utils/logger";
import type { IntegrationConfig, RoutstrModel } from "./registry";
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
export async function installHermesIntegration(
config: RoutstrdConfig,
apiKey: string,
integrationConfig: IntegrationConfig,
): Promise<void> {
const { name, configPath } = integrationConfig;
logger.log(`\nInstalling routstr configuration in ${configPath}...`);
logger.log(`Using API key for ${name}`);
const baseUrl = getDaemonBaseUrl(config);
const baseUrlV1 = `${baseUrl}/v1`;
let defaultModel = "minimax-m2.7";
try {
const data = await callDaemon("/models");
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
if (models.length >= 3) {
defaultModel = models[2]!.id;
logger.log(`Set default model to 3rd available model: ${defaultModel}`);
} else if (models.length > 0) {
defaultModel = models[0]!.id;
logger.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
} else {
logger.log("No models available from routstr daemon, using fallback default.");
}
} catch (error) {
logger.error("Failed to fetch models for Hermes integration:", error);
logger.log("Using fallback default model.");
}
let content = "";
try {
if (existsSync(configPath)) {
content = await readFile(configPath, "utf-8");
}
} catch (error) {
logger.error(`Error reading ${configPath}, creating new one.`);
}
// Remove existing model block
content = content.replace(/^model:\n(?: .*\n)*/gm, "");
// Remove existing custom_providers block
content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
// Clean up extra blank lines
content = content.replace(/\n{3,}/g, "\n\n").trim();
const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
const modelBlock = `model:
default: ${defaultModel}
provider: custom
base_url: ${baseUrlV1}
api_key: ${apiKey}`;
const providerBlock = `custom_providers:
- name: Routstr (${urlDisplay})
base_url: ${baseUrlV1}
api_key: ${apiKey}
model: ${defaultModel}`;
const parts: string[] = [modelBlock];
if (content) {
parts.push(content);
}
parts.push(providerBlock);
const newContent = parts.join("\n\n") + "\n";
try {
mkdirSync(dirname(configPath), { recursive: true });
await writeFile(configPath, newContent);
logger.log(`Successfully updated ${configPath} with routstr settings.`);
} catch (error) {
logger.error(`Failed to write to ${configPath}:`, error);
}
}

View File

@@ -9,6 +9,7 @@ import { installOpencodeIntegration } from "./opencode";
import { installOpenClawIntegration } from "./openclaw";
import { installPiIntegration } from "./pi";
import { installClaudeCodeIntegration } from "./claudecode";
import { installHermesIntegration } from "./hermes";
import type { IntegrationConfig } from "./registry";
import { CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
@@ -56,7 +57,7 @@ function parseChoice(input: string): number {
}
const parsed = Number.parseInt(input, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 5) {
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 6) {
return parsed;
}
@@ -71,7 +72,8 @@ export async function setupIntegration(
logger.log("2. OpenClaw");
logger.log("3. Pi");
logger.log("4. Claude Code");
logger.log("5. Skip for now");
logger.log("5. Hermes");
logger.log("6. Skip for now");
const answer = await ask("Select integration [1]: ");
const choice = parseChoice(answer);
@@ -81,6 +83,7 @@ export async function setupIntegration(
2: "openclaw",
3: "pi-agent",
4: "claude-code",
5: "hermes",
};
const key = integrationByChoice[choice];
@@ -92,7 +95,6 @@ export async function setupIntegration(
const integrationConfig = CLIENT_CONFIGS[key]!;
const { client, created } = await addDaemonClient(
integrationConfig.name,
integrationConfig.clientId,
);
if (created) {
@@ -118,5 +120,11 @@ export async function setupIntegration(
if (key === "claude-code") {
await installClaudeCodeIntegration(config, client.apiKey, integrationConfig);
return;
}
if (key === "hermes") {
await installHermesIntegration(config, client.apiKey, integrationConfig);
return;
}
}

View File

@@ -4,6 +4,7 @@ import { installOpencodeIntegration } from "./opencode";
import { installPiIntegration } from "./pi";
import { installOpenClawIntegration } from "./openclaw";
import { installClaudeCodeIntegration } from "./claudecode";
import { installHermesIntegration } from "./hermes";
export interface IntegrationConfig {
clientId: string;
@@ -43,6 +44,11 @@ export const CLIENT_CONFIGS: Record<string, IntegrationConfig> = {
name: "Claude Code",
configPath: join(process.env.HOME || "", ".claude/settings.json"),
},
hermes: {
clientId: "hermes",
name: "Hermes",
configPath: join(process.env.HOME || "", ".hermes/config.yaml"),
},
};
export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
@@ -50,6 +56,7 @@ export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
"pi-agent": installPiIntegration,
openclaw: installOpenClawIntegration,
"claude-code": installClaudeCodeIntegration,
hermes: installHermesIntegration,
};
export async function runIntegrationsForClients(