summaryrefslogtreecommitdiff
path: root/pi/agent/extensions/inline-bash
diff options
context:
space:
mode:
Diffstat (limited to 'pi/agent/extensions/inline-bash')
-rw-r--r--pi/agent/extensions/inline-bash/README.md44
-rw-r--r--pi/agent/extensions/inline-bash/index.ts72
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 };
+ });
+}