MusePi

Extension Loading (TypeScript/JavaScript Modules)

This document covers how the coding agent discovers and loads extension modules (.ts/.js) at startup.

It does not cover gemini-extension.json manifest extensions (documented separately).

What this subsystem does

Extension loading builds a list of module entry files, imports each module with Bun, executes its factory, and returns:

Primary implementation files


Inputs to extension loading

1) Auto-discovered native extension modules

discoverAndLoadExtensions() first asks discovery providers for extension-module capability items, then keeps only provider native items.

Native extension-module discovery comes from:

The project root is the native provider’s .omp directory (SOURCE_PATHS.native.projectDir), cwd-only; it does not walk ancestors. The user root is the active profile’s agent directory via getAgentDir(), so under musepi --profile <name> it becomes ~/.musepi/profiles/<name>/agent/extensions (and it honors PI_CODING_AGENT_DIR). See Profiles.

Notes:

2) Discovered JS/TS hook factories

After native auto-discovery, discoverAndLoadExtensions() also appends JS/TS hook factories from the hook capability — any hook whose entry path is a .ts/.js file — so they load through the same module pipeline.

Hook-capability loading already applies its own hook-specific disabled ids, so these paths are not additionally filtered by disabledExtensions extension-module names.

3) Installed plugin extension entries

After hook discovery, discoverAndLoadExtensions() appends extension entry points from enabled installed plugins via getAllPluginExtensionPaths(cwd).

Plugin extension entries come from package omp.extensions / pi.extensions manifests, including enabled feature entries.

4) Explicitly configured paths

After plugin extension entries, configured paths are appended and resolved.

Configured path sources in the main session startup path (sdk.ts):

  1. CLI-provided paths (--extension/-e, and --hook is also treated as an extension path)
  2. Merged settings extensions array

Settings files:

Native extension-module discovery also reads legacy JSON extension lists from:

Examples:

# ~/.musepi/agent/config.yml
extensions:
  - ~/my-exts/safety.ts
  - ./local/ext-pack
{
  "extensions": ["./.musepi/extensions/my-extra"]
}

Enable/disable controls

Disable discovery

Behavior split:

This flag governs extension factories and OMP extension-package sibling roots; it is not a whole-process capability-isolation switch. Skills, MCP servers, tools, prompts, and rules owned by other discovery subsystems retain their own enable/disable controls.

Disable specific extension modules

disabledExtensions setting filters by extension id format:

derivedName is based on entry path (getExtensionNameFromPath), for example:

Example:

disabledExtensions:
  - extension-module:foo

Path and entry resolution

Path normalization

For configured paths:

  1. Normalize unicode spaces
  2. Expand ~
  3. If relative, resolve against current cwd

If configured path is a file

It is used directly as a module entry candidate.

If configured path is a directory

Resolution order:

  1. package.json in that directory with omp.extensions (or legacy pi.extensions) -> use declared entries
  2. index.ts
  3. index.js
  4. Otherwise scan one level for extension entries:
    • direct *.ts / *.js
    • subdir index.ts / index.js
    • subdir package.json with omp.extensions / pi.extensions

Rules and constraints:

Ignore behavior differs by source


Load order and precedence

discoverAndLoadExtensions() builds one ordered list and then calls loadExtensions().

Order:

  1. Native auto-discovered modules
  2. Discovered JS/TS hook factories
  3. Installed plugin extension entries
  4. Explicit configured paths (in provided order)

In sdk.ts, configured order is:

  1. CLI additional paths
  2. Settings extensions

De-duplication:

Implication: if the same module path is both auto-discovered and explicitly configured, it is loaded once at the first position (auto-discovered stage).


Module import and factory contract

Each candidate path is loaded via loadLegacyPiModule() (src/extensibility/plugins/legacy-pi-compat.ts):

If export is not a function, that path fails with a structured error and loading continues.


Failure handling and isolation

During loading

Per extension path, failures are captured as { path, error } and do not stop other paths from loading.

Common cases:

Runtime isolation model

After loading

When events run through ExtensionRunner, handler exceptions are caught and emitted as extension errors instead of crashing the runner loop.


Minimal user/project layout examples

User-level

~/.musepi/agent/
  config.yml
  extensions/
    guardrails.ts
    audit/
      index.ts

Project-level

<repo>/
  .musepi/
    settings.json
    extensions/
      checks/
        package.json
      lint-gates.ts

checks/package.json:

{
  "omp": {
    "extensions": ["./src/check-a.ts", "./src/check-b.js"]
  }
}

Legacy manifest key still accepted:

{
  "pi": {
    "extensions": ["./index.ts"]
  }
}