my-pi 0.3.5 → 0.4.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/README.md +32 -252
- package/dist/extensions.d.ts +11 -0
- package/dist/extensions.js +148 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +40 -523
- package/dist/index.js.map +1 -1
- package/dist/manager.d.ts +3 -0
- package/dist/manager.js +71 -0
- package/dist/manager.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +16 -0
- package/dist/settings.js.map +1 -0
- package/package.json +33 -78
- package/dist/api.d.ts +0 -494
- package/dist/api.js +0 -532
- package/dist/api.js.map +0 -1
- package/dist/builtin-registry-wSfsQBi4.js +0 -563
- package/dist/builtin-registry-wSfsQBi4.js.map +0 -1
- package/dist/footer-BAZIMVSb.js +0 -1
- package/dist/handoff-C6wR2mSG.js +0 -28
- package/dist/handoff-C6wR2mSG.js.map +0 -1
- package/dist/hooks-resolution-qlhPfMFJ.js +0 -485
- package/dist/hooks-resolution-qlhPfMFJ.js.map +0 -1
- package/dist/openrouter-fusion-config-BWsHBPQv.js +0 -65
- package/dist/openrouter-fusion-config-BWsHBPQv.js.map +0 -1
- package/dist/prompt-presets-XraV03Ed.js +0 -1402
- package/dist/prompt-presets-XraV03Ed.js.map +0 -1
- package/dist/session-name-CD6TVRnh.js +0 -158
- package/dist/session-name-CD6TVRnh.js.map +0 -1
- package/dist/settings-BCn3P9qH.js +0 -177
- package/dist/settings-BCn3P9qH.js.map +0 -1
- package/dist/startup-screen-DUeBWuAc.js +0 -218
- package/dist/startup-screen-DUeBWuAc.js.map +0 -1
- package/dist/turn-timer-CoyDI4gQ.js +0 -64
- package/dist/turn-timer-CoyDI4gQ.js.map +0 -1
- package/dist/umans-provider-C2UJ4qkO.js +0 -307
- package/dist/umans-provider-C2UJ4qkO.js.map +0 -1
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { create_project_trust_wrapper, resolve_project_trust } from "@spences10/pi-project-trust";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { create_child_process_env, create_pi_session_env } from "@spences10/pi-child-env";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
//#region src/extensions/hooks-resolution/config.ts
|
|
8
|
-
function is_file(path) {
|
|
9
|
-
try {
|
|
10
|
-
return statSync(path).isFile();
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function as_record(value) {
|
|
16
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
function walk_up_directories(start_dir, stop_dir) {
|
|
20
|
-
const directories = [];
|
|
21
|
-
const has_stop_dir = stop_dir !== void 0;
|
|
22
|
-
let current = resolve(start_dir);
|
|
23
|
-
let parent = dirname(current);
|
|
24
|
-
let reached_stop_dir = has_stop_dir && current === stop_dir;
|
|
25
|
-
let reached_filesystem_root = parent === current;
|
|
26
|
-
directories.push(current);
|
|
27
|
-
while (!reached_stop_dir && !reached_filesystem_root) {
|
|
28
|
-
current = parent;
|
|
29
|
-
parent = dirname(current);
|
|
30
|
-
reached_stop_dir = has_stop_dir && current === stop_dir;
|
|
31
|
-
reached_filesystem_root = parent === current;
|
|
32
|
-
directories.push(current);
|
|
33
|
-
}
|
|
34
|
-
return directories;
|
|
35
|
-
}
|
|
36
|
-
function find_nearest_git_root(start_dir) {
|
|
37
|
-
for (const directory of walk_up_directories(start_dir)) if (existsSync(join(directory, ".git"))) return directory;
|
|
38
|
-
}
|
|
39
|
-
function has_hooks_config(directory) {
|
|
40
|
-
return is_file(join(directory, ".claude", "settings.json")) || is_file(join(directory, ".rulesync", "hooks.json")) || is_file(join(directory, ".pi", "hooks.json"));
|
|
41
|
-
}
|
|
42
|
-
function find_project_dir(cwd) {
|
|
43
|
-
const git_root = find_nearest_git_root(cwd);
|
|
44
|
-
for (const directory of walk_up_directories(cwd, git_root)) if (has_hooks_config(directory)) return directory;
|
|
45
|
-
return git_root ?? resolve(cwd);
|
|
46
|
-
}
|
|
47
|
-
function read_json_file(path) {
|
|
48
|
-
if (!is_file(path)) return void 0;
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
-
} catch {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function resolve_hook_command(command, project_dir) {
|
|
56
|
-
return command.replace(/\$CLAUDE_PROJECT_DIR\b/g, project_dir);
|
|
57
|
-
}
|
|
58
|
-
function compile_matcher(matcher_text) {
|
|
59
|
-
if (matcher_text === void 0) return void 0;
|
|
60
|
-
try {
|
|
61
|
-
return new RegExp(matcher_text);
|
|
62
|
-
} catch {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
function create_hook(event_name, matcher_text, command, source, project_dir) {
|
|
67
|
-
const matcher = compile_matcher(matcher_text);
|
|
68
|
-
if (matcher_text !== void 0 && matcher === void 0) return void 0;
|
|
69
|
-
return {
|
|
70
|
-
event_name,
|
|
71
|
-
matcher,
|
|
72
|
-
matcher_text,
|
|
73
|
-
command: resolve_hook_command(command, project_dir),
|
|
74
|
-
source
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function get_hook_entries(hooks_record, event_name) {
|
|
78
|
-
const keys = event_name === "PreToolUse" ? ["PreToolUse", "preToolUse"] : event_name === "PostToolUse" ? ["PostToolUse", "postToolUse"] : ["PostToolUseFailure", "postToolUseFailure"];
|
|
79
|
-
for (const key of keys) {
|
|
80
|
-
const value = hooks_record[key];
|
|
81
|
-
if (Array.isArray(value)) return value;
|
|
82
|
-
}
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
function parse_claude_settings_hooks(config, source, project_dir) {
|
|
86
|
-
const root = as_record(config);
|
|
87
|
-
const hooks_root = root ? as_record(root.hooks) : void 0;
|
|
88
|
-
if (!hooks_root) return [];
|
|
89
|
-
const hooks = [];
|
|
90
|
-
for (const event_name of [
|
|
91
|
-
"PreToolUse",
|
|
92
|
-
"PostToolUse",
|
|
93
|
-
"PostToolUseFailure"
|
|
94
|
-
]) {
|
|
95
|
-
const entries = get_hook_entries(hooks_root, event_name);
|
|
96
|
-
for (const entry of entries) {
|
|
97
|
-
const entry_record = as_record(entry);
|
|
98
|
-
if (!entry_record || !Array.isArray(entry_record.hooks)) continue;
|
|
99
|
-
const matcher_text = typeof entry_record.matcher === "string" ? entry_record.matcher : void 0;
|
|
100
|
-
for (const nested_hook of entry_record.hooks) {
|
|
101
|
-
const nested_record = as_record(nested_hook);
|
|
102
|
-
if (!nested_record) continue;
|
|
103
|
-
if (nested_record.type !== "command") continue;
|
|
104
|
-
if (typeof nested_record.command !== "string") continue;
|
|
105
|
-
const hook = create_hook(event_name, matcher_text, nested_record.command, source, project_dir);
|
|
106
|
-
if (hook) hooks.push(hook);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return hooks;
|
|
111
|
-
}
|
|
112
|
-
function parse_simple_hooks_file(config, source, project_dir) {
|
|
113
|
-
const root = as_record(config);
|
|
114
|
-
const hooks_root = root ? as_record(root.hooks) : void 0;
|
|
115
|
-
if (!hooks_root) return [];
|
|
116
|
-
const hooks = [];
|
|
117
|
-
for (const event_name of [
|
|
118
|
-
"PreToolUse",
|
|
119
|
-
"PostToolUse",
|
|
120
|
-
"PostToolUseFailure"
|
|
121
|
-
]) {
|
|
122
|
-
const entries = get_hook_entries(hooks_root, event_name);
|
|
123
|
-
for (const entry of entries) {
|
|
124
|
-
const entry_record = as_record(entry);
|
|
125
|
-
if (!entry_record || typeof entry_record.command !== "string") continue;
|
|
126
|
-
const hook = create_hook(event_name, typeof entry_record.matcher === "string" ? entry_record.matcher : void 0, entry_record.command, source, project_dir);
|
|
127
|
-
if (hook) hooks.push(hook);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return hooks;
|
|
131
|
-
}
|
|
132
|
-
function hook_config_paths(project_dir) {
|
|
133
|
-
return [
|
|
134
|
-
join(project_dir, ".claude", "settings.json"),
|
|
135
|
-
join(project_dir, ".rulesync", "hooks.json"),
|
|
136
|
-
join(project_dir, ".pi", "hooks.json")
|
|
137
|
-
];
|
|
138
|
-
}
|
|
139
|
-
function parse_hooks_config_file(path, project_dir) {
|
|
140
|
-
const config = read_json_file(path);
|
|
141
|
-
if (config === void 0) return [];
|
|
142
|
-
if (path.endsWith(join(".claude", "settings.json"))) return parse_claude_settings_hooks(config, path, project_dir);
|
|
143
|
-
return parse_simple_hooks_file(config, path, project_dir);
|
|
144
|
-
}
|
|
145
|
-
function load_hooks(cwd) {
|
|
146
|
-
const project_dir = find_project_dir(cwd);
|
|
147
|
-
return {
|
|
148
|
-
project_dir,
|
|
149
|
-
hooks: hook_config_paths(project_dir).flatMap((path) => parse_hooks_config_file(path, project_dir))
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function get_hooks_config_info(cwd) {
|
|
153
|
-
const project_dir = find_project_dir(cwd);
|
|
154
|
-
const sources = hook_config_paths(project_dir).filter(is_file);
|
|
155
|
-
if (sources.length === 0) return void 0;
|
|
156
|
-
const hash = createHash("sha256");
|
|
157
|
-
for (const source of sources) {
|
|
158
|
-
hash.update(source);
|
|
159
|
-
hash.update("\0");
|
|
160
|
-
hash.update(readFileSync(source, "utf8"));
|
|
161
|
-
hash.update("\0");
|
|
162
|
-
}
|
|
163
|
-
const hooks = sources.flatMap((source) => parse_hooks_config_file(source, project_dir)).map((hook) => ({
|
|
164
|
-
event_name: hook.event_name,
|
|
165
|
-
matcher_text: hook.matcher_text,
|
|
166
|
-
command: hook.command,
|
|
167
|
-
source: hook.source
|
|
168
|
-
}));
|
|
169
|
-
return {
|
|
170
|
-
project_dir,
|
|
171
|
-
hash: hash.digest("hex"),
|
|
172
|
-
sources,
|
|
173
|
-
hooks
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region src/extensions/hooks-resolution/env.ts
|
|
178
|
-
function create_hook_session_env(ctx) {
|
|
179
|
-
return create_pi_session_env({
|
|
180
|
-
session_id: ctx.sessionManager.getSessionId(),
|
|
181
|
-
session_file: ctx.sessionManager.getSessionFile(),
|
|
182
|
-
provider: ctx.model?.provider,
|
|
183
|
-
model: ctx.model?.id,
|
|
184
|
-
reasoning_level: ctx.thinkingLevel
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
function create_child_process_env$1(explicit_env = {}, source_env = process.env) {
|
|
188
|
-
return create_child_process_env({
|
|
189
|
-
profile: "hooks",
|
|
190
|
-
explicit_env,
|
|
191
|
-
source_env
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
//#endregion
|
|
195
|
-
//#region src/extensions/hooks-resolution/payload.ts
|
|
196
|
-
function to_claude_tool_name(tool_name) {
|
|
197
|
-
if (tool_name === "ls") return "LS";
|
|
198
|
-
if (tool_name.length === 0) return tool_name;
|
|
199
|
-
return tool_name[0].toUpperCase() + tool_name.slice(1);
|
|
200
|
-
}
|
|
201
|
-
function matches_hook(hook, tool_name) {
|
|
202
|
-
if (!hook.matcher) return true;
|
|
203
|
-
const claude_tool_name = to_claude_tool_name(tool_name);
|
|
204
|
-
hook.matcher.lastIndex = 0;
|
|
205
|
-
if (hook.matcher.test(tool_name)) return true;
|
|
206
|
-
hook.matcher.lastIndex = 0;
|
|
207
|
-
return hook.matcher.test(claude_tool_name);
|
|
208
|
-
}
|
|
209
|
-
function extract_text_content(content) {
|
|
210
|
-
if (!Array.isArray(content)) return "";
|
|
211
|
-
const parts = [];
|
|
212
|
-
for (const item of content) {
|
|
213
|
-
if (!item || typeof item !== "object") continue;
|
|
214
|
-
const item_record = item;
|
|
215
|
-
if (item_record.type === "text" && typeof item_record.text === "string") parts.push(item_record.text);
|
|
216
|
-
}
|
|
217
|
-
return parts.join("\n");
|
|
218
|
-
}
|
|
219
|
-
function normalize_tool_input(input) {
|
|
220
|
-
const normalized = { ...input };
|
|
221
|
-
const path_value = typeof input.path === "string" ? input.path : void 0;
|
|
222
|
-
if (path_value !== void 0) {
|
|
223
|
-
normalized.file_path = path_value;
|
|
224
|
-
normalized.filePath = path_value;
|
|
225
|
-
}
|
|
226
|
-
return normalized;
|
|
227
|
-
}
|
|
228
|
-
function build_tool_response(event, normalized_input) {
|
|
229
|
-
const response = {
|
|
230
|
-
is_error: event.isError,
|
|
231
|
-
isError: event.isError,
|
|
232
|
-
content: event.content,
|
|
233
|
-
text: extract_text_content(event.content),
|
|
234
|
-
details: event.details ?? null
|
|
235
|
-
};
|
|
236
|
-
const file_path = typeof normalized_input.file_path === "string" ? normalized_input.file_path : void 0;
|
|
237
|
-
if (file_path !== void 0) {
|
|
238
|
-
response.file_path = file_path;
|
|
239
|
-
response.filePath = file_path;
|
|
240
|
-
}
|
|
241
|
-
return response;
|
|
242
|
-
}
|
|
243
|
-
function build_hook_payload(event, event_name, ctx, project_dir) {
|
|
244
|
-
const normalized_input = normalize_tool_input(event.input);
|
|
245
|
-
const payload = {
|
|
246
|
-
session_id: ctx.sessionManager.getSessionFile() ?? "ephemeral",
|
|
247
|
-
cwd: ctx.cwd,
|
|
248
|
-
claude_project_dir: project_dir,
|
|
249
|
-
hook_event_name: event_name,
|
|
250
|
-
tool_name: to_claude_tool_name(event.toolName),
|
|
251
|
-
tool_call_id: event.toolCallId,
|
|
252
|
-
tool_input: normalized_input
|
|
253
|
-
};
|
|
254
|
-
if ("content" in event) payload.tool_response = build_tool_response(event, normalized_input);
|
|
255
|
-
return payload;
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
//#region src/extensions/hooks-resolution/runner.ts
|
|
259
|
-
const HOOK_TIMEOUT_MS = 6e5;
|
|
260
|
-
async function run_command_hook(command, cwd, payload, pi_session_env = {}) {
|
|
261
|
-
return await new Promise((resolve) => {
|
|
262
|
-
const started_at = Date.now();
|
|
263
|
-
const child = spawn("bash", ["-lc", command], {
|
|
264
|
-
cwd,
|
|
265
|
-
env: create_child_process_env$1({
|
|
266
|
-
CLAUDE_PROJECT_DIR: cwd,
|
|
267
|
-
...pi_session_env
|
|
268
|
-
}),
|
|
269
|
-
stdio: [
|
|
270
|
-
"pipe",
|
|
271
|
-
"pipe",
|
|
272
|
-
"pipe"
|
|
273
|
-
]
|
|
274
|
-
});
|
|
275
|
-
let stdout = "";
|
|
276
|
-
let stderr = "";
|
|
277
|
-
let timed_out = false;
|
|
278
|
-
let resolved = false;
|
|
279
|
-
const finish = (code) => {
|
|
280
|
-
if (resolved) return;
|
|
281
|
-
resolved = true;
|
|
282
|
-
resolve({
|
|
283
|
-
code,
|
|
284
|
-
stdout,
|
|
285
|
-
stderr,
|
|
286
|
-
elapsed_ms: Date.now() - started_at,
|
|
287
|
-
timed_out
|
|
288
|
-
});
|
|
289
|
-
};
|
|
290
|
-
const timeout = setTimeout(() => {
|
|
291
|
-
timed_out = true;
|
|
292
|
-
child.kill("SIGTERM");
|
|
293
|
-
setTimeout(() => {
|
|
294
|
-
child.kill("SIGKILL");
|
|
295
|
-
}, 1e3).unref?.();
|
|
296
|
-
}, HOOK_TIMEOUT_MS);
|
|
297
|
-
timeout.unref?.();
|
|
298
|
-
child.stdout.on("data", (chunk) => {
|
|
299
|
-
stdout += chunk.toString("utf8");
|
|
300
|
-
});
|
|
301
|
-
child.stderr.on("data", (chunk) => {
|
|
302
|
-
stderr += chunk.toString("utf8");
|
|
303
|
-
});
|
|
304
|
-
child.on("error", (error) => {
|
|
305
|
-
clearTimeout(timeout);
|
|
306
|
-
stderr += `${error.message}\n`;
|
|
307
|
-
finish(-1);
|
|
308
|
-
});
|
|
309
|
-
child.on("close", (code) => {
|
|
310
|
-
clearTimeout(timeout);
|
|
311
|
-
finish(code ?? -1);
|
|
312
|
-
});
|
|
313
|
-
try {
|
|
314
|
-
child.stdin.write(JSON.stringify(payload));
|
|
315
|
-
child.stdin.end();
|
|
316
|
-
} catch (error) {
|
|
317
|
-
stderr += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
function hook_event_name_for_result(event) {
|
|
322
|
-
return event.isError ? "PostToolUseFailure" : "PostToolUse";
|
|
323
|
-
}
|
|
324
|
-
function hook_block_reason(result) {
|
|
325
|
-
const parse_json = (text) => {
|
|
326
|
-
const trimmed = text.trim();
|
|
327
|
-
if (!trimmed) return void 0;
|
|
328
|
-
try {
|
|
329
|
-
return as_record(JSON.parse(trimmed));
|
|
330
|
-
} catch {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
const json = parse_json(result.stdout) ?? parse_json(result.stderr);
|
|
335
|
-
if (json?.decision === "block") return typeof json.reason === "string" ? json.reason : "Blocked by hook";
|
|
336
|
-
if (result.code === 2) return result.stderr.trim() || result.stdout.trim() || "Blocked by hook";
|
|
337
|
-
}
|
|
338
|
-
function format_duration(elapsed_ms) {
|
|
339
|
-
if (elapsed_ms < 1e3) return `${elapsed_ms}ms`;
|
|
340
|
-
return `${(elapsed_ms / 1e3).toFixed(1)}s`;
|
|
341
|
-
}
|
|
342
|
-
function hook_name(command) {
|
|
343
|
-
const sh_path_match = command.match(/[^\s|;&]+\.sh\b/);
|
|
344
|
-
if (sh_path_match) return basename(sh_path_match[0]);
|
|
345
|
-
const first_token = command.trim().split(/\s+/)[0] ?? "hook";
|
|
346
|
-
return basename(first_token);
|
|
347
|
-
}
|
|
348
|
-
//#endregion
|
|
349
|
-
//#region src/extensions/hooks-resolution/trust.ts
|
|
350
|
-
const HOOKS_CONFIG_ENV$1 = "MY_PI_HOOKS_CONFIG";
|
|
351
|
-
const hooks_config_trust = create_project_trust_wrapper({
|
|
352
|
-
store_filename: "trusted-hooks.json",
|
|
353
|
-
legacy_matcher: (entry, subject) => {
|
|
354
|
-
const legacy_entry = entry;
|
|
355
|
-
return legacy_entry?.project_dir === subject.id && legacy_entry.hash === subject.hash;
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
function default_hooks_trust_store_path() {
|
|
359
|
-
return hooks_config_trust.default_trust_store_path();
|
|
360
|
-
}
|
|
361
|
-
function create_hooks_config_trust_subject(project_dir, hash) {
|
|
362
|
-
return {
|
|
363
|
-
kind: "hooks-config",
|
|
364
|
-
id: project_dir,
|
|
365
|
-
store_key: project_dir,
|
|
366
|
-
hash,
|
|
367
|
-
env_key: HOOKS_CONFIG_ENV$1,
|
|
368
|
-
prompt_title: "Project hook config can execute shell commands after tool use. Trust these hooks?"
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
function is_hooks_config_trusted(project_dir, hash, trust_store_path = default_hooks_trust_store_path()) {
|
|
372
|
-
return hooks_config_trust.is_trusted(create_hooks_config_trust_subject(project_dir, hash), trust_store_path);
|
|
373
|
-
}
|
|
374
|
-
//#endregion
|
|
375
|
-
//#region src/extensions/hooks-resolution/trust-gate.ts
|
|
376
|
-
const HOOKS_CONFIG_ENV = "MY_PI_HOOKS_CONFIG";
|
|
377
|
-
function create_hooks_trust_subject(info) {
|
|
378
|
-
const source_lines = info.sources.map((source) => `- ${source}`);
|
|
379
|
-
const hook_lines = info.hooks.length === 0 ? ["- no valid command hooks detected"] : info.hooks.map((hook) => {
|
|
380
|
-
const matcher = hook.matcher_text ? ` matcher=${hook.matcher_text}` : "";
|
|
381
|
-
return `- ${hook.event_name}${matcher}: ${hook.command}`;
|
|
382
|
-
});
|
|
383
|
-
return {
|
|
384
|
-
kind: "hooks-config",
|
|
385
|
-
id: info.project_dir,
|
|
386
|
-
store_key: info.project_dir,
|
|
387
|
-
hash: info.hash,
|
|
388
|
-
env_key: HOOKS_CONFIG_ENV,
|
|
389
|
-
prompt_title: "Project hook config can execute shell commands after tool use. Trust these hooks?",
|
|
390
|
-
summary_lines: [
|
|
391
|
-
"Sources:",
|
|
392
|
-
...source_lines,
|
|
393
|
-
"Commands:",
|
|
394
|
-
...hook_lines
|
|
395
|
-
],
|
|
396
|
-
choices: {
|
|
397
|
-
allow_once: "Allow once for this session",
|
|
398
|
-
trust: "Trust this repo until hook config changes",
|
|
399
|
-
skip: "Skip project hooks"
|
|
400
|
-
},
|
|
401
|
-
headless_warning: `Skipping untrusted hook config in ${info.project_dir}. Set ${HOOKS_CONFIG_ENV}=allow to enable hooks for this run.`
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
async function should_load_hooks_config(cwd, ctx) {
|
|
405
|
-
const info = get_hooks_config_info(cwd);
|
|
406
|
-
if (!info) return true;
|
|
407
|
-
if (is_hooks_config_trusted(info.project_dir, info.hash)) return true;
|
|
408
|
-
const decision = await resolve_project_trust(create_hooks_trust_subject(info), {
|
|
409
|
-
has_ui: ctx?.hasUI,
|
|
410
|
-
select: ctx?.hasUI ? async (message, choices) => {
|
|
411
|
-
return await ctx.ui.select(message, choices) ?? "";
|
|
412
|
-
} : void 0,
|
|
413
|
-
env: process.env,
|
|
414
|
-
trust_store_path: default_hooks_trust_store_path()
|
|
415
|
-
});
|
|
416
|
-
return decision.action === "allow-once" || decision.action === "trust-persisted";
|
|
417
|
-
}
|
|
418
|
-
//#endregion
|
|
419
|
-
//#region src/extensions/hooks-resolution/index.ts
|
|
420
|
-
function create_hooks_resolution_extension(options = {}) {
|
|
421
|
-
const load_hooks_impl = options.load_hooks ?? load_hooks;
|
|
422
|
-
const run_command_hook_impl = options.run_command_hook ?? run_command_hook;
|
|
423
|
-
return async function hooks_resolution(pi) {
|
|
424
|
-
let state = {
|
|
425
|
-
project_dir: process.cwd(),
|
|
426
|
-
hooks: []
|
|
427
|
-
};
|
|
428
|
-
const refresh_hooks = async (cwd, ctx) => {
|
|
429
|
-
if (!await should_load_hooks_config(cwd, ctx)) {
|
|
430
|
-
state = {
|
|
431
|
-
project_dir: cwd,
|
|
432
|
-
hooks: []
|
|
433
|
-
};
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
state = load_hooks_impl(cwd);
|
|
437
|
-
};
|
|
438
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
439
|
-
await refresh_hooks(ctx.cwd, ctx);
|
|
440
|
-
});
|
|
441
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
442
|
-
if (state.hooks.length === 0) return;
|
|
443
|
-
const matching_hooks = state.hooks.filter((hook) => hook.event_name === "PreToolUse" && matches_hook(hook, event.toolName));
|
|
444
|
-
if (matching_hooks.length === 0) return;
|
|
445
|
-
const payload = build_hook_payload(event, "PreToolUse", ctx, state.project_dir);
|
|
446
|
-
const executed_commands = /* @__PURE__ */ new Set();
|
|
447
|
-
for (const hook of matching_hooks) {
|
|
448
|
-
if (executed_commands.has(hook.command)) continue;
|
|
449
|
-
executed_commands.add(hook.command);
|
|
450
|
-
const reason = hook_block_reason(await run_command_hook_impl(hook.command, state.project_dir, payload, create_hook_session_env(ctx)));
|
|
451
|
-
if (reason) return {
|
|
452
|
-
block: true,
|
|
453
|
-
reason
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
pi.on("tool_result", async (event, ctx) => {
|
|
458
|
-
if (state.hooks.length === 0) return;
|
|
459
|
-
const event_name = hook_event_name_for_result(event);
|
|
460
|
-
const matching_hooks = state.hooks.filter((hook) => hook.event_name === event_name && matches_hook(hook, event.toolName));
|
|
461
|
-
if (matching_hooks.length === 0) return;
|
|
462
|
-
const payload = build_hook_payload(event, event_name, ctx, state.project_dir);
|
|
463
|
-
const executed_commands = /* @__PURE__ */ new Set();
|
|
464
|
-
for (const hook of matching_hooks) {
|
|
465
|
-
if (executed_commands.has(hook.command)) continue;
|
|
466
|
-
executed_commands.add(hook.command);
|
|
467
|
-
const result = await run_command_hook_impl(hook.command, state.project_dir, payload, create_hook_session_env(ctx));
|
|
468
|
-
const name = hook_name(hook.command);
|
|
469
|
-
const duration = format_duration(result.elapsed_ms);
|
|
470
|
-
if (ctx.hasUI) {
|
|
471
|
-
if (result.code === 0) ctx.ui.notify(`Hook \`${name}\` ran (${duration})`, "info");
|
|
472
|
-
else {
|
|
473
|
-
const error_line = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
474
|
-
ctx.ui.notify(`Hook \`${name}\` failed (${duration}): ${error_line}`, "warning");
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
var hooks_resolution_default = create_hooks_resolution_extension();
|
|
482
|
-
//#endregion
|
|
483
|
-
export { hooks_resolution_default as default };
|
|
484
|
-
|
|
485
|
-
//# sourceMappingURL=hooks-resolution-qlhPfMFJ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-resolution-qlhPfMFJ.js","names":["create_child_process_env","create_shared_child_process_env","create_child_process_env","HOOKS_CONFIG_ENV"],"sources":["../src/extensions/hooks-resolution/config.ts","../src/extensions/hooks-resolution/env.ts","../src/extensions/hooks-resolution/payload.ts","../src/extensions/hooks-resolution/runner.ts","../src/extensions/hooks-resolution/trust.ts","../src/extensions/hooks-resolution/trust-gate.ts","../src/extensions/hooks-resolution/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { existsSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport type {\n\tHookEventName,\n\tHooksConfigInfo,\n\tHookState,\n\tJsonValue,\n\tResolvedCommandHook,\n} from './types.js';\n\nexport function is_file(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function as_record(\n\tvalue: unknown,\n): Record<string, unknown> | undefined {\n\tif (typeof value !== 'object' || value === null) return undefined;\n\treturn value as Record<string, unknown>;\n}\n\nexport function walk_up_directories(\n\tstart_dir: string,\n\tstop_dir?: string,\n): string[] {\n\tconst directories: string[] = [];\n\tconst has_stop_dir = stop_dir !== undefined;\n\tlet current = resolve(start_dir);\n\tlet parent = dirname(current);\n\tlet reached_stop_dir = has_stop_dir && current === stop_dir;\n\tlet reached_filesystem_root = parent === current;\n\n\tdirectories.push(current);\n\twhile (!reached_stop_dir && !reached_filesystem_root) {\n\t\tcurrent = parent;\n\t\tparent = dirname(current);\n\t\treached_stop_dir = has_stop_dir && current === stop_dir;\n\t\treached_filesystem_root = parent === current;\n\t\tdirectories.push(current);\n\t}\n\n\treturn directories;\n}\n\nexport function find_nearest_git_root(\n\tstart_dir: string,\n): string | undefined {\n\tfor (const directory of walk_up_directories(start_dir)) {\n\t\tif (existsSync(join(directory, '.git'))) {\n\t\t\treturn directory;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function has_hooks_config(directory: string): boolean {\n\treturn (\n\t\tis_file(join(directory, '.claude', 'settings.json')) ||\n\t\tis_file(join(directory, '.rulesync', 'hooks.json')) ||\n\t\tis_file(join(directory, '.pi', 'hooks.json'))\n\t);\n}\n\nexport function find_project_dir(cwd: string): string {\n\tconst git_root = find_nearest_git_root(cwd);\n\tfor (const directory of walk_up_directories(cwd, git_root)) {\n\t\tif (has_hooks_config(directory)) {\n\t\t\treturn directory;\n\t\t}\n\t}\n\treturn git_root ?? resolve(cwd);\n}\n\nexport function read_json_file(path: string): JsonValue | undefined {\n\tif (!is_file(path)) return undefined;\n\ttry {\n\t\treturn JSON.parse(readFileSync(path, 'utf8')) as JsonValue;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function resolve_hook_command(\n\tcommand: string,\n\tproject_dir: string,\n): string {\n\treturn command.replace(/\\$CLAUDE_PROJECT_DIR\\b/g, project_dir);\n}\n\nexport function compile_matcher(\n\tmatcher_text: string | undefined,\n): RegExp | undefined {\n\tif (matcher_text === undefined) return undefined;\n\ttry {\n\t\treturn new RegExp(matcher_text);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function create_hook(\n\tevent_name: HookEventName,\n\tmatcher_text: string | undefined,\n\tcommand: string,\n\tsource: string,\n\tproject_dir: string,\n): ResolvedCommandHook | undefined {\n\tconst matcher = compile_matcher(matcher_text);\n\tif (matcher_text !== undefined && matcher === undefined)\n\t\treturn undefined;\n\treturn {\n\t\tevent_name,\n\t\tmatcher,\n\t\tmatcher_text,\n\t\tcommand: resolve_hook_command(command, project_dir),\n\t\tsource,\n\t};\n}\n\nexport function get_hook_entries(\n\thooks_record: Record<string, unknown>,\n\tevent_name: HookEventName,\n): unknown[] {\n\tconst keys =\n\t\tevent_name === 'PreToolUse'\n\t\t\t? ['PreToolUse', 'preToolUse']\n\t\t\t: event_name === 'PostToolUse'\n\t\t\t\t? ['PostToolUse', 'postToolUse']\n\t\t\t\t: ['PostToolUseFailure', 'postToolUseFailure'];\n\n\tfor (const key of keys) {\n\t\tconst value = hooks_record[key];\n\t\tif (Array.isArray(value)) return value;\n\t}\n\treturn [];\n}\n\nexport function parse_claude_settings_hooks(\n\tconfig: unknown,\n\tsource: string,\n\tproject_dir: string,\n): ResolvedCommandHook[] {\n\tconst root = as_record(config);\n\tconst hooks_root = root ? as_record(root.hooks) : undefined;\n\tif (!hooks_root) return [];\n\n\tconst hooks: ResolvedCommandHook[] = [];\n\tconst events: HookEventName[] = [\n\t\t'PreToolUse',\n\t\t'PostToolUse',\n\t\t'PostToolUseFailure',\n\t];\n\n\tfor (const event_name of events) {\n\t\tconst entries = get_hook_entries(hooks_root, event_name);\n\t\tfor (const entry of entries) {\n\t\t\tconst entry_record = as_record(entry);\n\t\t\tif (!entry_record || !Array.isArray(entry_record.hooks))\n\t\t\t\tcontinue;\n\n\t\t\tconst matcher_text =\n\t\t\t\ttypeof entry_record.matcher === 'string'\n\t\t\t\t\t? entry_record.matcher\n\t\t\t\t\t: undefined;\n\t\t\tfor (const nested_hook of entry_record.hooks) {\n\t\t\t\tconst nested_record = as_record(nested_hook);\n\t\t\t\tif (!nested_record) continue;\n\t\t\t\tif (nested_record.type !== 'command') continue;\n\t\t\t\tif (typeof nested_record.command !== 'string') continue;\n\n\t\t\t\tconst hook = create_hook(\n\t\t\t\t\tevent_name,\n\t\t\t\t\tmatcher_text,\n\t\t\t\t\tnested_record.command,\n\t\t\t\t\tsource,\n\t\t\t\t\tproject_dir,\n\t\t\t\t);\n\t\t\t\tif (hook) hooks.push(hook);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn hooks;\n}\n\nexport function parse_simple_hooks_file(\n\tconfig: unknown,\n\tsource: string,\n\tproject_dir: string,\n): ResolvedCommandHook[] {\n\tconst root = as_record(config);\n\tconst hooks_root = root ? as_record(root.hooks) : undefined;\n\tif (!hooks_root) return [];\n\n\tconst hooks: ResolvedCommandHook[] = [];\n\tconst events: HookEventName[] = [\n\t\t'PreToolUse',\n\t\t'PostToolUse',\n\t\t'PostToolUseFailure',\n\t];\n\n\tfor (const event_name of events) {\n\t\tconst entries = get_hook_entries(hooks_root, event_name);\n\t\tfor (const entry of entries) {\n\t\t\tconst entry_record = as_record(entry);\n\t\t\tif (!entry_record || typeof entry_record.command !== 'string') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst matcher_text =\n\t\t\t\ttypeof entry_record.matcher === 'string'\n\t\t\t\t\t? entry_record.matcher\n\t\t\t\t\t: undefined;\n\t\t\tconst hook = create_hook(\n\t\t\t\tevent_name,\n\t\t\t\tmatcher_text,\n\t\t\t\tentry_record.command,\n\t\t\t\tsource,\n\t\t\t\tproject_dir,\n\t\t\t);\n\t\t\tif (hook) hooks.push(hook);\n\t\t}\n\t}\n\n\treturn hooks;\n}\n\nfunction hook_config_paths(project_dir: string): string[] {\n\treturn [\n\t\tjoin(project_dir, '.claude', 'settings.json'),\n\t\tjoin(project_dir, '.rulesync', 'hooks.json'),\n\t\tjoin(project_dir, '.pi', 'hooks.json'),\n\t];\n}\n\nfunction parse_hooks_config_file(\n\tpath: string,\n\tproject_dir: string,\n): ResolvedCommandHook[] {\n\tconst config = read_json_file(path);\n\tif (config === undefined) return [];\n\tif (path.endsWith(join('.claude', 'settings.json'))) {\n\t\treturn parse_claude_settings_hooks(config, path, project_dir);\n\t}\n\treturn parse_simple_hooks_file(config, path, project_dir);\n}\n\nexport function load_hooks(cwd: string): HookState {\n\tconst project_dir = find_project_dir(cwd);\n\tconst hooks = hook_config_paths(project_dir).flatMap((path) =>\n\t\tparse_hooks_config_file(path, project_dir),\n\t);\n\n\treturn { project_dir, hooks };\n}\n\nexport function get_hooks_config_info(\n\tcwd: string,\n): HooksConfigInfo | undefined {\n\tconst project_dir = find_project_dir(cwd);\n\tconst sources = hook_config_paths(project_dir).filter(is_file);\n\tif (sources.length === 0) return undefined;\n\n\tconst hash = createHash('sha256');\n\tfor (const source of sources) {\n\t\thash.update(source);\n\t\thash.update('\\0');\n\t\thash.update(readFileSync(source, 'utf8'));\n\t\thash.update('\\0');\n\t}\n\n\tconst hooks = sources\n\t\t.flatMap((source) => parse_hooks_config_file(source, project_dir))\n\t\t.map((hook) => ({\n\t\t\tevent_name: hook.event_name,\n\t\t\tmatcher_text: hook.matcher_text,\n\t\t\tcommand: hook.command,\n\t\t\tsource: hook.source,\n\t\t}));\n\n\treturn {\n\t\tproject_dir,\n\t\thash: hash.digest('hex'),\n\t\tsources,\n\t\thooks,\n\t};\n}\n","import type { ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n\tcreate_pi_session_env,\n\tcreate_child_process_env as create_shared_child_process_env,\n} from '@spences10/pi-child-env';\n\nexport function create_hook_session_env(\n\tctx: ExtensionContext,\n): Record<string, string> {\n\treturn create_pi_session_env({\n\t\tsession_id: ctx.sessionManager.getSessionId(),\n\t\tsession_file: ctx.sessionManager.getSessionFile(),\n\t\tprovider: ctx.model?.provider,\n\t\tmodel: ctx.model?.id,\n\t\treasoning_level: ctx.thinkingLevel,\n\t});\n}\n\nexport function create_child_process_env(\n\texplicit_env: Record<string, string> = {},\n\tsource_env: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\treturn create_shared_child_process_env({\n\t\tprofile: 'hooks',\n\t\texplicit_env,\n\t\tsource_env,\n\t});\n}\n","import type {\n\tExtensionContext,\n\tToolCallEvent,\n\tToolResultEvent,\n} from '@earendil-works/pi-coding-agent';\nimport type { HookEventName, ResolvedCommandHook } from './types.js';\n\nexport function to_claude_tool_name(tool_name: string): string {\n\tif (tool_name === 'ls') return 'LS';\n\tif (tool_name.length === 0) return tool_name;\n\treturn tool_name[0].toUpperCase() + tool_name.slice(1);\n}\n\nexport function matches_hook(\n\thook: ResolvedCommandHook,\n\ttool_name: string,\n): boolean {\n\tif (!hook.matcher) return true;\n\n\tconst claude_tool_name = to_claude_tool_name(tool_name);\n\thook.matcher.lastIndex = 0;\n\tif (hook.matcher.test(tool_name)) return true;\n\n\thook.matcher.lastIndex = 0;\n\treturn hook.matcher.test(claude_tool_name);\n}\n\nexport function extract_text_content(content: unknown): string {\n\tif (!Array.isArray(content)) return '';\n\n\tconst parts: string[] = [];\n\tfor (const item of content) {\n\t\tif (!item || typeof item !== 'object') continue;\n\t\tconst item_record = item as Record<string, unknown>;\n\t\tif (\n\t\t\titem_record.type === 'text' &&\n\t\t\ttypeof item_record.text === 'string'\n\t\t) {\n\t\t\tparts.push(item_record.text);\n\t\t}\n\t}\n\n\treturn parts.join('\\n');\n}\n\nexport function normalize_tool_input(\n\tinput: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...input };\n\tconst path_value =\n\t\ttypeof input.path === 'string' ? input.path : undefined;\n\tif (path_value !== undefined) {\n\t\tnormalized.file_path = path_value;\n\t\tnormalized.filePath = path_value;\n\t}\n\treturn normalized;\n}\n\nexport function build_tool_response(\n\tevent: ToolResultEvent,\n\tnormalized_input: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst response: Record<string, unknown> = {\n\t\tis_error: event.isError,\n\t\tisError: event.isError,\n\t\tcontent: event.content,\n\t\ttext: extract_text_content(event.content),\n\t\tdetails: event.details ?? null,\n\t};\n\n\tconst file_path =\n\t\ttypeof normalized_input.file_path === 'string'\n\t\t\t? normalized_input.file_path\n\t\t\t: undefined;\n\tif (file_path !== undefined) {\n\t\tresponse.file_path = file_path;\n\t\tresponse.filePath = file_path;\n\t}\n\n\treturn response;\n}\n\nexport function build_hook_payload(\n\tevent: ToolCallEvent | ToolResultEvent,\n\tevent_name: HookEventName,\n\tctx: ExtensionContext,\n\tproject_dir: string,\n): Record<string, unknown> {\n\tconst normalized_input = normalize_tool_input(\n\t\tevent.input as Record<string, unknown>,\n\t);\n\tconst session_id =\n\t\tctx.sessionManager.getSessionFile() ?? 'ephemeral';\n\tconst payload: Record<string, unknown> = {\n\t\tsession_id,\n\t\tcwd: ctx.cwd,\n\t\tclaude_project_dir: project_dir,\n\t\thook_event_name: event_name,\n\t\ttool_name: to_claude_tool_name(event.toolName),\n\t\ttool_call_id: event.toolCallId,\n\t\ttool_input: normalized_input,\n\t};\n\n\tif ('content' in event) {\n\t\tpayload.tool_response = build_tool_response(\n\t\t\tevent,\n\t\t\tnormalized_input,\n\t\t);\n\t}\n\n\treturn payload;\n}\n","import type { ToolResultEvent } from '@earendil-works/pi-coding-agent';\nimport { spawn } from 'node:child_process';\nimport { basename } from 'node:path';\nimport { as_record } from './config.js';\nimport { create_child_process_env } from './env.js';\nimport type { CommandRunResult, HookEventName } from './types.js';\n\nconst HOOK_TIMEOUT_MS = 10 * 60 * 1000;\n\nexport async function run_command_hook(\n\tcommand: string,\n\tcwd: string,\n\tpayload: Record<string, unknown>,\n\tpi_session_env: Record<string, string> = {},\n): Promise<CommandRunResult> {\n\treturn await new Promise((resolve) => {\n\t\tconst started_at = Date.now();\n\t\tconst child = spawn('bash', ['-lc', command], {\n\t\t\tcwd,\n\t\t\tenv: create_child_process_env({\n\t\t\t\tCLAUDE_PROJECT_DIR: cwd,\n\t\t\t\t...pi_session_env,\n\t\t\t}),\n\t\t\tstdio: ['pipe', 'pipe', 'pipe'],\n\t\t});\n\n\t\tlet stdout = '';\n\t\tlet stderr = '';\n\t\tlet timed_out = false;\n\t\tlet resolved = false;\n\n\t\tconst finish = (code: number) => {\n\t\t\tif (resolved) return;\n\t\t\tresolved = true;\n\t\t\tresolve({\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\telapsed_ms: Date.now() - started_at,\n\t\t\t\ttimed_out,\n\t\t\t});\n\t\t};\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\ttimed_out = true;\n\t\t\tchild.kill('SIGTERM');\n\t\t\tconst kill_timer = setTimeout(() => {\n\t\t\t\tchild.kill('SIGKILL');\n\t\t\t}, 1000);\n\t\t\t(\n\t\t\t\tkill_timer as NodeJS.Timeout & { unref?: () => void }\n\t\t\t).unref?.();\n\t\t}, HOOK_TIMEOUT_MS);\n\t\t(timeout as NodeJS.Timeout & { unref?: () => void }).unref?.();\n\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\tstdout += chunk.toString('utf8');\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString('utf8');\n\t\t});\n\n\t\tchild.on('error', (error) => {\n\t\t\tclearTimeout(timeout);\n\t\t\tstderr += `${error.message}\\n`;\n\t\t\tfinish(-1);\n\t\t});\n\n\t\tchild.on('close', (code) => {\n\t\t\tclearTimeout(timeout);\n\t\t\tfinish(code ?? -1);\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(payload));\n\t\t\tchild.stdin.end();\n\t\t} catch (error) {\n\t\t\tstderr += `${error instanceof Error ? error.message : String(error)}\\n`;\n\t\t}\n\t});\n}\n\nexport function hook_event_name_for_result(\n\tevent: ToolResultEvent,\n): HookEventName {\n\treturn event.isError ? 'PostToolUseFailure' : 'PostToolUse';\n}\n\nexport function hook_block_reason(\n\tresult: CommandRunResult,\n): string | undefined {\n\tconst parse_json = (\n\t\ttext: string,\n\t): Record<string, unknown> | undefined => {\n\t\tconst trimmed = text.trim();\n\t\tif (!trimmed) return undefined;\n\t\ttry {\n\t\t\treturn as_record(JSON.parse(trimmed));\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\tconst json = parse_json(result.stdout) ?? parse_json(result.stderr);\n\tif (json?.decision === 'block') {\n\t\treturn typeof json.reason === 'string'\n\t\t\t? json.reason\n\t\t\t: 'Blocked by hook';\n\t}\n\tif (result.code === 2) {\n\t\treturn (\n\t\t\tresult.stderr.trim() ||\n\t\t\tresult.stdout.trim() ||\n\t\t\t'Blocked by hook'\n\t\t);\n\t}\n\treturn undefined;\n}\n\nexport function format_duration(elapsed_ms: number): string {\n\tif (elapsed_ms < 1000) return `${elapsed_ms}ms`;\n\treturn `${(elapsed_ms / 1000).toFixed(1)}s`;\n}\n\nexport function hook_name(command: string): string {\n\tconst sh_path_match = command.match(/[^\\s|;&]+\\.sh\\b/);\n\tif (sh_path_match) return basename(sh_path_match[0]);\n\tconst first_token = command.trim().split(/\\s+/)[0] ?? 'hook';\n\treturn basename(first_token);\n}\n","import {\n\tcreate_project_trust_wrapper,\n\ttype ProjectTrustSubject,\n} from '@spences10/pi-project-trust';\n\nconst HOOKS_CONFIG_ENV = 'MY_PI_HOOKS_CONFIG';\n\nconst hooks_config_trust = create_project_trust_wrapper({\n\tstore_filename: 'trusted-hooks.json',\n\tlegacy_matcher: (entry, subject) => {\n\t\tconst legacy_entry = entry as\n\t\t\t| { project_dir?: unknown; hash?: unknown }\n\t\t\t| undefined;\n\t\treturn (\n\t\t\tlegacy_entry?.project_dir === subject.id &&\n\t\t\tlegacy_entry.hash === subject.hash\n\t\t);\n\t},\n});\n\nexport function default_hooks_trust_store_path(): string {\n\treturn hooks_config_trust.default_trust_store_path();\n}\n\nexport function create_hooks_config_trust_subject(\n\tproject_dir: string,\n\thash: string,\n): ProjectTrustSubject {\n\treturn {\n\t\tkind: 'hooks-config',\n\t\tid: project_dir,\n\t\tstore_key: project_dir,\n\t\thash,\n\t\tenv_key: HOOKS_CONFIG_ENV,\n\t\tprompt_title:\n\t\t\t'Project hook config can execute shell commands after tool use. Trust these hooks?',\n\t};\n}\n\nexport function is_hooks_config_trusted(\n\tproject_dir: string,\n\thash: string,\n\ttrust_store_path = default_hooks_trust_store_path(),\n): boolean {\n\treturn hooks_config_trust.is_trusted(\n\t\tcreate_hooks_config_trust_subject(project_dir, hash),\n\t\ttrust_store_path,\n\t);\n}\n\nexport function trust_hooks_config(\n\tproject_dir: string,\n\thash: string,\n\ttrust_store_path = default_hooks_trust_store_path(),\n): void {\n\thooks_config_trust.trust(\n\t\tcreate_hooks_config_trust_subject(project_dir, hash),\n\t\ttrust_store_path,\n\t);\n}\n","import type { ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n\tresolve_project_trust,\n\ttype ProjectTrustSubject,\n} from '@spences10/pi-project-trust';\nimport { get_hooks_config_info } from './config.js';\nimport {\n\tdefault_hooks_trust_store_path,\n\tis_hooks_config_trusted,\n} from './trust.js';\nimport type { HooksConfigInfo } from './types.js';\n\nconst HOOKS_CONFIG_ENV = 'MY_PI_HOOKS_CONFIG';\n\nexport function create_hooks_trust_subject(\n\tinfo: HooksConfigInfo,\n): ProjectTrustSubject {\n\tconst source_lines = info.sources.map((source) => `- ${source}`);\n\tconst hook_lines =\n\t\tinfo.hooks.length === 0\n\t\t\t? ['- no valid command hooks detected']\n\t\t\t: info.hooks.map((hook) => {\n\t\t\t\t\tconst matcher = hook.matcher_text\n\t\t\t\t\t\t? ` matcher=${hook.matcher_text}`\n\t\t\t\t\t\t: '';\n\t\t\t\t\treturn `- ${hook.event_name}${matcher}: ${hook.command}`;\n\t\t\t\t});\n\treturn {\n\t\tkind: 'hooks-config',\n\t\tid: info.project_dir,\n\t\tstore_key: info.project_dir,\n\t\thash: info.hash,\n\t\tenv_key: HOOKS_CONFIG_ENV,\n\t\tprompt_title:\n\t\t\t'Project hook config can execute shell commands after tool use. Trust these hooks?',\n\t\tsummary_lines: [\n\t\t\t'Sources:',\n\t\t\t...source_lines,\n\t\t\t'Commands:',\n\t\t\t...hook_lines,\n\t\t],\n\t\tchoices: {\n\t\t\tallow_once: 'Allow once for this session',\n\t\t\ttrust: 'Trust this repo until hook config changes',\n\t\t\tskip: 'Skip project hooks',\n\t\t},\n\t\theadless_warning: `Skipping untrusted hook config in ${info.project_dir}. Set ${HOOKS_CONFIG_ENV}=allow to enable hooks for this run.`,\n\t};\n}\n\nexport async function should_load_hooks_config(\n\tcwd: string,\n\tctx?: ExtensionContext,\n): Promise<boolean> {\n\tconst info = get_hooks_config_info(cwd);\n\tif (!info) return true;\n\tif (is_hooks_config_trusted(info.project_dir, info.hash))\n\t\treturn true;\n\n\tconst decision = await resolve_project_trust(\n\t\tcreate_hooks_trust_subject(info),\n\t\t{\n\t\t\thas_ui: ctx?.hasUI,\n\t\t\tselect: ctx?.hasUI\n\t\t\t\t? async (\n\t\t\t\t\t\tmessage: string,\n\t\t\t\t\t\tchoices: string[],\n\t\t\t\t\t): Promise<string> => {\n\t\t\t\t\t\tconst selected = await ctx.ui.select(message, choices);\n\t\t\t\t\t\treturn selected ?? '';\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t\tenv: process.env,\n\t\t\ttrust_store_path: default_hooks_trust_store_path(),\n\t\t},\n\t);\n\treturn (\n\t\tdecision.action === 'allow-once' ||\n\t\tdecision.action === 'trust-persisted'\n\t);\n}\n","// Hooks resolution — Claude Code style hook compatibility\n\nimport type {\n\tExtensionAPI,\n\tExtensionContext,\n\tExtensionFactory,\n\tToolCallEventResult,\n} from '@earendil-works/pi-coding-agent';\nimport { load_hooks } from './config.js';\nimport { create_hook_session_env } from './env.js';\nimport { build_hook_payload, matches_hook } from './payload.js';\nimport {\n\tformat_duration,\n\thook_block_reason,\n\thook_event_name_for_result,\n\thook_name,\n\trun_command_hook,\n} from './runner.js';\nimport { should_load_hooks_config } from './trust-gate.js';\nimport type { CommandRunResult, HookState } from './types.js';\n\nexport {\n\tas_record,\n\tcompile_matcher,\n\tcreate_hook,\n\tfind_nearest_git_root,\n\tfind_project_dir,\n\tget_hook_entries,\n\tget_hooks_config_info,\n\thas_hooks_config,\n\tis_file,\n\tload_hooks,\n\tparse_claude_settings_hooks,\n\tparse_simple_hooks_file,\n\tread_json_file,\n\tresolve_hook_command,\n\twalk_up_directories,\n} from './config.js';\nexport {\n\tbuild_hook_payload,\n\tbuild_tool_response,\n\textract_text_content,\n\tmatches_hook,\n\tnormalize_tool_input,\n\tto_claude_tool_name,\n} from './payload.js';\nexport {\n\tformat_duration,\n\thook_block_reason,\n\thook_event_name_for_result,\n\thook_name,\n\trun_command_hook,\n} from './runner.js';\nexport type {\n\tCommandRunResult,\n\tHookEventName,\n\tHooksConfigInfo,\n\tHookState,\n\tResolvedCommandHook,\n} from './types.js';\nexport interface HooksResolutionOptions {\n\tload_hooks?: (cwd: string) => HookState;\n\trun_command_hook?: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\tpayload: Record<string, unknown>,\n\t\tpi_session_env?: Record<string, string>,\n\t) => Promise<CommandRunResult>;\n}\n\nexport function create_hooks_resolution_extension(\n\toptions: HooksResolutionOptions = {},\n): ExtensionFactory {\n\tconst load_hooks_impl = options.load_hooks ?? load_hooks;\n\tconst run_command_hook_impl =\n\t\toptions.run_command_hook ?? run_command_hook;\n\n\treturn async function hooks_resolution(pi: ExtensionAPI) {\n\t\tlet state: HookState = {\n\t\t\tproject_dir: process.cwd(),\n\t\t\thooks: [],\n\t\t};\n\n\t\tconst refresh_hooks = async (\n\t\t\tcwd: string,\n\t\t\tctx?: ExtensionContext,\n\t\t) => {\n\t\t\tif (!(await should_load_hooks_config(cwd, ctx))) {\n\t\t\t\tstate = { project_dir: cwd, hooks: [] };\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate = load_hooks_impl(cwd);\n\t\t};\n\n\t\tpi.on('session_start', async (_event, ctx) => {\n\t\t\tawait refresh_hooks(ctx.cwd, ctx);\n\t\t});\n\n\t\tpi.on(\n\t\t\t'tool_call',\n\t\t\tasync (\n\t\t\t\tevent,\n\t\t\t\tctx,\n\t\t\t): Promise<ToolCallEventResult | undefined> => {\n\t\t\t\tif (state.hooks.length === 0) return;\n\n\t\t\t\tconst matching_hooks = state.hooks.filter(\n\t\t\t\t\t(hook) =>\n\t\t\t\t\t\thook.event_name === 'PreToolUse' &&\n\t\t\t\t\t\tmatches_hook(hook, event.toolName),\n\t\t\t\t);\n\t\t\t\tif (matching_hooks.length === 0) return;\n\n\t\t\t\tconst payload = build_hook_payload(\n\t\t\t\t\tevent,\n\t\t\t\t\t'PreToolUse',\n\t\t\t\t\tctx,\n\t\t\t\t\tstate.project_dir,\n\t\t\t\t);\n\t\t\t\tconst executed_commands = new Set<string>();\n\n\t\t\t\tfor (const hook of matching_hooks) {\n\t\t\t\t\tif (executed_commands.has(hook.command)) continue;\n\t\t\t\t\texecuted_commands.add(hook.command);\n\n\t\t\t\t\tconst result = await run_command_hook_impl(\n\t\t\t\t\t\thook.command,\n\t\t\t\t\t\tstate.project_dir,\n\t\t\t\t\t\tpayload,\n\t\t\t\t\t\tcreate_hook_session_env(ctx),\n\t\t\t\t\t);\n\t\t\t\t\tconst reason = hook_block_reason(result);\n\t\t\t\t\tif (reason) return { block: true, reason };\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tpi.on('tool_result', async (event, ctx) => {\n\t\t\tif (state.hooks.length === 0) return;\n\n\t\t\tconst event_name = hook_event_name_for_result(event);\n\t\t\tconst matching_hooks = state.hooks.filter(\n\t\t\t\t(hook) =>\n\t\t\t\t\thook.event_name === event_name &&\n\t\t\t\t\tmatches_hook(hook, event.toolName),\n\t\t\t);\n\t\t\tif (matching_hooks.length === 0) return;\n\n\t\t\tconst payload = build_hook_payload(\n\t\t\t\tevent,\n\t\t\t\tevent_name,\n\t\t\t\tctx,\n\t\t\t\tstate.project_dir,\n\t\t\t);\n\t\t\tconst executed_commands = new Set<string>();\n\n\t\t\tfor (const hook of matching_hooks) {\n\t\t\t\tif (executed_commands.has(hook.command)) continue;\n\t\t\t\texecuted_commands.add(hook.command);\n\n\t\t\t\tconst result = await run_command_hook_impl(\n\t\t\t\t\thook.command,\n\t\t\t\t\tstate.project_dir,\n\t\t\t\t\tpayload,\n\t\t\t\t\tcreate_hook_session_env(ctx),\n\t\t\t\t);\n\t\t\t\tconst name = hook_name(hook.command);\n\t\t\t\tconst duration = format_duration(result.elapsed_ms);\n\n\t\t\t\tif (ctx.hasUI) {\n\t\t\t\t\tif (result.code === 0) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Hook \\`${name}\\` ran (${duration})`,\n\t\t\t\t\t\t\t'info',\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst error_line =\n\t\t\t\t\t\t\tresult.stderr.trim() ||\n\t\t\t\t\t\t\tresult.stdout.trim() ||\n\t\t\t\t\t\t\t`exit code ${result.code}`;\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Hook \\`${name}\\` failed (${duration}): ${error_line}`,\n\t\t\t\t\t\t\t'warning',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n}\n\nexport default create_hooks_resolution_extension();\n"],"mappings":"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"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { t as load_settings } from "./settings-BCn3P9qH.js";
|
|
2
|
-
//#region src/extensions/openrouter-fusion-config/index.ts
|
|
3
|
-
const DEFAULT_FUSION_ANALYSIS_MODELS = [
|
|
4
|
-
"~openai/gpt-latest",
|
|
5
|
-
"~google/gemini-pro-latest",
|
|
6
|
-
"deepseek/deepseek-v3.2",
|
|
7
|
-
"~moonshotai/kimi-latest"
|
|
8
|
-
];
|
|
9
|
-
const FUSION_MODEL_ID = "openrouter/fusion";
|
|
10
|
-
const FUSION_PLUGIN_ID = "fusion";
|
|
11
|
-
function split_models(value) {
|
|
12
|
-
return (value ?? "").split(",").map((model) => model.trim()).filter(Boolean);
|
|
13
|
-
}
|
|
14
|
-
function string_array(value) {
|
|
15
|
-
if (!Array.isArray(value)) return void 0;
|
|
16
|
-
const values = value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
|
|
17
|
-
return values.length > 0 ? values : void 0;
|
|
18
|
-
}
|
|
19
|
-
function env_flag(value) {
|
|
20
|
-
if (!value) return void 0;
|
|
21
|
-
const normalized = value.trim().toLowerCase();
|
|
22
|
-
if ([
|
|
23
|
-
"1",
|
|
24
|
-
"true",
|
|
25
|
-
"yes",
|
|
26
|
-
"on"
|
|
27
|
-
].includes(normalized)) return true;
|
|
28
|
-
if ([
|
|
29
|
-
"0",
|
|
30
|
-
"false",
|
|
31
|
-
"no",
|
|
32
|
-
"off"
|
|
33
|
-
].includes(normalized)) return false;
|
|
34
|
-
}
|
|
35
|
-
function get_fusion_request_config(options = {}) {
|
|
36
|
-
const env = options.env ?? process.env;
|
|
37
|
-
const settings = options.settings ?? load_settings().openRouterFusion ?? {};
|
|
38
|
-
const env_models = split_models(env.MY_PI_FUSION_ANALYSIS_MODELS);
|
|
39
|
-
return {
|
|
40
|
-
plugin: {
|
|
41
|
-
id: FUSION_PLUGIN_ID,
|
|
42
|
-
analysis_models: env_models.length > 0 ? env_models : string_array(settings.analysisModels) ?? [...DEFAULT_FUSION_ANALYSIS_MODELS],
|
|
43
|
-
model: env.MY_PI_FUSION_JUDGE_MODEL?.trim() || settings.judgeModel?.trim() || "~openai/gpt-latest"
|
|
44
|
-
},
|
|
45
|
-
force: env_flag(env.MY_PI_FUSION_FORCE) ?? settings.force ?? true
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function apply_fusion_config_plugin(payload, config) {
|
|
49
|
-
if (!payload || typeof payload !== "object") return void 0;
|
|
50
|
-
const request = payload;
|
|
51
|
-
if (request.model !== FUSION_MODEL_ID) return void 0;
|
|
52
|
-
const next_plugins = [...(Array.isArray(request.plugins) ? request.plugins : []).filter((plugin) => !plugin || typeof plugin !== "object" || plugin.id !== FUSION_PLUGIN_ID), config.plugin];
|
|
53
|
-
return {
|
|
54
|
-
...request,
|
|
55
|
-
plugins: next_plugins,
|
|
56
|
-
...config.force ? { tool_choice: "required" } : {}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
const extension = (pi) => {
|
|
60
|
-
pi.on("before_provider_request", (event) => apply_fusion_config_plugin(event.payload, get_fusion_request_config()));
|
|
61
|
-
};
|
|
62
|
-
//#endregion
|
|
63
|
-
export { extension as default };
|
|
64
|
-
|
|
65
|
-
//# sourceMappingURL=openrouter-fusion-config-BWsHBPQv.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"openrouter-fusion-config-BWsHBPQv.js","names":[],"sources":["../src/extensions/openrouter-fusion-config/index.ts"],"sourcesContent":["import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';\n\nimport { load_settings } from '../../settings/index.js';\n\nexport const DEFAULT_FUSION_ANALYSIS_MODELS = [\n\t'~openai/gpt-latest',\n\t'~google/gemini-pro-latest',\n\t'deepseek/deepseek-v3.2',\n\t'~moonshotai/kimi-latest',\n] as const;\n\nexport const DEFAULT_FUSION_JUDGE_MODEL = '~openai/gpt-latest';\n\nconst FUSION_MODEL_ID = 'openrouter/fusion';\nconst FUSION_PLUGIN_ID = 'fusion';\n\nexport interface FusionPluginConfig {\n\tid: 'fusion';\n\tanalysis_models: string[];\n\tmodel: string;\n}\n\nexport interface OpenRouterFusionSettings {\n\tanalysisModels?: string[];\n\tjudgeModel?: string;\n\tforce?: boolean;\n}\n\nexport interface FusionRequestConfig {\n\tplugin: FusionPluginConfig;\n\tforce: boolean;\n}\n\nfunction split_models(value: string | undefined): string[] {\n\treturn (value ?? '')\n\t\t.split(',')\n\t\t.map((model) => model.trim())\n\t\t.filter(Boolean);\n}\n\nfunction string_array(value: unknown): string[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\tconst values = value\n\t\t.map((item) => (typeof item === 'string' ? item.trim() : ''))\n\t\t.filter(Boolean);\n\treturn values.length > 0 ? values : undefined;\n}\n\nfunction env_flag(value: string | undefined): boolean | undefined {\n\tif (!value) return undefined;\n\tconst normalized = value.trim().toLowerCase();\n\tif (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n\tif (['0', 'false', 'no', 'off'].includes(normalized)) return false;\n\treturn undefined;\n}\n\nexport function get_fusion_request_config(\n\toptions: {\n\t\tsettings?: OpenRouterFusionSettings;\n\t\tenv?: NodeJS.ProcessEnv;\n\t} = {},\n): FusionRequestConfig {\n\tconst env = options.env ?? process.env;\n\tconst settings =\n\t\toptions.settings ?? load_settings().openRouterFusion ?? {};\n\tconst env_models = split_models(env.MY_PI_FUSION_ANALYSIS_MODELS);\n\n\treturn {\n\t\tplugin: {\n\t\t\tid: FUSION_PLUGIN_ID,\n\t\t\tanalysis_models:\n\t\t\t\tenv_models.length > 0\n\t\t\t\t\t? env_models\n\t\t\t\t\t: (string_array(settings.analysisModels) ?? [\n\t\t\t\t\t\t\t...DEFAULT_FUSION_ANALYSIS_MODELS,\n\t\t\t\t\t\t]),\n\t\t\tmodel:\n\t\t\t\tenv.MY_PI_FUSION_JUDGE_MODEL?.trim() ||\n\t\t\t\tsettings.judgeModel?.trim() ||\n\t\t\t\tDEFAULT_FUSION_JUDGE_MODEL,\n\t\t},\n\t\tforce: env_flag(env.MY_PI_FUSION_FORCE) ?? settings.force ?? true,\n\t};\n}\n\nexport function get_fusion_plugin_config(\n\toptions: {\n\t\tsettings?: OpenRouterFusionSettings;\n\t\tenv?: NodeJS.ProcessEnv;\n\t} = {},\n): FusionPluginConfig {\n\treturn get_fusion_request_config(options).plugin;\n}\n\nexport function apply_fusion_config_plugin(\n\tpayload: unknown,\n\tconfig: FusionRequestConfig,\n): unknown {\n\tif (!payload || typeof payload !== 'object') return undefined;\n\n\tconst request = payload as Record<string, unknown>;\n\tif (request.model !== FUSION_MODEL_ID) return undefined;\n\n\tconst plugins = Array.isArray(request.plugins)\n\t\t? request.plugins\n\t\t: [];\n\tconst next_plugins = [\n\t\t...plugins.filter(\n\t\t\t(plugin) =>\n\t\t\t\t!plugin ||\n\t\t\t\ttypeof plugin !== 'object' ||\n\t\t\t\t(plugin as Record<string, unknown>).id !== FUSION_PLUGIN_ID,\n\t\t),\n\t\tconfig.plugin,\n\t];\n\n\treturn {\n\t\t...request,\n\t\tplugins: next_plugins,\n\t\t...(config.force ? { tool_choice: 'required' } : {}),\n\t};\n}\n\nconst extension: ExtensionFactory = (pi) => {\n\tpi.on('before_provider_request', (event) =>\n\t\tapply_fusion_config_plugin(\n\t\t\tevent.payload,\n\t\t\tget_fusion_request_config(),\n\t\t),\n\t);\n};\n\nexport default extension;\n"],"mappings":";;AAIA,MAAa,iCAAiC;CAC7C;CACA;CACA;CACA;AACD;AAIA,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAmBzB,SAAS,aAAa,OAAqC;CAC1D,QAAQ,SAAS,GAAA,CACf,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO;AACjB;AAEA,SAAS,aAAa,OAAsC;CAC3D,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,KAAA;CAClC,MAAM,SAAS,MACb,KAAK,SAAU,OAAO,SAAS,WAAW,KAAK,KAAK,IAAI,EAAG,CAAC,CAC5D,OAAO,OAAO;CAChB,OAAO,OAAO,SAAS,IAAI,SAAS,KAAA;AACrC;AAEA,SAAS,SAAS,OAAgD;CACjE,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;CAC5C,IAAI;EAAC;EAAK;EAAQ;EAAO;CAAI,CAAC,CAAC,SAAS,UAAU,GAAG,OAAO;CAC5D,IAAI;EAAC;EAAK;EAAS;EAAM;CAAK,CAAC,CAAC,SAAS,UAAU,GAAG,OAAO;AAE9D;AAEA,SAAgB,0BACf,UAGI,CAAC,GACiB;CACtB,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,MAAM,WACL,QAAQ,YAAY,cAAc,CAAC,CAAC,oBAAoB,CAAC;CAC1D,MAAM,aAAa,aAAa,IAAI,4BAA4B;CAEhE,OAAO;EACN,QAAQ;GACP,IAAI;GACJ,iBACC,WAAW,SAAS,IACjB,aACC,aAAa,SAAS,cAAc,KAAK,CAC1C,GAAG,8BACJ;GACH,OACC,IAAI,0BAA0B,KAAK,KACnC,SAAS,YAAY,KAAK,KAAA;EAE5B;EACA,OAAO,SAAS,IAAI,kBAAkB,KAAK,SAAS,SAAS;CAC9D;AACD;AAWA,SAAgB,2BACf,SACA,QACU;CACV,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO,KAAA;CAEpD,MAAM,UAAU;CAChB,IAAI,QAAQ,UAAU,iBAAiB,OAAO,KAAA;CAK9C,MAAM,eAAe,CACpB,IAJe,MAAM,QAAQ,QAAQ,OAAO,IAC1C,QAAQ,UACR,CAAC,EAAA,CAEQ,QACT,WACA,CAAC,UACD,OAAO,WAAW,YACjB,OAAmC,OAAO,gBAC7C,GACA,OAAO,MACR;CAEA,OAAO;EACN,GAAG;EACH,SAAS;EACT,GAAI,OAAO,QAAQ,EAAE,aAAa,WAAW,IAAI,CAAC;CACnD;AACD;AAEA,MAAM,aAA+B,OAAO;CAC3C,GAAG,GAAG,4BAA4B,UACjC,2BACC,MAAM,SACN,0BAA0B,CAC3B,CACD;AACD"}
|