@normful/picadillo 1.2.0 → 2.0.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/CHANGELOG.md +15 -0
- package/README.md +9 -1
- package/bun.lock +1 -1
- package/dev-scripts/copy-these-extensions-to-local-system-extensions-dir.sh +3 -0
- package/extensions/parrot.ts +239 -0
- package/package.json +5 -4
- package/extensions/.gitkeep +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
## [2.0.0] - 2026-02-16
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- *(extension)* Add parrot extension for editing AI responses externally
|
|
6
|
+
- *(pkg)* Update package info and developer scripts
|
|
7
|
+
## [1.2.1] - 2026-02-16
|
|
8
|
+
|
|
9
|
+
### ⚙️ Miscellaneous Tasks
|
|
10
|
+
|
|
11
|
+
- Release 1.2.1
|
|
1
12
|
## [1.2.0] - 2026-02-16
|
|
2
13
|
|
|
3
14
|
### 🚜 Refactor
|
|
4
15
|
|
|
5
16
|
- *(readme)* Update installation and uninstallation commands to use direct HTTPS URL
|
|
6
17
|
- *(package.json)* Remove obsolete "pi" configuration section
|
|
18
|
+
|
|
19
|
+
### ⚙️ Miscellaneous Tasks
|
|
20
|
+
|
|
21
|
+
- Release 1.2.0
|
|
7
22
|
## [1.1.0] - 2026-02-16
|
|
8
23
|
|
|
9
24
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ pi install https://github.com/normful/picadillo
|
|
|
31
31
|
|
|
32
32
|
Configure what you don't want with `pi config`. It will modify `~/.pi/agent/settings.json`
|
|
33
33
|
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
- [`uv`](https://github.com/astral-sh/uv) — Required runtime dependency for running Python scripts
|
|
37
|
+
|
|
34
38
|
## Skills
|
|
35
39
|
|
|
36
40
|
| Skill | Description |
|
|
@@ -40,7 +44,11 @@ Configure what you don't want with `pi config`. It will modify `~/.pi/agent/sett
|
|
|
40
44
|
|
|
41
45
|
## Extensions
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
| Extension | Description |
|
|
48
|
+
|-----------|-------------|
|
|
49
|
+
| [parrot](extensions/parrot.ts) | Opens the last AI response in an external text editor (respects `$VISUAL` or `$EDITOR`), then sends your edited content back to the chat. Useful for editing AI responses before re-sending, copying output to a full-featured editor, or iterating with custom edits. Usage: `/parrot` or `Alt+R` |
|
|
50
|
+
|
|
51
|
+
[](https://asciinema.org/a/788693)
|
|
44
52
|
|
|
45
53
|
## Uninstall
|
|
46
54
|
|
package/bun.lock
CHANGED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parrot Command Extension
|
|
3
|
+
*
|
|
4
|
+
* Opens the last AI response in an external text editor (respects $VISUAL
|
|
5
|
+
* or $EDITOR environment variables). When you save and exit the editor,
|
|
6
|
+
* the edited content is automatically sent back to the chat as your next
|
|
7
|
+
* message.
|
|
8
|
+
*
|
|
9
|
+
* This is useful for:
|
|
10
|
+
* - Editing AI responses before re-sending them
|
|
11
|
+
* - Copying AI output to a full-featured editor
|
|
12
|
+
* - Iterating on AI responses with custom edits
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* /parrot - Open last AI message in external editor
|
|
16
|
+
* Alt+R - Keyboard shortcut for the same action
|
|
17
|
+
*
|
|
18
|
+
* The extension preserves the original message content, opens it in your
|
|
19
|
+
* preferred editor, and sends your edited version back to the chat.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as os from "node:os";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
|
|
27
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
28
|
+
import type {
|
|
29
|
+
ExtensionContext,
|
|
30
|
+
ExtensionUIContext,
|
|
31
|
+
} from "@mariozechner/pi-coding-agent";
|
|
32
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
33
|
+
import type { TextContent } from "@mariozechner/pi-ai";
|
|
34
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
35
|
+
|
|
36
|
+
export const PARROT_DESCRIPTION =
|
|
37
|
+
"Open last AI message in external editor, then send edited message after you save and exit external editor";
|
|
38
|
+
|
|
39
|
+
export const PARROT_DEFAULT_KEYBOARD_SHORTCUT = Key.alt("r");
|
|
40
|
+
export const PARROT_CUSTOM_MESSAGE_TYPE = "🦜 parrot squawking";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find the last assistant message text on the current branch.
|
|
44
|
+
* Excludes thinking content, returns only user-visible text.
|
|
45
|
+
*/
|
|
46
|
+
export function findLastAssistantMessage(
|
|
47
|
+
sessionEntry: SessionEntry[],
|
|
48
|
+
): string | undefined {
|
|
49
|
+
for (let i = sessionEntry.length - 1; i >= 0; i--) {
|
|
50
|
+
const entry = sessionEntry[i];
|
|
51
|
+
if (!entry || entry.type !== "message") continue;
|
|
52
|
+
|
|
53
|
+
const msg = entry.message;
|
|
54
|
+
if (!msg || msg.role !== "assistant") continue;
|
|
55
|
+
|
|
56
|
+
const textParts = msg.content
|
|
57
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
58
|
+
.map((c) => c.text);
|
|
59
|
+
|
|
60
|
+
if (textParts.length > 0) {
|
|
61
|
+
return textParts.join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getEditorCommand(): string {
|
|
68
|
+
return process.env.VISUAL || process.env.EDITOR || "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Result from running the external editor
|
|
73
|
+
*/
|
|
74
|
+
export interface EditorResult {
|
|
75
|
+
content: string | null;
|
|
76
|
+
error: string | null;
|
|
77
|
+
exitCode: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function clearScreen() {
|
|
81
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run the external editor on the given file path.
|
|
86
|
+
* Handles TUI suspension, terminal setup, and result parsing.
|
|
87
|
+
*/
|
|
88
|
+
export function runEditor(filePath: string): EditorResult {
|
|
89
|
+
clearScreen();
|
|
90
|
+
|
|
91
|
+
const editorCmd = getEditorCommand();
|
|
92
|
+
if (!editorCmd) {
|
|
93
|
+
return {
|
|
94
|
+
content: null,
|
|
95
|
+
error:
|
|
96
|
+
"No editor configured. Set $VISUAL or $EDITOR environment variable.",
|
|
97
|
+
exitCode: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let exitCode: number | null = null;
|
|
102
|
+
let errorMessage: string | null = null;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = spawnSync(editorCmd, [filePath], {
|
|
106
|
+
stdio: "inherit",
|
|
107
|
+
env: process.env,
|
|
108
|
+
shell: true,
|
|
109
|
+
});
|
|
110
|
+
exitCode = result.status;
|
|
111
|
+
|
|
112
|
+
if (result.error) {
|
|
113
|
+
errorMessage = result.error.message;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (result.signal) {
|
|
117
|
+
errorMessage = `Killed by signal: ${result.signal}`;
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errorMessage) {
|
|
124
|
+
return { content: null, error: errorMessage, exitCode };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Read the edited content
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(filePath, "utf-8").replace(/\n$/, "");
|
|
130
|
+
return { content, error: null, exitCode };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
133
|
+
return {
|
|
134
|
+
content: null,
|
|
135
|
+
error: `Could not read edited file: ${error}`,
|
|
136
|
+
exitCode,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle the result from running the editor and decide what to notify/send
|
|
143
|
+
*/
|
|
144
|
+
export function handleEditorResult(
|
|
145
|
+
result: EditorResult,
|
|
146
|
+
ui: ExtensionUIContext,
|
|
147
|
+
sendMessage: ExtensionAPI["sendMessage"],
|
|
148
|
+
): void {
|
|
149
|
+
const { content, error, exitCode } = result;
|
|
150
|
+
|
|
151
|
+
if (error) {
|
|
152
|
+
ui.notify(`nvim error: ${error}`, "error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
157
|
+
const editorCmd = getEditorCommand();
|
|
158
|
+
|
|
159
|
+
ui.notify(
|
|
160
|
+
`'${editorCmd}' exited with code ${exitCode}. Not sending message`,
|
|
161
|
+
"warning",
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!content) {
|
|
167
|
+
ui.notify("No message to send", "info");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sendMessage(
|
|
172
|
+
{
|
|
173
|
+
customType: PARROT_CUSTOM_MESSAGE_TYPE,
|
|
174
|
+
content,
|
|
175
|
+
display: true,
|
|
176
|
+
},
|
|
177
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function parrotHandler(pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
182
|
+
if (!ctx.hasUI) {
|
|
183
|
+
ctx.ui.notify("parrot requires interactive mode", "error");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const branch = ctx.sessionManager.getBranch();
|
|
188
|
+
const lastAssistantText = findLastAssistantMessage(branch);
|
|
189
|
+
|
|
190
|
+
if (!lastAssistantText) {
|
|
191
|
+
ctx.ui.notify("No assistant messages found", "error");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const tmpFile = path.join(os.tmpdir(), `pi-parrot-${Date.now()}.md`);
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync(tmpFile, lastAssistantText, "utf-8");
|
|
198
|
+
} catch (err) {
|
|
199
|
+
ctx.ui.notify(`Failed to create temp file: ${err}`, "error");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = await ctx.ui.custom<EditorResult>((tui, _theme, _kb, done) => {
|
|
204
|
+
tui.stop();
|
|
205
|
+
|
|
206
|
+
const editorResult = runEditor(tmpFile);
|
|
207
|
+
|
|
208
|
+
tui.start();
|
|
209
|
+
tui.requestRender(true);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
fs.unlinkSync(tmpFile);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
ctx.ui.notify(`Failed to delete ${tmpFile}: ${err}`, "error");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
done(editorResult);
|
|
218
|
+
|
|
219
|
+
return { render: () => [], invalidate: () => {} };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
handleEditorResult(result, ctx.ui, pi.sendMessage);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default function (pi: ExtensionAPI) {
|
|
226
|
+
pi.registerShortcut(PARROT_DEFAULT_KEYBOARD_SHORTCUT, {
|
|
227
|
+
description: PARROT_DESCRIPTION,
|
|
228
|
+
handler: async (ctx: ExtensionContext) => {
|
|
229
|
+
await parrotHandler(pi, ctx);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
pi.registerCommand("parrot", {
|
|
234
|
+
description: PARROT_DESCRIPTION,
|
|
235
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
236
|
+
await parrotHandler(pi, ctx);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@normful/picadillo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "pi agent skills/extensions: run-in-tmux, parrot (send AI message with edited version of AI message)",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi",
|
|
8
8
|
"pi-coding-agent",
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"main": "none",
|
|
29
29
|
"scripts": {
|
|
30
30
|
"test": "bun test",
|
|
31
|
-
"release": "release-it"
|
|
31
|
+
"release": "release-it",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"@mariozechner/pi-coding-agent": "*",
|
|
35
36
|
"@mariozechner/pi-ai": "*",
|
|
36
37
|
"@mariozechner/pi-tui": "*",
|
|
37
|
-
"typescript": "^5"
|
|
38
|
+
"typescript": "^5.9.3"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/bun": "latest",
|
package/extensions/.gitkeep
DELETED
|
File without changes
|