Spirit
Download

Extensions

Create extensions with the npm create wizard or by hand, and run your own extension marketplace.

An extension is a folder with a .spirit/extension.json — the Spirit manifest dump — next to a pure-npm package.json. Spirit installs extensions per user and per host, not under the workspace .spirit/ directory, and scopes each install by the marketplace it came from: the install identity is (source id, name).

HostPath
Desktop{spiritDataDir}/extensions/desktop/<sourceId>/<name>/
CLI{spiritDataDir}/extensions/cli/<sourceId>/<name>/

That is different from Skills (SKILL.md folders), MCP (external servers), and Hooks (hooks.json scripts). contributes.cli.hooks styles CLI TUI slots. It is not hooks.json. An extension can still bundle those same files when it declares mcp, hooks, skills, or rules.

Each installed extension has an enable switch. Disable or uninstall it to stop its contributions. Turning the extension on is consent to run the MCP servers, agent hooks, skills, and rules it declared.

Create an extension

Scaffold a new extension with the create wizard:

npm create @spiritagent/extension

The wizard asks for an extension name (kebab-case), a display name, a one-line description, and the capabilities to scaffold. It writes package.json, icon.svg, and .spirit/extension.json, plus the files for the capabilities you picked (skills/<name>/SKILL.md, rule.md, mcp.json, hooks.json, styles.css, index.mjs), and prints a ready-to-paste marketplace entry when it finishes.

For scripting, every prompt has a flag: [target-dir], --name, --display-name, --description, --capabilities <csv>, and --yes to skip confirmation.

Validate the result at any time:

npx @spiritagent/extension-toolkit check ./my-extension

The manifest

The manifest object is the extension's runtime declaration: which hosts it supports, when it activates, and what it contributes. In a marketplace entry, the manifest is endorsed by the marketplace operator; in a ZIP package, it is self-declared and gated by the review confirmation.

Capabilities

ValueRuntime
tool-definitionsRequired with tool-execution to expose contributes.tools to the model
tool-executionRequired with tool-definitions to run those tools
system-promptRequired with main to contribute a system prompt fragment
desktop-uiMust be paired with contributes.desktop
cli-uiMust be paired with contributes.cli
mcpMust be paired with contributes.mcp; loads package-root mcp.json
hooksMust be paired with contributes.hooks; loads package-root hooks.json
skillsMust be paired with contributes.skills; loads skills/<name>/SKILL.md
rulesMust be paired with contributes.rules; loads package-root rule.md
approval-flowDeclared only. Approval is driven by each tool's approvalMode
questions-flowDeclared only. Questions are driven by approvalMode: need-questions
settingsDeclared only. Settings come from settingsSchema
secret-storageUsed with secretSlots
structured-resultsDeclared only

desktop-ui / cli-ui / mcp / hooks / skills / rules and the matching contributes entry must both be present or both be absent.

Activation events

EventWhen it fires
onStartupHost warmup
onExtensionInstalledAfter an install completes
onSessionOpenedSession becomes active
onSessionResetSession reset
onUserMessageUser submits a message
onToolCallA tool is about to run
onToolResultA tool result is available
onApprovalResolvedAn approval decision is resolved

The host calls activate when the extension lists the event and has a readable main.

contributes.tools

FieldDescription
nameRequired. [a-z0-9]+(?:[._-][a-z0-9]+)*
descriptionRequired. Shown to the model
inputSchemaRequired. JSON Schema object
outputSchemaOptional. JSON Schema object
approvalModeOptional. allowed, need-approval, or need-questions
executionModeOptional. foreground or background

The model does not see name as-is. The host builds an invocation name such as extension__{id}__{tool}__{hash}.

contributes.desktop

Requires desktop-ui.

