@dyyz1993/pi-coding-agent 0.74.24 → 0.74.25
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +6 -5
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ClaudeHookConfig, MatcherGroup } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function loadSingleConfig(path: string): ClaudeHookConfig | null {
|
|
7
|
+
if (!existsSync(path)) return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.debug("[claude-hooks-compat] config load failed:", err instanceof Error ? err.message : err);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadConfigs(projectDir: string): Map<string, MatcherGroup[]> {
|
|
17
|
+
const merged = new Map<string, MatcherGroup[]>();
|
|
18
|
+
|
|
19
|
+
const policyPath = process.env.CLAUDE_POLICY_FILE ?? "";
|
|
20
|
+
if (policyPath) {
|
|
21
|
+
const policy = loadSingleConfig(policyPath);
|
|
22
|
+
if (policy?.disableAllHooks) return merged;
|
|
23
|
+
if (policy?.hooks) {
|
|
24
|
+
for (const [eventName, groups] of Object.entries(policy.hooks)) {
|
|
25
|
+
merged.set(eventName, [...groups]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sources = [
|
|
31
|
+
{ path: join(homedir(), ".claude/settings.json"), name: "global" },
|
|
32
|
+
{ path: join(projectDir, ".claude/settings.json"), name: "project" },
|
|
33
|
+
{ path: join(projectDir, ".claude/settings.local.json"), name: "local" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const source of sources) {
|
|
37
|
+
const config = loadSingleConfig(source.path);
|
|
38
|
+
if (!config) continue;
|
|
39
|
+
if (config.disableAllHooks) continue;
|
|
40
|
+
if (!config.hooks) continue;
|
|
41
|
+
|
|
42
|
+
for (const [eventName, groups] of Object.entries(config.hooks)) {
|
|
43
|
+
const existing = merged.get(eventName) ?? [];
|
|
44
|
+
merged.set(eventName, [...existing, ...groups]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { HookHandler, HookOutput, HookStdinData } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type CallLLMFn = (options: {
|
|
5
|
+
systemPrompt?: string;
|
|
6
|
+
messages: { role: "user" | "assistant"; content: string }[];
|
|
7
|
+
tools?: string[];
|
|
8
|
+
maxTurns?: number;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}) => Promise<string>;
|
|
12
|
+
|
|
13
|
+
export async function runHandler(
|
|
14
|
+
handler: HookHandler,
|
|
15
|
+
stdinData: HookStdinData,
|
|
16
|
+
ctx: { cwd: string; hasUI: boolean },
|
|
17
|
+
callLLM?: CallLLMFn,
|
|
18
|
+
): Promise<HookOutput> {
|
|
19
|
+
switch (handler.type) {
|
|
20
|
+
case "command":
|
|
21
|
+
return runCommandHandler(handler, stdinData, ctx);
|
|
22
|
+
case "http":
|
|
23
|
+
return runHttpHandler(handler, stdinData);
|
|
24
|
+
case "prompt":
|
|
25
|
+
return runPromptHandler(handler, stdinData, callLLM);
|
|
26
|
+
case "agent":
|
|
27
|
+
return runAgentHandler(handler, stdinData, callLLM);
|
|
28
|
+
case "mcp_tool":
|
|
29
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
30
|
+
default:
|
|
31
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function replaceVariables(command: string, stdinData: HookStdinData, cwd: string): string {
|
|
36
|
+
return command
|
|
37
|
+
.replace(/\$CLAUDE_PROJECT_DIR/g, cwd)
|
|
38
|
+
.replace(/\$TOOL/g, stdinData.tool_name ?? "")
|
|
39
|
+
.replace(/\$BASH_COMMAND/g, ((stdinData.tool_input as Record<string, unknown>)?.command as string) ?? "")
|
|
40
|
+
.replace(/\$ARGUMENTS/g, JSON.stringify(stdinData.tool_input ?? {}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function replaceInputPlaceholders(
|
|
44
|
+
template: Record<string, unknown>,
|
|
45
|
+
stdinData: HookStdinData,
|
|
46
|
+
): Record<string, unknown> {
|
|
47
|
+
const result: Record<string, unknown> = {};
|
|
48
|
+
for (const [key, value] of Object.entries(template)) {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
result[key] = value.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
51
|
+
return String(resolvePath(stdinData, path) ?? "");
|
|
52
|
+
});
|
|
53
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
54
|
+
result[key] = replaceInputPlaceholders(value as Record<string, unknown>, stdinData);
|
|
55
|
+
} else {
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePath(obj: unknown, path: string): unknown {
|
|
63
|
+
const parts = path.split(".");
|
|
64
|
+
let current: unknown = obj;
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
67
|
+
current = (current as Record<string, unknown>)[part];
|
|
68
|
+
}
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function replaceEnvVarsInHeaders(
|
|
73
|
+
headers: Record<string, string>,
|
|
74
|
+
allowedEnvVars: string[] | undefined,
|
|
75
|
+
): Record<string, string> {
|
|
76
|
+
if (!allowedEnvVars || allowedEnvVars.length === 0) return headers;
|
|
77
|
+
|
|
78
|
+
const allowed = new Set(allowedEnvVars);
|
|
79
|
+
const result: Record<string, string> = {};
|
|
80
|
+
|
|
81
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
82
|
+
result[key] = value.replace(/\$\{?(\w+)\}?/g, (match, varName) => {
|
|
83
|
+
if (allowed.has(varName)) {
|
|
84
|
+
return process.env[varName] ?? "";
|
|
85
|
+
}
|
|
86
|
+
return match;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildHookEnv(cwd: string, eventName: string): NodeJS.ProcessEnv {
|
|
94
|
+
const env = { ...process.env } as NodeJS.ProcessEnv;
|
|
95
|
+
|
|
96
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
97
|
+
for (const key of Object.keys(env)) {
|
|
98
|
+
if (key.startsWith("ANTHROPIC_") || key.startsWith("CLAUDE_CODE_OAUTH")) {
|
|
99
|
+
delete env[key];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
env.CLAUDE_PROJECT_DIR = cwd;
|
|
105
|
+
env.CLAUDE_CODE_SHELL_PREFIX = "";
|
|
106
|
+
|
|
107
|
+
if (["SessionStart", "Setup", "CwdChanged", "FileChanged"].includes(eventName)) {
|
|
108
|
+
env.CLAUDE_ENV_FILE = `/tmp/claude-env-${Date.now()}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return env;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function runCommandHandler(handler: HookHandler, stdinData: HookStdinData, ctx: { cwd: string }): Promise<HookOutput> {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const timeout = (handler.timeout ?? 600) * 1000;
|
|
117
|
+
const command = replaceVariables(handler.command ?? "", stdinData, ctx.cwd);
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
let settled = false;
|
|
121
|
+
|
|
122
|
+
const env = buildHookEnv(ctx.cwd, stdinData.hook_event_name);
|
|
123
|
+
const proc = spawn("bash", ["-c", command], {
|
|
124
|
+
cwd: ctx.cwd,
|
|
125
|
+
env,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
proc.kill("SIGTERM");
|
|
132
|
+
resolve({ exitCode: 1, stdout: "", stderr: "Hook timed out" });
|
|
133
|
+
}, timeout);
|
|
134
|
+
|
|
135
|
+
proc.stdin.write(JSON.stringify(stdinData));
|
|
136
|
+
proc.stdin.end();
|
|
137
|
+
|
|
138
|
+
proc.stdout.on("data", (d: Buffer) => {
|
|
139
|
+
stdout += d.toString();
|
|
140
|
+
});
|
|
141
|
+
proc.stderr.on("data", (d: Buffer) => {
|
|
142
|
+
stderr += d.toString();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
proc.on("close", (code) => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
|
|
150
|
+
const result: HookOutput = {
|
|
151
|
+
exitCode: code ?? 0,
|
|
152
|
+
stdout: stdout.trim(),
|
|
153
|
+
stderr: stderr.trim(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (result.exitCode === 0 && stdout.trim().startsWith("{")) {
|
|
157
|
+
try {
|
|
158
|
+
result.parsed = JSON.parse(stdout.trim());
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.debug("[claude-hooks-compat] stdout JSON parse failed:", err instanceof Error ? err.message : err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolve(result);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
proc.on("error", (err) => {
|
|
168
|
+
if (settled) return;
|
|
169
|
+
settled = true;
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
resolve({ exitCode: 1, stdout: "", stderr: err.message });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runHttpHandler(handler: HookHandler, stdinData: HookStdinData): Promise<HookOutput> {
|
|
177
|
+
const timeout = (handler.timeout ?? 600) * 1000;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const urlStr = handler.url ?? "";
|
|
181
|
+
let parsedUrl: URL;
|
|
182
|
+
try {
|
|
183
|
+
parsedUrl = new URL(urlStr);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.debug("[claude-hooks-compat] URL parse failed:", err instanceof Error ? err.message : err);
|
|
186
|
+
return { exitCode: 1, stdout: "", stderr: `Invalid URL: ${urlStr}` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (parsedUrl.protocol !== "https:") {
|
|
190
|
+
return { exitCode: 1, stdout: "", stderr: "Only HTTPS URLs are allowed" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
194
|
+
if (isPrivateIP(hostname)) {
|
|
195
|
+
return { exitCode: 1, stdout: "", stderr: `Requests to private addresses are not allowed: ${hostname}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rawHeaders = handler.headers ?? {};
|
|
199
|
+
const headers = replaceEnvVarsInHeaders(rawHeaders, handler.allowedEnvVars);
|
|
200
|
+
headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
201
|
+
|
|
202
|
+
const resp = await fetch(urlStr, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers,
|
|
205
|
+
body: JSON.stringify(stdinData),
|
|
206
|
+
signal: AbortSignal.timeout(timeout),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (resp.ok) {
|
|
210
|
+
const text = await resp.text();
|
|
211
|
+
let parsed: HookOutput["parsed"];
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(text);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.debug("[claude-hooks-compat] HTTP response JSON parse failed:", err instanceof Error ? err.message : err);
|
|
216
|
+
}
|
|
217
|
+
return { exitCode: 0, stdout: text, stderr: "", parsed };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (resp.status === 403) {
|
|
221
|
+
const body = await resp.text().catch((err) => {
|
|
222
|
+
console.debug("[claude-hooks-compat] 403 body read failed:", err instanceof Error ? err.message : err);
|
|
223
|
+
return "";
|
|
224
|
+
});
|
|
225
|
+
return { exitCode: 2, stdout: "", stderr: body || `HTTP ${resp.status}` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { exitCode: 1, stdout: "", stderr: `HTTP ${resp.status}` };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return { exitCode: 1, stdout: "", stderr: (err as Error).message };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isPrivateIP(hostname: string): boolean {
|
|
235
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (hostname === "::1" || hostname === "[::1]") {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
244
|
+
if (ipv4Match) {
|
|
245
|
+
const [, a, b] = ipv4Match.map(Number);
|
|
246
|
+
if (a === 10) return true;
|
|
247
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
248
|
+
if (a === 192 && b === 168) return true;
|
|
249
|
+
if (a === 169 && b === 254) return true;
|
|
250
|
+
if (a === 127) return true;
|
|
251
|
+
if (a === 0) return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (hostname.startsWith("fc") || hostname.startsWith("fd") || hostname.startsWith("fe80")) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runPromptHandler(
|
|
262
|
+
handler: HookHandler,
|
|
263
|
+
stdinData: HookStdinData,
|
|
264
|
+
callLLM?: CallLLMFn,
|
|
265
|
+
): Promise<HookOutput> {
|
|
266
|
+
if (!callLLM || !handler.prompt) {
|
|
267
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const timeout = (handler.timeout ?? 30) * 1000;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const promptText = handler.prompt.replace(/\$ARGUMENTS/g, JSON.stringify(stdinData));
|
|
274
|
+
|
|
275
|
+
const response = await callLLM({
|
|
276
|
+
systemPrompt:
|
|
277
|
+
"You are a hook evaluator. Respond with a JSON object. If the action should proceed, respond with {\"ok\":true}. If it should be blocked, respond with {\"ok\":false,\"reason\":\"...\"}.",
|
|
278
|
+
messages: [{ role: "user", content: promptText }],
|
|
279
|
+
maxTokens: 1024,
|
|
280
|
+
signal: AbortSignal.timeout(timeout),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
let parsed: HookOutput["parsed"];
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(response);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.debug("[claude-hooks-compat] prompt handler JSON parse failed:", err instanceof Error ? err.message : err);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { exitCode: 0, stdout: response, stderr: "", parsed };
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return { exitCode: 1, stdout: "", stderr: (err as Error).message };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function runAgentHandler(
|
|
297
|
+
handler: HookHandler,
|
|
298
|
+
stdinData: HookStdinData,
|
|
299
|
+
callLLM?: CallLLMFn,
|
|
300
|
+
): Promise<HookOutput> {
|
|
301
|
+
if (!callLLM || !handler.prompt) {
|
|
302
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const timeout = (handler.timeout ?? 60) * 1000;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const promptText = handler.prompt.replace(/\$ARGUMENTS/g, JSON.stringify(stdinData));
|
|
309
|
+
|
|
310
|
+
const response = await callLLM({
|
|
311
|
+
systemPrompt:
|
|
312
|
+
"You are a hook evaluator agent. You can use tools to verify conditions. Respond with a JSON object. If the action should proceed, respond with {\"ok\":true}. If it should be blocked, respond with {\"ok\":false,\"reason\":\"...\"}.",
|
|
313
|
+
messages: [{ role: "user", content: promptText }],
|
|
314
|
+
maxTurns: 50,
|
|
315
|
+
maxTokens: 4096,
|
|
316
|
+
signal: AbortSignal.timeout(timeout),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
let parsed: HookOutput["parsed"];
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(response);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.debug("[claude-hooks-compat] agent handler JSON parse failed:", err instanceof Error ? err.message : err);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { exitCode: 0, stdout: response, stderr: "", parsed };
|
|
327
|
+
} catch (err) {
|
|
328
|
+
return { exitCode: 1, stdout: "", stderr: (err as Error).message };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function interpretHookOutput(output: HookOutput): {
|
|
333
|
+
shouldBlock: boolean;
|
|
334
|
+
reason: string;
|
|
335
|
+
updatedInput?: Record<string, unknown>;
|
|
336
|
+
additionalContext?: string;
|
|
337
|
+
systemMessage?: string;
|
|
338
|
+
suppressOutput?: boolean;
|
|
339
|
+
retry?: boolean;
|
|
340
|
+
} {
|
|
341
|
+
if (output.exitCode === 2) {
|
|
342
|
+
return { shouldBlock: true, reason: output.stderr || "Blocked by hook" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (output.parsed) {
|
|
346
|
+
const p = output.parsed;
|
|
347
|
+
|
|
348
|
+
if (p.continue === false) {
|
|
349
|
+
return { shouldBlock: true, reason: p.stopReason || "Hook stopped execution" };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (p.ok === false) {
|
|
353
|
+
return { shouldBlock: true, reason: p.reason || "Blocked by hook" };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (p.decision === "block") {
|
|
357
|
+
return { shouldBlock: true, reason: p.reason || "Blocked by hook" };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const hso = p.hookSpecificOutput;
|
|
361
|
+
if (hso?.permissionDecision === "deny") {
|
|
362
|
+
return { shouldBlock: true, reason: hso.permissionDecisionReason || "Denied by hook" };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
shouldBlock: false,
|
|
367
|
+
reason: "",
|
|
368
|
+
updatedInput: hso?.updatedInput ?? hso?.modifiedToolInput,
|
|
369
|
+
additionalContext: hso?.additionalContext,
|
|
370
|
+
systemMessage: p.systemMessage,
|
|
371
|
+
suppressOutput: p.suppressOutput,
|
|
372
|
+
retry: hso?.retry === true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { shouldBlock: false, reason: "" };
|
|
377
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { IfClause } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function parseIfClause(ifClause: string): IfClause | null {
|
|
4
|
+
const match = ifClause.match(/^(\w+)\((.+)\)$/);
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
return { tool: match[1], pattern: match[2] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function matchesIfClause(
|
|
10
|
+
ifClause: string | undefined,
|
|
11
|
+
toolName: string,
|
|
12
|
+
toolInput: Record<string, unknown>,
|
|
13
|
+
): boolean {
|
|
14
|
+
if (!ifClause) return true;
|
|
15
|
+
|
|
16
|
+
const parsed = parseIfClause(ifClause);
|
|
17
|
+
if (!parsed) return true;
|
|
18
|
+
if (parsed.tool !== toolName) return false;
|
|
19
|
+
|
|
20
|
+
if (toolName === "Bash") {
|
|
21
|
+
const command = (toolInput.command as string) ?? "";
|
|
22
|
+
return globMatch(parsed.pattern, command);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
26
|
+
const filePath = (toolInput.file_path as string) ?? (toolInput.path as string) ?? "";
|
|
27
|
+
return globMatch(parsed.pattern, filePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (toolName === "Read") {
|
|
31
|
+
const filePath = (toolInput.file_path as string) ?? (toolInput.path as string) ?? "";
|
|
32
|
+
return globMatch(parsed.pattern, filePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function globMatch(pattern: string, text: string): boolean {
|
|
39
|
+
const regex = globToRegex(pattern);
|
|
40
|
+
try {
|
|
41
|
+
return new RegExp(regex).test(text);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.debug("[claude-hooks-compat] glob regex match failed:", err instanceof Error ? err.message : err);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function globToRegex(pattern: string): string {
|
|
49
|
+
return pattern
|
|
50
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
51
|
+
.replace(/\*/g, ".*")
|
|
52
|
+
.replace(/\?/g, ".");
|
|
53
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolResultEvent, BeforeAgentStartEvent } from "@dyyz1993/pi-coding-agent";
|
|
2
|
+
import type { MatcherGroup, HookHandler } from "./types.js";
|
|
3
|
+
import { loadConfigs } from "./config-loader.js";
|
|
4
|
+
import { matchesMatcher } from "./matcher.js";
|
|
5
|
+
import { matchesIfClause } from "./if-parser.js";
|
|
6
|
+
import { buildStdinData } from "./stdin-builder.js";
|
|
7
|
+
import { runHandler, interpretHookOutput } from "./handler-runner.js";
|
|
8
|
+
|
|
9
|
+
function matchesPiVariables(
|
|
10
|
+
handler: HookHandler,
|
|
11
|
+
ctxVars: Record<string, unknown> | undefined,
|
|
12
|
+
): boolean {
|
|
13
|
+
const piVars = handler["x-pi-variables"];
|
|
14
|
+
if (!piVars || !ctxVars) return true;
|
|
15
|
+
|
|
16
|
+
for (const [key, value] of Object.entries(piVars)) {
|
|
17
|
+
const ctxValue = String(ctxVars[key] ?? "");
|
|
18
|
+
const allowedValues = value.split("|");
|
|
19
|
+
if (!allowedValues.includes(ctxValue)) return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function (pi: ExtensionAPI) {
|
|
26
|
+
let configs: Map<string, MatcherGroup[]> = new Map();
|
|
27
|
+
const onceHandlers = new Set<number>();
|
|
28
|
+
|
|
29
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
30
|
+
configs = loadConfigs(ctx.cwd);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
34
|
+
const result = await processHookEvent("PreToolUse", event, ctx);
|
|
35
|
+
return result;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
pi.on("tool_result", async (event: ToolResultEvent, ctx) => {
|
|
39
|
+
const toolOutput = event.content.map((c) => c.type === "text" ? c.text : "").join("");
|
|
40
|
+
const hookName = event.isError ? "PostToolUseFailure" : "PostToolUse";
|
|
41
|
+
await processHookEvent(hookName, {
|
|
42
|
+
toolName: event.toolName,
|
|
43
|
+
input: event.input ?? {},
|
|
44
|
+
toolCallId: event.toolCallId,
|
|
45
|
+
toolOutput,
|
|
46
|
+
}, ctx);
|
|
47
|
+
return undefined;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
pi.on("before_agent_start", async (event: BeforeAgentStartEvent, ctx) => {
|
|
51
|
+
const result = await processHookEvent("UserPromptSubmit", {
|
|
52
|
+
toolName: "",
|
|
53
|
+
input: { prompt: event.prompt ?? "" },
|
|
54
|
+
}, ctx);
|
|
55
|
+
if (result?.block) {
|
|
56
|
+
return { message: { customType: "hook_block", content: result.reason, display: true } };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
62
|
+
await processHookEvent("SessionEnd", { toolName: "", input: {} }, ctx);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
66
|
+
await processHookEvent("PreCompact", { toolName: "", input: {} }, ctx);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
pi.on("message_start", async (event, ctx) => {
|
|
70
|
+
const messageText = typeof event.message === "object" && event.message !== null && "content" in event.message
|
|
71
|
+
? String((event.message as { content: unknown }).content ?? "")
|
|
72
|
+
: "";
|
|
73
|
+
await processHookEvent("Notification", {
|
|
74
|
+
toolName: "",
|
|
75
|
+
input: { message: messageText },
|
|
76
|
+
}, ctx);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
80
|
+
await processHookEvent("SubagentStart", { toolName: "", input: {} }, ctx);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
84
|
+
await processHookEvent("SubagentStop", { toolName: "", input: {} }, ctx);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
88
|
+
await processHookEvent("Stop", { toolName: "", input: {} }, ctx);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function processHookEvent(
|
|
92
|
+
hookEventName: string,
|
|
93
|
+
event: { toolName: string; input: Record<string, unknown>; toolCallId?: string; toolOutput?: string },
|
|
94
|
+
ctx: { cwd: string; hasUI: boolean },
|
|
95
|
+
): Promise<{ block: boolean; reason: string } | undefined> {
|
|
96
|
+
const groups = configs.get(hookEventName) ?? [];
|
|
97
|
+
if (groups.length === 0) return undefined;
|
|
98
|
+
|
|
99
|
+
const ctxVars = ((ctx as unknown) as Record<string, unknown>).variables as Record<string, unknown> | undefined;
|
|
100
|
+
const agentType = (ctxVars?.role ?? ctxVars?.agent_type) as string | undefined;
|
|
101
|
+
const stdinData = buildStdinData(hookEventName, {
|
|
102
|
+
toolName: event.toolName,
|
|
103
|
+
toolInput: event.input,
|
|
104
|
+
toolOutput: event.toolOutput,
|
|
105
|
+
toolUseId: event.toolCallId,
|
|
106
|
+
cwd: ctx.cwd,
|
|
107
|
+
agentType,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
for (const group of groups) {
|
|
111
|
+
if (!matchesMatcher(group.matcher, event.toolName)) continue;
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < group.hooks.length; i++) {
|
|
114
|
+
const handler = group.hooks[i];
|
|
115
|
+
|
|
116
|
+
if (handler.once && onceHandlers.has(handlerIndex(hookEventName, i))) continue;
|
|
117
|
+
if (!matchesIfClause(handler.if, event.toolName, event.input)) continue;
|
|
118
|
+
if (!matchesPiVariables(handler, ctxVars)) continue;
|
|
119
|
+
|
|
120
|
+
if (handler.once) onceHandlers.add(handlerIndex(hookEventName, i));
|
|
121
|
+
|
|
122
|
+
const isAsync = handler.async ?? handler.asyncRewake ?? false;
|
|
123
|
+
if (isAsync && hookEventName === "PreToolUse") {
|
|
124
|
+
const runner = getCallLLM(pi);
|
|
125
|
+
runHandler(handler, stdinData, ctx, runner).then((output) => {
|
|
126
|
+
const result = interpretHookOutput(output);
|
|
127
|
+
if (handler.asyncRewake && output.exitCode === 2 && result.reason) {
|
|
128
|
+
pi.sendMessage({
|
|
129
|
+
customType: "hook_async_block",
|
|
130
|
+
content: result.reason,
|
|
131
|
+
display: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const output = await runHandler(handler, stdinData, ctx, getCallLLM(pi));
|
|
139
|
+
const result = interpretHookOutput(output);
|
|
140
|
+
|
|
141
|
+
if (result.shouldBlock) {
|
|
142
|
+
return { block: true, reason: result.reason };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.updatedInput) {
|
|
146
|
+
Object.assign(event.input, result.updatedInput);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function handlerIndex(event: string, idx: number): number {
|
|
156
|
+
return (hashString(event) * 31 + idx) | 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hashString(s: string): number {
|
|
160
|
+
let h = 0;
|
|
161
|
+
for (let i = 0; i < s.length; i++) {
|
|
162
|
+
h = (h * 31 + s.charCodeAt(i)) | 0;
|
|
163
|
+
}
|
|
164
|
+
return h;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getCallLLM(pi: ExtensionAPI) {
|
|
168
|
+
return ((pi as unknown) as Record<string, unknown>).callLLM as
|
|
169
|
+
| ((options: {
|
|
170
|
+
systemPrompt?: string;
|
|
171
|
+
messages: { role: "user" | "assistant"; content: string }[];
|
|
172
|
+
tools?: string[];
|
|
173
|
+
maxTurns?: number;
|
|
174
|
+
maxTokens?: number;
|
|
175
|
+
signal?: AbortSignal;
|
|
176
|
+
}) => Promise<string>)
|
|
177
|
+
| undefined;
|
|
178
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function matchesMatcher(matcher: string | undefined, toolName: string): boolean {
|
|
2
|
+
if (!matcher || matcher === "" || matcher === "*") return true;
|
|
3
|
+
|
|
4
|
+
if (/^[a-zA-Z0-9_|]+$/.test(matcher)) {
|
|
5
|
+
return matcher
|
|
6
|
+
.split("|")
|
|
7
|
+
.map((s) => s.trim().toLowerCase())
|
|
8
|
+
.includes(toolName.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
return new RegExp(matcher).test(toolName);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.debug("[claude-hooks-compat] matcher regex failed:", err instanceof Error ? err.message : err);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HookStdinData } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function buildStdinData(
|
|
4
|
+
eventName: string,
|
|
5
|
+
extra: {
|
|
6
|
+
toolName?: string;
|
|
7
|
+
toolInput?: Record<string, unknown>;
|
|
8
|
+
toolOutput?: string;
|
|
9
|
+
toolUseId?: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
agentType?: string;
|
|
13
|
+
},
|
|
14
|
+
): HookStdinData {
|
|
15
|
+
return {
|
|
16
|
+
session_id: extra.sessionId ?? "",
|
|
17
|
+
transcript_path: "",
|
|
18
|
+
cwd: extra.cwd,
|
|
19
|
+
permission_mode: "default",
|
|
20
|
+
hook_event_name: eventName,
|
|
21
|
+
tool_name: extra.toolName,
|
|
22
|
+
tool_input: extra.toolInput,
|
|
23
|
+
tool_use_id: extra.toolUseId,
|
|
24
|
+
tool_output: extra.toolOutput,
|
|
25
|
+
agent_type: extra.agentType,
|
|
26
|
+
};
|
|
27
|
+
}
|