| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- /**
- * Hermes Agent target.
- *
- * Hermes reads MCP servers from `$HERMES_HOME/config.yaml` under the
- * top-level `mcp_servers` key, and exposes discovered MCP tools through
- * dynamic toolsets named `mcp-<server>`. We add:
- *
- * mcp_servers.codegraph -> `codegraph serve --mcp`
- * platform_toolsets.cli -> `mcp-codegraph`
- *
- * The second entry matters because Hermes CLI profiles often enable an
- * explicit `platform_toolsets.cli` list. Without `mcp-codegraph` in that
- * list, the MCP server can be configured and connected but its tools may
- * still be filtered out of normal CLI sessions.
- */
- import * as fs from 'fs';
- import * as path from 'path';
- import * as os from 'os';
- import {
- AgentTarget,
- DetectionResult,
- InstallOptions,
- Location,
- WriteResult,
- } from './types';
- import { atomicWriteFileSync } from './shared';
- type LineRange = { start: number; end: number };
- class HermesTarget implements AgentTarget {
- readonly id = 'hermes' as const;
- readonly displayName = 'Hermes Agent';
- readonly docsUrl = 'https://hermes-agent.nousresearch.com';
- supportsLocation(loc: Location): boolean {
- return loc === 'global';
- }
- detect(loc: Location): DetectionResult {
- if (loc !== 'global') {
- return { installed: false, alreadyConfigured: false };
- }
- const file = configPath();
- const content = readText(file);
- const installed = fs.existsSync(hermesHome()) || fs.existsSync(file);
- return {
- installed,
- alreadyConfigured: hasCodeGraphMcpServer(content),
- configPath: file,
- };
- }
- install(loc: Location, _opts: InstallOptions): WriteResult {
- if (loc !== 'global') {
- return {
- files: [],
- notes: ['Hermes Agent uses $HERMES_HOME/config.yaml; re-run with --location=global.'],
- };
- }
- return {
- files: [writeHermesConfig()],
- notes: ['Start a new Hermes session for MCP changes to take effect.'],
- };
- }
- uninstall(loc: Location): WriteResult {
- if (loc !== 'global') return { files: [] };
- const file = configPath();
- if (!fs.existsSync(file)) {
- return { files: [{ path: file, action: 'not-found' }] };
- }
- const before = readText(file);
- const after = removeCodeGraphToolset(removeCodeGraphMcpServer(before));
- if (after === before) {
- return { files: [{ path: file, action: 'not-found' }] };
- }
- atomicWriteFileSync(file, ensureTrailingNewline(after));
- return { files: [{ path: file, action: 'removed' }] };
- }
- printConfig(loc: Location): string {
- if (loc !== 'global') {
- return '# Hermes Agent uses $HERMES_HOME/config.yaml; use --location=global.\n';
- }
- return [
- `# Add to ${configPath()}`,
- '',
- renderCodeGraphMcpBlock().join('\n'),
- '',
- 'platform_toolsets:',
- ' cli:',
- ' - hermes-cli',
- ' - mcp-codegraph',
- '',
- ].join('\n');
- }
- describePaths(loc: Location): string[] {
- return loc === 'global' ? [configPath()] : [];
- }
- }
- function hermesHome(): string {
- return process.env.HERMES_HOME
- ? path.resolve(process.env.HERMES_HOME)
- : path.join(os.homedir(), '.hermes');
- }
- function configPath(): string {
- return path.join(hermesHome(), 'config.yaml');
- }
- function readText(file: string): string {
- try {
- return fs.readFileSync(file, 'utf-8');
- } catch {
- return '';
- }
- }
- function writeHermesConfig(): WriteResult['files'][number] {
- const file = configPath();
- const existed = fs.existsSync(file);
- const before = readText(file);
- const afterMcp = upsertCodeGraphMcpServer(before);
- const after = upsertCodeGraphToolset(afterMcp);
- if (after === before) {
- return { path: file, action: 'unchanged' };
- }
- atomicWriteFileSync(file, ensureTrailingNewline(after));
- return { path: file, action: existed ? 'updated' : 'created' };
- }
- function ensureTrailingNewline(text: string): string {
- return text.endsWith('\n') ? text : text + '\n';
- }
- function splitLines(content: string): string[] {
- return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
- }
- function joinLines(lines: string[]): string {
- while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
- return lines.join('\n') + '\n';
- }
- function topLevelRange(lines: string[], key: string): LineRange | null {
- const start = lines.findIndex((line) => line.trim() === `${key}:`);
- if (start === -1) return null;
- let end = lines.length;
- for (let i = start + 1; i < lines.length; i++) {
- const line = lines[i] ?? '';
- if (line.trim() === '') continue;
- if (/^[A-Za-z_][A-Za-z0-9_-]*:\s*(?:#.*)?$/.test(line)) {
- end = i;
- break;
- }
- }
- return { start, end };
- }
- function childRange(lines: string[], parent: LineRange, child: string): LineRange | null {
- const startPattern = new RegExp(`^ ${escapeRegExp(child)}:\\s*(?:#.*)?$`);
- let start = -1;
- for (let i = parent.start + 1; i < parent.end; i++) {
- if (startPattern.test(lines[i] ?? '')) {
- start = i;
- break;
- }
- }
- if (start === -1) return null;
- let end = parent.end;
- for (let i = start + 1; i < parent.end; i++) {
- const line = lines[i] ?? '';
- if (line.trim() === '') continue;
- if (/^ \S/.test(line)) {
- end = i;
- break;
- }
- }
- while (end > start + 1 && (lines[end - 1] ?? '').trim() === '') {
- end--;
- }
- return { start, end };
- }
- function escapeRegExp(value: string): string {
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
- function renderCodeGraphMcpChild(): string[] {
- return [
- ' codegraph:',
- ' command: codegraph',
- ' args:',
- ' - serve',
- ' - --mcp',
- ' timeout: 120',
- ' connect_timeout: 60',
- ' enabled: true',
- ];
- }
- function renderCodeGraphMcpBlock(): string[] {
- return ['mcp_servers:', ...renderCodeGraphMcpChild()];
- }
- function hasCodeGraphMcpServer(content: string): boolean {
- const lines = splitLines(content);
- const parent = topLevelRange(lines, 'mcp_servers');
- return !!parent && !!childRange(lines, parent, 'codegraph');
- }
- function upsertCodeGraphMcpServer(content: string): string {
- const lines = splitLines(content);
- const parent = topLevelRange(lines, 'mcp_servers');
- const child = parent ? childRange(lines, parent, 'codegraph') : null;
- const replacement = renderCodeGraphMcpChild();
- if (!parent) {
- if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
- if (lines.length > 0) lines.push('');
- lines.push(...renderCodeGraphMcpBlock());
- return joinLines(lines);
- }
- if (child) {
- const existing = lines.slice(child.start, child.end);
- if (arrayEqual(existing, replacement)) return joinLines(lines);
- lines.splice(child.start, child.end - child.start, ...replacement);
- return joinLines(lines);
- }
- lines.splice(parent.end, 0, ...replacement);
- return joinLines(lines);
- }
- function removeCodeGraphMcpServer(content: string): string {
- const lines = splitLines(content);
- const parent = topLevelRange(lines, 'mcp_servers');
- const child = parent ? childRange(lines, parent, 'codegraph') : null;
- if (!child) return content;
- lines.splice(child.start, child.end - child.start);
- return joinLines(lines);
- }
- function upsertCodeGraphToolset(content: string): string {
- const lines = splitLines(content);
- const parent = topLevelRange(lines, 'platform_toolsets');
- const cli = parent ? childRange(lines, parent, 'cli') : null;
- if (!parent) {
- if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
- if (lines.length > 0) lines.push('');
- lines.push('platform_toolsets:', ' cli:', ' - hermes-cli', ' - mcp-codegraph');
- return joinLines(lines);
- }
- if (!cli) {
- lines.splice(parent.end, 0, ' cli:', ' - hermes-cli', ' - mcp-codegraph');
- return joinLines(lines);
- }
- const hasEntry = lines
- .slice(cli.start + 1, cli.end)
- .some((line) => line.trim() === '- mcp-codegraph');
- if (hasEntry) return joinLines(lines);
- lines.splice(cli.end, 0, ' - mcp-codegraph');
- return joinLines(lines);
- }
- function removeCodeGraphToolset(content: string): string {
- const lines = splitLines(content);
- const parent = topLevelRange(lines, 'platform_toolsets');
- const cli = parent ? childRange(lines, parent, 'cli') : null;
- if (!cli) return content;
- const hasEntry = lines
- .slice(cli.start + 1, cli.end)
- .some((line) => line.trim() === '- mcp-codegraph');
- if (!hasEntry) return content;
- const next = lines.filter((line, idx) => {
- if (idx <= cli.start || idx >= cli.end) return true;
- return line.trim() !== '- mcp-codegraph';
- });
- return joinLines(next);
- }
- function arrayEqual(a: string[], b: string[]): boolean {
- return a.length === b.length && a.every((value, idx) => value === b[idx]);
- }
- export const hermesTarget: AgentTarget = new HermesTarget();
|