@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 +48 -0
- package/package.json +54 -0
- package/src/auto-name-utils.ts +90 -0
- package/src/index.ts +90 -0
- package/src/short-label.ts +82 -0
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
|
+
}
|