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 | 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('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('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 ...]`) sys(`built-in toolset: /tools ${subcommand} web`) sys(`MCP tool: /tools ${subcommand} github:create_issue`) return true } rpc('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 }