pi-edit-session-in-place 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitch Fultz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # pi edit-session-in-place
2
+
3
+ A [pi](https://github.com/badlogic/pi-mono) extension that lets you rewind to an earlier user message in the current branch, then either **edit it in place** or **delete it and continue from there**.
4
+
5
+ ## What it does
6
+
7
+ - Adds `/edit-turn`
8
+ - Adds a global hotkey: `Ctrl+Shift+E`
9
+ - Lets you choose an earlier user message from the current branch
10
+ - Rewinds pi to that point in the same session file
11
+ - Loads your edited text back into the main editor
12
+ - Treats an empty submit as **delete this message and continue from here**
13
+
14
+ ## Install
15
+
16
+ Install from npm with pi:
17
+
18
+ ```bash
19
+ pi install npm:pi-edit-session-in-place
20
+ ```
21
+
22
+ Or install directly from GitHub with pi:
23
+
24
+ ```bash
25
+ pi install https://github.com/fitchmultz/pi-edit-session-in-place
26
+ ```
27
+
28
+ Then reload pi from inside the app with:
29
+
30
+ ```text
31
+ /reload
32
+ ```
33
+
34
+ For local development you can point pi at the extension directly:
35
+
36
+ ```bash
37
+ pi -e ./extensions/edit-session-in-place.ts
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ Inside pi:
43
+
44
+ ```text
45
+ /edit-turn
46
+ ```
47
+
48
+ Or press:
49
+
50
+ ```text
51
+ Ctrl+Shift+E
52
+ ```
53
+
54
+ ### Message picker behavior
55
+
56
+ - Shows earlier user messages from the current branch only
57
+ - Uses a viewport so long threads stay navigable
58
+ - Orders messages **oldest → newest**
59
+ - Starts with the **newest** message selected at the bottom
60
+ - `↑` moves to older messages, `↓` moves to newer ones
61
+ - `PageUp` / `PageDown` jump faster
62
+
63
+ ### Editor behavior
64
+
65
+ - `Ctrl+X` clears the entire selected message instantly
66
+ - `Enter` submits the edited message
67
+ - `Shift+Enter` inserts a newline
68
+ - `Escape` cancels without changing history
69
+ - `Ctrl+G` opens your external editor if `$VISUAL` or `$EDITOR` is set
70
+
71
+ If you clear the message and submit an empty value, the selected message is effectively deleted: pi rewinds to just before that message and leaves the main editor empty so you can type a new prompt.
72
+
73
+ ## Behavior notes
74
+
75
+ - Works in interactive mode; non-interactive modes do not show the picker/editor UI
76
+ - Later messages on the abandoned branch are not deleted from the session file; they remain reachable through `/tree`
77
+ - If the selected message contains images, the extension warns that re-editing or deleting it will drop the images and keep only text behavior
78
+ - The extension only offers text-bearing user messages for editing; image-only or whitespace-only user messages are skipped
79
+ - Queued messages must be cleared before using the command
80
+
81
+ ## Development
82
+
83
+ This repo includes a small regression test in:
84
+
85
+ - `tests/edit-session-in-place.test.ts`
86
+
87
+ Checks:
88
+
89
+ - message extraction from mixed session content
90
+ - oldest-to-newest ordering for the picker
91
+ - skipping image-only and whitespace-only user messages
92
+ - preserving the image-warning flag for mixed text+image messages
93
+
94
+ ## Files
95
+
96
+ - `extensions/edit-session-in-place.ts` — publishable extension implementation
97
+ - `tests/edit-session-in-place.test.ts` — regression test for message extraction and ordering
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Purpose: Let the user rewind to and re-edit or delete an earlier user message in the current session branch.
3
+ * Responsibilities: Provide a selector UI, a fast clear-all edit UI, and tree navigation that rewinds to the selected point.
4
+ * Scope: Single publishable pi extension plus pure helpers exported for regression tests.
5
+ * Usage: Install as a pi package and invoke with /edit-turn or Ctrl+Shift+E.
6
+ * Invariants/Assumptions: Operates on the current branch only; later branch history remains in /tree; empty submit means delete.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ import {
14
+ CustomEditor,
15
+ DynamicBorder,
16
+ keyHint,
17
+ rawKeyHint,
18
+ type ExtensionAPI,
19
+ type ExtensionCommandContext,
20
+ type KeybindingsManager,
21
+ type SessionEntry,
22
+ type Theme,
23
+ } from "@mariozechner/pi-coding-agent";
24
+ import {
25
+ Container,
26
+ Editor,
27
+ SelectList,
28
+ Spacer,
29
+ Text,
30
+ matchesKey,
31
+ type EditorTheme,
32
+ type Focusable,
33
+ type TUI,
34
+ } from "@mariozechner/pi-tui";
35
+
36
+ const HOTKEY = "ctrl+shift+e";
37
+ const CLEAR_ALL_KEY = "ctrl+x";
38
+ const COMMAND_NAME = "edit-turn";
39
+ const COMMAND_TEXT = `/${COMMAND_NAME}`;
40
+ const SELECT_TITLE = "Pick a previous user message to edit";
41
+ const EDIT_TITLE = "Edit previous user message";
42
+ const PREVIEW_MAX_LENGTH = 90;
43
+ const SELECTOR_MAX_VISIBLE = 12;
44
+ const SELECTOR_PAGE_STEP = SELECTOR_MAX_VISIBLE - 1;
45
+
46
+ type TextContentBlock = {
47
+ type?: string;
48
+ text?: string;
49
+ };
50
+
51
+ type ImageContentBlock = {
52
+ type?: string;
53
+ };
54
+
55
+ export type EditableUserMessage = {
56
+ entryId: string;
57
+ text: string;
58
+ hasImages: boolean;
59
+ label: string;
60
+ };
61
+
62
+ let draftBeforeHotkey: string | undefined;
63
+
64
+ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max));
65
+ const collapseWhitespace = (text: string) => text.replace(/\s+/g, " ").trim();
66
+
67
+ const truncate = (text: string, maxLength: number) =>
68
+ text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
69
+
70
+ const formatTimestamp = (timestamp: string) => timestamp.slice(0, 16).replace("T", " ");
71
+
72
+ const createEditorTheme = (theme: Theme): EditorTheme => ({
73
+ borderColor: (text) => theme.fg("accent", text),
74
+ selectList: {
75
+ selectedPrefix: (text) => theme.fg("accent", text),
76
+ selectedText: (text) => theme.fg("accent", text),
77
+ description: (text) => theme.fg("muted", text),
78
+ scrollInfo: (text) => theme.fg("dim", text),
79
+ noMatch: (text) => theme.fg("warning", text),
80
+ },
81
+ });
82
+
83
+ export const extractEditableText = (content: unknown): { text: string | undefined; hasImages: boolean } => {
84
+ if (typeof content === "string") {
85
+ const text = content.trim();
86
+ return { text: text.length > 0 ? content : undefined, hasImages: false };
87
+ }
88
+
89
+ if (!Array.isArray(content)) {
90
+ return { text: undefined, hasImages: false };
91
+ }
92
+
93
+ const textParts: string[] = [];
94
+ let hasImages = false;
95
+
96
+ for (const block of content) {
97
+ if (!block || typeof block !== "object") {
98
+ continue;
99
+ }
100
+
101
+ const textBlock = block as TextContentBlock;
102
+ if (textBlock.type === "text" && typeof textBlock.text === "string") {
103
+ textParts.push(textBlock.text);
104
+ continue;
105
+ }
106
+
107
+ const imageBlock = block as ImageContentBlock;
108
+ if (imageBlock.type === "image") {
109
+ hasImages = true;
110
+ }
111
+ }
112
+
113
+ const joined = textParts.join("\n");
114
+ return {
115
+ text: joined.trim().length > 0 ? joined : undefined,
116
+ hasImages,
117
+ };
118
+ };
119
+
120
+ export const getEditableMessages = (branch: SessionEntry[]): EditableUserMessage[] => {
121
+ const editable: EditableUserMessage[] = [];
122
+ const userEntries = branch.filter(
123
+ (entry): entry is SessionEntry & { type: "message"; message: { role: "user"; content: unknown } } =>
124
+ entry.type === "message" && entry.message.role === "user",
125
+ );
126
+
127
+ for (const entry of userEntries) {
128
+ const { text, hasImages } = extractEditableText(entry.message.content);
129
+ if (!text) {
130
+ continue;
131
+ }
132
+
133
+ const previewSource = collapseWhitespace(text.split("\n").find((line) => line.trim().length > 0) ?? text);
134
+ const preview = truncate(previewSource, PREVIEW_MAX_LENGTH);
135
+ const suffix = hasImages ? " [drops images]" : "";
136
+ const index = editable.length + 1;
137
+ editable.push({
138
+ entryId: entry.id,
139
+ text,
140
+ hasImages,
141
+ label: `${index}. ${formatTimestamp(entry.timestamp)} — ${preview}${suffix}`,
142
+ });
143
+ }
144
+
145
+ return editable;
146
+ };
147
+
148
+ class EditableMessageSelector extends Container {
149
+ private readonly tui: TUI;
150
+ private readonly keybindings: KeybindingsManager;
151
+ private readonly messages: EditableUserMessage[];
152
+ private readonly selectList: SelectList;
153
+ private readonly onSelect: (message: EditableUserMessage) => void;
154
+ private readonly onCancel: () => void;
155
+ private selectedIndex: number;
156
+
157
+ constructor(
158
+ tui: TUI,
159
+ theme: Theme,
160
+ keybindings: KeybindingsManager,
161
+ title: string,
162
+ messages: EditableUserMessage[],
163
+ onSelect: (message: EditableUserMessage) => void,
164
+ onCancel: () => void,
165
+ ) {
166
+ super();
167
+ this.tui = tui;
168
+ this.keybindings = keybindings;
169
+ this.messages = messages;
170
+ this.onSelect = onSelect;
171
+ this.onCancel = onCancel;
172
+ this.selectedIndex = Math.max(0, messages.length - 1);
173
+
174
+ this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
175
+ this.addChild(new Spacer(1));
176
+ this.addChild(new Text(theme.fg("accent", title), 1, 0));
177
+ this.addChild(new Spacer(1));
178
+
179
+ this.selectList = new SelectList(
180
+ messages.map((message) => ({ value: message.entryId, label: message.label })),
181
+ SELECTOR_MAX_VISIBLE,
182
+ createEditorTheme(theme).selectList,
183
+ { minPrimaryColumnWidth: 56, maxPrimaryColumnWidth: 120 },
184
+ );
185
+ this.selectList.setSelectedIndex(this.selectedIndex);
186
+ this.addChild(this.selectList);
187
+ this.addChild(new Spacer(1));
188
+ this.addChild(
189
+ new Text(
190
+ [
191
+ rawKeyHint("↑", "older"),
192
+ rawKeyHint("↓", "newer"),
193
+ keyHint("tui.select.pageUp", "jump up"),
194
+ keyHint("tui.select.pageDown", "jump down"),
195
+ keyHint("tui.select.confirm", "edit"),
196
+ keyHint("tui.select.cancel", "cancel"),
197
+ ].join(" "),
198
+ 1,
199
+ 0,
200
+ ),
201
+ );
202
+ this.addChild(new Spacer(1));
203
+ this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
204
+ }
205
+
206
+ private setSelectedIndex(index: number) {
207
+ this.selectedIndex = clamp(index, 0, this.messages.length - 1);
208
+ this.selectList.setSelectedIndex(this.selectedIndex);
209
+ this.tui.requestRender();
210
+ }
211
+
212
+ handleInput(data: string): void {
213
+ if (this.keybindings.matches(data, "tui.select.up")) {
214
+ this.setSelectedIndex(this.selectedIndex - 1);
215
+ return;
216
+ }
217
+
218
+ if (this.keybindings.matches(data, "tui.select.down")) {
219
+ this.setSelectedIndex(this.selectedIndex + 1);
220
+ return;
221
+ }
222
+
223
+ if (this.keybindings.matches(data, "tui.select.pageUp")) {
224
+ this.setSelectedIndex(this.selectedIndex - SELECTOR_PAGE_STEP);
225
+ return;
226
+ }
227
+
228
+ if (this.keybindings.matches(data, "tui.select.pageDown")) {
229
+ this.setSelectedIndex(this.selectedIndex + SELECTOR_PAGE_STEP);
230
+ return;
231
+ }
232
+
233
+ if (this.keybindings.matches(data, "tui.select.confirm")) {
234
+ const selected = this.messages[this.selectedIndex];
235
+ if (selected) {
236
+ this.onSelect(selected);
237
+ }
238
+ return;
239
+ }
240
+
241
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
242
+ this.onCancel();
243
+ }
244
+ }
245
+ }
246
+
247
+ class ReeditMessageEditor extends Container implements Focusable {
248
+ private readonly editor: Editor;
249
+ private readonly tui: TUI;
250
+ private readonly keybindings: KeybindingsManager;
251
+ private readonly onCancel: () => void;
252
+ private _focused = false;
253
+
254
+ get focused(): boolean {
255
+ return this._focused;
256
+ }
257
+
258
+ set focused(value: boolean) {
259
+ this._focused = value;
260
+ this.editor.focused = value;
261
+ }
262
+
263
+ constructor(
264
+ tui: TUI,
265
+ theme: Theme,
266
+ keybindings: KeybindingsManager,
267
+ title: string,
268
+ prefill: string,
269
+ onSubmit: (value: string) => void,
270
+ onCancel: () => void,
271
+ ) {
272
+ super();
273
+ this.tui = tui;
274
+ this.keybindings = keybindings;
275
+ this.onCancel = onCancel;
276
+
277
+ this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
278
+ this.addChild(new Spacer(1));
279
+ this.addChild(new Text(theme.fg("accent", title), 1, 0));
280
+ this.addChild(new Spacer(1));
281
+
282
+ this.editor = new Editor(tui, createEditorTheme(theme));
283
+ this.editor.setText(prefill);
284
+ this.editor.onSubmit = (value) => onSubmit(value);
285
+ this.addChild(this.editor);
286
+ this.addChild(new Spacer(1));
287
+
288
+ const hasExternalEditor = Boolean(process.env.VISUAL || process.env.EDITOR);
289
+ const hint = [
290
+ keyHint("tui.select.confirm", "submit"),
291
+ keyHint("tui.input.newLine", "newline"),
292
+ rawKeyHint("ctrl+x", "clear all"),
293
+ rawKeyHint("empty+enter", "delete"),
294
+ keyHint("tui.select.cancel", "cancel"),
295
+ ...(hasExternalEditor ? [keyHint("app.editor.external", "external editor")] : []),
296
+ ].join(" ");
297
+ this.addChild(new Text(hint, 1, 0));
298
+ this.addChild(new Spacer(1));
299
+ this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
300
+ }
301
+
302
+ handleInput(data: string): void {
303
+ if (matchesKey(data, CLEAR_ALL_KEY)) {
304
+ this.editor.setText("");
305
+ this.tui.requestRender();
306
+ return;
307
+ }
308
+
309
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
310
+ this.onCancel();
311
+ return;
312
+ }
313
+
314
+ if (this.keybindings.matches(data, "app.editor.external")) {
315
+ this.openExternalEditor();
316
+ return;
317
+ }
318
+
319
+ this.editor.handleInput(data);
320
+ }
321
+
322
+ private openExternalEditor() {
323
+ const editorCmd = process.env.VISUAL || process.env.EDITOR;
324
+ if (!editorCmd) {
325
+ return;
326
+ }
327
+
328
+ const currentText = this.editor.getText();
329
+ const tmpFile = path.join(os.tmpdir(), `pi-reedit-message-${Date.now()}.md`);
330
+
331
+ try {
332
+ fs.writeFileSync(tmpFile, currentText, "utf-8");
333
+ this.tui.stop();
334
+
335
+ const [editor, ...editorArgs] = editorCmd.split(" ");
336
+ const result = spawnSync(editor, [...editorArgs, tmpFile], {
337
+ stdio: "inherit",
338
+ shell: process.platform === "win32",
339
+ });
340
+
341
+ if (result.status === 0) {
342
+ const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
343
+ this.editor.setText(newContent);
344
+ }
345
+ } finally {
346
+ try {
347
+ fs.unlinkSync(tmpFile);
348
+ } catch {
349
+ // Ignore cleanup errors.
350
+ }
351
+
352
+ this.tui.start();
353
+ this.tui.requestRender(true);
354
+ }
355
+ }
356
+ }
357
+
358
+ const selectEditableMessage = async (ctx: ExtensionCommandContext, messages: EditableUserMessage[]) =>
359
+ ctx.ui.custom<EditableUserMessage | undefined>((tui, theme, keybindings, done) =>
360
+ new EditableMessageSelector(tui, theme, keybindings, SELECT_TITLE, messages, (message) => done(message), () => done(undefined)),
361
+ );
362
+
363
+ const editTextInCustomEditor = async (ctx: ExtensionCommandContext, prefill: string) =>
364
+ ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) =>
365
+ new ReeditMessageEditor(tui, theme, keybindings, EDIT_TITLE, prefill, (value) => done(value), () => done(undefined)),
366
+ );
367
+
368
+ const restoreDraftIfNeeded = (ctx: ExtensionCommandContext) => {
369
+ if (draftBeforeHotkey === undefined) {
370
+ return;
371
+ }
372
+
373
+ ctx.ui.setEditorText(draftBeforeHotkey);
374
+ draftBeforeHotkey = undefined;
375
+ };
376
+
377
+ const clearSavedDraft = () => {
378
+ draftBeforeHotkey = undefined;
379
+ };
380
+
381
+ const handleEditTurn = async (ctx: ExtensionCommandContext) => {
382
+ if (!ctx.hasUI) {
383
+ clearSavedDraft();
384
+ return;
385
+ }
386
+
387
+ if (ctx.hasPendingMessages()) {
388
+ ctx.ui.notify("Queued messages are pending. Press Escape first, then try again.", "warning");
389
+ restoreDraftIfNeeded(ctx);
390
+ return;
391
+ }
392
+
393
+ if (!ctx.isIdle()) {
394
+ ctx.abort();
395
+ await ctx.waitForIdle();
396
+ }
397
+
398
+ const editableMessages = getEditableMessages(ctx.sessionManager.getBranch());
399
+ if (editableMessages.length === 0) {
400
+ ctx.ui.notify("No editable text user messages found on the current branch.", "warning");
401
+ restoreDraftIfNeeded(ctx);
402
+ return;
403
+ }
404
+
405
+ const selected = await selectEditableMessage(ctx, editableMessages);
406
+ if (!selected) {
407
+ restoreDraftIfNeeded(ctx);
408
+ return;
409
+ }
410
+
411
+ if (selected.hasImages) {
412
+ const keepGoing = await ctx.ui.confirm(
413
+ "Drop images?",
414
+ "That message contains images. Editing or deleting it here will keep only the text and drop the images. Continue?",
415
+ );
416
+ if (!keepGoing) {
417
+ restoreDraftIfNeeded(ctx);
418
+ return;
419
+ }
420
+ }
421
+
422
+ const editedText = await editTextInCustomEditor(ctx, selected.text);
423
+ if (editedText === undefined) {
424
+ restoreDraftIfNeeded(ctx);
425
+ return;
426
+ }
427
+
428
+ const isDelete = editedText.trim().length === 0;
429
+ const result = await ctx.navigateTree(selected.entryId, { summarize: false });
430
+ if (result.cancelled) {
431
+ restoreDraftIfNeeded(ctx);
432
+ return;
433
+ }
434
+
435
+ clearSavedDraft();
436
+ ctx.ui.setEditorText(isDelete ? "" : editedText);
437
+ ctx.ui.notify(
438
+ isDelete
439
+ ? "Message deleted. Type a new prompt to continue from that point."
440
+ : "Edited message loaded. Press Enter to continue from that point.",
441
+ "info",
442
+ );
443
+ };
444
+
445
+ class EditSessionInPlaceEditor extends CustomEditor {
446
+ handleInput(data: string): void {
447
+ if (matchesKey(data, HOTKEY)) {
448
+ draftBeforeHotkey = this.getText();
449
+ this.setText(COMMAND_TEXT);
450
+ super.handleInput("\r");
451
+ return;
452
+ }
453
+
454
+ super.handleInput(data);
455
+ }
456
+ }
457
+
458
+ export default function editSessionInPlace(pi: ExtensionAPI) {
459
+ pi.registerCommand(COMMAND_NAME, {
460
+ description: `Select and re-edit a previous user message on the current branch (${HOTKEY})`,
461
+ handler: async (_args, ctx) => {
462
+ await handleEditTurn(ctx);
463
+ },
464
+ });
465
+
466
+ pi.on("session_start", async () => {
467
+ clearSavedDraft();
468
+ });
469
+
470
+ pi.on("session_shutdown", async () => {
471
+ clearSavedDraft();
472
+ });
473
+
474
+ pi.on("session_start", (_event, ctx) => {
475
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => new EditSessionInPlaceEditor(tui, theme, keybindings));
476
+ });
477
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "pi-edit-session-in-place",
3
+ "version": "0.1.0",
4
+ "description": "pi extension that lets you re-edit or delete an earlier user message in the current session branch",
5
+ "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi",
11
+ "pi-extension",
12
+ "extension",
13
+ "session",
14
+ "history",
15
+ "typescript"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/fitchmultz/pi-edit-session-in-place.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/fitchmultz/pi-edit-session-in-place/issues"
23
+ },
24
+ "homepage": "https://github.com/fitchmultz/pi-edit-session-in-place#readme",
25
+ "scripts": {
26
+ "test": "node --experimental-strip-types tests/edit-session-in-place.test.ts",
27
+ "typecheck": "tsc --noEmit",
28
+ "check": "npm test && npm run typecheck",
29
+ "prepublishOnly": "npm run check && npm pack --dry-run"
30
+ },
31
+ "files": [
32
+ "extensions",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "peerDependencies": {
37
+ "@mariozechner/pi-coding-agent": "*",
38
+ "@mariozechner/pi-tui": "*"
39
+ },
40
+ "devDependencies": {
41
+ "@mariozechner/pi-coding-agent": "^0.65.2",
42
+ "@mariozechner/pi-tui": "^0.65.2",
43
+ "@types/node": "^24.6.0",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "pi": {
47
+ "extensions": [
48
+ "./extensions"
49
+ ]
50
+ }
51
+ }