AAPlugin is a Rolldown-powered canonical AI plugin framework and CLI. You author Commands, Skills, Agents, and optional Hooks, MCP servers, or Node runtimes once; AAPlugin builds Platform-owned deliveries for Claude Code, Codex, Cursor, Antigravity, OpenCode, and Pi.
The canonical project is the source of truth, and each Platform owns its final manifest, paths, compatibility decisions, and deterministic serialization.
- Published CLI/runtime: Node.js
^20.19.0 || ^22.13.0 || >=23.5.0 - Repository development/build: Node.js
^22.18.0 || >=24.11.0 - pnpm for generated projects and this repository
pnpm dlx aaplugin init my-plugin --yes
cd my-plugin
pnpm install
pnpm buildOr add the framework and the Platforms you want to an existing empty project:
pnpm add -D aaplugin \
@aaplugin/platform-claude-code \
@aaplugin/platform-codex// aaplugin.config.ts
import { defineConfig } from 'aaplugin';
import claudeCode from '@aaplugin/platform-claude-code';
import codex from '@aaplugin/platform-codex';
export default defineConfig({
name: 'my-plugin',
version: '1.0.0',
description: 'Reusable AI workflows.',
platforms: [claudeCode(), codex()],
});init selects Claude Code and Codex unless you pass --platform, but it writes both packages and imports explicitly. The runtime has no implicit Platforms: every build uses exactly the instances in platforms.
aaplugin.config.ts, Hook descriptors, and MCP descriptors are trusted executable project code loaded by the local Node.js process. Review them with the same care as build scripts.
my-plugin/
├── aaplugin.config.ts
├── package.json
├── public/ # optional files copied to each Platform root
└── src/
├── commands/
│ └── review.md
├── skills/
│ └── review/
│ ├── SKILL.md
│ └── references/ # copied with the Skill
├── agents/
│ └── reviewer.md
├── hooks/ # only with the Hooks Extension
│ └── policy/hook.ts
├── mcp/ # only with the MCP Extension
│ └── docs/mcp.ts
└── runtime/ # optional Core-managed Node Runtime sources
├── cli.ts # direct files are entries by convention
└── internal/helpers.ts # nested files are normal dependencies
IDs and directory names use lowercase kebab-case. Markdown Components require YAML Frontmatter and a non-empty body. Symlinks and paths escaping the project are rejected.
AAPlugin deliberately has no Instructions Component. Repository-wide instructions are host/project configuration, not an installable plugin capability.
aaplugin.config.ts exports an object or a sync/async function receiving { command, mode }.
import { defineConfig } from 'aaplugin';
import claudeCode from '@aaplugin/platform-claude-code';
import codex from '@aaplugin/platform-codex';
export default defineConfig(({ mode }) => ({
name: 'team-review',
version: '1.0.0',
description: 'Shared review workflows.',
displayName: 'Team Review',
platforms: [
claudeCode(),
codex({ strict: mode === 'production' }),
],
public: {
dir: 'public',
copy: [
{ from: 'assets', to: 'assets' },
{ from: 'NOTICE.md', to: 'NOTICE.md' },
],
},
build: {
outDir: 'dist',
strict: true,
},
}));Top-level fields:
| Field | Meaning |
|---|---|
name, version, description |
Required plugin identity. |
displayName |
Optional presentation name. |
srcDir |
Canonical source directory; defaults to src. |
public |
false, a directory, or explicit copy rules. |
platforms |
Required, non-empty list of explicitly imported Platform instances. |
runtime |
Built-in Node Runtime convention, explicit entries, compile options, or false. |
extensions |
Optional horizontal capabilities such as Hooks and MCP. |
build.outDir |
Managed output directory; defaults to dist. |
build.strict |
Fail on degraded/unsupported compatibility; defaults to true. |
Official Platforms are independent packages with a peer dependency on the framework:
import antigravity from '@aaplugin/platform-antigravity';
import claudeCode from '@aaplugin/platform-claude-code';
import codex from '@aaplugin/platform-codex';
import cursor from '@aaplugin/platform-cursor';
import openCode from '@aaplugin/platform-opencode';
import pi from '@aaplugin/platform-pi';
const platforms = [claudeCode(), codex(), cursor(), antigravity(), openCode(), pi()];Claude Code, Codex, Cursor, and Antigravity emit static Plugin Packages. OpenCode emits a workspace overlay; Pi emits an npm package. aaplugin init --platform <id...> installs and writes the selected packages explicitly. The main package does not re-export official integrations, discover packages by ID, or install anything during a build.
---
description: Review a change for correctness and maintainability.
invocation:
user: true
model: true
requires:
agents: [reviewer]
---
Review the selected change and report concrete findings.Place it at src/skills/review/SKILL.md. Every other regular file below that directory is copied as a Skill auxiliary file.
---
description: Review a named change.
argumentHint: <commit-or-branch>
requires:
skills: [review]
---
Review {{arguments}} using the review Skill.Place it at src/commands/review.md.
---
description: Focused read-only code reviewer.
model: capable
capabilities: [filesystem:read, search]
---
Inspect the change, verify evidence, and report only actionable findings.Place it at src/agents/reviewer.md. Canonical model classes are inherit, fast, and capable. Capabilities are semantic declarations rather than target tool names.
Components may require Skills and Agents. Missing dependencies, self-dependencies, and cycles are build errors.
| Component | Claude Code | Codex | Cursor | Antigravity | OpenCode | Pi |
|---|---|---|---|---|---|---|
| Skill | Native | Native | Native | Native | Native | Native |
| Command | Native | Transform to Skill | Native | Transform to Skill | Native | Transform to Prompt |
| Agent | Native | Degraded Skill | Native with field-level limits | Degraded Skill | Native with capability transform | Degraded Skill |
Codex installable plugins cannot register custom project/user Agents. Therefore an Agent makes a strict Codex build fail; configure codex({ strict: false }) only when the explicit fallback and its structured warning are acceptable.
See the complete compatibility matrix for Package shapes, every portable Hook event, and MCP transport support.
pnpm add -D @aaplugin/extension-hooksimport { defineConfig } from 'aaplugin';
import claudeCode from '@aaplugin/platform-claude-code';
import hooks from '@aaplugin/extension-hooks';
export default defineConfig({
name: 'policy-plugin',
version: '1.0.0',
description: 'Portable policy hooks.',
platforms: [claudeCode()],
extensions: [hooks()],
});// src/hooks/policy/hook.ts
import type { Hook } from '@aaplugin/extension-hooks';
export default {
event: 'PreToolUse',
matcher: 'Bash',
timeout: 5,
async run(input) {
return input.cwd
? { decision: 'allow' }
: { decision: 'deny', reason: 'Missing working directory.' };
},
} satisfies Hook<'PreToolUse'>;Portable events are:
SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PermissionRequest,
PostToolUse, PreCompact, PostCompact, SubagentStart, SubagentStop, Stop
Claude Code-only events remain explicitly platform-scoped and do not affect Codex compatibility:
Setup, UserPromptExpansion, PermissionDenied, PostToolUseFailure, PostToolBatch,
Notification, MessageDisplay, TaskCreated, TaskCompleted, StopFailure,
TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded,
FileChanged, WorktreeCreate, WorktreeRemove, Elicitation, ElicitationResult
Declare one with event: { platform: 'claude-code', name: 'Setup' }; a bare 'Setup' string is rejected.
AAPlugin bundles each handler once as a self-contained, platform-neutral Node 20 ESM executable. Verified platform wire profiles are compiled into that same Bundle for native input validation, recursive camelCase conversion, root/data mapping, and output mapping; no adjacent runtime JavaScript is required. The shared Handler owns bounded JSON I/O, semantic result validation, safe failures, and deterministic third-party license notices. Meaningful matchers ignored by the selected host are reported per Hook as degraded; unsupported events generate no fake runtime.
pnpm add -D @aaplugin/extension-mcpimport { defineConfig } from 'aaplugin';
import claudeCode from '@aaplugin/platform-claude-code';
import mcp from '@aaplugin/extension-mcp';
export default defineConfig({
name: 'tools-plugin',
version: '1.0.0',
description: 'Portable MCP tools.',
platforms: [claudeCode()],
extensions: [mcp()],
});Remote Streamable HTTP server:
// src/mcp/docs/mcp.ts
import type { McpServer } from '@aaplugin/extension-mcp';
export default {
transport: 'http',
url: 'https://example.com/mcp',
auth: { type: 'bearer', env: 'DOCS_TOKEN' },
headers: { 'X-Tenant': { env: 'TENANT_ID' } },
} satisfies McpServer;Local stdio server:
// src/mcp/local-tools/mcp.ts
import type { McpServer } from '@aaplugin/extension-mcp';
export default {
transport: 'stdio',
entry: 'server.ts',
env: { API_TOKEN: { env: 'LOCAL_API_TOKEN' } },
} satisfies McpServer;For local MCP, you provide a complete stdio MCP implementation in server.ts; AAPlugin bundles it for Node 20 ESM. Both development and production builds reject unresolved runtime dynamic imports, start the bundle with only declared literal environment values, and require a bounded initialize → initialized → tools/list smoke test to pass. No mode branch or cache bypasses this protocol check. Referenced secret values are never read. For HTTP MCP, you declare the remote endpoint and auth/header references—there is no local server implementation to provide. Production HTTP endpoints require HTTPS; development permits loopback HTTP.
Claude Code, Codex, and OpenCode support both remote HTTP and bundled local stdio. Cursor and Antigravity support remote HTTP only; Pi reports MCP unsupported. See the complete compatibility matrix.
// aaplugin.config.ts
export default defineConfig({
// ...metadata and explicit Platforms
runtime: {
entries: {
cli: { entry: 'bin/cli.ts', kind: 'executable' },
library: { entry: 'library.ts', kind: 'module' },
},
compile: { treeshake: true },
},
});With no runtime field, every supported direct file under src/runtime/ is an executable entry; nested files remain normal dependencies. An explicit runtime.entries map completely replaces auto-discovery, and runtime: false disables the convention. Each entry becomes one deterministic, self-contained Node 20 ESM bundle at runtime/<id>/main.mjs. npm dependencies are bundled, only node: built-ins remain external, executable entries use mode 0755, module entries use 0644, and third-party notices are emitted next to the bundle when required. Core compiles every entry once, then Claude Code and Codex inherit the same framework-owned bytes. Platforms without a stable local Node/plugin-root contract report unsupported and receive no substitute Asset. Type checking remains the project-owned tsc --noEmit step.
All Extensions participate in the same Core-owned pipeline:
config → setup Sessions → discover Resources → Canonical Project
→ validate → compile → Platform base Package → Contributors → Core merge
→ finalize → materialize/validate candidates → Distributions
→ compatibility → transaction → reverse close
Descriptor loading goes through context.modules, while executable output goes through the Core-owned Rolldown service at context.compiler. The services register the actual module, license, plugin, and tsconfig graph for dev; integrations receive owner-scoped capabilities, do not create private bundlers, and cannot write dist. Platform Contributors can return owned Assets, add fields at declared Document extension points, and report compatibility from the same immutable base Package. They can also submit an opaque Platform Component Contribution using the target Platform package's public payload type: Core transports only JSON and provenance, while that Platform validates, renders, names, and registers its native resource during finalization. Unsupported Platforms fail a non-empty contribution instead of silently dropping it or generating a fallback. Contributors cannot replace Platform output or observe other Extension state. Session close always runs in reverse initialization order.
aaplugin init [directory]
aaplugin dev
aaplugin validate
aaplugin inspect
aaplugin build
Common project options include --config, --platform, --mode, and --json. Compatibility strictness is declared in aaplugin.config.ts through build.strict or a Platform factory override.
validateruns complete Platform generation and materialization validation without writingdist.inspectadds detailed Package/Asset metadata without writingdist.buildatomically replaces the complete manageddistonly after every selected Platform succeeds.devwatches config, the Core Module/Build Service graph, Components, Public files, descriptors, and bundler/plugin/license/tsconfig dependencies. Package dependencies are watched at their resolved package roots. It performs a catch-up build after each new watcher becomes ready, retains the last successful output after failures, and rebuilds after recovery. Runtime-computed import targets that Rolldown cannot place in a static module graph are rejected for managed executable bundles.- Bare
aapluginprints Help and never prompts.
Exit codes are 0 success, 1 project/build failure, 2 CLI usage or internal framework failure, and 130 cancellation. JSON mode writes one schema-versioned document to stdout for non-watch commands; diagnostics/logs use stderr.
- Assets are immutable owner-scoped references reported with mode, size, SHA-256, and structured origin.
- Absolute/traversal paths, symlinks, path collisions, and sources outside approved roots are rejected.
- Builds use a same-filesystem stage, lock, transaction record, backup, and whole-output swap.
- Any Platform failure preserves the previous complete
dist. - Generated files and reports contain no timestamps, temporary paths, environment values, or credentials.
- Extension source under
src/hooksorsrc/mcpwithout its Extension enabled is an error;src/runtimeis owned directly by Core.
The repository includes two private, repository-only workspaces beside the publishable packages:
packages/docsis a VitePress site with task-oriented Guide, Config, Platform, Extension, Ecosystem, Playground, and Resource sections. TypeDoc regenerates API pages and the sidebar for all nine public package root entries before every docs dev/build.packages/playgroundis a domain-neutral six-Platform/Hooks/MCP/Node Runtime capability template. It validates canonical Commands, Skill auxiliary files, Agents, all portable Hook events, HTTP and local MCP, portable Node runtime delivery, Public files, and Claude Code/Codex Marketplaces without implementing product-specific behavior.
pnpm run docs:dev # generate API pages, then start VitePress
pnpm run docs:build # generate API pages and build the static site
pnpm run docs:check # docs structure/build plus the real playground checksGenerated API Markdown/sidebar, VitePress cache/output, and Playground dist are reproducible and ignored by Git.
Public packages:
aaplugin@aaplugin/platform-claude-code@aaplugin/platform-codex@aaplugin/platform-cursor@aaplugin/platform-antigravity@aaplugin/platform-opencode@aaplugin/platform-pi@aaplugin/extension-hooks@aaplugin/extension-mcp
The official integrations use the same public lifecycle SDK available to third-party packages and declare the main package as a peer dependency. Core, the Vitest integration workspace, Docs, and Playground remain private; Core is bundled into the main package and no public runtime manifest contains @aaplugin/*.
pnpm install
pnpm run check
pnpm run docs:checkPull requests run separate Lint and Typecheck Actions. After a feature PR containing Changesets merges into main, the Changelog Action consumes the pending Changesets and opens or updates a version PR containing the independent package version bumps and changelogs. It never publishes packages.
The manually dispatched Release Action only publishes stable semver versions to npm latest; it does not create tags or GitHub Releases. Beta publication stays local to an authorized maintainer:
pnpm run publish:beta:dry-run
pnpm run publish:betaBoth beta commands intentionally target npm latest, allowing untagged installs before the first stable release. The dry run mirrors the real publish command and adds only --dry-run; it does not create a separate beta dist-tag.
Both beta and stable release commands build once and then use pnpm's recursive public-workspace publish flow, which rewrites workspace:^ peer ranges in packed manifests. Packages are independently versioned; do not create tags or GitHub Releases unless separately authorized.
MIT