@fyeeme/pi-session-name 1.0.1 → 1.0.3

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +13 -9
  2. package/README.md +125 -46
  3. package/index.ts +8 -6
  4. package/package.json +41 -13
package/CHANGELOG.md CHANGED
@@ -7,26 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.0.1] - 2025-07-15
10
+ ## [1.0.3] - 2026-09-09
11
+
12
+ ### Changed
13
+
14
+ - Project config path uses `CONFIG_DIR_NAME` instead of a hardcoded `.pi`.
15
+ - Title generation forwards `ctx.signal` so aborts cancel the nested model call.
16
+
17
+ ## [1.0.2] - 2025-07-25
11
18
 
12
19
  ### Fixed
13
20
 
14
21
  - Race condition where auto-generated title could overwrite manual rename when `generateTitle` async call completes after user renamed the session
15
22
 
23
+ ## [1.0.1] - 2025-07-25
24
+
16
25
  ### Added
17
26
 
18
- - `/rename [name]` command: manually rename the current session on demand. With
19
- a name argument it sets that name; with no argument it auto-generates one from
20
- the conversation. Invoking it locks out background auto-naming (manual
21
- control), consistent with `/name`.
27
+ - `/rename [name]` command: manually rename the current session on demand. With a name argument it sets that name; with no argument it auto-generates one from the conversation. Invoking it locks out background auto-naming (manual control), consistent with `/name`.
22
28
 
23
29
  ### Changed
24
30
 
25
- - Title-generation prompt now favors descriptive titles (key entity + action +
26
- goal, ~15-40 characters) over terse labels. Previously the prompt emphasized
27
- "short/concise", which produced overly brief session names.
31
+ - Title-generation prompt now favors descriptive titles (key entity + action + goal, ~15-40 characters) over terse labels. Previously the prompt emphasized "short/concise", which produced overly brief session names.
28
32
 
29
- ## [1.0.0] - 2026-07-20
33
+ ## [1.0.0] - 2025-07-20
30
34
 
31
35
  ### Added
32
36
 
package/README.md CHANGED
@@ -1,72 +1,151 @@
1
1
  # pi-session-name
2
2
 
