feat(skills): add extraDirs load paths
This commit is contained in:
@@ -157,6 +157,21 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `skillsLoad`
|
||||||
|
|
||||||
|
Additional skill directories to scan (lowest precedence). This is useful if you keep skills in a separate repo but want Clawdis to pick them up without copying them into the workspace.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
skillsLoad: {
|
||||||
|
extraDirs: [
|
||||||
|
"~/Projects/agent-scripts/skills",
|
||||||
|
"~/Projects/oss/some-skill-pack/skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `browser` (clawd-managed Chrome)
|
### `browser` (clawd-managed Chrome)
|
||||||
|
|
||||||
Clawdis can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server.
|
Clawdis can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server.
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ If a skill name conflicts, precedence is:
|
|||||||
|
|
||||||
`<workspace>/skills` (highest) → `~/.clawdis/skills` → bundled skills (lowest)
|
`<workspace>/skills` (highest) → `~/.clawdis/skills` → bundled skills (lowest)
|
||||||
|
|
||||||
|
Additionally, you can configure extra skill folders (lowest precedence) via `skillsLoad.extraDirs` in `~/.clawdis/clawdis.json`.
|
||||||
|
|
||||||
## Format (AgentSkills + Pi-compatible)
|
## Format (AgentSkills + Pi-compatible)
|
||||||
|
|
||||||
`SKILL.md` must include at least:
|
`SKILL.md` must include at least:
|
||||||
|
|||||||
@@ -67,6 +67,49 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
|||||||
expect(prompt).toContain(path.join(bundledSkillDir, "SKILL.md"));
|
expect(prompt).toContain(path.join(bundledSkillDir, "SKILL.md"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads extra skill folders from config (lowest precedence)", async () => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-"));
|
||||||
|
const extraDir = path.join(workspaceDir, ".extra");
|
||||||
|
const bundledDir = path.join(workspaceDir, ".bundled");
|
||||||
|
const managedDir = path.join(workspaceDir, ".managed");
|
||||||
|
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(extraDir, "demo-skill"),
|
||||||
|
name: "demo-skill",
|
||||||
|
description: "Extra version",
|
||||||
|
body: "# Extra\n",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(bundledDir, "demo-skill"),
|
||||||
|
name: "demo-skill",
|
||||||
|
description: "Bundled version",
|
||||||
|
body: "# Bundled\n",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(managedDir, "demo-skill"),
|
||||||
|
name: "demo-skill",
|
||||||
|
description: "Managed version",
|
||||||
|
body: "# Managed\n",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "demo-skill"),
|
||||||
|
name: "demo-skill",
|
||||||
|
description: "Workspace version",
|
||||||
|
body: "# Workspace\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||||
|
bundledSkillsDir: bundledDir,
|
||||||
|
managedSkillsDir: managedDir,
|
||||||
|
config: { skillsLoad: { extraDirs: [extraDir] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain("Workspace version");
|
||||||
|
expect(prompt).not.toContain("Managed version");
|
||||||
|
expect(prompt).not.toContain("Bundled version");
|
||||||
|
expect(prompt).not.toContain("Extra version");
|
||||||
|
});
|
||||||
|
|
||||||
it("loads skills from workspace skills/", async () => {
|
it("loads skills from workspace skills/", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-"));
|
||||||
const skillDir = path.join(workspaceDir, "skills", "demo-skill");
|
const skillDir = path.join(workspaceDir, "skills", "demo-skill");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
import type { ClawdisConfig, SkillConfig } from "../config/config.js";
|
import type { ClawdisConfig, SkillConfig } from "../config/config.js";
|
||||||
import { CONFIG_DIR } from "../utils.js";
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||||
|
|
||||||
export type SkillInstallSpec = {
|
export type SkillInstallSpec = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -414,6 +414,10 @@ function loadSkillEntries(
|
|||||||
opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
|
opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
|
||||||
const workspaceSkillsDir = path.join(workspaceDir, "skills");
|
const workspaceSkillsDir = path.join(workspaceDir, "skills");
|
||||||
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
|
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
|
||||||
|
const extraDirsRaw = opts?.config?.skillsLoad?.extraDirs ?? [];
|
||||||
|
const extraDirs = extraDirsRaw
|
||||||
|
.map((d) => (typeof d === "string" ? d.trim() : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const bundledSkills = bundledSkillsDir
|
const bundledSkills = bundledSkillsDir
|
||||||
? loadSkillsFromDir({
|
? loadSkillsFromDir({
|
||||||
@@ -421,6 +425,13 @@ function loadSkillEntries(
|
|||||||
source: "clawdis-bundled",
|
source: "clawdis-bundled",
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
const extraSkills = extraDirs.flatMap((dir) => {
|
||||||
|
const resolved = resolveUserPath(dir);
|
||||||
|
return loadSkillsFromDir({
|
||||||
|
dir: resolved,
|
||||||
|
source: "clawdis-extra",
|
||||||
|
});
|
||||||
|
});
|
||||||
const managedSkills = loadSkillsFromDir({
|
const managedSkills = loadSkillsFromDir({
|
||||||
dir: managedSkillsDir,
|
dir: managedSkillsDir,
|
||||||
source: "clawdis-managed",
|
source: "clawdis-managed",
|
||||||
@@ -431,7 +442,8 @@ function loadSkillEntries(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const merged = new Map<string, Skill>();
|
const merged = new Map<string, Skill>();
|
||||||
// Precedence: bundled < managed < workspace
|
// Precedence: extra < bundled < managed < workspace
|
||||||
|
for (const skill of extraSkills) merged.set(skill.name, skill);
|
||||||
for (const skill of bundledSkills) merged.set(skill.name, skill);
|
for (const skill of bundledSkills) merged.set(skill.name, skill);
|
||||||
for (const skill of managedSkills) merged.set(skill.name, skill);
|
for (const skill of managedSkills) merged.set(skill.name, skill);
|
||||||
for (const skill of workspaceSkills) merged.set(skill.name, skill);
|
for (const skill of workspaceSkills) merged.set(skill.name, skill);
|
||||||
|
|||||||
@@ -127,6 +127,14 @@ export type SkillConfig = {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SkillsLoadConfig = {
|
||||||
|
/**
|
||||||
|
* Additional skill folders to scan (lowest precedence).
|
||||||
|
* Each directory should contain skill subfolders with `SKILL.md`.
|
||||||
|
*/
|
||||||
|
extraDirs?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ClawdisConfig = {
|
export type ClawdisConfig = {
|
||||||
identity?: {
|
identity?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -135,6 +143,7 @@ export type ClawdisConfig = {
|
|||||||
};
|
};
|
||||||
logging?: LoggingConfig;
|
logging?: LoggingConfig;
|
||||||
browser?: BrowserConfig;
|
browser?: BrowserConfig;
|
||||||
|
skillsLoad?: SkillsLoadConfig;
|
||||||
inbound?: {
|
inbound?: {
|
||||||
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
|
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
|
||||||
/** Agent working directory (preferred). Used as the default cwd for agent runs. */
|
/** Agent working directory (preferred). Used as the default cwd for agent runs. */
|
||||||
@@ -357,6 +366,11 @@ const ClawdisSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
skillsLoad: z
|
||||||
|
.object({
|
||||||
|
extraDirs: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
skills: z
|
skills: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user