diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 216183e6..6358550d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,6 +10,10 @@ - **Process suspension**: Press `Ctrl+Z` to suspend pi and return to the shell. Resume with `fg` as usual. ([#267](https://github.com/badlogic/pi-mono/pull/267) by [@aliou](https://github.com/aliou)) +- **Configurable skills directories**: Added granular control over skill sources with `enableCodexUser`, `enableClaudeUser`, `enableClaudeProject`, `enablePiUser`, `enablePiProject` toggles, plus `customDirectories` and `ignoredSkills` settings. ([#269](https://github.com/badlogic/pi-mono/pull/269) by [@nicobailon](https://github.com/nicobailon)) + +- **Skills CLI filtering**: Added `--skills ` flag for filtering skills with glob patterns. Also added `includeSkills` setting and glob pattern support for `ignoredSkills`. ([#268](https://github.com/badlogic/pi-mono/issues/268)) + ## [0.25.2] - 2025-12-21 ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index d9443a42..c5910120 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -733,6 +733,7 @@ pi [options] [@files...] [messages...] | `--thinking ` | Thinking level: `off`, `minimal`, `low`, `medium`, `high` | | `--hook ` | Load a hook file (can be used multiple times) | | `--no-skills` | Disable skills discovery and loading | +| `--skills ` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) | | `--export [output]` | Export session to HTML | | `--help`, `-h` | Show help | | `--version`, `-v` | Show version | diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 4b9c56b0..a32a2397 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -159,7 +159,8 @@ Configure skill loading in `~/.pi/agent/settings.json`: "enablePiUser": true, "enablePiProject": true, "customDirectories": ["~/my-skills-repo"], - "ignoredSkills": ["deprecated-skill"] + "ignoredSkills": ["deprecated-skill"], + "includeSkills": ["git-*", "docker"] } } ``` @@ -173,7 +174,27 @@ Configure skill loading in `~/.pi/agent/settings.json`: | `enablePiUser` | `true` | Load from `~/.pi/agent/skills/` | | `enablePiProject` | `true` | Load from `/.pi/skills/` | | `customDirectories` | `[]` | Additional directories to scan (supports `~` expansion) | -| `ignoredSkills` | `[]` | Skill names to exclude | +| `ignoredSkills` | `[]` | Glob patterns to exclude (e.g., `["deprecated-*", "test-skill"]`) | +| `includeSkills` | `[]` | Glob patterns to include (empty = all; e.g., `["git-*", "docker"]`) | + +**Note:** `ignoredSkills` takes precedence over both `includeSkills` in settings and the `--skills` CLI flag. A skill matching any ignore pattern will be excluded regardless of include patterns. + +### CLI Filtering + +Use `--skills` to filter skills for a specific invocation: + +```bash +# Only load specific skills +pi --skills git,docker + +# Glob patterns +pi --skills "git-*,docker-*" + +# All skills matching a prefix +pi --skills "aws-*" +``` + +This overrides the `includeSkills` setting for the current session. ## How Skills Work diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index bc68953b..c846ee95 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -30,6 +30,7 @@ export interface Args { print?: boolean; export?: string; noSkills?: boolean; + skills?: string[]; listModels?: string | true; messages: string[]; fileArgs: string[]; @@ -115,6 +116,9 @@ export function parseArgs(args: string[]): Args { result.customTools.push(args[++i]); } else if (arg === "--no-skills") { result.noSkills = true; + } else if (arg === "--skills" && i + 1 < args.length) { + // Comma-separated glob patterns for skill filtering + result.skills = args[++i].split(",").map((s) => s.trim()); } else if (arg === "--list-models") { // Check if next arg is a search pattern (not a flag or file arg) if (i + 1 < args.length && !args[i + 1].startsWith("-") && !args[i + 1].startsWith("@")) { @@ -157,6 +161,7 @@ ${chalk.bold("Options:")} --hook Load a hook file (can be used multiple times) --tool Load a custom tool file (can be used multiple times) --no-skills Disable skills discovery and loading + --skills Comma-separated glob patterns to filter skills (e.g., git-*,docker) --export Export session file to HTML and exit --list-models [search] List available models (with optional fuzzy search) --help, -h Show this help diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index d04a5156..de4d0d06 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -22,7 +22,8 @@ export interface SkillsSettings { enablePiUser?: boolean; // default: true enablePiProject?: boolean; // default: true customDirectories?: string[]; // default: [] - ignoredSkills?: string[]; // default: [] + ignoredSkills?: string[]; // default: [] (glob patterns to exclude; takes precedence over includeSkills) + includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter) } export interface TerminalSettings { @@ -270,6 +271,7 @@ export class SettingsManager { enablePiProject: this.settings.skills?.enablePiProject ?? true, customDirectories: this.settings.skills?.customDirectories ?? [], ignoredSkills: this.settings.skills?.ignoredSkills ?? [], + includeSkills: this.settings.skills?.includeSkills ?? [], }; } diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 0e396012..260e3583 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -1,4 +1,5 @@ import { existsSync, readdirSync, readFileSync } from "fs"; +import { minimatch } from "minimatch"; import { homedir } from "os"; import { basename, dirname, join, resolve } from "path"; import { CONFIG_DIR_NAME } from "../config.js"; @@ -325,16 +326,34 @@ export function loadSkills(options: SkillsSettings = {}): LoadSkillsResult { enablePiProject = true, customDirectories = [], ignoredSkills = [], + includeSkills = [], } = options; const skillMap = new Map(); const allWarnings: SkillWarning[] = []; const collisionWarnings: SkillWarning[] = []; + // Check if skill name matches any of the include patterns + function matchesIncludePatterns(name: string): boolean { + if (includeSkills.length === 0) return true; // No filter = include all + return includeSkills.some((pattern) => minimatch(name, pattern)); + } + + // Check if skill name matches any of the ignore patterns + function matchesIgnorePatterns(name: string): boolean { + if (ignoredSkills.length === 0) return false; + return ignoredSkills.some((pattern) => minimatch(name, pattern)); + } + function addSkills(result: LoadSkillsResult) { allWarnings.push(...result.warnings); for (const skill of result.skills) { - if (ignoredSkills.includes(skill.name)) { + // Apply ignore filter (glob patterns) - takes precedence over include + if (matchesIgnorePatterns(skill.name)) { + continue; + } + // Apply include filter (glob patterns) + if (!matchesIncludePatterns(skill.name)) { continue; } const existing = skillMap.get(skill.name); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index aa7235ce..093b7ab4 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -289,6 +289,9 @@ export async function main(args: string[]) { if (parsed.noSkills) { skillsSettings.enabled = false; } + if (parsed.skills && parsed.skills.length > 0) { + skillsSettings.includeSkills = parsed.skills; + } const systemPrompt = buildSystemPrompt({ customPrompt: parsed.systemPrompt, selectedTools: parsed.tools, diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index 9661f60c..94859eb6 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -258,6 +258,34 @@ describe("skills", () => { expect(skills).toHaveLength(0); }); + it("should support glob patterns in ignoredSkills", () => { + const { skills } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + ignoredSkills: ["valid-*"], + }); + expect(skills.every((s) => !s.name.startsWith("valid-"))).toBe(true); + }); + + it("should have ignoredSkills take precedence over includeSkills", () => { + const { skills } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + includeSkills: ["valid-*"], + ignoredSkills: ["valid-skill"], + }); + // valid-skill should be excluded even though it matches includeSkills + expect(skills.every((s) => s.name !== "valid-skill")).toBe(true); + }); + it("should expand ~ in customDirectories", () => { const homeSkillsDir = join(homedir(), ".pi/agent/skills"); const { skills: withTilde } = loadSkills({ @@ -289,6 +317,67 @@ describe("skills", () => { }); expect(skills).toHaveLength(0); }); + + it("should filter skills with includeSkills glob patterns", () => { + // Load all skills from fixtures + const { skills: allSkills } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + }); + expect(allSkills.length).toBeGreaterThan(0); + + // Filter to only include "valid-skill" + const { skills: filtered } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + includeSkills: ["valid-skill"], + }); + expect(filtered).toHaveLength(1); + expect(filtered[0].name).toBe("valid-skill"); + }); + + it("should support glob patterns in includeSkills", () => { + const { skills } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + includeSkills: ["valid-*"], + }); + expect(skills.length).toBeGreaterThan(0); + expect(skills.every((s) => s.name.startsWith("valid-"))).toBe(true); + }); + + it("should return all skills when includeSkills is empty", () => { + const { skills: withEmpty } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + includeSkills: [], + }); + const { skills: withoutOption } = loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [fixturesDir], + }); + expect(withEmpty.length).toBe(withoutOption.length); + }); }); describe("collision handling", () => {