FieldDescription
css[].pathRequired. CSS file relative to the package root
css[].mediaOptional. CSS media query
settingsPageOptional. true, {}, or { title } — adds a Desktop settings entry
views[].idRequired. Kebab-case id, unique within the extension
views[].pathRequired. Precompiled single-file ESM (.js / .mjs) relative to the package root. The host does not compile TSX. Relative imports 404 — the protocol only serves this file.
views[].titleOptional. Dialog title
views[].widthOptional. Dialog width
views[].heightOptional. Max dialog height. Omit to hug the view. The iframe scrolls if the view is taller.
views[].chromeOptional. Dialog chrome: default (title + close), close-only (hide title, keep close), or none (hide both). Omit for default.

Default export: export default function View({ params, close }). close(result) finishes the view. The host injects React and @spirit/desktop-ui.

contributes.cli

Requires cli-ui. hooks is an object with path, not an inline array:

{
  "hooks": { "path": "cli-hooks.json" }
}

The file must be { "hooks": [ ... ] }.

FieldDescription
slotRequired. One of the slots below
variantOptional. default, accented, muted, warning, success, danger
tokensOptional. { foreground?, border?, accent? }
prefixOptional. String
suffixOptional. String

Slots: message.user, message.assistant, message.tool, assistant.thinking, input.frame, bottom_form, bottom_form.section, slash_suggestions, approval.panel, questions.panel.

Token roles: default, primary, secondary, muted, accent, success, warning, danger.

{
  "hooks": [
    {
      "slot": "input.frame",
      "variant": "accented",
      "tokens": { "border": "accent" },
      "prefix": "[",
      "suffix": "]"
    }
  ]
}

Bundled MCP, hooks, skills, and rules

Requires the matching capability. contributes.mcp, contributes.hooks, contributes.skills, and contributes.rules are true or {}. Paths are conventional; custom paths are not allowed.

ContributionCapabilityFile
MCPmcpPackage-root mcp.json (same schema as user/workspace MCP)
Agent hookshooksPackage-root hooks.json (same schema as agent hooks; not contributes.cli.hooks)
Skillsskillsskills/<name>/SKILL.md
RulesrulesPackage-root rule.md

Declared but missing or invalid files fail install. Files in the package that are not declared are ignored and never loaded.

Relative command and cwd values resolve against the extension root. If an MCP stdio server omits cwd, it defaults to the extension root. PATH-only commands are unchanged.

These contributions overlay into the agent session when the extension is enabled. They do not appear in Settings → MCPs | Hooks | Skills | Rules. On a name conflict, workspace wins, then user, then extension.

During tools/call, a bundled MCP server may send a JSON-RPC request spirit/ui/open with { viewId, params } when the client advertised experimental["spirit/ui"]. Spirit only opens a view declared by the same extension in contributes.desktop.views. User and workspace MCP servers are rejected. The Dialog stays open while that tools/call is in flight; close(result) returns on the same request. Hosts without a window respond { "kind": "unavailable", "reason": "host-has-no-ui" } — the tool should degrade.

settingsSchema

FieldDescription
keyRequired. Same pattern as tool names
typeRequired. string, boolean, number, or select
titleRequired. UI label
descriptionOptional
placeholderOptional
requiredOptional. Boolean
defaultValueOptional. Must match type
optionsRequired for select. Array of { value, label, description? }

Values are string, number, boolean, or null. null clears a non-required setting.

secretSlots

FieldDescription
keyRequired
titleRequired
descriptionOptional
requiredOptional. Boolean

Desktop stores secrets in the OS keyring. On the CLI daemon path, secrets.set / secrets.delete may be unavailable.

activate

main is loaded with dynamic import. Export one of:

  • export function activate(ctx) { ... }
  • export default function activate(ctx) { ... }
  • export default { activate }

ctx

FieldDescription
extension{ id, name, version, directoryPath, manifestPath, main }
hostHost API. Desktop implements showMessageBox and ui.open / ui.close. CLI is {}
log(message: string) => void
settingsget(key), getAll(), set(key, value), setAll(values)
secretsget(key), has(key), set(key, value), delete(key) — keys must be declared in secretSlots
activationEventOptional. { type, detail? }

Return value

Return an object, or export the same fields from the module.

