fix: truncate skill command descriptions to 100 chars for Discord (#1018)

* fix: truncate skill command descriptions to 100 chars for Discord

Discord slash commands have a 100 character limit for descriptions.
Skill descriptions were not being truncated, causing command registration
to fail with an empty error from the Discord API.

* style: format

* style: format
This commit is contained in:
Wilkins
2026-01-16 16:01:59 +00:00
committed by GitHub
parent 0d6af15d1c
commit bb14b19922
11 changed files with 83 additions and 66 deletions

View File

@@ -50,7 +50,7 @@ test("background exec still times out after tool signal abort", async () => {
const result = await tool.execute(
"toolcall",
{
command: "node -e \"setTimeout(() => {}, 5000)\"",
command: 'node -e "setTimeout(() => {}, 5000)"',
background: true,
timeout: 0.2,
},
@@ -85,7 +85,7 @@ test("yielded background exec is not killed when tool signal aborts", async () =
const result = await tool.execute(
"toolcall",
{ command: "node -e \"setTimeout(() => {}, 5000)\"", yieldMs: 5 },
{ command: 'node -e "setTimeout(() => {}, 5000)"', yieldMs: 5 },
abortController.signal,
);
@@ -112,7 +112,7 @@ test("yielded background exec still times out", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 10 });
const result = await tool.execute("toolcall", {
command: "node -e \"setTimeout(() => {}, 5000)\"",
command: 'node -e "setTimeout(() => {}, 5000)"',
yieldMs: 5,
timeout: 0.2,
});

View File

@@ -61,4 +61,32 @@ describe("buildWorkspaceSkillCommandSpecs", () => {
expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]);
expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined();
});
it("truncates descriptions longer than 100 characters for Discord compatibility", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
const longDescription =
"This is a very long description that exceeds Discord's 100 character limit for slash command descriptions and should be truncated";
await writeSkill({
dir: path.join(workspaceDir, "skills", "long-desc"),
name: "long-desc",
description: longDescription,
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "short-desc"),
name: "short-desc",
description: "Short description",
});
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
});
const longCmd = commands.find((entry) => entry.skillName === "long-desc");
const shortCmd = commands.find((entry) => entry.skillName === "short-desc");
expect(longCmd?.description.length).toBeLessThanOrEqual(100);
expect(longCmd?.description.endsWith("…")).toBe(true);
expect(shortCmd?.description).toBe("Short description");
});
});

View File

@@ -87,12 +87,7 @@ function parseFrontmatterBool(value: string | undefined, fallback: boolean): boo
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
return true;
}
if (
normalized === "false" ||
normalized === "0" ||
normalized === "no" ||
normalized === "off"
) {
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
return false;
}
return fallback;

View File

@@ -50,6 +50,8 @@ function filterSkillEntries(
const SKILL_COMMAND_MAX_LENGTH = 32;
const SKILL_COMMAND_FALLBACK = "skill";
// Discord command descriptions must be ≤100 characters
const SKILL_COMMAND_DESCRIPTION_MAX_LENGTH = 100;
function sanitizeSkillCommandName(raw: string): string {
const normalized = raw
@@ -311,9 +313,7 @@ export function buildWorkspaceSkillCommandSpecs(
opts?.skillFilter,
opts?.eligibility,
);
const userInvocable = eligible.filter(
(entry) => entry.invocation?.userInvocable !== false,
);
const userInvocable = eligible.filter((entry) => entry.invocation?.userInvocable !== false);
const used = new Set<string>();
for (const reserved of opts?.reservedNames ?? []) {
used.add(reserved.toLowerCase());
@@ -324,10 +324,15 @@ export function buildWorkspaceSkillCommandSpecs(
const base = sanitizeSkillCommandName(entry.skill.name);
const unique = resolveUniqueSkillCommandName(base, used);
used.add(unique.toLowerCase());
const rawDescription = entry.skill.description?.trim() || entry.skill.name;
const description =
rawDescription.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH
? rawDescription.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…"
: rawDescription;
specs.push({
name: unique,
skillName: entry.skill.name,
description: entry.skill.description?.trim() || entry.skill.name,
description,
});
}
return specs;