@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 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
- Coming soon!
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
+ [![asciicast](https://asciinema.org/a/788693.svg)](https://asciinema.org/a/788693)
44
52
 
45
53
  ## Uninstall
46
54
 
package/bun.lock CHANGED
@@ -12,7 +12,7 @@
12
12
  "@mariozechner/pi-ai": "*",
13
13
  "@mariozechner/pi-coding-agent": "*",
14
14
  "@mariozechner/pi-tui": "*",
15
- "typescript": "^5",
15
+ "typescript": "^5.9.3",
16
16
  },
17
17
  },
18
18
  },
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ mkdir -p ~/.pi/agent/extensions/
3
+ cp -v "extensions"/*.ts ~/.pi/agent/extensions/
@@ -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": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
- "description": "Norman's pi coding agent commands, skills, extensions",
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",
File without changes