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.
- package/dist/{api-CWEizv2k.js → api-1ZXLxSgP.js} +223 -43
- package/dist/api-1ZXLxSgP.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/config.test.ts +3 -0
- package/src/extensions/config.ts +12 -2
- package/src/extensions/handoff.ts +152 -66
- package/src/extensions/session-name.ts +234 -0
- package/dist/api-CWEizv2k.js.map +0 -1
|
@@ -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: "
|
|
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: "
|
|
827
|
+
description: "Transfer context to a new focused session with an AI-generated prompt",
|
|
810
828
|
handler: async (args, ctx) => {
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
ctx.ui.
|
|
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-
|
|
4771
|
+
//# sourceMappingURL=api-1ZXLxSgP.js.map
|