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
+127
View File
@@ -0,0 +1,127 @@
/**
* @file banner.ts
* @description Console startup UI for the MCP server's non-stdio transports (HTTP and REPL):
* the ASCII-art wordmark, a boxed server-info panel (version, transport, dashboard URL, port,
* tool count, mutation/destructive policy state), a "ready" line, and a shutdown message. The
* stdio transport never calls any of these, since stdout there is the MCP JSON-RPC channel.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
* =============================================================================
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
*
* ## Design constraints
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
*
* ## Remote data & SSH
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
*
* ## Observability
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
* Docker Compose profiles are documented in `monitoring/README.md`.
*
* ## Public surface
* - `printBanner` — exported API; see TSDoc on the symbol for behavior.
* - `printServerInfo` — exported API; see TSDoc on the symbol for behavior.
* - `printReady` — exported API; see TSDoc on the symbol for behavior.
* - `printShutdown` — exported API; see TSDoc on the symbol for behavior.
*
* ## Testing pointers
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
* - Server contract changes require `npm run test:server` and OpenAPI sync.
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
*
* ## Related docs
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
* - `docs/API.md` — REST reference.
* - `.claude/skills/file-headers/` — mandatory `@author` header policy.
* ============================================================================= */
/* -----------------------------------------------------------------------------
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
* -----------------------------------------------------------------------------
* **printBanner**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* **printServerInfo**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* **printReady**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* **printShutdown**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* ----------------------------------------------------------------------------- */
import * as c from "./colors.js";
/** ASCII-art wordmark rendered by {@link printBanner} with a color gradient. */
const BANNER = `
$$\\ $$\\ $$$$$$\\ $$$$$$$\\ $$$$$$$$\\ $$\\
$$$\\ $$$ |$$ __$$\\ $$ __$$\\ \\__$$ __| $$ |
$$$$\\ $$$$ |$$ / \\__|$$ | $$ | $$ | $$$$$$\\ $$$$$$\\ $$ | $$$$$$$\\
$$\\$$\\$$ $$ |$$ | $$$$$$$ | $$ |$$ __$$\\ $$ __$$\\ $$ |$$ _____|
$$ \\$$$ $$ |$$ | $$ ____/ $$ |$$ / $$ |$$ / $$ |$$ |\\$$$$$$\\
$$ |\\$ /$$ |$$ | $$\\ $$ | $$ |$$ | $$ |$$ | $$ |$$ | \\____$$\\
$$ | \\_/ $$ |\\$$$$$$ |$$ | $$ |\\$$$$$$ |\\$$$$$$ |$$ |$$$$$$$ |
\\__| \\__| \\______/ \\__| \\__| \\______/ \\______/ \\__|\\_______/ `;
/** Prints {@link BANNER} one line per gradient color (cyan to magenta).
* Called at HTTP/REPL startup only. */
export function printBanner() {
const gradient = [c.brightCyan, c.cyan, c.brightBlue, c.blue, c.brightMagenta, c.magenta];
const lines = BANNER.split("\n").filter((l) => l.length > 0);
for (let i = 0; i < lines.length; i++) {
const colorFn = gradient[Math.min(i, gradient.length - 1)];
process.stdout.write(colorFn(lines[i]) + "\n");
}
process.stdout.write("\n");
}
/** Prints a boxed config summary beneath the banner, shared by HTTP (`port`
* set) and REPL (`port` omitted). Mutations/Destructive rows mirror the
* `policy/tool-guards.ts` flags, warning-colored when enabled. Ends with a
* reminder that the dashboard must already be running at the printed URL. */
export function printServerInfo(info) {
const divider = c.dim(c.cyan("─".repeat(62)));
const line = (label, value) => ` ${c.dim(c.cyan("│"))} ${c.label(label.padEnd(18))} ${value}`;
process.stdout.write(divider + "\n");
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.bold(c.brightWhite("Agent Dashboard MCP Server"))}\n`);
process.stdout.write(divider + "\n");
process.stdout.write(line("Version", c.brightCyan(info.version)) + "\n");
process.stdout.write(line("Transport", c.accent(info.transport.toUpperCase())) + "\n");
process.stdout.write(line("Dashboard API", c.green(info.dashboard)) + "\n");
if (info.port !== undefined) {
process.stdout.write(line("HTTP Port", c.brightYellow(String(info.port))) + "\n");
}
process.stdout.write(line("Tools Registered", c.brightWhite(String(info.tools))) + "\n");
process.stdout.write(line("Mutations", info.mutations ? c.warn("ENABLED") : c.success("disabled")) + "\n");
process.stdout.write(line("Destructive", info.destructive ? c.error("ENABLED") : c.success("disabled")) + "\n");
process.stdout.write(divider + "\n");
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.warn("⚠")} ${c.dim("Dashboard must be running at the URL above.")}\n`);
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.dim(" Start it first:")} ${c.brightWhite("npm run dev")} ${c.dim("or")} ${c.brightWhite("npm start")}\n`);
process.stdout.write(divider + "\n\n");
}
/** Prints "Server ready" once the HTTP server has bound to its port; not
* used by the REPL transport. */
export function printReady(transport) {
const icon = "✔";
process.stdout.write(` ${c.success(icon)} ${c.bold(c.brightWhite("Server ready"))} ${c.muted(`(${transport})`)}\n\n`);
}
/** Prints "Shutting down...". Called from HTTP/REPL shutdown paths and
* `index.ts`'s SIGINT/SIGTERM handler; never from stdio. */
export function printShutdown() {
process.stdout.write(`\n ${c.warn("⏻")} ${c.bold(c.brightWhite("Shutting down..."))}\n`);
}