Skip to content

Commit a974515

Browse files
authored
Merge pull request #114 from RUFFY-369/feat/hermes-agent-native-support
feat: add Hermes agent adapter (Native Hermes Support)
2 parents 1e7cad1 + b5b251a commit a974515

21 files changed

Lines changed: 281 additions & 8 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,9 @@ skillkit cicd init # CI/CD templates
419419
</details>
420420

421421
<details>
422-
<summary><b>Plus 34 more</b></summary>
422+
<summary><b>Plus 35 more</b></summary>
423423

424-
Amp, Antigravity, Augment Code, Bolt, Clawdbot, Cline, CodeBuddy, CodeGPT, CommandCode, Continue, Crush, Droid, Factory, Goose, Kilo Code, Kiro CLI, Lovable, MCPJam, Mux, Neovate, OpenClaw, OpenHands, Pi, PlayCode, Qoder, Qwen, Replit Agent, Roo Code, Tabby, Tabnine, Trae, Vercel, Zencoder, Universal.
424+
Amp, Antigravity, Augment Code, Bolt, Clawdbot, Cline, CodeBuddy, CodeGPT, CommandCode, Continue, Crush, Droid, Factory, Goose, Hermes Agent, Kilo Code, Kiro CLI, Lovable, MCPJam, Mux, Neovate, OpenClaw, OpenHands, Pi, PlayCode, Qoder, Qwen, Replit Agent, Roo Code, Tabby, Tabnine, Trae, Vercel, Zencoder, Universal.
425425
</details>
426426

