@zhushanwen/pi-unified-hooks 0.0.2 → 0.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/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/**
|
|
40
|
+
* Subset of `BashToolCallEvent` fields used by this hook.
|
|
41
|
+
* Local interface because the SDK's full event type is not re-exported
|
|
42
|
+
* by the CI ambient type stubs in `shared/types/mariozechner/index.d.ts`.
|
|
43
|
+
*/
|
|
44
|
+
interface BashToolCallLikeEvent {
|
|
45
|
+
toolName: string;
|
|
46
|
+
input: { command: string; timeout?: number };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function setupNetworkTimeoutGuard(pi: ExtensionAPI): void {
|
|
50
|
+
pi.on("tool_call", async (event: unknown) => {
|
|
51
|
+
// Only intercept bash tool calls
|
|
52
|
+
const e = event as BashToolCallLikeEvent;
|
|
53
|
+
if (e.toolName !== "bash") return;
|
|
54
|
+
|
|
55
|
+
const { command, timeout } = e.input;
|
|
56
|
+
|
|
57
|
+
// No network command detected — let it through
|
|
58
|
+
const networkLabel = detectNetworkCommand(command);
|
|
59
|
+
if (!networkLabel) return;
|
|
60
|
+
|
|
61
|
+
// Timeout is already set — good, let it through
|
|
62
|
+
if (timeout != null && timeout > 0) return;
|
|
63
|
+
|
|
64
|
+
// Block and ask AI to add timeout
|
|
65
|
+
return {
|
|
66
|
+
block: true,
|
|
67
|
+
reason:
|
|
68
|
+
`[network-timeout-guard] Detected network command (${networkLabel}) without timeout.\n` +
|
|
69
|
+
`Please do one of the following:\n` +
|
|
70
|
+
`1. Set the bash tool's timeout parameter (recommended: 60-120s)\n` +
|
|
71
|
+
`2. If duration is unclear, start with timeout: 30 to probe, then adjust after timeout\n` +
|
|
72
|
+
`3. Accessing overseas sites (npmjs.org / GitHub / PyPI) may require a proxy — on timeout, run 'proxy' to enable proxy and retry`,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
/**
|
|
75
|
+
* Subset of `BashToolCallEvent` fields used by this hook.
|
|
76
|
+
* Local interface because the SDK's full event type is not re-exported
|
|
77
|
+
* by the CI ambient type stubs in `shared/types/mariozechner/index.d.ts`.
|
|
78
|
+
*/
|
|
79
|
+
interface BashToolCallLikeEvent {
|
|
80
|
+
toolName: string;
|
|
81
|
+
input: { command: string; timeout?: number };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setupTestTimeoutGuard(pi: ExtensionAPI): void {
|
|
85
|
+
pi.on("tool_call", async (event: unknown) => {
|
|
86
|
+
const e = event as BashToolCallLikeEvent;
|
|
87
|
+
if (e.toolName !== "bash") return;
|
|
88
|
+
|
|
89
|
+
const { command, timeout } = e.input;
|
|
90
|
+
|
|
91
|
+
const testLabel = detectTestCommand(command);
|
|
92
|
+
if (!testLabel) return;
|
|
93
|
+
|
|
94
|
+
// Timeout already set — let it through
|
|
95
|
+
if (timeout != null && timeout > 0) return;
|
|
96
|
+
|
|
97
|
+
// Block and require timeout
|
|
98
|
+
return {
|
|
99
|
+
block: true,
|
|
100
|
+
reason:
|
|
101
|
+
`[test-timeout-guard] Detected test command (${testLabel}) without timeout.\n` +
|
|
102
|
+
`Test duration is unpredictable — without timeout the process may hang indefinitely. Please do one of the following:\n` +
|
|
103
|
+
`1. Set the bash tool's timeout parameter (60-120s for unit tests, 180-300s for E2E tests)\n` +
|
|
104
|
+
`2. If test suite size or duration is unknown, start with a short timeout (e.g. 30s) to probe\n` +
|
|
105
|
+
`3. For known slow tests (cargo test, gradle test, etc.), allow up to 300s`,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -7,9 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Subset of `ToolExecutionEndEvent` fields used by this hook.
|
|
12
|
+
* Local interface because the SDK's full event type is not re-exported
|
|
13
|
+
* by the CI ambient type stubs in `shared/types/mariozechner/index.d.ts`.
|
|
14
|
+
*/
|
|
15
|
+
interface ToolExecutionEndLikeEvent {
|
|
16
|
+
isError: boolean;
|
|
17
|
+
toolName: string;
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export function setupToolErrorHandler(pi: ExtensionAPI): void {
|
|
11
|
-
pi.on("tool_execution_end", async (event:
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
pi.on("tool_execution_end", async (event: unknown) => {
|
|
23
|
+
const e = event as ToolExecutionEndLikeEvent;
|
|
24
|
+
if (!e.isError) return;
|
|
25
|
+
console.log(`[unified-hooks] ${e.toolName} error (callId=${e.toolCallId})`);
|
|
14
26
|
});
|
|
15
27
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
// Re-export hook modules for easy access
|
|
11
|
+
export { setupNetworkTimeoutGuard } from "./hooks/network-timeout-guard";
|
|
12
|
+
export { setupTestTimeoutGuard } from "./hooks/test-timeout-guard";
|
|
11
13
|
export { setupToolErrorHandler } from "./hooks/tool-error-handler";
|
|
12
14
|
|
|
15
|
+
import { setupNetworkTimeoutGuard } from "./hooks/network-timeout-guard";
|
|
16
|
+
import { setupTestTimeoutGuard } from "./hooks/test-timeout-guard";
|
|
13
17
|
import { setupToolErrorHandler } from "./hooks/tool-error-handler";
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -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) {
|