feat: add user-invocable skill commands
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
- Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007.
|
- Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007.
|
||||||
- Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
|
- Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
|
||||||
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
||||||
|
- Skills: add user-invocable skill commands with sanitized names and an opt-out for model invocation.
|
||||||
- Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
|
- Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
|
||||||
- TUI: show provider/model labels for the active session and default model.
|
- TUI: show provider/model labels for the active session and default model.
|
||||||
- Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.
|
- Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ Notes:
|
|||||||
- Use `{baseDir}` in instructions to reference the skill folder path.
|
- Use `{baseDir}` in instructions to reference the skill folder path.
|
||||||
- Optional frontmatter keys:
|
- Optional frontmatter keys:
|
||||||
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.clawdbot.homepage`).
|
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.clawdbot.homepage`).
|
||||||
|
- `user-invocable` — `true|false` (default: `true`). When `true`, the skill is exposed as a user slash command.
|
||||||
|
- `disable-model-invocation` — `true|false` (default: `false`). When `true`, the skill is excluded from the model prompt (still available via user invocation).
|
||||||
|
|
||||||
## Gating (load-time filters)
|
## Gating (load-time filters)
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ Notes:
|
|||||||
- Example: `hey /status` triggers a status reply, and the remaining text continues through the normal flow.
|
- Example: `hey /status` triggers a status reply, and the remaining text continues through the normal flow.
|
||||||
- Currently: `/help`, `/commands`, `/status` (`/usage`), `/whoami` (`/id`).
|
- Currently: `/help`, `/commands`, `/status` (`/usage`), `/whoami` (`/id`).
|
||||||
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
|
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
|
||||||
|
- **Skill commands:** `user-invocable` skills are exposed as slash commands. Names are sanitized to `a-z0-9_` (max 32 chars); collisions get numeric suffixes (e.g. `_2`).
|
||||||
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg.
|
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg.
|
||||||
|
|
||||||
## Usage vs cost (what shows where)
|
## Usage vs cost (what shows where)
|
||||||
|
|||||||
64
src/agents/skills.buildworkspaceskillcommands.test.ts
Normal file
64
src/agents/skills.buildworkspaceskillcommands.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildWorkspaceSkillCommandSpecs } from "./skills.js";
|
||||||
|
|
||||||
|
async function writeSkill(params: {
|
||||||
|
dir: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
frontmatterExtra?: string;
|
||||||
|
}) {
|
||||||
|
const { dir, name, description, frontmatterExtra } = params;
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(dir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: ${name}
|
||||||
|
description: ${description}
|
||||||
|
${frontmatterExtra ?? ""}
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${name}
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("buildWorkspaceSkillCommandSpecs", () => {
|
||||||
|
it("sanitizes and de-duplicates command names", async () => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "hello-world"),
|
||||||
|
name: "hello-world",
|
||||||
|
description: "Hello world skill",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "hello_world"),
|
||||||
|
name: "hello_world",
|
||||||
|
description: "Hello underscore skill",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "help"),
|
||||||
|
name: "help",
|
||||||
|
description: "Help skill",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "hidden"),
|
||||||
|
name: "hidden-skill",
|
||||||
|
description: "Hidden skill",
|
||||||
|
frontmatterExtra: "user-invocable: false",
|
||||||
|
});
|
||||||
|
|
||||||
|
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
|
||||||
|
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||||
|
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
|
||||||
|
reservedNames: new Set(["help"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const names = commands.map((entry) => entry.name).sort();
|
||||||
|
expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]);
|
||||||
|
expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,15 +9,17 @@ async function _writeSkill(params: {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
metadata?: string;
|
metadata?: string;
|
||||||
|
frontmatterExtra?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
}) {
|
}) {
|
||||||
const { dir, name, description, metadata, body } = params;
|
const { dir, name, description, metadata, frontmatterExtra, body } = params;
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(dir, "SKILL.md"),
|
path.join(dir, "SKILL.md"),
|
||||||
`---
|
`---
|
||||||
name: ${name}
|
name: ${name}
|
||||||
description: ${description}${metadata ? `\nmetadata: ${metadata}` : ""}
|
description: ${description}${metadata ? `\nmetadata: ${metadata}` : ""}
|
||||||
|
${frontmatterExtra ?? ""}
|
||||||
---
|
---
|
||||||
|
|
||||||
${body ?? `# ${name}\n`}
|
${body ?? `# ${name}\n`}
|
||||||
@@ -38,4 +40,31 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
|||||||
expect(snapshot.prompt).toBe("");
|
expect(snapshot.prompt).toBe("");
|
||||||
expect(snapshot.skills).toEqual([]);
|
expect(snapshot.skills).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("omits disable-model-invocation skills from the prompt", async () => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
|
await _writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "visible-skill"),
|
||||||
|
name: "visible-skill",
|
||||||
|
description: "Visible skill",
|
||||||
|
});
|
||||||
|
await _writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "hidden-skill"),
|
||||||
|
name: "hidden-skill",
|
||||||
|
description: "Hidden skill",
|
||||||
|
frontmatterExtra: "disable-model-invocation: true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
|
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||||
|
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.prompt).toContain("visible-skill");
|
||||||
|
expect(snapshot.prompt).not.toContain("hidden-skill");
|
||||||
|
expect(snapshot.skills.map((skill) => skill.name).sort()).toEqual([
|
||||||
|
"hidden-skill",
|
||||||
|
"visible-skill",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export {
|
|||||||
export type {
|
export type {
|
||||||
ClawdbotSkillMetadata,
|
ClawdbotSkillMetadata,
|
||||||
SkillEligibilityContext,
|
SkillEligibilityContext,
|
||||||
|
SkillCommandSpec,
|
||||||
SkillEntry,
|
SkillEntry,
|
||||||
SkillInstallSpec,
|
SkillInstallSpec,
|
||||||
SkillSnapshot,
|
SkillSnapshot,
|
||||||
@@ -24,6 +25,7 @@ export type {
|
|||||||
export {
|
export {
|
||||||
buildWorkspaceSkillSnapshot,
|
buildWorkspaceSkillSnapshot,
|
||||||
buildWorkspaceSkillsPrompt,
|
buildWorkspaceSkillsPrompt,
|
||||||
|
buildWorkspaceSkillCommandSpecs,
|
||||||
filterWorkspaceSkillEntries,
|
filterWorkspaceSkillEntries,
|
||||||
loadWorkspaceSkillEntries,
|
loadWorkspaceSkillEntries,
|
||||||
resolveSkillsPromptForRun,
|
resolveSkillsPromptForRun,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ParsedSkillFrontmatter,
|
ParsedSkillFrontmatter,
|
||||||
SkillEntry,
|
SkillEntry,
|
||||||
SkillInstallSpec,
|
SkillInstallSpec,
|
||||||
|
SkillInvocationPolicy,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
function stripQuotes(value: string): string {
|
function stripQuotes(value: string): string {
|
||||||
@@ -79,6 +80,24 @@ function getFrontmatterValue(frontmatter: ParsedSkillFrontmatter, key: string):
|
|||||||
return typeof raw === "string" ? raw : undefined;
|
return typeof raw === "string" ? raw : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseFrontmatterBool(value: string | undefined, fallback: boolean): boolean {
|
||||||
|
if (!value) return fallback;
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (!normalized) return fallback;
|
||||||
|
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized === "false" ||
|
||||||
|
normalized === "0" ||
|
||||||
|
normalized === "no" ||
|
||||||
|
normalized === "off"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveClawdbotMetadata(
|
export function resolveClawdbotMetadata(
|
||||||
frontmatter: ParsedSkillFrontmatter,
|
frontmatter: ParsedSkillFrontmatter,
|
||||||
): ClawdbotSkillMetadata | undefined {
|
): ClawdbotSkillMetadata | undefined {
|
||||||
@@ -121,6 +140,18 @@ export function resolveClawdbotMetadata(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSkillInvocationPolicy(
|
||||||
|
frontmatter: ParsedSkillFrontmatter,
|
||||||
|
): SkillInvocationPolicy {
|
||||||
|
return {
|
||||||
|
userInvocable: parseFrontmatterBool(getFrontmatterValue(frontmatter, "user-invocable"), true),
|
||||||
|
disableModelInvocation: parseFrontmatterBool(
|
||||||
|
getFrontmatterValue(frontmatter, "disable-model-invocation"),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveSkillKey(skill: Skill, entry?: SkillEntry): string {
|
export function resolveSkillKey(skill: Skill, entry?: SkillEntry): string {
|
||||||
return entry?.clawdbot?.skillKey ?? skill.name;
|
return entry?.clawdbot?.skillKey ?? skill.name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ export type ClawdbotSkillMetadata = {
|
|||||||
install?: SkillInstallSpec[];
|
install?: SkillInstallSpec[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SkillInvocationPolicy = {
|
||||||
|
userInvocable: boolean;
|
||||||
|
disableModelInvocation: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SkillCommandSpec = {
|
||||||
|
name: string;
|
||||||
|
skillName: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SkillsInstallPreferences = {
|
export type SkillsInstallPreferences = {
|
||||||
preferBrew: boolean;
|
preferBrew: boolean;
|
||||||
nodeManager: "npm" | "pnpm" | "yarn" | "bun";
|
nodeManager: "npm" | "pnpm" | "yarn" | "bun";
|
||||||
@@ -37,6 +48,7 @@ export type SkillEntry = {
|
|||||||
skill: Skill;
|
skill: Skill;
|
||||||
frontmatter: ParsedSkillFrontmatter;
|
frontmatter: ParsedSkillFrontmatter;
|
||||||
clawdbot?: ClawdbotSkillMetadata;
|
clawdbot?: ClawdbotSkillMetadata;
|
||||||
|
invocation?: SkillInvocationPolicy;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SkillEligibilityContext = {
|
export type SkillEligibilityContext = {
|
||||||
|
|||||||
@@ -11,11 +11,16 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
|||||||
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
||||||
import { resolveBundledSkillsDir } from "./bundled-dir.js";
|
import { resolveBundledSkillsDir } from "./bundled-dir.js";
|
||||||
import { shouldIncludeSkill } from "./config.js";
|
import { shouldIncludeSkill } from "./config.js";
|
||||||
import { parseFrontmatter, resolveClawdbotMetadata } from "./frontmatter.js";
|
import {
|
||||||
|
parseFrontmatter,
|
||||||
|
resolveClawdbotMetadata,
|
||||||
|
resolveSkillInvocationPolicy,
|
||||||
|
} from "./frontmatter.js";
|
||||||
import { serializeByKey } from "./serialize.js";
|
import { serializeByKey } from "./serialize.js";
|
||||||
import type {
|
import type {
|
||||||
ParsedSkillFrontmatter,
|
ParsedSkillFrontmatter,
|
||||||
SkillEligibilityContext,
|
SkillEligibilityContext,
|
||||||
|
SkillCommandSpec,
|
||||||
SkillEntry,
|
SkillEntry,
|
||||||
SkillSnapshot,
|
SkillSnapshot,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
@@ -43,6 +48,34 @@ function filterSkillEntries(
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SKILL_COMMAND_MAX_LENGTH = 32;
|
||||||
|
const SKILL_COMMAND_FALLBACK = "skill";
|
||||||
|
|
||||||
|
function sanitizeSkillCommandName(raw: string): string {
|
||||||
|
const normalized = raw
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_]+/g, "_")
|
||||||
|
.replace(/_+/g, "_")
|
||||||
|
.replace(/^_+|_+$/g, "");
|
||||||
|
const trimmed = normalized.slice(0, SKILL_COMMAND_MAX_LENGTH);
|
||||||
|
return trimmed || SKILL_COMMAND_FALLBACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUniqueSkillCommandName(base: string, used: Set<string>): string {
|
||||||
|
const normalizedBase = base.toLowerCase();
|
||||||
|
if (!used.has(normalizedBase)) return base;
|
||||||
|
for (let index = 2; index < 1000; index += 1) {
|
||||||
|
const suffix = `_${index}`;
|
||||||
|
const maxBaseLength = Math.max(1, SKILL_COMMAND_MAX_LENGTH - suffix.length);
|
||||||
|
const trimmedBase = base.slice(0, maxBaseLength);
|
||||||
|
const candidate = `${trimmedBase}${suffix}`;
|
||||||
|
const candidateKey = candidate.toLowerCase();
|
||||||
|
if (!used.has(candidateKey)) return candidate;
|
||||||
|
}
|
||||||
|
const fallback = `${base.slice(0, Math.max(1, SKILL_COMMAND_MAX_LENGTH - 2))}_x`;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function loadSkillEntries(
|
function loadSkillEntries(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
@@ -114,6 +147,7 @@ function loadSkillEntries(
|
|||||||
skill,
|
skill,
|
||||||
frontmatter,
|
frontmatter,
|
||||||
clawdbot: resolveClawdbotMetadata(frontmatter),
|
clawdbot: resolveClawdbotMetadata(frontmatter),
|
||||||
|
invocation: resolveSkillInvocationPolicy(frontmatter),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return skillEntries;
|
return skillEntries;
|
||||||
@@ -139,7 +173,10 @@ export function buildWorkspaceSkillSnapshot(
|
|||||||
opts?.skillFilter,
|
opts?.skillFilter,
|
||||||
opts?.eligibility,
|
opts?.eligibility,
|
||||||
);
|
);
|
||||||
const resolvedSkills = eligible.map((entry) => entry.skill);
|
const promptEntries = eligible.filter(
|
||||||
|
(entry) => entry.invocation?.disableModelInvocation !== true,
|
||||||
|
);
|
||||||
|
const resolvedSkills = promptEntries.map((entry) => entry.skill);
|
||||||
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
||||||
const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
|
const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
|
||||||
return {
|
return {
|
||||||
@@ -172,8 +209,11 @@ export function buildWorkspaceSkillsPrompt(
|
|||||||
opts?.skillFilter,
|
opts?.skillFilter,
|
||||||
opts?.eligibility,
|
opts?.eligibility,
|
||||||
);
|
);
|
||||||
|
const promptEntries = eligible.filter(
|
||||||
|
(entry) => entry.invocation?.disableModelInvocation !== true,
|
||||||
|
);
|
||||||
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
||||||
return [remoteNote, formatSkillsForPrompt(eligible.map((entry) => entry.skill))]
|
return [remoteNote, formatSkillsForPrompt(promptEntries.map((entry) => entry.skill))]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
@@ -251,3 +291,44 @@ export function filterWorkspaceSkillEntries(
|
|||||||
): SkillEntry[] {
|
): SkillEntry[] {
|
||||||
return filterSkillEntries(entries, config);
|
return filterSkillEntries(entries, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildWorkspaceSkillCommandSpecs(
|
||||||
|
workspaceDir: string,
|
||||||
|
opts?: {
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
managedSkillsDir?: string;
|
||||||
|
bundledSkillsDir?: string;
|
||||||
|
entries?: SkillEntry[];
|
||||||
|
skillFilter?: string[];
|
||||||
|
eligibility?: SkillEligibilityContext;
|
||||||
|
reservedNames?: Set<string>;
|
||||||
|
},
|
||||||
|
): SkillCommandSpec[] {
|
||||||
|
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
||||||
|
const eligible = filterSkillEntries(
|
||||||
|
skillEntries,
|
||||||
|
opts?.config,
|
||||||
|
opts?.skillFilter,
|
||||||
|
opts?.eligibility,
|
||||||
|
);
|
||||||
|
const userInvocable = eligible.filter(
|
||||||
|
(entry) => entry.invocation?.userInvocable !== false,
|
||||||
|
);
|
||||||
|
const used = new Set<string>();
|
||||||
|
for (const reserved of opts?.reservedNames ?? []) {
|
||||||
|
used.add(reserved.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
const specs: SkillCommandSpec[] = [];
|
||||||
|
for (const entry of userInvocable) {
|
||||||
|
const base = sanitizeSkillCommandName(entry.skill.name);
|
||||||
|
const unique = resolveUniqueSkillCommandName(base, used);
|
||||||
|
used.add(unique.toLowerCase());
|
||||||
|
specs.push({
|
||||||
|
name: unique,
|
||||||
|
skillName: entry.skill.name,
|
||||||
|
description: entry.skill.description?.trim() || entry.skill.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,29 @@ describe("commands registry", () => {
|
|||||||
expect(nativeDisabled.find((spec) => spec.name === "debug")).toBeFalsy();
|
expect(nativeDisabled.find((spec) => spec.name === "debug")).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("appends skill commands when provided", () => {
|
||||||
|
const skillCommands = [
|
||||||
|
{
|
||||||
|
name: "demo_skill",
|
||||||
|
skillName: "demo-skill",
|
||||||
|
description: "Demo skill",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const commands = listChatCommandsForConfig(
|
||||||
|
{
|
||||||
|
commands: { config: false, debug: false },
|
||||||
|
},
|
||||||
|
{ skillCommands },
|
||||||
|
);
|
||||||
|
expect(commands.find((spec) => spec.nativeName === "demo_skill")).toBeTruthy();
|
||||||
|
|
||||||
|
const native = listNativeCommandSpecsForConfig(
|
||||||
|
{ commands: { config: false, debug: false, native: true } },
|
||||||
|
{ skillCommands },
|
||||||
|
);
|
||||||
|
expect(native.find((spec) => spec.name === "demo_skill")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("detects known text commands", () => {
|
it("detects known text commands", () => {
|
||||||
const detection = getCommandDetection();
|
const detection = getCommandDetection();
|
||||||
expect(detection.exact.has("/commands")).toBe(true);
|
expect(detection.exact.has("/commands")).toBe(true);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotConfig } from "../config/types.js";
|
import type { ClawdbotConfig } from "../config/types.js";
|
||||||
|
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||||
import { CHAT_COMMANDS, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
import { CHAT_COMMANDS, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||||
@@ -61,8 +62,24 @@ function escapeRegExp(value: string) {
|
|||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listChatCommands(): ChatCommandDefinition[] {
|
function buildSkillCommandDefinitions(
|
||||||
return [...CHAT_COMMANDS];
|
skillCommands?: SkillCommandSpec[],
|
||||||
|
): ChatCommandDefinition[] {
|
||||||
|
if (!skillCommands || skillCommands.length === 0) return [];
|
||||||
|
return skillCommands.map((spec) => ({
|
||||||
|
key: `skill:${spec.skillName}`,
|
||||||
|
nativeName: spec.name,
|
||||||
|
description: spec.description,
|
||||||
|
textAliases: [`/${spec.name}`],
|
||||||
|
acceptsArgs: true,
|
||||||
|
argsParsing: "none",
|
||||||
|
scope: "both",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listChatCommands(params?: { skillCommands?: SkillCommandSpec[] }): ChatCommandDefinition[] {
|
||||||
|
if (!params?.skillCommands?.length) return [...CHAT_COMMANDS];
|
||||||
|
return [...CHAT_COMMANDS, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boolean {
|
export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boolean {
|
||||||
@@ -72,23 +89,31 @@ export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boole
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listChatCommandsForConfig(cfg: ClawdbotConfig): ChatCommandDefinition[] {
|
export function listChatCommandsForConfig(
|
||||||
return CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key));
|
cfg: ClawdbotConfig,
|
||||||
|
params?: { skillCommands?: SkillCommandSpec[] },
|
||||||
|
): ChatCommandDefinition[] {
|
||||||
|
const base = CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key));
|
||||||
|
if (!params?.skillCommands?.length) return base;
|
||||||
|
return [...base, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listNativeCommandSpecs(): NativeCommandSpec[] {
|
export function listNativeCommandSpecs(params?: { skillCommands?: SkillCommandSpec[] }): NativeCommandSpec[] {
|
||||||
return CHAT_COMMANDS.filter((command) => command.scope !== "text" && command.nativeName).map(
|
return listChatCommands({ skillCommands: params?.skillCommands })
|
||||||
(command) => ({
|
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||||
|
.map((command) => ({
|
||||||
name: command.nativeName ?? command.key,
|
name: command.nativeName ?? command.key,
|
||||||
description: command.description,
|
description: command.description,
|
||||||
acceptsArgs: Boolean(command.acceptsArgs),
|
acceptsArgs: Boolean(command.acceptsArgs),
|
||||||
args: command.args,
|
args: command.args,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listNativeCommandSpecsForConfig(cfg: ClawdbotConfig): NativeCommandSpec[] {
|
export function listNativeCommandSpecsForConfig(
|
||||||
return listChatCommandsForConfig(cfg)
|
cfg: ClawdbotConfig,
|
||||||
|
params?: { skillCommands?: SkillCommandSpec[] },
|
||||||
|
): NativeCommandSpec[] {
|
||||||
|
return listChatCommandsForConfig(cfg, params)
|
||||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||||
.map((command) => ({
|
.map((command) => ({
|
||||||
name: command.nativeName ?? command.key,
|
name: command.nativeName ?? command.key,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
|
import { listSkillCommandsForWorkspace } from "../skill-commands.js";
|
||||||
import { buildCommandsMessage, buildHelpMessage } from "../status.js";
|
import { buildCommandsMessage, buildHelpMessage } from "../status.js";
|
||||||
import { buildStatusReply } from "./commands-status.js";
|
import { buildStatusReply } from "./commands-status.js";
|
||||||
import { buildContextReply } from "./commands-context-report.js";
|
import { buildContextReply } from "./commands-context-report.js";
|
||||||
@@ -28,9 +29,15 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex
|
|||||||
);
|
);
|
||||||
return { shouldContinue: false };
|
return { shouldContinue: false };
|
||||||
}
|
}
|
||||||
|
const skillCommands =
|
||||||
|
params.skillCommands ??
|
||||||
|
listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
cfg: params.cfg,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
shouldContinue: false,
|
shouldContinue: false,
|
||||||
reply: { text: buildCommandsMessage(params.cfg) },
|
reply: { text: buildCommandsMessage(params.cfg, skillCommands) },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { SessionEntry, SessionScope } from "../../config/sessions.js";
|
import type { SessionEntry, SessionScope } from "../../config/sessions.js";
|
||||||
|
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||||
import type { ReplyPayload } from "../types.js";
|
import type { ReplyPayload } from "../types.js";
|
||||||
@@ -47,6 +48,7 @@ export type HandleCommandsParams = {
|
|||||||
model: string;
|
model: string;
|
||||||
contextTokens: number;
|
contextTokens: number;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
skillCommands?: SkillCommandSpec[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CommandHandlerResult = {
|
export type CommandHandlerResult = {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { isDirectiveOnly } from "./directive-handling.js";
|
|||||||
import type { createModelSelectionState } from "./model-selection.js";
|
import type { createModelSelectionState } from "./model-selection.js";
|
||||||
import { extractInlineSimpleCommand } from "./reply-inline.js";
|
import { extractInlineSimpleCommand } from "./reply-inline.js";
|
||||||
import type { TypingController } from "./typing.js";
|
import type { TypingController } from "./typing.js";
|
||||||
|
import { listSkillCommandsForWorkspace, resolveSkillCommandInvocation } from "../skill-commands.js";
|
||||||
|
import { logVerbose } from "../../globals.js";
|
||||||
|
|
||||||
export type InlineActionResult =
|
export type InlineActionResult =
|
||||||
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
||||||
@@ -55,6 +57,7 @@ export async function handleInlineActions(params: {
|
|||||||
contextTokens: number;
|
contextTokens: number;
|
||||||
directiveAck?: ReplyPayload;
|
directiveAck?: ReplyPayload;
|
||||||
abortedLastRun: boolean;
|
abortedLastRun: boolean;
|
||||||
|
skillFilter?: string[];
|
||||||
}): Promise<InlineActionResult> {
|
}): Promise<InlineActionResult> {
|
||||||
const {
|
const {
|
||||||
ctx,
|
ctx,
|
||||||
@@ -89,11 +92,47 @@ export async function handleInlineActions(params: {
|
|||||||
contextTokens,
|
contextTokens,
|
||||||
directiveAck,
|
directiveAck,
|
||||||
abortedLastRun: initialAbortedLastRun,
|
abortedLastRun: initialAbortedLastRun,
|
||||||
|
skillFilter,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let directives = initialDirectives;
|
let directives = initialDirectives;
|
||||||
let cleanedBody = initialCleanedBody;
|
let cleanedBody = initialCleanedBody;
|
||||||
|
|
||||||
|
const shouldLoadSkillCommands = command.commandBodyNormalized.startsWith("/");
|
||||||
|
const skillCommands = shouldLoadSkillCommands
|
||||||
|
? listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir,
|
||||||
|
cfg,
|
||||||
|
skillFilter,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const skillInvocation =
|
||||||
|
allowTextCommands && skillCommands.length > 0
|
||||||
|
? resolveSkillCommandInvocation({
|
||||||
|
commandBodyNormalized: command.commandBodyNormalized,
|
||||||
|
skillCommands,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (skillInvocation) {
|
||||||
|
if (!command.isAuthorizedSender) {
|
||||||
|
logVerbose(
|
||||||
|
`Ignoring /${skillInvocation.command.name} from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
||||||
|
);
|
||||||
|
typing.cleanup();
|
||||||
|
return { kind: "reply", reply: undefined };
|
||||||
|
}
|
||||||
|
const promptParts = [
|
||||||
|
`Use the "${skillInvocation.command.skillName}" skill for this request.`,
|
||||||
|
skillInvocation.args ? `User input:\n${skillInvocation.args}` : null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const rewrittenBody = promptParts.join("\n\n");
|
||||||
|
ctx.Body = rewrittenBody;
|
||||||
|
sessionCtx.Body = rewrittenBody;
|
||||||
|
sessionCtx.BodyStripped = rewrittenBody;
|
||||||
|
cleanedBody = rewrittenBody;
|
||||||
|
}
|
||||||
|
|
||||||
const sendInlineReply = async (reply?: ReplyPayload) => {
|
const sendInlineReply = async (reply?: ReplyPayload) => {
|
||||||
if (!reply) return;
|
if (!reply) return;
|
||||||
if (!opts?.onBlockReply) return;
|
if (!opts?.onBlockReply) return;
|
||||||
@@ -148,33 +187,34 @@ export async function handleInlineActions(params: {
|
|||||||
commandBodyNormalized: inlineCommand.command,
|
commandBodyNormalized: inlineCommand.command,
|
||||||
};
|
};
|
||||||
const inlineResult = await handleCommands({
|
const inlineResult = await handleCommands({
|
||||||
ctx,
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
command: inlineCommandContext,
|
command: inlineCommandContext,
|
||||||
agentId,
|
agentId,
|
||||||
directives,
|
directives,
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: elevatedEnabled,
|
enabled: elevatedEnabled,
|
||||||
allowed: elevatedAllowed,
|
allowed: elevatedAllowed,
|
||||||
failures: elevatedFailures,
|
failures: elevatedFailures,
|
||||||
},
|
},
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
storePath,
|
storePath,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
defaultGroupActivation: defaultActivation,
|
defaultGroupActivation: defaultActivation,
|
||||||
resolvedThinkLevel,
|
resolvedThinkLevel,
|
||||||
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
resolveDefaultThinkingLevel,
|
resolveDefaultThinkingLevel,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
contextTokens,
|
contextTokens,
|
||||||
isGroup,
|
isGroup,
|
||||||
});
|
skillCommands,
|
||||||
|
});
|
||||||
if (inlineResult.reply) {
|
if (inlineResult.reply) {
|
||||||
if (!inlineCommand.cleaned) {
|
if (!inlineCommand.cleaned) {
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
@@ -235,6 +275,7 @@ export async function handleInlineActions(params: {
|
|||||||
model,
|
model,
|
||||||
contextTokens,
|
contextTokens,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
skillCommands,
|
||||||
});
|
});
|
||||||
if (!commandResult.shouldContinue) {
|
if (!commandResult.shouldContinue) {
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export async function getReplyFromConfig(
|
|||||||
contextTokens,
|
contextTokens,
|
||||||
directiveAck,
|
directiveAck,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
|
skillFilter: opts?.skillFilter,
|
||||||
});
|
});
|
||||||
if (inlineActionResult.kind === "reply") {
|
if (inlineActionResult.kind === "reply") {
|
||||||
return inlineActionResult.reply;
|
return inlineActionResult.reply;
|
||||||
|
|||||||
25
src/auto-reply/skill-commands.test.ts
Normal file
25
src/auto-reply/skill-commands.test.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveSkillCommandInvocation } from "./skill-commands.js";
|
||||||
|
|
||||||
|
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" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(invocation?.command.skillName).toBe("demo-skill");
|
||||||
|
expect(invocation?.args).toBe("do the thing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for unknown commands", () => {
|
||||||
|
const invocation = resolveSkillCommandInvocation({
|
||||||
|
commandBodyNormalized: "/unknown arg",
|
||||||
|
skillCommands: [
|
||||||
|
{ name: "demo_skill", skillName: "demo-skill", description: "Demo" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(invocation).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/auto-reply/skill-commands.ts
Normal file
51
src/auto-reply/skill-commands.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||||
|
import {
|
||||||
|
buildWorkspaceSkillCommandSpecs,
|
||||||
|
type SkillCommandSpec,
|
||||||
|
} from "../agents/skills.js";
|
||||||
|
import { listChatCommands } from "./commands-registry.js";
|
||||||
|
|
||||||
|
function resolveReservedCommandNames(): Set<string> {
|
||||||
|
const reserved = new Set<string>();
|
||||||
|
for (const command of listChatCommands()) {
|
||||||
|
if (command.nativeName) reserved.add(command.nativeName.toLowerCase());
|
||||||
|
for (const alias of command.textAliases) {
|
||||||
|
const trimmed = alias.trim();
|
||||||
|
if (!trimmed.startsWith("/")) continue;
|
||||||
|
reserved.add(trimmed.slice(1).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSkillCommandsForWorkspace(params: {
|
||||||
|
workspaceDir: string;
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
skillFilter?: string[];
|
||||||
|
}): SkillCommandSpec[] {
|
||||||
|
return buildWorkspaceSkillCommandSpecs(params.workspaceDir, {
|
||||||
|
config: params.cfg,
|
||||||
|
skillFilter: params.skillFilter,
|
||||||
|
eligibility: { remote: getRemoteSkillEligibility() },
|
||||||
|
reservedNames: resolveReservedCommandNames(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSkillCommandInvocation(params: {
|
||||||
|
commandBodyNormalized: string;
|
||||||
|
skillCommands: SkillCommandSpec[];
|
||||||
|
}): { command: SkillCommandSpec; args?: string } | null {
|
||||||
|
const trimmed = params.commandBodyNormalized.trim();
|
||||||
|
if (!trimmed.startsWith("/")) return null;
|
||||||
|
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||||
|
if (!match) return null;
|
||||||
|
const commandName = match[1]?.trim().toLowerCase();
|
||||||
|
if (!commandName) return null;
|
||||||
|
const command = params.skillCommands.find(
|
||||||
|
(entry) => entry.name.toLowerCase() === commandName,
|
||||||
|
);
|
||||||
|
if (!command) return null;
|
||||||
|
const args = match[2]?.trim();
|
||||||
|
return { command, args: args || undefined };
|
||||||
|
}
|
||||||
@@ -318,6 +318,22 @@ describe("buildCommandsMessage", () => {
|
|||||||
expect(text).not.toContain("/config");
|
expect(text).not.toContain("/config");
|
||||||
expect(text).not.toContain("/debug");
|
expect(text).not.toContain("/debug");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes skill commands when provided", () => {
|
||||||
|
const text = buildCommandsMessage(
|
||||||
|
{
|
||||||
|
commands: { config: false, debug: false },
|
||||||
|
} as ClawdbotConfig,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "demo_skill",
|
||||||
|
skillName: "demo-skill",
|
||||||
|
description: "Demo skill",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(text).toContain("/demo_skill - Demo skill");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildHelpMessage", () => {
|
describe("buildHelpMessage", () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "../utils/usage-format.js";
|
} from "../utils/usage-format.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js";
|
import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js";
|
||||||
|
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
||||||
|
|
||||||
type AgentConfig = Partial<NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>>;
|
type AgentConfig = Partial<NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>>;
|
||||||
@@ -352,9 +353,14 @@ export function buildHelpMessage(cfg?: ClawdbotConfig): string {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCommandsMessage(cfg?: ClawdbotConfig): string {
|
export function buildCommandsMessage(
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
skillCommands?: SkillCommandSpec[],
|
||||||
|
): string {
|
||||||
const lines = ["ℹ️ Slash commands"];
|
const lines = ["ℹ️ Slash commands"];
|
||||||
const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands();
|
const commands = cfg
|
||||||
|
? listChatCommandsForConfig(cfg, { skillCommands })
|
||||||
|
: listChatCommands({ skillCommands });
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
const primary = command.nativeName
|
const primary = command.nativeName
|
||||||
? `/${command.nativeName}`
|
? `/${command.nativeName}`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
|||||||
import { Routes } from "discord-api-types/v10";
|
import { Routes } from "discord-api-types/v10";
|
||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||||
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
||||||
|
import { listSkillCommandsForWorkspace } from "../../auto-reply/skill-commands.js";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||||
import {
|
import {
|
||||||
isNativeCommandsExplicitlyDisabled,
|
isNativeCommandsExplicitlyDisabled,
|
||||||
@@ -116,7 +118,15 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
throw new Error("Failed to resolve Discord application id");
|
throw new Error("Failed to resolve Discord application id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg) : [];
|
const skillCommands = nativeEnabled
|
||||||
|
? listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)),
|
||||||
|
cfg,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const commandSpecs = nativeEnabled
|
||||||
|
? listNativeCommandSpecsForConfig(cfg, { skillCommands })
|
||||||
|
: [];
|
||||||
const commands = commandSpecs.map((spec) =>
|
const commands = commandSpecs.map((spec) =>
|
||||||
createDiscordNativeCommand({
|
createDiscordNativeCommand({
|
||||||
command: spec,
|
command: spec,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
parseCommandArgs,
|
parseCommandArgs,
|
||||||
resolveCommandArgMenu,
|
resolveCommandArgMenu,
|
||||||
} from "../../auto-reply/commands-registry.js";
|
} from "../../auto-reply/commands-registry.js";
|
||||||
|
import { listSkillCommandsForWorkspace } from "../../auto-reply/skill-commands.js";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||||
import { resolveNativeCommandsEnabled } from "../../config/commands.js";
|
import { resolveNativeCommandsEnabled } from "../../config/commands.js";
|
||||||
import { danger, logVerbose } from "../../globals.js";
|
import { danger, logVerbose } from "../../globals.js";
|
||||||
@@ -403,7 +405,15 @@ export function registerSlackMonitorSlashCommands(params: {
|
|||||||
providerSetting: account.config.commands?.native,
|
providerSetting: account.config.commands?.native,
|
||||||
globalSetting: cfg.commands?.native,
|
globalSetting: cfg.commands?.native,
|
||||||
});
|
});
|
||||||
const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg) : [];
|
const skillCommands = nativeEnabled
|
||||||
|
? listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)),
|
||||||
|
cfg,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const nativeCommands = nativeEnabled
|
||||||
|
? listNativeCommandSpecsForConfig(cfg, { skillCommands })
|
||||||
|
: [];
|
||||||
if (nativeCommands.length > 0) {
|
if (nativeCommands.length > 0) {
|
||||||
for (const command of nativeCommands) {
|
for (const command of nativeCommands) {
|
||||||
ctx.app.command(
|
ctx.app.command(
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
parseCommandArgs,
|
parseCommandArgs,
|
||||||
resolveCommandArgMenu,
|
resolveCommandArgMenu,
|
||||||
} from "../auto-reply/commands-registry.js";
|
} from "../auto-reply/commands-registry.js";
|
||||||
|
import { listSkillCommandsForWorkspace } from "../auto-reply/skill-commands.js";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import type { CommandArgs } from "../auto-reply/commands-registry.js";
|
import type { CommandArgs } from "../auto-reply/commands-registry.js";
|
||||||
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
|
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
|
||||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||||
@@ -43,10 +45,21 @@ export const registerTelegramNativeCommands = ({
|
|||||||
shouldSkipUpdate,
|
shouldSkipUpdate,
|
||||||
opts,
|
opts,
|
||||||
}) => {
|
}) => {
|
||||||
const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg) : [];
|
const skillCommands = nativeEnabled
|
||||||
|
? listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)),
|
||||||
|
cfg,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const nativeCommands = nativeEnabled
|
||||||
|
? listNativeCommandSpecsForConfig(cfg, { skillCommands })
|
||||||
|
: [];
|
||||||
const reservedCommands = new Set(
|
const reservedCommands = new Set(
|
||||||
listNativeCommandSpecs().map((command) => command.name.toLowerCase()),
|
listNativeCommandSpecs().map((command) => command.name.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
for (const command of skillCommands) {
|
||||||
|
reservedCommands.add(command.name.toLowerCase());
|
||||||
|
}
|
||||||
const customResolution = resolveTelegramCustomCommands({
|
const customResolution = resolveTelegramCustomCommands({
|
||||||
commands: telegramCfg.customCommands,
|
commands: telegramCfg.customCommands,
|
||||||
reservedCommands,
|
reservedCommands,
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ import {
|
|||||||
listNativeCommandSpecs,
|
listNativeCommandSpecs,
|
||||||
listNativeCommandSpecsForConfig,
|
listNativeCommandSpecsForConfig,
|
||||||
} from "../auto-reply/commands-registry.js";
|
} from "../auto-reply/commands-registry.js";
|
||||||
|
import { listSkillCommandsForWorkspace } from "../auto-reply/skill-commands.js";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
import * as replyModule from "../auto-reply/reply.js";
|
import * as replyModule from "../auto-reply/reply.js";
|
||||||
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
|
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
|
||||||
import { resolveTelegramFetch } from "./fetch.js";
|
import { resolveTelegramFetch } from "./fetch.js";
|
||||||
|
|
||||||
|
function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsForConfig>[0]) {
|
||||||
|
return listSkillCommandsForWorkspace({
|
||||||
|
workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)),
|
||||||
|
cfg: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -187,7 +196,8 @@ describe("createTelegramBot", () => {
|
|||||||
command: string;
|
command: string;
|
||||||
description: string;
|
description: string;
|
||||||
}>;
|
}>;
|
||||||
const native = listNativeCommandSpecsForConfig(config).map((command) => ({
|
const skillCommands = resolveSkillCommands(config);
|
||||||
|
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||||
command: command.name,
|
command: command.name,
|
||||||
description: command.description,
|
description: command.description,
|
||||||
}));
|
}));
|
||||||
@@ -227,7 +237,8 @@ describe("createTelegramBot", () => {
|
|||||||
command: string;
|
command: string;
|
||||||
description: string;
|
description: string;
|
||||||
}>;
|
}>;
|
||||||
const native = listNativeCommandSpecsForConfig(config).map((command) => ({
|
const skillCommands = resolveSkillCommands(config);
|
||||||
|
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||||
command: command.name,
|
command: command.name,
|
||||||
description: command.description,
|
description: command.description,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user