refactor(skills): stabilize watcher targets and include agents skills

This commit is contained in:
Peter Steinberger
2026-02-14 19:54:06 +01:00
parent 013e8f6b3b
commit eed6113359
2 changed files with 21 additions and 5 deletions

View File

@@ -1,3 +1,4 @@
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
@@ -22,10 +23,15 @@ describe("ensureSkillsWatcher", () => {
const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown }; const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown };
expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED); expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED);
const posix = (p: string) => p.replaceAll("\\", "/");
expect(targets).toEqual( expect(targets).toEqual(
expect.arrayContaining([ expect.arrayContaining([
path.join("/tmp/workspace", "skills", "SKILL.md"), posix(path.join("/tmp/workspace", "skills", "SKILL.md")),
path.join("/tmp/workspace", "skills", "*", "SKILL.md"), posix(path.join("/tmp/workspace", "skills", "*", "SKILL.md")),
posix(path.join("/tmp/workspace", ".agents", "skills", "SKILL.md")),
posix(path.join("/tmp/workspace", ".agents", "skills", "*", "SKILL.md")),
posix(path.join(os.homedir(), ".agents", "skills", "SKILL.md")),
posix(path.join(os.homedir(), ".agents", "skills", "*", "SKILL.md")),
]), ]),
); );
expect(targets.every((target) => target.includes("SKILL.md"))).toBe(true); expect(targets.every((target) => target.includes("SKILL.md"))).toBe(true);

View File

@@ -1,4 +1,5 @@
import chokidar, { type FSWatcher } from "chokidar"; import chokidar, { type FSWatcher } from "chokidar";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -59,8 +60,10 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin
const paths: string[] = []; const paths: string[] = [];
if (workspaceDir.trim()) { if (workspaceDir.trim()) {
paths.push(path.join(workspaceDir, "skills")); paths.push(path.join(workspaceDir, "skills"));
paths.push(path.join(workspaceDir, ".agents", "skills"));
} }
paths.push(path.join(CONFIG_DIR, "skills")); paths.push(path.join(CONFIG_DIR, "skills"));
paths.push(path.join(os.homedir(), ".agents", "skills"));
const extraDirsRaw = config?.skills?.load?.extraDirs ?? []; const extraDirsRaw = config?.skills?.load?.extraDirs ?? [];
const extraDirs = extraDirsRaw const extraDirs = extraDirsRaw
.map((d) => (typeof d === "string" ? d.trim() : "")) .map((d) => (typeof d === "string" ? d.trim() : ""))
@@ -72,17 +75,24 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin
return paths; return paths;
} }
function toWatchGlobRoot(raw: string): string {
// Chokidar treats globs as POSIX-ish patterns. Normalize Windows separators
// so `*` works consistently across platforms.
return raw.replaceAll("\\", "/").replace(/\/+$/, "");
}
function resolveWatchTargets(workspaceDir: string, config?: OpenClawConfig): string[] { function resolveWatchTargets(workspaceDir: string, config?: OpenClawConfig): string[] {
// Skills are defined by SKILL.md; watch only those files to avoid traversing // Skills are defined by SKILL.md; watch only those files to avoid traversing
// or watching unrelated large trees (e.g. datasets) that can exhaust FDs. // or watching unrelated large trees (e.g. datasets) that can exhaust FDs.
const targets = new Set<string>(); const targets = new Set<string>();
for (const root of resolveWatchPaths(workspaceDir, config)) { for (const root of resolveWatchPaths(workspaceDir, config)) {
const globRoot = toWatchGlobRoot(root);
// Some configs point directly at a skill folder. // Some configs point directly at a skill folder.
targets.add(path.join(root, "SKILL.md")); targets.add(`${globRoot}/SKILL.md`);
// Standard layout: <skillsRoot>/<skillName>/SKILL.md // Standard layout: <skillsRoot>/<skillName>/SKILL.md
targets.add(path.join(root, "*", "SKILL.md")); targets.add(`${globRoot}/*/SKILL.md`);
} }
return Array.from(targets); return Array.from(targets).toSorted();
} }
export function registerSkillsChangeListener(listener: (event: SkillsChangeEvent) => void) { export function registerSkillsChangeListener(listener: (event: SkillsChangeEvent) => void) {