// skills/explainshell/index.ts // Programmatic handler — fetch and parse shell command explanations from explainshell.com. import { ApiSkill } from '../../core/apiSkillBase' const skill = new ApiSkill({ name: 'explainshell', baseUrl: 'https://explainshell.com', authType: 'none', rateLimit: { requests: 5, windowMs: 1_000 }, timeout: 15_000, retries: 2, }) // ── HTML parsing helpers ────────────────────────────────────── /** Strip HTML tags from a string. */ function stripTags(html: string): string { return html .replace(/<[^>]+>/g, '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim() } /** * Extract explanation fragments from explainshell HTML. * * The page contains elements like: * explanation text... *
...
* as well as argument tokens in .... * * We extract helptext spans and pair them with their nearest * argument token for a readable breakdown. */ function parseExplainshellHtml(html: string): string[] { const results: string[] = [] // Match each helptext block — may span multiple lines const helptextRe = /]*class="helptext"[^>]*>([\s\S]*?)<\/span>/gi let match: RegExpExecArray | null while ((match = helptextRe.exec(html)) !== null) { const text = stripTags(match[1]) if (text.length > 2) results.push(text) } // If helptext approach yields nothing, try extracting from help-box divs if (results.length === 0) { const boxRe = /]*class="[^"]*help-box[^"]*"[^>]*>([\s\S]*?)<\/div>/gi while ((match = boxRe.exec(html)) !== null) { const text = stripTags(match[1]) if (text.length > 5) results.push(text) } } // Deduplicate while preserving order const seen = new Set() return results.filter(r => { const k = r.slice(0, 80) if (seen.has(k)) return false seen.add(k) return true }) } /** Check if the HTML indicates the command was not found. */ function isNotFound(html: string): boolean { return /no match for/i.test(html) || /couldn.t parse/i.test(html) } // ── Public API ──────────────────────────────────────────────── export interface ExplainResult { command: string url: string breakdown: string[] // plain-text explanations, one per flag/segment parsed: boolean // false if HTML parsing yielded nothing (URL still valid) } /** * Fetch a plain-English breakdown of a shell command. * * @param command The shell command to explain (e.g. "tar -xzf file.tar.gz") * @returns ExplainResult with URL and parsed breakdown array */ export async function explain(command: string): Promise { const cmd = command.trim() const encoded = encodeURIComponent(cmd) const url = `https://explainshell.com/explain?cmd=${encoded}` let breakdown: string[] = [] let parsed = false try { const html = await skill.get('/explain', { cmd }) if (typeof html === 'string') { if (isNotFound(html)) { breakdown = [`explainshell did not recognise "${cmd}" — try visiting the URL directly.`] } else { breakdown = parseExplainshellHtml(html) parsed = breakdown.length > 0 if (!parsed) { breakdown = ['Explanation available at the URL below — could not parse the response automatically.'] } } } } catch { breakdown = ['Could not reach explainshell.com — visit the URL manually.'] } return { command: cmd, url, breakdown, parsed } } /** Format an ExplainResult as a readable string. */ export function formatExplanation(result: ExplainResult): string { const lines = [`Command: ${result.command}`, ''] if (result.breakdown.length > 0) { result.breakdown.forEach(b => lines.push(` • ${b}`)) } lines.push('', `Full explanation: ${result.url}`) return lines.join('\n') }