@zhushanwen/pi-unified-hooks 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,55 +1,42 @@
1
- # unified-hooks — Unified Hooks Extension
1
+ # unified-hooks
2
2
 
3
- Collect scattered hooks in one place for easy maintenance. Each hook is a self-contained module that can be enabled/disabled independently.
3
+ 统一 hooks 管理器 — 将散落的 hooks 收集到一个扩展中统一维护,每个 hook 可独立启用/禁用。
4
4
 
5
- ## Installation
5
+ ## 功能
6
6
 
7
- ```bash
8
- ln -s /path/to/xyz-pi-extensions/unified-hooks ~/.pi/agent/extensions/unified-hooks
9
- ```
10
-
11
- ## Available Hooks
12
-
13
- ### edit-whitespace-autofix
14
-
15
- When edit tool fails due to whitespace mismatch, injects a steering message that tells the AI to fix whitespace and retry.
7
+ ### 内置 Hooks
16
8
 
17
- **Trigger patterns:**
18
- - `Could not find the exact text`
19
- - `oldText must match exactly`
20
- - `Could not find edits[`
9
+ | Hook | 说明 |
10
+ |------|------|
11
+ | `edit-whitespace-autofix` | edit 工具因空白字符不匹配失败时,自动注入 steering 让 AI 修复空白并重试 |
12
+ | `tool-error-handler` | 记录所有工具执行错误到控制台,方便调试 |
21
13
 
22
- **Behavior:**
23
- 1. Detect whitespace mismatch error from edit tool
24
- 2. Extract the file path from tool args
25
- 3. Inject steer message with `fix_whitespace.py --fix <file>` command
26
- 4. AI automatically fixes whitespace and retries the edit
14
+ ### 扩展方式
27
15
 
28
- ### tool-error-handler
16
+ 在 `src/hooks/` 下新建 hook 模块,然后在 `src/index.ts` 的 `hookModules` 数组中注册即可。
29
17
 
30
- Logs all tool execution errors to console for debugging.
18
+ ## 安装
31
19
 
32
- ## Adding New Hooks
33
-
34
- 1. Create `src/hooks/my-hook.ts`:
35
- ```typescript
36
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
+ ```bash
21
+ # symlink 方式(开发推荐)
22
+ ln -s /path/to/xyz-pi-extensions-workspace/main/packages/unified-hooks \
23
+ ~/.pi/agent/extensions/unified-hooks
37
24
 
38
- export function setupMyHook(pi: ExtensionAPI): void {
39
- pi.on("tool_execution_end", async (event) => {
40
- if (event.toolName === "edit" && event.isError) {
41
- // Your logic
42
- }
43
- });
44
- }
45
- ```
25
+ # npm 方式(正式)
26
+ pi install npm:@zhushanwen/pi-unified-hooks
27
+ ```
46
28
 
47
- 2. Register in `src/index.ts` hookModules array
29
+ ## 使用
48
30
 
49
- 3. Type check: `npx tsc --noEmit`
31
+ 安装后自动生效。edit 空白修复无需配置,工具错误日志自动输出到控制台。
50
32
 
51
- ## Important API Notes
33
+ ## 文件结构
52
34
 
53
- - Use `pi.sendUserMessage()` in event handlers, **not** `ctx.sendUserMessage()` (only available in command context)
54
- - `tool_execution_end` event has `{ toolCallId, toolName, args, result, isError }`
55
- - Inject direct instructions in steer messages, don't try to invoke `/skill-name` via text
35
+ ```
36
+ unified-hooks/
37
+ ├── index.ts
38
+ └── src/
39
+ ├── index.ts # 入口 — hook 注册
40
+ └── hooks/
41
+ └── tool-error-handler.ts # 工具错误处理
42
+ ```
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-unified-hooks",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Unified hooks extension - collect scattered hooks in one place for easy maintenance",
5
- "main": "index.js",
5
+ "main": "index.ts",
6
6
  "type": "module",
