my-pi 0.0.13 → 0.1.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.
@@ -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,17 @@ 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
+ ]
411
423
  }
412
424
  ];
413
425
  function get_builtin_extensions_config_path() {
@@ -789,48 +801,97 @@ async function filter_output(pi) {
789
801
  }
790
802
  //#endregion
791
803
  //#region src/extensions/handoff.ts
804
+ 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:
805
+
806
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
807
+ 2. Lists any relevant files that were discussed or modified
808
+ 3. Clearly states the next task based on the user's goal
809
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
810
+
811
+ 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.
812
+
813
+ Example output format:
814
+ ## Context
815
+ We've been working on X. Key decisions:
816
+ - Decision 1
817
+ - Decision 2
818
+
819
+ Files involved:
820
+ - path/to/file1.ts
821
+ - path/to/file2.ts
822
+
823
+ ## Task
824
+ [Clear description of what to do next based on user's goal]`;
792
825
  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
826
  pi.registerCommand("handoff", {
809
- description: "Export session context as a handoff prompt for a new session",
827
+ description: "Transfer context to a new focused session with an AI-generated prompt",
810
828
  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");
829
+ if (!ctx.hasUI) {
830
+ ctx.ui.notify("handoff requires interactive mode", "error");
814
831
  return;
815
832
  }
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}`);
833
+ if (!ctx.model) {
834
+ ctx.ui.notify("No model selected", "error");
835
+ return;
836
+ }
837
+ const goal = args.trim();
838
+ if (!goal) {
839
+ ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
840
+ return;
841
+ }
842
+ const messages = ctx.sessionManager.getBranch().filter((entry) => entry.type === "message").map((entry) => entry.message);
843
+ if (messages.length === 0) {
844
+ ctx.ui.notify("No conversation to hand off", "error");
845
+ return;
846
+ }
847
+ const conversation_text = serializeConversation(convertToLlm(messages));
848
+ const current_session_file = ctx.sessionManager.getSessionFile();
849
+ const model = ctx.model;
850
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
851
+ const loader = new BorderedLoader(tui, theme, "Generating handoff prompt...");
852
+ loader.onAbort = () => done(null);
853
+ const generate = async () => {
854
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
855
+ if (!auth.ok || !auth.apiKey) throw new Error(auth.ok ? `No API key for ${model.provider}` : auth.error);
856
+ const response = await complete(model, {
857
+ systemPrompt: SYSTEM_PROMPT$1,
858
+ messages: [{
859
+ role: "user",
860
+ content: [{
861
+ type: "text",
862
+ text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`
863
+ }],
864
+ timestamp: Date.now()
865
+ }]
866
+ }, {
867
+ apiKey: auth.apiKey,
868
+ headers: auth.headers,
869
+ signal: loader.signal
870
+ });
871
+ if (response.stopReason === "aborted") return null;
872
+ return response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
873
+ };
874
+ generate().then(done).catch((err) => {
875
+ console.error("Handoff generation failed:", err);
876
+ done(null);
877
+ });
878
+ return loader;
879
+ });
880
+ if (result === null) {
881
+ ctx.ui.notify("Cancelled", "info");
882
+ return;
883
+ }
884
+ const edited_prompt = await ctx.ui.editor("Edit handoff prompt", result);
885
+ if (edited_prompt === void 0) {
886
+ ctx.ui.notify("Cancelled", "info");
887
+ return;
888
+ }
889
+ if ((await ctx.newSession({ parentSession: current_session_file })).cancelled) {
890
+ ctx.ui.notify("New session cancelled", "info");
891
+ return;
892
+ }
893
+ ctx.ui.setEditorText(edited_prompt);
894
+ ctx.ui.notify("Handoff ready. Submit when ready.", "info");
834
895
  }
835
896
  });
836
897
  }
@@ -3132,6 +3193,122 @@ Always pass \`--json\` for structured output.` };
3132
3193
  });
3133
3194
  }
3134
3195
  //#endregion