427427
[Full agent details](https://skillkit.sh/docs/agents)

packages/agents/src/__tests__/adapters.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ const ALL_AGENTS = AgentType.options;
3535

3636
describe('Agent Adapters', () => {
3737
describe('getAllAdapters', () => {
38-
it('should return all 45 registered adapters', () => {
38+
it('should return all 46 registered adapters', () => {
3939
const adapters = getAllAdapters();
4040
expect(adapters).toBeInstanceOf(Array);
41-
expect(adapters.length).toBe(45);
41+
expect(adapters.length).toBe(46);
4242
});
4343

4444
it('should include common agents', () => {
@@ -140,4 +140,43 @@ describe('Agent Adapters', () => {
140140
expect(typeof agent).toBe('string');
141141
});
142142
});
143+
144+
describe('HermesAdapter', () => {
145+
it('should generate valid XML config', () => {
146+
const adapter = getAdapter('hermes');
147+
const mockSkills = [
148+
{ name: 'test-skill', description: 'Testing', enabled: true, path: '', location: 'project' }
149+
];
150+
const config = adapter.generateConfig(mockSkills as any);
151+
152+
expect(config).toContain('<skills_system');
153+
expect(config).toContain('<name>test-skill</name>');
154+
expect(config).toContain('SKILLS_TABLE_START');
155+
});
156+
157+
it('should parse skill names from XML correctly within markers', () => {
158+
const adapter = getAdapter('hermes');
159+
const xml = `
160+
<!-- SKILLS_TABLE_START -->
161+
<available_skills><skill><name>git-ops</name></skill></available_skills>
162+
<!-- SKILLS_TABLE_END -->`;
163+
const names = adapter.parseConfig(xml);
164+
expect(names).toContain('git-ops');
165+
});
166+
167+
it('should ignore <name> tags outside of sync markers', () => {
168+
const adapter = getAdapter('hermes');
169+
const xml = `
170+
<mission><name>save-the-world</name></mission>
171+
<!-- SKILLS_TABLE_START -->
172+
<available_skills><skill><name>git-ops</name></skill></available_skills>
173+
<!-- SKILLS_TABLE_END -->
174+
<footer_meta><name>internal-id</name></footer_meta>`;
175+
const names = adapter.parseConfig(xml);
176+
expect(names).toHaveLength(1);
177+
expect(names).toContain('git-ops');
178+
expect(names).not.toContain('save-the-world');
179+
expect(names).not.toContain('internal-id');
180+
});
181+
});
143182
});

packages/agents/src/generic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const AGENT_DISPLAY_NAMES: Partial<Record<AgentType, string>> = {
3333
'tabnine': 'Tabnine',
3434
'codegpt': 'CodeGPT',
3535
'playcode-agent': 'PlayCode Agent',
36+
'hermes': 'Hermes',
3637
};
3738

3839
export class GenericAgentAdapter implements AgentAdapter {

packages/agents/src/hermes.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { existsSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { homedir } from 'node:os';
4+
import type { AgentAdapter } from './base.js';
5+
import { createSkillXml } from './base.js';
6+
import type { Skill, AgentType } from '@skillkit/core';
7+
import { AGENT_CONFIG } from '@skillkit/core';
8+
9+
const config = AGENT_CONFIG.hermes;
10+
11+
/**
12+
* Hermes Agent Adapter
13+
*
14+
* Hermes Agent is an advanced AI agent with support for XML-based skills.
15+
* It uses AGENTS.md for instructions and ~/.hermes/skills/ for global skills.
16+
*/
17+
export class HermesAdapter implements AgentAdapter {
18+
readonly type: AgentType = 'hermes';
19+
readonly name = 'Hermes Agent';
20+
readonly skillsDir = config.skillsDir;
21+
readonly configFile = config.configFile;
22+
23+
generateConfig(skills: Skill[]): string {
24+
const enabledSkills = skills.filter(s => s.enabled);
25+
26+
if (enabledSkills.length === 0) {
27+
return '';
28+
}
29+
30+
const skillsXml = enabledSkills.map(createSkillXml).join('\n\n');
31+
32+
return `<skills_system priority="1">
33+
34+
## Available Skills
35+
36+
<!-- SKILLS_TABLE_START -->
37+
<usage>
38+
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
39+
40+
How to use skills:
41+
- Invoke: \`skillkit read <skill-name>\` or \`npx skillkit read <skill-name>\`
42+
- The skill content will load with detailed instructions on how to complete the task
43+
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
44+
45+
Usage notes:
46+
- Only use skills listed in <available_skills> below
47+
- Do not invoke a skill that is already loaded in your context
48+
- Each skill invocation is stateless
49+
</usage>
50+
51+
<available_skills>
52+
53+
${skillsXml}
54+
55+
</available_skills>
56+
<!-- SKILLS_TABLE_END -->
57+
58+
</skills_system>`;
59+
}
60+
61+
parseConfig(content: string): string[] {
62+
const startMarker = '<!-- SKILLS_TABLE_START -->';
63+
const endMarker = '<!-- SKILLS_TABLE_END -->';
64+
const startIndex = content.indexOf(startMarker);
65+
const endIndex = content.indexOf(endMarker);
66+
67+
if (startIndex === -1 || endIndex === -1) {
68+
return [];
69+
}
70+
71+
const scopedContent = content.substring(startIndex + startMarker.length, endIndex);
72+
const skillNames: string[] = [];
73+
const skillRegex = /<name>([^<]+)<\/name>/g;
74+
let match;
75+
76+
while ((match = skillRegex.exec(scopedContent)) !== null) {
77+
skillNames.push(match[1].trim());
78+
}
79+
80+
return skillNames;
81+
}
82+
83+
getInvokeCommand(skillName: string): string {
84+
return `skillkit read ${skillName}`;
85+
}
86+
87+
async isDetected(): Promise<boolean> {
88+
const projectHermes = join(process.cwd(), '.hermes');
89+
const globalHermes = join(homedir(), '.hermes');
90+
91+
return existsSync(projectHermes) || existsSync(globalHermes);
92+
}
93+
}

packages/agents/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RooAdapter } from './roo.js';
1919
import { TraeAdapter } from './trae.js';
2020
import { WindsurfAdapter } from './windsurf.js';
2121
import { UniversalAdapter } from './universal.js';
22+
import { HermesAdapter } from './hermes.js';
2223
import { GenericAgentAdapter } from './generic.js';
2324

2425
export * from './base.js';
@@ -41,6 +42,7 @@ export * from './roo.js';
4142
export * from './trae.js';
4243
export * from './windsurf.js';
4344
export * from './universal.js';
45+
export * from './hermes.js';
4446
export * from './generic.js';
4547

4648
// Agent features
@@ -92,6 +94,7 @@ const adapters: Record<AgentType, AgentAdapter> = {
9294
tabnine: new GenericAgentAdapter('tabnine'),
9395
codegpt: new GenericAgentAdapter('codegpt'),
9496
'playcode-agent': new GenericAgentAdapter('playcode-agent'),
97+
hermes: new HermesAdapter(),
9598
};
9699

97100
export function getAdapter(type: AgentType): AgentAdapter {
@@ -160,6 +163,7 @@ export async function detectAgent(): Promise<AgentType> {
160163
'tabnine',
161164
'codegpt',
162165
'playcode-agent',
166+
'hermes',
163167
'universal',
164168
];
165169

packages/cli/src/commands/sync.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ function updateConfigContent(existing: string, newConfig: string, agentType: Age
214214
start: '<!-- SKILLKIT_SKILLS_START -->',
215215
end: '<!-- SKILLKIT_SKILLS_END -->',
216216
},
217+
hermes: {
218+
start: '<!-- SKILLS_TABLE_START -->',
219+
end: '<!-- SKILLS_TABLE_END -->',
220+
},
217221
};
218222

219223
const agentMarkers = markers[agentType] || markers.universal;

packages/core/src/agent-config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,16 @@ export const AGENT_CONFIG: Record<AgentType, AgentDirectoryConfig> = {
477477
usesFrontmatter: true,
478478
supportsAutoDiscovery: true,
479479
},
480+
481+
// Hermes Agent
482+
hermes: {
483+
skillsDir: '.hermes/skills',
484+
configFile: 'AGENTS.md',
485+
globalSkillsDir: '~/.hermes/skills',
486+
configFormat: 'xml',
487+
usesFrontmatter: true,
488+
supportsAutoDiscovery: true,
489+
},
480490
};
481491

482492
/**

packages/core/src/agents/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export const AGENT_DISCOVERY_PATHS: Record<AgentType, string[]> = {
264264
'tabnine': ['.tabnine/agents'],
265265
'codegpt': ['.codegpt/agents'],
266266
'playcode-agent': ['.playcode/agents'],
267+
'hermes': ['.hermes/agents'],
267268
};
268269

269270
/**
@@ -324,6 +325,7 @@ export const ALL_AGENT_DISCOVERY_PATHS = [
324325
'.tabnine/agents',
325326
'.codegpt/agents',
326327
'.playcode/agents',
328+
'.hermes/agents',
327329
];
328330

329331
/**
@@ -375,4 +377,5 @@ export const CUSTOM_AGENT_FORMAT_MAP: Record<AgentType, AgentFormatCategory> = {
375377
'tabnine': 'universal',
376378
'codegpt': 'universal',
377379
'playcode-agent': 'universal',
380+
'hermes': 'claude-agent',
378381
};

packages/core/src/commands/generator.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ const AGENT_FORMATS: Record<AgentType, AgentCommandFormat> = {
332332
supportsSlashCommands: false,
333333
supportsCommandFiles: true,
334334
},
335+
hermes: {
336+
agent: 'hermes',
337+
extension: '.md',
338+
directory: '.hermes/commands',
339+
supportsSlashCommands: true,
340+
supportsCommandFiles: true,
341+
},
335342
};
336343

337344
/**
@@ -593,6 +600,7 @@ export class CommandGenerator {
593600
case 'amp':
594601
case 'roo':
595602
case 'kiro-cli':
603+
case 'hermes':
596604
return this.generateClaudeCommands(commands);
597605

598606
case 'cursor':

packages/core/src/context/sync.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export class ContextSync {
286286
const commonDirs = [
287287
'.claude/skills',
288288
'.cursor/skills',
289+
'.hermes/skills',
289290
'.agent/skills',
290291
'skills',
291292
];

0 commit comments

Comments
 (0)