# How to Create Extensions for Earendil Pi Agents: A Complete Developer’s Guide

> Learn to create Earendil Pi agent extensions. Export a factory function returning an Extension interface object with metadata and commands. Load with the --extension CLI flag.

- Repository: [Earendil Works/pi](https://github.com/earendil-works/pi)
- Tags: how-to-guide
- Published: 2026-05-25

---

**To create an extension for Earendil π agents, export a default factory function that receives an `ExtensionRuntime` and returns an object implementing the `Extension` interface with metadata, commands, and optional UI components, then load it via the `--extension` CLI flag.**

The **earendil-works/pi** repository provides a modular plug-in system that allows developers to add new slash-commands, UI widgets, and runtime behaviors to the built-in coding agent. Understanding how to create extensions for Earendil pi agents requires familiarity with the core interfaces defined in [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) and the loading mechanism orchestrated by [`loader.ts`](https://github.com/earendil-works/pi/blob/main/loader.ts).

## Extension Architecture Overview

The extension system is built around six core components that handle discovery, instantiation, and integration:

- **Extension Interface** – Defines the contract for metadata (`name`, `description`, `version`), command handlers, and UI hooks. Defined in [[`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts)](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts).
- **ExtensionRuntime** – Supplies services including the event bus (`runtime.eventBus`), logger (`runtime.log`), and UI factory methods like `runtime.createDialog`. Also defined in [`types.ts`](https://github.com/earendil-works/pi/blob/main/types.ts).
- **Loader** – Discovers and instantiates extensions via `loadExtensions` and `loadExtensionFromFactory` in [[`packages/coding-agent/src/core/extensions/loader.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/loader.ts)](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/loader.ts).
- **ResourceLoader** – Integrates extensions into the agent’s resource graph by calling `loadExtensionFactories` from [[`packages/coding-agent/src/core/resource-loader.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/resource-loader.ts)](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/resource-loader.ts).
- **Extension Bindings** – Exposes registered commands as slash-commands (`/command-name`) and manages event emission through `ExtensionBindings` in [[`packages/coding-agent/src/core/agent-session.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts)](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts).
- **UI Layer** – Renders optional components (status bars, dialogs) based on `ExtensionUIDialogOptions` and `ExtensionWidgetOptions` types.

When the agent starts, `ResourceLoader` invokes `loadExtensions` with paths provided via the `--extension` flag. Each path is resolved as an ES module, imported dynamically, and the default export (a factory function) is called with a fresh `ExtensionRuntime` instance.

## Creating Your First Extension

Every extension follows a three-step pattern: export a factory, define metadata and commands, and optionally provide UI components.

### Minimal "Hello World" Extension

Create a new file (for example, [`packages/coding-agent/examples/extensions/hello.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/hello.ts)) and export an async factory that returns an `Extension` object:

```typescript
// packages/coding-agent/examples/extensions/hello.ts
export default async function helloExtension(runtime: ExtensionRuntime) {
  return {
    name: "hello",
    description: "Demo extension that greets the user",
    version: "0.1.0",
    commands: [
      {
        name: "greet",
        description: "Say hello",
        handler: async (ctx: ExtensionCommandContext) => {
          ctx.api.sendMessage("👋 Hello from the hello extension!");
        },
      },
    ],
  };
}

```

**Key implementation details:**

- The factory receives `runtime` containing utilities like `runtime.log` and `runtime.eventBus`.
- The `commands` array defines slash-commands accessible via `/greet` in the interactive UI.
- Each command handler receives an `ExtensionCommandContext` (`ctx`) providing access to the agent’s messaging API.

### Adding UI Components

Extensions can render status-line indicators or dialogs by returning a `ui` object. The following example creates a dynamic status widget:

```typescript
// packages/coding-agent/examples/extensions/status-line.ts
export default async function statusLineExtension(runtime: ExtensionRuntime) {
  const widget = runtime.createStatusLine({
    render: (state) => (state.isThinking ? "⏳" : "✅"),
  });

  return {
    name: "status-line",
    description: "Shows a tiny status indicator",
    version: "0.1.0",
    ui: {
      statusLine: widget,
    },
  };
}

```

The `ui` field accepts any combination of `statusLine`, `dialog`, or `widget` options as defined in [`types.ts`](https://github.com/earendil-works/pi/blob/main/types.ts). These components are rendered by the agent’s TUI layer without requiring manual DOM manipulation.

## Loading Extensions at Runtime

Pass the file path to your extension factory using the `--extension` flag when launching π:

```bash
pi --extension ./packages/coding-agent/examples/extensions/hello.ts

```

To load multiple extensions simultaneously, repeat the flag:

```bash
pi \
  --extension ./packages/coding-agent/examples/extensions/hello.ts \
  --extension ./packages/coding-agent/examples/extensions/status-line.ts

```

Under the hood, `loadExtensions` performs the following operations:

1. Resolves each path relative to the current working directory.
2. Imports the module using `await import(extensionPath)`.
3. Invokes the default export (the factory) with a newly created `ExtensionRuntime`.
4. Registers commands with `ExtensionBindings` and UI components with the session renderer.

## Testing Extensions

The repository provides a test harness to validate extensions without running a full agent session. Import `createTestExtensionsResult` from [`loader.ts`](https://github.com/earendil-works/pi/blob/main/loader.ts) to instantiate extensions in a controlled environment:

```typescript
import { createTestExtensionsResult } from "../src/core/extensions/loader.ts";
import { createTestResourceLoader } from "../src/core/resource-loader.ts";

const extensionsResult = await createTestExtensionsResult(
  [helloExtensionFactory, statusLineExtensionFactory],
  tempDir
);

const resourceLoader = createTestResourceLoader({ extensionsResult });
await resourceLoader.load(); // Installs extensions into the test agent

```

Execute specific test suites using Vitest:

```bash
node ../../node_modules/vitest/dist/cli.js --run packages/coding-agent/test/extensions/hello.test.ts

```

## Publishing and Distribution

To distribute an extension as a standalone package, structure your project as an ES module that depends on `@earendil/pi` (the core agent types). Expose the factory function as the default export from your package entry point. Consumers can then reference the compiled JavaScript file directly:

```bash
pi --extension ./node_modules/my-extension/dist/index.js

```

Ensure your [`package.json`](https://github.com/earendil-works/pi/blob/main/package.json) specifies `"type": "module"` and lists `@earendil/pi` as a peer dependency to avoid version conflicts with the host agent.

## Summary

- **Export a factory**: Extensions must export a default async function receiving `ExtensionRuntime` and returning an `Extension` object.
- **Define commands**: Register interactive commands in the `commands` array; these become accessible as slash-commands (`/command-name`).
- **Optional UI**: Inject status bars or dialogs via the `ui` field using runtime factory methods.
- **Load via CLI**: Use the `--extension` flag to specify paths to extension files or compiled modules.
- **Test isolated**: Use `createTestExtensionsResult` and `createTestResourceLoader` to unit-test extension logic without booting the full agent.

## Frequently Asked Questions

### What is the minimum required structure for an Earendil Pi extension file?

At minimum, the file must export a default async factory function that accepts an `ExtensionRuntime` parameter and returns an object containing `name`, `description`, `version`, and optionally `commands` or `ui` properties. The factory can be defined in TypeScript or JavaScript as long as it conforms to the `Extension` interface defined in [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts).

### Can extensions access the agent’s LLM or tool-calling capabilities?

Yes. The `ExtensionCommandContext` passed to command handlers includes `ctx.api`, which provides methods to send messages and interact with the agent’s core systems. For deeper integration, extensions can listen to the `runtime.eventBus` to react to agent lifecycle events or emit custom events that other extensions can consume.

### How do I debug an extension during development?

Run the agent with the `--extension` flag pointing to your source file and enable verbose logging. The `runtime.log` object provided to your factory supports standard log levels (`debug`, `info`, `error`). Additionally, you can write unit tests using `createTestExtensionsResult` from [`packages/coding-agent/src/core/extensions/loader.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/loader.ts) to verify command handlers and UI state outside the interactive TUI.

### Are extensions hot-reloadable without restarting the agent?

No. According to the current implementation in [`loader.ts`](https://github.com/earendil-works/pi/blob/main/loader.ts), extensions are discovered and instantiated once during the agent’s initialization phase when `ResourceLoader` calls `loadExtensions`. To apply changes, you must restart the π process with the `--extension` flag.