my-pi 0.0.13 → 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.
@@ -1,4 +1,4 @@
1
- import { InteractiveMode as InteractiveMode$1, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, defineTool, getAgentDir, parseFrontmatter, runPrintMode as runPrintMode$1 } from "@mariozechner/pi-coding-agent";
1
+ import { BorderedLoader, InteractiveMode as InteractiveMode$1, SessionManager, SettingsManager, convertToLlm, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, defineTool, getAgentDir, parseFrontmatter, runPrintMode as runPrintMode$1, serializeConversation } from "@mariozechner/pi-coding-agent";
2
2
  import { cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -6,6 +6,7 @@ import { Type } from "@sinclair/typebox";
6
6
  import { spawn } from "node:child_process";
7
7
  import { homedir } from "node:os";
8
8
  import { Container, SettingsList, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
+ import { complete } from "@mariozechner/pi-ai";
9
10
  import { readFile } from "node:fs/promises";
10
11
  import { EventEmitter } from "node:events";
11
12
  import { createHash, randomUUID } from "node:crypto";
@@ -380,7 +381,7 @@ const BUILTIN_EXTENSIONS = [
380
381
  {
381
382
  key: "handoff",
382
383
  label: "Handoff",
383
- description: "Session handoff export and /handoff command",
384
+ description: "AI-generated session handoff with editor review and new-session prefill",
384
385
  cli_flag: "--no-handoff",
385
386
  aliases: ["handoff"]
386
387
  },
@@ -408,6 +409,31 @@ const BUILTIN_EXTENSIONS = [
408
409
  description: "Language Server Protocol tools (diagnostics, hover, definition, references)",
409
410
  cli_flag: "--no-lsp",
410
411
  aliases: ["lsp", "language-server"]
412
+ },
413
+ {
414
+ key: "session-name",
415
+ label: "Session name",
416
+ description: "AI-powered session auto-naming and /session-name command",
417
+ cli_flag: "--no-session-name",
418
+ aliases: [
419
+ "session-name",
420
+ "session",
421
+ "auto-name"
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"]
411
437
  }
412
438
  ];
413
439
  function get_builtin_extensions_config_path() {
@@ -471,6 +497,33 @@ function find_builtin_extension(query) {
471
497
  ].some((value) => value.toLowerCase() === normalized));
472
498
  }
473
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
474
527
  //#region src/extensions/extensions.ts
475
528
  const ENABLED$2 = "[x]";
476
529
  const DISABLED$2 = "[ ]";
@@ -534,6 +587,66 @@ function save_extension_enabled(key, enabled) {
534
587
  }
535
588
  function create_extensions_extension(options = {}) {
536
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
+ }
537
650
  return async function extensions(pi) {
538
651
  const subs = [
539
652
  "list",
@@ -565,65 +678,8 @@ function create_extensions_extension(options = {}) {
565
678
  },
566
679
  handler: async (args, ctx) => {
567
680
  const trimmed = args.trim();
568
- if (!trimmed && ctx.hasUI) {
569
- const states = resolve_builtin_extension_states(force_disabled);
570
- const initial_enabled = new Set(states.filter((state) => state.saved_enabled).map((state) => state.key));
571
- const current_enabled = new Set(initial_enabled);
572
- await ctx.ui.custom((tui, theme, _kb, done) => {
573
- const items = states.map(to_setting_item$1);
574
- const container = new Container();
575
- container.addChild({
576
- render: () => {
577
- const saved_enabled = current_enabled.size;
578
- const saved_disabled = states.length - saved_enabled;
579
- const enabled_now = [...current_enabled].filter((key) => !force_disabled.has(key)).length;
580
- const disabled_now = states.length - enabled_now;
581
- return [
582
- theme.fg("accent", theme.bold("Built-in extensions")),
583
- theme.fg("muted", `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`),
584
- ""
585
- ];
586
- },
587
- invalidate: () => {}
588
- });
589
- const settings_list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 16), {
590
- cursor: theme.fg("accent", "›"),
591
- label: (text, selected) => selected ? theme.fg("accent", text) : text,
592
- value: (text, selected) => {
593
- const color = text === ENABLED$2 ? "success" : "dim";
594
- const rendered = theme.fg(color, text);
595
- return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
596
- },
597
- description: (text) => theme.fg("muted", text),
598
- hint: (text) => theme.fg("dim", text)
599
- }, (id, new_value) => {
600
- const key = id;
601
- const enabled = new_value === ENABLED$2;
602
- if (enabled) current_enabled.add(key);
603
- else current_enabled.delete(key);
604
- save_extension_enabled(key, enabled);
605
- }, () => done(void 0), { enableSearch: true });
606
- container.addChild(settings_list);
607
- container.addChild(new Text(theme.fg("dim", "esc close • search filters • changes save immediately • CLI --no-* flags still win in this process"), 0, 1));
608
- return {
609
- render(width) {
610
- return container.render(width);
611
- },
612
- invalidate() {
613
- container.invalidate();
614
- },
615
- handleInput(data) {
616
- settings_list.handleInput(data);
617
- tui.requestRender();
618
- }
619
- };
620
- });
621
- if (!sets_equal$2(initial_enabled, current_enabled)) {
622
- 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");
623
- await ctx.reload();
624
- return;
625
- }
626
- return;
681
+ if (!trimmed) {
682
+ if (await show_manager(ctx)) return;
627
683
  }
628
684
  const [sub, ...rest] = (trimmed || "list").split(/\s+/);
629
685
  const arg = rest.join(" ");
@@ -636,6 +692,7 @@ function create_extensions_extension(options = {}) {
636
692
  case "disable":
637
693
  case "toggle": {
638
694
  if (!arg) {
695
+ if (await show_manager(ctx)) return;
639
696
  ctx.ui.notify(`Usage: /extensions ${sub} <key>`, "warning");
640
697
  return;
641
698
  }
@@ -789,52 +846,407 @@ async function filter_output(pi) {
789
846
  }
790
847
  //#endregion
791
848
  //#region src/extensions/handoff.ts
849
+ const SYSTEM_PROMPT$1 = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
850
+
851
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
852
+ 2. Lists any relevant files that were discussed or modified
853
+ 3. Clearly states the next task based on the user's goal
854
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
855
+
856
+ Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
857
+
858
+ Example output format:
859
+ ## Context
860
+ We've been working on X. Key decisions:
861
+ - Decision 1
862
+ - Decision 2
863
+
864
+ Files involved:
865
+ - path/to/file1.ts
866
+ - path/to/file2.ts
867
+
868
+ ## Task
869
+ [Clear description of what to do next based on user's goal]`;
792
870
  async function handoff(pi) {
793
- const history = [];
794
- pi.on("message_end", async (event) => {
795
- const msg = event.message;
796
- if (!msg) return;
797
- const content = msg.content;
798
- if (!Array.isArray(content)) return;
799
- const text = content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
800
- if (!text) return;
801
- const summary = text.length > 200 ? text.slice(0, 200) + "..." : text;
802
- history.push({
803
- role: msg.role || "unknown",
804
- summary,
805
- timestamp: Date.now()
806
- });
807
- });
808
871
  pi.registerCommand("handoff", {
809
- description: "Export session context as a handoff prompt for a new session",
872
+ description: "Transfer context to a new focused session with an AI-generated prompt",
810
873
  handler: async (args, ctx) => {
811
- const task = args.trim();
812
- if (history.length === 0) {
813
- ctx.ui.notify("No conversation history to hand off", "warning");
874
+ if (!ctx.hasUI) {
875
+ ctx.ui.notify("handoff requires interactive mode", "error");
814
876
  return;
815
877
  }
816
- const handoff = `## Handoff from Previous Session
817
-
818
- ### Context
819
- The previous session covered the following:
820
-
821
- ${history.map((h) => `[${h.role}] ${h.summary}`).join("\n\n")}
822
-
823
- ### Task
824
- ${task || "Continue from where the previous session left off."}
825
-
826
- ### Instructions
827
- - Review the context above to understand what was done
828
- - Do NOT repeat work that was already completed
829
- - Focus on the task described above
830
- `;
831
- const filename = `handoff-${Date.now()}.md`;
832
- writeFileSync(join(ctx.cwd, filename), handoff, "utf-8");
833
- ctx.ui.notify(`Handoff written to ${filename}\n\nUse: my-pi < ${filename}`);
878
+ if (!ctx.model) {
879
+ ctx.ui.notify("No model selected", "error");
880
+ return;
881
+ }
882
+ const goal = args.trim();
883
+ if (!goal) {
884
+ ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
885
+ return;
886
+ }
887
+ const messages = ctx.sessionManager.getBranch().filter((entry) => entry.type === "message").map((entry) => entry.message);
888
+ if (messages.length === 0) {
889
+ ctx.ui.notify("No conversation to hand off", "error");
890
+ return;
891
+ }
892
+ const conversation_text = serializeConversation(convertToLlm(messages));
893
+ const current_session_file = ctx.sessionManager.getSessionFile();
894
+ const model = ctx.model;
895
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
896
+ const loader = new BorderedLoader(tui, theme, "Generating handoff prompt...");
897
+ loader.onAbort = () => done(null);
898
+ const generate = async () => {
899
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
900
+ if (!auth.ok || !auth.apiKey) throw new Error(auth.ok ? `No API key for ${model.provider}` : auth.error);
901
+ const response = await complete(model, {
902
+ systemPrompt: SYSTEM_PROMPT$1,
903
+ messages: [{
904
+ role: "user",
905
+ content: [{
906
+ type: "text",
907
+ text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`
908
+ }],
909
+ timestamp: Date.now()
910
+ }]
911
+ }, {
912
+ apiKey: auth.apiKey,
913
+ headers: auth.headers,
914
+ signal: loader.signal
915
+ });
916
+ if (response.stopReason === "aborted") return null;
917
+ return response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
918
+ };
919
+ generate().then(done).catch((err) => {
920
+ console.error("Handoff generation failed:", err);
921
+ done(null);
922
+ });
923
+ return loader;
924
+ });
925
+ if (result === null) {
926
+ ctx.ui.notify("Cancelled", "info");
927
+ return;
928
+ }
929
+ const edited_prompt = await ctx.ui.editor("Edit handoff prompt", result);
930
+ if (edited_prompt === void 0) {
931
+ ctx.ui.notify("Cancelled", "info");
932
+ return;
933
+ }
934
+ if ((await ctx.newSession({ parentSession: current_session_file })).cancelled) {
935
+ ctx.ui.notify("New session cancelled", "info");
936
+ return;
937
+ }
938
+ ctx.ui.setEditorText(edited_prompt);
939
+ ctx.ui.notify("Handoff ready. Submit when ready.", "info");
834
940
  }
835
941
  });
836
942
  }
837
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
838
1250
  //#region src/lsp/client.ts
839
1251
  var LspClientStartError = class extends Error {
840
1252
  command;
@@ -3132,6 +3544,122 @@ Always pass \`--json\` for structured output.` };
3132
3544
  });
3133
3545
  }
3134
3546
  //#endregion
3547
+ //#region src/extensions/session-name.ts
3548
+ const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task.
3549
+
3550
+ Guidelines:
3551
+ - Be concise but specific
3552
+ - Use kebab-case or natural language
3553
+ - Focus on the core task/question
3554
+ - Avoid generic names like "discussion" or "conversation"
3555
+ - No quotes, no punctuation at the end
3556
+
3557
+ Examples:
3558
+ - "fix auth bug" -> "fix-auth-bug" or "authentication fix"
3559
+ - "how do I deploy to vercel" -> "vercel deployment"
3560
+ - "explain react hooks" -> "react hooks explanation"
3561
+ - "optimize database queries" -> "db query optimization"
3562
+
3563
+ Output ONLY the session name, nothing else.`;
3564
+ const AUTO_NAME_THRESHOLD = 1;
3565
+ const MAX_CHARS = 4e3;
3566
+ const MAX_NAME_LEN = 50;
3567
+ function clean_name(value) {
3568
+ return value.replace(/^["']|["']$/g, "").replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_NAME_LEN);
3569
+ }
3570
+ function truncate_conversation(value) {
3571
+ return value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + "\n..." : value;
3572
+ }
3573
+ async function generate_session_name(ctx, model, conversation_text, signal) {
3574
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
3575
+ if (!auth.ok || !auth.apiKey) throw new Error(auth.ok ? `No API key for ${model.provider}` : auth.error);
3576
+ const response = await complete(model, {
3577
+ systemPrompt: SYSTEM_PROMPT,
3578
+ messages: [{
3579
+ role: "user",
3580
+ content: [{
3581
+ type: "text",
3582
+ text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`
3583
+ }],
3584
+ timestamp: Date.now()
3585
+ }]
3586
+ }, {
3587
+ apiKey: auth.apiKey,
3588
+ headers: auth.headers,
3589
+ signal
3590
+ });
3591
+ if (response.stopReason === "aborted") return null;
3592
+ return clean_name(response.content.filter((c) => c.type === "text").map((c) => c.text.trim()).join(" "));
3593
+ }
3594
+ async function session_name(pi) {
3595
+ let auto_named_attempted = false;
3596
+ pi.on("agent_end", async (_event, ctx) => {
3597
+ if (!ctx.hasUI || !ctx.model) return;
3598
+ if (pi.getSessionName() || auto_named_attempted) return;
3599
+ const branch = ctx.sessionManager.getBranch();
3600
+ if (branch.filter((entry) => entry.type === "message" && entry.message.role === "user").length < AUTO_NAME_THRESHOLD) return;
3601
+ auto_named_attempted = true;
3602
+ const messages = branch.filter((entry) => entry.type === "message").map((entry) => entry.message);
3603
+ if (messages.length === 0) return;
3604
+ const conversation_text = serializeConversation(convertToLlm(messages));
3605
+ generate_session_name(ctx, ctx.model, conversation_text).then((name) => {
3606
+ if (!name) return;
3607
+ pi.setSessionName(name);
3608
+ ctx.ui.notify(`Auto-named: ${name}`, "info");
3609
+ }).catch((err) => {
3610
+ console.error("Auto-naming failed:", err);
3611
+ });
3612
+ });
3613
+ pi.on("session_start", async () => {
3614
+ auto_named_attempted = false;
3615
+ });
3616
+ pi.registerCommand("session-name", {
3617
+ description: "Set, show, or auto-generate the current session name",
3618
+ handler: async (args, ctx) => {
3619
+ const trimmed = args.trim();
3620
+ if (!trimmed) {
3621
+ const current = pi.getSessionName();
3622
+ ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info");
3623
+ return;
3624
+ }
3625
+ if (trimmed === "--auto" || trimmed === "-a") {
3626
+ if (!ctx.hasUI || !ctx.model) {
3627
+ ctx.ui.notify("Auto-naming requires interactive mode and a selected model", "error");
3628
+ return;
3629
+ }
3630
+ const messages = ctx.sessionManager.getBranch().filter((entry) => entry.type === "message").map((entry) => entry.message);
3631
+ if (messages.length === 0) {
3632
+ ctx.ui.notify("No conversation to analyze", "error");
3633
+ return;
3634
+ }
3635
+ const conversation_text = serializeConversation(convertToLlm(messages));
3636
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
3637
+ const loader = new BorderedLoader(tui, theme, "Generating session name...");
3638
+ loader.onAbort = () => done(null);
3639
+ generate_session_name(ctx, ctx.model, conversation_text, loader.signal).then(done).catch((err) => {
3640
+ console.error("Auto-naming failed:", err);
3641
+ done(null);
3642
+ });
3643
+ return loader;
3644
+ });
3645
+ if (result === null) {
3646
+ ctx.ui.notify("Auto-naming cancelled", "info");
3647
+ return;
3648
+ }
3649
+ if (!result) {
3650
+ ctx.ui.notify("Failed to generate name", "error");
3651
+ return;
3652
+ }
3653
+ pi.setSessionName(result);
3654
+ ctx.ui.notify(`Session named: ${result}`, "info");
3655
+ return;
3656
+ }
3657
+ pi.setSessionName(clean_name(trimmed));
3658
+ ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, "info");
3659
+ }
3660
+ });
3661
+ }
3662
+ //#endregion
3135
3663
  //#region src/skills/config.ts
3136
3664
  const DEFAULT_CONFIG$1 = {
3137
3665
  version: 1,
@@ -4480,7 +5008,10 @@ const BUILTIN_EXTENSION_FACTORIES = {
4480
5008
  handoff,
4481
5009
  recall,
4482
5010
  "prompt-presets": prompt_presets,
4483
- lsp: lsp_default
5011
+ lsp: lsp_default,
5012
+ "session-name": session_name,
5013
+ "confirm-destructive": confirm_destructive,
5014
+ "hooks-resolution": hooks_resolution_default
4484
5015
  };
4485
5016
  const PACKAGE_THEME_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "themes");
4486
5017
  const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
@@ -4497,6 +5028,9 @@ function get_force_disabled_builtins(options) {
4497
5028
  if (!options.recall) force_disabled.add("recall");
4498
5029
  if (!options.prompt_presets) force_disabled.add("prompt-presets");
4499
5030
  if (!options.lsp) force_disabled.add("lsp");
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");
4500
5034
  return force_disabled;
4501
5035
  }
4502
5036
  function create_builtin_extension_factory(key, extension, force_disabled) {
@@ -4518,7 +5052,7 @@ function create_extensions_override(managed_inline_paths) {
4518
5052
  };
4519
5053
  }
4520
5054
  async function create_my_pi(options = {}) {
4521
- 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, 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;
4522
5056
  const effective_agent_dir = resolve_agent_dir(cwd, agent_dir);
4523
5057
  if (agent_dir) process.env[PI_AGENT_DIR_ENV] = effective_agent_dir;
4524
5058
  const resolved_extensions = extensions.map((p) => resolve(cwd, p));
@@ -4530,7 +5064,10 @@ async function create_my_pi(options = {}) {
4530
5064
  handoff,
4531
5065
  recall,
4532
5066
  prompt_presets,
4533
- lsp
5067
+ lsp,
5068
+ session_name,
5069
+ confirm_destructive,
5070
+ hooks_resolution
4534
5071
  });
4535
5072
  const managed_extension_factories = [
4536
5073
  create_telemetry_extension({
@@ -4588,4 +5125,4 @@ async function create_my_pi(options = {}) {
4588
5125
  //#endregion
4589
5126
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
4590
5127
 
4591
- //# sourceMappingURL=api-CWEizv2k.js.map
5128
+ //# sourceMappingURL=api-Dxi4curf.js.map