Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions desktop/src/agent-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { AGENT_CATALOG, ALL_SKILL_IDS, getAgentSkills } from "./agent-catalog";

const EXPECTED_SKILL_IDS = [
"1password",
"blucli",
"canvas",
"coding-agent",
"desktop-organizer",
"excel-xlsx",
"healthcheck",
"mcporter",
"nano-pdf",
"obsidian",
"officecli",
"openai-whisper",
"openhue",
"oracle",
"powerpoint-pptx",
"security-practice",
"session-logs",
"sherpa-onnx-tts",
"skill-creator",
"songsee",
"sonoscli",
"video-frames",
"word-docx",
];

describe("agent catalog skills binding", () => {
it("exposes exactly the 23 expected skill IDs", () => {
expect(ALL_SKILL_IDS).toEqual(EXPECTED_SKILL_IDS);
expect(ALL_SKILL_IDS).toHaveLength(23);
});

it("keeps ALL_SKILL_IDS sorted with no duplicates", () => {
const sorted = [...ALL_SKILL_IDS].sort((a, b) => a.localeCompare(b, "en"));
expect([...ALL_SKILL_IDS]).toEqual(sorted);
expect(new Set(ALL_SKILL_IDS).size).toBe(ALL_SKILL_IDS.length);
});

it("binds every agent to the full skill list", () => {
for (const agent of AGENT_CATALOG) {
expect(agent.skills).toEqual(ALL_SKILL_IDS);
}
});

it("gives each agent its own skills array instance", () => {
// No agent shares a reference with another agent or with ALL_SKILL_IDS.
for (const agent of AGENT_CATALOG) {
expect(agent.skills).not.toBe(ALL_SKILL_IDS);
}
const seen = new Set<readonly string[]>();
for (const agent of AGENT_CATALOG) {
expect(seen.has(agent.skills)).toBe(false);
seen.add(agent.skills);
}

// Mutating one agent's array does not affect others or ALL_SKILL_IDS.
const [first, second] = AGENT_CATALOG;
(first.skills as string[]).push("__mutation-probe__");
try {
expect(second.skills).toEqual(ALL_SKILL_IDS);
expect(ALL_SKILL_IDS).toEqual(EXPECTED_SKILL_IDS);
} finally {
(first.skills as string[]).pop();
}
});

it("resolves skills for a known agent and returns [] for an unknown one", () => {
expect(getAgentSkills("coder")).toEqual(ALL_SKILL_IDS);
expect(getAgentSkills("does-not-exist")).toEqual([]);
});
});
49 changes: 49 additions & 0 deletions desktop/src/agent-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
// The complete universe of skill IDs shipped by MicroClaw: the union of the
// certified/bundled skills (SKILL_CATALOG) and the managed skills
// (MANAGED_SKILL_CATALOG), alphabetically sorted. This MUST be kept in sync with
// `deployer/skill_catalog.py` (SKILL_CATALOG + MANAGED_SKILL_CATALOG) — adding or
// removing a skill there requires the same change here.
export const ALL_SKILL_IDS: readonly string[] = [
"1password",
"blucli",
"canvas",
"coding-agent",
"desktop-organizer",
"excel-xlsx",
"healthcheck",
"mcporter",
"nano-pdf",
"obsidian",
"officecli",
"openai-whisper",
"openhue",
"oracle",
"powerpoint-pptx",
"security-practice",
"session-logs",
"sherpa-onnx-tts",
"skill-creator",
"songsee",
"sonoscli",
"video-frames",
"word-docx",
];

export interface AgentCatalogEntry {
id: string;
name: string;
Expand All @@ -8,6 +39,7 @@ export interface AgentCatalogEntry {
tagKeys: string[];
taskKeys: Array<{ titleKey: string; descKey: string }>;
installedByDefault: boolean;
skills: readonly string[];
workspaceDirName?: string;
personaProfile?: "master-archive";
}
Expand All @@ -27,6 +59,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.main.task.3.title", descKey: "agent.main.task.3.desc" },
],
installedByDefault: true,
skills: [...ALL_SKILL_IDS],
},
{
id: "master-archive",
Expand Down Expand Up @@ -55,6 +88,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
},
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
workspaceDirName: "workspace-master-archive",
personaProfile: "master-archive",
},
Expand All @@ -72,6 +106,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.coder.task.3.title", descKey: "agent.coder.task.3.desc" },
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
{
id: "painter",
Expand All @@ -87,6 +122,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.painter.task.3.title", descKey: "agent.painter.task.3.desc" },
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
{
id: "master",
Expand All @@ -102,6 +138,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.master.task.3.title", descKey: "agent.master.task.3.desc" },
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
{
id: "growth-hacker",
Expand Down Expand Up @@ -130,6 +167,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
},
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
{
id: "leopard",
Expand All @@ -145,6 +183,7 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.leopard.task.3.title", descKey: "agent.leopard.task.3.desc" },
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
{
id: "singer",
Expand All @@ -160,9 +199,19 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
{ titleKey: "agent.singer.task.3.title", descKey: "agent.singer.task.3.desc" },
],
installedByDefault: false,
skills: [...ALL_SKILL_IDS],
},
];

export const DEFAULT_AGENT_IDS = AGENT_CATALOG.filter(
(agent) => agent.installedByDefault,
).map((agent) => agent.id);

/**
* Returns the skill IDs bound to the given agent, or an empty array if the agent
* id is not present in AGENT_CATALOG.
*/
export function getAgentSkills(agentId: string): readonly string[] {
const agent = AGENT_CATALOG.find((entry) => entry.id === agentId);
return agent ? agent.skills : [];
}
Loading