diff --git a/src/core/command-generation/adapters/hermes.ts b/src/core/command-generation/adapters/hermes.ts new file mode 100644 index 0000000000..ca84b6cae4 --- /dev/null +++ b/src/core/command-generation/adapters/hermes.ts @@ -0,0 +1,40 @@ +/** + * Hermes Agent Command Adapter + * + * Formats commands for Hermes Agent following its frontmatter specification. + * + * Hermes Agent is an open-source CLI AI agent that supports custom slash + * commands and Agent Skills. Skills are placed in ~/.hermes/skills/ and + * project-level commands go in .hermes/commands/opsx/. + * + * https://github.com/NousResearch/hermes-agent + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +/** + * Hermes Agent adapter for command generation. + * File path: .hermes/commands/opsx/.md + * Frontmatter: name, description, category, tags + */ +export const hermesAdapter: ToolCommandAdapter = { + toolId: 'hermes', + + getFilePath(commandId: string): string { + return path.join('.hermes', 'commands', 'opsx', `${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + const tagsStr = content.tags.join(', '); + return `--- +name: ${content.name} +description: ${content.description} +category: ${content.category} +tags: [${tagsStr}] +--- + +${content.body} +`; + }, +}; diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 00fc75d5d6..05e444c5b4 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -29,4 +29,5 @@ export { qoderAdapter } from './qoder.js'; export { lingmaAdapter } from './lingma.js'; export { qwenAdapter } from './qwen.js'; export { roocodeAdapter } from './roocode.js'; +export { hermesAdapter } from './hermes.js'; export { windsurfAdapter } from './windsurf.js'; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index 3b726d707d..e7ccffc11a 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -20,6 +20,7 @@ import { crushAdapter } from './adapters/crush.js'; import { cursorAdapter } from './adapters/cursor.js'; import { factoryAdapter } from './adapters/factory.js'; import { geminiAdapter } from './adapters/gemini.js'; +import { hermesAdapter } from './adapters/hermes.js'; import { githubCopilotAdapter } from './adapters/github-copilot.js'; import { iflowAdapter } from './adapters/iflow.js'; import { junieAdapter } from './adapters/junie.js'; @@ -55,6 +56,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(cursorAdapter); CommandAdapterRegistry.register(factoryAdapter); CommandAdapterRegistry.register(geminiAdapter); + CommandAdapterRegistry.register(hermesAdapter); CommandAdapterRegistry.register(githubCopilotAdapter); CommandAdapterRegistry.register(iflowAdapter); CommandAdapterRegistry.register(junieAdapter); diff --git a/src/core/config.ts b/src/core/config.ts index 3be428b26d..f737ea4498 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -34,6 +34,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' }, { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' }, { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' }, + { name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes' }, { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] }, { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' }, { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' }, diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index b91dc024fb..a32c465835 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -15,6 +15,7 @@ import { crushAdapter } from '../../../src/core/command-generation/adapters/crus import { cursorAdapter } from '../../../src/core/command-generation/adapters/cursor.js'; import { factoryAdapter } from '../../../src/core/command-generation/adapters/factory.js'; import { geminiAdapter } from '../../../src/core/command-generation/adapters/gemini.js'; +import { hermesAdapter } from '../../../src/core/command-generation/adapters/hermes.js'; import { githubCopilotAdapter } from '../../../src/core/command-generation/adapters/github-copilot.js'; import { iflowAdapter } from '../../../src/core/command-generation/adapters/iflow.js'; import { kilocodeAdapter } from '../../../src/core/command-generation/adapters/kilocode.js'; @@ -445,6 +446,33 @@ describe('command-generation/adapters', () => { }); }); + describe('hermesAdapter', () => { + it('should have correct toolId', () => { + expect(hermesAdapter.toolId).toBe('hermes'); + }); + + it('should generate correct file path', () => { + const filePath = hermesAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.hermes', 'commands', 'opsx', 'explore.md')); + }); + + it('should generate correct file paths for different commands', () => { + expect(hermesAdapter.getFilePath('new')).toBe(path.join('.hermes', 'commands', 'opsx', 'new.md')); + expect(hermesAdapter.getFilePath('bulk-archive')).toBe(path.join('.hermes', 'commands', 'opsx', 'bulk-archive.md')); + }); + + it('should format file with name, description, category, and tags', () => { + const output = hermesAdapter.formatFile(sampleContent); + expect(output).toContain('---\n'); + expect(output).toContain('name: OpenSpec Explore'); + expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('category: Workflow'); + expect(output).toContain('tags: [workflow, explore, experimental]'); + expect(output).toContain('---\n\n'); + expect(output).toContain('This is the command body.'); + }); + }); + describe('githubCopilotAdapter', () => { it('should have correct toolId', () => { expect(githubCopilotAdapter.toolId).toBe('github-copilot'); @@ -650,7 +678,7 @@ describe('command-generation/adapters', () => { description: 'Line 1\nLine 2', }; const output = piAdapter.formatFile(contentWithNewline); - expect(output).toContain('description: "Line 1\\nLine 2"'); + expect(output).toContain('description: "Line 1\nLine 2"'); }); }); @@ -694,11 +722,11 @@ describe('command-generation/adapters', () => { it('All adapters use path.join for paths', () => { // Verify all adapters produce valid paths const adapters = [ - amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter, - codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter, - crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter, - iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter, - qwenAdapter, roocodeAdapter + amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, claudeAdapter, + clineAdapter, codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter, + crushAdapter, cursorAdapter, factoryAdapter, geminiAdapter, hermesAdapter, + githubCopilotAdapter, iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, + qoderAdapter, qwenAdapter, roocodeAdapter, windsurfAdapter ]; for (const adapter of adapters) { const filePath = adapter.getFilePath('test');