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:
@@ -11,7 +11,7 @@
|
||||
* a fresh clone or checkout stamps every file with the same time in arbitrary
|
||||
* order.
|
||||
*
|
||||
* Run by the pre-commit hook and by `/ccam-doctor`.
|
||||
* Run by the pre-commit hook and by `/agenthub-doctor`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
|
||||
+11
-11
@@ -15,7 +15,7 @@ const SETTINGS_PATH = getSettingsPath();
|
||||
const HOOK_HANDLER = path.resolve(__dirname, "hook-handler.js").replace(/\\/g, "/");
|
||||
|
||||
/**
|
||||
* True when the plugin bootstrap has run on this machine — i.e. the `ccam`
|
||||
* True when the plugin bootstrap has run on this machine — i.e. the `agenthub`
|
||||
* plugin is (or was) installed and supplies its own hook entries.
|
||||
* Read-only and never throws; `plugin-bootstrap.js` is not required here to
|
||||
* keep this module free of a circular dependency.
|
||||
@@ -38,8 +38,8 @@ function envFlag(name) {
|
||||
/**
|
||||
* True when this process is running inside a container (Docker, Podman, or a
|
||||
* Kubernetes pod). Detected via the Docker/Podman marker files, the OCI/systemd
|
||||
* `container` env var, and a Linux cgroup heuristic. `CCAM_FORCE_CONTAINER=1`
|
||||
* forces a positive result and `CCAM_FORCE_HOST=1` forces a negative result
|
||||
* `container` env var, and a Linux cgroup heuristic. `AGENTHUB_FORCE_CONTAINER=1`
|
||||
* forces a positive result and `AGENTHUB_FORCE_HOST=1` forces a negative result
|
||||
* (used by tests / to override misfiring detection).
|
||||
*
|
||||
* Why this matters (GitHub #193): the hook command written into
|
||||
@@ -53,8 +53,8 @@ function envFlag(name) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInsideContainer() {
|
||||
if (envFlag("CCAM_FORCE_CONTAINER")) return true;
|
||||
if (envFlag("CCAM_FORCE_HOST")) return false;
|
||||
if (envFlag("AGENTHUB_FORCE_CONTAINER")) return true;
|
||||
if (envFlag("AGENTHUB_FORCE_HOST")) return false;
|
||||
try {
|
||||
if (fs.existsSync("/.dockerenv")) return true; // Docker
|
||||
if (fs.existsSync("/run/.containerenv")) return true; // Podman
|
||||
@@ -95,7 +95,7 @@ function containerRefusalMessage() {
|
||||
" publishes — so a host-installed hook reaches the containerized dashboard.",
|
||||
"",
|
||||
" If you genuinely run Claude Code inside this same container, override with:",
|
||||
" CCAM_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks",
|
||||
" AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -138,19 +138,19 @@ function installHooks(silent = false) {
|
||||
// Host-only guard (issue #193): never write a container-internal handler path
|
||||
// into a (potentially bind-mounted) host settings file. Honors an explicit
|
||||
// opt-out for the rare case of running Claude Code inside this same container.
|
||||
if (isInsideContainer() && !envFlag("CCAM_ALLOW_CONTAINER_HOOKS")) {
|
||||
if (isInsideContainer() && !envFlag("AGENTHUB_ALLOW_CONTAINER_HOOKS")) {
|
||||
if (!silent) console.error(containerRefusalMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// The `ccam` plugin installs the same eight hooks itself. Running both means
|
||||
// The `agenthub` plugin installs the same eight hooks itself. Running both means
|
||||
// every event is POSTed twice and token/cost figures double — warn loudly
|
||||
// rather than silently double-count. (`/ccam-doctor` reports the same state.)
|
||||
// rather than silently double-count. (`/agenthub-doctor` reports the same state.)
|
||||
if (!silent && pluginBootstrapRan()) {
|
||||
console.warn(
|
||||
"WARNING: the ccam plugin is installed and already provides these hooks.\n" +
|
||||
"WARNING: the agenthub plugin is installed and already provides these hooks.\n" +
|
||||
" Installing them again double-counts every event. Uninstall the\n" +
|
||||
" plugin first, or skip this step. Run /ccam-doctor to check."
|
||||
" plugin first, or skip this step. Run /agenthub-doctor to check."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+34
-31
@@ -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 {
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file plugin-doctor.js
|
||||
* @description Reports the health of a plugin-installed CCAM: Node version,
|
||||
* @description Reports the health of a plugin-installed AgentHub: Node version,
|
||||
* bootstrap state, runtime dependencies, server liveness, duplicate hook
|
||||
* entries (the one failure that silently doubles every token and cost figure),
|
||||
* the `ccam` CLI launcher and its PATH, whether the committed MCP build still
|
||||
* the `agenthub` CLI launcher and its PATH, whether the committed MCP build still
|
||||
* matches `mcp/src`, and whether the slash commands the pipeline templates name
|
||||
* as node drivers are actually resolvable (the `superpowers` plugin, today).
|
||||
* Read-only — it diagnoses, it never repairs.
|
||||
@@ -32,7 +32,7 @@ function diagnose() {
|
||||
"Node",
|
||||
boot.nodeVersionOk()
|
||||
? `v${process.versions.node}`
|
||||
: `v${process.versions.node} — CCAM needs >= ${boot.MIN_NODE.join(".")} (node:sqlite)`
|
||||
: `v${process.versions.node} — AgentHub needs >= ${boot.MIN_NODE.join(".")} (node:sqlite)`
|
||||
);
|
||||
|
||||
add("ok", "Plugin root", PLUGIN_ROOT);
|
||||
@@ -51,7 +51,7 @@ function diagnose() {
|
||||
moved ? "warn" : "ok",
|
||||
"Bootstrap",
|
||||
moved
|
||||
? `recorded against a previous plugin version (${state.pluginRoot}) — run /ccam-update`
|
||||
? `recorded against a previous plugin version (${state.pluginRoot}) — run /agenthub-update`
|
||||
: `last run ${state.updatedAt}`
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function diagnose() {
|
||||
add(
|
||||
deps ? "ok" : "fail",
|
||||
"Runtime deps",
|
||||
deps ? path.join(rt, "node_modules") : "missing — run /ccam-update"
|
||||
deps ? path.join(rt, "node_modules") : "missing — run /agenthub-update"
|
||||
);
|
||||
|
||||
const live = boot.liveServers();
|
||||
@@ -96,7 +96,7 @@ function diagnose() {
|
||||
add(
|
||||
fs.existsSync(path.join(dist, "index.html")) ? "ok" : "warn",
|
||||
"Dashboard UI",
|
||||
fs.existsSync(path.join(dist, "index.html")) ? dist : "not built yet — run /ccam-open"
|
||||
fs.existsSync(path.join(dist, "index.html")) ? dist : "not built yet — run /agenthub-open"
|
||||
);
|
||||
|
||||
return out;
|
||||
@@ -148,16 +148,16 @@ function countLegacyHookEntries() {
|
||||
|
||||
function cliStatus() {
|
||||
const dir = path.join(os.homedir(), ".local", "bin");
|
||||
const file = path.join(dir, process.platform === "win32" ? "ccam.cmd" : "ccam");
|
||||
if (!fs.existsSync(file)) return ["warn", "ccam CLI", `no launcher at ${file}`];
|
||||
const file = path.join(dir, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
|
||||
if (!fs.existsSync(file)) return ["warn", "agenthub CLI", `no launcher at ${file}`];
|
||||
const onPath = (process.env.PATH || "")
|
||||
.split(path.delimiter)
|
||||
.some((p) => path.resolve(p) === path.resolve(dir));
|
||||
return onPath
|
||||
? ["ok", "ccam CLI", file]
|
||||
? ["ok", "agenthub CLI", file]
|
||||
: [
|
||||
"warn",
|
||||
"ccam CLI",
|
||||
"agenthub CLI",
|
||||
`${file} exists but ${dir} is not on PATH — add: export PATH="${dir}:$PATH"`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @description Builds the dashboard UI bundle for a plugin install and prints
|
||||
* the dashboard URL. `scripts/plugin-bootstrap.js` calls `buildClient` eagerly
|
||||
* on session start, so client-only routes (e.g. `/run`) work the moment
|
||||
* `claude` is started, not just the API and MCP tools. `/ccam-open` re-runs
|
||||
* `claude` is started, not just the API and MCP tools. `/agenthub-open` re-runs
|
||||
* this standalone — a no-op when the bundle already matches — as a fallback
|
||||
* for `--force` rebuilds or a first build that failed during bootstrap, and it
|
||||
* re-runs bootstrap first when the live server belongs to a retired plugin root
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
/**
|
||||
* @file systemd-service.js
|
||||
* @description Shared helper for installing/removing a systemd `--user`
|
||||
* service that keeps the CCAM dashboard server running across reboots,
|
||||
* service that keeps the AgentHub dashboard server running across reboots,
|
||||
* without needing Claude Code (or any other tool) to be started first.
|
||||
* Used by both `ccam service install` (checkout installs) and
|
||||
* Used by both `agenthub service install` (checkout installs) and
|
||||
* `plugin-bootstrap.js` (plugin installs) so the two paths stay in sync.
|
||||
* Linux/systemd only — callers fall back to their existing manual-start
|
||||
* path when `systemctlAvailable()` is false.
|
||||
@@ -16,7 +16,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const MARKER = "# ccam-managed-unit — regenerated on install, do not edit by hand";
|
||||
const MARKER = "# agenthub-managed-unit — regenerated on install, do not edit by hand";
|
||||
|
||||
function unitDir() {
|
||||
return path.join(os.homedir(), ".config", "systemd", "user");
|
||||
|
||||
Reference in New Issue
Block a user