@fyeeme/pi-hooks 1.0.0
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 +21 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/index.ts +294 -0
- package/package.json +54 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2025-05-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release
|
|
15
|
+
- Claude Code-compatible hooks runner for pi
|
|
16
|
+
- SessionStart hooks via `before_agent_start` with context injection
|
|
17
|
+
- PreToolUse hooks via `tool_call` with queued context injection
|
|
18
|
+
- Stop hooks via `session_shutdown`
|
|
19
|
+
- All additionalContext injected via `context` event into last user message
|
|
20
|
+
- Config loaded from `.pi/hooks.json` or `PI_HOOKS_CONFIG` env var
|
|
21
|
+
- Glob matching for tool name matchers
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# pi-hooks
|
|
2
|
+
|
|
3
|
+
A Claude Code-compatible hooks runner for [pi](https://pi.dev). Reads `.pi/hooks.json` from your project and maps `SessionStart`, `PreToolUse`, and `Stop` events to pi lifecycle events — matching Claude Code's hooks protocol including stdin JSON and stdout `additionalContext` capture.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires the [pi](https://pi.dev) CLI.
|
|
8
|
+
|
|
9
|
+
### From npm (recommended)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Global (user) install — available in every project
|
|
13
|
+
pi install npm:@fyeeme/pi-hooks
|
|
14
|
+
|
|
15
|
+
# Project-local — written to .pi/settings.json, shareable with your team
|
|
16
|
+
pi install -l npm:@fyeeme/pi-hooks
|
|
17
|
+
|
|
18
|
+
# Pinned version — skipped by `pi update`
|
|
19
|
+
pi install npm:@fyeeme/pi-hooks@1.0.0
|
|
20
|
+
|
|
21
|
+
# Try it once without saving (current run only)
|
|
22
|
+
pi -e npm:@fyeeme/pi-hooks
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From GitHub
|
|
26
|
+
|
|
27
|
+
Source: [`fyeeme/pi-packages`](https://github.com/fyeeme/pi-packages).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# HTTPS shorthand
|
|
31
|
+
pi install git:github.com/fyeeme/pi-packages
|
|
32
|
+
# Pin to a tag or commit (skipped by `pi update`)
|
|
33
|
+
pi install git:github.com/fyeeme/pi-packages@v1.0.0
|
|
34
|
+
# Raw URL form
|
|
35
|
+
pi install https://github.com/fyeeme/pi-packages
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the Pi Packages guide on [pi.dev](https://pi.dev) for the full list of source types, scopes, and `pi update` behavior.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Create `.pi/hooks.json` in your project root:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"hooks": {
|
|
47
|
+
"SessionStart": [
|
|
48
|
+
{
|
|
49
|
+
"matcher": "",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "serena-hooks activate --client=claude-code"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"PreToolUse": [
|
|
59
|
+
{
|
|
60
|
+
"matcher": "",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "serena-hooks remind --client=claude-code"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"matcher": "plugin_serena_serena_*",
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "serena-hooks auto-approve --client=claude-code"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"Stop": [
|
|
79
|
+
{
|
|
80
|
+
"matcher": "",
|
|
81
|
+
"hooks": [
|
|
82
|
+
{
|
|
83
|
+
"type": "command",
|
|
84
|
+
"command": "serena-hooks cleanup --client=claude-code"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Event Mapping
|
|
94
|
+
|
|
95
|
+
| hooks.json event | pi event | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `SessionStart` | `session_start` | Runs on startup. `additionalContext` sent via `sendUserMessage`. |
|
|
98
|
+
| `PreToolUse` (empty matcher) | `tool_call` | Runs before every tool with real `tool_name`. `additionalContext` injected before next LLM call. |
|
|
99
|
+
| `PreToolUse` (pattern matcher) | `tool_call` | Glob match against pi tool name (e.g. `plugin_serena_serena_*`). |
|
|
100
|
+
| `Stop` | `session_shutdown` | Runs on exit. |
|
|
101
|
+
|
|
102
|
+
## Protocol
|
|
103
|
+
|
|
104
|
+
Commands receive Claude Code-compatible JSON on stdin:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{ "type": "session_start", "session_id": "...", "transcript_path": "..." }
|
|
108
|
+
{ "type": "pre_tool_use", "session_id": "...", "tool_name": "bash", "tool_input": {} }
|
|
109
|
+
{ "type": "stop", "session_id": "..." }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Commands may return JSON on stdout:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "hookSpecificOutput": { "additionalContext": "..." } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `additionalContext` is injected into the pi conversation.
|
|
119
|
+
|
|
120
|
+
## MCP Tool Names
|
|
121
|
+
|
|
122
|
+
Pi names MCP tools as `<serverName>_<toolName>` (not `mcp__server__tool` like Claude Code). Check your actual tool names with `/mcp` in pi to set the correct `matcher`.
|
|
123
|
+
|
|
124
|
+
## Config Override
|
|
125
|
+
|
|
126
|
+
Set `PI_HOOKS_CONFIG` env var to point to a custom config path.
|
package/index.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-hooks
|
|
3
|
+
*
|
|
4
|
+
* Claude Code-compatible hooks runner for pi.
|
|
5
|
+
*
|
|
6
|
+
* Reads `.pi/hooks.json` (or `PI_HOOKS_CONFIG` env) and maps:
|
|
7
|
+
* SessionStart → before_agent_start (first turn)
|
|
8
|
+
* PreToolUse → tool_call
|
|
9
|
+
* Stop → session_shutdown
|
|
10
|
+
*
|
|
11
|
+
* All additionalContext—from both SessionStart and PreToolUse—is injected
|
|
12
|
+
* into the last user message via the context event. This guarantees the LLM
|
|
13
|
+
* sees and acts on the context without extra turns, fake user messages, or
|
|
14
|
+
* system prompt passivity.
|
|
15
|
+
*
|
|
16
|
+
* Sequence guarantee:
|
|
17
|
+
* emitBeforeAgentStart() is awaited by agent-session.ts before
|
|
18
|
+
* _runAgentPrompt() starts, so before_agent_start always completes
|
|
19
|
+
* before the first context event fires. No race condition.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Config schema
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
interface HookEntry {
|
|
32
|
+
type: "command";
|
|
33
|
+
command: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface HookGroup {
|
|
37
|
+
matcher: string;
|
|
38
|
+
hooks: HookEntry[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface HooksConfig {
|
|
42
|
+
hooks: {
|
|
43
|
+
SessionStart?: HookGroup[];
|
|
44
|
+
PreToolUse?: HookGroup[];
|
|
45
|
+
Stop?: HookGroup[];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface HookOutput {
|
|
50
|
+
hookSpecificOutput?: {
|
|
51
|
+
additionalContext?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Glob matching
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
export function globMatch(pattern: string, value: string): boolean {
|
|
60
|
+
if (pattern === "") return true;
|
|
61
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
62
|
+
const regexStr = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
63
|
+
return new RegExp(`^${regexStr}$`).test(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Config loader - cached per session via ??=
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export function loadConfig(cwd: string): HooksConfig | null {
|
|
71
|
+
const envPath = process.env.PI_HOOKS_CONFIG;
|
|
72
|
+
const candidates = envPath
|
|
73
|
+
? [envPath]
|
|
74
|
+
: [join(cwd, ".pi", "hooks.json"), join(process.env.HOME ?? "", ".pi", "hooks.json")];
|
|
75
|
+
|
|
76
|
+
for (const p of candidates) {
|
|
77
|
+
if (!existsSync(p)) continue;
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(p, "utf-8")) as HooksConfig;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(`[hooks] failed to parse ${p}: ${err}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Session ID
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
function getSessionId(ctx: ExtensionContext): string {
|
|
92
|
+
try {
|
|
93
|
+
const sm = ctx.sessionManager as { getSessionFile?: () => string | undefined };
|
|
94
|
+
const file = sm.getSessionFile?.();
|
|
95
|
+
if (file) return file;
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
return `${ctx.cwd}:${Date.now()}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Command runner - pipes JSON to stdin, captures stdout JSON
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
async function runCommand(command: string, cwd: string, stdinJson: unknown): Promise<HookOutput | null> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const proc = spawn(command, [], {
|
|
107
|
+
shell: true,
|
|
108
|
+
cwd,
|
|
109
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let stdout = "";
|
|
113
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
114
|
+
stdout += chunk.toString();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const timer = setTimeout(() => proc.kill("SIGTERM"), 10_000);
|
|
118
|
+
|
|
119
|
+
proc.on("close", (code) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
if (code !== 0) console.error(`[hooks] exited ${code}: ${command}`);
|
|
122
|
+
try {
|
|
123
|
+
const trimmed = stdout.trim();
|
|
124
|
+
resolve(trimmed ? (JSON.parse(trimmed) as HookOutput) : null);
|
|
125
|
+
} catch {
|
|
126
|
+
resolve(null);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
proc.on("error", (err) => {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
console.error(`[hooks] spawn error: ${command}: ${err}`);
|
|
133
|
+
resolve(null);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
proc.stdin?.write(JSON.stringify(stdinJson));
|
|
138
|
+
proc.stdin?.end();
|
|
139
|
+
} catch { /* already exited */ }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Run matching hook groups, return collected additionalContext strings
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
async function runGroups(
|
|
148
|
+
groups: HookGroup[] | undefined,
|
|
149
|
+
toolName: string,
|
|
150
|
+
cwd: string,
|
|
151
|
+
stdinJson: unknown,
|
|
152
|
+
): Promise<string[]> {
|
|
153
|
+
const contexts: string[] = [];
|
|
154
|
+
if (!groups) return contexts;
|
|
155
|
+
for (const group of groups) {
|
|
156
|
+
if (!globMatch(group.matcher, toolName)) continue;
|
|
157
|
+
for (const hook of group.hooks) {
|
|
158
|
+
if (hook.type !== "command") continue;
|
|
159
|
+
const out = await runCommand(hook.command, cwd, stdinJson);
|
|
160
|
+
const ctx = out?.hookSpecificOutput?.additionalContext;
|
|
161
|
+
if (ctx) contexts.push(ctx);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return contexts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Extension
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
export default function (pi: ExtensionAPI): void {
|
|
172
|
+
let config: HooksConfig | null | undefined;
|
|
173
|
+
|
|
174
|
+
// SessionStart additionalContext waiting to be injected on the first LLM call.
|
|
175
|
+
// Set by before_agent_start (which is awaited before the agent loop starts),
|
|
176
|
+
// consumed once by the context handler.
|
|
177
|
+
let activateContext: string | null = null;
|
|
178
|
+
let activated = false;
|
|
179
|
+
|
|
180
|
+
// PreToolUse additionalContext queued by tool_call, injected before each LLM call.
|
|
181
|
+
const pendingContexts: string[] = [];
|
|
182
|
+
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// before_agent_start: SessionStart hooks (first turn only).
|
|
185
|
+
//
|
|
186
|
+
// Runs the hook and stores additionalContext for injection. Does NOT return
|
|
187
|
+
// a message or modify the system prompt—injection happens in context so all
|
|
188
|
+
// LLM message modification is in one place and the activate context is
|
|
189
|
+
// treated identically to remind context by the LLM.
|
|
190
|
+
//
|
|
191
|
+
// Sequencing: agent-session.ts awaits emitBeforeAgentStart() before calling
|
|
192
|
+
// _runAgentPrompt(), so this handler always completes before context fires.
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
195
|
+
config ??= loadConfig(ctx.cwd);
|
|
196
|
+
if (!config || activated) return;
|
|
197
|
+
activated = true;
|
|
198
|
+
|
|
199
|
+
const stdin = {
|
|
200
|
+
type: "session_start",
|
|
201
|
+
session_id: getSessionId(ctx),
|
|
202
|
+
transcript_path: getSessionId(ctx),
|
|
203
|
+
};
|
|
204
|
+
const contexts = await runGroups(config.hooks.SessionStart, "", ctx.cwd, stdin);
|
|
205
|
+
if (contexts.length > 0) {
|
|
206
|
+
activateContext = contexts.join("\n\n");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// context: inject all pending contexts before each LLM call.
|
|
212
|
+
//
|
|
213
|
+
// Combines activate (SessionStart) and remind (PreToolUse) contexts.
|
|
214
|
+
// Appends to the last user message's content array—never adds a new message—
|
|
215
|
+
// so there are no consecutive-user-message issues and no extra turns.
|
|
216
|
+
// The injected text is invisible to the display layer (UI shows original).
|
|
217
|
+
// -------------------------------------------------------------------------
|
|
218
|
+
pi.on("context", (event) => {
|
|
219
|
+
const toInject: string[] = [];
|
|
220
|
+
|
|
221
|
+
if (activateContext !== null) {
|
|
222
|
+
toInject.push(activateContext);
|
|
223
|
+
activateContext = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (pendingContexts.length > 0) {
|
|
227
|
+
toInject.push(...pendingContexts.splice(0));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (toInject.length === 0) return;
|
|
231
|
+
|
|
232
|
+
const text = toInject.join("\n\n");
|
|
233
|
+
const messages = [...event.messages];
|
|
234
|
+
const lastUserIdx = messages.findLastIndex((m) => (m as { role: string }).role === "user");
|
|
235
|
+
|
|
236
|
+
if (lastUserIdx >= 0) {
|
|
237
|
+
// Append to the last user message's content array. pi-hooks operates on
|
|
238
|
+
// messages structurally (any role:"user" message whose content is an
|
|
239
|
+
// array); it does not depend on a specific message variant type, so we
|
|
240
|
+
// bridge through `unknown` rather than asserting an incompatible shape.
|
|
241
|
+
const last = messages[lastUserIdx] as unknown as {
|
|
242
|
+
role: string;
|
|
243
|
+
content: unknown[];
|
|
244
|
+
};
|
|
245
|
+
if (Array.isArray(last.content)) {
|
|
246
|
+
messages[lastUserIdx] = {
|
|
247
|
+
...last,
|
|
248
|
+
content: [...last.content, { type: "text" as const, text }],
|
|
249
|
+
} as unknown as (typeof messages)[number];
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
(messages as unknown[]).push({ role: "user", content: [{ type: "text" as const, text }] });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { messages: messages as typeof event.messages };
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// -------------------------------------------------------------------------
|
|
259
|
+
// tool_call: PreToolUse hooks.
|
|
260
|
+
// Empty-matcher groups (remind) run for every tool with the real tool_name.
|
|
261
|
+
// Non-empty-matcher groups (auto-approve) run only for matching tools.
|
|
262
|
+
// -------------------------------------------------------------------------
|
|
263
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
264
|
+
config ??= loadConfig(ctx.cwd);
|
|
265
|
+
if (!config?.hooks.PreToolUse) return;
|
|
266
|
+
|
|
267
|
+
const sessionId = getSessionId(ctx);
|
|
268
|
+
const stdin = {
|
|
269
|
+
type: "pre_tool_use",
|
|
270
|
+
session_id: sessionId,
|
|
271
|
+
tool_name: event.toolName,
|
|
272
|
+
tool_input: event.input ?? {},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Empty-matcher groups: remind (runs for every tool).
|
|
276
|
+
const emptyGroups = config.hooks.PreToolUse.filter((g) => g.matcher === "");
|
|
277
|
+
const remindContexts = await runGroups(emptyGroups, event.toolName, ctx.cwd, stdin);
|
|
278
|
+
if (remindContexts.length > 0) pendingContexts.push(...remindContexts);
|
|
279
|
+
|
|
280
|
+
// Non-empty-matcher groups: auto-approve etc. (filtered by toolName).
|
|
281
|
+
const specificGroups = config.hooks.PreToolUse.filter((g) => g.matcher !== "");
|
|
282
|
+
await runGroups(specificGroups, event.toolName, ctx.cwd, stdin);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// -------------------------------------------------------------------------
|
|
286
|
+
// session_shutdown: Stop hooks.
|
|
287
|
+
// -------------------------------------------------------------------------
|
|
288
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
289
|
+
config ??= loadConfig(ctx.cwd);
|
|
290
|
+
if (!config) return;
|
|
291
|
+
const stdin = { type: "stop", session_id: getSessionId(ctx) };
|
|
292
|
+
await runGroups(config.hooks.Stop, "", ctx.cwd, stdin);
|
|
293
|
+
});
|
|
294
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fyeeme/pi-hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code-compatible hooks runner for pi. Reads .pi/hooks.json and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "fyeeme",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/fyeeme/pi-packages"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/fyeeme/pi-packages/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/fyeeme/pi-packages#readme",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi",
|
|
25
|
+
"hooks",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"serena",
|
|
28
|
+
"automation",
|
|
29
|
+
"lifecycle"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"index.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./index.ts"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "vitest --run",
|
|
44
|
+
"typecheck": "tsc"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=0.77.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "0.77.0",
|
|
51
|
+
"@types/node": "22.19.19",
|
|
52
|
+
"typescript": "5.9.3"
|
|
53
|
+
}
|
|
54
|
+
}
|