feat: add OpenProse plugin skills
This commit is contained in:
@@ -39,4 +39,82 @@ describe("loadWorkspaceSkillEntries", () => {
|
||||
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("includes plugin-shipped skills when the plugin is enabled", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||
const managedDir = path.join(workspaceDir, ".managed");
|
||||
const bundledDir = path.join(workspaceDir, ".bundled");
|
||||
const pluginRoot = path.join(workspaceDir, ".clawdbot", "extensions", "open-prose");
|
||||
|
||||
await fs.mkdir(path.join(pluginRoot, "skills", "prose"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "clawdbot.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "open-prose",
|
||||
skills: ["./skills"],
|
||||
configSchema: { type: "object", additionalProperties: false, properties: {} },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
||||
`---\nname: prose\ndescription: test\n---\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir, {
|
||||
config: {
|
||||
plugins: {
|
||||
entries: { "open-prose": { enabled: true } },
|
||||
},
|
||||
},
|
||||
managedSkillsDir: managedDir,
|
||||
bundledSkillsDir: bundledDir,
|
||||
});
|
||||
|
||||
expect(entries.map((entry) => entry.skill.name)).toContain("prose");
|
||||
});
|
||||
|
||||
it("excludes plugin-shipped skills when the plugin is not allowed", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||
const managedDir = path.join(workspaceDir, ".managed");
|
||||
const bundledDir = path.join(workspaceDir, ".bundled");
|
||||
const pluginRoot = path.join(workspaceDir, ".clawdbot", "extensions", "open-prose");
|
||||
|
||||
await fs.mkdir(path.join(pluginRoot, "skills", "prose"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "clawdbot.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "open-prose",
|
||||
skills: ["./skills"],
|
||||
configSchema: { type: "object", additionalProperties: false, properties: {} },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
||||
`---\nname: prose\ndescription: test\n---\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir, {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["something-else"],
|
||||
},
|
||||
},
|
||||
managedSkillsDir: managedDir,
|
||||
bundledSkillsDir: bundledDir,
|
||||
});
|
||||
|
||||
expect(entries.map((entry) => entry.skill.name)).not.toContain("prose");
|
||||
});
|
||||
});
|
||||
|
||||
61
src/agents/skills/plugin-skills.ts
Normal file
61
src/agents/skills/plugin-skills.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEnableState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
|
||||
const log = createSubsystemLogger("skills");
|
||||
|
||||
export function resolvePluginSkillDirs(params: {
|
||||
workspaceDir: string;
|
||||
config?: ClawdbotConfig;
|
||||
}): string[] {
|
||||
const workspaceDir = params.workspaceDir.trim();
|
||||
if (!workspaceDir) return [];
|
||||
const registry = loadPluginManifestRegistry({
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
});
|
||||
if (registry.plugins.length === 0) return [];
|
||||
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
||||
const memorySlot = normalizedPlugins.slots.memory;
|
||||
let selectedMemoryPluginId: string | null = null;
|
||||
const seen = new Set<string>();
|
||||
const resolved: string[] = [];
|
||||
|
||||
for (const record of registry.plugins) {
|
||||
if (!record.skills || record.skills.length === 0) continue;
|
||||
const enableState = resolveEnableState(record.id, record.origin, normalizedPlugins);
|
||||
if (!enableState.enabled) continue;
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
if (!memoryDecision.enabled) continue;
|
||||
if (memoryDecision.selected && record.kind === "memory") {
|
||||
selectedMemoryPluginId = record.id;
|
||||
}
|
||||
for (const raw of record.skills) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) continue;
|
||||
const candidate = path.resolve(record.rootDir, trimmed);
|
||||
if (!fs.existsSync(candidate)) {
|
||||
log.warn(`plugin skill path not found (${record.id}): ${candidate}`);
|
||||
continue;
|
||||
}
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
resolved.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import chokidar, { type FSWatcher } from "chokidar";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
||||
import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
||||
|
||||
type SkillsChangeEvent = {
|
||||
workspaceDir?: string;
|
||||
@@ -59,6 +60,8 @@ function resolveWatchPaths(workspaceDir: string, config?: ClawdbotConfig): strin
|
||||
.filter(Boolean)
|
||||
.map((dir) => resolveUserPath(dir));
|
||||
paths.push(...extraDirs);
|
||||
const pluginSkillDirs = resolvePluginSkillDirs({ workspaceDir, config });
|
||||
paths.push(...pluginSkillDirs);
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveClawdbotMetadata,
|
||||
resolveSkillInvocationPolicy,
|
||||
} from "./frontmatter.js";
|
||||
import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
||||
import { serializeByKey } from "./serialize.js";
|
||||
import type {
|
||||
ParsedSkillFrontmatter,
|
||||
@@ -120,6 +121,11 @@ function loadSkillEntries(
|
||||
const extraDirs = extraDirsRaw
|
||||
.map((d) => (typeof d === "string" ? d.trim() : ""))
|
||||
.filter(Boolean);
|
||||
const pluginSkillDirs = resolvePluginSkillDirs({
|
||||
workspaceDir,
|
||||
config: opts?.config,
|
||||
});
|
||||
const mergedExtraDirs = [...extraDirs, ...pluginSkillDirs];
|
||||
|
||||
const bundledSkills = bundledSkillsDir
|
||||
? loadSkills({
|
||||
@@ -127,7 +133,7 @@ function loadSkillEntries(
|
||||
source: "clawdbot-bundled",
|
||||
})
|
||||
: [];
|
||||
const extraSkills = extraDirs.flatMap((dir) => {
|
||||
const extraSkills = mergedExtraDirs.flatMap((dir) => {
|
||||
const resolved = resolveUserPath(dir);
|
||||
return loadSkills({
|
||||
dir: resolved,
|
||||
|
||||
@@ -15,6 +15,7 @@ export type PluginManifestRecord = {
|
||||
kind?: PluginKind;
|
||||
channels: string[];
|
||||
providers: string[];
|
||||
skills: string[];
|
||||
origin: PluginOrigin;
|
||||
workspaceDir?: string;
|
||||
rootDir: string;
|
||||
@@ -86,6 +87,7 @@ function buildRecord(params: {
|
||||
kind: params.manifest.kind,
|
||||
channels: params.manifest.channels ?? [],
|
||||
providers: params.manifest.providers ?? [],
|
||||
skills: params.manifest.skills ?? [],
|
||||
origin: params.candidate.origin,
|
||||
workspaceDir: params.candidate.workspaceDir,
|
||||
rootDir: params.candidate.rootDir,
|
||||
|
||||
@@ -11,6 +11,7 @@ export type PluginManifest = {
|
||||
kind?: PluginKind;
|
||||
channels?: string[];
|
||||
providers?: string[];
|
||||
skills?: string[];
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
@@ -67,6 +68,7 @@ export function loadPluginManifest(rootDir: string): PluginManifestLoadResult {
|
||||
const version = typeof raw.version === "string" ? raw.version.trim() : undefined;
|
||||
const channels = normalizeStringList(raw.channels);
|
||||
const providers = normalizeStringList(raw.providers);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
|
||||
let uiHints: Record<string, PluginConfigUiHint> | undefined;
|
||||
if (isRecord(raw.uiHints)) {
|
||||
@@ -81,6 +83,7 @@ export function loadPluginManifest(rootDir: string): PluginManifestLoadResult {
|
||||
kind,
|
||||
channels,
|
||||
providers,
|
||||
skills,
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
|
||||
Reference in New Issue
Block a user