/** * Runner for built-in diagnostic skills. * * Fetches the skill by id, filters its steps for the target session's shell * family, executes each step sequentially via the bridge, and assembles a * single structured report with labels, stdout/stderr, and exit codes. */ import { getBuiltinSkill, filterStepsForShell, type SkillStep } from './builtinSkills'; import type { ToolDeps, ToolExecResult } from '../shared/toolExecutors'; interface RunOptions { chatSessionId?: string; } interface StepResult { label: string; command: string; stdout: string; stderr: string; exitCode: number | null; durationMs: number; ok: boolean; } export interface SkillRunReport { skill: string; sessionId: string; shellType?: string; os?: string; hostname?: string; startedAt: string; completedAt: string; durationMs: number; steps: StepResult[]; } export async function executeSkillRun( deps: ToolDeps, args: Record, options: RunOptions = {}, ): Promise> { const { bridge, context, permissionMode } = deps; const resolveContext = () => (typeof context === 'function' ? context() : context); const ctx = resolveContext(); const sessionId = typeof args.sessionId === 'string' ? args.sessionId.trim() : ''; const skillName = typeof args.skillName === 'string' ? args.skillName.trim() : ''; if (!sessionId) { return { ok: false, error: 'Missing sessionId.' }; } if (!skillName) { return { ok: false, error: 'Missing skillName.' }; } if (permissionMode === 'observer') { return { ok: false, error: 'Observer mode: skill execution is disabled. Switch to Confirm or Auto mode.' }; } // Validate session is in scope const session = ctx.sessions.find(s => s.sessionId === sessionId); if (!session) { return { ok: false, error: `Session "${sessionId}" is not in the current AI scope.` }; } // Resolve skill const skill = getBuiltinSkill(skillName); if (!skill) { const available = Object.keys((await import('./builtinSkills')).BUILTIN_SKILLS).join(', '); return { ok: false, error: `Unknown skill "${skillName}". Available: ${available}` }; } // Filter steps for this shell family const steps = filterStepsForShell(skill, session.shellType); if (steps.length === 0) { return { ok: false, error: `Skill "${skill.id}" has no steps for shell family "${session.shellType ?? 'unknown'}".`, }; } // Run each step sequentially, capture everything const startedAt = new Date(); const stepResults: StepResult[] = []; for (const step of steps) { const stepResult = await runStep(bridge, sessionId, step, options.chatSessionId); stepResults.push(stepResult); } const completedAt = new Date(); const report: SkillRunReport = { skill: skill.id, sessionId, shellType: session.shellType, os: session.os, hostname: session.hostname, startedAt: startedAt.toISOString(), completedAt: completedAt.toISOString(), durationMs: completedAt.getTime() - startedAt.getTime(), steps: stepResults, }; return { ok: true, data: report }; } async function runStep( bridge: NonNullable, sessionId: string, step: SkillStep, chatSessionId?: string, ): Promise { const t0 = Date.now(); try { const result = await bridge.aiExec(sessionId, step.command, chatSessionId); const durationMs = Date.now() - t0; if (!result.ok && result.error) { return { label: step.label, command: step.command, stdout: result.stdout || '', stderr: `[exec error] ${result.error}`, exitCode: null, durationMs, ok: false, }; } return { label: step.label, command: step.command, stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode ?? -1, durationMs, ok: (result.exitCode ?? 0) === 0, }; } catch (err) { const durationMs = Date.now() - t0; return { label: step.label, command: step.command, stdout: '', stderr: `[exception] ${err instanceof Error ? err.message : String(err)}`, exitCode: null, durationMs, ok: false, }; } }