diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9608d5b8..d59a244b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ ### Added - Added Android/Termux support with graceful clipboard fallback ([#1164](https://github.com/badlogic/pi-mono/issues/1164)) +- Added bash tool spawn hook support for adjusting command, cwd, and env before execution ([#1160](https://github.com/badlogic/pi-mono/pull/1160) by [@mitsuhiko](https://github.com/mitsuhiko)) - Added typed `ToolCallEvent.input` per tool with `isToolCallEventType()` type guard for narrowing built-in tool events ([#1147](https://github.com/badlogic/pi-mono/pull/1147) by [@giuseppeg](https://github.com/giuseppeg)) - Exported `discoverAndLoadExtensions` from package to enable extension testing without a local repo clone ([#1148](https://github.com/badlogic/pi-mono/issues/1148)) - Added Extension UI Protocol documentation to RPC docs covering all request/response types for extension dialogs and notifications ([#1144](https://github.com/badlogic/pi-mono/pull/1144) by [@aliou](https://github.com/aliou)) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 089ec377..9e4b8bf3 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1237,6 +1237,20 @@ pi.registerTool({ **Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations` +The bash tool also supports a spawn hook to adjust the command, cwd, or env before execution: + +```typescript +import { createBashTool } from "@mariozechner/pi-coding-agent"; + +const bashTool = createBashTool(cwd, { + spawnHook: ({ command, cwd, env }) => ({ + command: `source ~/.profile\n${command}`, + cwd: `/mnt/sandbox${cwd}`, + env: { ...env, CI: "1" }, + }), +}); +``` + See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag. ### Output Truncation @@ -1777,4 +1791,5 @@ All examples in [examples/extensions/](../examples/extensions/). | **Misc** ||| | `antigravity-image-gen.ts` | Image generation tool | `registerTool`, Google Antigravity | | `inline-bash.ts` | Inline bash in tool calls | `on("tool_call")` | +| `bash-spawn-hook.ts` | Adjust bash command, cwd, and env before execution | `createBashTool`, `spawnHook` | | `with-deps/` | Extension with npm dependencies | Package structure with `package.json` | diff --git a/packages/coding-agent/examples/extensions/bash-spawn-hook.ts b/packages/coding-agent/examples/extensions/bash-spawn-hook.ts new file mode 100644 index 00000000..977638ae --- /dev/null +++ b/packages/coding-agent/examples/extensions/bash-spawn-hook.ts @@ -0,0 +1,30 @@ +/** + * Bash Spawn Hook Example + * + * Adjusts command, cwd, and env before execution. + * + * Usage: + * pi -e ./bash-spawn-hook.ts + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { createBashTool } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + const cwd = process.cwd(); + + const bashTool = createBashTool(cwd, { + spawnHook: ({ command, cwd, env }) => ({ + command: `source ~/.profile\n${command}`, + cwd, + env: { ...env, PI_SPAWN_HOOK: "1" }, + }), + }); + + pi.registerTool({ + ...bashTool, + execute: async (id, params, onUpdate, _ctx, signal) => { + return bashTool.execute(id, params, signal, onUpdate); + }, + }); +} diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9b166cca..28a10b2f 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -47,6 +47,7 @@ export interface BashOperations { onData: (data: Buffer) => void; signal?: AbortSignal; timeout?: number; + env?: NodeJS.ProcessEnv; }, ) => Promise<{ exitCode: number | null }>; } @@ -55,7 +56,7 @@ export interface BashOperations { * Default bash operations using local shell */ const defaultBashOperations: BashOperations = { - exec: (command, cwd, { onData, signal, timeout }) => { + exec: (command, cwd, { onData, signal, timeout, env }) => { return new Promise((resolve, reject) => { const { shell, args } = getShellConfig(); @@ -67,7 +68,7 @@ const defaultBashOperations: BashOperations = { const child = spawn(shell, [...args, command], { cwd, detached: true, - env: getShellEnv(), + env: env ?? getShellEnv(), stdio: ["ignore", "pipe", "pipe"], }); @@ -135,16 +136,37 @@ const defaultBashOperations: BashOperations = { }, }; +export interface BashSpawnContext { + command: string; + cwd: string; + env: NodeJS.ProcessEnv; +} + +export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext; + +function resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext { + const baseContext: BashSpawnContext = { + command, + cwd, + env: { ...getShellEnv() }, + }; + + return spawnHook ? spawnHook(baseContext) : baseContext; +} + export interface BashToolOptions { /** Custom operations for command execution. Default: local shell */ operations?: BashOperations; /** Command prefix prepended to every command (e.g., "shopt -s expand_aliases" for alias support) */ commandPrefix?: string; + /** Hook to adjust command, cwd, or env before execution */ + spawnHook?: BashSpawnHook; } export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool { const ops = options?.operations ?? defaultBashOperations; const commandPrefix = options?.commandPrefix; + const spawnHook = options?.spawnHook; return { name: "bash", @@ -159,6 +181,7 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo ) => { // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support) const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command; + const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook); return new Promise((resolve, reject) => { // We'll stream to a temp file if output gets large @@ -215,7 +238,12 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo } }; - ops.exec(resolvedCommand, cwd, { onData: handleData, signal, timeout }) + ops.exec(spawnContext.command, spawnContext.cwd, { + onData: handleData, + signal, + timeout, + env: spawnContext.env, + }) .then(({ exitCode }) => { // Close temp file stream if (tempFileStream) { diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 28687198..56ccec02 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -1,5 +1,7 @@ export { type BashOperations, + type BashSpawnContext, + type BashSpawnHook, type BashToolDetails, type BashToolInput, type BashToolOptions, diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 339ee7d4..b335f343 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -204,6 +204,8 @@ export { // Tools export { type BashOperations, + type BashSpawnContext, + type BashSpawnHook, type BashToolDetails, type BashToolInput, type BashToolOptions,