@carter-mcalister/pi-auto-name 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 ADDED
@@ -0,0 +1,48 @@
1
+ # `@carter-mcalister/pi-auto-name`
2
+
3
+ English-only automatic session naming for [Pi Coding Agent](https://github.com/badlogic/pi-mono).
4
+
5
+ This package replaces `@ryan_nookpi/pi-extension-auto-name` with the same basic behavior, but it forces generated session titles to be in English.
6
+
7
+ ## What it does
8
+
9
+ - Watches the first user prompt in a session
10
+ - Generates a short session title with the current model
11
+ - Forces the generated title to be English-only
12
+ - Applies the title through `pi.setSessionName()`
13
+ - Mirrors the title into the Pi status area and terminal title
14
+ - Skips subagent sessions
15
+
16
+ ## Why this exists
17
+
18
+ The original package was prompting the model in Korean, which caused auto-generated session names to show up in Korean in the Pi session list and tree.
19
+
20
+ This replacement keeps the auto-naming workflow while switching the prompt and context text to English.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pi install /Users/carter/Developer/repos/pi-packages/packages/pi-auto-name
26
+ ```
27
+
28
+ If Pi is already running, use `/reload` after installing the extension.
29
+
30
+ ## Remove the original package
31
+
32
+ ```bash
33
+ pi remove npm:@ryan_nookpi/pi-extension-auto-name
34
+ ```
35
+
36
+ ## Notes
37
+
38
+ - This affects session display names, not Pi compaction summaries.
39
+ - If the model or auth is unavailable when a session starts, the session will simply remain unnamed.
40
+ - Titles are intentionally short and clipped to 30 characters.
41
+
42
+ ## Development
43
+
44
+ ```bash
45
+ mise install
46
+ bun install
47
+ mise run check
48
+ ```
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@carter-mcalister/pi-auto-name",
3
+ "version": "0.1.1",
4
+ "description": "English-only automatic session naming for Pi",
5
+ "author": "Carter McAlister",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/CarterMcAlister/pi-packages.git",
10
+ "directory": "packages/pi-auto-name"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/CarterMcAlister/pi-packages/issues"
14
+ },
15
+ "homepage": "https://github.com/CarterMcAlister/pi-packages/tree/main/packages/pi-auto-name#readme",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "README.md"
22
+ ],
23
+ "keywords": [
24
+ "pi-package",
25
+ "pi-extension",
26
+ "pi",
27
+ "session-name",
28
+ "auto-name"
29
+ ],
30
+ "scripts": {
31
+ "format": "bunx @biomejs/biome check --write .",
32
+ "lint": "bunx @biomejs/biome check .",
33
+ "typecheck": "bunx tsc -p tsconfig.json --noEmit",
34
+ "test": "bun test",
35
+ "check": "bun run lint && bun run typecheck && bun run test"
36
+ },
37
+ "pi": {
38
+ "extensions": [
39
+ "./src/index.ts"
40
+ ]
41
+ },
42
+ "peerDependencies": {
43
+ "@mariozechner/pi-ai": "*",
44
+ "@mariozechner/pi-coding-agent": "*"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "2.3.5",
48
+ "@mariozechner/pi-ai": "*",
49
+ "@mariozechner/pi-coding-agent": "*",
50
+ "bun-types": "latest",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "license": "MIT"
54
+ }
@@ -0,0 +1,90 @@
1
+ import * as os from 'node:os'
2
+ import * as path from 'node:path'
3
+
4
+ export const SUBAGENT_SESSION_DIR = path.join(
5
+ os.homedir(),
6
+ '.pi',
7
+ 'agent',
8
+ 'sessions',
9
+ 'subagents',
10
+ )
11
+
12
+ export const NAME_SYSTEM_PROMPT = [
13
+ "Read the user's first request and generate a very short session title.",
14
+ 'Requirements:',
15
+ '- Output English only, even if the request is written in another language.',
16
+ '- Output only the title text.',
17
+ '- No quotes, markdown, prefixes, or explanations.',
18
+ '- Keep it concise: usually 2 to 6 words.',
19
+ '- Maximum 30 characters.',
20
+ ].join('\n')
21
+
22
+ export const MAX_MESSAGE_LENGTH = 500
23
+ export const MAX_NAME_LENGTH = 30
24
+ export const MAX_STATUS_CHARS = 90
25
+ export const SUCCESSFUL_STOP_REASON = 'stop'
26
+
27
+ export function isSubagentSessionPath(
28
+ sessionFilePath: string | undefined,
29
+ ): boolean {
30
+ if (!sessionFilePath) return false
31
+ return (
32
+ sessionFilePath.startsWith(SUBAGENT_SESSION_DIR + path.sep) ||
33
+ sessionFilePath.startsWith(`${SUBAGENT_SESSION_DIR}/`)
34
+ )
35
+ }
36
+
37
+ export function extractSessionFilePath(
38
+ sessionManager: unknown,
39
+ ): string | undefined {
40
+ try {
41
+ if (
42
+ sessionManager &&
43
+ typeof sessionManager === 'object' &&
44
+ 'getSessionFile' in sessionManager
45
+ ) {
46
+ const getSessionFile = (sessionManager as Record<string, unknown>)
47
+ .getSessionFile
48
+ if (typeof getSessionFile === 'function') {
49
+ const raw = String(getSessionFile() ?? '')
50
+ const cleaned = raw.replace(/[\r\n\t]+/g, '').trim()
51
+ return cleaned || undefined
52
+ }
53
+ }
54
+ } catch {
55
+ // Ignore errors and fall back to unnamed sessions.
56
+ }
57
+
58
+ return undefined
59
+ }
60
+
61
+ export function formatNameStatus(name: string): string {
62
+ const singleLine = name.replace(/\s+/g, ' ').trim()
63
+ return singleLine.length > MAX_STATUS_CHARS
64
+ ? `${singleLine.slice(0, MAX_STATUS_CHARS - 1)}…`
65
+ : singleLine
66
+ }
67
+
68
+ export function buildNameContext(userMessage: string): string {
69
+ return `User message: ${userMessage.slice(0, MAX_MESSAGE_LENGTH)}`
70
+ }
71
+
72
+ export function isSuccessfulResult(stopReason: string | undefined): boolean {
73
+ return stopReason === SUCCESSFUL_STOP_REASON
74
+ }
75
+
76
+ export function extractNameFromResult(
77
+ content: ReadonlyArray<{ type: string; text?: string }>,
78
+ ): string {
79
+ const text = content
80
+ .filter(
81
+ (part): part is { type: 'text'; text: string } =>
82
+ part.type === 'text' && typeof part.text === 'string',
83
+ )
84
+ .map((part) => part.text)
85
+ .join('')
86
+ .trim()
87
+ .replace(/^['"`]+|['"`]+$/g, '')
88
+
89
+ return text.slice(0, MAX_NAME_LENGTH)
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ import * as path from 'node:path'
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from '@mariozechner/pi-coding-agent'
6
+ import {
7
+ buildNameContext,
8
+ extractNameFromResult,
9
+ extractSessionFilePath,
10
+ formatNameStatus,
11
+ isSubagentSessionPath,
12
+ NAME_SYSTEM_PROMPT,
13
+ } from './auto-name-utils.ts'
14
+ import { generateShortLabel } from './short-label.ts'
15
+
16
+ const NAME_STATUS_KEY = 'name-footer'
17
+
18
+ function isSubagentSession(ctx: ExtensionContext): boolean {
19
+ const sessionFilePath = extractSessionFilePath(ctx.sessionManager)
20
+ return isSubagentSessionPath(sessionFilePath)
21
+ }
22
+
23
+ async function detectNameFromMessage(
24
+ userMessage: string,
25
+ ctx: ExtensionContext,
26
+ ): Promise<string> {
27
+ return generateShortLabel(ctx, {
28
+ systemPrompt: NAME_SYSTEM_PROMPT,
29
+ prompt: buildNameContext(userMessage),
30
+ extractText: extractNameFromResult,
31
+ })
32
+ }
33
+
34
+ export default function autoSessionName(pi: ExtensionAPI) {
35
+ const updateTerminalTitle = (ctx: ExtensionContext) => {
36
+ if (!ctx.hasUI) return
37
+
38
+ const cwdBasename = path.basename(process.cwd())
39
+ const name = pi.getSessionName()
40
+ if (!name) return
41
+
42
+ ctx.ui.setTitle(`π - ${name} - ${cwdBasename}`)
43
+ }
44
+
45
+ const updateStatus = (ctx: ExtensionContext) => {
46
+ if (!ctx.hasUI) return
47
+
48
+ const name = pi.getSessionName()
49
+ if (!name) {
50
+ ctx.ui.setStatus(NAME_STATUS_KEY, undefined)
51
+ return
52
+ }
53
+
54
+ ctx.ui.setStatus(NAME_STATUS_KEY, formatNameStatus(name))
55
+ updateTerminalTitle(ctx)
56
+ }
57
+
58
+ pi.on('before_agent_start', async (event, ctx) => {
59
+ if (isSubagentSession(ctx)) return
60
+ if (pi.getSessionName()) return
61
+
62
+ const text = event.prompt.trim()
63
+ if (!text) return
64
+
65
+ void (async () => {
66
+ try {
67
+ const detected = await detectNameFromMessage(text, ctx)
68
+ if (detected && !pi.getSessionName()) {
69
+ pi.setSessionName(detected)
70
+ updateStatus(ctx)
71
+ }
72
+ } catch {
73
+ // Leave the session unnamed on failures.
74
+ }
75
+ })()
76
+ })
77
+
78
+ pi.on('session_start', async (_event, ctx) => {
79
+ updateStatus(ctx)
80
+ })
81
+
82
+ pi.on('session_tree', async (_event, ctx) => {
83
+ updateStatus(ctx)
84
+ })
85
+
86
+ pi.on('session_shutdown', async (_event, ctx) => {
87
+ if (!ctx.hasUI) return
88
+ ctx.ui.setStatus(NAME_STATUS_KEY, undefined)
89
+ })
90
+ }
@@ -0,0 +1,82 @@
1
+ import { completeSimple } from '@mariozechner/pi-ai'
2
+
3
+ type SummaryModel = Parameters<typeof completeSimple>[0]
4
+ type SummaryResult = Awaited<ReturnType<typeof completeSimple>>
5
+
6
+ type AuthResult = {
7
+ ok: boolean
8
+ apiKey?: string
9
+ headers?: Record<string, string>
10
+ }
11
+
12
+ export type ShortLabelContext = {
13
+ model?: SummaryModel
14
+ modelRegistry?: {
15
+ getApiKeyAndHeaders: (model: SummaryModel) => Promise<AuthResult>
16
+ }
17
+ }
18
+
19
+ export type GenerateShortLabelOptions = {
20
+ systemPrompt: string
21
+ prompt: string
22
+ maxTokens?: number
23
+ timeoutMs?: number
24
+ extractText?: (content: SummaryResult['content']) => string
25
+ }
26
+
27
+ function defaultExtractText(content: SummaryResult['content']): string {
28
+ return content
29
+ .filter(
30
+ (part): part is { type: 'text'; text: string } =>
31
+ part.type === 'text' && typeof part.text === 'string',
32
+ )
33
+ .map((part) => part.text)
34
+ .join('')
35
+ .trim()
36
+ }
37
+
38
+ export async function generateShortLabel(
39
+ ctx: ShortLabelContext,
40
+ options: GenerateShortLabelOptions,
41
+ ): Promise<string> {
42
+ const model = ctx.model
43
+ if (!model || !ctx.modelRegistry) return ''
44
+
45
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model)
46
+ if (!auth.ok) return ''
47
+
48
+ const controller = new AbortController()
49
+ const timeoutMs = options.timeoutMs ?? 10000
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
51
+
52
+ try {
53
+ const result = await completeSimple(
54
+ model,
55
+ {
56
+ systemPrompt: options.systemPrompt,
57
+ messages: [
58
+ {
59
+ role: 'user',
60
+ content: [{ type: 'text', text: options.prompt }],
61
+ timestamp: Date.now(),
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ apiKey: auth.apiKey,
67
+ headers: auth.headers,
68
+ signal: controller.signal,
69
+ reasoning: 'minimal',
70
+ maxTokens: options.maxTokens ?? 60,
71
+ },
72
+ )
73
+
74
+ if (result.stopReason !== 'stop') return ''
75
+ const extractText = options.extractText ?? defaultExtractText
76
+ return extractText(result.content)
77
+ } catch {
78
+ return ''
79
+ } finally {
80
+ clearTimeout(timer)
81
+ }
82
+ }