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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from '@mariozechner/pi-coding-agent';
|
|
2
5
|
import {
|
|
3
6
|
Container,
|
|
4
7
|
SettingsList,
|
|
@@ -136,6 +139,113 @@ export function create_extensions_extension(
|
|
|
136
139
|
options.force_disabled,
|
|
137
140
|
);
|
|
138
141
|
|
|
142
|
+
async function show_manager(
|
|
143
|
+
ctx: ExtensionCommandContext,
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
if (!ctx.hasUI) return false;
|
|
146
|
+
|
|
147
|
+
const states = resolve_builtin_extension_states(force_disabled);
|
|
148
|
+
const initial_enabled = new Set(
|
|
149
|
+
states
|
|
150
|
+
.filter((state) => state.saved_enabled)
|
|
151
|
+
.map((state) => state.key),
|
|
152
|
+
);
|
|
153
|
+
const current_enabled = new Set(initial_enabled);
|
|
154
|
+
|
|
155
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
156
|
+
const items = states.map(to_setting_item);
|
|
157
|
+
const container = new Container();
|
|
158
|
+
|
|
159
|
+
container.addChild({
|
|
160
|
+
render: () => {
|
|
161
|
+
const saved_enabled = current_enabled.size;
|
|
162
|
+
const saved_disabled = states.length - saved_enabled;
|
|
163
|
+
const enabled_now = [...current_enabled].filter(
|
|
164
|
+
(key) => !force_disabled.has(key as BuiltinExtensionKey),
|
|
165
|
+
).length;
|
|
166
|
+
const disabled_now = states.length - enabled_now;
|
|
167
|
+
return [
|
|
168
|
+
theme.fg('accent', theme.bold('Built-in extensions')),
|
|
169
|
+
theme.fg(
|
|
170
|
+
'muted',
|
|
171
|
+
`${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`,
|
|
172
|
+
),
|
|
173
|
+
'',
|
|
174
|
+
];
|
|
175
|
+
},
|
|
176
|
+
invalidate: () => {},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const settings_list = new SettingsList(
|
|
180
|
+
items,
|
|
181
|
+
Math.min(Math.max(items.length + 4, 8), 16),
|
|
182
|
+
{
|
|
183
|
+
cursor: theme.fg('accent', '›'),
|
|
184
|
+
label: (text, selected) =>
|
|
185
|
+
selected ? theme.fg('accent', text) : text,
|
|
186
|
+
value: (text, selected) => {
|
|
187
|
+
const color = text === ENABLED ? 'success' : 'dim';
|
|
188
|
+
const rendered = theme.fg(color, text);
|
|
189
|
+
return selected
|
|
190
|
+
? theme.bold(theme.fg('accent', rendered))
|
|
191
|
+
: rendered;
|
|
192
|
+
},
|
|
193
|
+
description: (text) => theme.fg('muted', text),
|
|
194
|
+
hint: (text) => theme.fg('dim', text),
|
|
195
|
+
},
|
|
196
|
+
(id, new_value) => {
|
|
197
|
+
const key = id as BuiltinExtensionKey;
|
|
198
|
+
const enabled = new_value === ENABLED;
|
|
199
|
+
if (enabled) {
|
|
200
|
+
current_enabled.add(key);
|
|
201
|
+
} else {
|
|
202
|
+
current_enabled.delete(key);
|
|
203
|
+
}
|
|
204
|
+
save_extension_enabled(key, enabled);
|
|
205
|
+
},
|
|
206
|
+
() => done(undefined),
|
|
207
|
+
{ enableSearch: true },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
container.addChild(settings_list);
|
|
211
|
+
container.addChild(
|
|
212
|
+
new Text(
|
|
213
|
+
theme.fg(
|
|
214
|
+
'dim',
|
|
215
|
+
'esc close • search filters • changes save immediately • CLI --no-* flags still win in this process',
|
|
216
|
+
),
|
|
217
|
+
0,
|
|
218
|
+
1,
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
render(width: number) {
|
|
224
|
+
return container.render(width);
|
|
225
|
+
},
|
|
226
|
+
invalidate() {
|
|
227
|
+
container.invalidate();
|
|
228
|
+
},
|
|
229
|
+
handleInput(data: string) {
|
|
230
|
+
settings_list.handleInput(data);
|
|
231
|
+
tui.requestRender();
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!sets_equal(initial_enabled, current_enabled)) {
|
|
237
|
+
ctx.ui.notify(
|
|
238
|
+
force_disabled.size > 0
|
|
239
|
+
? 'Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process.'
|
|
240
|
+
: 'Reloading to apply updated built-in extensions...',
|
|
241
|
+
'info',
|
|
242
|
+
);
|
|
243
|
+
await ctx.reload();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
139
249
|
return async function extensions(pi: ExtensionAPI) {
|
|
140
250
|
const subs = ['list', 'enable', 'disable', 'toggle', 'search'];
|
|
141
251
|
|
|
@@ -169,113 +279,8 @@ export function create_extensions_extension(
|
|
|
169
279
|
handler: async (args, ctx) => {
|
|
170
280
|
const trimmed = args.trim();
|
|
171
281
|
|
|
172
|
-
if (!trimmed
|
|
173
|
-
|
|
174
|
-
resolve_builtin_extension_states(force_disabled);
|
|
175
|
-
const initial_enabled = new Set(
|
|
176
|
-
states
|
|
177
|
-
.filter((state) => state.saved_enabled)
|
|
178
|
-
.map((state) => state.key),
|
|
179
|
-
);
|
|
180
|
-
const current_enabled = new Set(initial_enabled);
|
|
181
|
-
|
|
182
|
-
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
183
|
-
const items = states.map(to_setting_item);
|
|
184
|
-
const container = new Container();
|
|
185
|
-
|
|
186
|
-
container.addChild({
|
|
187
|
-
render: () => {
|
|
188
|
-
const saved_enabled = current_enabled.size;
|
|
189
|
-
const saved_disabled = states.length - saved_enabled;
|
|
190
|
-
const enabled_now = [...current_enabled].filter(
|
|
191
|
-
(key) =>
|
|
192
|
-
!force_disabled.has(key as BuiltinExtensionKey),
|
|
193
|
-
).length;
|
|
194
|
-
const disabled_now = states.length - enabled_now;
|
|
195
|
-
return [
|
|
196
|
-
theme.fg(
|
|
197
|
-
'accent',
|
|
198
|
-
theme.bold('Built-in extensions'),
|
|
199
|
-
),
|
|
200
|
-
theme.fg(
|
|
201
|
-
'muted',
|
|
202
|
-
`${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`,
|
|
203
|
-
),
|
|
204
|
-
'',
|
|
205
|
-
];
|
|
206
|
-
},
|
|
207
|
-
invalidate: () => {},
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const settings_list = new SettingsList(
|
|
211
|
-
items,
|
|
212
|
-
Math.min(Math.max(items.length + 4, 8), 16),
|
|
213
|
-
{
|
|
214
|
-
cursor: theme.fg('accent', '›'),
|
|
215
|
-
label: (text, selected) =>
|
|
216
|
-
selected ? theme.fg('accent', text) : text,
|
|
217
|
-
value: (text, selected) => {
|
|
218
|
-
const color = text === ENABLED ? 'success' : 'dim';
|
|
219
|
-
const rendered = theme.fg(color, text);
|
|
220
|
-
return selected
|
|
221
|
-
? theme.bold(theme.fg('accent', rendered))
|
|
222
|
-
: rendered;
|
|
223
|
-
},
|
|
224
|
-
description: (text) => theme.fg('muted', text),
|
|
225
|
-
hint: (text) => theme.fg('dim', text),
|
|
226
|
-
},
|
|
227
|
-
(id, new_value) => {
|
|
228
|
-
const key = id as BuiltinExtensionKey;
|
|
229
|
-
const enabled = new_value === ENABLED;
|
|
230
|
-
if (enabled) {
|
|
231
|
-
current_enabled.add(key);
|
|
232
|
-
} else {
|
|
233
|
-
current_enabled.delete(key);
|
|
234
|
-
}
|
|
235
|
-
save_extension_enabled(key, enabled);
|
|
236
|
-
},
|
|
237
|
-
() => done(undefined),
|
|
238
|
-
{ enableSearch: true },
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
container.addChild(settings_list);
|
|
242
|
-
container.addChild(
|
|
243
|
-
new Text(
|
|
244
|
-
theme.fg(
|
|
245
|
-
'dim',
|
|
246
|
-
'esc close • search filters • changes save immediately • CLI --no-* flags still win in this process',
|
|
247
|
-
),
|
|
248
|
-
0,
|
|
249
|
-
1,
|
|
250
|
-
),
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
render(width: number) {
|
|
255
|
-
return container.render(width);
|
|
256
|
-
},
|
|
257
|
-
invalidate() {
|
|
258
|
-
container.invalidate();
|
|
259
|
-
},
|
|
260
|
-
handleInput(data: string) {
|
|
261
|
-
settings_list.handleInput(data);
|
|
262
|
-
tui.requestRender();
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
if (!sets_equal(initial_enabled, current_enabled)) {
|
|
268
|
-
ctx.ui.notify(
|
|
269
|
-
force_disabled.size > 0
|
|
270
|
-
? 'Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process.'
|
|
271
|
-
: 'Reloading to apply updated built-in extensions...',
|
|
272
|
-
'info',
|
|
273
|
-
);
|
|
274
|
-
await ctx.reload();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return;
|
|
282
|
+
if (!trimmed) {
|
|
283
|
+
if (await show_manager(ctx)) return;
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
const [sub, ...rest] = (trimmed || 'list').split(/\s+/);
|
|
@@ -296,6 +301,7 @@ export function create_extensions_extension(
|
|
|
296
301
|
case 'disable':
|
|
297
302
|
case 'toggle': {
|
|
298
303
|
if (!arg) {
|
|
304
|
+
if (await show_manager(ctx)) return;
|
|
299
305
|
ctx.ui.notify(
|
|
300
306
|
`Usage: /extensions ${sub} <key>`,
|
|
301
307
|
'warning',
|
|
@@ -1,87 +1,173 @@
|
|
|
1
|
-
// Handoff extension —
|
|
2
|
-
// Inspired by jayshah5696/pi-agent-extensions
|
|
1
|
+
// Handoff extension — generate a focused prompt for a new session
|
|
3
2
|
|
|
4
|
-
import type
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { complete, type Message } from '@mariozechner/pi-ai';
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
SessionEntry,
|
|
7
|
+
} from '@mariozechner/pi-coding-agent';
|
|
8
|
+
import {
|
|
9
|
+
BorderedLoader,
|
|
10
|
+
convertToLlm,
|
|
11
|
+
serializeConversation,
|
|
12
|
+
} from '@mariozechner/pi-coding-agent';
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.join('\n');
|
|
30
|
-
|
|
31
|
-
if (!text) return;
|
|
32
|
-
|
|
33
|
-
const summary =
|
|
34
|
-
text.length > 200 ? text.slice(0, 200) + '...' : text;
|
|
35
|
-
|
|
36
|
-
history.push({
|
|
37
|
-
role: (msg.role as string) || 'unknown',
|
|
38
|
-
summary,
|
|
39
|
-
timestamp: Date.now(),
|
|
40
|
-
});
|
|
41
|
-
});
|
|
14
|
+
const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
|
|
15
|
+
|
|
16
|
+
1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
|
|
17
|
+
2. Lists any relevant files that were discussed or modified
|
|
18
|
+
3. Clearly states the next task based on the user's goal
|
|
19
|
+
4. Is self-contained - the new thread should be able to proceed without the old conversation
|
|
20
|
+
|
|
21
|
+
Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
|
|
22
|
+
|
|
23
|
+
Example output format:
|
|
24
|
+
## Context
|
|
25
|
+
We've been working on X. Key decisions:
|
|
26
|
+
- Decision 1
|
|
27
|
+
- Decision 2
|
|
28
|
+
|
|
29
|
+
Files involved:
|
|
30
|
+
- path/to/file1.ts
|
|
31
|
+
- path/to/file2.ts
|
|
32
|
+
|
|
33
|
+
## Task
|
|
34
|
+
[Clear description of what to do next based on user's goal]`;
|
|
42
35
|
|
|
36
|
+
export default async function handoff(pi: ExtensionAPI) {
|
|
43
37
|
pi.registerCommand('handoff', {
|
|
44
38
|
description:
|
|
45
|
-
'
|
|
39
|
+
'Transfer context to a new focused session with an AI-generated prompt',
|
|
46
40
|
handler: async (args, ctx) => {
|
|
47
|
-
|
|
41
|
+
if (!ctx.hasUI) {
|
|
42
|
+
ctx.ui.notify('handoff requires interactive mode', 'error');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!ctx.model) {
|
|
47
|
+
ctx.ui.notify('No model selected', 'error');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
const goal = args.trim();
|
|
52
|
+
if (!goal) {
|
|
50
53
|
ctx.ui.notify(
|
|
51
|
-
'
|
|
52
|
-
'
|
|
54
|
+
'Usage: /handoff <goal for new thread>',
|
|
55
|
+
'error',
|
|
53
56
|
);
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
+
const branch = ctx.sessionManager.getBranch();
|
|
61
|
+
const messages = branch
|
|
62
|
+
.filter(
|
|
63
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
64
|
+
entry.type === 'message',
|
|
65
|
+
)
|
|
66
|
+
.map((entry) => entry.message);
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if (messages.length === 0) {
|
|
69
|
+
ctx.ui.notify('No conversation to hand off', 'error');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
const llm_messages = convertToLlm(messages);
|
|
74
|
+
const conversation_text = serializeConversation(llm_messages);
|
|
75
|
+
const current_session_file =
|
|
76
|
+
ctx.sessionManager.getSessionFile();
|
|
77
|
+
const model = ctx.model;
|
|
78
|
+
|
|
79
|
+
const result = await ctx.ui.custom<string | null>(
|
|
80
|
+
(tui, theme, _kb, done) => {
|
|
81
|
+
const loader = new BorderedLoader(
|
|
82
|
+
tui,
|
|
83
|
+
theme,
|
|
84
|
+
'Generating handoff prompt...',
|
|
85
|
+
);
|
|
86
|
+
loader.onAbort = () => done(null);
|
|
87
|
+
|
|
88
|
+
const generate = async () => {
|
|
89
|
+
const auth =
|
|
90
|
+
await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
91
|
+
if (!auth.ok || !auth.apiKey) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
auth.ok
|
|
94
|
+
? `No API key for ${model.provider}`
|
|
95
|
+
: auth.error,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const user_message: Message = {
|
|
100
|
+
role: 'user',
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const response = await complete(
|
|
111
|
+
model,
|
|
112
|
+
{
|
|
113
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
114
|
+
messages: [user_message],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
apiKey: auth.apiKey,
|
|
118
|
+
headers: auth.headers,
|
|
119
|
+
signal: loader.signal,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (response.stopReason === 'aborted') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response.content
|
|
128
|
+
.filter(
|
|
129
|
+
(c): c is { type: 'text'; text: string } =>
|
|
130
|
+
c.type === 'text',
|
|
131
|
+
)
|
|
132
|
+
.map((c) => c.text)
|
|
133
|
+
.join('\n');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
generate()
|
|
137
|
+
.then(done)
|
|
138
|
+
.catch((err) => {
|
|
139
|
+
console.error('Handoff generation failed:', err);
|
|
140
|
+
done(null);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return loader;
|
|
144
|
+
},
|
|
145
|
+
);
|
|
67
146
|
|
|
68
|
-
|
|
69
|
-
|
|
147
|
+
if (result === null) {
|
|
148
|
+
ctx.ui.notify('Cancelled', 'info');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
70
151
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
const edited_prompt = await ctx.ui.editor(
|
|
153
|
+
'Edit handoff prompt',
|
|
154
|
+
result,
|
|
155
|
+
);
|
|
156
|
+
if (edited_prompt === undefined) {
|
|
157
|
+
ctx.ui.notify('Cancelled', 'info');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
76
160
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
161
|
+
const new_session_result = await ctx.newSession({
|
|
162
|
+
parentSession: current_session_file,
|
|
163
|
+
});
|
|
164
|
+
if (new_session_result.cancelled) {
|
|
165
|
+
ctx.ui.notify('New session cancelled', 'info');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
81
168
|
|
|
82
|
-
ctx.ui.
|
|
83
|
-
|
|
84
|
-
);
|
|
169
|
+
ctx.ui.setEditorText(edited_prompt);
|
|
170
|
+
ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
|
|
85
171
|
},
|
|
86
172
|
});
|
|
87
173
|
}
|