> ## Documentation Index
> Fetch the complete documentation index at: https://actelos.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Custom Module

> File layout, registration, and the end-to-end loop

This page covers the mechanics shared by every custom module, registration, and
lifecycle. For type-specific skeletons and examples see:

* [Writing a custom adapter module](/cyrnel/specs/writing-custom-adapter-module)
* [Writing a custom environment module](/cyrnel/specs/writing-custom-environment-module)

For the full interface reference, see [`AdapterModule`](/cyrnel/specs/adapter-module)
and [`EnvironmentModule`](/cyrnel/specs/environment-module).

## Installation

To use the cyrnel SDK in your TypeScript codebase, install the
[`@cyrnel/sdk`](https://www.npmjs.com/package/@cyrnel/sdk) package:

```bash theme={null}
npm install @cyrnel/sdk
```

## Where Modules Live

Custom modules live under `$CYRNEL_DATA_DIR/modules/<id>/`. Each directory
must contain a `module.json`:

```json theme={null}
{
  "$schema": "https://raw.githubusercontent.com/actelos/cyrnel/main/schema/module.schema.json",
  "id": "my-adapter",
  "name": "My Adapter",
  "type": "adapter",
  "description": "Talks to my service",
  "main": "./index.js"
}
```

Fields:

* `id`: Unique identifier for the module.
* `name`: Human readable name for the module.
* `type`: `Adapter` or `environment`.
* `description`: Surfaced in `GET /modules`.
* `main`: Path to the built module file the host can `import()`. The file must
  default-export the module object (see below).

The full JSON Schema is available at
[`schema/module.schema.json`](https://github.com/actelos/cyrnel/blob/main/schema/module.schema.json).
You can reference it from your `module.json` via `$schema` for editor
autocompletion and validation.

The host expects compiled JavaScript here, not TypeScript. The
expectation is that you ship modules built ahead of time.

## Registering a Module

1. Place the built directory at `$CYRNEL_DATA_DIR/modules/my-adapter/`.
2. Ensure it contains `module.json` and the file named in `main`.
3. Either restart the API or call:

   ```bash theme={null}
   curl -X POST http://localhost:9371/modules/reload
   ```

`reload` re-scans `$CYRNEL_DATA_DIR/modules/`, registers any new directories,
and reconciles the result against the `modules` table. A new module is
inserted with `enabled: true, missing: false` by default.

## Verifying

```bash theme={null}
curl http://localhost:9371/modules
```

You should see your module alongside the built-ins, with
`isBuiltin: false`.

To explicitly enable / disable:

```bash theme={null}
curl -X POST http://localhost:9371/modules/my-adapter/enabled \
  -H 'content-type: application/json' \
  -d '{"enabled":true}'
```

For adapters, enabling triggers `setup({ config, secrets })` and hydration
of any enabled services that target it. For environments, enabling makes it
the active environment and drains the previous one.

## Removing a Module

Stop the API, delete `$CYRNEL_DATA_DIR/modules/<id>/`, and restart. Or
delete the directory and `POST /modules/reload`.

The `modules` row is **not** deleted, it's marked `missing: true`. The
`enabled` flag is preserved so the module resumes at its previous state if it
reappears. Trying to enable a missing module returns `409`.

## Module ID Collisions

If a custom module declares a `name` that already exists (built-in or
otherwise registered), the host keeps the first registration and skips
the duplicate. Built-ins are registered first.

## Module Entry File

The file pointed to by `main` must default-export an object with the
following shape:

```ts theme={null}
import type { ModuleExport } from "@cyrnel/sdk";

// For adapter modules:
import type { AdapterModule } from "@cyrnel/sdk";

export default {
  configSchema: { /* JSON Schema for module-level config */ },
  secretsSchema: { /* JSON Schema for module-level secrets */ },
  instantiate(): AdapterModule {
    return new MyAdapter();
  },
} satisfies ModuleExport;
```

* `configSchema` and `secretsSchema` must be **plain JSON-only objects**.
  No functions, class instances, Proxies, symbols, or circular references.
  The host validates this at registration time.
* `instantiate` is a zero-argument factory that returns a fresh module
  instance. The host calls it each time the module is activated.

For the type-specific skeletons see:

* [Writing a custom adapter module](/cyrnel/specs/writing-custom-adapter-module)
* [Writing a custom environment module](/cyrnel/specs/writing-custom-environment-module)

## Iteration Workflow

While developing:

1. Build the module (`tsc`, `esbuild`, whatever).
2. `cp -r dist $CYRNEL_DATA_DIR/modules/<id>/`.
3. `POST /modules/reload`.
4. Disable + re-enable the module to force `teardown` / `setup`.

The `reload` endpoint refreshes the registry but does not re-activate
already-enabled modules. Toggle `enabled` to pick up code changes.

## Packaging Notes

* The host loads `main` with the native `import()`, it must be an ESM
  file the Node runtime can resolve directly. Bundle into a single file
  if your module has dependencies; the loader does not run `npm install`
  for you.
* `peerDependencies` are not honoured. If your module needs `@cyrnel/sdk`
  types, ship them as build-time `devDependencies` and bundle anything
  you import.
* Keep modules narrow. A single module that does many unrelated things
  is harder to review than several small ones.
