mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(tui): route /skills subcommands through skills.manage instead of curses slash.exec
/skills install, inspect, search, browse, list now call the typed skills.manage RPC and render results via panel/page. Previously they fell through to slash.exec which invokes v1's curses code path — that hangs or crashes inside the Ink worker per the §2 parity-audit finding. Also drop Enter-as-install from the Skills Hub action stage since the Hub lists locally installed skills; primary action is inspect-and-close. x still triggers a manual reinstall for power users.
This commit is contained in:
parent
949b8f5521
commit
5e148ca3d0
3 changed files with 198 additions and 22 deletions
|
|
@ -1,26 +1,158 @@
|
|||
import type { SlashExecResponse, ToolsConfigureResponse } from '../../../gatewayTypes.js'
|
||||
import type { ToolsConfigureResponse } from '../../../gatewayTypes.js'
|
||||
import type { PanelSection } from '../../../types.js'
|
||||
import { patchOverlayState } from '../../overlayStore.js'
|
||||
import type { SlashCommand } from '../types.js'
|
||||
|
||||
interface SkillInfo {
|
||||
category?: string
|
||||
description?: string
|
||||
name?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
interface SkillsListResponse {
|
||||
skills?: Record<string, string[]>
|
||||
}
|
||||
|
||||
interface SkillsInspectResponse {
|
||||
info?: SkillInfo
|
||||
}
|
||||
|
||||
interface SkillsSearchResponse {
|
||||
results?: { description?: string; name: string }[]
|
||||
}
|
||||
|
||||
interface SkillsInstallResponse {
|
||||
installed?: boolean
|
||||
name?: string
|
||||
}
|
||||
|
||||
export const opsCommands: SlashCommand[] = [
|
||||
{
|
||||
help: 'browse, inspect, and install skills',
|
||||
help: 'browse, inspect, install skills',
|
||||
name: 'skills',
|
||||
run: (arg, ctx) => {
|
||||
if (!arg.trim()) {
|
||||
const text = arg.trim()
|
||||
|
||||
if (!text) {
|
||||
return patchOverlayState({ skillsHub: true })
|
||||
}
|
||||
|
||||
ctx.gateway
|
||||
.rpc<SlashExecResponse>('slash.exec', { command: `skills ${arg}`, session_id: ctx.sid })
|
||||
.then(
|
||||
ctx.guarded<SlashExecResponse>(r => {
|
||||
if (r.output) {
|
||||
ctx.transcript.page(r.output, 'Skills')
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
const [sub, ...rest] = text.split(/\s+/)
|
||||
const query = rest.join(' ').trim()
|
||||
const { rpc } = ctx.gateway
|
||||
const { page, panel, sys } = ctx.transcript
|
||||
|
||||
if (sub === 'list') {
|
||||
rpc<SkillsListResponse>('skills.manage', { action: 'list' })
|
||||
.then(
|
||||
ctx.guarded<SkillsListResponse>(r => {
|
||||
const cats = Object.entries(r.skills ?? {}).sort()
|
||||
|
||||
if (!cats.length) {
|
||||
return sys('no skills available')
|
||||
}
|
||||
|
||||
panel(
|
||||
'Skills',
|
||||
cats.map<PanelSection>(([title, items]) => ({ items, title }))
|
||||
)
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'inspect') {
|
||||
if (!query) {
|
||||
return sys('usage: /skills inspect <name>')
|
||||
}
|
||||
|
||||
rpc<SkillsInspectResponse>('skills.manage', { action: 'inspect', query })
|
||||
.then(
|
||||
ctx.guarded<SkillsInspectResponse>(r => {
|
||||
const info = r.info ?? {}
|
||||
|
||||
if (!info.name) {
|
||||
return sys(`unknown skill: ${query}`)
|
||||
}
|
||||
|
||||
const rows: [string, string][] = [
|
||||
['Name', String(info.name)],
|
||||
['Category', String(info.category ?? '')],
|
||||
['Path', String(info.path ?? '')]
|
||||
]
|
||||
|
||||
const sections: PanelSection[] = [{ rows }]
|
||||
|
||||
if (info.description) {
|
||||
sections.push({ text: String(info.description) })
|
||||
}
|
||||
|
||||
panel('Skill', sections)
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'search') {
|
||||
if (!query) {
|
||||
return sys('usage: /skills search <query>')
|
||||
}
|
||||
|
||||
rpc<SkillsSearchResponse>('skills.manage', { action: 'search', query })
|
||||
.then(
|
||||
ctx.guarded<SkillsSearchResponse>(r => {
|
||||
const results = r.results ?? []
|
||||
|
||||
if (!results.length) {
|
||||
return sys(`no results for: ${query}`)
|
||||
}
|
||||
|
||||
panel(`Search: ${query}`, [{ rows: results.map(s => [s.name, s.description ?? '']) }])
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'install') {
|
||||
if (!query) {
|
||||
return sys('usage: /skills install <name or url>')
|
||||
}
|
||||
|
||||
sys(`installing ${query}…`)
|
||||
|
||||
rpc<SkillsInstallResponse>('skills.manage', { action: 'install', query })
|
||||
.then(
|
||||
ctx.guarded<SkillsInstallResponse>(r =>
|
||||
sys(r.installed ? `installed ${r.name ?? query}` : 'install failed')
|
||||
)
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'browse') {
|
||||
const pageNum = parseInt(query, 10) || 1
|
||||
|
||||
rpc<Record<string, unknown>>('skills.manage', { action: 'browse', page: pageNum })
|
||||
.then(
|
||||
ctx.guarded<Record<string, unknown>>(r =>
|
||||
page(JSON.stringify(r, null, 2).slice(0, 4000), `Browse Skills — p${pageNum}`)
|
||||
)
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sys('usage: /skills [list | inspect <n> | install <n> | search <q> | browse [page]]')
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue