This gist contains both the supported current-Pi approach and a custom raw-process approach.
| Example | Status | Files |
|---|---|---|
| Supported dialog bridge | Works with current Pi | client.mjs, server.ts |
| Custom raw-process bridge | Works with Pi versions before 0.62.0 | custom-client.mjs, custom-server.ts |
Both examples keep the file on the client device until the client chooses to send it. The Pi extension receives a response and returns a normal tool result.
- Node.js 20 or later
- Pi 0.80.6 or later, authenticated with a tool-capable model
- A regular local file
The examples do not impose a file-size limit. The practical limit is set by available memory, process I/O, transport limits, and any model context included by the server-side handler. Base64 adds size overhead. Production code should set its own size, type, timeout, and consent policy.
client.mjs starts Pi in RPC mode and selects the local file. server.ts is a Pi extension loaded into that process. When the model calls receive_file_from_client, the extension uses the supported editor dialog. The client replies with a JSON envelope containing the filename, MIME type, and base64-encoded bytes. The extension validates the bytes, computes a SHA-256 digest, and returns a tool result.
+-------------------+ +--------------------------+
| RPC client/device | | Pi process + server.ts |
| reads local file | | extension tool |
+---------+---------+ +------------+-------------+
| |
| prompt |
+----------------------------------------------->|
| | model calls tool
| extension_ui_request: method=editor |
|<-----------------------------------------------+
| |
| extension_ui_response: value=<base64 envelope> |
+----------------------------------------------->|
| | decode, hash
| tool_execution_end |
|<-----------------------------------------------+
Run it:
node client.mjs /path/to/file.pdfPass normal Pi options after the file path when needed:
node client.mjs ./notes.txt --provider anthropic --model claude-sonnet-4-20250514Set PI_BIN when pi is not on PATH:
PI_BIN=/path/to/pi node client.mjs ./notes.txtExpected output includes both sides of the transfer:
[client] Sending notes.txt (42 bytes) to the server extension.
[pi] [receive-file] received notes.txt: 42 bytes, sha256=...
[server tool result]
Received notes.txt from the RPC client.
MIME type: text/plain
Bytes: 42
SHA-256: ...
A custom native_tool_call method can write its own extension_ui_request JSON line and listen directly to process.stdin; the client recognizes that method, runs a native tool, and replies with a result envelope.
+-------------------+ +-----------------------------------+
| RPC client/device | | Pi process + custom-server.ts |
| local tool runner | | custom stdout/stdin bypass |
+---------+---------+ +----------------+------------------+
| |
| extension_ui_request: method=native_tool_call |
|<---------------------------------------------------+
| |
| run tool on device |
| |
| extension_ui_response: { ok, result } |
+--------------------------------------------------->|
| | resolve pending tool call
The JavaScript client handles the custom request and sends its file envelope back as a normal response:
if (event.type === "extension_ui_request" && event.method === "native_tool_call") {
const bytes = readFileSync(filePath);
send({
type: "extension_ui_response",
id: event.id,
value: {
ok: true,
result: {
name: basename(filePath),
mimeType: mimeTypeFor(filePath),
data: bytes.toString("base64"),
},
},
});
}Run this bridge with a compatible Pi binary:
PI_BIN=/path/to/pi-0.61-or-earlier node custom-client.mjs /path/to/file.pdfPi 0.62.0 and later reserve stdout for RPC output. In current Pi source, runRpcMode() calls takeOverStdout() before extensions load; takeOverStdout() redirects process.stdout.write() to stderr. That prevents custom-server.ts from putting its request on the RPC stdout stream. The supported dialog bridge above is the current-Pi approach.
Current Pi RPC mode accepts only its documented extension UI methods. ctx.ui.custom() is TUI-only and emits nothing in RPC mode. The supported example uses the editor dialog:
server.tscallsctx.ui.editor(...)inside the registered tool.- Pi emits
extension_ui_requestwithmethod: "editor". client.mjsreplies withextension_ui_responsecontaining a string value.- The extension decodes that string and returns a normal Pi tool result.
The client is intentionally the only process that reads the local file. The Pi process receives only the response payload. For PDF, audio, or other binary data, replace the SHA-256 handling in server.ts with the server-side processor you need. Do not send sensitive files without explicit user consent.