diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f4de7060..761aef6f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `markdown.codeBlockIndent` setting to customize code block indentation in rendered output +- Added `inline-bash.ts` example extension for expanding `!{command}` patterns in prompts ### Fixed diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 48b75dd6..fb8f3cc4 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -63,6 +63,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `doom-overlay/` | DOOM game running as an overlay at 35 FPS (demonstrates real-time game rendering) | | `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` | | `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook | +| `inline-bash.ts` | Expands `!{command}` patterns in prompts via `input` event transformation | ### Git Integration diff --git a/packages/coding-agent/examples/extensions/inline-bash.ts b/packages/coding-agent/examples/extensions/inline-bash.ts new file mode 100644 index 00000000..07b56d00 --- /dev/null +++ b/packages/coding-agent/examples/extensions/inline-bash.ts @@ -0,0 +1,94 @@ +/** + * Inline Bash Extension - expands inline bash commands in user prompts. + * + * Start pi with this extension: + * pi -e ./examples/extensions/inline-bash.ts + * + * Then type prompts with inline bash: + * What's in !{pwd}? + * The current branch is !{git branch --show-current} and status: !{git status --short} + * My node version is !{node --version} + * + * The !{command} patterns are executed and replaced with their output before + * the prompt is sent to the agent. + * + * Note: Regular !command syntax (whole-line bash) is preserved and works as before. + */ +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + const PATTERN = /!\{([^}]+)\}/g; + const TIMEOUT_MS = 30000; + + pi.on("input", async (event, ctx) => { + const text = event.text; + + // Don't process if it's a whole-line bash command (starts with !) + // This preserves the existing !command behavior + if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) { + return { action: "continue" }; + } + + // Check if there are any inline bash patterns + if (!PATTERN.test(text)) { + return { action: "continue" }; + } + + // Reset regex state after test() + PATTERN.lastIndex = 0; + + let result = text; + const expansions: Array<{ command: string; output: string; error?: string }> = []; + + // Find all matches first (to avoid issues with replacing while iterating) + const matches: Array<{ full: string; command: string }> = []; + let match = PATTERN.exec(text); + while (match) { + matches.push({ full: match[0], command: match[1] }); + match = PATTERN.exec(text); + } + + // Execute each command and collect results + for (const { full, command } of matches) { + try { + const bashResult = await pi.exec("bash", ["-c", command], { + timeout: TIMEOUT_MS, + }); + + const output = bashResult.stdout || bashResult.stderr || ""; + const trimmed = output.trim(); + + if (bashResult.code !== 0 && bashResult.stderr) { + expansions.push({ + command, + output: trimmed, + error: `exit code ${bashResult.code}`, + }); + } else { + expansions.push({ command, output: trimmed }); + } + + result = result.replace(full, trimmed); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + expansions.push({ command, output: "", error: errorMsg }); + result = result.replace(full, `[error: ${errorMsg}]`); + } + } + + // Show what was expanded (if UI available) + if (ctx.hasUI && expansions.length > 0) { + const summary = expansions + .map((e) => { + const status = e.error ? ` (${e.error})` : ""; + const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output; + return `!{${e.command}}${status} -> "${preview}"`; + }) + .join("\n"); + + ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, "info"); + } + + return { action: "transform", text: result, images: event.images }; + }); +}