fix: add /skill fallback for native limits
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user