piagent_
← ~/guides

Publishing Extensions

Last updated: Aug 7, 2026

$ pi –publish

TL;DR. Local-first: drop a file in ~/.pi/agent/extensions/ or .pi/extensions/, or run pi -e ./my-extension.ts for a quick test; /reload hot-loads. To distribute, ship a package.json with a pi: { extensions: [...] } field pointing at the compiled JS and every runtime dependency in dependencies (not devDependencies — Pi installs with --omit=dev). Test via npm link + pi install link:, then publish to npm or push to a git host and install via pi install git:.... Extensions run with your full system permissions — declare what you need in the README.

A Pi extension starts life as a single TypeScript file. It graduates to a distributable package when you want other people — or your future self across multiple machines — to install it with pi install. This guide covers the structure of an extension package, the pi field in package.json, how to test locally before publishing, and the gotchas around runtime dependencies. It follows the Extensions documentation, the Packages documentation, and the working examples under packages/coding-agent/examples/extensions.

The minimum viable extension

Before worrying about packaging, make sure your extension works as a plain file. The smallest useful shape:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greet someone by name",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),
    async execute(_id, params) {
      return {
        content: [{ type: "text", text: `Hello, ${params.name}!` }],
        details: {},
      };
    },
  });
}

Save it as greet.ts. Test it without installing anything:

pi -e ./greet.ts

Inside the TUI, the LLM can now call the greet tool with a name parameter. If it does, you will see the registered tool listed under the available tools.

Where extensions are discovered

For local development, Pi picks up .ts files from these locations automatically:

  • ~/.pi/agent/extensions/*.ts — global, single file
  • ~/.pi/agent/extensions/*/index.ts — global, subdirectory
  • .pi/extensions/*.ts — project-local, single file
  • .pi/extensions/*/index.ts — project-local, subdirectory

Project-local extensions load only after the project has been trusted (resolved through the project_trust event). You can trigger a reload without restarting with /reload.

For non-standard paths — your own scratch directory, a checkout of someone’s repo — list them in settings.json:

{
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

Promoting a file to a package

When an extension grows beyond a single file or needs third-party dependencies, package it. The minimal package.json looks like this:

{
  "name": "pi-greet",
  "version": "0.1.0",
  "description": "Adds a greet tool to Pi.",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "dependencies": {
    "typebox": "^0.32.0",
    "@earendil-works/pi-coding-agent": "^0.83.0"
  },
  "pi": {
    "extensions": ["./dist/index.js"]
  }
}

Two details that catch people out:

  • pi.extensions is the entry-point manifest. It points at compiled JS (not the TS source) so Pi can load the package without a TypeScript toolchain at runtime.
  • dependencies, not devDependencies. Package installation uses production installs (npm install --omit=dev) by default, so anything you import at runtime must be in dependencies.

A peerDependencies entry on @earendil-works/pi-coding-agent is optional but recommended, since Pi itself ships with the matching version. Pinning loosely (^0.83.0) avoids spurious version conflicts when users upgrade Pi independently.

Testing the package before publishing

Before pushing to npm, link the package locally so a pi install of the local path works exactly as it would for an end user:

# Inside your extension repo
npm link

# Globally available; now test inside any project
pi install link:pi-greet

# Or, point at the built directory directly
pi -e /absolute/path/to/pi-greet/dist/index.js

Once linked, exercise every entry point: register a tool, subscribe to a session event, render custom UI. Confirm hot reload with /reload and that the extension survives a full Pi restart.

Publishing to npm

Standard npm publishing works:

npm login
npm publish --access public

Make sure dist/ is up to date (compile your TypeScript with your preferred tool — tsc, tsup, unbuild), package.json lists every runtime dependency, and the files field (or .npmignore) keeps source maps and dev artifacts out of the tarball.

After publishing, install from a fresh shell:

pi install npm:pi-greet
pi

Inside the new Pi session, the greet tool should be available just as it was during local testing.

Publishing to a git host

Git-based packages are useful when you want to install a private fork or pin to a specific commit. The package still needs a package.json with a pi field; users install with:

pi install git:github.com/your-org/pi-greet@v1.0.0

The Packages documentation covers the supported URL shapes and how to point at a subdirectory in a monorepo. Note that git installs use plain npm install (not --omit=dev) when npmCommand is configured, so wrappers and monorepos with hoisted dependencies work without surprise.

What you can build on

A handful of official packages are exposed to extension authors:

Package Use it for
@earendil-works/pi-coding-agent Extension types (ExtensionAPI, ExtensionContext, events)
typebox JSON 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

If your package needs to add a custom provider rather than just a tool, see the Custom providers documentation. The full-provider extension surface (registration, model refresh, filtering, custom streaming) shipped in v0.81.0.

Permissions, trust, and disclosure

Pi extensions run with your full system permissions and can execute arbitrary code. The agent makes no sandboxing attempt, so the install trust model is “whoever ships the package ships the code.” Two practices keep your users safe and your package credible:

  1. State the permissions your extension needs in the README. If you touch the network, write files outside the working directory, or invoke subprocesses, say so.
  2. Keep the entry-point surface narrow. A single pi.extensions entry that re-exports everything is easier to audit than a sprawling tree of side-effect imports.

For project-local extensions, trust is gated by the project_trust event — extensions do not auto-load in a freshly cloned repo until the user has explicitly trusted it.

A small publishing checklist

Before tagging 1.0.0:

  • package.json has name, version, description, type: "module", pi.extensions, and every runtime dependency.
  • npm pack --dry-run lists only the files you intend.
  • npm install in a clean directory produces a working package.
  • pi install npm:your-package in a fresh Pi session loads the extension.
  • README documents install, configure, and the permissions the extension uses.
  • Version is pinned relative to @earendil-works/pi-coding-agent to avoid breaking changes in the API.

For ecosystem visibility, drop a one-line entry on this site’s resource map under the extensions or tools category — see the Contributing page for the schema and submission flow.

Further reading