153 lines
4.2 KiB
TypeScript
153 lines
4.2 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string, unknown>,
|
||
|
|
options: RunOptions = {},
|
||
|
|
): Promise<ToolExecResult<SkillRunReport>> {
|
||
|
|
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<ToolDeps['bridge']>,
|
||
|
|
sessionId: string,
|
||
|
|
step: SkillStep,
|
||
|
|
chatSessionId?: string,
|
||
|
|
): Promise<StepResult> {
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|