diff --git a/src/integrations/hermes.ts b/src/integrations/hermes.ts new file mode 100644 index 0000000..964f6bb --- /dev/null +++ b/src/integrations/hermes.ts @@ -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 { + 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); + } +} diff --git a/src/integrations/index.ts b/src/integrations/index.ts index c70cf35..54ccf9c 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -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; } } diff --git a/src/integrations/registry.ts b/src/integrations/registry.ts index fb5b447..c69375f 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -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 = { 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 = { @@ -50,6 +56,7 @@ export const CLIENT_INTEGRATIONS: Record = { "pi-agent": installPiIntegration, openclaw: installOpenClawIntegration, "claude-code": installClaudeCodeIntegration, + hermes: installHermesIntegration, }; export async function runIntegrationsForClients(