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

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 and the loading mechanism orchestrated by loader.ts.

Extension Architecture Overview

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

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) and export an async factory that returns an Extension object:

// 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:

// 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. 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 π:

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

To load multiple extensions simultaneously, repeat the flag:

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 to instantiate extensions in a controlled environment:

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:

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:

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

Ensure your 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.

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 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →