Pi extension that runs a local Executor sidecar and exposes
execute/resume agent tools so Pi can call any MCP server, OpenAPI spec, or
GraphQL endpoint without burning your context window.
Forked from jeremyosih/pi-executor. This fork fixes compatibility with Executor >= 1.5.x and adds quality-of-life improvements for daily use.
The original pi-executor targets Executor <= 1.4.x which shipped the native
binary at bin/runtime/executor. Executor 1.5.x moved the binary to
platform-specific optionalDependencies packages (e.g. executor-darwin-arm64)
and changed boot behaviour in ways that broke the original extension:
- Bootstrap failure: the installer path changed, causing
BOOTSTRAP_FAILED - Missing
--foreground: executor now needs--foregroundfor throwaway sidecars unless you previously ranexecutor install - Removed
/api/scope: health checks now use/api/health - Bearer token auth: all gated surfaces require a token from
<dataDir>/server-control/auth.json - Shared data directory: multiple Pi sessions conflicted over
~/.executor
This fork fixes all of the above.
In Pi, run:
pi install git:github.com/SamuelLHuber/pi-executorThen /reload Pi to load the extension.
After /reload you should see a green executor dot in Pi's footer.
Open the Executor web UI (auto-authenticated):
/executor-web
Add your first integration from the web UI, or use execute to add one
programmatically:
const result = await tools.executor.mcp.addServer({
transport: "stdio",
name: "My MCP Server",
command: "node",
args: ["/path/to/server/build/index.js"],
slug: "my-server",
});Then create a connection for it:
const conn = await tools.executor.coreTools.connections.create({
integration: "my-server",
name: "default",
template: "none",
});Now the server's tools are available as tools.my_server.* inside execute.
execute— run TypeScript inside Executor's sandboxed QuickJS runtimeresume— resume a paused execution (headless / no-UI sessions only)
Inside execute, you get a tools lazy proxy with these discovery helpers:
tools.search({ query, namespace?, limit? })tools.describe.tool({ path })tools.executor.coreTools.integrations.list({})tools.executor.coreTools.connections.list({})
Follow the executor-usage skill (/skill:executor-usage) for the full
discovery and calling pattern.
| Command | What it does |
|---|---|
/executor-web |
Open the Web UI with auto-authentication (?_token=...) |
/executor-start |
Start the sidecar and print its URL |
/executor-stop |
Stop the local sidecar for the current cwd |
/executor-settings |
Configure local vs remote, autoStart, footer status, etc. |
/executor-logs |
Show the last 200 lines of sidecar stdout / stderr logs |
Configure the extension in ~/.pi/agent/settings.json (global) or .pi/settings.json (project).
{
"piExecutor": {
"mode": "local",
"autoStart": true,
"remoteUrl": "",
"showFooterStatus": true,
"stopLocalOnShutdown": true,
"dataDir": ""
}
}mode:"local"or"remote"autoStart: connect on session startremoteUrl: required for remote mode (e.g.http://127.0.0.1:4788)showFooterStatus: show the green dot in Pi's footerstopLocalOnShutdown: stop Pi-owned sidecars when the session endsdataDir: custom data directory for the executor sidecarscopeDir: workspace scope directory that determines the executor tenant. Defaults todataDir. If you have legacy data created under a different directory (e.g. a specific project), set this to that path so the global executor can see it
You can also manage these interactively with /executor-settings.
pi-executor supports three modes depending on how you configure it. The mode is selected at runtime by evaluating settings in this order:
mode === "remote"→ Remote mode (regardless of any other setting)- Project has any explicit
piExecutorsettings in.pi/settings.json→ Project-local mode - Otherwise (no project settings, mode is "local") → Global-local mode (default)
Connect to an existing executor server.
// ~/.pi/agent/settings.json
{
"piExecutor": {
"mode": "remote",
"remoteUrl": "http://127.0.0.1:4788"
}
}Pi never starts or stops this server. You run it yourself (e.g. npx executor web --port 4788).
When no project settings exist — meaning there is no .pi/settings.json file
or the file exists but has no piExecutor key — Pi uses a single shared
executor at ~/.executor on port 4788.
This mode is designed so that:
- You open Pi in many different projects and they all share one executor instance
- The executor survives Pi restarts
- No manual
npx executor webis needed
Behaviour:
- The first Pi session that needs executor checks
~/.executor/server-control/server.jsonand health-checks the port - If the server is running, it connects to it
- If not, it spawns the binary detached (
--foregroundwithchild.unref()) so it outlives Pi - Logs go to
~/.executor/executor.stdout.logand.stderr.log - No Pi session "owns" the server, so
session_shutdownwill not stop it /executor-stopexplicitly refuses to stop the global executor
scopeDir controls which workspace tenant the global executor uses. If your
existing integrations were created while running executor inside a specific
project (e.g. /Users/samuel/git/pi-executor-plugin), set scopeDir to that
path in global settings so the global executor loads them. Otherwise the server
starts with an empty default tenant.
This is the default behaviour out of the box. You do not need to configure anything.
To isolate a specific project, create a .pi/settings.json anywhere with any piExecutor key:
{
"piExecutor": {
"dataDir": "./.executor"
}
}When a project has any explicit executor settings, it gets its own sidecar:
- Its own data directory (defaults to
<cwd>/.executor) - Port scanning to find a free port
- Full lifecycle ownership — Pi starts it on demand and can stop it with
/executor-stop
Most useful for connecting local MCP servers:
await tools.executor.mcp.addServer({
transport: "stdio",
name: "ERPNext",
command: "node",
args: ["/path/to/erpnext-server/build/index.js"],
cwd: "/path/to/erpnext-server",
slug: "erpnext",
});Then create a connection. For env-var auth:
await tools.executor.coreTools.connections.create({
integration: "erpnext",
name: "default",
template: "env",
});For API key auth stored in the Executor keychain:
await tools.executor.coreTools.connections.createHandoff({
integration: "openapi_petstore",
label: "production",
});
// Returns a URL for the user to enter credentials in the web UIawait tools.executor.openapi.addSpec({
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
namespace: "petstore",
baseUrl: "https://petstore3.swagger.io/api/v3",
});await tools.executor.graphql.addEndpoint({
endpoint: "https://api.github.com/graphql",
namespace: "github",
});- Search for the tool you need
- Describe it to see the TypeScript shapes
- Call it with the full namespace path
const matches = await tools.search({ query: "linear issues", limit: 5 });
const path = matches.items[0]?.path;
if (!path) return "No matching tools found.";
const details = await tools.describe.tool({ path });
console.log(details.inputTypeScript);
const result = await tools.mcp_linear_app.list_issues({
project: "<project-id>",
limit: 5,
});
return result;By default (no project settings), pi-executor uses a single shared executor at ~/.executor on port 4788:
- The first session that needs executor checks if it's running via
server.jsonand a health check - If missing, it spawns the binary detached (
no-hupstyle) with logs to~/.executor/executor.stdout.log - No Pi session "owns" the global executor, so it survives shutdowns and restarts
- Other sessions simply discover and connect to the existing server
This means you can open Pi in many different projects and they all talk to the same executor instance.
When a project has any explicit settings in .pi/settings.json, it gets full isolation:
- Its own
.executor/directory inside the project - Port scanning from
4788upwards to find a free one - Pi owns the lifecycle: starts it on demand, stops it on shutdown,
/executor-stopworks - Auth tokens and integrations are scoped per directory
Current executor gates /api/* and /mcp behind a bearer token. The extension:
- Spawns the sidecar (or connects to an existing one)
- Reads the token from
.executor/server-control/auth.json - Passes it to the MCP client via
Authorizationheaders - Includes it in
/executor-webas?_token=...for auto-login
Sidecar stdout and stderr are written to:
.executor/executor.stdout.log.executor/executor.stderr.log
Use /executor-logs to view the last 200 lines from Pi, or read the files
directly.
The extension could not find the executor binary. Make sure pi-executor is
installed (it pulls executor as a dependency). If you have executor
installed globally, make sure it's on PATH or switch to remote mode pointing at
it.
- Check
.executor/executor.stdout.logand.executor/executor.stderr.log - Use
/executor-logsfrom Pi - Make sure no other executor is squatting the port (
lsof -i :4788) - Check if executor prompts for setup — run it manually with:
<executor_binary> web --port 4788 --foreground
Use /executor-web which opens with ?_token=... appended. If you opened the
bare URL manually, get the token from .executor/server-control/auth.json.
Another executor process owns ~/.executor. Either:
- Kill it (
pkill -f "executor web") and let Pi manage its own, or - Switch to remote mode pointing at the existing instance
Sessions in different projects with no explicit settings share the same global executor at ~/.executor. No conflicts.
Sessions in different projects with explicit project settings run fully isolated sidecars on separate ports. They do not interfere with each other.
If you want to share a single executor across all projects but still have Pi manage its lifecycle, switch one project to local mode with default settings and all others to remote mode pointing at it. But in practice, the global-local default handles this automatically.
cd ~/git/pi-executor-plugin
bun installThe extension loads TypeScript directly via jiti, so no build step is needed.
MIT — same as the original.