feat(plugins): make CCAM installable straight from a Claude Code plugin

Adds a root `ccam` plugin (`.claude-plugin/plugin.json`, `"source": "./"`) so
`/plugin marketplace add` + `/plugin install ccam@...` is enough on a machine
with nothing but Claude Code: no clone, no npm run setup, no manual npm start.

- scripts/plugin-bootstrap.js: SessionStart hook. Fast-path exit, Node >=22.5
  gate (node:sqlite), mkdir lock with stale reclaim, deps installed into
  ~/.claude/agent-dashboard/runtime/ (never the plugin cache), legacy
  checkout-hook cleanup (backed up), ~/.local/bin/ccam launcher, eager UI
  build so client routes like /run work immediately, detached server spawn.
- scripts/plugin-open.js, scripts/plugin-doctor.js: /ccam-open, /ccam-doctor.
- server/index.js: DASHBOARD_CLIENT_DIST override (plugin cache is read-only).
- mcp/build/ is committed (plugin MCP servers start before any bootstrap could
  build them) and kept honest by scripts/check-mcp-build.js (content hash,
  not mtime), enforced by pre-commit when mcp/src changes.
- plugins/ccam-dashboard/.mcp.json moved under plugins/ccam/ with a working
  ${CLAUDE_PLUGIN_ROOT} path (the old relative path never resolved from a
  marketplace-cached subdir).
- Docs: README, INSTALL, SETUP, ARCHITECTURE, CLAUDE.md, docs/PLUGINS.md,
  docs/MCP.md, docs/CLI.md, docs/HOOKS.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:05:37 +07:00
