my-pi 0.1.0 → 0.1.1
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 +2 -0
- package/dist/{api-1ZXLxSgP.js → api-Dxi4curf.js} +420 -63
- package/dist/api-Dxi4curf.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/config.test.ts +6 -0
- package/src/extensions/config.ts +19 -1
- package/src/extensions/confirm-destructive.test.ts +157 -0
- package/src/extensions/confirm-destructive.ts +61 -0
- package/src/extensions/extensions.test.ts +114 -0
- package/src/extensions/extensions.ts +114 -108
- package/src/extensions/hooks-resolution.test.ts +246 -0
- package/src/extensions/hooks-resolution.ts +584 -0
- package/dist/api-1ZXLxSgP.js.map +0 -1
package/README.md
CHANGED
|
@@ -321,6 +321,8 @@ In interactive mode:
|
|
|
321
321
|
- `/extensions` — open the built-in extensions manager
|
|
322
322
|
- `/extensions list` — print built-in extensions with saved/effective
|
|
323
323
|
state
|
|
324
|
+
- `/extensions enable|disable|toggle` — without a key, open the
|
|
325
|
+
interactive toggle list
|
|
324
326
|
- `/extensions enable <key>` / `/extensions disable <key>` — toggle a
|
|
325
327
|
built-in extension
|
|
326
328
|
- `/skills` — open the interactive skills manager (unified list with
|
|
@@ -420,6 +420,20 @@ const BUILTIN_EXTENSIONS = [
|
|
|
420
420
|
"session",
|
|
421
421
|
"auto-name"
|
|
422
422
|
]
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
key: "confirm-destructive",
|
|
426
|
+
label: "Confirm destructive",
|
|
427
|
+
description: "Prompt before destructive session actions like clear, switch, and fork",
|
|
428
|
+
cli_flag: "--no-confirm-destructive",
|
|
429
|
+
aliases: ["confirm-destructive", "confirm"]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
key: "hooks-resolution",
|
|
433
|
+
label: "Hooks resolution",
|
|
434
|
+
description: "Claude Code style PostToolUse hook compatibility from .claude, .rulesync, and .pi configs",
|
|
435
|
+
cli_flag: "--no-hooks",
|
|
436
|
+
aliases: ["hooks-resolution", "hooks"]
|
|
423
437
|
}
|
|
424
438
|
];
|
|
425
439
|
function get_builtin_extensions_config_path() {
|
|
@@ -483,6 +497,33 @@ function find_builtin_extension(query) {
|
|
|
483
497
|
].some((value) => value.toLowerCase() === normalized));
|
|
484
498
|
}
|
|
485
499
|
//#endregion
|
|
500
|
+
//#region src/extensions/confirm-destructive.ts
|
|
501
|
+
async function confirm_destructive(pi) {
|
|
502
|
+
pi.on("session_before_switch", async (event, ctx) => {
|
|
503
|
+
if (!ctx.hasUI) return;
|
|
504
|
+
if (event.reason === "new") {
|
|
505
|
+
if (!await ctx.ui.confirm("Clear session?", "This will delete all messages in the current session.")) {
|
|
506
|
+
ctx.ui.notify("Clear cancelled", "info");
|
|
507
|
+
return { cancel: true };
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (ctx.sessionManager.getEntries().some((e) => e.type === "message" && e.message.role === "user")) {
|
|
512
|
+
if (!await ctx.ui.confirm("Switch session?", "You have messages in the current session. Switch anyway?")) {
|
|
513
|
+
ctx.ui.notify("Switch cancelled", "info");
|
|
514
|
+
return { cancel: true };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
pi.on("session_before_fork", async (event, ctx) => {
|
|
519
|
+
if (!ctx.hasUI) return;
|
|
520
|
+
if (await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, ["Yes, create fork", "No, stay in current session"]) !== "Yes, create fork") {
|
|
521
|
+
ctx.ui.notify("Fork cancelled", "info");
|
|
522
|
+
return { cancel: true };
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
486
527
|
//#region src/extensions/extensions.ts
|
|
487
528
|
const ENABLED$2 = "[x]";
|
|
488
529
|
const DISABLED$2 = "[ ]";
|
|
@@ -546,6 +587,66 @@ function save_extension_enabled(key, enabled) {
|
|
|
546
587
|
}
|
|
547
588
|
function create_extensions_extension(options = {}) {
|
|
548
589
|
const force_disabled = to_force_disabled_set(options.force_disabled);
|
|
590
|
+
async function show_manager(ctx) {
|
|
591
|
+
if (!ctx.hasUI) return false;
|
|
592
|
+
const states = resolve_builtin_extension_states(force_disabled);
|
|
593
|
+
const initial_enabled = new Set(states.filter((state) => state.saved_enabled).map((state) => state.key));
|
|
594
|
+
const current_enabled = new Set(initial_enabled);
|
|
595
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
596
|
+
const items = states.map(to_setting_item$1);
|
|
597
|
+
const container = new Container();
|
|
598
|
+
container.addChild({
|
|
599
|
+
render: () => {
|
|
600
|
+
const saved_enabled = current_enabled.size;
|
|
601
|
+
const saved_disabled = states.length - saved_enabled;
|
|
602
|
+
const enabled_now = [...current_enabled].filter((key) => !force_disabled.has(key)).length;
|
|
603
|
+
const disabled_now = states.length - enabled_now;
|
|
604
|
+
return [
|
|
605
|
+
theme.fg("accent", theme.bold("Built-in extensions")),
|
|
606
|
+
theme.fg("muted", `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`),
|
|
607
|
+
""
|
|
608
|
+
];
|
|
609
|
+
},
|
|
610
|
+
invalidate: () => {}
|
|
611
|
+
});
|
|
612
|
+
const settings_list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 16), {
|
|
613
|
+
cursor: theme.fg("accent", "›"),
|
|
614
|
+
label: (text, selected) => selected ? theme.fg("accent", text) : text,
|
|
615
|
+
value: (text, selected) => {
|
|
616
|
+
const color = text === ENABLED$2 ? "success" : "dim";
|
|
617
|
+
const rendered = theme.fg(color, text);
|
|
618
|
+
return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
|
|
619
|
+
},
|
|
620
|
+
description: (text) => theme.fg("muted", text),
|
|
621
|
+
hint: (text) => theme.fg("dim", text)
|
|
622
|
+
}, (id, new_value) => {
|
|
623
|
+
const key = id;
|
|
624
|
+
const enabled = new_value === ENABLED$2;
|
|
625
|
+
if (enabled) current_enabled.add(key);
|
|
626
|
+
else current_enabled.delete(key);
|
|
627
|
+
save_extension_enabled(key, enabled);
|
|
628
|
+
}, () => done(void 0), { enableSearch: true });
|
|
629
|
+
container.addChild(settings_list);
|
|
630
|
+
container.addChild(new Text(theme.fg("dim", "esc close • search filters • changes save immediately • CLI --no-* flags still win in this process"), 0, 1));
|
|
631
|
+
return {
|
|
632
|
+
render(width) {
|
|
633
|
+
return container.render(width);
|
|
634
|
+
},
|
|
635
|
+
invalidate() {
|
|
636
|
+
container.invalidate();
|
|
637
|
+
},
|
|
638
|
+
handleInput(data) {
|
|
639
|
+
settings_list.handleInput(data);
|
|
640
|
+
tui.requestRender();
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
if (!sets_equal$2(initial_enabled, current_enabled)) {
|
|
645
|
+
ctx.ui.notify(force_disabled.size > 0 ? "Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process." : "Reloading to apply updated built-in extensions...", "info");
|
|
646
|
+
await ctx.reload();
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
549
650
|
return async function extensions(pi) {
|
|
550
651
|
const subs = [
|
|
551
652
|
"list",
|
|
@@ -577,65 +678,8 @@ function create_extensions_extension(options = {}) {
|
|
|
577
678
|
},
|
|
578
679
|
handler: async (args, ctx) => {
|
|
579
680
|
const trimmed = args.trim();
|
|
580
|
-
if (!trimmed
|
|
581
|
-
|
|
582
|
-
const initial_enabled = new Set(states.filter((state) => state.saved_enabled).map((state) => state.key));
|
|
583
|
-
const current_enabled = new Set(initial_enabled);
|
|
584
|
-
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
585
|
-
const items = states.map(to_setting_item$1);
|
|
586
|
-
const container = new Container();
|
|
587
|
-
container.addChild({
|
|
588
|
-
render: () => {
|
|
589
|
-
const saved_enabled = current_enabled.size;
|
|
590
|
-
const saved_disabled = states.length - saved_enabled;
|
|
591
|
-
const enabled_now = [...current_enabled].filter((key) => !force_disabled.has(key)).length;
|
|
592
|
-
const disabled_now = states.length - enabled_now;
|
|
593
|
-
return [
|
|
594
|
-
theme.fg("accent", theme.bold("Built-in extensions")),
|
|
595
|
-
theme.fg("muted", `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`),
|
|
596
|
-
""
|
|
597
|
-
];
|
|
598
|
-
},
|
|
599
|
-
invalidate: () => {}
|
|
600
|
-
});
|
|
601
|
-
const settings_list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 16), {
|
|
602
|
-
cursor: theme.fg("accent", "›"),
|
|
603
|
-
label: (text, selected) => selected ? theme.fg("accent", text) : text,
|
|
604
|
-
value: (text, selected) => {
|
|
605
|
-
const color = text === ENABLED$2 ? "success" : "dim";
|
|
606
|
-
const rendered = theme.fg(color, text);
|
|
607
|
-
return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
|
|
608
|
-
},
|
|
609
|
-
description: (text) => theme.fg("muted", text),
|
|
610
|
-
hint: (text) => theme.fg("dim", text)
|
|
611
|
-
}, (id, new_value) => {
|
|
612
|
-
const key = id;
|
|
613
|
-
const enabled = new_value === ENABLED$2;
|
|
614
|
-
if (enabled) current_enabled.add(key);
|
|
615
|
-
else current_enabled.delete(key);
|
|
616
|
-
save_extension_enabled(key, enabled);
|
|
617
|
-
}, () => done(void 0), { enableSearch: true });
|
|
618
|
-
container.addChild(settings_list);
|
|
619
|
-
container.addChild(new Text(theme.fg("dim", "esc close • search filters • changes save immediately • CLI --no-* flags still win in this process"), 0, 1));
|
|
620
|
-
return {
|
|
621
|
-
render(width) {
|
|
622
|
-
return container.render(width);
|
|
623
|
-
},
|
|
624
|
-
invalidate() {
|
|
625
|
-
container.invalidate();
|
|
626
|
-
},
|
|
627
|
-
handleInput(data) {
|
|
628
|
-
settings_list.handleInput(data);
|
|
629
|
-
tui.requestRender();
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
});
|
|
633
|
-
if (!sets_equal$2(initial_enabled, current_enabled)) {
|
|
634
|
-
ctx.ui.notify(force_disabled.size > 0 ? "Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process." : "Reloading to apply updated built-in extensions...", "info");
|
|
635
|
-
await ctx.reload();
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
return;
|
|
681
|
+
if (!trimmed) {
|
|
682
|
+
if (await show_manager(ctx)) return;
|
|
639
683
|
}
|
|
640
684
|
const [sub, ...rest] = (trimmed || "list").split(/\s+/);
|
|
641
685
|
const arg = rest.join(" ");
|
|
@@ -648,6 +692,7 @@ function create_extensions_extension(options = {}) {
|
|
|
648
692
|
case "disable":
|
|
649
693
|
case "toggle": {
|
|
650
694
|
if (!arg) {
|
|
695
|
+
if (await show_manager(ctx)) return;
|
|
651
696
|
ctx.ui.notify(`Usage: /extensions ${sub} <key>`, "warning");
|
|
652
697
|
return;
|
|
653
698
|
}
|
|
@@ -896,6 +941,312 @@ async function handoff(pi) {
|
|
|
896
941
|
});
|
|
897
942
|
}
|
|
898
943
|
//#endregion
|
|
944
|
+
//#region src/extensions/hooks-resolution.ts
|
|
945
|
+
const HOOK_TIMEOUT_MS = 600 * 1e3;
|
|
946
|
+
function is_file(path) {
|
|
947
|
+
try {
|
|
948
|
+
return statSync(path).isFile();
|
|
949
|
+
} catch {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function as_record(value) {
|
|
954
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
957
|
+
function walk_up_directories(start_dir, stop_dir) {
|
|
958
|
+
const directories = [];
|
|
959
|
+
const has_stop_dir = stop_dir !== void 0;
|
|
960
|
+
let current = resolve(start_dir);
|
|
961
|
+
let parent = dirname(current);
|
|
962
|
+
let reached_stop_dir = has_stop_dir && current === stop_dir;
|
|
963
|
+
let reached_filesystem_root = parent === current;
|
|
964
|
+
directories.push(current);
|
|
965
|
+
while (!reached_stop_dir && !reached_filesystem_root) {
|
|
966
|
+
current = parent;
|
|
967
|
+
parent = dirname(current);
|
|
968
|
+
reached_stop_dir = has_stop_dir && current === stop_dir;
|
|
969
|
+
reached_filesystem_root = parent === current;
|
|
970
|
+
directories.push(current);
|
|
971
|
+
}
|
|
972
|
+
return directories;
|
|
973
|
+
}
|
|
974
|
+
function find_nearest_git_root(start_dir) {
|
|
975
|
+
for (const directory of walk_up_directories(start_dir)) if (existsSync(join(directory, ".git"))) return directory;
|
|
976
|
+
}
|
|
977
|
+
function has_hooks_config(directory) {
|
|
978
|
+
return is_file(join(directory, ".claude", "settings.json")) || is_file(join(directory, ".rulesync", "hooks.json")) || is_file(join(directory, ".pi", "hooks.json"));
|
|
979
|
+
}
|
|
980
|
+
function find_project_dir(cwd) {
|
|
981
|
+
const git_root = find_nearest_git_root(cwd);
|
|
982
|
+
for (const directory of walk_up_directories(cwd, git_root)) if (has_hooks_config(directory)) return directory;
|
|
983
|
+
return git_root ?? resolve(cwd);
|
|
984
|
+
}
|
|
985
|
+
function read_json_file(path) {
|
|
986
|
+
if (!is_file(path)) return void 0;
|
|
987
|
+
try {
|
|
988
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
989
|
+
} catch {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function resolve_hook_command(command, project_dir) {
|
|
994
|
+
return command.replace(/\$CLAUDE_PROJECT_DIR\b/g, project_dir);
|
|
995
|
+
}
|
|
996
|
+
function compile_matcher(matcher_text) {
|
|
997
|
+
if (matcher_text === void 0) return void 0;
|
|
998
|
+
try {
|
|
999
|
+
return new RegExp(matcher_text);
|
|
1000
|
+
} catch {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function create_hook(event_name, matcher_text, command, source, project_dir) {
|
|
1005
|
+
const matcher = compile_matcher(matcher_text);
|
|
1006
|
+
if (matcher_text !== void 0 && matcher === void 0) return void 0;
|
|
1007
|
+
return {
|
|
1008
|
+
event_name,
|
|
1009
|
+
matcher,
|
|
1010
|
+
matcher_text,
|
|
1011
|
+
command: resolve_hook_command(command, project_dir),
|
|
1012
|
+
source
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function get_hook_entries(hooks_record, event_name) {
|
|
1016
|
+
const keys = event_name === "PostToolUse" ? ["PostToolUse", "postToolUse"] : ["PostToolUseFailure", "postToolUseFailure"];
|
|
1017
|
+
for (const key of keys) {
|
|
1018
|
+
const value = hooks_record[key];
|
|
1019
|
+
if (Array.isArray(value)) return value;
|
|
1020
|
+
}
|
|
1021
|
+
return [];
|
|
1022
|
+
}
|
|
1023
|
+
function parse_claude_settings_hooks(config, source, project_dir) {
|
|
1024
|
+
const root = as_record(config);
|
|
1025
|
+
const hooks_root = root ? as_record(root.hooks) : void 0;
|
|
1026
|
+
if (!hooks_root) return [];
|
|
1027
|
+
const hooks = [];
|
|
1028
|
+
for (const event_name of ["PostToolUse", "PostToolUseFailure"]) {
|
|
1029
|
+
const entries = get_hook_entries(hooks_root, event_name);
|
|
1030
|
+
for (const entry of entries) {
|
|
1031
|
+
const entry_record = as_record(entry);
|
|
1032
|
+
if (!entry_record || !Array.isArray(entry_record.hooks)) continue;
|
|
1033
|
+
const matcher_text = typeof entry_record.matcher === "string" ? entry_record.matcher : void 0;
|
|
1034
|
+
for (const nested_hook of entry_record.hooks) {
|
|
1035
|
+
const nested_record = as_record(nested_hook);
|
|
1036
|
+
if (!nested_record) continue;
|
|
1037
|
+
if (nested_record.type !== "command") continue;
|
|
1038
|
+
if (typeof nested_record.command !== "string") continue;
|
|
1039
|
+
const hook = create_hook(event_name, matcher_text, nested_record.command, source, project_dir);
|
|
1040
|
+
if (hook) hooks.push(hook);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return hooks;
|
|
1045
|
+
}
|
|
1046
|
+
function parse_simple_hooks_file(config, source, project_dir) {
|
|
1047
|
+
const root = as_record(config);
|
|
1048
|
+
const hooks_root = root ? as_record(root.hooks) : void 0;
|
|
1049
|
+
if (!hooks_root) return [];
|
|
1050
|
+
const hooks = [];
|
|
1051
|
+
for (const event_name of ["PostToolUse", "PostToolUseFailure"]) {
|
|
1052
|
+
const entries = get_hook_entries(hooks_root, event_name);
|
|
1053
|
+
for (const entry of entries) {
|
|
1054
|
+
const entry_record = as_record(entry);
|
|
1055
|
+
if (!entry_record || typeof entry_record.command !== "string") continue;
|
|
1056
|
+
const hook = create_hook(event_name, typeof entry_record.matcher === "string" ? entry_record.matcher : void 0, entry_record.command, source, project_dir);
|
|
1057
|
+
if (hook) hooks.push(hook);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return hooks;
|
|
1061
|
+
}
|
|
1062
|
+
function load_hooks(cwd) {
|
|
1063
|
+
const project_dir = find_project_dir(cwd);
|
|
1064
|
+
const hooks = [];
|
|
1065
|
+
const claude_settings_path = join(project_dir, ".claude", "settings.json");
|
|
1066
|
+
const rulesync_hooks_path = join(project_dir, ".rulesync", "hooks.json");
|
|
1067
|
+
const pi_hooks_path = join(project_dir, ".pi", "hooks.json");
|
|
1068
|
+
const claude_settings = read_json_file(claude_settings_path);
|
|
1069
|
+
if (claude_settings !== void 0) hooks.push(...parse_claude_settings_hooks(claude_settings, claude_settings_path, project_dir));
|
|
1070
|
+
const rulesync_hooks = read_json_file(rulesync_hooks_path);
|
|
1071
|
+
if (rulesync_hooks !== void 0) hooks.push(...parse_simple_hooks_file(rulesync_hooks, rulesync_hooks_path, project_dir));
|
|
1072
|
+
const pi_hooks = read_json_file(pi_hooks_path);
|
|
1073
|
+
if (pi_hooks !== void 0) hooks.push(...parse_simple_hooks_file(pi_hooks, pi_hooks_path, project_dir));
|
|
1074
|
+
return {
|
|
1075
|
+
project_dir,
|
|
1076
|
+
hooks
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function to_claude_tool_name(tool_name) {
|
|
1080
|
+
if (tool_name === "ls") return "LS";
|
|
1081
|
+
if (tool_name.length === 0) return tool_name;
|
|
1082
|
+
return tool_name[0].toUpperCase() + tool_name.slice(1);
|
|
1083
|
+
}
|
|
1084
|
+
function matches_hook(hook, tool_name) {
|
|
1085
|
+
if (!hook.matcher) return true;
|
|
1086
|
+
const claude_tool_name = to_claude_tool_name(tool_name);
|
|
1087
|
+
hook.matcher.lastIndex = 0;
|
|
1088
|
+
if (hook.matcher.test(tool_name)) return true;
|
|
1089
|
+
hook.matcher.lastIndex = 0;
|
|
1090
|
+
return hook.matcher.test(claude_tool_name);
|
|
1091
|
+
}
|
|
1092
|
+
function extract_text_content(content) {
|
|
1093
|
+
if (!Array.isArray(content)) return "";
|
|
1094
|
+
const parts = [];
|
|
1095
|
+
for (const item of content) {
|
|
1096
|
+
if (!item || typeof item !== "object") continue;
|
|
1097
|
+
const item_record = item;
|
|
1098
|
+
if (item_record.type === "text" && typeof item_record.text === "string") parts.push(item_record.text);
|
|
1099
|
+
}
|
|
1100
|
+
return parts.join("\n");
|
|
1101
|
+
}
|
|
1102
|
+
function normalize_tool_input(input) {
|
|
1103
|
+
const normalized = { ...input };
|
|
1104
|
+
const path_value = typeof input.path === "string" ? input.path : void 0;
|
|
1105
|
+
if (path_value !== void 0) {
|
|
1106
|
+
normalized.file_path = path_value;
|
|
1107
|
+
normalized.filePath = path_value;
|
|
1108
|
+
}
|
|
1109
|
+
return normalized;
|
|
1110
|
+
}
|
|
1111
|
+
function build_tool_response(event, normalized_input) {
|
|
1112
|
+
const response = {
|
|
1113
|
+
is_error: event.isError,
|
|
1114
|
+
isError: event.isError,
|
|
1115
|
+
content: event.content,
|
|
1116
|
+
text: extract_text_content(event.content),
|
|
1117
|
+
details: event.details ?? null
|
|
1118
|
+
};
|
|
1119
|
+
const file_path = typeof normalized_input.file_path === "string" ? normalized_input.file_path : void 0;
|
|
1120
|
+
if (file_path !== void 0) {
|
|
1121
|
+
response.file_path = file_path;
|
|
1122
|
+
response.filePath = file_path;
|
|
1123
|
+
}
|
|
1124
|
+
return response;
|
|
1125
|
+
}
|
|
1126
|
+
function build_hook_payload(event, event_name, ctx, project_dir) {
|
|
1127
|
+
const normalized_input = normalize_tool_input(event.input);
|
|
1128
|
+
return {
|
|
1129
|
+
session_id: ctx.sessionManager.getSessionFile() ?? "ephemeral",
|
|
1130
|
+
cwd: ctx.cwd,
|
|
1131
|
+
claude_project_dir: project_dir,
|
|
1132
|
+
hook_event_name: event_name,
|
|
1133
|
+
tool_name: to_claude_tool_name(event.toolName),
|
|
1134
|
+
tool_call_id: event.toolCallId,
|
|
1135
|
+
tool_input: normalized_input,
|
|
1136
|
+
tool_response: build_tool_response(event, normalized_input)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
async function run_command_hook(command, cwd, payload) {
|
|
1140
|
+
return await new Promise((resolve) => {
|
|
1141
|
+
const started_at = Date.now();
|
|
1142
|
+
const child = spawn("bash", ["-lc", command], {
|
|
1143
|
+
cwd,
|
|
1144
|
+
env: {
|
|
1145
|
+
...process.env,
|
|
1146
|
+
CLAUDE_PROJECT_DIR: cwd
|
|
1147
|
+
},
|
|
1148
|
+
stdio: [
|
|
1149
|
+
"pipe",
|
|
1150
|
+
"pipe",
|
|
1151
|
+
"pipe"
|
|
1152
|
+
]
|
|
1153
|
+
});
|
|
1154
|
+
let stdout = "";
|
|
1155
|
+
let stderr = "";
|
|
1156
|
+
let timed_out = false;
|
|
1157
|
+
let resolved = false;
|
|
1158
|
+
const finish = (code) => {
|
|
1159
|
+
if (resolved) return;
|
|
1160
|
+
resolved = true;
|
|
1161
|
+
resolve({
|
|
1162
|
+
code,
|
|
1163
|
+
stdout,
|
|
1164
|
+
stderr,
|
|
1165
|
+
elapsed_ms: Date.now() - started_at,
|
|
1166
|
+
timed_out
|
|
1167
|
+
});
|
|
1168
|
+
};
|
|
1169
|
+
const timeout = setTimeout(() => {
|
|
1170
|
+
timed_out = true;
|
|
1171
|
+
child.kill("SIGTERM");
|
|
1172
|
+
setTimeout(() => {
|
|
1173
|
+
child.kill("SIGKILL");
|
|
1174
|
+
}, 1e3).unref?.();
|
|
1175
|
+
}, HOOK_TIMEOUT_MS);
|
|
1176
|
+
timeout.unref?.();
|
|
1177
|
+
child.stdout.on("data", (chunk) => {
|
|
1178
|
+
stdout += chunk.toString("utf8");
|
|
1179
|
+
});
|
|
1180
|
+
child.stderr.on("data", (chunk) => {
|
|
1181
|
+
stderr += chunk.toString("utf8");
|
|
1182
|
+
});
|
|
1183
|
+
child.on("error", (error) => {
|
|
1184
|
+
clearTimeout(timeout);
|
|
1185
|
+
stderr += `${error.message}\n`;
|
|
1186
|
+
finish(-1);
|
|
1187
|
+
});
|
|
1188
|
+
child.on("close", (code) => {
|
|
1189
|
+
clearTimeout(timeout);
|
|
1190
|
+
finish(code ?? -1);
|
|
1191
|
+
});
|
|
1192
|
+
try {
|
|
1193
|
+
child.stdin.write(JSON.stringify(payload));
|
|
1194
|
+
child.stdin.end();
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
stderr += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
function hook_event_name_for_result(event) {
|
|
1201
|
+
return event.isError ? "PostToolUseFailure" : "PostToolUse";
|
|
1202
|
+
}
|
|
1203
|
+
function format_duration$1(elapsed_ms) {
|
|
1204
|
+
if (elapsed_ms < 1e3) return `${elapsed_ms}ms`;
|
|
1205
|
+
return `${(elapsed_ms / 1e3).toFixed(1)}s`;
|
|
1206
|
+
}
|
|
1207
|
+
function hook_name(command) {
|
|
1208
|
+
const sh_path_match = command.match(/[^\s|;&]+\.sh\b/);
|
|
1209
|
+
if (sh_path_match) return basename(sh_path_match[0]);
|
|
1210
|
+
return basename(command.trim().split(/\s+/)[0] ?? "hook");
|
|
1211
|
+
}
|
|
1212
|
+
function create_hooks_resolution_extension(options = {}) {
|
|
1213
|
+
const load_hooks_impl = options.load_hooks ?? load_hooks;
|
|
1214
|
+
const run_command_hook_impl = options.run_command_hook ?? run_command_hook;
|
|
1215
|
+
return async function hooks_resolution(pi) {
|
|
1216
|
+
let state = {
|
|
1217
|
+
project_dir: process.cwd(),
|
|
1218
|
+
hooks: []
|
|
1219
|
+
};
|
|
1220
|
+
const refresh_hooks = (cwd) => {
|
|
1221
|
+
state = load_hooks_impl(cwd);
|
|
1222
|
+
};
|
|
1223
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1224
|
+
refresh_hooks(ctx.cwd);
|
|
1225
|
+
});
|
|
1226
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
1227
|
+
if (state.hooks.length === 0) return;
|
|
1228
|
+
const event_name = hook_event_name_for_result(event);
|
|
1229
|
+
const matching_hooks = state.hooks.filter((hook) => hook.event_name === event_name && matches_hook(hook, event.toolName));
|
|
1230
|
+
if (matching_hooks.length === 0) return;
|
|
1231
|
+
const payload = build_hook_payload(event, event_name, ctx, state.project_dir);
|
|
1232
|
+
const executed_commands = /* @__PURE__ */ new Set();
|
|
1233
|
+
for (const hook of matching_hooks) {
|
|
1234
|
+
if (executed_commands.has(hook.command)) continue;
|
|
1235
|
+
executed_commands.add(hook.command);
|
|
1236
|
+
const result = await run_command_hook_impl(hook.command, state.project_dir, payload);
|
|
1237
|
+
const name = hook_name(hook.command);
|
|
1238
|
+
const duration = format_duration$1(result.elapsed_ms);
|
|
1239
|
+
if (ctx.hasUI) if (result.code === 0) ctx.ui.notify(`Hook \`${name}\` ran (${duration})`, "info");
|
|
1240
|
+
else {
|
|
1241
|
+
const error_line = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
1242
|
+
ctx.ui.notify(`Hook \`${name}\` failed (${duration}): ${error_line}`, "warning");
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
var hooks_resolution_default = create_hooks_resolution_extension();
|
|
1249
|
+
//#endregion
|
|
899
1250
|
//#region src/lsp/client.ts
|
|
900
1251
|
var LspClientStartError = class extends Error {
|
|
901
1252
|
command;
|
|
@@ -4658,7 +5009,9 @@ const BUILTIN_EXTENSION_FACTORIES = {
|
|
|
4658
5009
|
recall,
|
|
4659
5010
|
"prompt-presets": prompt_presets,
|
|
4660
5011
|
lsp: lsp_default,
|
|
4661
|
-
"session-name": session_name
|
|
5012
|
+
"session-name": session_name,
|
|
5013
|
+
"confirm-destructive": confirm_destructive,
|
|
5014
|
+
"hooks-resolution": hooks_resolution_default
|
|
4662
5015
|
};
|
|
4663
5016
|
const PACKAGE_THEME_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "themes");
|
|
4664
5017
|
const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
@@ -4676,6 +5029,8 @@ function get_force_disabled_builtins(options) {
|
|
|
4676
5029
|
if (!options.prompt_presets) force_disabled.add("prompt-presets");
|
|
4677
5030
|
if (!options.lsp) force_disabled.add("lsp");
|
|
4678
5031
|
if (!options.session_name) force_disabled.add("session-name");
|
|
5032
|
+
if (!options.confirm_destructive) force_disabled.add("confirm-destructive");
|
|
5033
|
+
if (!options.hooks_resolution) force_disabled.add("hooks-resolution");
|
|
4679
5034
|
return force_disabled;
|
|
4680
5035
|
}
|
|
4681
5036
|
function create_builtin_extension_factory(key, extension, force_disabled) {
|
|
@@ -4697,7 +5052,7 @@ function create_extensions_override(managed_inline_paths) {
|
|
|
4697
5052
|
};
|
|
4698
5053
|
}
|
|
4699
5054
|
async function create_my_pi(options = {}) {
|
|
4700
|
-
const { cwd = process.cwd(), agent_dir, extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, lsp = true, session_name = true, telemetry, telemetry_db_path, model, system_prompt, append_system_prompt } = options;
|
|
5055
|
+
const { cwd = process.cwd(), agent_dir, extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, lsp = true, session_name = true, confirm_destructive = true, hooks_resolution = true, telemetry, telemetry_db_path, model, system_prompt, append_system_prompt } = options;
|
|
4701
5056
|
const effective_agent_dir = resolve_agent_dir(cwd, agent_dir);
|
|
4702
5057
|
if (agent_dir) process.env[PI_AGENT_DIR_ENV] = effective_agent_dir;
|
|
4703
5058
|
const resolved_extensions = extensions.map((p) => resolve(cwd, p));
|
|
@@ -4710,7 +5065,9 @@ async function create_my_pi(options = {}) {
|
|
|
4710
5065
|
recall,
|
|
4711
5066
|
prompt_presets,
|
|
4712
5067
|
lsp,
|
|
4713
|
-
session_name
|
|
5068
|
+
session_name,
|
|
5069
|
+
confirm_destructive,
|
|
5070
|
+
hooks_resolution
|
|
4714
5071
|
});
|
|
4715
5072
|
const managed_extension_factories = [
|
|
4716
5073
|
create_telemetry_extension({
|
|
@@ -4768,4 +5125,4 @@ async function create_my_pi(options = {}) {
|
|
|
4768
5125
|
//#endregion
|
|
4769
5126
|
export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
|
|
4770
5127
|
|
|
4771
|
-
//# sourceMappingURL=api-
|
|
5128
|
+
//# sourceMappingURL=api-Dxi4curf.js.map
|