mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
Surfaces Hermes Agent version info on demand without leaving chat; works mid-run like /help and /update.
274 lines
7.9 KiB
TypeScript
274 lines
7.9 KiB
TypeScript
export interface CommandsCatalogSection {
|
|
name: string
|
|
pairs: [string, string][]
|
|
}
|
|
|
|
export interface CommandsCatalogLike {
|
|
categories?: CommandsCatalogSection[]
|
|
pairs?: [string, string][]
|
|
skill_count?: number
|
|
warning?: string
|
|
}
|
|
|
|
export interface DesktopSlashCompletion {
|
|
display: string
|
|
meta: string
|
|
text: string
|
|
}
|
|
|
|
export interface DesktopThemeCommandOption {
|
|
description: string
|
|
label: string
|
|
name: string
|
|
}
|
|
|
|
const DESKTOP_COMMAND_META = [
|
|
['/agents', 'Show active desktop sessions and running tasks'],
|
|
['/background', 'Run a prompt in the background'],
|
|
['/branch', 'Branch the latest message into a new chat'],
|
|
['/compress', 'Compress this conversation context'],
|
|
['/debug', 'Create a debug report'],
|
|
['/goal', 'Manage the standing goal for this session'],
|
|
['/help', 'Show desktop slash commands'],
|
|
['/new', 'Start a new desktop chat'],
|
|
['/profile', 'Switch the active Hermes profile'],
|
|
['/queue', 'Queue a prompt for the next turn'],
|
|
['/resume', 'Resume a saved session'],
|
|
['/retry', 'Retry the last user message'],
|
|
['/rollback', 'List or restore filesystem checkpoints'],
|
|
['/skin', 'Switch desktop theme or cycle to the next one'],
|
|
['/status', 'Show current session status'],
|
|
['/steer', 'Steer the current run after the next tool call'],
|
|
['/stop', 'Stop running background processes'],
|
|
['/title', 'Rename the current session'],
|
|
['/undo', 'Remove the last user/assistant exchange'],
|
|
['/usage', 'Show token usage for this session'],
|
|
['/version', 'Show Hermes Agent version'],
|
|
['/yolo', 'Toggle YOLO — auto-approve dangerous commands']
|
|
] as const
|
|
|
|
const DESKTOP_COMMANDS: ReadonlySet<string> = new Set(DESKTOP_COMMAND_META.map(([command]) => command))
|
|
|
|
const DESKTOP_ALIASES = new Map([
|
|
['/bg', '/background'],
|
|
['/btw', '/background'],
|
|
['/fork', '/branch'],
|
|
['/q', '/queue'],
|
|
['/reload_mcp', '/reload-mcp'],
|
|
['/reload_skills', '/reload-skills'],
|
|
['/reset', '/new'],
|
|
['/tasks', '/agents']
|
|
])
|
|
|
|
const DESKTOP_COMMAND_DESCRIPTIONS: ReadonlyMap<string, string> = new Map(DESKTOP_COMMAND_META)
|
|
|
|
const PICKER_OWNED_COMMANDS = new Set(['/model'])
|
|
|
|
const TERMINAL_ONLY_COMMANDS = new Set([
|
|
'/browser',
|
|
'/busy',
|
|
'/clear',
|
|
'/commands',
|
|
'/compact',
|
|
'/config',
|
|
'/copy',
|
|
'/cron',
|
|
'/details',
|
|
'/exit',
|
|
'/footer',
|
|
'/gateway',
|
|
'/gquota',
|
|
'/history',
|
|
'/image',
|
|
'/indicator',
|
|
'/logs',
|
|
'/mouse',
|
|
'/paste',
|
|
'/platforms',
|
|
'/plugins',
|
|
'/quit',
|
|
'/redraw',
|
|
'/reload',
|
|
'/restart',
|
|
'/save',
|
|
'/sb',
|
|
'/set-home',
|
|
'/sethome',
|
|
'/snap',
|
|
'/snapshot',
|
|
'/statusbar',
|
|
'/toolsets',
|
|
'/tools',
|
|
'/update',
|
|
'/verbose'
|
|
])
|
|
|
|
const MESSAGING_ONLY_COMMANDS = new Set(['/approve', '/deny'])
|
|
|
|
const SETTINGS_OWNED_COMMANDS = new Set(['/skills'])
|
|
|
|
const ADVANCED_COMMANDS = new Set([
|
|
'/curator',
|
|
'/fast',
|
|
'/insights',
|
|
'/kanban',
|
|
'/personality',
|
|
'/reasoning',
|
|
'/reload-mcp',
|
|
'/reload-skills',
|
|
'/voice'
|
|
])
|
|
|
|
const BLOCKED_COMMANDS = new Set([
|
|
...PICKER_OWNED_COMMANDS,
|
|
...TERMINAL_ONLY_COMMANDS,
|
|
...MESSAGING_ONLY_COMMANDS,
|
|
...SETTINGS_OWNED_COMMANDS,
|
|
...ADVANCED_COMMANDS
|
|
])
|
|
|
|
function normalizeCommand(command: string): string {
|
|
const trimmed = command.trim()
|
|
const base = (trimmed.startsWith('/') ? trimmed : `/${trimmed}`).split(/\s+/, 1)[0]?.toLowerCase() || ''
|
|
|
|
return base
|
|
}
|
|
|
|
export function canonicalDesktopSlashCommand(command: string): string {
|
|
const normalized = normalizeCommand(command)
|
|
|
|
return DESKTOP_ALIASES.get(normalized) || normalized
|
|
}
|
|
|
|
export function isDesktopSlashCommand(command: string): boolean {
|
|
const normalized = normalizeCommand(command)
|
|
const canonical = canonicalDesktopSlashCommand(normalized)
|
|
|
|
if (BLOCKED_COMMANDS.has(normalized) || BLOCKED_COMMANDS.has(canonical)) {
|
|
return false
|
|
}
|
|
|
|
return DESKTOP_COMMANDS.has(canonical) || !isKnownHermesSlashCommand(normalized)
|
|
}
|
|
|
|
/**
|
|
* An "extension" command is anything the backend surfaces that is NOT one of
|
|
* Hermes' built-in slash commands — i.e. skill commands (`/gif-search`,
|
|
* `/codex`, …) and user-defined quick commands. These are user-activated, so
|
|
* they should appear in the desktop slash palette even though they aren't in
|
|
* the curated `DESKTOP_COMMANDS` allow-list. This mirrors the predicate in
|
|
* `isDesktopSlashCommand` that already lets them EXECUTE when typed.
|
|
*/
|
|
export function isDesktopSlashExtensionCommand(command: string): boolean {
|
|
const normalized = normalizeCommand(command)
|
|
|
|
if (!normalized || normalized === '/') {
|
|
return false
|
|
}
|
|
|
|
return !isKnownHermesSlashCommand(normalized)
|
|
}
|
|
|
|
export function isDesktopSlashSuggestion(command: string): boolean {
|
|
const normalized = normalizeCommand(command)
|
|
const canonical = canonicalDesktopSlashCommand(normalized)
|
|
|
|
// Surface skill / quick commands (extensions the backend provides) alongside
|
|
// the curated built-ins. Built-in aliases stay hidden so the popover isn't
|
|
// cluttered with duplicates.
|
|
if (isDesktopSlashExtensionCommand(normalized)) {
|
|
return true
|
|
}
|
|
|
|
return DESKTOP_COMMANDS.has(canonical) && !DESKTOP_ALIASES.has(normalized)
|
|
}
|
|
|
|
export function desktopSlashUnavailableMessage(command: string): string | null {
|
|
const normalized = normalizeCommand(command)
|
|
const canonical = canonicalDesktopSlashCommand(normalized)
|
|
|
|
if (PICKER_OWNED_COMMANDS.has(canonical)) {
|
|
return `/${canonical.slice(1)} uses the desktop model picker instead of a slash command.`
|
|
}
|
|
|
|
if (SETTINGS_OWNED_COMMANDS.has(canonical)) {
|
|
return `/${canonical.slice(1)} is managed from the desktop sidebar.`
|
|
}
|
|
|
|
if (MESSAGING_ONLY_COMMANDS.has(canonical)) {
|
|
return `/${canonical.slice(1)} is only used from messaging platforms.`
|
|
}
|
|
|
|
if (ADVANCED_COMMANDS.has(canonical)) {
|
|
return `/${canonical.slice(1)} is not shown in the desktop slash palette. Use the relevant desktop control or terminal interface instead.`
|
|
}
|
|
|
|
if (TERMINAL_ONLY_COMMANDS.has(normalized) || TERMINAL_ONLY_COMMANDS.has(canonical)) {
|
|
return `/${canonical.slice(1)} is only available in the terminal interface.`
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function desktopSlashDescription(command: string, fallback = ''): string {
|
|
const canonical = canonicalDesktopSlashCommand(command)
|
|
|
|
return DESKTOP_COMMAND_DESCRIPTIONS.get(canonical) || fallback
|
|
}
|
|
|
|
export function desktopSkinSlashCompletions(
|
|
themes: DesktopThemeCommandOption[],
|
|
activeThemeName: string,
|
|
argPrefix: string
|
|
): DesktopSlashCompletion[] {
|
|
const prefix = argPrefix.trim().toLowerCase()
|
|
|
|
const commands: DesktopSlashCompletion[] = [
|
|
{
|
|
text: '/skin list',
|
|
display: '/skin list',
|
|
meta: 'Show available desktop themes'
|
|
},
|
|
{
|
|
text: '/skin next',
|
|
display: '/skin next',
|
|
meta: 'Cycle to the next desktop theme'
|
|
},
|
|
...themes.map(theme => ({
|
|
text: `/skin ${theme.name}`,
|
|
display: `/skin ${theme.name}`,
|
|
meta: `${theme.label}${theme.name === activeThemeName ? ' (current)' : ''} - ${theme.description}`
|
|
}))
|
|
]
|
|
|
|
if (!prefix) {
|
|
return commands
|
|
}
|
|
|
|
return commands.filter(item => item.text.slice('/skin '.length).toLowerCase().startsWith(prefix))
|
|
}
|
|
|
|
export function filterDesktopCommandsCatalog(catalog: CommandsCatalogLike): CommandsCatalogLike {
|
|
const categories = catalog.categories
|
|
?.map(section => ({
|
|
...section,
|
|
pairs: section.pairs
|
|
.filter(([command]) => isDesktopSlashSuggestion(command))
|
|
.map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string])
|
|
}))
|
|
.filter(section => section.pairs.length > 0)
|
|
|
|
const pairs = catalog.pairs
|
|
?.filter(([command]) => isDesktopSlashSuggestion(command))
|
|
.map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string])
|
|
|
|
return {
|
|
...catalog,
|
|
...(categories ? { categories } : {}),
|
|
...(pairs ? { pairs } : {})
|
|
}
|
|
}
|
|
|
|
function isKnownHermesSlashCommand(command: string): boolean {
|
|
return DESKTOP_COMMANDS.has(command) || DESKTOP_ALIASES.has(command) || BLOCKED_COMMANDS.has(command)
|
|
}
|