diff options
Diffstat (limited to 'pi/agent/extensions/inline-bash')
| -rw-r--r-- | pi/agent/extensions/inline-bash/README.md | 44 | ||||
| -rw-r--r-- | pi/agent/extensions/inline-bash/index.ts | 72 |
2 files changed, 116 insertions, 0 deletions
diff --git a/pi/agent/extensions/inline-bash/README.md b/pi/agent/extensions/inline-bash/README.md new file mode 100644 index 0000000..777f2fa --- /dev/null +++ b/pi/agent/extensions/inline-bash/README.md @@ -0,0 +1,44 @@ +# Inline Bash + +Inline shell expansion for Pi prompts. + +This is the upstream `inline-bash.ts` example installed as a local extension in +your dotfiles-backed Pi tree. It expands `!{...}` before the prompt is sent to +the model. + +## What It Does + +- `!{command}` runs a shell command locally +- the command output replaces the inline expression in your prompt +- regular whole-line `!command` behavior stays unchanged + +## Usage Flows + +### Flow 1: Inline one value into a prompt + +```text +What files are in !{pwd}? +``` + +Pi sends the expanded prompt after `pwd` runs locally. + +### Flow 2: Inline git state + +```text +Summarize the current branch !{git branch --show-current} and these changes: !{git status --short} +``` + +### Flow 3: Inline system context + +```text +I am on kernel !{uname -r} and hostname !{hostname}. Explain whether that matters for this bug. +``` + +## Notes And Limits + +- Commands run on your local machine, not on the model provider. +- Expansion happens before the prompt is sent. +- Each inline command has a 30 second timeout. +- If a command fails, the prompt gets an inline error marker. +- This is convenient, but it is still shell execution. Treat prompt text + accordingly. diff --git a/pi/agent/extensions/inline-bash/index.ts b/pi/agent/extensions/inline-bash/index.ts new file mode 100644 index 0000000..957c14c --- /dev/null +++ b/pi/agent/extensions/inline-bash/index.ts @@ -0,0 +1,72 @@ +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; + + // Preserve the existing whole-line !command behavior. + if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) { + return { action: "continue" }; + } + + if (!PATTERN.test(text)) { + return { action: "continue" }; + } + + PATTERN.lastIndex = 0; + + let result = text; + const expansions: Array<{ command: string; output: string; error?: string }> = []; + 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); + } + + 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}]`); + } + } + + if (ctx.hasUI && expansions.length > 0) { + const summary = expansions + .map((entry) => { + const status = entry.error ? ` (${entry.error})` : ""; + const preview = + entry.output.length > 50 ? `${entry.output.slice(0, 50)}...` : entry.output; + return `!{${entry.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 }; + }); +} |
