feat(extensions): add inline-bash example for expanding !{command} in prompts (#881)

* feat(extensions): add inline-bash example for expanding !{command} in prompts

Adds an example extension that expands inline bash commands within user
prompts before sending to the agent. Uses the `input` event to transform
patterns like `!{pwd}` or `!{git status}` into their output.

Preserves existing `!command` whole-line bash behavior.

* docs(extensions): add inline-bash to README

* chore: fix stupid bug
This commit is contained in:
scutifer 2026-01-21 18:38:07 +05:30 committed by GitHub
parent 565488fde6
commit 82cc0fe866
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 96 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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 };
});
}