Pi Extensions: Location, Storage, and Loading

pi is a coding agent whose behavior can be extended through extensions. Extensions are TypeScript modules that hook into lifecycle events, register custom tools for the LLM, add commands, and more.

This article explains where extensions live on the system, how they are structured, and how pi knows to use and invoke one. The two different paths are compared: extensions from the npm route (installed packages) and extensions from the manual auto-discover route.

Table of Contents


What an extension is

An extension is a TypeScript module that by default exports a factory function. This function receives an ExtensionAPI instance and connects to it through pi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greets someone by name",
    // ...
  });
}

Important: pi loads extensions via jiti. This means TypeScript is executed directly, without a build step.

The entry point determines the directory

There is no mandated pi-standard directory for extension source code. pi only cares about the entry point you declare in package.json under pi.extensions. The directory is freely choosable.

The three styles described in the docs:

1
2
3
4
5
6
7
1. Single File          2. Directory + index.ts              3. Package with dependencies
~/.pi/agent/            ~/.pi/agent/                         ~/.pi/agent/
  extensions/             extensions/                          my-ext/
  my-extension.ts         my-extension/                      package.json
                          index.ts        ← Entry Point      src/index.ts  ← Entry Point
                          tools.ts                             ...
                          utils.ts

Note on src/: In the official example extensions (examples/extensions/), the entry point is almost always placed directly as index.ts in the package root β€” not in src/. The src/ in the docs only appears in the illustrative example of the β€œPackage with dependencies” style. It is a general TypeScript/npm convention, not a pi-specific requirement.

Route 1: Extensions from npm packages

Installed extensions usually do not live in a ~/.pi/agent/extensions/ folder, but in pi’s npm folder:

1
2
3
4
5
6
7
8
9
10
11
12
13
~/.pi/agent/
β”œβ”€β”€ npm/
β”‚   β”œβ”€β”€ package.json              ← your dependencies
β”‚   β”œβ”€β”€ package-lock.json
β”‚   └── node_modules/
β”‚       β”œβ”€β”€ pi-zentui/
β”‚       β”œβ”€β”€ pi-web-access/
β”‚       β”œβ”€β”€ pi-mcp-adapter/
β”‚       └── @juicesharp/
β”‚           β”œβ”€β”€ rpiv-todo/
β”‚           └── rpiv-ask-user-question/
β”œβ”€β”€ settings.json
└── ...

The connection is created through a three-step chain:

1. settings.json β†’ packages lists the packages:

1
2
3
4
5
6
7
{ "packages": [
  "npm:pi-zentui",
  "npm:pi-web-access",
  "npm:pi-mcp-adapter",
  "npm:@juicesharp/rpiv-ask-user-question",
  "npm:@juicesharp/rpiv-todo"
]}

pi installs these packages on startup into ~/.pi/agent/npm/node_modules/.

2. Each package declares a pi.extensions entry in its package.json β€” this is the anchor pi uses to recognize that the package ships an extension:

1
2
3
4
5
6
7
8
// e.g. pi-zentui
{ "pi": { "extensions": ["./extensions"] } }

// e.g. rpiv-todo
{ "pi": { "extensions": ["./index.ts"] } }

// pi-mcp-adapter can declare both
{ "pi": { "extensions": ["./index.ts"], "skills": ["./skills"] } }

3. pi loads the specified entry point (file or directory with index.ts) via jiti.

A special case is pi-zentui: it packs everything into a subfolder and points at the folder:

1
{ "files": ["extensions"], "pi": { "extensions": ["./extensions"] } }

pi resolves ./extensions and finds extensions/zentui/index.ts.

Route 2: Manual extensions in the auto-discover route

The folder ~/.pi/agent/extensions/ (or .pi/extensions/ in the project) is the manual auto-discover route for extensions you place directly as files β€” i.e. without an npm package. It is clearly separate from the npm route.

