@fyeeme/pi-hooks 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/index.ts +31 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.0.4] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `denyAsContext` per-hook flag: a deny (`permissionDecision: "deny"` or exit code 2) is demoted to `additionalContext` — the tool call proceeds and the nudge text is injected before the next LLM call instead of blocking the agent (e.g. `serena-hooks remind` stops killing read bursts).
|
|
9
13
|
|
|
10
14
|
## [1.0.3] - 2026-09-09
|
|
11
15
|
|
package/index.ts
CHANGED
|
@@ -51,6 +51,11 @@ interface HookEntry {
|
|
|
51
51
|
command: string;
|
|
52
52
|
/** Per-hook timeout in seconds (Claude Code compatible). Default 60. */
|
|
53
53
|
timeout?: number;
|
|
54
|
+
/** Demote deny (`permissionDecision: "deny"` or exit code 2) to
|
|
55
|
+
* `additionalContext`: the tool call proceeds and the nudge text is
|
|
56
|
+
* injected before the next LLM call instead of blocking the agent
|
|
57
|
+
* (e.g. `serena-hooks remind`). */
|
|
58
|
+
denyAsContext?: boolean;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
interface HookGroup {
|
|
@@ -105,7 +110,12 @@ export function normalizeConfig(raw: unknown): HooksConfig | null {
|
|
|
105
110
|
if (he.type !== "command" || typeof he.command !== "string") continue;
|
|
106
111
|
const timeout =
|
|
107
112
|
typeof he.timeout === "number" && he.timeout > 0 ? he.timeout : undefined;
|
|
108
|
-
entries.push({
|
|
113
|
+
entries.push({
|
|
114
|
+
type: "command",
|
|
115
|
+
command: he.command,
|
|
116
|
+
...(timeout === undefined ? {} : { timeout }),
|
|
117
|
+
...(he.denyAsContext === true ? { denyAsContext: true } : {}),
|
|
118
|
+
});
|
|
109
119
|
}
|
|
110
120
|
if (entries.length > 0) groups.push({ matcher: gr.matcher, hooks: entries });
|
|
111
121
|
}
|
|
@@ -242,6 +252,18 @@ export interface HookResult {
|
|
|
242
252
|
block: string | null;
|
|
243
253
|
}
|
|
244
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Demote a deny to a pure context hint (`denyAsContext` per-hook flag): the
|
|
257
|
+
* tool call proceeds and the nudge text rides along before the next LLM call
|
|
258
|
+
* instead of blocking the agent. Prefers the hook's own `additionalContext`
|
|
259
|
+
* (e.g. serena-hooks remind's "Consider using Serena's symbolic tools...")
|
|
260
|
+
* and falls back to the block reason.
|
|
261
|
+
*/
|
|
262
|
+
export function applyDenyAsContext(result: HookResult): HookResult {
|
|
263
|
+
if (!result.block) return result;
|
|
264
|
+
return { context: result.context ?? result.block, block: null };
|
|
265
|
+
}
|
|
266
|
+
|
|
245
267
|
function emptyResult(): HookResult {
|
|
246
268
|
return { context: null, block: null };
|
|
247
269
|
}
|
|
@@ -282,6 +304,7 @@ async function runCommand(
|
|
|
282
304
|
cwd: string,
|
|
283
305
|
stdinText: string,
|
|
284
306
|
timeoutMs: number,
|
|
307
|
+
denyAsContext: boolean,
|
|
285
308
|
signal?: AbortSignal,
|
|
286
309
|
): Promise<HookResult> {
|
|
287
310
|
return new Promise((resolve) => {
|
|
@@ -377,7 +400,8 @@ async function runCommand(
|
|
|
377
400
|
// parseHookOutput honors it as a block, so don't log it as a failure.
|
|
378
401
|
if (code !== 0 && code !== 2 && code !== null) console.error(`[hooks] exited ${code}: ${command}`);
|
|
379
402
|
const stdout = Buffer.concat(chunks).toString("utf8").trim();
|
|
380
|
-
|
|
403
|
+
const result = parseHookOutput(command, stdout, code);
|
|
404
|
+
finish(denyAsContext ? applyDenyAsContext(result) : result);
|
|
381
405
|
});
|
|
382
406
|
|
|
383
407
|
proc.on("error", (err) => {
|
|
@@ -407,13 +431,14 @@ async function runGroups(
|
|
|
407
431
|
): Promise<{ contexts: string[]; block: string | null }> {
|
|
408
432
|
if (!groups) return { contexts: [], block: null };
|
|
409
433
|
|
|
410
|
-
const commands: Array<{ command: string; timeoutMs: number }> = [];
|
|
434
|
+
const commands: Array<{ command: string; timeoutMs: number; denyAsContext: boolean }> = [];
|
|
411
435
|
for (const group of groups) {
|
|
412
436
|
if (!matchTool(group.matcher, toolName)) continue;
|
|
413
437
|
for (const hook of group.hooks) {
|
|
414
438
|
commands.push({
|
|
415
439
|
command: hook.command,
|
|
416
440
|
timeoutMs: (hook.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
441
|
+
denyAsContext: hook.denyAsContext === true,
|
|
417
442
|
});
|
|
418
443
|
}
|
|
419
444
|
}
|
|
@@ -425,7 +450,9 @@ async function runGroups(
|
|
|
425
450
|
const results: HookResult[] = [];
|
|
426
451
|
for (let i = 0; i < commands.length; i += MAX_CONCURRENT) {
|
|
427
452
|
const batch = commands.slice(i, i + MAX_CONCURRENT);
|
|
428
|
-
results.push(
|
|
453
|
+
results.push(
|
|
454
|
+
...(await Promise.all(batch.map((c) => runCommand(c.command, cwd, stdinText, c.timeoutMs, c.denyAsContext, signal)))),
|
|
455
|
+
);
|
|
429
456
|
}
|
|
430
457
|
|
|
431
458
|
const contexts: string[] = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-hooks",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Claude Code-compatible hooks runner for pi. Reads hooks config (priority: ~/.pi/agent/hooks.json, then project .pi/hooks.json) and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|