@d3ara1n/pi-session-namer 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/README.md +64 -0
- package/package.json +40 -0
- package/src/config.ts +64 -0
- package/src/index.ts +141 -0
- package/src/namer.ts +96 -0
- package/src/types.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @d3ara1n/pi-session-namer
|
|
2
|
+
|
|
3
|
+
Auto-name pi sessions using a cheap side agent.
|
|
4
|
+
|
|
5
|
+
On the first user prompt of a new session, calls a lightweight side agent model
|
|
6
|
+
to generate a concise session title, then sets it via `pi.setSessionName()`.
|
|
7
|
+
Subsequent turns are skipped with near-zero overhead.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Zero-config**: Works out of the box with pi-model-roles' `utility` role
|
|
12
|
+
- **First-turn only**: Adds ~0.5-1s latency on the first prompt, zero overhead after
|
|
13
|
+
- **Graceful fallback**: If the side agent fails, truncates the user prompt as name
|
|
14
|
+
- **Manual rename**: `/namer:rename` to regenerate at any time
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
In `~/.pi/agent/settings.json`:
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
{
|
|
22
|
+
"sessionNamer": {
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"sideAgentRole": "utility",
|
|
25
|
+
"maxLength": 50,
|
|
26
|
+
"language": "zh"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Field | Default | Description |
|
|
32
|
+
|-------|---------|-------------|
|
|
33
|
+
| `enabled` | `true` | Global on/off switch |
|
|
34
|
+
| `sideAgentRole` | `"utility"` | pi-model-roles role for the naming side agent |
|
|
35
|
+
| `maxLength` | `50` | Maximum name length in characters |
|
|
36
|
+
|
|
37
|
+
Project-level `.pi/settings.json` overrides global settings.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `/namer` | Show status and config |
|
|
44
|
+
| `/namer on` | Enable auto-naming |
|
|
45
|
+
| `/namer off` | Disable auto-naming |
|
|
46
|
+
| `/namer:rename` | Regenerate session name from last prompt |
|
|
47
|
+
|
|
48
|
+
## Dependencies
|
|
49
|
+
|
|
50
|
+
- `@earendil-works/pi-ai` — `complete()` for LLM calls
|
|
51
|
+
- `@earendil-works/pi-coding-agent` — Extension API
|
|
52
|
+
- `@d3ara1n/pi-model-roles` — Model role resolution
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
Add to `~/.pi/agent/settings.json`:
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{
|
|
60
|
+
"extensions": [
|
|
61
|
+
"/path/to/pi-extensions/packages/pi-session-namer"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@d3ara1n/pi-session-namer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auto-name pi sessions using a cheap side agent — generates concise titles from the first user message",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi"
|
|
9
|
+
],
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@earendil-works/pi-ai": "*",
|
|
12
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
13
|
+
"@d3ara1n/pi-model-roles": "*"
|
|
14
|
+
},
|
|
15
|
+
"peerDependenciesMeta": {
|
|
16
|
+
"@earendil-works/pi-ai": {
|
|
17
|
+
"optional": true
|
|
18
|
+
},
|
|
19
|
+
"@earendil-works/pi-coding-agent": {
|
|
20
|
+
"optional": true
|
|
21
|
+
},
|
|
22
|
+
"@d3ara1n/pi-model-roles": {
|
|
23
|
+
"optional": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@d3ara1n/pi-model-roles": "^0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./src/index.ts"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/d3ara1n/pi-extensions",
|
|
37
|
+
"directory": "packages/pi-session-namer"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read session-namer configuration from settings files.
|
|
3
|
+
*
|
|
4
|
+
* Global (~/.pi/agent/settings.json) + project (.pi/settings.json),
|
|
5
|
+
* project overrides global.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { SessionNamerConfig } from "./types.ts";
|
|
12
|
+
import { DEFAULT_CONFIG } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
function getAgentDir(): string {
|
|
15
|
+
const envDir = process.env.PI_AGENT_DIR;
|
|
16
|
+
if (envDir) return envDir;
|
|
17
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readSettingsFile(filePath: string): any {
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(filePath)) return {};
|
|
23
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
24
|
+
const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
25
|
+
return JSON.parse(stripped);
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function merge(target: any, source: any): any {
|
|
32
|
+
if (!source || typeof source !== "object") return target;
|
|
33
|
+
if (!target || typeof target !== "object") return source;
|
|
34
|
+
const result = { ...target };
|
|
35
|
+
for (const key of Object.keys(source)) {
|
|
36
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
37
|
+
result[key] = merge(result[key], source[key]);
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = source[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load session-namer config from merged settings.
|
|
47
|
+
* @param cwd - Project working directory
|
|
48
|
+
*/
|
|
49
|
+
export function loadNamerConfig(cwd?: string): SessionNamerConfig {
|
|
50
|
+
const globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
|
|
51
|
+
const projectSettings = cwd
|
|
52
|
+
? readSettingsFile(path.join(cwd, ".pi", "settings.json"))
|
|
53
|
+
: {};
|
|
54
|
+
const settings = merge(globalSettings, projectSettings);
|
|
55
|
+
|
|
56
|
+
const raw = settings?.sessionNamer;
|
|
57
|
+
if (!raw) return DEFAULT_CONFIG;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
enabled: raw.enabled ?? DEFAULT_CONFIG.enabled,
|
|
61
|
+
sideAgentRole: raw.sideAgentRole ?? DEFAULT_CONFIG.sideAgentRole,
|
|
62
|
+
maxLength: raw.maxLength ?? DEFAULT_CONFIG.maxLength,
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-session-namer — Auto-name pi sessions using a cheap side agent.
|
|
3
|
+
*
|
|
4
|
+
* On the first user prompt of a new session, calls a lightweight side agent
|
|
5
|
+
* to generate a concise session title, then sets it via pi.setSessionName().
|
|
6
|
+
* Subsequent turns are skipped with near-zero overhead.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { getModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
11
|
+
import { DEFAULT_CONFIG } from "./types.ts";
|
|
12
|
+
import type { SessionNamerConfig } from "./types.ts";
|
|
13
|
+
import { loadNamerConfig } from "./config.ts";
|
|
14
|
+
import { generateSessionName } from "./namer.ts";
|
|
15
|
+
|
|
16
|
+
export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
17
|
+
let config: SessionNamerConfig = DEFAULT_CONFIG;
|
|
18
|
+
let hasNamed = false;
|
|
19
|
+
let lastPrompt = "";
|
|
20
|
+
|
|
21
|
+
// ── session_start: load config, reset flag ──────────────────────
|
|
22
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
23
|
+
config = loadNamerConfig(_ctx.cwd);
|
|
24
|
+
hasNamed = false;
|
|
25
|
+
lastPrompt = "";
|
|
26
|
+
|
|
27
|
+
// 如果会话已有名称(resume/fork/用户手动命名),不再自动命名
|
|
28
|
+
const existingName = pi.getSessionName();
|
|
29
|
+
if (existingName) {
|
|
30
|
+
hasNamed = true;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── before_agent_start: auto-name on first prompt ───────────────
|
|
35
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
36
|
+
if (!config.enabled || hasNamed) return;
|
|
37
|
+
|
|
38
|
+
// Cache prompt for /namer:rename
|
|
39
|
+
lastPrompt = event.prompt;
|
|
40
|
+
|
|
41
|
+
// Skip empty prompts (e.g. image-only messages)
|
|
42
|
+
if (!event.prompt?.trim()) return;
|
|
43
|
+
|
|
44
|
+
// 标记为已处理(无论后续成功与否,不重试)
|
|
45
|
+
hasNamed = true;
|
|
46
|
+
|
|
47
|
+
let rolesApi;
|
|
48
|
+
try {
|
|
49
|
+
rolesApi = getModelRolesAPI();
|
|
50
|
+
} catch {
|
|
51
|
+
console.warn("[pi-session-namer] pi-model-roles not initialized — skipping");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolved = await rolesApi.resolveRoleAsync(config.sideAgentRole);
|
|
56
|
+
if (!resolved.model) {
|
|
57
|
+
console.warn(`[pi-session-namer] Side agent role "${config.sideAgentRole}" not available — skipping`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const name = await generateSessionName(
|
|
62
|
+
resolved.model,
|
|
63
|
+
resolved.apiKey,
|
|
64
|
+
resolved.headers,
|
|
65
|
+
config,
|
|
66
|
+
event.prompt,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
pi.setSessionName(name);
|
|
70
|
+
console.log(`[pi-session-namer] Session named: ${name}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── /namer — show status ────────────────────────────────────────
|
|
74
|
+
pi.registerCommand("namer", {
|
|
75
|
+
description: "Show session namer status and config",
|
|
76
|
+
handler: async (args, ctx) => {
|
|
77
|
+
const value = (args ?? "").trim().toLowerCase();
|
|
78
|
+
|
|
79
|
+
// Handle on/off toggles
|
|
80
|
+
if (value === "on") {
|
|
81
|
+
config.enabled = true;
|
|
82
|
+
ctx.ui.notify("Session Namer: enabled", "info");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (value === "off") {
|
|
86
|
+
config.enabled = false;
|
|
87
|
+
ctx.ui.notify("Session Namer: disabled", "info");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const currentName = pi.getSessionName();
|
|
92
|
+
const lines = [
|
|
93
|
+
`Session Namer: ${config.enabled ? "enabled" : "disabled"}`,
|
|
94
|
+
`Side agent role: ${config.sideAgentRole}`,
|
|
95
|
+
`Max length: ${config.maxLength}`,
|
|
96
|
+
`Current name: ${currentName ?? "(none)"}`,
|
|
97
|
+
`Has auto-named: ${hasNamed}`,
|
|
98
|
+
];
|
|
99
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── /namer:rename — force regenerate ────────────────────────────
|
|
104
|
+
pi.registerCommand("namer:rename", {
|
|
105
|
+
description: "Regenerate session name from the last user prompt",
|
|
106
|
+
handler: async (_args, ctx) => {
|
|
107
|
+
if (!lastPrompt?.trim()) {
|
|
108
|
+
ctx.ui.notify("No user prompt available to generate a name from.", "warning");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let rolesApi;
|
|
113
|
+
try {
|
|
114
|
+
rolesApi = getModelRolesAPI();
|
|
115
|
+
} catch {
|
|
116
|
+
ctx.ui.notify("pi-model-roles not initialized. Cannot rename.", "error");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resolved = await rolesApi.resolveRoleAsync(config.sideAgentRole);
|
|
121
|
+
if (!resolved.model) {
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
`Side agent role "${config.sideAgentRole}" not available. Cannot rename.`,
|
|
124
|
+
"error",
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const name = await generateSessionName(
|
|
130
|
+
resolved.model,
|
|
131
|
+
resolved.apiKey,
|
|
132
|
+
resolved.headers,
|
|
133
|
+
config,
|
|
134
|
+
lastPrompt,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
pi.setSessionName(name);
|
|
138
|
+
ctx.ui.notify(`Session renamed: ${name}`, "info");
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
package/src/namer.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side agent invocation for session naming.
|
|
3
|
+
*
|
|
4
|
+
* Calls the side agent model using pi-ai's complete() function
|
|
5
|
+
* and returns a cleaned session name string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { complete } from "@earendil-works/pi-ai";
|
|
9
|
+
import type { SessionNamerConfig } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the system prompt for the naming side agent.
|
|
13
|
+
*/
|
|
14
|
+
export function buildNamerSystemPrompt(maxLength: number): string {
|
|
15
|
+
return [
|
|
16
|
+
`You are a session naming assistant. Generate a concise title for a coding session based on the user's first message.`,
|
|
17
|
+
``,
|
|
18
|
+
`Rules:`,
|
|
19
|
+
`- Output in the SAME language as the user's message`,
|
|
20
|
+
`- Maximum ${maxLength} characters`,
|
|
21
|
+
`- Output ONLY the title, no quotes, no prefix, no explanation`,
|
|
22
|
+
`- Summarize intent, do not copy the original message verbatim`,
|
|
23
|
+
`- If the message mentions specific files, modules, or functions, keep those names`,
|
|
24
|
+
`- Be specific: "Fix auth token refresh bug" is better than "Fix a bug"`,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Call the side agent to generate a session name.
|
|
30
|
+
*/
|
|
31
|
+
export async function generateSessionName(
|
|
32
|
+
sideModel: any,
|
|
33
|
+
apiKey: string | undefined,
|
|
34
|
+
headers: Record<string, string> | undefined,
|
|
35
|
+
config: SessionNamerConfig,
|
|
36
|
+
userPrompt: string,
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const systemPrompt = buildNamerSystemPrompt(config.maxLength);
|
|
39
|
+
|
|
40
|
+
// Truncate very long prompts to avoid wasting tokens
|
|
41
|
+
const truncatedPrompt = userPrompt.length > 2000
|
|
42
|
+
? userPrompt.slice(0, 2000) + "..."
|
|
43
|
+
: userPrompt;
|
|
44
|
+
|
|
45
|
+
const options: Record<string, any> = {
|
|
46
|
+
maxTokens: 64,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (apiKey) options.apiKey = apiKey;
|
|
50
|
+
if (headers) options.headers = headers;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await complete(sideModel, {
|
|
54
|
+
systemPrompt,
|
|
55
|
+
messages: [{ role: "user", content: truncatedPrompt }],
|
|
56
|
+
}, options);
|
|
57
|
+
|
|
58
|
+
const raw = result.content
|
|
59
|
+
?.filter((block: any) => block.type === "text")
|
|
60
|
+
?.map((block: any) => block.text)
|
|
61
|
+
?.join("")
|
|
62
|
+
?.trim() ?? "";
|
|
63
|
+
|
|
64
|
+
return cleanSessionName(raw, config.maxLength);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn("[pi-session-namer] Side agent call failed:", err);
|
|
67
|
+
// Fallback: truncate user prompt as name
|
|
68
|
+
return userPrompt.slice(0, config.maxLength).replace(/\n/g, " ").trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clean and truncate the generated name.
|
|
74
|
+
*/
|
|
75
|
+
function cleanSessionName(raw: string, maxLength: number): string {
|
|
76
|
+
let name = raw.trim();
|
|
77
|
+
|
|
78
|
+
// Strip surrounding quotes if present
|
|
79
|
+
if (
|
|
80
|
+
(name.startsWith('"') && name.endsWith('"')) ||
|
|
81
|
+
(name.startsWith("'") && name.endsWith("'")) ||
|
|
82
|
+
(name.startsWith("「") && name.endsWith("」"))
|
|
83
|
+
) {
|
|
84
|
+
name = name.slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Remove newlines
|
|
88
|
+
name = name.replace(/\n/g, " ").trim();
|
|
89
|
+
|
|
90
|
+
// Truncate
|
|
91
|
+
if (name.length > maxLength) {
|
|
92
|
+
name = name.slice(0, maxLength - 3) + "...";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return name || "New session";
|
|
96
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for pi-session-namer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Configuration for the session-namer extension, stored in settings.json. */
|
|
6
|
+
export interface SessionNamerConfig {
|
|
7
|
+
/** Whether auto-naming is enabled */
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
/** pi-model-roles role name for the side agent */
|
|
10
|
+
sideAgentRole: string;
|
|
11
|
+
/** Maximum name length (characters) */
|
|
12
|
+
maxLength: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_CONFIG: SessionNamerConfig = {
|
|
16
|
+
enabled: true,
|
|
17
|
+
sideAgentRole: "utility",
|
|
18
|
+
maxLength: 50,
|
|
19
|
+
};
|