refactor!: rebrand CCAM to AgentHub

Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
This commit is contained in:
2026-09-07 12:09:15 +07:00
parent ae9b3dbcd5
commit 17d56d0e01
345 changed files with 2705 additions and 2727 deletions
+1 -1
View File
@@ -202,7 +202,7 @@ router.delete("/file", (req, res) => {
// These shell out to `claude plugin …` with an argv array — never a shell
// string — because the CLI owns the manifest + cache tree that plugin state
// lives in. Identifiers travel in the body, not the URL, so a key like
// `ccam@sg` needs no encoding (same reason `DELETE /file` takes a body).
// `agenthub@sg` needs no encoding (same reason `DELETE /file` takes a body).
const CLI_ERR_TO_STATUS = {
EBADARG: 400,
+9 -9
View File
@@ -43,10 +43,10 @@ const { getClaudeHome, getProjectsDir } = require("../lib/claude-home");
// Upload limits — deliberately generous because transcripts can be large.
// Configurable at runtime via env for deployments that need tighter bounds.
const MAX_UPLOAD_BYTES = parseInt(
process.env.CCAM_IMPORT_MAX_BYTES || String(1024 * 1024 * 1024), // 1 GB default
process.env.AGENTHUB_IMPORT_MAX_BYTES || String(1024 * 1024 * 1024), // 1 GB default
10
);
const MAX_UPLOAD_FILES = parseInt(process.env.CCAM_IMPORT_MAX_FILES || "2000", 10);
const MAX_UPLOAD_FILES = parseInt(process.env.AGENTHUB_IMPORT_MAX_FILES || "2000", 10);
/**
* Lazily build a multer upload middleware. Kept lazy so the server still
@@ -67,8 +67,8 @@ function getUploader() {
}
const storage = multer.diskStorage({
destination: (req, _file, cb) => {
if (!req._ccamUploadDir) req._ccamUploadDir = mkTempDir("ccam-upload-");
cb(null, req._ccamUploadDir);
if (!req._agenthubUploadDir) req._agenthubUploadDir = mkTempDir("agenthub-upload-");
cb(null, req._agenthubUploadDir);
},
filename: (_req, file, cb) => {
// Preserve the original name for kind-detection later, but prefix with
@@ -90,8 +90,8 @@ function getUploader() {
if (kind === "unknown") {
// Track rejected filenames on the request so we can surface the count
// in the response — users wonder why their upload "partially worked".
if (!req._ccamRejected) req._ccamRejected = [];
req._ccamRejected.push(file.originalname);
if (!req._agenthubRejected) req._agenthubRejected = [];
req._agenthubRejected.push(file.originalname);
cb(null, false);
} else {
cb(null, true);
@@ -296,8 +296,8 @@ router.post("/upload", uploadMiddleware, async (req, res) => {
});
}
const files = Array.isArray(req.files) ? req.files : [];
const rejectedNames = Array.isArray(req._ccamRejected) ? req._ccamRejected : [];
const reqUploadDir = req._ccamUploadDir || null;
const rejectedNames = Array.isArray(req._agenthubRejected) ? req._agenthubRejected : [];
const reqUploadDir = req._agenthubUploadDir || null;
if (files.length === 0) {
// Clean up the upload dir if multer created one before rejecting all files.
@@ -314,7 +314,7 @@ router.post("/upload", uploadMiddleware, async (req, res) => {
});
}
const workDir = mkTempDir("ccam-import-work-");
const workDir = mkTempDir("agenthub-import-work-");
let extractedCount = 0;
let skippedEntries = 0;
+4 -4
View File
@@ -562,8 +562,8 @@ router.post("/:id/mcp/sync", sameOriginGuard, async (req, res) => {
/**
* Detect a Node.js project at this lane's OWN directory and scaffold
* .ccam/profile/ if one is found — the HTTP equivalent of
* `ccam lanes profile init`, always targeting lane.cwd (never an arbitrary
* .agenthub/profile/ if one is found — the HTTP equivalent of
* `agenthub lanes profile init`, always targeting lane.cwd (never an arbitrary
* path; the CLI's <repo> argument has no meaning here, this lane's own
* directory is the only sensible target). "No Node.js project detected" is
* a normal 200 outcome, not an error — most lanes won't be auto-detectable
@@ -590,7 +590,7 @@ router.post("/:id/profile/init", sameOriginGuard, (req, res) => {
/**
* Whether a named integration (tracker, dev_qc, ci_wait, ...) is turned on
* for this lane — reads .ccam/profile/integrations.env. Read-only, no
* for this lane — reads .agenthub/profile/integrations.env. Read-only, no
* sameOriginGuard needed (same reasoning GET /:id/git already documents:
* that guard exists for destructive actions).
*/
@@ -628,7 +628,7 @@ router.get("/:id/git", async (req, res) => {
/**
* A single GitHub Actions status snapshot for one ref (branch or commit sha)
* in the lane's repo — NOT a poll loop. ship-feature-lane's Stage 13 owns
* the wait/retry decision (docs/LANES.md "Orchestration: what CCAM does NOT
* the wait/retry decision (docs/LANES.md "Orchestration: what AgentHub does NOT
* do"); this route only reports what `gh` currently knows. Read-only, no
* same-origin guard, same reasoning as GET /:id/git.
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* @file HTTP surface for cross-lane named locks (`server/lib/named-lock.js`).
* Every route is single-shot — this layer never blocks waiting for a lock to
* free up. The CLI (`ccam lock acquire`) owns the polling loop; CCAM's
* free up. The CLI (`agenthub lock acquire`) owns the polling loop; AgentHub's
* server-side primitives stay non-orchestrating, same as every other lane
* route in this project.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
+10 -10
View File
@@ -74,26 +74,26 @@ function appendMetric(out, name, help, type, samples) {
router.get("/", (_req, res) => {
const out = [];
appendMetric(out, "ccam_up", "1 when the dashboard API is serving this scrape.", "gauge", [
appendMetric(out, "agenthub_up", "1 when the dashboard API is serving this scrape.", "gauge", [
{ value: 1 },
]);
appendMetric(
out,
"ccam_build_info",
"agenthub_build_info",
"Dashboard build info; the value is always 1, the version rides on the label.",
"gauge",
[{ labels: { version: APP_VERSION }, value: 1 }]
);
appendMetric(
out,
"ccam_process_uptime_seconds",
"agenthub_process_uptime_seconds",
"Uptime of the dashboard server process in seconds.",
"gauge",
[{ value: Math.round(process.uptime()) }]
);
appendMetric(
out,
"ccam_process_resident_memory_bytes",
"agenthub_process_resident_memory_bytes",
"Resident set size (RSS) of the dashboard server process in bytes.",
"gauge",
[{ value: process.memoryUsage().rss }]
@@ -103,7 +103,7 @@ router.get("/", (_req, res) => {
const sessionCounts = new Map(stmts.sessionStatusCounts.all().map((r) => [r.status, r.count]));
appendMetric(
out,
"ccam_sessions",
"agenthub_sessions",
"Number of sessions by lifecycle status.",
"gauge",
SESSION_STATUSES.map((status) => ({
@@ -116,7 +116,7 @@ router.get("/", (_req, res) => {
const agentCounts = new Map(stmts.agentStatusCounts.all().map((r) => [r.status, r.count]));
appendMetric(
out,
"ccam_agents",
"agenthub_agents",
"Number of agents (main + subagents) by status.",
"gauge",
AGENT_STATUSES.map((status) => ({ labels: { status }, value: agentCounts.get(status) || 0 }))
@@ -125,7 +125,7 @@ router.get("/", (_req, res) => {
// Event throughput (monotonic — a counter).
appendMetric(
out,
"ccam_events_total",
"agenthub_events_total",
"Total hook and synthetic events recorded since the database was created.",
"counter",
[{ value: stmts.countEvents.get().count }]
@@ -140,7 +140,7 @@ router.get("/", (_req, res) => {
}
appendMetric(
out,
"ccam_websocket_clients",
"agenthub_websocket_clients",
"Currently connected realtime (WebSocket) dashboard clients.",
"gauge",
[{ value: clients }]
@@ -158,7 +158,7 @@ router.get("/", (_req, res) => {
}
appendMetric(
out,
"ccam_remote_sources",
"agenthub_remote_sources",
"Configured Remote Data Sources, split by auto-sync enabled state.",
"gauge",
[
@@ -181,7 +181,7 @@ router.get("/", (_req, res) => {
.get();
appendMetric(
out,
"ccam_tokens_total",
"agenthub_tokens_total",
"Cumulative token usage across all sessions, by kind.",
"counter",
[
+1 -1
View File
@@ -10,7 +10,7 @@ const { sameOriginGuard } = require("./run");
const router = Router();
/** This server's own checkout root — server/routes/skills.js is two levels
* under it (server/routes/), same computation bin/ccam.js's REPO_ROOT
* under it (server/routes/), same computation bin/agenthub.js's REPO_ROOT
* already does from its own location. */
const REPO_ROOT = path.resolve(__dirname, "..", "..");
+1 -1
View File
@@ -95,7 +95,7 @@ router.get("/skill-usage", (req, res) => {
for (const m of mcpCalls) {
// mcp__plugin_<plugin>_<server>__<tool>. Both halves may contain
// underscores, so the split is only unambiguous when the plugin names its
// server after itself — the overwhelmingly common case (`ccam_ccam`,
// server after itself — the overwhelmingly common case (`agenthub_agenthub`,
// `vercel_vercel`), which the dedupe below collapses back to one name.
// ponytail: a plugin whose server has its own name reports under
// "<plugin>_<server>"; read the real pairing off the MCP config if that