3
- Auto-name your [pi](https://pi.dev) sessions with a short, LLM-generated title so
4
- `pi --resume` lists are easy to scan — instead of the raw first message.
3
+ [![npm version](https://img.shields.io/npm/v/@fyeeme/pi-session-name)](https://www.npmjs.com/package/@fyeeme/pi-session-name)
4
+ [![License](https://img.shields.io/npm/l/@fyeeme/pi-session-name)](LICENSE)
5
5
 
6
- ## Install
6
+ Auto-name [pi](https://pi.dev) sessions with a short LLM-generated title so `--resume` lists are easy to scan — instead of the raw first message.
7
+
8
+ ## Features
9
+
10
+ - **First-mode** (default) — names the session once on first agent response, then leaves it alone
11
+ - **Auto-mode** — re-evaluates each turn; the title tracks the current topic
12
+ - **Never overwrites manual names** — detects `/name`, `--name`, the resume picker's rename, or any other extension calling `setSessionName`, and locks itself for the rest of the session
13
+ - **`/rename [name]`** — rename the current session on demand. With an argument it sets that name; without, it generates one from the conversation
14
+ - **Language-aware** — titles use the same language as your first message
15
+ - **Descriptive titles** — key entity + action + goal (~15-40 chars), not terse labels
16
+ - **Graceful failure** — model unavailable or no API key? Stays silent, never blocks the session
17
+
18
+ ## Prerequisites
19
+
20
+ - [pi](https://pi.dev) >= 0.80.0 (uses `agent_settled` / `session_info_changed` events)
21
+
22
+ ## Installation
23
+
24
+ ### As a pi package (recommended)
7
25
 
8
26
  ```bash
9
27
  # Global (user) install
10
- pi install npm:@fyeeme/pi-session-name
28
+ pi install @fyeeme/pi-session-name
11
29
 
12
30
  # Project-local (.pi/settings.json)
13
- pi install -l npm:@fyeeme/pi-session-name
31
+ pi install -l @fyeeme/pi-session-name
14
32
 
15
33
  # Try once without saving
16
- pi -e npm:@fyeeme/pi-session-name
34
+ pi -e @fyeeme/pi-session-name
17
35
  ```
18
36
 
19
- Requires pi `>= 0.80.0` (uses the `agent_settled` / `session_info_changed` events).
20
-
21
- ## Behavior
22
-
23
- - When the first agent run of a session **settles**, the extension asks the model
24
- for a descriptive title (same language as your first message) — key entity +
25
- action + goal, not a terse label — and writes it via `setSessionName`. It then
26
- shows up in the resume picker immediately.
27
- - **Never overwrites a name you set manually** (`/name`, `--name`, the picker's
28
- rename, or any other extension). Once it detects an external rename it locks
29
- itself for the rest of the session.
30
- - Two modes (config `mode`):
31
- - `first` (default) — name once, then leave it alone.
32
- - `auto` — re-evaluate each turn; the model replies `KEEP` or a new title, so
33
- the name tracks the current topic.
34
- - **`/rename [name]`** — manually rename the current session on demand. With a
35
- name argument it sets that name; with no argument it generates one from the
36
- conversation (same descriptive prompt as auto-naming). Invoking `/rename` is a
37
- manual action, so it locks out background auto-naming for the rest of the
38
- session — you've taken control.
37
+ ### Manual (development)
38
+
39
+ ```bash
40
+ # Clone the repo
41
+ git clone https://github.com/fyeeme/pi-packages.git
42
+ cd pi-packages/packages/extensions/pi-session-name
43
+
44
+ # Symlink to pi extensions
45
+ ln -s "$(pwd)" ~/.pi/agent/extensions/pi-session-name
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ No configuration needed for the default `first` mode. Just install and use pi — your first session will auto-name itself.
51
+
52
+ If you want to change behavior, see [Configuration](#configuration).
53
+
54
+ ### Commands
55
+
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `/rename <name>` | Rename session to `<name>` and lock out auto-naming |
59
+ | `/rename` | Generate a descriptive name from the current conversation |
60
+
61
+ > **Note**: `/rename` is a manual action — once used, auto-naming is locked for the rest of the session. This is consistent with pi's built-in `/name` command.
39
62
 
40
63
  ## Configuration
41
64
 
42
65
  Create `.pi/session-name.json` in your project root:
43
66
 
44
67
  ```json
45
- { "mode": "auto", "maxLength": 200, "model": { "provider": "openai", "id": "gpt-4o-mini" } }
68
+ {
69
+ "mode": "auto",
70
+ "maxLength": 200,
71
+ "model": { "provider": "openai", "id": "gpt-4o-mini" }
72
+ }
46
73
  ```
47
74
 
48
- | Option | Default | Env override | Notes |
49
- |---|---|---|---|
50
- | `mode` | `"first"` | `PI_SESSION_NAME_MODE` | `first` \| `auto` |
51
- | `maxLength` | `200` | `PI_SESSION_NAME_MAX_LENGTH` | Title character cap |
52
- | `enabled` | `true` | `PI_SESSION_NAME_ENABLED=false` | Master switch |
75
+ ### Options
76
+
77
+ | Option | Default | Env override | Description |
78
+ |--------|---------|--------------|-------------|
79
+ | `mode` | `"first"` | `PI_SESSION_NAME_MODE` | `"first"` — name once; `"auto"` — re-evaluate each turn |
80
+ | `maxLength` | `200` | `PI_SESSION_NAME_MAX_LENGTH` | Character cap for generated titles |
81
+ | `enabled` | `true` | `PI_SESSION_NAME_ENABLED=false` | Master switch to disable auto-naming |
53
82
  | `model` | current session model | `PI_SESSION_NAME_MODEL_PROVIDER` + `PI_SESSION_NAME_MODEL_ID` | Override the model used to generate titles |
54
83
 
55
- By default the extension uses the model you're already chatting with (`ctx.model`),
56
- so no extra API key is needed.
84
+ By default the extension uses the model you're already chatting with (`ctx.model`), so no extra API key is needed.
85
+
86
+ ### Environment variables only
87
+
88
+ If you prefer environment variables over a config file:
89
+
90
+ ```bash
91
+ export PI_SESSION_NAME_MODE=auto
92
+ export PI_SESSION_NAME_MAX_LENGTH=150
93
+ export PI_SESSION_NAME_ENABLED=true
94
+ export PI_SESSION_NAME_MODEL_PROVIDER=openai
95
+ export PI_SESSION_NAME_MODEL_ID=gpt-4o-mini
96
+ ```
97
+
98
+ ## How it works
99
+
100
+ 1. On `agent_settled` (after the first turn completes), the extension builds a condensed text of the conversation
101
+ 2. It asks the LLM (same model as the session, unless overridden) to generate a descriptive title
102
+ 3. The title is set via `pi.setSessionName()`, which updates the resume picker immediately
103
+ 4. In `first` mode, it stops there. In `auto` mode, each subsequent turn re-evaluates: the LLM returns either `KEEP` or a new title
104
+ 5. If a manual rename is detected (`session_info_changed` with a name the extension didn't set), auto-naming locks permanently
105
+
106
+ ## Smoke test
107
+
108
+ ```bash
109
+ # 1. Fresh session with auto-naming
110
+ pi -e @fyeeme/pi-session-name
111
+ # Ask a question, wait for reply → resume picker shows a generated title
112
+
113
+ # 2. Manual rename protection
114
+ /name foo
115
+ # Chat a few turns → name stays "foo" (locked)
116
+
117
+ # 3. Auto mode: topic tracking
118
+ # Set mode to "auto", shift topic across turns → name updates; same topic → stays
119
+
120
+ # 4. /rename command
121
+ /rename foo # → name is "foo", locked
122
+ /rename # → generates a fresh name from the conversation
123
+
124
+ # 5. Graceful degradation (unset model's API key)
125
+ # → No errors, session runs normally
126
+ ```
127
+
128
+ ## API (for extension developers)
129
+
130
+ This extension exposes utilities that other extensions can import:
131
+
132
+ ```typescript
133
+ import {
134
+ buildConversationText,
135
+ cleanTitle,
136
+ buildFirstPrompt,
137
+ buildAutoPrompt,
138
+ loadConfig,
139
+ generateTitle,
140
+ } from "@fyeeme/pi-session-name";
141
+ ```
142
+
143
+ See `index.ts` for complete type signatures.
57
144
 
58
- ## Failure handling
145
+ ## Changelog
59
146
 
60
- If the model is unavailable, has no API key, or returns garbage, the extension
61
- stays silent — your session is never blocked. It retries on the next turn while
62
- the session is still unnamed.
147
+ See [CHANGELOG.md](CHANGELOG.md).
63
148
 
64
- ## Manual smoke test
149
+ ## License
65
150
 
66
- 1. `pi -e ./packages/extensions/pi-session-name/index.ts`, ask a question in a
67
- fresh session, wait for the reply → the resume picker shows a generated title.
68
- 2. `/name foo`, chat a few turns → name stays `foo` (locked).
69
- 3. `mode: "auto"`: shift topic across turns → name updates; same topic → stays.
70
- 4. `/rename foo` → name becomes `foo` and stays (locked). `/rename` with no
71
- argument → generates a fresh name from the conversation.
72
- 5. Unset the model's API key → no errors, session runs normally.
151
+ MIT
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import type { Model } from "@earendil-works/pi-ai";
5
5
  import { complete } from "@earendil-works/pi-ai/compat";
6
6
 
@@ -122,7 +122,7 @@ const DEFAULT_CONFIG: SessionNameConfig = { mode: "first", enabled: true, maxLen
122
122
 
123
123
  export function loadConfig(cwd: string, env: Record<string, string | undefined> = process.env): SessionNameConfig {
124
124
  let fileCfg: Partial<SessionNameConfig> = {};
125
- const file = join(cwd, ".pi", "session-name.json");
125
+ const file = join(cwd, CONFIG_DIR_NAME, "session-name.json");
126
126
  try {
127
127
  fileCfg = JSON.parse(readFileSync(file, "utf8")) as Partial<SessionNameConfig>;
128
128
  } catch {
@@ -168,6 +168,7 @@ export async function generateTitle(
168
168
  prompt: string,
169
169
  auth: ModelAuth,
170
170
  completeFn: typeof complete = complete,
171
+ signal?: AbortSignal,
171
172
  ): Promise<string> {
172
173
  const response = await completeFn(
173
174
  auth.model,
@@ -176,7 +177,8 @@ export async function generateTitle(
176
177
  { role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() },
177
178
  ],
178
179
  },
179
- { apiKey: auth.apiKey, headers: auth.headers },
180
+ // ctx.signal (undefined while idle) lets Esc/abort cancel the nested call.
181
+ { apiKey: auth.apiKey, headers: auth.headers, signal },
180
182
  );
181
183
  return response.content
182
184
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -224,9 +226,9 @@ export default function (pi: ExtensionAPI): void {
224
226
 
225
227
  let title: string | null;
226
228
  if (cfg.mode === "first" || !currentName) {
227
- title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth), cfg.maxLength);
229
+ title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth, complete, ctx.signal), cfg.maxLength);
228
230
  } else {
229
- const verdict = await generateTitle(buildAutoPrompt(currentName, text, cfg), auth);
231
+ const verdict = await generateTitle(buildAutoPrompt(currentName, text, cfg), auth, complete, ctx.signal);
230
232
  title = /^keep$/i.test(verdict.trim()) ? null : cleanTitle(verdict, cfg.maxLength);
231
233
  }
232
234
  if (!title) return;
@@ -276,7 +278,7 @@ export default function (pi: ExtensionAPI): void {
276
278
  return;
277
279
  }
278
280
  try {
279
- const title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth), cfg.maxLength);
281
+ const title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth, complete, ctx.signal), cfg.maxLength);
280
282
  if (!title) {
281
283
  ctx.ui.notify("Could not generate a name from the model response", "warning");
282
284
  return;
package/package.json CHANGED
@@ -1,26 +1,54 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-session-name",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Auto-name pi sessions with a short LLM-generated title so --resume lists are easy to scan. Supports first (once) and auto (content-aware rename) modes; never overwrites manual names.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "fyeeme",
8
- "publishConfig": { "access": "public" },
9
- "repository": { "type": "git", "url": "https://github.com/fyeeme/pi-packages" },
10
- "bugs": { "url": "https://github.com/fyeeme/pi-packages/issues" },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fyeeme/pi-packages.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/fyeeme/pi-packages/issues"
17
+ },
11
18
  "homepage": "https://github.com/fyeeme/pi-packages#readme",
12
- "engines": { "node": ">=18" },
13
- "keywords": ["pi-package", "pi", "session", "auto-name", "rename", "resume"],
14
- "files": ["index.ts", "README.md", "LICENSE", "CHANGELOG.md"],
15
- "pi": { "extensions": ["./index.ts"] },
16
- "scripts": { "test": "vitest --run", "typecheck": "tsc" },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi",
25
+ "session",
26
+ "auto-name",
27
+ "rename",
28
+ "resume"
29
+ ],
30
+ "files": [
31
+ "index.ts",
32
+ "README.md",
33
+ "LICENSE",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "pi": {
37
+ "extensions": [
38
+ "./index.ts"
39
+ ]
40
+ },
41
+ "scripts": {
42
+ "test": "vitest --run",
43
+ "typecheck": "tsc"
44
+ },
17
45
  "peerDependencies": {
18
- "@earendil-works/pi-coding-agent": ">=0.80.0",
19
- "@earendil-works/pi-ai": ">=0.80.0"
46
+ "@earendil-works/pi-coding-agent": ">=0.84.1",
47
+ "@earendil-works/pi-ai": ">=0.84.1"
20
48
  },
21
49
  "devDependencies": {
22
- "@earendil-works/pi-coding-agent": "0.80.10",
23
- "@earendil-works/pi-ai": "0.80.10",
50
+ "@earendil-works/pi-coding-agent": "0.84.1",
51
+ "@earendil-works/pi-ai": "0.84.1",
24
52
  "@types/node": "22.19.19",
25
53
  "typescript": "5.9.3",
26
54
  "vitest": "3.2.7"