3196
+ //#region src/extensions/session-name.ts
3197
+ 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.
3198
+
3199
+ Guidelines:
3200
+ - Be concise but specific
3201
+ - Use kebab-case or natural language
3202
+ - Focus on the core task/question
3203
+ - Avoid generic names like "discussion" or "conversation"
3204
+ - No quotes, no punctuation at the end
3205
+
3206
+ Examples:
3207
+ - "fix auth bug" -> "fix-auth-bug" or "authentication fix"
3208
+ - "how do I deploy to vercel" -> "vercel deployment"
3209
+ - "explain react hooks" -> "react hooks explanation"
3210
+ - "optimize database queries" -> "db query optimization"
3211
+
3212
+ Output ONLY the session name, nothing else.`;
3213
+ const AUTO_NAME_THRESHOLD = 1;
3214
+ const MAX_CHARS = 4e3;
3215
+ const MAX_NAME_LEN = 50;
3216
+ function clean_name(value) {
3217
+ return value.replace(/^["']|["']$/g, "").replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_NAME_LEN);
3218
+ }
3219
+ function truncate_conversation(value) {
3220
+ return value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + "\n..." : value;
3221
+ }
3222
+ async function generate_session_name(ctx, model, conversation_text, signal) {
3223
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
3224
+ if (!auth.ok || !auth.apiKey) throw new Error(auth.ok ? `No API key for ${model.provider}` : auth.error);
3225
+ const response = await complete(model, {
3226
+ systemPrompt: SYSTEM_PROMPT,
3227
+ messages: [{
3228
+ role: "user",
3229
+ content: [{
3230
+ type: "text",
3231
+ text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`
3232
+ }],
3233
+ timestamp: Date.now()
3234
+ }]
3235
+ }, {
3236
+ apiKey: auth.apiKey,
3237
+ headers: auth.headers,
3238
+ signal
3239
+ });
3240
+ if (response.stopReason === "aborted") return null;
3241
+ return clean_name(response.content.filter((c) => c.type === "text").map((c) => c.text.trim()).join(" "));
3242
+ }
3243
+ async function session_name(pi) {
3244
+ let auto_named_attempted = false;
3245
+ pi.on("agent_end", async (_event, ctx) => {
3246
+ if (!ctx.hasUI || !ctx.model) return;
3247
+ if (pi.getSessionName() || auto_named_attempted) return;
3248
+ const branch = ctx.sessionManager.getBranch();
3249
+ if (branch.filter((entry) => entry.type === "message" && entry.message.role === "user").length < AUTO_NAME_THRESHOLD) return;
3250
+ auto_named_attempted = true;
3251
+ const messages = branch.filter((entry) => entry.type === "message").map((entry) => entry.message);
3252
+ if (messages.length === 0) return;
3253
+ const conversation_text = serializeConversation(convertToLlm(messages));
3254
+ generate_session_name(ctx, ctx.model, conversation_text).then((name) => {
3255
+ if (!name) return;
3256
+ pi.setSessionName(name);
3257
+ ctx.ui.notify(`Auto-named: ${name}`, "info");
3258
+ }).catch((err) => {
3259
+ console.error("Auto-naming failed:", err);
3260
+ });
3261
+ });
3262
+ pi.on("session_start", async () => {
3263
+ auto_named_attempted = false;
3264
+ });
3265
+ pi.registerCommand("session-name", {
3266
+ description: "Set, show, or auto-generate the current session name",
3267
+ handler: async (args, ctx) => {
3268
+ const trimmed = args.trim();
3269
+ if (!trimmed) {
3270
+ const current = pi.getSessionName();
3271
+ ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info");
3272
+ return;
3273
+ }
3274
+ if (trimmed === "--auto" || trimmed === "-a") {
3275
+ if (!ctx.hasUI || !ctx.model) {
3276
+ ctx.ui.notify("Auto-naming requires interactive mode and a selected model", "error");
3277
+ return;
3278
+ }
3279
+ const messages = ctx.sessionManager.getBranch().filter((entry) => entry.type === "message").map((entry) => entry.message);
3280
+ if (messages.length === 0) {
3281
+ ctx.ui.notify("No conversation to analyze", "error");
3282
+ return;
3283
+ }
3284
+ const conversation_text = serializeConversation(convertToLlm(messages));
3285
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
3286
+ const loader = new BorderedLoader(tui, theme, "Generating session name...");
3287
+ loader.onAbort = () => done(null);
3288
+ generate_session_name(ctx, ctx.model, conversation_text, loader.signal).then(done).catch((err) => {
3289
+ console.error("Auto-naming failed:", err);
3290
+ done(null);
3291
+ });
3292
+ return loader;
3293
+ });
3294
+ if (result === null) {
3295
+ ctx.ui.notify("Auto-naming cancelled", "info");
3296
+ return;
3297
+ }
3298
+ if (!result) {
3299
+ ctx.ui.notify("Failed to generate name", "error");
3300
+ return;
3301
+ }
3302
+ pi.setSessionName(result);
3303
+ ctx.ui.notify(`Session named: ${result}`, "info");
3304
+ return;
3305
+ }
3306
+ pi.setSessionName(clean_name(trimmed));
3307
+ ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, "info");
3308
+ }
3309
+ });
3310
+ }
3311
+ //#endregion
3135
3312
  //#region src/skills/config.ts
3136
3313
  const DEFAULT_CONFIG$1 = {
3137
3314
  version: 1,
@@ -4480,7 +4657,8 @@ const BUILTIN_EXTENSION_FACTORIES = {
4480
4657
  handoff,
4481
4658
  recall,
4482
4659
  "prompt-presets": prompt_presets,
4483
- lsp: lsp_default
4660
+ lsp: lsp_default,
4661
+ "session-name": session_name
4484
4662
  };
4485
4663
  const PACKAGE_THEME_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "themes");
4486
4664
  const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
@@ -4497,6 +4675,7 @@ function get_force_disabled_builtins(options) {
4497
4675
  if (!options.recall) force_disabled.add("recall");
4498
4676
  if (!options.prompt_presets) force_disabled.add("prompt-presets");
4499
4677
  if (!options.lsp) force_disabled.add("lsp");
4678
+ if (!options.session_name) force_disabled.add("session-name");
4500
4679
  return force_disabled;
4501
4680
  }
4502
4681
  function create_builtin_extension_factory(key, extension, force_disabled) {
@@ -4518,7 +4697,7 @@ function create_extensions_override(managed_inline_paths) {
4518
4697
  };
4519
4698
  }
4520
4699
  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;
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;
4522
4701
  const effective_agent_dir = resolve_agent_dir(cwd, agent_dir);
4523
4702
  if (agent_dir) process.env[PI_AGENT_DIR_ENV] = effective_agent_dir;
4524
4703
  const resolved_extensions = extensions.map((p) => resolve(cwd, p));
@@ -4530,7 +4709,8 @@ async function create_my_pi(options = {}) {
4530
4709
  handoff,
4531
4710
  recall,
4532
4711
  prompt_presets,
4533
- lsp
4712
+ lsp,
4713
+ session_name
4534
4714
  });
4535
4715
  const managed_extension_factories = [
4536
4716
  create_telemetry_extension({
@@ -4588,4 +4768,4 @@ async function create_my_pi(options = {}) {
4588
4768
  //#endregion
4589
4769
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
4590
4770
 
4591
- //# sourceMappingURL=api-CWEizv2k.js.map
4771
+ //# sourceMappingURL=api-1ZXLxSgP.js.map