hook notifs

This commit is contained in:
2026-03-31 12:29:47 +01:00
parent 63caa82199
commit 85632c2e29
+32 -41
View File
@@ -1,11 +1,9 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { existsSync, readFileSync, statSync } from "node:fs";
import { spawn } from "node:child_process";
import * as os from "node:os";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";
const HOOK_TIMEOUT_MS = 10 * 60 * 1000;
const LOG_FILE = join(os.homedir(), ".pi", "hooks-resolution.log");
type HookEventName = "PostToolUse" | "PostToolUseFailure";
@@ -30,18 +28,6 @@ type CommandRunResult = {
timedOut: boolean;
};
function logHook(message: string): void {
const line = `[${new Date().toISOString()}] ${message}`;
console.error(`[hooks-resolution] ${line}`);
try {
mkdirSync(dirname(LOG_FILE), { recursive: true });
appendFileSync(LOG_FILE, `${line}\n`);
} catch {
// ignore logging failures
}
}
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
@@ -471,6 +457,23 @@ function hookEventNameForResult(event: ToolResultEvent): HookEventName {
return event.isError ? "PostToolUseFailure" : "PostToolUse";
}
function formatDuration(elapsedMs: number): string {
if (elapsedMs < 1000) {
return `${elapsedMs}ms`;
}
return `${(elapsedMs / 1000).toFixed(1)}s`;
}
function hookName(command: string): string {
const shPathMatch = command.match(/[^\s|;&]+\.sh\b/);
if (shPathMatch) {
return basename(shPathMatch[0]);
}
const firstToken = command.trim().split(/\s+/)[0] ?? "hook";
return basename(firstToken);
}
export default function(pi: ExtensionAPI) {
let state: HookState = {
projectDir: process.cwd(),
@@ -479,13 +482,6 @@ export default function(pi: ExtensionAPI) {
const refreshHooks = (cwd: string) => {
state = loadHooks(cwd);
const postCount = state.hooks.filter((hook) => hook.eventName === "PostToolUse").length;
const failureCount = state.hooks.filter(
(hook) => hook.eventName === "PostToolUseFailure",
).length;
logHook(
`loaded hooks projectDir=${state.projectDir} postToolUse=${postCount} postToolUseFailure=${failureCount}`,
);
};
pi.on("session_start", (_event, ctx) => {
@@ -514,31 +510,26 @@ export default function(pi: ExtensionAPI) {
for (const hook of matchingHooks) {
if (executedCommands.has(hook.command)) {
logHook(
`deduped event=${eventName} tool=${event.toolName} source=${hook.source} command=${JSON.stringify(hook.command)}`,
);
continue;
}
executedCommands.add(hook.command);
logHook(
`run event=${eventName} tool=${event.toolName} matcher=${hook.matcherText ?? "*"} source=${hook.source} command=${JSON.stringify(hook.command)}`,
);
const result = await runCommandHook(hook.command, state.projectDir, payload);
logHook(
`done event=${eventName} tool=${event.toolName} code=${result.code} durationMs=${result.elapsedMs} timedOut=${result.timedOut}`,
);
const name = hookName(hook.command);
const duration = formatDuration(result.elapsedMs);
if (result.code !== 0) {
const matcherLabel = hook.matcherText ?? "*";
const errorLine =
result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
ctx.ui.notify(
`Hook failed (${matcherLabel}) from ${hook.source}: ${errorLine}`,
"warning",
);
if (result.code === 0) {
ctx.ui.notify(`󰛢 Hook \`${name}\` executed, took ${duration}`, "info");
continue;
}
const matcherLabel = hook.matcherText ?? "*";
const errorLine =
result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
ctx.ui.notify(
`󰛢 Hook \`${name}\` failed after ${duration} (${matcherLabel}) from ${hook.source}: ${errorLine}`,
"warning",
);
}
});
}