@ryan_nookpi/pi-extension-auto-name 0.1.3 → 0.2.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 +27 -0
- package/index.ts +85 -5
- package/package.json +1 -1
- package/utils/settings.test.ts +109 -0
- package/utils/settings.ts +61 -0
- package/utils/short-label.ts +4 -1
package/README.md
CHANGED
|
@@ -16,6 +16,33 @@ pi install npm:@ryan_nookpi/pi-extension-auto-name
|
|
|
16
16
|
- avoiding manual naming with `/name`
|
|
17
17
|
- showing the current task clearly in the terminal title and status area
|
|
18
18
|
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
You can customize the model and reasoning level used for name generation.
|
|
22
|
+
|
|
23
|
+
### `/auto-name:setting`
|
|
24
|
+
|
|
25
|
+
Show current settings:
|
|
26
|
+
```
|
|
27
|
+
/auto-name:setting
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Set a specific model (must be `provider/model-id` format):
|
|
31
|
+
```
|
|
32
|
+
/auto-name:setting model anthropic/claude-sonnet-4-20250514
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Set thinking level (affects name generation quality vs speed):
|
|
36
|
+
```
|
|
37
|
+
/auto-name:setting thinking minimal
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Available thinking levels: `minimal` (default), `low`, `medium`, `high`, `xhigh`.
|
|
41
|
+
|
|
42
|
+
Settings are saved to `~/.pi/agent/auto-name/settings.json` and persist across sessions.
|
|
43
|
+
|
|
44
|
+
If no custom model is set, the extension uses the current session's model.
|
|
45
|
+
|
|
19
46
|
## How it works
|
|
20
47
|
|
|
21
48
|
- It reads the first user message and generates a short session name.
|
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Footer display: shows session name in status bar via setStatus()
|
|
7
7
|
* - Manual control: use built-in /name command (no custom command needed)
|
|
8
8
|
* - Skips auto-detection for subagent sessions
|
|
9
|
+
* - Configurable model / thinking level via /auto-name:setting
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import * as path from "node:path";
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
isSubagentSessionPath,
|
|
19
20
|
NAME_SYSTEM_PROMPT,
|
|
20
21
|
} from "./utils/auto-name-utils.ts";
|
|
22
|
+
import { formatSettings, loadSettings, setSetting, type ThinkingLevel } from "./utils/settings.ts";
|
|
21
23
|
import { generateShortLabel } from "./utils/short-label.js";
|
|
22
24
|
import { NAME_STATUS_KEY } from "./utils/status-keys.ts";
|
|
23
25
|
|
|
@@ -28,12 +30,33 @@ function isSubagentSession(ctx: ExtensionContext): boolean {
|
|
|
28
30
|
return isSubagentSessionPath(sessionFilePath);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function resolveModel(ctx: ExtensionContext) {
|
|
34
|
+
const settings = loadSettings();
|
|
35
|
+
if (settings.modelId) {
|
|
36
|
+
const parts = settings.modelId.split("/");
|
|
37
|
+
if (parts.length === 2) {
|
|
38
|
+
const [provider, modelId] = parts;
|
|
39
|
+
const m = ctx.modelRegistry.find(provider, modelId);
|
|
40
|
+
if (m) return m;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return ctx.model;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
async function detectNameFromMessage(userMessage: string, ctx: ExtensionContext): Promise<string> {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
const settings = loadSettings();
|
|
48
|
+
const model = resolveModel(ctx);
|
|
49
|
+
if (!model) return "";
|
|
50
|
+
|
|
51
|
+
return generateShortLabel(
|
|
52
|
+
{ model, modelRegistry: ctx.modelRegistry },
|
|
53
|
+
{
|
|
54
|
+
systemPrompt: NAME_SYSTEM_PROMPT,
|
|
55
|
+
prompt: buildNameContext(userMessage),
|
|
56
|
+
reasoning: settings.thinkingLevel ?? "minimal",
|
|
57
|
+
extractText: extractNameFromResult,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
// ── Extension ────────────────────────────────────────────────────────────────
|
|
@@ -85,6 +108,63 @@ export default function autoSessionName(pi: ExtensionAPI) {
|
|
|
85
108
|
})();
|
|
86
109
|
});
|
|
87
110
|
|
|
111
|
+
// ── Command: /auto-name:setting ─────────────────────────────
|
|
112
|
+
|
|
113
|
+
pi.registerCommand("auto-name:setting", {
|
|
114
|
+
description: "Configure auto-name model and thinking level. Usage: /auto-name:setting [model|thinking] [value]",
|
|
115
|
+
handler: async (args, ctx) => {
|
|
116
|
+
if (!ctx.hasUI) return;
|
|
117
|
+
const tokens = args.trim().split(/\s+/);
|
|
118
|
+
const subCommand = tokens[0];
|
|
119
|
+
|
|
120
|
+
// 값 없이 입력하면 현재 설정 표시
|
|
121
|
+
if (!subCommand) {
|
|
122
|
+
const settings = loadSettings();
|
|
123
|
+
ctx.ui.notify(formatSettings(settings), "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (subCommand === "model") {
|
|
128
|
+
const modelId = tokens.slice(1).join(" ").trim();
|
|
129
|
+
if (!modelId) {
|
|
130
|
+
ctx.ui.notify(
|
|
131
|
+
"사용법: /auto-name:setting model <provider/model-id> (예: anthropic/claude-sonnet-4-20250514)",
|
|
132
|
+
"warning",
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const parts = modelId.split("/");
|
|
137
|
+
if (parts.length !== 2) {
|
|
138
|
+
ctx.ui.notify('모델 ID는 "provider/model-id" 형식이어야 합니다. (예: openai/gpt-4o)', "warning");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const [provider, id] = parts;
|
|
142
|
+
const m = ctx.modelRegistry.find(provider, id);
|
|
143
|
+
if (!m) {
|
|
144
|
+
ctx.ui.notify(`모델을 찾을 수 없습니다: ${modelId}`, "error");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
setSetting("modelId", modelId);
|
|
148
|
+
ctx.ui.notify(`auto-name 모델이 설정되었습니다: ${m.name} (${modelId})`, "info");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (subCommand === "thinking") {
|
|
153
|
+
const level = tokens[1]?.trim() as ThinkingLevel | undefined;
|
|
154
|
+
const validLevels: ThinkingLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
|
|
155
|
+
if (!level || !validLevels.includes(level)) {
|
|
156
|
+
ctx.ui.notify(`사용법: /auto-name:setting thinking <${validLevels.join("|")}>`, "warning");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setSetting("thinkingLevel", level);
|
|
160
|
+
ctx.ui.notify(`auto-name 추론 레벨이 설정되었습니다: ${level}`, "info");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ctx.ui.notify(`알 수 없는 설정: ${subCommand}. 사용 가능: model, thinking`, "warning");
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
88
168
|
// ── Lifecycle ─────────────────────────────────────────────────
|
|
89
169
|
|
|
90
170
|
pi.on("session_start", async (_event, ctx) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
formatSettings,
|
|
5
|
+
getSetting,
|
|
6
|
+
loadSettings,
|
|
7
|
+
SETTINGS_DIR,
|
|
8
|
+
SETTINGS_FILE,
|
|
9
|
+
saveSettings,
|
|
10
|
+
setSetting,
|
|
11
|
+
} from "./settings.ts";
|
|
12
|
+
|
|
13
|
+
vi.mock("node:fs", () => ({
|
|
14
|
+
mkdirSync: vi.fn(),
|
|
15
|
+
readFileSync: vi.fn(),
|
|
16
|
+
writeFileSync: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
function resetMocks() {
|
|
20
|
+
vi.mocked(fs.readFileSync).mockReset();
|
|
21
|
+
vi.mocked(fs.writeFileSync).mockReset();
|
|
22
|
+
vi.mocked(fs.mkdirSync).mockReset();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("settings", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
resetMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("loads default settings when file does not exist", () => {
|
|
31
|
+
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
32
|
+
throw new Error("ENOENT");
|
|
33
|
+
});
|
|
34
|
+
const s = loadSettings();
|
|
35
|
+
expect(s).toEqual({});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("loads parsed settings from file", () => {
|
|
39
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ modelId: "openai/gpt-4o", thinkingLevel: "high" }));
|
|
40
|
+
const s = loadSettings();
|
|
41
|
+
expect(s.modelId).toBe("openai/gpt-4o");
|
|
42
|
+
expect(s.thinkingLevel).toBe("high");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("loads default on invalid json", () => {
|
|
46
|
+
vi.mocked(fs.readFileSync).mockReturnValue("not-json");
|
|
47
|
+
const s = loadSettings();
|
|
48
|
+
expect(s).toEqual({});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("loads default on non-object parse", () => {
|
|
52
|
+
vi.mocked(fs.readFileSync).mockReturnValue("123");
|
|
53
|
+
const s = loadSettings();
|
|
54
|
+
expect(s).toEqual({});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("saves settings to correct path", () => {
|
|
58
|
+
saveSettings({ modelId: "anthropic/claude-sonnet-4" });
|
|
59
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(SETTINGS_DIR, { recursive: true });
|
|
60
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
61
|
+
SETTINGS_FILE,
|
|
62
|
+
JSON.stringify({ modelId: "anthropic/claude-sonnet-4" }, null, 2),
|
|
63
|
+
"utf-8",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("setSetting adds a new key", () => {
|
|
68
|
+
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
69
|
+
throw new Error("ENOENT");
|
|
70
|
+
});
|
|
71
|
+
setSetting("thinkingLevel", "medium");
|
|
72
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
73
|
+
const written = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls[0][1] as string);
|
|
74
|
+
expect(written.thinkingLevel).toBe("medium");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("setSetting removes key when value is undefined", () => {
|
|
78
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ modelId: "a/b", thinkingLevel: "low" }));
|
|
79
|
+
setSetting("thinkingLevel", undefined);
|
|
80
|
+
const written = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls[0][1] as string);
|
|
81
|
+
expect(written).not.toHaveProperty("thinkingLevel");
|
|
82
|
+
expect(written.modelId).toBe("a/b");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("setSetting updates existing key", () => {
|
|
86
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ thinkingLevel: "low" }));
|
|
87
|
+
setSetting("thinkingLevel", "high");
|
|
88
|
+
const written = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls[0][1] as string);
|
|
89
|
+
expect(written.thinkingLevel).toBe("high");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("formatSettings shows defaults when unset", () => {
|
|
93
|
+
const text = formatSettings({});
|
|
94
|
+
expect(text).toContain("기본 (현재 세션 모델)");
|
|
95
|
+
expect(text).toContain("기본 (minimal)");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("formatSettings shows configured values", () => {
|
|
99
|
+
const text = formatSettings({ modelId: "openai/gpt-4o", thinkingLevel: "high" });
|
|
100
|
+
expect(text).toContain("openai/gpt-4o");
|
|
101
|
+
expect(text).toContain("high");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("getSetting retrieves a specific value", () => {
|
|
105
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ modelId: "openai/gpt-4o", thinkingLevel: "high" }));
|
|
106
|
+
const val = getSetting("thinkingLevel");
|
|
107
|
+
expect(val).toBe("high");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
6
|
+
|
|
7
|
+
export const SETTINGS_DIR = path.join(os.homedir(), ".pi", "agent", "auto-name");
|
|
8
|
+
export const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
|
|
9
|
+
|
|
10
|
+
export interface AutoNameSettings {
|
|
11
|
+
/** Model identifier in "provider/model-id" format */
|
|
12
|
+
modelId?: string;
|
|
13
|
+
/** Thinking/reasoning level for name generation */
|
|
14
|
+
thinkingLevel?: ThinkingLevel;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SETTINGS: AutoNameSettings = {};
|
|
18
|
+
|
|
19
|
+
function loadRaw(): AutoNameSettings {
|
|
20
|
+
try {
|
|
21
|
+
const data = fs.readFileSync(SETTINGS_FILE, "utf-8");
|
|
22
|
+
const parsed = JSON.parse(data) as unknown;
|
|
23
|
+
if (parsed && typeof parsed === "object") {
|
|
24
|
+
return parsed as AutoNameSettings;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// 파일이 없거나 JSON 파싱 실패 시 기본값 사용
|
|
28
|
+
}
|
|
29
|
+
return { ...DEFAULT_SETTINGS };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadSettings(): AutoNameSettings {
|
|
33
|
+
return loadRaw();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveSettings(settings: AutoNameSettings): void {
|
|
37
|
+
fs.mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
38
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getSetting<K extends keyof AutoNameSettings>(key: K): AutoNameSettings[K] {
|
|
42
|
+
return loadRaw()[key];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setSetting<K extends keyof AutoNameSettings>(key: K, value: AutoNameSettings[K]): void {
|
|
46
|
+
const settings = loadRaw();
|
|
47
|
+
if (value === undefined) {
|
|
48
|
+
delete settings[key];
|
|
49
|
+
} else {
|
|
50
|
+
settings[key] = value;
|
|
51
|
+
}
|
|
52
|
+
saveSettings(settings);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 현재 설정값을 사람이 읽기 좋은 문자열로 포맷팅 */
|
|
56
|
+
export function formatSettings(settings: AutoNameSettings): string {
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
lines.push(`모델: ${settings.modelId ?? "기본 (현재 세션 모델)"}`);
|
|
59
|
+
lines.push(`추론 레벨: ${settings.thinkingLevel ?? "기본 (minimal)"}`);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
package/utils/short-label.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { completeSimple } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ThinkingLevel } from "./settings.ts";
|
|
2
3
|
|
|
3
4
|
type SummaryModel = Parameters<typeof completeSimple>[0];
|
|
4
5
|
type SummaryResult = Awaited<ReturnType<typeof completeSimple>>;
|
|
@@ -21,6 +22,8 @@ export type GenerateShortLabelOptions = {
|
|
|
21
22
|
prompt: string;
|
|
22
23
|
maxTokens?: number;
|
|
23
24
|
timeoutMs?: number;
|
|
25
|
+
/** 추론 레벨 (기본값: "minimal") */
|
|
26
|
+
reasoning?: ThinkingLevel;
|
|
24
27
|
extractText?: (content: SummaryResult["content"]) => string;
|
|
25
28
|
};
|
|
26
29
|
|
|
@@ -60,7 +63,7 @@ export async function generateShortLabel(ctx: ShortLabelContext, options: Genera
|
|
|
60
63
|
apiKey: auth.apiKey,
|
|
61
64
|
headers: auth.headers,
|
|
62
65
|
signal: controller.signal,
|
|
63
|
-
reasoning: "minimal",
|
|
66
|
+
reasoning: options.reasoning ?? "minimal",
|
|
64
67
|
maxTokens: options.maxTokens ?? 60,
|
|
65
68
|
},
|
|
66
69
|
);
|