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
+34 -31
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env node
/**
* @file plugin-bootstrap.js
* @description Makes CCAM usable straight from a Claude Code plugin install.
* @description Makes AgentHub usable straight from a Claude Code plugin install.
* Runs from the plugin's `SessionStart` hook and must NEVER block a session:
* the foreground pass only checks a fast path, then hands the real work to a
* detached worker copy of itself.
*
* The worker installs runtime dependencies, removes hook entries left behind by
* an older `npm run install-hooks` (they would double-count every event), puts
* the `ccam` CLI on PATH, and starts the dashboard server — as a systemd
* the `agenthub` CLI on PATH, and starts the dashboard server — as a systemd
* `--user` service when available (see `systemd-service.js`), so it survives
* reboots without Claude Code being opened first; otherwise a detached spawn
* scoped to this session, same as before.
@@ -248,7 +248,7 @@ function stripLegacyHooks(settingsPath = getSettingsPath()) {
}
if (!removed) return 0;
fs.copyFileSync(settingsPath, `${settingsPath}.ccam-bak`);
fs.copyFileSync(settingsPath, `${settingsPath}.agenthub-bak`);
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
return removed;
}
@@ -260,26 +260,26 @@ function cliDir() {
}
/**
* Write a launcher for `ccam` into ~/.local/bin. A launcher rather than a
* Write a launcher for `agenthub` into ~/.local/bin. A launcher rather than a
* symlink into the plugin cache: that directory is replaced on every plugin
* update, which would leave a dangling link until the next session.
*
* Never overwrites a `ccam` this bootstrap did not write — a developer's
* Never overwrites a `agenthub` this bootstrap did not write — a developer's
* `npm link`ed checkout CLI must keep winning.
*
* @returns {"written"|"exists"|"foreign"|"failed"}
*/
function linkCli(dir = cliDir(), root = PLUGIN_ROOT) {
const marker = "# ccam-plugin-launcher";
const target = path.join(dir, process.platform === "win32" ? "ccam.cmd" : "ccam");
const marker = "# agenthub-plugin-launcher";
const target = path.join(dir, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
const body =
process.platform === "win32"
? `@rem ccam-plugin-launcher\r\n@node "${path.join(root, "bin", "ccam.js")}" %*\r\n`
: `#!/bin/sh\n${marker}\nexec "${process.execPath}" "${path.join(root, "bin", "ccam.js")}" "$@"\n`;
? `@rem agenthub-plugin-launcher\r\n@node "${path.join(root, "bin", "agenthub.js")}" %*\r\n`
: `#!/bin/sh\n${marker}\nexec "${process.execPath}" "${path.join(root, "bin", "agenthub.js")}" "$@"\n`;
try {
if (fs.existsSync(target)) {
const current = fs.readFileSync(target, "utf8");
if (!current.includes("ccam-plugin-launcher")) return "foreign";
if (!current.includes("agenthub-plugin-launcher")) return "foreign";
if (current === body) return "exists";
}
fs.mkdirSync(dir, { recursive: true });
@@ -382,7 +382,7 @@ function startDashboard(rt = runtimeDir(), root = PLUGIN_ROOT) {
env: {
...process.env,
NODE_PATH: path.join(rt, "node_modules"),
// The plugin cache is read-only; /ccam-open builds the bundle here.
// The plugin cache is read-only; /agenthub-open builds the bundle here.
DASHBOARD_CLIENT_DIST: path.join(rt, "client-dist"),
},
});
@@ -402,18 +402,18 @@ function log(rt, line) {
}
/**
* Install the plugins CCAM's pipelines drive but this machine cannot resolve —
* today `superpowers`, which `/ccam:ship-feature(-lane)` calls step by step. A
* fresh `ccam` install otherwise runs those skills partway and fails at the
* Install the plugins AgentHub's pipelines drive but this machine cannot resolve —
* today `superpowers`, which `/agenthub:ship-feature(-lane)` calls step by step. A
* fresh `agenthub` install otherwise runs those skills partway and fails at the
* first call with no warning beforehand.
*
* Off by setting `CCAM_AUTO_INSTALL_PLUGINS` to `0`, `false` or `off`; that
* leaves `/ccam-doctor`'s WARN row as the only notice, which is the right
* Off by setting `AGENTHUB_AUTO_INSTALL_PLUGINS` to `0`, `false` or `off`; that
* leaves `/agenthub-doctor`'s WARN row as the only notice, which is the right
* behaviour for anyone who wants to choose their own plugins.
*
* One-shot: the outcome is recorded in the runtime state and the fast path
* takes it into account, so a machine with no network does not re-attempt a
* clone on every session. `/ccam-update` (`--force`) retries.
* clone on every session. `/agenthub-update` (`--force`) retries.
*
* Best-effort throughout. Nothing here may throw or block: the dashboard,
* hooks, CLI and MCP all work without these plugins.
@@ -422,7 +422,7 @@ function log(rt, line) {
* recorded outcome when this run had nothing to settle.
*/
function autoInstallSkillPlugins(rt, { force = false } = {}) {
const flag = process.env.CCAM_AUTO_INSTALL_PLUGINS;
const flag = process.env.AGENTHUB_AUTO_INSTALL_PLUGINS;
if (flag === "0" || flag === "false" || flag === "off") return "off";
// Return the PREVIOUS outcome, not a marker: writeState records whatever this
// returns, and overwriting "installed" with "already-settled" would lose the
@@ -443,7 +443,7 @@ function autoInstallSkillPlugins(rt, { force = false } = {}) {
/**
* The real work. Runs detached from the SessionStart hook, so its only channel
* is the log file; `/ccam-update` runs it in the foreground with `force`, which
* is the log file; `/agenthub-update` runs it in the foreground with `force`, which
* reinstalls dependencies and restarts the server unconditionally.
*
* @returns {"ok"|"locked"|"node-too-old"|"install-failed"}
@@ -454,7 +454,7 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
if (!nodeVersionOk()) {
log(
rt,
`Node ${process.versions.node} is too old — CCAM needs Node >= ${MIN_NODE.join(".")} ` +
`Node ${process.versions.node} is too old — AgentHub needs Node >= ${MIN_NODE.join(".")} ` +
`(the server stores data through node:sqlite). Upgrade Node, then start a new session.`
);
return "node-too-old";
@@ -471,7 +471,7 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
if (force || state.depsHash !== wantDeps || !fs.existsSync(path.join(rt, "node_modules"))) {
log(rt, "installing runtime dependencies (first run takes a few minutes)");
if (!installDeps(rt)) {
log(rt, "npm install failed — see the output above; run /ccam-doctor after fixing it");
log(rt, "npm install failed — see the output above; run /agenthub-doctor after fixing it");
return "install-failed";
}
}
@@ -481,13 +481,13 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
log(
rt,
`removed ${removed} hook entr${removed === 1 ? "y" : "ies"} left by npm run install-hooks ` +
`(backup: ${getSettingsPath()}.ccam-bak) — the plugin installs its own`
`(backup: ${getSettingsPath()}.agenthub-bak) — the plugin installs its own`
);
}
log(rt, `ccam CLI launcher: ${linkCli()}`);
log(rt, `agenthub CLI launcher: ${linkCli()}`);
// Built eagerly (not lazily behind /ccam-open) so the dashboard, including
// Built eagerly (not lazily behind /agenthub-open) so the dashboard, including
// client-only routes like /run, works the instant `claude` is started —
// same trigger as the dependency install above: missing, or this plugin
// version has not built one yet.
@@ -499,7 +499,10 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
logPath: path.join(rt, "client-build.log"),
});
if (uiResult === "failed") {
log(rt, "dashboard UI build failed — API and MCP tools still work; run /ccam-open to retry");
log(
rt,
"dashboard UI build failed — API and MCP tools still work; run /agenthub-open to retry"
);
} else if (uiResult === "built") {
log(rt, "built the dashboard UI bundle");
}
@@ -516,8 +519,8 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
const svc = require("./systemd-service");
const result = svc.systemctlAvailable()
? svc.installUserService({
name: "ccam",
description: "CCAM dashboard server (Claude Code plugin)",
name: "agenthub",
description: "AgentHub dashboard server (Claude Code plugin)",
workingDir: PLUGIN_ROOT,
execStart: `${process.execPath} ${path.join(PLUGIN_ROOT, "server", "index.js")}`,
env: {
@@ -545,7 +548,7 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
const pid = startDashboard(rt);
log(
rt,
`started the dashboard server (pid ${pid})${result === "foreign" ? " — a foreign systemd unit named 'ccam' already exists" : ""}`
`started the dashboard server (pid ${pid})${result === "foreign" ? " — a foreign systemd unit named 'agenthub' already exists" : ""}`
);
}
}
@@ -563,7 +566,7 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
claudeHome: getClaudeHome(),
// Settles the fast path: whatever the outcome, the next SessionStart
// must not spawn a worker just to retry a network clone. Cleared by
// `/ccam-update` (force), which is the retry.
// `/agenthub-update` (force), which is the retry.
skillPluginsSettled: skillPlugins,
updatedAt: new Date().toISOString(),
},
@@ -592,11 +595,11 @@ function main() {
if (require.main === module) {
const force = process.argv.includes("--force");
if (force || process.argv.includes("--worker")) {
// `--force` (from /ccam-update) runs in the foreground so the user sees the
// `--force` (from /agenthub-update) runs in the foreground so the user sees the
// npm output and the result.
const result = bootstrap(runtimeDir(), { force });
if (force) {
console.log(result === "ok" ? "CCAM runtime refreshed." : `bootstrap: ${result}`);
console.log(result === "ok" ? "AgentHub runtime refreshed." : `bootstrap: ${result}`);
if (result !== "ok") process.exitCode = 1;
}
} else {