@d3ara1n/pi-session-namer 0.2.0 → 0.2.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/package.json +1 -1
- package/src/config.ts +9 -19
- package/src/index.ts +39 -16
- package/src/namer.ts +9 -15
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -19,7 +19,6 @@ function getAgentDir(): string {
|
|
|
19
19
|
|
|
20
20
|
function readSettingsFile(filePath: string): any {
|
|
21
21
|
try {
|
|
22
|
-
if (!fs.existsSync(filePath)) return {};
|
|
23
22
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
24
23
|
return JSON.parse(content);
|
|
25
24
|
} catch {
|
|
@@ -27,30 +26,21 @@ function readSettingsFile(filePath: string): any {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const key of Object.keys(source)) {
|
|
35
|
-
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
36
|
-
result[key] = merge(result[key], source[key]);
|
|
37
|
-
} else {
|
|
38
|
-
result[key] = source[key];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return result;
|
|
29
|
+
/** Read the `sessionNamer` block from a settings file. */
|
|
30
|
+
function readNamer(filePath: string): Record<string, any> | undefined {
|
|
31
|
+
const raw = readSettingsFile(filePath)?.sessionNamer;
|
|
32
|
+
return raw && typeof raw === "object" ? raw : undefined;
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
/**
|
|
45
|
-
* Load session-namer config
|
|
36
|
+
* Load session-namer config. Project overrides global wholesale; per-field
|
|
37
|
+
* `?? DEFAULT` fills any gap. (No field-level merge — project replaces global.)
|
|
46
38
|
* @param cwd - Project working directory
|
|
47
39
|
*/
|
|
48
40
|
export function loadNamerConfig(cwd?: string): SessionNamerConfig {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const raw = settings?.sessionNamer;
|
|
41
|
+
const globalRaw = readNamer(path.join(getAgentDir(), "settings.json"));
|
|
42
|
+
const projectRaw = cwd ? readNamer(path.join(cwd, ".pi", "settings.json")) : undefined;
|
|
43
|
+
const raw = projectRaw ?? globalRaw;
|
|
54
44
|
if (!raw) return DEFAULT_CONFIG;
|
|
55
45
|
|
|
56
46
|
return {
|
package/src/index.ts
CHANGED
|
@@ -17,14 +17,12 @@ import { generateSessionName } from "./namer.ts";
|
|
|
17
17
|
export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
18
18
|
let config: SessionNamerConfig = DEFAULT_CONFIG;
|
|
19
19
|
let hasNamed = false;
|
|
20
|
-
let lastPrompt = "";
|
|
21
20
|
|
|
22
21
|
// ── session_start: load config, reset flag ──────────────────────
|
|
23
22
|
pi.on("session_start", async (_event, _ctx) => {
|
|
24
23
|
if (!_ctx.hasUI) return;
|
|
25
24
|
config = loadNamerConfig(_ctx.cwd);
|
|
26
25
|
hasNamed = false;
|
|
27
|
-
lastPrompt = "";
|
|
28
26
|
|
|
29
27
|
// If the session already has a name (resume/fork/user-set), don't auto-name
|
|
30
28
|
const existingName = pi.getSessionName();
|
|
@@ -36,8 +34,6 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
36
34
|
// ── before_agent_start: auto-name on first prompt ───────────────
|
|
37
35
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
38
36
|
if (!ctx.hasUI) return;
|
|
39
|
-
// Always cache the latest prompt for /namer:rename, even if auto-naming is done
|
|
40
|
-
lastPrompt = event.prompt;
|
|
41
37
|
|
|
42
38
|
if (!config.enabled || hasNamed) return;
|
|
43
39
|
|
|
@@ -57,8 +53,7 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
57
53
|
return;
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
if (!resolved.model) {
|
|
56
|
+
if (!rolesApi.resolveRole(config.sideAgentRole).model) {
|
|
62
57
|
console.warn(
|
|
63
58
|
`[pi-session-namer] Side agent role "${config.sideAgentRole}" not available — skipping`,
|
|
64
59
|
);
|
|
@@ -66,9 +61,8 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
const name = await generateSessionName(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
resolved.headers,
|
|
64
|
+
rolesApi,
|
|
65
|
+
config.sideAgentRole,
|
|
72
66
|
config,
|
|
73
67
|
event.prompt,
|
|
74
68
|
);
|
|
@@ -111,7 +105,8 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
111
105
|
pi.registerCommand("namer:rename", {
|
|
112
106
|
description: "Regenerate session name from the last user prompt",
|
|
113
107
|
handler: async (_args, ctx) => {
|
|
114
|
-
|
|
108
|
+
const lastUserPrompt = getLastUserPrompt(ctx.sessionManager.getEntries());
|
|
109
|
+
if (!lastUserPrompt?.trim()) {
|
|
115
110
|
ctx.ui.notify("No user prompt available to generate a name from.", "warning");
|
|
116
111
|
return;
|
|
117
112
|
}
|
|
@@ -124,8 +119,7 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
124
119
|
return;
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
if (!resolved.model) {
|
|
122
|
+
if (!rolesApi.resolveRole(config.sideAgentRole).model) {
|
|
129
123
|
ctx.ui.notify(
|
|
130
124
|
`Side agent role "${config.sideAgentRole}" not available. Cannot rename.`,
|
|
131
125
|
"error",
|
|
@@ -134,11 +128,10 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
134
128
|
}
|
|
135
129
|
|
|
136
130
|
const name = await generateSessionName(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
resolved.headers,
|
|
131
|
+
rolesApi,
|
|
132
|
+
config.sideAgentRole,
|
|
140
133
|
config,
|
|
141
|
-
|
|
134
|
+
lastUserPrompt,
|
|
142
135
|
);
|
|
143
136
|
|
|
144
137
|
pi.setSessionName(name);
|
|
@@ -146,3 +139,33 @@ export default function sessionNamerExtension(pi: ExtensionAPI) {
|
|
|
146
139
|
},
|
|
147
140
|
});
|
|
148
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract the most recent user message text from session entries.
|
|
145
|
+
*
|
|
146
|
+
* Read live from the session manager rather than a cached variable, so it
|
|
147
|
+
* survives extension reloads (which reset closure state).
|
|
148
|
+
*/
|
|
149
|
+
function getLastUserPrompt(entries: unknown[]): string | undefined {
|
|
150
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
151
|
+
const entry = entries[i] as any;
|
|
152
|
+
if (entry?.type !== "message") continue;
|
|
153
|
+
const msg = entry.message;
|
|
154
|
+
if (msg?.role !== "user") continue;
|
|
155
|
+
const text = extractEntryText(msg.content);
|
|
156
|
+
if (text.trim()) return text;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Pull text out of a content field that may be a string or a ContentBlock[]. */
|
|
162
|
+
function extractEntryText(content: unknown): string {
|
|
163
|
+
if (typeof content === "string") return content;
|
|
164
|
+
if (Array.isArray(content)) {
|
|
165
|
+
return content
|
|
166
|
+
.filter((b: any) => b?.type === "text" && typeof b.text === "string")
|
|
167
|
+
.map((b: any) => b.text)
|
|
168
|
+
.join("");
|
|
169
|
+
}
|
|
170
|
+
return "";
|
|
171
|
+
}
|
package/src/namer.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Side agent invocation for session naming.
|
|
3
3
|
*
|
|
4
|
-
* Calls the side agent model
|
|
4
|
+
* Calls the side agent via model-roles' complete() (auth resolved internally)
|
|
5
5
|
* and returns a cleaned session name string.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
9
9
|
import type { SessionNamerConfig } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -29,9 +29,8 @@ export function buildNamerSystemPrompt(maxLength: number): string {
|
|
|
29
29
|
* Call the side agent to generate a session name.
|
|
30
30
|
*/
|
|
31
31
|
export async function generateSessionName(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
headers: Record<string, string> | undefined,
|
|
32
|
+
rolesApi: ModelRolesAPI,
|
|
33
|
+
roleName: string,
|
|
35
34
|
config: SessionNamerConfig,
|
|
36
35
|
userPrompt: string,
|
|
37
36
|
): Promise<string> {
|
|
@@ -40,21 +39,16 @@ export async function generateSessionName(
|
|
|
40
39
|
// Truncate very long prompts to avoid wasting tokens
|
|
41
40
|
const truncatedPrompt = userPrompt.length > 2000 ? userPrompt.slice(0, 2000) + "..." : userPrompt;
|
|
42
41
|
|
|
43
|
-
const options: Record<string, any> = {
|
|
44
|
-
maxTokens: 64,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (apiKey) options.apiKey = apiKey;
|
|
48
|
-
if (headers) options.headers = headers;
|
|
49
|
-
|
|
50
42
|
try {
|
|
51
|
-
const result = await complete(
|
|
52
|
-
|
|
43
|
+
const result = await rolesApi.complete(
|
|
44
|
+
roleName,
|
|
53
45
|
{
|
|
54
46
|
systemPrompt,
|
|
55
47
|
messages: [{ role: "user", content: truncatedPrompt, timestamp: Date.now() }],
|
|
56
48
|
},
|
|
57
|
-
|
|
49
|
+
// No maxTokens: cost is controlled by the role's thinking level.
|
|
50
|
+
// The namer side-agent role (utility, thinking:off) skips reasoning,
|
|
51
|
+
// so a short title needs only ~dozens of tokens.
|
|
58
52
|
);
|
|
59
53
|
|
|
60
54
|
const raw =
|