@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-session-namer",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Auto-name pi sessions using a cheap side agent — generates concise titles from the first user message",
6
6
  "main": "src/index.ts",
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
- function merge(target: any, source: any): any {
31
- if (!source || typeof source !== "object") return target;
32
- if (!target || typeof target !== "object") return source;
33
- const result = { ...target };
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 from merged settings.
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 globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
50
- const projectSettings = cwd ? readSettingsFile(path.join(cwd, ".pi", "settings.json")) : {};
51
- const settings = merge(globalSettings, projectSettings);
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
- const resolved = await rolesApi.resolveRoleAsync(config.sideAgentRole);
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
- resolved.model,
70
- resolved.apiKey,
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
- if (!lastPrompt?.trim()) {
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
- const resolved = await rolesApi.resolveRoleAsync(config.sideAgentRole);
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
- resolved.model,
138
- resolved.apiKey,
139
- resolved.headers,
131
+ rolesApi,
132
+ config.sideAgentRole,
140
133
  config,
141
- lastPrompt,
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 using pi-ai's complete() function
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 { complete } from "@earendil-works/pi-ai";
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
- sideModel: any,
33
- apiKey: string | undefined,
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
- sideModel,
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
- options,
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 =