mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix: publish plugin manifest skills for agent discovery
This commit is contained in:
@@ -52,7 +52,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
|
||||
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
|
||||
- TUI/render: stop the long-token sanitizer from injecting literal spaces inside inline code spans, fenced code blocks, table borders, and bare hyphenated/dotted identifiers, so copied package names, entity IDs, and shell line-continuations stay byte-for-byte intact while narrow-terminal protection still chunks unidentifiable long prose tokens. Fixes #48432, #39505. Thanks @DocOellerson, @xeusoc, @CCcassiusdjs, @akramcodez, @brokemac79, @romneyda.
|
||||
- Plugin skills: publish plugin-declared skills into the managed skills directory (`~/.openclaw/skills/`) via symlinks at resolution time, so the agent SDK file-based discovery paths find plugin skill SKILL.md files and stop logging ENOENT when the agent tries to read them. Fixes #77296.
|
||||
- Plugin skills: publish plugin-declared skills through the generated plugin skills directory (`~/.openclaw/plugin-skills/`) while keeping direct prompt loading intact, so agent file-based discovery paths find plugin skill `SKILL.md` files and inactive plugin links are cleaned up. Fixes #77296. (#77328) Thanks @zhangguiping-xydt.
|
||||
- Gateway/status: label Linux managed gateway services as `systemd user`, making status output explicit about the user-service scope instead of implying a system-level unit. Thanks @vincentkoc.
|
||||
- Plugins/install: remove the previous managed plugin directory when a reinstall switches sources, so stale ClawHub and npm copies no longer keep duplicate plugin ids in discovery after the new install wins. Thanks @vincentkoc.
|
||||
- Plugins/install: let official plugin reinstall recovery repair source-only installed runtime shadows, so `openclaw plugins install npm:@openclaw/discord --force` can replace the bad package instead of stopping at stale config validation. Thanks @vincentkoc.
|
||||
|
||||
@@ -196,7 +196,12 @@ describe("loadWorkspaceSkillEntries", () => {
|
||||
managedSkillsDir: managedDir,
|
||||
});
|
||||
|
||||
expect(enabledEntries.map((entry) => entry.skill.name)).toContain("browser-automation");
|
||||
const browserEntry = enabledEntries.find((entry) => entry.skill.name === "browser-automation");
|
||||
const browserSkillDir = path.join(pluginRoot, "skills", "browser-automation");
|
||||
expect(browserEntry?.skill.baseDir).toBe(browserSkillDir);
|
||||
await expect(
|
||||
fs.readlink(path.join(workspaceDir, ".plugin-skills", "browser-automation")),
|
||||
).resolves.toBe(browserSkillDir);
|
||||
|
||||
const blockedEntries = loadTestWorkspaceSkillEntries(workspaceDir, {
|
||||
config: {
|
||||
@@ -208,6 +213,9 @@ describe("loadWorkspaceSkillEntries", () => {
|
||||
});
|
||||
|
||||
expect(blockedEntries.map((entry) => entry.skill.name)).not.toContain("browser-automation");
|
||||
await expect(
|
||||
fs.lstat(path.join(workspaceDir, ".plugin-skills", "browser-automation")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("loads frontmatter edge cases in one workspace", async () => {
|
||||
|
||||
@@ -391,40 +391,33 @@ describe("publishPluginSkills", () => {
|
||||
expect(fsSync.readlinkSync(path.join(managedDir, "my-skill"))).toBe(dir);
|
||||
});
|
||||
|
||||
it("preserves existing managed skill symlinks when a plugin skill has the same name", async () => {
|
||||
const skillParent = await tempDirs.make("plugin-skills-");
|
||||
it("replaces owned generated symlinks when a plugin skill target moves", async () => {
|
||||
const skillParent1 = await tempDirs.make("plugin-skills-1-");
|
||||
const skillParent2 = await tempDirs.make("plugin-skills-2-");
|
||||
const managedDir = await tempDirs.make("managed-skills-");
|
||||
|
||||
const dir1 = await writeSkillDir(skillParent, "skill-v1", "old");
|
||||
const dir2 = await writeSkillDir(skillParent, "my-skill", "new");
|
||||
const dir1 = await writeSkillDir(skillParent1, "my-skill", "old");
|
||||
const dir2 = await writeSkillDir(skillParent2, "my-skill", "new");
|
||||
|
||||
// Manually create a symlink to dir1 under the same name as dir2's basename.
|
||||
fsSync.symlinkSync(dir1, path.join(managedDir, "my-skill"), "dir");
|
||||
|
||||
// Now publish dir2 (basename "my-skill"); must NOT replace existing symlink.
|
||||
publishPluginSkills([dir2], { pluginSkillsDir: managedDir });
|
||||
|
||||
// Existing managed symlink is preserved.
|
||||
expect(fsSync.readlinkSync(path.join(managedDir, "my-skill"))).toBe(dir1);
|
||||
expect(fsSync.readlinkSync(path.join(managedDir, "my-skill"))).toBe(dir2);
|
||||
});
|
||||
|
||||
it("cleans up stale symlinks whose targets no longer exist", async () => {
|
||||
it("cleans up stale symlinks whose targets still exist", async () => {
|
||||
const skillParent = await tempDirs.make("plugin-skills-");
|
||||
const managedDir = await tempDirs.make("managed-skills-");
|
||||
|
||||
const dir = await writeSkillDir(skillParent, "current-skill");
|
||||
const staleDir = path.join(skillParent, "stale-skill");
|
||||
await fs.mkdir(staleDir, { recursive: true });
|
||||
const staleDir = await writeSkillDir(skillParent, "stale-skill");
|
||||
|
||||
// Create a stale symlink pointing to a directory we'll delete.
|
||||
fsSync.symlinkSync(staleDir, path.join(managedDir, "stale-skill"), "dir");
|
||||
await fs.rm(staleDir, { recursive: true, force: true });
|
||||
|
||||
// Publish only the current skill; stale should be cleaned up.
|
||||
publishPluginSkills([dir], { pluginSkillsDir: managedDir });
|
||||
|
||||
expect(fsSync.existsSync(path.join(managedDir, "current-skill"))).toBe(true);
|
||||
// Stale symlink pointing to nonexistent target should be removed.
|
||||
expect(fsSync.existsSync(path.join(managedDir, "stale-skill"))).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -184,10 +184,7 @@ function publishPluginSkills(skillDirs: string[], opts?: { pluginSkillsDir?: str
|
||||
if (existingTarget === target) {
|
||||
continue;
|
||||
}
|
||||
log.warn(
|
||||
`plugin skill symlink "${linkPath}" already exists, skipping plugin skill "${target}"`,
|
||||
);
|
||||
continue;
|
||||
fs.unlinkSync(linkPath);
|
||||
} catch (err) {
|
||||
if (!isNotFoundError(err)) {
|
||||
log.warn(`failed to inspect plugin skill symlink "${linkPath}": ${String(err)}`);
|
||||
@@ -219,18 +216,9 @@ function publishPluginSkills(skillDirs: string[], opts?: { pluginSkillsDir?: str
|
||||
}
|
||||
const linkPath = path.join(pluginSkillsDir, entry.name);
|
||||
try {
|
||||
const target = fs.readlinkSync(linkPath);
|
||||
// Only remove symlinks that point to directories that no longer exist.
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.unlinkSync(linkPath);
|
||||
}
|
||||
fs.unlinkSync(linkPath);
|
||||
} catch {
|
||||
// Broken symlink or other issue — best-effort cleanup.
|
||||
try {
|
||||
fs.unlinkSync(linkPath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user