This document covers the full lifecycle of creating, developing, testing,
and releasing extensions in this monorepo. Extensions work with both
Gemini CLI (as extensions) and Claude Code (as plugins) from a
single extension.json manifest. See
extension-skeleton/EXTENSION-FORMAT.md
for the complete manifest specification.
Always use the interactive scaffolding task:
task create-extension NAME=my-extension
An fzf menu lets you select which components to include (use TAB to toggle, ENTER to confirm):
.toml slash commandSKILL.md agent skillThe script will:
extensions/my-extension/.SKELETON_NAME placeholder with your extension name.The extension-skeleton/ directory contains the template source:
extension-skeleton/
├── .geminiignore # Prevents Gemini from loading templates
├── base/ # Always copied
│ ├── extension.json # Unified manifest (all tools)
│ ├── gemini-extension.json # Legacy Gemini-only manifest
│ ├── CLAUDE.md # Claude Code context placeholder
│ ├── package.json # npm package with MCP SDK dependency
│ ├── tsconfig.json # TypeScript ES2022 / Node16
│ ├── GEMINI.md # Agent context placeholder
│ ├── README.md # Documentation placeholder
│ └── .gitignore # node_modules, dist, .env
├── mcp-server/ # MCP server component
│ ├── src/index.ts # Stdio MCP server with sample tool
│ └── mcp-server.json.fragment # Merged into manifest on creation
├── command/commands/sample.toml # Sample slash command
├── skill/skills/sample-skill/SKILL.md # Sample agent skill
├── agent/agents/sample-agent/PROMPT.md # Sample sub-agent prompt
├── hook/hooks/ # Lifecycle hook
│ ├── hooks.json # Hook event configuration
│ └── logger.sh # Sample BeforeTool hook script
└── theme/theme.json.fragment # Merged into manifest on creation
Every occurrence of SKELETON_NAME in these files is replaced with your
extension name during scaffolding.
cd extensions/my-extension
npm install
During development, use symlinks so changes take effect immediately without reinstalling:
Gemini CLI:
task link-extensions
Start a Gemini session and your extension is loaded. Edit source files,
rebuild with npm run build, and restart Gemini to pick up changes.
Claude Code:
task build-claude-plugin EXT=my-extension
claude --plugin-dir extensions/my-extension/dist-claude-plugin/my-extension
The --plugin-dir flag loads the plugin directly for development.
Run /reload-plugins after changes to pick up updates without
restarting.
# Build the extension
cd extensions/my-extension
npm run build
# Verify the MCP server starts
node dist/index.js
# (Ctrl+C to stop — it runs on stdio)
build-claude-plugin.sh propagates dist/ and package.json into the plugin output directory because the generated .mcp.json resolves ${extensionPath} to that directory at build time, and Node requires both to load an ESM MCP server at runtime.
Implications for contributors:
dist/ must be rebuildable from source via npm run build (tsconfig.json and src/ must be committed; dist/ is in .gitignore)package.json must declare "type": "module" for ESM resolution (confirmed: this is the value in extension-skeleton/base/package.json)dist/ inside the extension directory; the build script regenerates itRebuild reminder:
cd extensions/my-extension
npm run build
task build-claude-plugin EXT=my-extension
Claude Code does not auto-discover plugins placed under ~/.claude/plugins/. Plugins must be declared in a marketplace and installed via the CLI before Claude Code can load them.
scripts/install-claude-plugin.sh handles this in four steps:
build-claude-plugin.sh → produces dist-claude-plugin/<name>/dist-claude-plugin/.claude-plugin/marketplace.json with a marketplace named <repo-basename>-local (e.g. crewrig-local)claude plugin marketplace add <dist-claude-plugin-dir> --scope userclaude plugin install <name>@<marketplace-name> --scope userInstall via the task wrapper:
task install-claude-plugin EXT=my-extension
Verify the installation:
claude plugin list
Running /<skill-name> inside a Claude Code session also confirms that a skill from the plugin is accessible.
For iterative development, prefer the --plugin-dir flag documented in the Link Mode section — it skips the marketplace step.
main: feat/my-extensionmain.main triggers the automated release pipeline.Semantic Release analyzes commit messages using Gitmoji to determine version bumps automatically:
| Gitmoji | Meaning | Release |
|---|---|---|
:boom: |
Breaking change | MAJOR |
:sparkles: |
New feature | MINOR |
:bug: |
Bug fix | PATCH |
:ambulance: |
Critical hotfix | PATCH |
:lock: |
Security fix | PATCH |
:zap: |
Performance improvement | PATCH |
Commits that do not match any rule (e.g., :memo:, :wrench:) do not
trigger a release.
main touching files in extensions/my-extension/.release-monorepo workflow detects the change.semantic-release-monorepo scopes the analysis to that extension only.semantic-release-gitmoji determines the version bump from the emoji.my-extension-vX.Y.Z is created..tgz as an asset.Other extensions in the monorepo are not affected.
To manually package an extension without releasing:
# Single extension
task package-extension EXT=my-extension
# All extensions
task package
The .tgz files are written to dist/.
extensions/my-extension/
├── extension.json # Unified manifest (generates Gemini ext + Claude plugin)
├── gemini-extension.json # Legacy Gemini-only manifest (optional)
├── package.json # npm package (dependencies, build script)
├── tsconfig.json # TypeScript configuration
├── GEMINI.md # Agent context for Gemini CLI
├── CLAUDE.md # Agent context for Claude Code
├── README.md # Documentation
├── src/ # MCP server source (TypeScript)
│ └── index.ts
├── commands/ # Slash command .toml files
├── skills/ # Agent skill directories (SKILL.md)
├── agents/ # Sub-agent prompts (PROMPT.md)
└── hooks/ # Lifecycle hooks (hooks.json + scripts)
Not all directories are required — include only what your extension needs.
Transcripts are disabled by default. To enable them, set the environment variable before starting Claude Code:
export MEMPALACE_TRANSCRIPT_ENABLED=1
Once enabled, the hook hooks/mempalace-transcript.sh is triggered on every matching event via hooks/claude-transcript-hooks.json (events: UserPromptSubmit, PostToolUse, Stop, SessionEnd).
What is recorded:
UserPromptSubmit → [USER] <raw prompt text>PostToolUse → [TOOL] <tool-name>: <command/path/pattern>Stop → [AGENT] Session turn completedSessionEnd → [SESSION] SessionEnd: <source>Each entry is stored as a drawer in wing="transcripts", room="<project-name>-<YYYY-MM-DD>-<session-id[:8]>". Content is capped at 4,000 characters per drawer. The transcripts wing is excluded from default MemPalace semantic searches — see config/TOOLS.md (Memory Activation Protocol section) for the rationale.
Every tool call and every prompt generates a drawer. Long sessions accumulate hundreds of drawers. Use the prune task to manage retention:
# Dry-run: shows what would be deleted (default retention: 30 days)
task prune-transcripts
# Apply deletion
task prune-transcripts -- --apply
# Filter by project, custom retention
task prune-transcripts -- --project my-extension --days 14 --apply
Privacy: Transcripts contain raw user prompts. Do not enable MEMPALACE_TRANSCRIPT_ENABLED=1 on a shared MemPalace instance without evaluating data exposure for all users sharing that instance.