forked from EduCraft/curriculum-project-hub
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { prisma } from "../db.js";
|
|
import { OrganizationAgentConfiguration } from "../agent/configuration.js";
|
|
import { readSkillStoreRoot, verifyStoredSkill } from "../agent/skillStore.js";
|
|
import { readSiloOrganizationId } from "./silo.js";
|
|
|
|
async function main(argv: readonly string[]): Promise<void> {
|
|
const [command, ...args] = argv;
|
|
if (command === undefined || command === "help" || command === "--help") {
|
|
printHelp();
|
|
return;
|
|
}
|
|
const options = parseOptions(args);
|
|
const organizationId = required(options, "organization");
|
|
const siloOrganizationId = readSiloOrganizationId();
|
|
if (organizationId !== siloOrganizationId) {
|
|
throw new Error(`Silo Agent configuration is restricted to ${siloOrganizationId}`);
|
|
}
|
|
const configuration = new OrganizationAgentConfiguration(prisma, readSkillStoreRoot());
|
|
|
|
switch (command) {
|
|
case "install-skill": {
|
|
const installed = await configuration.installSkill({
|
|
organizationId,
|
|
sourceDir: required(options, "source"),
|
|
version: required(options, "version"),
|
|
});
|
|
console.log(JSON.stringify(installed));
|
|
return;
|
|
}
|
|
case "upsert-role": {
|
|
const systemPromptFile = options.get("system-prompt-file");
|
|
const toolsJson = options.get("tools-json");
|
|
const tools = toolsJson === undefined
|
|
? undefined
|
|
: toolsJson === "null"
|
|
? null
|
|
: parseTools(toolsJson);
|
|
const sortOrderRaw = options.get("sort-order");
|
|
const role = await configuration.upsertRole({
|
|
organizationId,
|
|
roleId: required(options, "role"),
|
|
label: required(options, "label"),
|
|
...(options.has("model") ? { defaultModel: options.get("model") ?? null } : {}),
|
|
...(systemPromptFile !== undefined
|
|
? { systemPrompt: await readFile(systemPromptFile, "utf8") }
|
|
: {}),
|
|
...(tools !== undefined ? { tools } : {}),
|
|
...(sortOrderRaw !== undefined ? { sortOrder: integer(sortOrderRaw, "sort-order") } : {}),
|
|
});
|
|
console.log(JSON.stringify(role));
|
|
return;
|
|
}
|
|
case "set-role-skills": {
|
|
const skills = required(options, "skills").split(",").map((name) => name.trim()).filter(Boolean);
|
|
await configuration.setRoleSkills({
|
|
organizationId,
|
|
roleId: required(options, "role"),
|
|
skillNames: skills,
|
|
});
|
|
console.log(JSON.stringify({ roleId: required(options, "role"), skills }));
|
|
return;
|
|
}
|
|
case "list": {
|
|
const roles = await prisma.organizationAgentRole.findMany({
|
|
where: { organizationId },
|
|
orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
|
|
include: {
|
|
skillBindings: {
|
|
orderBy: [{ sortOrder: "asc" }, { agentSkillId: "asc" }],
|
|
include: { skill: { select: { name: true, version: true, disabledAt: true } } },
|
|
},
|
|
},
|
|
});
|
|
console.log(JSON.stringify(roles.map((role) => ({
|
|
roleId: role.roleId,
|
|
label: role.label,
|
|
defaultModel: role.defaultModel,
|
|
systemPromptConfigured: role.systemPrompt !== null,
|
|
tools: role.tools,
|
|
disabled: role.disabledAt !== null,
|
|
skills: role.skillBindings.map((binding) => ({
|
|
name: binding.skill.name,
|
|
version: binding.skill.version,
|
|
disabled: binding.skill.disabledAt !== null,
|
|
})),
|
|
})), null, 2));
|
|
return;
|
|
}
|
|
case "verify-store": {
|
|
const skills = await prisma.organizationAgentSkill.findMany({
|
|
where: { organizationId, disabledAt: null },
|
|
select: { name: true, contentDigest: true },
|
|
});
|
|
for (const skill of skills) {
|
|
await verifyStoredSkill({ storeRoot: readSkillStoreRoot(), ...skill });
|
|
}
|
|
console.log(JSON.stringify({ verifiedSkills: skills.length }));
|
|
return;
|
|
}
|
|
default:
|
|
throw new Error(`unknown Agent configuration command: ${command}`);
|
|
}
|
|
}
|
|
|
|
function parseOptions(args: readonly string[]): Map<string, string> {
|
|
const options = new Map<string, string>();
|
|
for (let index = 0; index < args.length; index += 2) {
|
|
const flag = args[index];
|
|
const value = args[index + 1];
|
|
if (flag === undefined || !flag.startsWith("--") || value === undefined) {
|
|
throw new Error(`expected --name value, got: ${args.slice(index).join(" ")}`);
|
|
}
|
|
const name = flag.slice(2);
|
|
if (options.has(name)) throw new Error(`duplicate option: --${name}`);
|
|
options.set(name, value);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function required(options: ReadonlyMap<string, string>, name: string): string {
|
|
const value = options.get(name)?.trim();
|
|
if (value === undefined || value === "") throw new Error(`--${name} is required`);
|
|
return value;
|
|
}
|
|
|
|
function parseTools(raw: string): readonly string[] {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
|
|
throw new Error("--tools-json must be null or a JSON string array");
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function integer(raw: string, name: string): number {
|
|
const value = Number(raw);
|
|
if (!Number.isSafeInteger(value)) throw new Error(`--${name} must be an integer`);
|
|
return value;
|
|
}
|
|
|
|
function printHelp(): void {
|
|
console.log(`Usage:
|
|
agent-config install-skill --organization ORG --source DIR --version VERSION
|
|
agent-config upsert-role --organization ORG --role ID --label LABEL [--model MODEL] [--system-prompt-file FILE] [--tools-json JSON] [--sort-order N]
|
|
agent-config set-role-skills --organization ORG --role ID --skills name,name
|
|
agent-config list --organization ORG
|
|
agent-config verify-store --organization ORG`);
|
|
}
|
|
|
|
main(process.argv.slice(2))
|
|
.catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|