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
+22 -22
View File
@@ -108,7 +108,7 @@ function fixtureLines(sessionId, cwd, model, inputTok, outputTok) {
}
function writeFixtureDir() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fixture-"));
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-fixture-"));
const projDir = path.join(root, "-Users-demo-project");
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(
@@ -175,14 +175,14 @@ describe("POST /api/import/scan-path validation", () => {
it("rejects non-existent paths", async () => {
const res = await post("/api/import/scan-path", {
path: "/definitely/does/not/exist/ccam-" + Date.now(),
path: "/definitely/does/not/exist/agenthub-" + Date.now(),
});
assert.equal(res.status, 400);
assert.equal(res.body.error.code, "PATH_NOT_FOUND");
});
it("rejects files (not directories)", async () => {
const tmp = path.join(os.tmpdir(), `ccam-not-dir-${Date.now()}.txt`);
const tmp = path.join(os.tmpdir(), `agenthub-not-dir-${Date.now()}.txt`);
fs.writeFileSync(tmp, "hello");
try {
const res = await post("/api/import/scan-path", { path: tmp });
@@ -260,14 +260,14 @@ describe("POST /api/import/scan-path happy path", () => {
describe("archive helpers", () => {
it("isPathInside rejects traversal", () => {
const root = path.resolve("/tmp/ccam-root");
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/ok.jsonl"), true);
const root = path.resolve("/tmp/agenthub-root");
assert.equal(archive.isPathInside(root, "/tmp/agenthub-root/ok.jsonl"), true);
assert.equal(archive.isPathInside(root, "/tmp/other/bad.jsonl"), false);
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/../escape"), false);
assert.equal(archive.isPathInside(root, "/tmp/agenthub-root/../escape"), false);
});
it("safeJoin rejects absolute and traversal entries", () => {
const root = path.resolve("/tmp/ccam-root");
const root = path.resolve("/tmp/agenthub-root");
assert.equal(archive.safeJoin(root, "/etc/passwd"), path.join(root, "etc/passwd"));
assert.equal(archive.safeJoin(root, "../escape.txt"), null);
assert.equal(archive.safeJoin(root, "deep/../../escape"), null);
@@ -286,7 +286,7 @@ describe("archive helpers", () => {
});
it("extractGzSingle decompresses plain gz", async () => {
const dest = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gz-"));
const dest = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gz-"));
const src = path.join(dest, "sample.jsonl.gz");
fs.writeFileSync(src, zlib.gzipSync(Buffer.from('{"ok":true}\n')));
try {
@@ -301,7 +301,7 @@ describe("archive helpers", () => {
describe("importFromDirectory directly", () => {
it("reports progress and never throws on empty dirs", async () => {
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-empty-"));
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-empty-"));
try {
const events = [];
const counters = await importHistory.importFromDirectory({ db, stmts }, empty, {
@@ -317,7 +317,7 @@ describe("importFromDirectory directly", () => {
it("matches the legacy importer's token totals on the same fixtures", async () => {
// Clean any tokens from prior tests for a fresh comparison.
const freshSession = "cccccccc-3333-4333-8333-cccccccccccc";
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fresh-"));
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-fresh-"));
const projDir = path.join(root, "-Users-demo-fresh");
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(
@@ -345,9 +345,9 @@ describe("importFromDirectory directly", () => {
// (default 30 days). Import must snapshot the transcript into the
// dashboard's own data dir so the conversation survives that deletion.
const prevDataDir = process.env.DASHBOARD_DATA_DIR;
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-data-"));
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-data-"));
process.env.DASHBOARD_DATA_DIR = dataDir;
const src = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-src-"));
const src = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-src-"));
const sessionId = "dddddddd-4444-4444-8444-dddddddddddd";
fs.writeFileSync(
path.join(src, `${sessionId}.jsonl`),
@@ -396,7 +396,7 @@ describe("POST /api/import/rescan", () => {
describe("tar path-traversal hardening", () => {
it("extractTar rejects entries with ../ segments", async () => {
const tar = require("tar");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-tar-bad-"));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-tar-bad-"));
const stageDir = path.join(tmp, "stage");
const targetDir = path.join(tmp, "target");
fs.mkdirSync(stageDir, { recursive: true });
@@ -435,12 +435,12 @@ describe("tar path-traversal hardening", () => {
describe("extraction size cap", () => {
it("extractGzSingle aborts past MAX_EXTRACT_BYTES", async () => {
const prev = process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = "128";
const prev = process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES;
process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES = "128";
// Re-require to pick up the lowered limit for this one check.
delete require.cache[require.resolve("../lib/archive")];
const localArchive = require("../lib/archive");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-bomb-"));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-bomb-"));
const gzPath = path.join(tmp, "bomb.jsonl.gz");
// 2 KB of zeros compresses to a few bytes — decompressing blows past 128 B.
fs.writeFileSync(gzPath, zlib.gzipSync(Buffer.alloc(2048, 0)));
@@ -451,8 +451,8 @@ describe("extraction size cap", () => {
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
if (prev === undefined) delete process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
else process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = prev;
if (prev === undefined) delete process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES;
else process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES = prev;
// Restore the module with production limits for subsequent tests.
delete require.cache[require.resolve("../lib/archive")];
require("../lib/archive");
@@ -464,7 +464,7 @@ describe("orphan subagent inference", () => {
it("attaches subagent via Layout 2: <proj>/subagents/<sessionId>/agent.jsonl", async () => {
const orphanSession = "dddddddd-4444-4444-8444-dddddddddddd";
// Seed a parent session first.
const seedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-seed-"));
const seedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-orphan-seed-"));
const seedProj = path.join(seedRoot, "project");
fs.mkdirSync(seedProj, { recursive: true });
fs.writeFileSync(
@@ -477,7 +477,7 @@ describe("orphan subagent inference", () => {
assert.ok(stmts.getSession.get(orphanSession), "parent session must exist before orphan pass");
// Now create an "orphan" subagent tree in the non-standard layout.
const orphanRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-"));
const orphanRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-orphan-"));
const orphanLayoutDir = path.join(orphanRoot, "project", "subagents", orphanSession);
fs.mkdirSync(orphanLayoutDir, { recursive: true });
const subAgentId = "agent-xyz";
@@ -519,8 +519,8 @@ describe("concurrent scan-path requests", () => {
it("two concurrent imports of different folders both succeed without clobbering", async () => {
const sessA = "eeeeeeee-5555-4555-8555-eeeeeeeeeeee";
const sessB = "ffffffff-6666-4666-8666-ffffffffffff";
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-a-"));
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-b-"));
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-concurrent-a-"));
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-concurrent-b-"));
fs.mkdirSync(path.join(rootA, "-Users-demo-a"), { recursive: true });
fs.mkdirSync(path.join(rootB, "-Users-demo-b"), { recursive: true });
fs.writeFileSync(