@agnishc/edb-auto-name-session 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-04-29
4
+
5
+ ### Added
6
+ - Initial release: replaces Pi's default first-message session label with a generated session title
7
+ - Uses the `opencode/big-pickle` model to generate a concise session title after the first user message
8
+ - Shows interactive notifications while auto-naming runs and when the session title is set
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agnish Chakraborty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @agnishc/edb-auto-name-session
2
+
3
+ A Pi CLI extension that replaces Pi's default first-message session label with a short generated title.
4
+
5
+ By default, Pi already shows the first user message in the session picker when no explicit session name exists. This extension watches the first real user prompt in a new unnamed session, sends that prompt to `opencode/big-pickle`, and then stores a cleaner display name with `pi.setSessionName()`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:@agnishc/edb-auto-name-session
11
+ ```
12
+
13
+ ## Use case
14
+
15
+ Without this extension:
16
+ - Pi falls back to the raw first user message as the session label
17
+
18
+ With this extension:
19
+ - Pi keeps the same session flow
20
+ - after the first user message, the fallback label is replaced with a concise generated title
21
+
22
+ Example:
23
+
24
+ - First prompt: `The next extension to build is auto-name-session. This will use the opencode big pickle model.`
25
+ - Generated session name: `Build Auto Name Session`
26
+
27
+ ## Behavior
28
+
29
+ - Runs once per fresh unnamed session
30
+ - Waits until the first user message is actually recorded
31
+ - Uses `opencode/big-pickle` to generate a concise title
32
+ - Shows an interactive notification while auto-naming runs, then confirms the final title
33
+ - Calls `pi.setSessionName()` only if the session is still unnamed
34
+ - Leaves already named, resumed, or forked sessions alone
35
+
36
+ ## Requirements
37
+
38
+ The extension uses the `opencode` provider and the `big-pickle` model.
39
+ Configure OpenCode access with either:
40
+
41
+ ```bash
42
+ export OPENCODE_API_KEY=...
43
+ ```
44
+
45
+ or `/login` in Pi if you store provider credentials there.
46
+
47
+ ## License
48
+
49
+ [MIT](LICENSE) © Agnish Chakraborty
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@agnishc/edb-auto-name-session",
3
+ "version": "0.4.0",
4
+ "description": "Pi extension: replace Pi's first-message session label with a generated title",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "edb"
9
+ ],
10
+ "type": "module",
11
+ "license": "MIT",
12
+ "author": "Agnish Chakraborty",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
16
+ "directory": "packages/edb-auto-name-session"
17
+ },
18
+ "homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-auto-name-session#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/agnishcc/pi-extention-monorepo/issues"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "test": "vitest run"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "README.md",
31
+ "LICENSE",
32
+ "CHANGELOG.md"
33
+ ],
34
+ "pi": {
35
+ "extensions": [
36
+ "./src/index.ts"
37
+ ]
38
+ },
39
+ "peerDependencies": {
40
+ "@mariozechner/pi-ai": "*",
41
+ "@mariozechner/pi-coding-agent": "*"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { complete } from "@mariozechner/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import { extractUserText, sanitizeSessionName, shouldArmAutoNaming } from "./title";
4
+
5
+ const MODEL_PROVIDER = "opencode";
6
+ const MODEL_ID = "big-pickle";
7
+
8
+ const SYSTEM_PROMPT = `You create short session titles for coding and technical work.
9
+ Return exactly one title based only on the user's first message.
10
+ Rules:
11
+ - Prefer 2 to 6 words
12
+ - Use Title Case
13
+ - Mention the task, feature, bug, or file focus when clear
14
+ - No quotes
15
+ - No markdown
16
+ - No labels like Title:
17
+ - No trailing punctuation
18
+ - Maximum 60 characters`;
19
+
20
+ export default function autoNameSessionExtension(pi: ExtensionAPI): void {
21
+ let sessionToken = 0;
22
+ let armed = false;
23
+ let pending = false;
24
+
25
+ pi.on("session_start", async (_event, ctx) => {
26
+ sessionToken += 1;
27
+ armed = shouldArmAutoNaming(ctx.sessionManager.getBranch(), pi.getSessionName());
28
+ pending = false;
29
+ });
30
+
31
+ pi.on("session_shutdown", async () => {
32
+ sessionToken += 1;
33
+ armed = false;
34
+ pending = false;
35
+ });
36
+
37
+ pi.on("message_end", async (event, ctx) => {
38
+ if (!armed || pending || pi.getSessionName()) return;
39
+ if (event.message.role !== "user") return;
40
+
41
+ const prompt = extractUserText(event.message.content);
42
+ armed = false;
43
+ if (!prompt) return;
44
+
45
+ pending = true;
46
+ const token = sessionToken;
47
+
48
+ if (ctx.hasUI) {
49
+ ctx.ui.notify("Auto-naming session…", "info");
50
+ }
51
+
52
+ try {
53
+ const name = await generateSessionName(prompt, ctx);
54
+ if (!name) return;
55
+ if (token !== sessionToken) return;
56
+ if (pi.getSessionName()) return;
57
+ pi.setSessionName(name);
58
+ if (ctx.hasUI) {
59
+ ctx.ui.notify(`Session named: ${name}`, "info");
60
+ }
61
+ } catch (error: unknown) {
62
+ console.error("[edb-auto-name-session] Failed to generate session name:", error);
63
+ } finally {
64
+ if (token === sessionToken) pending = false;
65
+ }
66
+ });
67
+ }
68
+
69
+ async function generateSessionName(prompt: string, ctx: ExtensionContext): Promise<string | undefined> {
70
+ const model = ctx.modelRegistry.find(MODEL_PROVIDER, MODEL_ID);
71
+ if (!model) return undefined;
72
+
73
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
74
+ if (!auth.ok || !auth.apiKey) return undefined;
75
+
76
+ const response = await complete(
77
+ model,
78
+ {
79
+ systemPrompt: SYSTEM_PROMPT,
80
+ messages: [
81
+ {
82
+ role: "user",
83
+ content: [{ type: "text", text: prompt }],
84
+ timestamp: Date.now(),
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ apiKey: auth.apiKey,
90
+ headers: auth.headers,
91
+ maxTokens: 256,
92
+ },
93
+ );
94
+
95
+ const text = response.content
96
+ .filter((part): part is { type: "text"; text: string } => part.type === "text")
97
+ .map((part) => part.text)
98
+ .join("\n")
99
+ .trim();
100
+
101
+ return sanitizeSessionName(text);
102
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { countUserMessages, extractUserText, sanitizeSessionName, shouldArmAutoNaming } from "./title";
3
+
4
+ describe("title helpers", () => {
5
+ it("extracts only text parts from user content", () => {
6
+ expect(
7
+ extractUserText([
8
+ { type: "text", text: "Build an auto name session extension" },
9
+ { type: "image", source: { type: "base64", data: "abc" } },
10
+ { type: "text", text: "Use big pickle" },
11
+ ]),
12
+ ).toBe("Build an auto name session extension\nUse big pickle");
13
+ });
14
+
15
+ it("sanitizes labels, quotes, and punctuation", () => {
16
+ expect(sanitizeSessionName('Title: "Auto Name Session".')).toBe("Auto Name Session");
17
+ });
18
+
19
+ it("counts only user messages", () => {
20
+ const entries = [
21
+ { type: "message", message: { role: "user", content: [{ type: "text", text: "one" }] } },
22
+ { type: "message", message: { role: "assistant", content: [{ type: "text", text: "two" }] } },
23
+ { type: "custom", customType: "x", data: {} },
24
+ ] as any;
25
+
26
+ expect(countUserMessages(entries)).toBe(1);
27
+ });
28
+
29
+ it("arms only for empty unnamed sessions", () => {
30
+ expect(shouldArmAutoNaming([], undefined)).toBe(true);
31
+ expect(shouldArmAutoNaming([], "Already Named")).toBe(false);
32
+ });
33
+ });
package/src/title.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { SessionEntry } from "@mariozechner/pi-coding-agent";
2
+
3
+ const MAX_PROMPT_CHARS = 4_000;
4
+ const MAX_TITLE_CHARS = 60;
5
+
6
+ type TextPart = {
7
+ type?: string;
8
+ text?: string;
9
+ };
10
+
11
+ export function shouldArmAutoNaming(entries: SessionEntry[], currentName: string | undefined): boolean {
12
+ return !currentName?.trim() && countUserMessages(entries) === 0;
13
+ }
14
+
15
+ export function countUserMessages(entries: SessionEntry[]): number {
16
+ return entries.filter(isUserMessageEntry).length;
17
+ }
18
+
19
+ export function extractUserText(content: unknown): string {
20
+ if (typeof content === "string") return normalizePrompt(content);
21
+ if (!Array.isArray(content)) return "";
22
+
23
+ return normalizePrompt(
24
+ content
25
+ .filter(isTextPart)
26
+ .map((part) => part.text)
27
+ .join("\n"),
28
+ );
29
+ }
30
+
31
+ export function sanitizeSessionName(value: string): string | undefined {
32
+ const firstLine = value
33
+ .replace(/^```[a-z0-9_-]*\s*/i, "")
34
+ .replace(/```$/g, "")
35
+ .split(/\r?\n/)
36
+ .map((line) => line.trim())
37
+ .find(Boolean);
38
+
39
+ if (!firstLine) return undefined;
40
+
41
+ let title = firstLine
42
+ .replace(/^(title|session name)\s*:\s*/i, "")
43
+ .replace(/^[-*]\s*/, "")
44
+ .replace(/[.?!:;,]+$/g, "")
45
+ .replace(/^['"`]+|['"`]+$/g, "")
46
+ .replace(/\s+/g, " ")
47
+ .trim();
48
+
49
+ if (!title) return undefined;
50
+ if (title.length <= MAX_TITLE_CHARS) return title;
51
+
52
+ title = title.slice(0, MAX_TITLE_CHARS).trimEnd();
53
+ const lastSpace = title.lastIndexOf(" ");
54
+ if (lastSpace > 20) title = title.slice(0, lastSpace);
55
+ return title.trim() || undefined;
56
+ }
57
+
58
+ function isUserMessageEntry(entry: SessionEntry): entry is SessionEntry & {
59
+ type: "message";
60
+ message: { role: "user"; content: unknown };
61
+ } {
62
+ return entry.type === "message" && entry.message.role === "user";
63
+ }
64
+
65
+ function isTextPart(value: unknown): value is TextPart & { type: "text"; text: string } {
66
+ return Boolean(
67
+ value &&
68
+ typeof value === "object" &&
69
+ (value as TextPart).type === "text" &&
70
+ typeof (value as TextPart).text === "string",
71
+ );
72
+ }
73
+
74
+ function normalizePrompt(value: string): string {
75
+ return value.replace(/\r\n/g, "\n").trim().slice(0, MAX_PROMPT_CHARS);
76
+ }