Files
hermes/ui-tui/src/app/slash/createSlashOpsHandler.ts
Brooklyn Nicholson cb31732c4f chore: uptick
2026-04-15 23:29:00 -05:00

373 lines
11 KiB
TypeScript

import type { ToolsConfigureResponse, ToolsListResponse, ToolsShowResponse } from '../../gatewayTypes.js'
import { rpcErrorMessage } from '../../lib/rpc.js'
import type { PanelSection } from '../../types.js'
import type { SlashHandlerContext } from '../interfaces.js'
import type { ParsedSlashCommand } from './shared.js'
export function createSlashOpsHandler(ctx: SlashHandlerContext) {
const { rpc } = ctx.gateway
const { resetVisibleHistory, setSessionStartedAt } = ctx.session
const { panel, sys } = ctx.transcript
return ({ arg, cmd, name, sid }: OpsSlashCommand) => {
switch (name) {
case 'rollback': {
const [sub, ...rest] = (arg || 'list').split(/\s+/)
if (!sub || sub === 'list') {
rpc('rollback.list', { session_id: sid }).then((r: any) => {
if (!r) {
return
}
if (!r.checkpoints?.length) {
sys('no checkpoints')
return
}
panel('Checkpoints', [
{
rows: r.checkpoints.map(
(c: any, i: number) => [`${i + 1} ${c.hash?.slice(0, 8)}`, c.message] as [string, string]
)
}
])
})
return true
}
const hash = sub === 'restore' || sub === 'diff' ? rest[0] : sub
const filePath = (sub === 'restore' || sub === 'diff' ? rest.slice(1) : rest).join(' ').trim()
rpc(sub === 'diff' ? 'rollback.diff' : 'rollback.restore', {
session_id: sid,
hash,
...(sub === 'diff' || !filePath ? {} : { file_path: filePath })
}).then((r: any) => r && sys(r.rendered || r.diff || r.message || 'done'))
return true
}
case 'browser': {
const [action, ...rest] = (arg || 'status').split(/\s+/)
rpc('browser.manage', { action, ...(rest[0] ? { url: rest[0] } : {}) }).then(
(r: any) => r && sys(r.connected ? `browser: ${r.url}` : 'browser: disconnected')
)
return true
}
case 'plugins':
rpc('plugins.list', {}).then((r: any) => {
if (!r) {
return
}
if (!r.plugins?.length) {
sys('no plugins')
return
}
panel('Plugins', [
{
items: r.plugins.map((p: any) => `${p.name} v${p.version}${p.enabled ? '' : ' (disabled)'}`)
}
])
})
return true
case 'skills': {
const [sub, ...rest] = (arg || '').split(/\s+/).filter(Boolean)
if (!sub || sub === 'list') {
rpc('skills.manage', { action: 'list' }).then((r: any) => {
if (!r) {
return
}
const skills = r.skills as Record<string, string[]> | undefined
if (!skills || !Object.keys(skills).length) {
sys('no skills installed')
return
}
panel(
'Installed Skills',
Object.entries(skills).map(([title, items]) => ({ items, title }))
)
})
return true
}
if (sub === 'browse') {
const pageNumber = parseInt(rest[0] ?? '1', 10) || 1
rpc('skills.manage', { action: 'browse', page: pageNumber }).then((r: any) => {
if (!r) {
return
}
if (!r.items?.length) {
sys('no skills found in the hub')
return
}
const sections: PanelSection[] = [
{
rows: r.items.map(
(s: any) =>
[s.name ?? '', (s.description ?? '').slice(0, 60) + (s.description?.length > 60 ? '…' : '')] as [
string,
string
]
)
}
]
if (r.page < r.total_pages) {
sections.push({ text: `/skills browse ${r.page + 1} → next page` })
}
if (r.page > 1) {
sections.push({ text: `/skills browse ${r.page - 1} → prev page` })
}
panel(`Skills Hub (page ${r.page}/${r.total_pages}, ${r.total} total)`, sections)
})
return true
}
ctx.gateway.gw
.request('slash.exec', { command: cmd.slice(1), session_id: sid })
.then((r: any) =>
sys(
r?.warning
? `warning: ${r.warning}\n${r?.output || '/skills: no output'}`
: r?.output || '/skills: no output'
)
)
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
}
case 'agents':
case 'tasks':
rpc('agents.list', {})
.then((r: any) => {
if (!r) {
return
}
const processes = r.processes ?? []
const running = processes.filter((p: any) => p.status === 'running')
const finished = processes.filter((p: any) => p.status !== 'running')
const sections: PanelSection[] = []
running.length &&
sections.push({
title: `Running (${running.length})`,
rows: running.map((p: any) => [p.session_id.slice(0, 8), p.command])
})
finished.length &&
sections.push({
title: `Finished (${finished.length})`,
rows: finished.map((p: any) => [p.session_id.slice(0, 8), p.command])
})
!sections.length && sections.push({ text: 'No active processes' })
panel('Agents', sections)
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
case 'cron':
if (!arg || arg === 'list') {
rpc('cron.manage', { action: 'list' })
.then((r: any) => {
if (!r) {
return
}
const jobs = r.jobs ?? []
if (!jobs.length) {
sys('no scheduled jobs')
return
}
panel('Cron', [
{
rows: jobs.map(
(j: any) =>
[j.name || j.job_id?.slice(0, 12), `${j.schedule} · ${j.state ?? 'active'}`] as [string, string]
)
}
])
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
} else {
ctx.gateway.gw
.request('slash.exec', { command: cmd.slice(1), session_id: sid })
.then((r: any) =>
sys(r?.warning ? `warning: ${r.warning}\n${r?.output || '(no output)'}` : r?.output || '(no output)')
)
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
}
return true
case 'config':
rpc('config.show', {})
.then((r: any) => {
if (!r) {
return
}
panel(
'Config',
(r.sections ?? []).map((s: any) => ({
title: s.title,
rows: s.rows
}))
)
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
case 'tools': {
const [subcommand, ...names] = arg.trim().split(/\s+/).filter(Boolean)
if (!subcommand) {
rpc<ToolsShowResponse>('tools.show', { session_id: sid })
.then(r => {
if (!r?.sections?.length) {
sys('no tools')
return
}
panel(
`Tools${typeof r.total === 'number' ? ` (${r.total})` : ''}`,
r.sections.map(section => ({
title: section.name,
rows: section.tools.map(tool => [tool.name, tool.description] as [string, string])
}))
)
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
}
if (subcommand === 'list') {
rpc<ToolsListResponse>('tools.list', { session_id: sid })
.then(r => {
if (!r?.toolsets?.length) {
sys('no tools')
return
}
panel(
'Tools',
r.toolsets.map(ts => ({
title: `${ts.enabled ? '*' : ' '} ${ts.name} [${ts.tool_count} tools]`,
items: ts.tools
}))
)
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
}
if (subcommand === 'disable' || subcommand === 'enable') {
if (!names.length) {
sys(`usage: /tools ${subcommand} <name> [name ...]`)
sys(`built-in toolset: /tools ${subcommand} web`)
sys(`MCP tool: /tools ${subcommand} github:create_issue`)
return true
}
rpc<ToolsConfigureResponse>('tools.configure', {
action: subcommand,
names,
session_id: sid
})
.then(r => {
if (!r) {
return
}
if (r.info) {
setSessionStartedAt(Date.now())
resetVisibleHistory(r.info)
}
r.changed?.length && sys(`${subcommand === 'disable' ? 'disabled' : 'enabled'}: ${r.changed.join(', ')}`)
r.unknown?.length && sys(`unknown toolsets: ${r.unknown.join(', ')}`)
r.missing_servers?.length && sys(`missing MCP servers: ${r.missing_servers.join(', ')}`)
r.reset && sys('session reset. new tool configuration is active.')
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
}
sys('usage: /tools [list|disable|enable] ...')
return true
}
case 'toolsets':
rpc('toolsets.list', { session_id: sid })
.then((r: any) => {
if (!r) {
return
}
if (!r.toolsets?.length) {
sys('no toolsets')
return
}
panel('Toolsets', [
{
rows: r.toolsets.map(
(ts: any) =>
[`${ts.enabled ? '(*)' : ' '} ${ts.name}`, `[${ts.tool_count}] ${ts.description}`] as [
string,
string
]
)
}
])
})
.catch((e: unknown) => sys(`error: ${rpcErrorMessage(e)}`))
return true
}
return false
}
}
interface OpsSlashCommand extends ParsedSlashCommand {
sid: null | string
}