diff --git a/src/agents/bash-tools.exec.background-abort.test.ts b/src/agents/bash-tools.exec.background-abort.test.ts index 403bc064d..a13dc2237 100644 --- a/src/agents/bash-tools.exec.background-abort.test.ts +++ b/src/agents/bash-tools.exec.background-abort.test.ts @@ -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, }); diff --git a/src/agents/skills.buildworkspaceskillcommands.test.ts b/src/agents/skills.buildworkspaceskillcommands.test.ts index ed5d53fbb..369166979 100644 --- a/src/agents/skills.buildworkspaceskillcommands.test.ts +++ b/src/agents/skills.buildworkspaceskillcommands.test.ts @@ -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"); + }); }); diff --git a/src/agents/skills/frontmatter.ts b/src/agents/skills/frontmatter.ts index cb01c1ab4..b50620ea4 100644 --- a/src/agents/skills/frontmatter.ts +++ b/src/agents/skills/frontmatter.ts @@ -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; diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index 66d999e3a..46911dfb6 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -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(); 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; diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index c4d07393f..0ce3be6b3 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -62,9 +62,7 @@ function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -function buildSkillCommandDefinitions( - skillCommands?: SkillCommandSpec[], -): ChatCommandDefinition[] { +function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] { if (!skillCommands || skillCommands.length === 0) return []; return skillCommands.map((spec) => ({ key: `skill:${spec.skillName}`, @@ -77,7 +75,9 @@ function buildSkillCommandDefinitions( })); } -export function listChatCommands(params?: { skillCommands?: SkillCommandSpec[] }): ChatCommandDefinition[] { +export function listChatCommands(params?: { + skillCommands?: SkillCommandSpec[]; +}): ChatCommandDefinition[] { if (!params?.skillCommands?.length) return [...CHAT_COMMANDS]; return [...CHAT_COMMANDS, ...buildSkillCommandDefinitions(params.skillCommands)]; } @@ -98,7 +98,9 @@ export function listChatCommandsForConfig( return [...base, ...buildSkillCommandDefinitions(params.skillCommands)]; } -export function listNativeCommandSpecs(params?: { skillCommands?: SkillCommandSpec[] }): NativeCommandSpec[] { +export function listNativeCommandSpecs(params?: { + skillCommands?: SkillCommandSpec[]; +}): NativeCommandSpec[] { return listChatCommands({ skillCommands: params?.skillCommands }) .filter((command) => command.scope !== "text" && command.nativeName) .map((command) => ({ diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index 5f2438927..1a46aa926 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -187,34 +187,34 @@ export async function handleInlineActions(params: { commandBodyNormalized: inlineCommand.command, }; const inlineResult = await handleCommands({ - ctx, - cfg, - command: inlineCommandContext, - agentId, - directives, - elevated: { - enabled: elevatedEnabled, - allowed: elevatedAllowed, - failures: elevatedFailures, - }, - sessionEntry, - sessionStore, - sessionKey, - storePath, - sessionScope, - workspaceDir, - defaultGroupActivation: defaultActivation, - resolvedThinkLevel, - resolvedVerboseLevel: resolvedVerboseLevel ?? "off", - resolvedReasoningLevel, - resolvedElevatedLevel, - resolveDefaultThinkingLevel, - provider, - model, - contextTokens, - isGroup, - skillCommands, - }); + ctx, + cfg, + command: inlineCommandContext, + agentId, + directives, + elevated: { + enabled: elevatedEnabled, + allowed: elevatedAllowed, + failures: elevatedFailures, + }, + sessionEntry, + sessionStore, + sessionKey, + storePath, + sessionScope, + workspaceDir, + defaultGroupActivation: defaultActivation, + resolvedThinkLevel, + resolvedVerboseLevel: resolvedVerboseLevel ?? "off", + resolvedReasoningLevel, + resolvedElevatedLevel, + resolveDefaultThinkingLevel, + provider, + model, + contextTokens, + isGroup, + skillCommands, + }); if (inlineResult.reply) { if (!inlineCommand.cleaned) { typing.cleanup(); diff --git a/src/auto-reply/skill-commands.test.ts b/src/auto-reply/skill-commands.test.ts index b0c2fd7a4..dcf844c7d 100644 --- a/src/auto-reply/skill-commands.test.ts +++ b/src/auto-reply/skill-commands.test.ts @@ -5,9 +5,7 @@ describe("resolveSkillCommandInvocation", () => { it("matches skill commands and parses args", () => { const invocation = resolveSkillCommandInvocation({ commandBodyNormalized: "/demo_skill do the thing", - skillCommands: [ - { name: "demo_skill", skillName: "demo-skill", description: "Demo" }, - ], + skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }], }); expect(invocation?.command.skillName).toBe("demo-skill"); expect(invocation?.args).toBe("do the thing"); @@ -16,9 +14,7 @@ describe("resolveSkillCommandInvocation", () => { it("returns null for unknown commands", () => { const invocation = resolveSkillCommandInvocation({ commandBodyNormalized: "/unknown arg", - skillCommands: [ - { name: "demo_skill", skillName: "demo-skill", description: "Demo" }, - ], + skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }], }); expect(invocation).toBeNull(); }); diff --git a/src/auto-reply/skill-commands.ts b/src/auto-reply/skill-commands.ts index e1790a084..f9266ea8e 100644 --- a/src/auto-reply/skill-commands.ts +++ b/src/auto-reply/skill-commands.ts @@ -1,9 +1,6 @@ import type { ClawdbotConfig } from "../config/config.js"; import { getRemoteSkillEligibility } from "../infra/skills-remote.js"; -import { - buildWorkspaceSkillCommandSpecs, - type SkillCommandSpec, -} from "../agents/skills.js"; +import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agents/skills.js"; import { listChatCommands } from "./commands-registry.js"; function resolveReservedCommandNames(): Set { @@ -42,9 +39,7 @@ export function resolveSkillCommandInvocation(params: { if (!match) return null; const commandName = match[1]?.trim().toLowerCase(); if (!commandName) return null; - const command = params.skillCommands.find( - (entry) => entry.name.toLowerCase() === commandName, - ); + const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName); if (!command) return null; const args = match[2]?.trim(); return { command, args: args || undefined }; diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 435875c0d..0fff3099d 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -211,9 +211,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { ), ); defaultRuntime.log( - theme.muted( - "Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`", - ), + theme.muted("Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`"), ); } defaultRuntime.exit(0); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 4f7fd0ed8..e856afbd9 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -124,9 +124,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { cfg, }) : []; - const commandSpecs = nativeEnabled - ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) - : []; + const commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) : []; const commands = commandSpecs.map((spec) => createDiscordNativeCommand({ command: spec, diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 02643fc40..51570b1f8 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -185,7 +185,7 @@ export async function processMessage(params: { const responsePrefix = resolvedMessages.responsePrefix ?? (configuredResponsePrefix === undefined && isSelfChat - ? resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]" + ? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]") : undefined); // Create mutable context for response prefix template interpolation