my-pi 0.0.13 → 0.1.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/README.md +2 -0
- package/dist/{api-CWEizv2k.js → api-Dxi4curf.js} +639 -102
- package/dist/api-Dxi4curf.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/config.test.ts +9 -0
- package/src/extensions/config.ts +30 -2
- package/src/extensions/confirm-destructive.test.ts +157 -0
- package/src/extensions/confirm-destructive.ts +61 -0
- package/src/extensions/extensions.test.ts +114 -0
- package/src/extensions/extensions.ts +114 -108
- package/src/extensions/handoff.ts +152 -66
- package/src/extensions/hooks-resolution.test.ts +246 -0
- package/src/extensions/hooks-resolution.ts +584 -0
- package/src/extensions/session-name.ts +234 -0
- package/dist/api-CWEizv2k.js.map +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Session name — AI-powered session naming
|
|
2
|
+
// Adapted from Thomas Lopes' pi dotfiles
|
|
3
|
+
|
|
4
|
+
import { complete, type Message } from '@mariozechner/pi-ai';
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
SessionEntry,
|
|
8
|
+
} from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import {
|
|
10
|
+
BorderedLoader,
|
|
11
|
+
convertToLlm,
|
|
12
|
+
serializeConversation,
|
|
13
|
+
} from '@mariozechner/pi-coding-agent';
|
|
14
|
+
|
|
15
|
+
const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task.
|
|
16
|
+
|
|
17
|
+
Guidelines:
|
|
18
|
+
- Be concise but specific
|
|
19
|
+
- Use kebab-case or natural language
|
|
20
|
+
- Focus on the core task/question
|
|
21
|
+
- Avoid generic names like "discussion" or "conversation"
|
|
22
|
+
- No quotes, no punctuation at the end
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- "fix auth bug" -> "fix-auth-bug" or "authentication fix"
|
|
26
|
+
- "how do I deploy to vercel" -> "vercel deployment"
|
|
27
|
+
- "explain react hooks" -> "react hooks explanation"
|
|
28
|
+
- "optimize database queries" -> "db query optimization"
|
|
29
|
+
|
|
30
|
+
Output ONLY the session name, nothing else.`;
|
|
31
|
+
|
|
32
|
+
const AUTO_NAME_THRESHOLD = 1;
|
|
33
|
+
const MAX_CHARS = 4000;
|
|
34
|
+
const MAX_NAME_LEN = 50;
|
|
35
|
+
|
|
36
|
+
function clean_name(value: string): string {
|
|
37
|
+
return value
|
|
38
|
+
.replace(/^["']|["']$/g, '')
|
|
39
|
+
.replace(/\n/g, ' ')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim()
|
|
42
|
+
.slice(0, MAX_NAME_LEN);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncate_conversation(value: string): string {
|
|
46
|
+
return value.length > MAX_CHARS
|
|
47
|
+
? value.slice(0, MAX_CHARS) + '\n...'
|
|
48
|
+
: value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function generate_session_name(
|
|
52
|
+
ctx: {
|
|
53
|
+
modelRegistry: {
|
|
54
|
+
getApiKeyAndHeaders: (
|
|
55
|
+
model: NonNullable<
|
|
56
|
+
Parameters<
|
|
57
|
+
Parameters<ExtensionAPI['registerCommand']>[1]['handler']
|
|
58
|
+
>[1]['model']
|
|
59
|
+
>,
|
|
60
|
+
) => Promise<any>;
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
model: NonNullable<
|
|
64
|
+
Parameters<
|
|
65
|
+
Parameters<ExtensionAPI['registerCommand']>[1]['handler']
|
|
66
|
+
>[1]['model']
|
|
67
|
+
>,
|
|
68
|
+
conversation_text: string,
|
|
69
|
+
signal?: AbortSignal,
|
|
70
|
+
): Promise<string | null> {
|
|
71
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
72
|
+
if (!auth.ok || !auth.apiKey) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
auth.ok ? `No API key for ${model.provider}` : auth.error,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user_message: Message = {
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const response = await complete(
|
|
90
|
+
model,
|
|
91
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: [user_message] },
|
|
92
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (response.stopReason === 'aborted') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return clean_name(
|
|
100
|
+
response.content
|
|
101
|
+
.filter(
|
|
102
|
+
(c): c is { type: 'text'; text: string } => c.type === 'text',
|
|
103
|
+
)
|
|
104
|
+
.map((c) => c.text.trim())
|
|
105
|
+
.join(' '),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default async function session_name(pi: ExtensionAPI) {
|
|
110
|
+
let auto_named_attempted = false;
|
|
111
|
+
|
|
112
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
113
|
+
if (!ctx.hasUI || !ctx.model) return;
|
|
114
|
+
if (pi.getSessionName() || auto_named_attempted) return;
|
|
115
|
+
|
|
116
|
+
const branch = ctx.sessionManager.getBranch();
|
|
117
|
+
const user_messages = branch.filter(
|
|
118
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
119
|
+
entry.type === 'message' && entry.message.role === 'user',
|
|
120
|
+
);
|
|
121
|
+
if (user_messages.length < AUTO_NAME_THRESHOLD) return;
|
|
122
|
+
|
|
123
|
+
auto_named_attempted = true;
|
|
124
|
+
const messages = branch
|
|
125
|
+
.filter(
|
|
126
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
127
|
+
entry.type === 'message',
|
|
128
|
+
)
|
|
129
|
+
.map((entry) => entry.message);
|
|
130
|
+
if (messages.length === 0) return;
|
|
131
|
+
|
|
132
|
+
const conversation_text = serializeConversation(
|
|
133
|
+
convertToLlm(messages),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
generate_session_name(ctx, ctx.model, conversation_text)
|
|
137
|
+
.then((name) => {
|
|
138
|
+
if (!name) return;
|
|
139
|
+
pi.setSessionName(name);
|
|
140
|
+
ctx.ui.notify(`Auto-named: ${name}`, 'info');
|
|
141
|
+
})
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
console.error('Auto-naming failed:', err);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
pi.on('session_start', async () => {
|
|
148
|
+
auto_named_attempted = false;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.registerCommand('session-name', {
|
|
152
|
+
description:
|
|
153
|
+
'Set, show, or auto-generate the current session name',
|
|
154
|
+
handler: async (args, ctx) => {
|
|
155
|
+
const trimmed = args.trim();
|
|
156
|
+
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
const current = pi.getSessionName();
|
|
159
|
+
ctx.ui.notify(
|
|
160
|
+
current ? `Session: ${current}` : 'No session name set',
|
|
161
|
+
'info',
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (trimmed === '--auto' || trimmed === '-a') {
|
|
167
|
+
if (!ctx.hasUI || !ctx.model) {
|
|
168
|
+
ctx.ui.notify(
|
|
169
|
+
'Auto-naming requires interactive mode and a selected model',
|
|
170
|
+
'error',
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const branch = ctx.sessionManager.getBranch();
|
|
176
|
+
const messages = branch
|
|
177
|
+
.filter(
|
|
178
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
179
|
+
entry.type === 'message',
|
|
180
|
+
)
|
|
181
|
+
.map((entry) => entry.message);
|
|
182
|
+
if (messages.length === 0) {
|
|
183
|
+
ctx.ui.notify('No conversation to analyze', 'error');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const conversation_text = serializeConversation(
|
|
188
|
+
convertToLlm(messages),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const result = await ctx.ui.custom<string | null>(
|
|
192
|
+
(tui, theme, _kb, done) => {
|
|
193
|
+
const loader = new BorderedLoader(
|
|
194
|
+
tui,
|
|
195
|
+
theme,
|
|
196
|
+
'Generating session name...',
|
|
197
|
+
);
|
|
198
|
+
loader.onAbort = () => done(null);
|
|
199
|
+
|
|
200
|
+
generate_session_name(
|
|
201
|
+
ctx,
|
|
202
|
+
ctx.model!,
|
|
203
|
+
conversation_text,
|
|
204
|
+
loader.signal,
|
|
205
|
+
)
|
|
206
|
+
.then(done)
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
console.error('Auto-naming failed:', err);
|
|
209
|
+
done(null);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return loader;
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (result === null) {
|
|
217
|
+
ctx.ui.notify('Auto-naming cancelled', 'info');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!result) {
|
|
221
|
+
ctx.ui.notify('Failed to generate name', 'error');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pi.setSessionName(result);
|
|
226
|
+
ctx.ui.notify(`Session named: ${result}`, 'info');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pi.setSessionName(clean_name(trimmed));
|
|
231
|
+
ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, 'info');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|