# How to write a Chi module

A Chi module is a separate Pi extension. It listens for `chi:discover`,
registers a definition, and starts in `initialize` after its dependencies are
ready.

## 1. Add the dependencies

Keep Chi Base as a peer dependency, use it as a development dependency for
TypeScript, and include Zod as a runtime dependency:

~~~json
{
  "peerDependencies": {
    "@henkaku-center/chi-base": "0.1.0",
    "@earendil-works/pi-coding-agent": "*"
  },
  "devDependencies": {
    "@henkaku-center/chi-base": "0.1.0"
  },
  "dependencies": {
    "zod": "^3.25.76"
  }
}
~~~

Pin the Chi Base version to the compatible contract used by the module.

## 2. Register during discovery

The contract is type-only. Runtime communication uses the `ChiBase` object
received from the event:

~~~ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { z } from "zod";
import type { ChiBase } from "@henkaku-center/chi-base/contract";

const config = z.object({
  enabled: z.enum(["on", "off"]).default("on"),
});
let currentConfig: z.infer<typeof config> | undefined;

export default function myModule(pi: Pick<ExtensionAPI, "events">) {
  pi.events.on("chi:discover", (data) => {
    const chi = data as ChiBase;
    chi.register({
      id: "chi-my-module",
      version: "1.0.0",
      dependencies: ["chi-example"],
      api: {
        run: () => "work",
      },
      config: {
        schemaVersion: 1,
        schema: config,
      },
      initialize(chi, parsedConfig) {
        currentConfig = parsedConfig;
        const dependency = chi.require<{ ping(): string }>("chi-example");
        if (parsedConfig.enabled === "on") dependency.ping();
        // Start resources and register handlers here.
      },
      onConfigChange(_chi, current, _changes) {
        // Replace retained runtime state; initialize is not called again.
        currentConfig = current;
      },
    });
  });
}
~~~

`dependencies` is optional. Use `chi.get<T>(id)` for an optional dependency
and `chi.require<T>(id)` for a required one. A required module must be ready
before `initialize` runs.

## 3. Use configuration correctly

`parsedConfig` is validated configuration for the active session. Configuration
is stored separately in global and trusted-project scopes. A change made from
`/chi` is persisted immediately, then passed to `onConfigChange(current,
changes)` when the module provides that callback. Update retained runtime state
there; `initialize` is not called again. Modules without the callback can read
the new value with `chi.getConfig(id)`.

Pi's event bus remains the discovery bridge; Chi configuration changes use the
typed module callback. Callback failures reject the update and are reported to
the settings command.

The current settings editor exposes required top-level `z.enum` fields and
defaulted `z.string` fields. Enum rows cycle on Enter/Space. String rows enter
edit mode on Enter and persist on a second Enter; Escape cancels and an empty
confirmation applies the default. Add a `migrate` function when a future schema
version needs to convert older data.

## 4. Try the module locally

Install the local package and load the module alongside the test fixture:

~~~sh
pnpm install
pi install "$PWD"
pi -e "$PWD/src/extension.ts" \
  -e "$PWD/tests/fixtures/example-module.ts" \
  -e "$PWD/path/to/my-module.ts"
~~~

The dependent fixture in `tests/fixtures/dependent-module.ts` demonstrates a
module with both a dependency and an editable setting. See [DEVELOPMENT.md](../DEVELOPMENT.md)
for the complete local testing workflow.

For the full interface, see [the module contract](contract.md),
[configuration and migrations](configuration.md), and [the example module](example-module.md).