parent 5a793e70cc
commit 8a82895c65
59 changed files with 5982 additions and 44 deletions
+8
View File
@@ -61,6 +61,14 @@ ccam help
If linking needed elevated permissions in your environment, setup still succeeds and prints a hint — run `npm link` once from the repo root yourself, or invoke the CLI directly with `node bin/ccam.js <command>`.
### With a plugin install (no checkout)
The `ccam` plugin's bootstrap writes a launcher to `~/.local/bin/ccam` on
session start — a small script rather than a symlink into the plugin cache,
which is replaced on every plugin update. It never overwrites a `ccam` it did
not write, so a linked checkout keeps winning. If `~/.local/bin` is not on your
PATH, `/ccam-doctor` says so and prints the exact `export PATH=...` line to add.
## Server Discovery
The CLI finds your running dashboard the same way the Claude Code hook handler does:
+11
View File
@@ -155,6 +155,17 @@ graph TB
npm run install-hooks
```
> [!IMPORTANT]
> **Skip this entirely when the `ccam` plugin is installed.** The plugin declares
> the same eight hooks itself (inline in `.claude-plugin/plugin.json`, each
> running `${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js`). Running both means
> every event is POSTed twice — events carry no id, so ingest cannot deduplicate
> them and every token and cost figure doubles. `scripts/plugin-bootstrap.js`
> removes checkout-installed entries on session start (backup:
> `~/.claude/settings.json.ccam-bak`), `install-hooks.js` warns when it detects
> a plugin install, and `/ccam-doctor` reports any duplicates that remain. See
> [PLUGINS.md](PLUGINS.md).
> [!IMPORTANT]
> **Hooks are a host-side step.** Claude Code runs on your host, so the hook
> command must reference a `hook-handler.js` path that exists on the **host**.
+25 -2
View File
@@ -135,13 +135,35 @@ graph TB
# Install MCP server dependencies
npm run mcp:install
# Build MCP server
# Build MCP server (also stamps mcp/build/.srchash)
npm run mcp:build
# Test MCP server
npm run mcp:start
```
Installing the `ccam` plugin needs none of this: it ships the built server and
wires it up itself (see [PLUGINS.md](PLUGINS.md)).
### Why `mcp/build/` is committed
Claude Code starts a plugin's MCP servers the moment a session opens and offers
no "not ready yet, retry" state, so an async bootstrap cannot win that race. The
build artifact is therefore committed, and `plugins/ccam/.mcp.json` points at
`${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`.
The cost is drift, so freshness is enforced by content hash — `mcp/src` plus the
MCP manifests and tsconfig are hashed into `mcp/build/.srchash`:
```bash
npm run mcp:check-build # fails when mcp/build is stale or unstamped
```
`npm run mcp:build` re-stamps it, the pre-commit hook runs the check whenever
`mcp/src` is part of the commit, and `/ccam-doctor` reports it. Modification
times are deliberately not used: a fresh clone stamps every file at checkout
time in arbitrary order.
### Directory Structure
```
@@ -157,7 +179,8 @@ mcp/
│ │ └── stats.ts # Statistics tools
│ └── types.ts # TypeScript type definitions
├── dist/ # Compiled JavaScript (gitignored)
├── build/ # Compiled JavaScript (COMMITTED — see above)
│ └── .srchash # hash of mcp/src the build was produced from
├── package.json
├── tsconfig.json
└── README.md
+126 -14
View File
@@ -1,6 +1,6 @@
# Claude Code Agent Monitor — Plugin Marketplace
Official Claude Code plugins for the Agent Monitor dashboard. **10 plugins** extend Claude Code with skills, agents, slash commands, hooks, and CLI tools for deep analytics, cost guardrails, productivity automation, developer tools, AI-powered insights, session forensics, workflow/fleet intelligence, reliability & SLOs, config & memory governance, and dashboard connectivity.
Official Claude Code plugins for the Agent Monitor dashboard. The **`ccam`** plugin _is_ the dashboard — hooks, server, `ccam` CLI and MCP tools, no checkout required. On top of it, **10 focused plugins** extend Claude Code with skills, agents, slash commands, hooks, and CLI tools for deep analytics, cost guardrails, productivity automation, developer tools, AI-powered insights, session forensics, workflow/fleet intelligence, reliability & SLOs, config & memory governance, and dashboard connectivity.
Every plugin is powered by the local Agent Monitor REST API at `http://localhost:4820`. They are read-only advisors unless a skill explicitly documents a mutating endpoint (and those preview + confirm before acting).
@@ -12,19 +12,43 @@ Every plugin is powered by the local Agent Monitor REST API at `http://localhost
claude plugin marketplace add Smartgift-AI/Claude-Code-Monitor
```
### Install a plugin
### Install the dashboard itself
```bash
claude plugin install ccam-analytics@smartgift-claude-code-monitor
claude plugin install ccam-cost-guard@smartgift-claude-code-monitor
claude plugin install ccam-productivity@smartgift-claude-code-monitor
claude plugin install ccam-devtools@smartgift-claude-code-monitor
claude plugin install ccam-insights@smartgift-claude-code-monitor
claude plugin install ccam-sessions@smartgift-claude-code-monitor
claude plugin install ccam-workflows@smartgift-claude-code-monitor
claude plugin install ccam-quality@smartgift-claude-code-monitor
claude plugin install ccam-config@smartgift-claude-code-monitor
claude plugin install ccam-dashboard@smartgift-claude-code-monitor
claude plugin install ccam@claude-code-agent-monitor-plugins
```
That is the whole install: no clone, no `npm run setup`, no `npm run install-hooks`, no manual `npm start`. See [The `ccam` plugin](#the-ccam-plugin) below for what it does on first session start.
### Or via the Smartgift skills marketplace
`ccam` is also listed as a standalone entry in
[`smartgift-claude-skills`](https://git.smartgift.io.vn/Smartgift-AI/smartgift-claude-skills)
— its `source` still points at this repo's `main` branch, so the two entries
install identically:
```bash
claude plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/smartgift-claude-skills.git
claude plugin install ccam@sg
```
Pick whichever marketplace you already have added; installing `ccam` from both
at once is redundant but harmless (Claude Code treats it as one plugin per
marketplace name, not per source).
### Install a focused plugin
```bash
claude plugin install ccam-analytics@claude-code-agent-monitor-plugins
claude plugin install ccam-cost-guard@claude-code-agent-monitor-plugins
claude plugin install ccam-productivity@claude-code-agent-monitor-plugins
claude plugin install ccam-devtools@claude-code-agent-monitor-plugins
claude plugin install ccam-insights@claude-code-agent-monitor-plugins
claude plugin install ccam-sessions@claude-code-agent-monitor-plugins
claude plugin install ccam-workflows@claude-code-agent-monitor-plugins
claude plugin install ccam-quality@claude-code-agent-monitor-plugins
claude plugin install ccam-config@claude-code-agent-monitor-plugins
claude plugin install ccam-dashboard@claude-code-agent-monitor-plugins
```
### Or install locally during development
@@ -37,11 +61,99 @@ claude --plugin-dir plugins/ccam-analytics
## Prerequisites
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
- Agent Monitor dashboard running at `http://localhost:4820` (see [SETUP.md](../SETUP.md))
- Hooks installed: `npm run setup` from the Agent Monitor project
- Node **>= 22.5** when installing via the `ccam` plugin. A plugin install has no
native `better-sqlite3`, so the server stores data through `node:sqlite`, which
landed in 22.5. The bootstrap refuses with one line on anything older instead
of letting the server crash. (A checkout install still works on Node >= 20.)
- The Agent Monitor dashboard reachable at `http://localhost:4820` — the `ccam`
plugin starts it for you; a checkout starts it with `npm start` (see
[SETUP.md](../SETUP.md))
Skills and commands are invoked as `/ccam-<plugin>:<name>`. Agents are dispatched automatically by Claude Code (or named explicitly).
## The `ccam` plugin
The marketplace's root entry (`"source": "./"`) is the entire repository, so
`server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` all land under
`${CLAUDE_PLUGIN_ROOT}` when Claude Code caches it. That is what makes a
checkout unnecessary.
### What it installs
| Component | Where it comes from |
|---|---|
| The eight event hooks | inline `hooks` in `.claude-plugin/plugin.json`, each running `scripts/hook-handler.js` |
| The dashboard server | started detached by the bootstrap, from the plugin cache |
| The `ccam` CLI | a launcher written to `~/.local/bin/ccam` |
| The MCP tools | `plugins/ccam/.mcp.json`, pointing at the committed `mcp/build/index.js` |
| The dashboard UI (`/run` and every other client route) | built into the runtime dir by the bootstrap, so it works the moment `claude` starts |
| `/ccam-doctor`, `/ccam-update`, `/ccam-open` | `plugins/ccam/commands/` |
Because the plugin ships the hooks itself, `npm run install-hooks` is not needed
for plugin users — and must not be run alongside it. Events carry no id, so two
handlers mean every token and cost figure is counted twice. The bootstrap
removes the older checkout-installed entries automatically (backing
`~/.claude/settings.json` up as `settings.json.ccam-bak` first), and
`/ccam-doctor` reports the state.
### First session start
`scripts/plugin-bootstrap.js` runs from `SessionStart`. It returns within
milliseconds — the real work happens in a detached worker, so a session never
waits on an install:
1. **Fast path** — recorded state matches this plugin build and a server is live → exit.
2. **Node gate** — refuse below 22.5 with one line (see Prerequisites).
3. **Lock** — atomic `mkdir` lock holding the PID; reclaimed when the owner is dead or the lock is older than 10 minutes, so two sessions cannot race the install or spawn two servers.
4. **Install**`npm install --omit=dev --ignore-scripts` into the runtime dir (never into the plugin cache, which is garbage-collected and replaced on every update). `--ignore-scripts` keeps the root `postinstall` from pulling the whole Vite client toolchain.
5. **Legacy hook cleanup** — see above.
6. **CLI** — write the `~/.local/bin/ccam` launcher, never clobbering a `ccam` the bootstrap did not write.
7. **UI build**`client/` is copied into the runtime dir and built there (`npm install && npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (API and MCP still work, and `/ccam-open` can retry) but does mean `/run` 404s until it's fixed.
8. **Server** — spawn `server/index.js` detached, with `NODE_PATH` at the runtime `node_modules` and `DASHBOARD_CLIENT_DIST` at the runtime `client-dist`.
9. **Record state**`runtime/state.json`.
The first run takes a few minutes — most of it is the UI build, which is what
makes client-only routes (`http://localhost:4820/run`, and every other page)
work the moment `claude` starts, with no manual `/ccam-open` step. Progress
goes to `~/.claude/agent-dashboard/runtime/bootstrap.log`; `npm run build`'s own
output goes to `client-build.log` next to it; the server's own output goes to
`server.log`.
### Commands
| Command | Does |
|---|---|
| `/ccam-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, MCP build freshness, UI bundle |
| `/ccam-update` | Reinstall dependencies and restart the server against the current plugin version (`plugin-bootstrap.js --force`) |
| `/ccam-open` | Build the UI bundle if missing, then print the dashboard URL |
### Where things live
| Thing | Location | Survives a plugin update |
|---|---|---|
| SQLite DB, transcripts | `~/.claude/agent-dashboard/` | yes |
| `node_modules`, `client-dist`, logs, lock, `state.json` | `~/.claude/agent-dashboard/runtime/` | yes (re-verified on every session start) |
| Source, hooks, `mcp/build` | the plugin cache version directory | no — re-bootstrapped |
The bootstrap deliberately does **not** set `DASHBOARD_DATA_DIR`: leaving the
default keeps a plugin-run server and a developer's `npm run dev` server on one
data directory, where `ingestGroupKey` already deduplicates hook ingest to a
single port.
### Uninstalling
`claude plugin uninstall ccam@claude-code-agent-monitor-plugins` removes the hooks
and the cached source. It does not touch the state the plugin created outside
the cache — remove those by hand if you want a clean machine:
```bash
kill "$(node -e 'console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.claude/.agent-dashboard.json","utf8")).servers[0].pid)')"
rm -rf ~/.claude/agent-dashboard/runtime # deps, UI bundle, logs, state
rm -f ~/.local/bin/ccam # the CLI launcher
# ~/.claude/agent-dashboard/ still holds the database — delete it only if you
# want the recorded history gone too.
```
## Available Plugins
### 1. `ccam-analytics` — Analytics & Monitoring
@@ -0,0 +1,239 @@
# Plugin-first install — CCAM usable straight from a Claude Code plugin install
Date: 2026-08-10
Status: approved (design)
## Goal
On a machine with nothing but Claude Code installed, these two commands must
leave the user with a working CCAM — hooks flowing, server up, dashboard
reachable, `ccam` CLI on PATH, MCP tools connected:
```
/plugin marketplace add <ccam repo>
/plugin install ccam@claude-code-agent-monitor-plugins
```
No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
`npm start`. This mirrors how `claude-mem` installs.
## Why the current state does not do this
- The only plugin with a server connection, `ccam-dashboard`, declares its MCP
server as `../../mcp/build/index.js` (`plugins/ccam-dashboard/.mcp.json`).
Plugins installed from a marketplace subdir are cached as **that subdir
only** — verified against `~/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/`,
which contains the plugin directory's contents and nothing above it. The
relative path escapes the cached tree and resolves to nothing.
- Every plugin's hooks and MCP config assume a dashboard is already listening
on `localhost:4820`. Nothing starts one.
- Hook installation today mutates `~/.claude/settings.json` through
`scripts/install-hooks.js`, which requires a checkout to point at.
## Approach: declare the whole repo as one plugin
Add a **root-level** plugin named `ccam` with `"source": "."` in
`.claude-plugin/marketplace.json`. Claude Code then caches the *entire repo*
into `~/.claude/plugins/cache/<marketplace>/ccam/<version>/` — verified against
`~/.claude/plugins/cache/caveman/`, whose cached tree contains the full source
repo (`src/`, `tests/`, `benchmarks/`), because that marketplace declares its
plugin with `"source": "./"`.
So `server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` are all present
under `${CLAUDE_PLUGIN_ROOT}` the moment the plugin is installed. No second
clone, no credentials beyond the marketplace add itself, no committed copy of
the server, no dependency on undocumented cache internals beyond the documented
`${CLAUDE_PLUGIN_ROOT}` variable.
The ten existing subdirectory plugins stay exactly as they are.
Repo working tree is 23 MB — an acceptable cache footprint.
## Components
### 1. Root plugin manifest — `.claude-plugin/plugin.json`
New file, alongside the existing `marketplace.json`. Declares name, metadata,
inline `hooks`, and paths to commands. Hooks are inlined in `plugin.json`
(as `caveman` does) rather than in a root `hooks/hooks.json`, so the repo root
gains no new top-level directories.
`.claude-plugin/marketplace.json` gains one entry:
```json
{ "name": "ccam", "source": ".", "description": "...", "tags": [...] }
```
### 2. Hooks — inline in `plugin.json`
All eight hook types currently installed by `scripts/install-hooks.js`
(`install-hooks.js:86,93`):
- with `"matcher": "*"``PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, `Notification`
- without a matcher — `SessionStart`, `SessionEnd`, `UserPromptSubmit`
Each runs:
```
node "${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js" <HookType>
```
`scripts/hook-handler.js` is used unchanged: it is dependency-free (`node:http`
only), resolves ports via `server/lib/server-info.js` — present in the cached
tree — falls back to 4820, and already fails silently so a not-yet-booted
server never blocks Claude Code.
`SessionStart` additionally runs the bootstrap (below).
Consequence: `npm run install-hooks` becomes unnecessary for plugin users, and
uninstalling the plugin removes the hooks cleanly. It stays supported for
checkout users.
### 3. Bootstrap — `scripts/plugin-bootstrap.js` (new)
Runs from the `SessionStart` hook. Never blocks a session.
Runtime state lives under `~/.claude/agent-dashboard/runtime/`, reusing the
existing data-dir convention (`server/lib/claude-home.js:37`, where the SQLite
DB already lives) rather than inventing a new directory. `/.ccam/` at the repo
root is an unrelated lane profile and must not be confused with it.
Sequence:
1. **Fast path.** Read `runtime/state.json`. If the recorded plugin version
matches and a live server is found via `~/.claude/.agent-dashboard.json`
(the existing PID-checked discovery file), exit in milliseconds.
2. **Node version gate.** `server/db.js` `require`s `better-sqlite3` — which is
*not* in `dependencies` — and falls back to `node:sqlite`, available only on
Node >= 22.5. On anything older the server cannot start at all. Refuse with
one clear line rather than letting the server crash.
3. **Lock.** `runtime/.bootstrap.lock` created with an atomic `mkdir`, holding
the PID. A lock whose PID is dead, or older than 10 minutes, is stale and
reclaimed. Prevents concurrent sessions racing an install or double-spawning
the server.
4. **Install.** `npm install --omit=dev --ignore-scripts` with the dependency
tree written to `runtime/node_modules`, never into the plugin cache
directory. Two reasons: `~/.claude/plugins/cache/` shows GC machinery
(`.in_use` markers, `.last_inuse_sweep`), and every plugin update creates a
fresh version directory, discarding anything installed into the old one.
`--ignore-scripts` is required because `scripts/postinstall.js` otherwise
runs a full `client/` install, pulling the whole Vite dev toolchain.
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries CCAM hook
entries from a previous `npm run install-hooks`, remove them and print one
line. Without this, every event is POSTed twice and token/cost figures
double. Detection reuses `isOurEntry()` from `scripts/install-hooks.js`.
6. **CLI on PATH.** Symlink `bin/ccam.js` to `~/.local/bin/ccam`. Skills and
commands throughout the repo (`ccam stage`, `ship-feature-lane`) shell out
to `ccam` and fail without it.
7. **Start server.** Spawn `server/index.js` detached, `stdio: "ignore"`,
`unref()`, with `NODE_PATH` pointed at `runtime/node_modules`. The hook
returns immediately; the first install (13 minutes) proceeds in the
background and prints progress to a log under `runtime/`.
8. **Record state.** Write `runtime/state.json`.
The client UI bundle is *not* built here. Hooks, the API and MCP do not need
it; it is built lazily by `/ccam-open`.
### 4. Client dist relocation — `server/index.js`
`server/index.js:156` hardcodes `path.join(__dirname, "..", "client", "dist")`.
Since the plugin cache tree must be treated as read-only, add a
`DASHBOARD_CLIENT_DIST` env override, defaulting to the current path so
checkout behavior is unchanged.
The bootstrap always starts the server with `DASHBOARD_CLIENT_DIST` set to
`runtime/client-dist/`, whether or not a bundle is there yet — a missing
directory serves the API fine and only the UI route 404s. `/ccam-open` builds
into that same directory, so no server restart is needed to pick the bundle up.
### 5. MCP — commit `mcp/build/`
Claude Code starts a plugin's MCP servers as the session opens and offers no
"not ready yet, retry" state, so an async bootstrap cannot win that race.
`mcp/build/` is therefore committed (removed from `.gitignore` and `mcp/.gitignore`)
and the root `.mcp.json` points at `${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`
with `CCAM_DASHBOARD_URL` defaulted to `http://localhost:4820`.
Cost: the build artifact must stay in sync with `mcp/src`. A pre-commit check
fails when `mcp/src` is newer than `mcp/build`.
The existing `plugins/ccam-dashboard/.mcp.json` keeps its broken relative path
fixed to `${CLAUDE_PLUGIN_ROOT}` form as part of this work, so the subdir plugin
is not left in a knowingly broken state.
### 6. Commands — `plugins/ccam/commands/`
The plugin root is the repo root, so `plugin.json` declares an explicit
`"commands": "./plugins/ccam/commands"` path. This keeps the new command files
inside `plugins/`, next to the ten existing plugins, instead of adding a
top-level `commands/` directory to the repo root.
- `/ccam-doctor` — bootstrap state, Node version, server liveness, DB path,
duplicate-hook detection, CLI symlink, MCP build freshness.
- `/ccam-update` — refresh dependencies and restart the server after a plugin
update.
- `/ccam-open` — lazily build the client bundle if missing, then open the
dashboard.
## Data and lifecycle
| Thing | Location | Survives plugin update |
|---|---|---|
| SQLite DB, transcripts | `~/.claude/agent-dashboard/` (unchanged) | yes |
| node_modules, client dist, logs, lock, state | `~/.claude/agent-dashboard/runtime/` | yes (re-verified each boot) |
| Source, hooks, MCP build | plugin cache version dir | no — re-bootstrapped |
Bootstrap must not set `DASHBOARD_DATA_DIR`. Leaving it at the default keeps a
plugin-run server and a developer's `npm run dev` server on the same data
directory, where `ingestGroupKey` in `server/lib/server-info.js` already
deduplicates hook ingest to a single port.
## Failure modes
**High severity**
- *SessionStart blocks the session.* The first install takes minutes. Mitigated
by full detachment (`detached`, `stdio: "ignore"`, `unref()`) and an
immediate hook return. Verified by timing the hook on a cold `HOME`.
- *Duplicate hooks double-count cost.* Events carry no id, so ingest cannot
deduplicate. Mitigated by step 5 of the bootstrap plus a `/ccam-doctor` check.
- *MCP unavailable at session start.* Resolved by committing `mcp/build/`.
**Medium severity**
- *Two sessions spawn two servers* → `EADDRINUSE`. Mitigated by the lock, with
a stale-lock timeout so a crashed bootstrap does not wedge every later
session.
- *`ccam` not on PATH* when `~/.local/bin` is absent from the user's PATH.
`/ccam-doctor` reports it and prints the line to add.
- *Windows.* `npm.cmd` resolution (already handled in `scripts/postinstall.js`
via `shell: true`), detached spawn semantics, and `mkdir` locking are the
least-tested paths.
**Low severity**
- Node 20 users are refused rather than broken; the message names the required
version.
- 23 MB cache footprint.
- Marketplace mixing one root-source plugin with ten subdir plugins is
unverified in this combination; it is the first thing the plan verifies, and
the fallback is a separate marketplace file for the root plugin.
## Verification
1. Install the local marketplace and confirm
`~/.claude/plugins/cache/<mp>/ccam/*/server/` exists — this gates everything
else.
2. `npm run test:server` for the `DASHBOARD_CLIENT_DIST` override.
3. Unit tests for bootstrap: fast path, stale-lock reclaim, Node version gate,
legacy-hook detection — all with injected paths, no real `$HOME` writes.
4. Cold-machine smoke: `HOME=$(mktemp -d)` with no checkout on PATH, install the
plugin, wait for bootstrap, `curl /api/health`, fire one synthetic hook, and
assert the event lands in the DB.
5. `npm run mcp:typecheck && npm run mcp:build` for the committed artifact.
## Out of scope
- Publishing to npm.
- Docker-based bootstrap.
- Changing the ten existing subdir plugins beyond the `.mcp.json` path fix.