import type { PilotDeckHookOutput, PilotDeckHookSpecificOutput } from "../protocol/output.js";
export function parseHookOutput(stdout: string): PilotDeckHookOutput {
const parsed = parseFirstJsonLine(stdout);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { type: "sync" };
}
const record = parsed as Record<string, unknown>;
if (record.async === true) {
return { type: "async", raw: parsed };
}
return {
type: "sync",
continue: booleanOrUndefined(record.continue),
suppressOutput: booleanOrUndefined(record.suppressOutput),
stopReason: stringOrUndefined(record.stopReason),
decision: record.decision === "approve" || record.decision === "block" ? record.decision : undefined,
reason: stringOrUndefined(record.reason),
systemMessage: stringOrUndefined(record.systemMessage),
specific: parseSpecificOutput(record.hookSpecificOutput),
raw: parsed,
};
}
function parseFirstJsonLine(stdout: string): unknown | undefined {
for (const line of stdout.split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed.startsWith("{")) {
continue;
}
try {
return JSON.parse(trimmed) as unknown;
} catch {
return undefined;
}
}
return undefined;
}
function parseSpecificOutput(value: unknown): PilotDeckHookSpecificOutput | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const record = value as Record<string, unknown>;
if (typeof record.hookEventName !== "string") {
return undefined;
}
return {
hookEventName: record.hookEventName,
additionalContext: stringOrUndefined(record.additionalContext),
initialUserMessage: stringOrUndefined(record.initialUserMessage),
watchPaths: Array.isArray(record.watchPaths)
? record.watchPaths.filter((item): item is string => typeof item === "string")
: undefined,
permissionDecision: parsePermissionDecision(record.permissionDecision),
permissionDecisionReason: stringOrUndefined(record.permissionDecisionReason),
updatedInput: isRecord(record.updatedInput) ? record.updatedInput : undefined,
updatedMCPToolOutput: record.updatedMCPToolOutput,
decision: parsePermissionRequestDecision(record.decision),
retry: booleanOrUndefined(record.retry),
worktreePath: stringOrUndefined(record.worktreePath),
};
}
function parsePermissionDecision(value: unknown): PilotDeckHookSpecificOutput["permissionDecision"] {
return value === "allow" || value === "deny" || value === "ask" || value === "passthrough" ? value : undefined;
}
function parsePermissionRequestDecision(value: unknown): PilotDeckHookSpecificOutput["decision"] {
if (!isRecord(value)) {
return undefined;
}
if (value.behavior === "allow") {
return {
behavior: "allow",
updatedInput: isRecord(value.updatedInput) ? value.updatedInput : undefined,
updatedPermissions: Array.isArray(value.updatedPermissions) ? value.updatedPermissions : undefined,
};
}
if (value.behavior === "deny") {
return {
behavior: "deny",
message: stringOrUndefined(value.message),
interrupt: booleanOrUndefined(value.interrupt),
};
}
return undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stringOrUndefined(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function booleanOrUndefined(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}