7
+ "pi": {
8
+ "extensions": [
9
+ "./index.ts"
10
+ ]
11
+ },
7
12
  "keywords": [
8
- "pi",
13
+ "pi-package",
9
14
  "extension",
10
15
  "hooks"
11
16
  ],
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Network Timeout Guard Hook
3
+ *
4
+ * Intercepts bash tool calls that involve network operations.
5
+ * Blocks execution if no timeout is set and the command appears to be network-bound,
6
+ * prompting the AI to add timeout or use async mode.
7
+ */
8
+
9
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+
11
+ /**
12
+ * Commands that indicate network activity.
13
+ * Matched against any segment of the bash command string (supports &&/||/; chaining).
14
+ */
15
+ const NETWORK_COMMAND_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
16
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(pnpm|npm|yarn|bun)\s+(install|add|i|ci|update|upgrade|dlx|exec|create|init)/, label: "package manager" },
17
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(pip|pip3|uv|poetry|conda)\s+(install|add|sync|download|run)/, label: "Python package manager" },
18
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*curl\s/, label: "curl" },
19
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*wget\s/, label: "wget" },
20
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*git\s+(clone|fetch|pull|push|submodule)/, label: "git network" },
21
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*gh\s+(repo\s+clone|api|run|release|pr\s+checkout)/, label: "gh CLI" },
22
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*docker\s+(pull|push|build|run|compose\s+(pull|up|build))/, label: "docker" },
23
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*cargo\s+(install|update|build|check|fetch)/, label: "cargo" },
24
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*go\s+(install|get|mod\s+(download|tidy))/, label: "go" },
25
+ ];
26
+
27
+ /**
28
+ * Check if a command involves network operations.
29
+ */
30
+ function detectNetworkCommand(command: string): string | null {
31
+ for (const { pattern, label } of NETWORK_COMMAND_PATTERNS) {
32
+ if (pattern.test(command)) {
33
+ return label;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export function setupNetworkTimeoutGuard(pi: ExtensionAPI): void {
40
+ pi.on("tool_call", async (event: any) => {
41
+ // Only intercept bash tool calls
42
+ if (event.toolName !== "bash") return;
43
+
44
+ const { command, timeout } = event.input as { command: string; timeout?: number };
45
+
46
+ // No network command detected — let it through
47
+ const networkLabel = detectNetworkCommand(command);
48
+ if (!networkLabel) return;
49
+
50
+ // Timeout is already set — good, let it through
51
+ if (timeout != null && timeout > 0) return;
52
+
53
+ // Block and ask AI to add timeout
54
+ return {
55
+ block: true,
56
+ reason:
57
+ `[network-timeout-guard] Detected network command (${networkLabel}) without timeout.\n` +
58
+ `Please do one of the following:\n` +
59
+ `1. Set the bash tool's timeout parameter (recommended: 60-120s)\n` +
60
+ `2. If duration is unclear, start with timeout: 30 to probe, then adjust after timeout\n` +
61
+ `3. Accessing overseas sites (npmjs.org / GitHub / PyPI) may require a proxy — on timeout, run 'proxy' to enable proxy and retry`,
62
+ };
63
+ });
64
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Test Timeout Guard Hook
3
+ *
4
+ * Intercepts bash tool calls that execute tests across various frameworks.
5
+ * Blocks execution if no timeout is set, prompting the AI to add one.
6
+ *
7
+ * Covered ecosystems: Node.js (vitest, jest, mocha, cypress, playwright),
8
+ * Python (pytest, unittest), Java (maven, gradle), Go, Rust, .NET, Ruby.
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
+
13
+ /**
14
+ * Patterns that indicate test execution.
15
+ * Designed to match via pipe/semicolon/&& chaining as well as direct invocation.
16
+ */
17
+ const TEST_COMMAND_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
18
+ // === Node.js / JS / TS ===
19
+ // Package manager test scripts: npm test, pnpm test, yarn test
20
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(pnpm|npm|yarn|bun)\s+test\b/, label: "npm test" },
21
+ // pnpm/npm run with test-related script names
22
+ {
23
+ pattern: /(^|\s|&&|\|{1,2}|;)\s*(pnpm|npm)\s+(--filter\s+\S+\s+)?run\s+\S*test/,
24
+ label: "npm run test",
25
+ },
26
+ // Direct test runner invocation via npx
27
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+vitest\b/, label: "vitest" },
28
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+jest\b/, label: "jest" },
29
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+mocha\b/, label: "mocha" },
30
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+(cypress|playwright)\s/, label: "e2e runner" },
31
+ // vue-cli-service / react-scripts test
32
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+vue-cli-service\s+test/, label: "vue-cli test" },
33
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*npx\s+react-scripts\s+test/, label: "react-scripts test" },
34
+ // Direct vitest/jest from node_modules
35
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*\.\/?node_modules\/\.bin\/(vitest|jest|mocha)\b/, label: "direct test runner" },
36
+
37
+ // === Python ===
38
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*pytest\b/, label: "pytest" },
39
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*python[3]?\s+-m\s+(pytest|unittest)\b/, label: "python test" },
40
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(uv|poetry)\s+run\s+pytest\b/, label: "uv/poetry pytest" },
41
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*nosetests\b/, label: "nosetests" },
42
+
43
+ // === Java / JVM ===
44
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(mvn|\.\/mvnw)\s+\S*test/, label: "maven test" },
45
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(gradle|\.\/gradlew)\s+\S*test/, label: "gradle test" },
46
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*sbt\s+test\b/, label: "sbt test" },
47
+
48
+ // === Go ===
49
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*go\s+test\b/, label: "go test" },
50
+
51
+ // === Rust ===
52
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*cargo\s+test\b/, label: "cargo test" },
53
+
54
+ // === .NET ===
55
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*dotnet\s+test\b/, label: "dotnet test" },
56
+
57
+ // === Ruby ===
58
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*(rspec|bundle\s+exec\s+rspec)\b/, label: "rspec" },
59
+ { pattern: /(^|\s|&&|\|{1,2}|;)\s*rake\s+test\b/, label: "rake test" },
60
+ ];
61
+
62
+ /**
63
+ * Check if a command executes tests.
64
+ */
65
+ function detectTestCommand(command: string): string | null {
66
+ for (const { pattern, label } of TEST_COMMAND_PATTERNS) {
67
+ if (pattern.test(command)) {
68
+ return label;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export function setupTestTimeoutGuard(pi: ExtensionAPI): void {
75
+ pi.on("tool_call", async (event: any) => {
76
+ if (event.toolName !== "bash") return;
77
+
78
+ const { command, timeout } = event.input as { command: string; timeout?: number };
79
+
80
+ const testLabel = detectTestCommand(command);
81
+ if (!testLabel) return;
82
+
83
+ // Timeout already set — let it through
84
+ if (timeout != null && timeout > 0) return;
85
+
86
+ // Block and require timeout
87
+ return {
88
+ block: true,
89
+ reason:
90
+ `[test-timeout-guard] Detected test command (${testLabel}) without timeout.\n` +
91
+ `Test duration is unpredictable — without timeout the process may hang indefinitely. Please do one of the following:\n` +
92
+ `1. Set the bash tool's timeout parameter (60-120s for unit tests, 180-300s for E2E tests)\n` +
93
+ `2. If test suite size or duration is unknown, start with a short timeout (e.g. 30s) to probe\n` +
94
+ `3. For known slow tests (cargo test, gradle test, etc.), allow up to 300s`,
95
+ };
96
+ });
97
+ }
@@ -8,7 +8,7 @@
8
8
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export function setupToolErrorHandler(pi: ExtensionAPI): void {
11
- pi.on("tool_execution_end", async (event) => {
11
+ pi.on("tool_execution_end", async (event: any) => {
12
12
  if (!event.isError) return;
13
13
  console.log(`[unified-hooks] ${event.toolName} error (callId=${event.toolCallId})`);
14
14
  });
package/src/index.ts CHANGED
@@ -9,8 +9,12 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  // Re-export hook modules for easy access
11
11
  export { setupToolErrorHandler } from "./hooks/tool-error-handler";
12
+ export { setupNetworkTimeoutGuard } from "./hooks/network-timeout-guard";
13
+ export { setupTestTimeoutGuard } from "./hooks/test-timeout-guard";
12
14
 
13
15
  import { setupToolErrorHandler } from "./hooks/tool-error-handler";
16
+ import { setupNetworkTimeoutGuard } from "./hooks/network-timeout-guard";
17
+ import { setupTestTimeoutGuard } from "./hooks/test-timeout-guard";
14
18
 
15
19
  /**
16
20
  * Extension factory - registers all unified hooks
@@ -23,6 +27,8 @@ export default function unifiedHooksExtension(pi: ExtensionAPI): void {
23
27
  // with hash-anchor mode, making oldText-based guard unreachable
24
28
  const hookModules = [
25
29
  { name: "tool-error-handler", setup: setupToolErrorHandler },
30
+ { name: "network-timeout-guard", setup: setupNetworkTimeoutGuard },
31
+ { name: "test-timeout-guard", setup: setupTestTimeoutGuard },
26
32
  ];
27
33
 
28
34
  for (const hook of hookModules) {