fix: add /skill fallback for native limits

Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-20 13:19:55 +00:00
parent 63797e841d
commit 6e17c463ae
9 changed files with 101 additions and 2 deletions

View File

@@ -131,6 +131,26 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "List all slash commands.",
textAlias: "/commands",
}),
defineChatCommand({
key: "skill",
nativeName: "skill",
description: "Run a skill by name.",
textAlias: "/skill",
args: [
{
name: "name",
description: "Skill name",
type: "string",
required: true,
},
{
name: "input",
description: "Skill input",
type: "string",
captureRemaining: true,
},
],
}),
defineChatCommand({
key: "status",
nativeName: "status",

View File

@@ -31,6 +31,7 @@ describe("commands registry", () => {
const specs = listNativeCommandSpecs();
expect(specs.find((spec) => spec.name === "help")).toBeTruthy();
expect(specs.find((spec) => spec.name === "stop")).toBeTruthy();
expect(specs.find((spec) => spec.name === "skill")).toBeTruthy();
expect(specs.find((spec) => spec.name === "whoami")).toBeTruthy();
expect(specs.find((spec) => spec.name === "compact")).toBeFalsy();
});
@@ -81,6 +82,7 @@ describe("commands registry", () => {
it("detects known text commands", () => {
const detection = getCommandDetection();
expect(detection.exact.has("/commands")).toBe(true);
expect(detection.exact.has("/skill")).toBe(true);
expect(detection.exact.has("/compact")).toBe(true);
expect(detection.exact.has("/whoami")).toBe(true);
expect(detection.exact.has("/id")).toBe(true);

View File

@@ -30,6 +30,24 @@ describe("resolveSkillCommandInvocation", () => {
expect(invocation?.args).toBe("do the thing");
});
it("supports /skill with name argument", () => {
const invocation = resolveSkillCommandInvocation({
commandBodyNormalized: "/skill demo_skill do the thing",
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
});
expect(invocation?.command.name).toBe("demo_skill");
expect(invocation?.args).toBe("do the thing");
});
it("normalizes /skill lookup names", () => {
const invocation = resolveSkillCommandInvocation({
commandBodyNormalized: "/skill demo-skill",
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
});
expect(invocation?.command.name).toBe("demo_skill");
expect(invocation?.args).toBeUndefined();
});
it("returns null for unknown commands", () => {
const invocation = resolveSkillCommandInvocation({
commandBodyNormalized: "/unknown arg",

View File

@@ -55,6 +55,31 @@ export function listSkillCommandsForAgents(params: {
return entries;
}
function normalizeSkillCommandLookup(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[\s_]+/g, "-");
}
function findSkillCommand(
skillCommands: SkillCommandSpec[],
rawName: string,
): SkillCommandSpec | undefined {
const trimmed = rawName.trim();
if (!trimmed) return undefined;
const lowered = trimmed.toLowerCase();
const normalized = normalizeSkillCommandLookup(trimmed);
return skillCommands.find((entry) => {
if (entry.name.toLowerCase() === lowered) return true;
if (entry.skillName.toLowerCase() === lowered) return true;
return (
normalizeSkillCommandLookup(entry.name) === normalized ||
normalizeSkillCommandLookup(entry.skillName) === normalized
);
});
}
export function resolveSkillCommandInvocation(params: {
commandBodyNormalized: string;
skillCommands: SkillCommandSpec[];
@@ -65,6 +90,16 @@ export function resolveSkillCommandInvocation(params: {
if (!match) return null;
const commandName = match[1]?.trim().toLowerCase();
if (!commandName) return null;
if (commandName === "skill") {
const remainder = match[2]?.trim();
if (!remainder) return null;
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
if (!skillMatch) return null;
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
if (!skillCommand) return null;
const args = skillMatch[2]?.trim();
return { command: skillCommand, args: args || undefined };
}
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
if (!command) return null;
const args = match[2]?.trim();

View File

@@ -366,6 +366,7 @@ describe("buildCommandsMessage", () => {
commands: { config: false, debug: false },
} as ClawdbotConfig);
expect(text).toContain("/commands - List all slash commands.");
expect(text).toContain("/skill - Run a skill by name.");
expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level.");
expect(text).toContain("/compact (text-only) - Compact the session context.");
expect(text).not.toContain("/config");
@@ -394,6 +395,7 @@ describe("buildHelpMessage", () => {
const text = buildHelpMessage({
commands: { config: false, debug: false },
} as ClawdbotConfig);
expect(text).toContain("Skills: /skill <name> [input]");
expect(text).not.toContain("/config");
expect(text).not.toContain("/debug");
});

View File

@@ -391,6 +391,7 @@ export function buildHelpMessage(cfg?: ClawdbotConfig): string {
" Help",
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
`Options: ${options.join(" | ")}`,
"Skills: /skill <name> [input]",
"More: /commands for all slash commands",
].join("\n");
}

View File

@@ -321,9 +321,27 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
throw new Error("Failed to resolve Discord application id");
}
const skillCommands =
const maxDiscordCommands = 100;
let skillCommands =
nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : [];
const commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) : [];
let commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) : [];
const initialCommandCount = commandSpecs.length;
if (nativeEnabled && nativeSkillsEnabled && commandSpecs.length > maxDiscordCommands) {
skillCommands = [];
commandSpecs = listNativeCommandSpecsForConfig(cfg, { skillCommands: [] });
runtime.log?.(
warn(
`discord: ${initialCommandCount} commands exceeds limit; removing per-skill commands and keeping /skill.`,
),
);
}
if (nativeEnabled && commandSpecs.length > maxDiscordCommands) {
runtime.log?.(
warn(
`discord: ${commandSpecs.length} commands exceeds limit; some commands may fail to deploy.`,
),
);
}
const commands = commandSpecs.map((spec) =>
createDiscordNativeCommand({
command: spec,