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
6 changes: 6 additions & 0 deletions packages/core/src/skills/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export function getAgentTargets(): AgentTarget[] {
detectDirs: [join(xdgConfig, "devin")],
},
simple("droid", "Droid", ".factory"),
// DeepSeek DSH reads global skills from <dshHome>/skills; DSH_HOME relocates the home
fromBase("dsh", "DeepSeek DSH", envBase(process.env.DSH_HOME, ".dsh")),
simple("forgecode", "ForgeCode", ".forge"),
simple("gemini-cli", "Gemini CLI", ".gemini"),
simple("github-copilot", "GitHub Copilot", ".copilot"),
Expand Down Expand Up @@ -224,8 +226,12 @@ export function getAgentTargets(): AgentTarget[] {
simple("terramind", "Terramind", ".terramind"),
simple("tinycloud", "Tinycloud", ".tinycloud"),
simple("trae", "Trae", ".trae"),
// Trae CLI reads global skills from ~/.traecli/skills (project-level .trae/skills stays project-only)
simple("trae-cli", "Trae CLI", ".traecli"),
simple("trae-cn", "Trae CN", ".trae-cn"),
simple("windsurf", "Windsurf", ".codeium/windsurf"),
// Tencent WorkBuddy reads global skills from ~/.workbuddy/skills
simple("workbuddy", "Tencent WorkBuddy", ".workbuddy"),
{
id: "zcode",
displayName: "ZCode",
Expand Down
38 changes: 35 additions & 3 deletions packages/core/tests/skills-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async function inFakeHome(fn: (home: string) => Promise<void>): Promise<void> {
HERMES_HOME: process.env.HERMES_HOME,
AUTOHAND_HOME: process.env.AUTOHAND_HOME,
GROK_HOME: process.env.GROK_HOME,
DSH_HOME: process.env.DSH_HOME,
APPDATA: process.env.APPDATA,
FLATPAK_XDG_CONFIG_HOME: process.env.FLATPAK_XDG_CONFIG_HOME,
};
Expand All @@ -50,6 +51,7 @@ async function inFakeHome(fn: (home: string) => Promise<void>): Promise<void> {
"HERMES_HOME",
"AUTOHAND_HOME",
"GROK_HOME",
"DSH_HOME",
"APPDATA",
"FLATPAK_XDG_CONFIG_HOME",
]) {
Expand Down Expand Up @@ -79,7 +81,7 @@ test("agents: registry mirrors upstream agent list minus non-symlinkable agents"
const ids = getAgentTargets().map((agent) => agent.id);
expect(ids).toContain("universal");
expect(ids).toContain("universal-xdg");
expect(getAgentTargets()).toHaveLength(65);
expect(getAgentTargets()).toHaveLength(68);
// eve (no global dir, upstream forces direct writes) and promptscript (project-only)
// cannot participate in global symlink fan-out
expect(ids).not.toContain("eve");
Expand All @@ -100,12 +102,13 @@ test("agents: expanded registry detects per-agent config dirs", async () => {
await inFakeHome(async (home) => {
mkdirSync(join(home, ".roo"), { recursive: true });
mkdirSync(join(home, ".trae"), { recursive: true });
mkdirSync(join(home, ".traecli"), { recursive: true });
mkdirSync(join(home, ".gemini"), { recursive: true });
mkdirSync(join(home, ".codeium", "windsurf"), { recursive: true });
mkdirSync(join(home, ".snowflake", "cortex"), { recursive: true });

const detected = detectInstalledAgents().map((agent) => agent.id);
expect(detected).toEqual(["cortex", "gemini-cli", "roo", "trae", "windsurf"]);
expect(detected).toEqual(["cortex", "gemini-cli", "roo", "trae", "trae-cli", "windsurf"]);

// skills dirs follow each agent's own convention
const targets = getAgentTargets();
Expand All @@ -118,6 +121,33 @@ test("agents: expanded registry detects per-agent config dirs", async () => {
});
});

test("agents: WorkBuddy/Trae CLI/DSH use their official global skills dirs", async () => {
await inFakeHome(async (home) => {
mkdirSync(join(home, ".workbuddy"), { recursive: true });
mkdirSync(join(home, ".traecli"), { recursive: true });
mkdirSync(join(home, ".dsh"), { recursive: true });

const detected = detectInstalledAgents();
expect(detected.map((agent) => agent.id)).toEqual(["dsh", "trae-cli", "workbuddy"]);
expect(detected.find((agent) => agent.id === "workbuddy")?.skillsDir).toBe(
join(home, ".workbuddy", "skills"),
);
expect(detected.find((agent) => agent.id === "trae-cli")?.skillsDir).toBe(
join(home, ".traecli", "skills"),
);
expect(detected.find((agent) => agent.id === "dsh")?.skillsDir).toBe(
join(home, ".dsh", "skills"),
);

seedCanonicalSkill("demo");
const results = linkSkillToAgents("demo");
expect(results.map((result) => result.agent)).toEqual(["dsh", "trae-cli", "workbuddy"]);
expect(lstatSync(join(home, ".workbuddy", "skills", "demo")).isSymbolicLink()).toBe(true);
expect(lstatSync(join(home, ".traecli", "skills", "demo")).isSymbolicLink()).toBe(true);
expect(lstatSync(join(home, ".dsh", "skills", "demo")).isSymbolicLink()).toBe(true);
});
});

test("agents: shared-dir agents (Warp/Zed/Kimi/…) light up the universal target", async () => {
await inFakeHome(async (home) => {
mkdirSync(join(home, ".warp"), { recursive: true });
Expand Down Expand Up @@ -165,18 +195,20 @@ test("agents: OpenClaw historical alias dirs are detected and link into the exis
});
});

test("agents: VIBE_HOME/HERMES_HOME/AUTOHAND_HOME/GROK_HOME relocate their agents", async () => {
test("agents: VIBE_HOME/HERMES_HOME/AUTOHAND_HOME/GROK_HOME/DSH_HOME relocate their agents", async () => {
await inFakeHome(async (home) => {
const customDirs = {
"mistral-vibe": join(home, "custom-vibe"),
hermes: join(home, "custom-hermes"),
"autohand-code": join(home, "custom-autohand"),
grok: join(home, "custom-grok"),
dsh: join(home, "custom-dsh"),
};
process.env.VIBE_HOME = customDirs["mistral-vibe"];
process.env.HERMES_HOME = customDirs.hermes;
process.env.AUTOHAND_HOME = customDirs["autohand-code"];
process.env.GROK_HOME = customDirs.grok;
process.env.DSH_HOME = customDirs.dsh;
for (const dir of Object.values(customDirs)) {
mkdirSync(dir, { recursive: true });
}
Expand Down
Loading