feat(FN-976): add companies.sh agent import for CLI and dashboard

- Add companies.sh type definitions and parser in @fusion/core with comprehensive tests
- Create CLI `kb agent-import` command to import agents from companies.sh
- Add POST /api/agents/import dashboard endpoint with dry-run support
- Build AgentImportModal component with search, preview, and batch import UI
- Add dashboard route tests for the agent import API endpoint
- Create changeset for @gsxdsm/fusion minor bump
This commit is contained in:
gsxdsm
2026-04-07 11:41:38 -07:00
parent 43b1f6e772
commit 39f4f4929f
17 changed files with 2388 additions and 20 deletions

View File

@@ -0,0 +1,314 @@
import { describe, it, expect } from "vitest";
import {
parseCompaniesShManifest,
companiesShAgentToAgentCreateInput,
convertCompaniesShAgents,
mapRoleToCapability,
CompaniesShParseError,
} from "./companies-sh-parser.js";
// ── Helpers ──────────────────────────────────────────────────────────────
function encodeManifest(agents: unknown[]): string {
return Buffer.from(JSON.stringify(agents)).toString("base64");
}
function makeScript(companyName: string, agents: unknown[], envLines?: string[]): string {
const manifest = encodeManifest(agents);
let script = `#!/bin/bash\n# Agent Company Manifest\nCOMPANY_NAME="${companyName}"\nAGENT_MANIFEST="${manifest}"`;
if (envLines && envLines.length > 0) {
script += "\n\n" + envLines.join("\n");
}
return script;
}
// ── Tests ────────────────────────────────────────────────────────────────
describe("companies-sh-parser", () => {
describe("parseCompaniesShManifest", () => {
it("parses a valid companies.sh manifest", () => {
const agents = [
{
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review", "security-audit"],
config: { model: "claude-sonnet-4", maxTokens: 4096, thinkingLevel: "medium" },
metadata: { title: "Senior Code Reviewer", icon: "👁" },
},
];
const script = makeScript("test-company", agents, [
'export KB_AGENT_MODEL="${KB_AGENT_MODEL:-claude-sonnet-4}"',
]);
const manifest = parseCompaniesShManifest(script);
expect(manifest.companyName).toBe("test-company");
expect(manifest.agents).toHaveLength(1);
expect(manifest.agents[0].name).toBe("Code Reviewer");
expect(manifest.agents[0].role).toBe("reviewer");
expect(manifest.agents[0].capabilities).toEqual(["code-review", "security-audit"]);
expect(manifest.agents[0].config?.model).toBe("claude-sonnet-4");
expect(manifest.agents[0].metadata?.title).toBe("Senior Code Reviewer");
expect(manifest.envVars).toHaveLength(1);
expect(manifest.envVars[0].name).toBe("KB_AGENT_MODEL");
expect(manifest.envVars[0].defaultValue).toBe("claude-sonnet-4");
});
it("parses a manifest with multiple agents", () => {
const agents = [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
{ name: "Agent 3", role: "triage" },
];
const script = makeScript("multi-agent", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents).toHaveLength(3);
expect(manifest.agents.map((a) => a.name)).toEqual(["Agent 1", "Agent 2", "Agent 3"]);
});
it("throws on empty content", () => {
expect(() => parseCompaniesShManifest("")).toThrow(CompaniesShParseError);
expect(() => parseCompaniesShManifest("")).toThrow("empty or not a string");
});
it("throws on missing COMPANY_NAME", () => {
const manifest = encodeManifest([{ name: "Test", role: "executor" }]);
const script = `#!/bin/bash\nAGENT_MANIFEST="${manifest}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Missing COMPANY_NAME");
});
it("throws on missing AGENT_MANIFEST", () => {
const script = `#!/bin/bash\nCOMPANY_NAME="test"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Missing AGENT_MANIFEST");
});
it("throws on invalid base64 encoding", () => {
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="not-valid-base64!!!"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Invalid base64");
});
it("throws on invalid JSON in manifest", () => {
const badJson = btoa("not json");
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="${badJson}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Invalid JSON");
});
it("throws when manifest decodes to non-array", () => {
const obj = btoa(JSON.stringify({ name: "not an array" }));
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="${obj}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("must decode to a JSON array");
});
it("throws on agent missing name", () => {
const agents = [{ role: "executor" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: name");
});
it("throws on agent missing role", () => {
const agents = [{ name: "Test Agent" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: role");
});
it("throws on agent with empty name", () => {
const agents = [{ name: " ", role: "executor" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: name");
});
it("throws on agent with empty role", () => {
const agents = [{ name: "Test", role: "" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: role");
});
it("handles empty capabilities array", () => {
const agents = [{ name: "Test", role: "executor", capabilities: [] }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toEqual([]);
});
it("handles agents with no optional fields", () => {
const agents = [{ name: "Minimal", role: "custom" }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toBeUndefined();
expect(manifest.agents[0].config).toBeUndefined();
expect(manifest.agents[0].metadata).toBeUndefined();
});
it("extracts multiple environment variables", () => {
const agents = [{ name: "Test", role: "executor" }];
const script = makeScript("test", agents, [
'export KB_MODEL="${KB_MODEL:-claude-sonnet-4}"',
'export KB_THINKING="${KB_THINKING:-medium}"',
'export KB_MAX_TOKENS="${KB_MAX_TOKENS:-4096}"',
]);
const manifest = parseCompaniesShManifest(script);
expect(manifest.envVars).toHaveLength(3);
expect(manifest.envVars.map((v) => v.name)).toEqual([
"KB_MODEL",
"KB_THINKING",
"KB_MAX_TOKENS",
]);
});
it("returns empty envVars when no export statements", () => {
const agents = [{ name: "Test", role: "executor" }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.envVars).toEqual([]);
});
it("handles non-object agent entries", () => {
const agents = ["not an object", 42, null];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("not an object");
});
it("filters non-string capabilities", () => {
const agents = [{ name: "Test", role: "executor", capabilities: ["valid", 123, null, "also-valid"] }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toEqual(["valid", "also-valid"]);
});
});
describe("mapRoleToCapability", () => {
it("maps all known roles correctly", () => {
expect(mapRoleToCapability("triage")).toBe("triage");
expect(mapRoleToCapability("executor")).toBe("executor");
expect(mapRoleToCapability("reviewer")).toBe("reviewer");
expect(mapRoleToCapability("merger")).toBe("merger");
expect(mapRoleToCapability("scheduler")).toBe("scheduler");
expect(mapRoleToCapability("engineer")).toBe("engineer");
expect(mapRoleToCapability("custom")).toBe("custom");
});
it("maps unknown roles to custom", () => {
expect(mapRoleToCapability("analyst")).toBe("custom");
expect(mapRoleToCapability("designer")).toBe("custom");
expect(mapRoleToCapability("")).toBe("custom");
});
});
describe("companiesShAgentToAgentCreateInput", () => {
it("converts a minimal agent", () => {
const agent = { name: "Test", role: "executor" };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.name).toBe("Test");
expect(input.role).toBe("executor");
expect(input.metadata).toBeUndefined();
expect(input.runtimeConfig).toBeUndefined();
});
it("converts a fully populated agent", () => {
const agent = {
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review"],
config: {
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium" as const,
maxTurns: 10,
},
metadata: {
title: "Senior Reviewer",
icon: "👁",
description: "Reviews code for quality",
},
};
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.name).toBe("Code Reviewer");
expect(input.role).toBe("reviewer");
expect(input.title).toBe("Senior Reviewer");
expect(input.icon).toBe("👁");
expect(input.runtimeConfig).toEqual({
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium",
maxTurns: 10,
});
expect(input.metadata).toEqual({
capabilities: ["code-review"],
description: "Reviews code for quality",
});
});
it("maps unknown roles to custom", () => {
const agent = { name: "Special", role: "analyst" };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.role).toBe("custom");
});
it("handles agent with empty capabilities", () => {
const agent = { name: "Test", role: "executor", capabilities: [] };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.metadata).toBeUndefined();
});
});
describe("convertCompaniesShAgents", () => {
it("converts all agents when no duplicates", () => {
const agents = [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
];
const { inputs, result } = convertCompaniesShAgents(agents);
expect(inputs).toHaveLength(2);
expect(result.created).toEqual(["Agent 1", "Agent 2"]);
expect(result.skipped).toEqual([]);
expect(result.errors).toEqual([]);
});
it("skips agents with existing names", () => {
const agents = [
{ name: "Existing Agent", role: "executor" },
{ name: "New Agent", role: "reviewer" },
];
const { inputs, result } = convertCompaniesShAgents(agents, {
skipExisting: ["Existing Agent"],
});
expect(inputs).toHaveLength(1);
expect(inputs[0].name).toBe("New Agent");
expect(result.skipped).toEqual(["Existing Agent"]);
});
it("handles empty agent list", () => {
const { inputs, result } = convertCompaniesShAgents([]);
expect(inputs).toHaveLength(0);
expect(result.created).toEqual([]);
});
});
});

View File

@@ -0,0 +1,269 @@
/**
* Parser for companies.sh manifest files.
*
* Extracts agent definitions from shell-script-based manifests following
* the companies.sh standard. Handles base64-encoded JSON payloads,
* shell variable extraction, and environment variable defaults.
*
* @module companies-sh-parser
*/
import type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShEnvVar,
CompaniesShImportResult,
} from "./companies-sh-types.js";
import type { AgentCreateInput, AgentCapability } from "./types.js";
// ── Parsing Errors ───────────────────────────────────────────────────────
export class CompaniesShParseError extends Error {
constructor(message: string) {
super(message);
this.name = "CompaniesShParseError";
}
}
// ── Role Mapping ─────────────────────────────────────────────────────────
const VALID_ROLES: Set<string> = new Set([
"triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom",
]);
/**
* Map a companies.sh role string to a kb AgentCapability.
* Unknown roles fall back to "custom".
*/
export function mapRoleToCapability(role: string): AgentCapability {
if (VALID_ROLES.has(role)) {
return role as AgentCapability;
}
return "custom";
}
// ── Shell Variable Extraction ────────────────────────────────────────────
/**
* Extract a shell variable value from script content.
* Handles both `VAR="value"` and `VAR='value'` syntax.
* Returns null if the variable is not found.
*/
function extractShellVariable(script: string, varName: string): string | null {
// Match VAR="value" or VAR='value' — capture the value inside quotes
const regex = new RegExp(`^${varName}=["'](.*)["']\\s*$`, "m");
const match = script.match(regex);
if (!match) return null;
return match[1];
}
/**
* Extract environment variable defaults from export statements.
* Matches `export VAR="${VAR:-default}"` pattern.
*/
function extractEnvVars(script: string): CompaniesShEnvVar[] {
const envVars: CompaniesShEnvVar[] = [];
const regex = /^export\s+(\w+)="\$\{(?:\w+):-(.*?)\}"\s*$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(script)) !== null) {
envVars.push({
name: match[1],
defaultValue: match[2],
});
}
return envVars;
}
// ── Validation ───────────────────────────────────────────────────────────
/**
* Validate a single parsed agent has required fields.
* Throws if name or role is missing or invalid type.
*/
function validateAgent(agent: unknown, index: number): CompaniesShAgent {
if (!agent || typeof agent !== "object") {
throw new CompaniesShParseError(`Agent at index ${index} is not an object`);
}
const obj = agent as Record<string, unknown>;
if (typeof obj.name !== "string" || obj.name.trim() === "") {
throw new CompaniesShParseError(`Agent at index ${index} is missing required field: name`);
}
if (typeof obj.role !== "string" || obj.role.trim() === "") {
throw new CompaniesShParseError(`Agent at index ${index} is missing required field: role`);
}
return {
name: obj.name,
role: obj.role,
capabilities: Array.isArray(obj.capabilities)
? obj.capabilities.filter((c: unknown) => typeof c === "string")
: undefined,
config: obj.config && typeof obj.config === "object"
? {
...(typeof (obj.config as Record<string, unknown>).model === "string" && { model: (obj.config as Record<string, unknown>).model as string }),
...(typeof (obj.config as Record<string, unknown>).maxTokens === "number" && { maxTokens: (obj.config as Record<string, unknown>).maxTokens as number }),
...(typeof (obj.config as Record<string, unknown>).thinkingLevel === "string" && { thinkingLevel: (obj.config as Record<string, unknown>).thinkingLevel as CompaniesShAgent["config"] extends { thinkingLevel?: infer T } ? T : never }),
...(typeof (obj.config as Record<string, unknown>).maxTurns === "number" && { maxTurns: (obj.config as Record<string, unknown>).maxTurns as number }),
}
: undefined,
metadata: obj.metadata && typeof obj.metadata === "object"
? {
...(typeof (obj.metadata as Record<string, unknown>).title === "string" && { title: (obj.metadata as Record<string, unknown>).title as string }),
...(typeof (obj.metadata as Record<string, unknown>).icon === "string" && { icon: (obj.metadata as Record<string, unknown>).icon as string }),
...(typeof (obj.metadata as Record<string, unknown>).description === "string" && { description: (obj.metadata as Record<string, unknown>).description as string }),
}
: undefined,
};
}
// ── Main Parser ──────────────────────────────────────────────────────────
/**
* Parse a companies.sh manifest from raw script content.
*
* Extracts:
* - COMPANY_NAME shell variable
* - AGENT_MANIFEST base64-encoded JSON array
* - Environment variable defaults from export statements
*
* @throws {CompaniesShParseError} If the manifest is malformed
*/
export function parseCompaniesShManifest(scriptContent: string): CompaniesShManifest {
if (!scriptContent || typeof scriptContent !== "string") {
throw new CompaniesShParseError("Manifest content is empty or not a string");
}
// Extract company name
const companyName = extractShellVariable(scriptContent, "COMPANY_NAME");
if (!companyName) {
throw new CompaniesShParseError("Missing COMPANY_NAME variable in manifest");
}
// Extract and decode agent manifest
const manifestBase64 = extractShellVariable(scriptContent, "AGENT_MANIFEST");
if (!manifestBase64) {
throw new CompaniesShParseError("Missing AGENT_MANIFEST variable in manifest");
}
let manifestJson: string;
try {
// Validate base64 format — atob throws on invalid base64 characters
atob(manifestBase64);
} catch {
throw new CompaniesShParseError("Invalid base64 encoding in AGENT_MANIFEST");
}
// Decode using Buffer for proper UTF-8 support
manifestJson = Buffer.from(manifestBase64, "base64").toString("utf-8");
let rawAgents: unknown[];
try {
const parsed = JSON.parse(manifestJson);
if (!Array.isArray(parsed)) {
throw new CompaniesShParseError("AGENT_MANIFEST must decode to a JSON array");
}
rawAgents = parsed;
} catch (err) {
if (err instanceof CompaniesShParseError) throw err;
throw new CompaniesShParseError(`Invalid JSON in AGENT_MANIFEST: ${(err as Error).message}`);
}
// Validate each agent
const agents: CompaniesShAgent[] = rawAgents.map((agent, index) =>
validateAgent(agent, index)
);
// Extract environment variable defaults
const envVars = extractEnvVars(scriptContent);
return { companyName, agents, envVars };
}
// ── Conversion ───────────────────────────────────────────────────────────
/**
* Convert a companies.sh agent definition to a kb AgentCreateInput.
* Maps roles and extracts relevant configuration.
*/
export function companiesShAgentToAgentCreateInput(
agent: CompaniesShAgent,
): AgentCreateInput {
const input: AgentCreateInput = {
name: agent.name,
role: mapRoleToCapability(agent.role),
};
if (agent.metadata?.title) {
input.title = agent.metadata.title;
}
if (agent.metadata?.icon) {
input.icon = agent.metadata.icon;
}
if (agent.config) {
input.runtimeConfig = {};
if (agent.config.model) input.runtimeConfig.model = agent.config.model;
if (agent.config.maxTokens) input.runtimeConfig.maxTokens = agent.config.maxTokens;
if (agent.config.thinkingLevel) input.runtimeConfig.thinkingLevel = agent.config.thinkingLevel;
if (agent.config.maxTurns) input.runtimeConfig.maxTurns = agent.config.maxTurns;
}
// Store capabilities and description in metadata
const metadata: Record<string, unknown> = {};
if (agent.capabilities && agent.capabilities.length > 0) {
metadata.capabilities = agent.capabilities;
}
if (agent.metadata?.description) {
metadata.description = agent.metadata.description;
}
if (Object.keys(metadata).length > 0) {
input.metadata = metadata;
}
return input;
}
/**
* Convert multiple companies.sh agents to AgentCreateInput array,
* optionally skipping agents with errors.
*
* Returns an import result with created names, skipped names, and errors.
*/
export function convertCompaniesShAgents(
agents: CompaniesShAgent[],
options?: { skipExisting?: string[] },
): { inputs: AgentCreateInput[]; result: CompaniesShImportResult } {
const existingNames = new Set(options?.skipExisting ?? []);
const inputs: AgentCreateInput[] = [];
const importResult: CompaniesShImportResult = {
created: [],
skipped: [],
errors: [],
};
for (const agent of agents) {
// Skip agents that already exist by name
if (existingNames.has(agent.name)) {
importResult.skipped.push(agent.name);
continue;
}
try {
const input = companiesShAgentToAgentCreateInput(agent);
inputs.push(input);
importResult.created.push(agent.name);
} catch (err) {
importResult.errors.push({
name: agent.name,
error: (err as Error).message,
});
}
}
return { inputs, result: importResult };
}

View File

@@ -0,0 +1,190 @@
import { describe, it, expect } from "vitest";
import type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShConfig,
CompaniesShMetadata,
CompaniesShEnvVar,
CompaniesShImportResult,
CompaniesShRole,
} from "./companies-sh-types.js";
describe("companies-sh-types", () => {
describe("CompaniesShAgent", () => {
it("accepts a valid minimal agent with required fields", () => {
const agent: CompaniesShAgent = {
name: "Code Reviewer",
role: "reviewer",
};
expect(agent.name).toBe("Code Reviewer");
expect(agent.role).toBe("reviewer");
});
it("accepts a fully populated agent", () => {
const agent: CompaniesShAgent = {
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review", "security-audit"],
config: {
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium",
maxTurns: 10,
},
metadata: {
title: "Senior Code Reviewer",
icon: "👁",
description: "Reviews code for quality and security",
},
};
expect(agent.name).toBe("Code Reviewer");
expect(agent.capabilities).toHaveLength(2);
expect(agent.config?.model).toBe("claude-sonnet-4");
expect(agent.metadata?.title).toBe("Senior Code Reviewer");
});
it("accepts an agent with optional fields omitted", () => {
const agent: CompaniesShAgent = {
name: "Simple Agent",
role: "executor",
};
expect(agent.capabilities).toBeUndefined();
expect(agent.config).toBeUndefined();
expect(agent.metadata).toBeUndefined();
});
});
describe("CompaniesShConfig", () => {
it("accepts a config with all optional fields", () => {
const config: CompaniesShConfig = {
model: "provider/model-id",
maxTokens: 8192,
thinkingLevel: "high",
maxTurns: 20,
};
expect(config.model).toBe("provider/model-id");
expect(config.maxTokens).toBe(8192);
});
it("accepts an empty config", () => {
const config: CompaniesShConfig = {};
expect(config.model).toBeUndefined();
});
});
describe("CompaniesShMetadata", () => {
it("accepts metadata with all fields", () => {
const meta: CompaniesShMetadata = {
title: "Job Title",
icon: "🤖",
description: "An AI agent",
};
expect(meta.title).toBe("Job Title");
expect(meta.icon).toBe("🤖");
});
});
describe("CompaniesShManifest", () => {
it("accepts a valid manifest structure", () => {
const manifest: CompaniesShManifest = {
companyName: "my-company",
agents: [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
],
envVars: [
{ name: "KB_AGENT_MODEL", defaultValue: "claude-sonnet-4" },
],
};
expect(manifest.companyName).toBe("my-company");
expect(manifest.agents).toHaveLength(2);
expect(manifest.envVars).toHaveLength(1);
});
it("accepts a manifest with empty agents array", () => {
const manifest: CompaniesShManifest = {
companyName: "empty-company",
agents: [],
envVars: [],
};
expect(manifest.agents).toHaveLength(0);
});
});
describe("CompaniesShEnvVar", () => {
it("accepts an env var with name and default", () => {
const envVar: CompaniesShEnvVar = {
name: "KB_MODEL",
defaultValue: "claude-sonnet-4",
};
expect(envVar.name).toBe("KB_MODEL");
expect(envVar.defaultValue).toBe("claude-sonnet-4");
});
});
describe("CompaniesShImportResult", () => {
it("accepts a valid import result", () => {
const result: CompaniesShImportResult = {
created: ["agent-1", "agent-2"],
skipped: ["agent-3"],
errors: [{ name: "bad-agent", error: "missing role" }],
};
expect(result.created).toHaveLength(2);
expect(result.skipped).toHaveLength(1);
expect(result.errors).toHaveLength(1);
});
it("accepts an empty result", () => {
const result: CompaniesShImportResult = {
created: [],
skipped: [],
errors: [],
};
expect(result.created).toHaveLength(0);
});
});
describe("CompaniesShRole", () => {
it("accepts all defined role types", () => {
const roles: CompaniesShRole[] = [
"triage",
"executor",
"reviewer",
"merger",
"scheduler",
"engineer",
"custom",
];
expect(roles).toHaveLength(7);
});
});
describe("runtime validation", () => {
it("validates that a minimal object satisfies CompaniesShAgent shape", () => {
// Simulate runtime validation of parsed JSON
const parsed = JSON.parse('{"name":"Test","role":"executor"}');
expect(typeof parsed.name).toBe("string");
expect(typeof parsed.role).toBe("string");
expect(parsed.name).toBe("Test");
expect(parsed.role).toBe("executor");
});
it("detects missing required fields in parsed data", () => {
const parsed = JSON.parse('{"name":"Test"}');
expect(parsed.role).toBeUndefined();
// This would fail validation: missing role
expect(() => {
if (!parsed.role) throw new Error("Missing required field: role");
}).toThrow("Missing required field: role");
});
it("detects malformed data types", () => {
const parsed = JSON.parse('{"name":123,"role":"executor"}');
expect(typeof parsed.name).toBe("number");
// This would fail validation: name should be string
expect(() => {
if (typeof parsed.name !== "string") throw new Error("name must be a string");
}).toThrow("name must be a string");
});
});
});

View File

@@ -0,0 +1,101 @@
/**
* TypeScript type definitions for the companies.sh agent manifest format.
*
* The companies.sh standard defines a shell-script manifest that contains
* base64-encoded agent definitions, enabling portability of agent configurations
* across different agent systems.
*
* @module companies-sh-types
*/
// ── Agent Config ─────────────────────────────────────────────────────────
/** Configuration options for a companies.sh agent */
export interface CompaniesShConfig {
/** AI model identifier (e.g., "provider/model-id") */
model?: string;
/** Maximum tokens for the agent's responses */
maxTokens?: number;
/** Thinking effort level */
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high";
/** Maximum number of conversation turns */
maxTurns?: number;
}
// ── Agent Metadata ───────────────────────────────────────────────────────
/** Display metadata for a companies.sh agent */
export interface CompaniesShMetadata {
/** Human-readable job title */
title?: string;
/** Emoji icon identifier */
icon?: string;
/** Agent description */
description?: string;
}
// ── Agent Definition ─────────────────────────────────────────────────────
/** Role types in the companies.sh manifest format */
export type CompaniesShRole =
| "triage"
| "executor"
| "reviewer"
| "merger"
| "scheduler"
| "engineer"
| "custom";
/**
* A single agent definition in the companies.sh manifest.
* Each agent has a required name and role, plus optional configuration.
*/
export interface CompaniesShAgent {
/** Display name (required) */
name: string;
/** Agent role (required) — maps to kb AgentCapability */
role: CompaniesShRole | string;
/** List of capability identifiers */
capabilities?: string[];
/** Agent runtime configuration */
config?: CompaniesShConfig;
/** Display metadata */
metadata?: CompaniesShMetadata;
}
// ── Environment Variable ─────────────────────────────────────────────────
/** An environment variable with its default value extracted from the manifest */
export interface CompaniesShEnvVar {
/** Variable name */
name: string;
/** Default value (from ${VAR:-default} syntax) */
defaultValue: string;
}
// ── Manifest ─────────────────────────────────────────────────────────────
/**
* Parsed companies.sh manifest representing a full shell-script agent company.
* Contains the company name, decoded agent definitions, and environment variables.
*/
export interface CompaniesShManifest {
/** Company name extracted from COMPANY_NAME variable */
companyName: string;
/** Decoded and parsed agent definitions */
agents: CompaniesShAgent[];
/** Environment variables with defaults extracted from export statements */
envVars: CompaniesShEnvVar[];
}
// ── Import Result ────────────────────────────────────────────────────────
/** Result of importing agents from a companies.sh manifest */
export interface CompaniesShImportResult {
/** Agents successfully created */
created: string[];
/** Agent names that were skipped (already exist or invalid) */
skipped: string[];
/** Errors encountered during import */
errors: Array<{ name: string; error: string }>;
}

View File

@@ -192,3 +192,25 @@ export {
buildExecutionMemoryInstructions,
readProjectMemory,
} from "./project-memory.js";
// ── companies.sh Types ───────────────────────────────────────────────────
export type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShConfig,
CompaniesShMetadata,
CompaniesShEnvVar,
CompaniesShImportResult,
CompaniesShRole,
} from "./companies-sh-types.js";
// ── companies.sh Parser ──────────────────────────────────────────────
export {
parseCompaniesShManifest,
companiesShAgentToAgentCreateInput,
convertCompaniesShAgents,
mapRoleToCapability,
CompaniesShParseError,
} from "./companies-sh-parser.js";