@sema-agent/core
Version:
Stateless, task-oriented AI agent core
118 lines (117 loc) • 5.44 kB
JavaScript
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { entryAccessible } from "../../agents/roster-store.js";
import { normalizeAgentName } from "../../core/task-registry.js";
import { atomicWriteFile } from "../file/fs-atomic.js";
import { withCcLock } from "./lockfile.js";
export function createCcTeamsRosterAdapter(opts) {
const scope = opts.scope ?? "default";
if (scope !== "default") {
throw new Error(`CcTeamsRosterAdapter: CC team files carry no tenant axis — this adapter is licensed for single-tenant scope:"default" deployments only (D-1); got scope ${JSON.stringify(scope)}`);
}
const dir = resolve(opts.teamDir);
const configPath = join(dir, "config.json");
const now = opts.now ?? Date.now;
const load = () => {
try {
return JSON.parse(readFileSync(configPath, "utf8"));
}
catch (err) {
if (err.code === "ENOENT")
return undefined;
throw err;
}
};
const save = (config) => {
atomicWriteFile(dir, configPath, JSON.stringify(config, null, 2));
};
const toEntry = (m) => ({
name: m.name,
agentId: m.agentId,
...(typeof m.semaSessionId === "string" ? { sessionId: m.semaSessionId } : {}),
...(typeof m.semaToolUseId === "string" ? { toolUseId: m.semaToolUseId } : {}),
...(typeof m.semaRootSessionId === "string" ? { rootSessionId: m.semaRootSessionId } : {}),
...(typeof m.semaModel === "string" ? { model: m.semaModel } : {}),
...(m.semaSessionScoped === true ? { sessionScoped: true } : {}),
owner: typeof m.semaOwner === "string" ? m.semaOwner : opts.owner,
scope,
createdAt: typeof m.joinedAt === "number" ? m.joinedAt : 0,
});
const absent = () => !existsSync(configPath);
return {
record: (entry) => {
if (entry.scope !== scope) {
throw new Error(`CcTeamsRosterAdapter: cannot record entry with scope ${JSON.stringify(entry.scope)} — this adapter persists scope:"${scope}" bindings only (D-1 single-tenant)`);
}
mkdirSync(dir, { recursive: true });
return withCcLock(configPath, () => {
const config = load() ?? {
name: basename(dir),
createdAt: now(),
members: [],
};
const members = Array.isArray(config.members) ? config.members : [];
const prior = members.find((m) => m.agentId === entry.agentId);
const member = {
...(prior ?? {}),
agentId: entry.agentId,
name: entry.name,
...(entry.model !== undefined ? { semaModel: entry.model } : {}),
...(entry.sessionId !== undefined ? { semaSessionId: entry.sessionId } : {}),
...(entry.toolUseId !== undefined ? { semaToolUseId: entry.toolUseId } : {}),
...(entry.rootSessionId !== undefined ? { semaRootSessionId: entry.rootSessionId } : {}),
...(entry.sessionScoped === true ? { semaSessionScoped: true } : {}),
semaOwner: entry.owner,
joinedAt: entry.createdAt,
};
config.members = [...members.filter((m) => m.agentId !== entry.agentId), member];
save(config);
});
},
resolve: (name, access) => {
if (absent())
return undefined;
return withCcLock(configPath, () => {
const config = load();
if (config === undefined || !Array.isArray(config.members))
return undefined;
const key = normalizeAgentName(name);
if (key === "")
return undefined;
const pool = config.members
.filter((m) => typeof m.agentId === "string" && typeof m.name === "string")
.map(toEntry)
.filter((e) => normalizeAgentName(e.name) === key && entryAccessible(e, access));
let best;
for (const e of pool)
if (best === undefined || e.createdAt >= best.createdAt)
best = e;
return best;
});
},
list: () => {
if (absent())
return [];
return withCcLock(configPath, () => {
const config = load();
if (config === undefined || !Array.isArray(config.members))
return [];
return config.members.filter((m) => typeof m.agentId === "string" && typeof m.name === "string").map(toEntry);
});
},
releaseAgent: (agentId) => {
if (absent())
return;
return withCcLock(configPath, () => {
const config = load();
if (config === undefined || !Array.isArray(config.members))
return;
const next = config.members.filter((m) => m.agentId !== agentId);
if (next.length === config.members.length)
return;
config.members = next;
save(config);
});
},
};
}