Location Scope
~/.pi/agent/extensions/*.ts Global (all projects)
~/.pi/agent/extensions/*/index.ts Global (subdirectory)
.pi/extensions/*.ts Project-local
.pi/extensions/*/index.ts Project-local (subdirectory)

Additional paths can be added via settings.json:

1
2
3
4
5
6
{
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

Extensions in the auto-discover route can be hot-reloaded with /reload. Extensions tested only with pi -e ./path.ts are active for that single run only.

How pi recognizes and invokes an extension

Not by a fixed folder name, but by three levels:

Level What How pi recognizes it
Declaration settings.json β†’ packages Package is installed
Entry Point package.json β†’ pi.extensions Lists the .ts files/directories
Contract Default-exported factory function pi.on(...), pi.registerTool(...), pi.registerCommand(...)

The actual β€œinvocation” happens through the lifecycle events and registered tools/commands that pi processes during a run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pi starts
  β”‚
  β”œβ”€β–Ί project_trust
  β”œβ”€β–Ί session_start
  └─► resources_discover
      β”‚
      β–Ό
  user sends a prompt
      β”‚
      β”œβ”€β–Ί (extensions: check commands first)
      β”œβ”€β–Ί input
      β”œβ”€β–Ί before_agent_start
      β”œβ”€β–Ί agent_start
      β”‚
      β”‚   β”Œβ”€β”€β”€ turn (repeats while the LLM calls tools) ───┐
      β”‚   β”‚     β”œβ”€β–Ί tool_call        (can be blocked)       β”‚
      β”‚   β”‚     β”œβ”€β–Ί tool_result     (can modify result)     β”‚
      β”‚   β”‚     └─► ...                                         β”‚
      β”‚   └─► turn_end
      β”‚
      β”œβ”€β–Ί agent_end
      └─► agent_settled

The most important events:

Event Purpose
session_start Session started / loaded / reloaded
tool_call The LLM called a tool β€” can be blocked
tool_result Tool result can be modified
before_agent_start Inject a message or modify the system prompt
input Intercept, transform, or process user input
resources_discover Contribute skill/prompt/theme paths

Structure of an extension package

A typical package has an index.ts as the entry point (default export = factory function) plus a modular remainder:

1
2
3
4
5
6
7
8
~/.pi/agent/npm/node_modules/@juicesharp/rpiv-todo/
β”œβ”€β”€ package.json          ← pi.extensions: ["./index.ts"]
β”œβ”€β”€ index.ts              ← Entry Point (factory)
β”œβ”€β”€ config.ts
β”œβ”€β”€ locales/
β”œβ”€β”€ state/                ← store, selectors, reducer ...
β”œβ”€β”€ tool/                 ← registerTool logic
└── view/                 ← TUI components

Available imports for extensions:

Package Purpose
@earendil-works/pi-coding-agent Extension types (ExtensionAPI, ExtensionContext, events)
typebox Schema definitions for tool parameters
@earendil-works/pi-ai AI utilities (StringEnum for Google-compatible enums)
@earendil-works/pi-tui TUI components for custom rendering

npm dependencies also work: create a package.json next to the extension, run npm install, and imports from node_modules/ are resolved automatically. Node.js built-in modules (node:fs, node:path, …) are also available.

Conclusion

  • There is no prescribed source-code directory for extensions. pi looks at the entry point declared in pi.extensions.
  • npm packages land in ~/.pi/agent/npm/node_modules/ and are installed via the packages entry in settings.json. The anchor is pi.extensions in the package’s package.json.
  • Manual extensions are placed directly in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (project-local).
  • pi recognizes an extension by the combination of declaration (packages / pi.extensions) and contract (default-exported factory function with pi.on, pi.registerTool, pi.registerCommand).
  • On the src/ vs extensions/ discussion: both are possible. For a pure pi extension, extensions/ is even particularly close to the auto-discover path ~/.pi/agent/extensions/.