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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user