FieldDescription
toolsRecord<string, (ctx) => unknown>. Keys are manifest tool names
invokeTool(ctx) => unknown. Used instead of tools[name] when present
systemPromptStatic system fragment
getSystemPrompt() => string | Promise<string>. Used instead of systemPrompt when present
onEvent(event) => void
dispose() => void. Called on remove or reload

Tool handler ctx

FieldDescription
extensionSame runtime info as activate
hostSame host API
toolNameManifest tool name
argumentsObject from the model
logSame logger
settingsSame settings accessor
secretsSame secrets accessor
toolCallIdOptional
questionsResultOptional. Set when approvalMode is need-questions

A string result is passed through. Other values are JSON.stringify(value, null, 2). undefined becomes "".

Desktop host.showMessageBox

FieldDescription
titleRequired. String
messageRequired. String
detailOptional. String
buttonsOptional. String array
cancelIdOptional. Number
defaultIdOptional. Number
typeOptional. none, info, error, question, warning
export function activate(ctx) {
  return {
    systemPrompt: "Use lookup_item when the user asks for an item by id.",
    async invokeTool({ toolName, arguments: args, settings, secrets }) {
      if (toolName !== "lookup_item") {
        throw new Error(`Unknown tool: ${toolName}`);
      }
      const region = await settings.get("region");
      const token = await secrets.get("api_token");
      return { id: args.id, region, hasToken: Boolean(token) };
    },
    dispose() {
      ctx.log("example-extension disposed");
    },
  };
}

Distribute as a ZIP

Pack an extension directory into the shareable ZIP format (content plus .spirit/extension.json):

npx @spiritagent/extension-toolkit pack ./my-extension

The command validates the package first and writes <name>-<version>.zip to the current directory. Users import the ZIP from the Extensions page or with spirit extension import; it lands in their Personal marketplace. See Extensions.

To list the extension in a marketplace instead, see Publish to a marketplace.

Run a marketplace

A marketplace is a servable directory with a .spirit/marketplace.json index: a Git repository, a static site, or a local directory. Users add it with spirit extension marketplace add <url|path> or from the Desktop Extensions page, then browse and install its entries.

Use this template to create your own marketplace repository from SpiritAgents/extension-template.

The template ships the registry layout, two example extensions, and CI that validates the registry on every push and pull request. To publish an extension:

  1. Scaffold it with npm create @spiritagent/extension.
  2. Run npx @spiritagent/extension-toolkit publish ./my-extension /path/to/your-marketplace-clone — it copies the extension into extensions/ and merges the entry into .spirit/marketplace.json. See Publish to a marketplace.
  3. Push — users add the marketplace with its Git URL.

Local marketplaces

Any local directory with a .spirit/marketplace.json works as a marketplace — add it with spirit extension marketplace add <path> or Desktop Add from Disk. Local marketplaces are read in place and need no CI, which makes them the development loop for extension authors: edit, reinstall, repeat.

Publish to a marketplace

extension-toolkit publish merges an extension's .spirit/extension.json into a marketplace's .spirit/marketplace.json and copies the content the entry's source needs:

npx @spiritagent/extension-toolkit publish ./my-extension ./my-marketplace
  • The first argument is the extension directory; the second is the marketplace root — any local directory with a .spirit/marketplace.json: a template-repository clone, the Personal marketplace at {spiritDataDir}/marketplaces/personal, and so on.
  • --source local (default): copy the full package into the marketplace's extensions/<name>/ — including .spirit/extension.json, excluding node_modules and .git — and point the entry's source at it.
  • --source npm: pin name@version from the extension's package.json as the entry's npm source; only the declared icon is copied into extensions/<name>/ (marketplaces self-host icons). Publishing the npm package itself stays with your npm CLI.
  • --dry-run: print the entry and the planned actions without writing anything.
  • Merge rules: a same-name entry is replaced in place, otherwise the entry is appended. New entries and version changes fall back to reviewStatus: "unverified"; republishing the same version keeps it. Curation fields such as featured always keep the marketplace's values.
  • The same validation as check runs first; on failure nothing is written.