@oh-my-pi/pi-coding-agent 2.3.1337 → 3.1.1337

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +72 -34
  2. package/README.md +100 -100
  3. package/docs/compaction.md +8 -8
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +8 -8
  6. package/docs/extension-loading.md +58 -58
  7. package/docs/hooks.md +11 -11
  8. package/docs/rpc.md +4 -4
  9. package/docs/sdk.md +14 -14
  10. package/docs/session-tree-plan.md +1 -1
  11. package/docs/session.md +2 -2
  12. package/docs/skills.md +16 -16
  13. package/docs/theme.md +9 -9
  14. package/docs/tui.md +1 -1
  15. package/examples/README.md +1 -1
  16. package/examples/custom-tools/README.md +4 -4
  17. package/examples/custom-tools/subagent/README.md +13 -13
  18. package/examples/custom-tools/subagent/agents.ts +2 -2
  19. package/examples/custom-tools/subagent/index.ts +5 -5
  20. package/examples/hooks/README.md +3 -3
  21. package/examples/hooks/auto-commit-on-exit.ts +1 -1
  22. package/examples/hooks/custom-compaction.ts +1 -1
  23. package/examples/sdk/01-minimal.ts +1 -1
  24. package/examples/sdk/04-skills.ts +1 -1
  25. package/examples/sdk/05-tools.ts +1 -1
  26. package/examples/sdk/08-slash-commands.ts +1 -1
  27. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  28. package/examples/sdk/README.md +2 -2
  29. package/package.json +13 -11
  30. package/src/capability/context-file.ts +40 -0
  31. package/src/capability/extension.ts +48 -0
  32. package/src/capability/hook.ts +40 -0
  33. package/src/capability/index.ts +616 -0
  34. package/src/capability/instruction.ts +37 -0
  35. package/src/capability/mcp.ts +52 -0
  36. package/src/capability/prompt.ts +35 -0
  37. package/src/capability/rule.ts +52 -0
  38. package/src/capability/settings.ts +35 -0
  39. package/src/capability/skill.ts +49 -0
  40. package/src/capability/slash-command.ts +40 -0
  41. package/src/capability/system-prompt.ts +35 -0
  42. package/src/capability/tool.ts +38 -0
  43. package/src/capability/types.ts +166 -0
  44. package/src/cli/args.ts +2 -2
  45. package/src/cli/plugin-cli.ts +24 -19
  46. package/src/cli/update-cli.ts +10 -10
  47. package/src/config.ts +290 -6
  48. package/src/core/auth-storage.ts +32 -9
  49. package/src/core/bash-executor.ts +1 -1
  50. package/src/core/custom-commands/loader.ts +44 -50
  51. package/src/core/custom-tools/index.ts +1 -0
  52. package/src/core/custom-tools/loader.ts +67 -69
  53. package/src/core/custom-tools/types.ts +10 -1
  54. package/src/core/hooks/loader.ts +13 -42
  55. package/src/core/index.ts +0 -1
  56. package/src/core/logger.ts +7 -7
  57. package/src/core/mcp/client.ts +1 -1
  58. package/src/core/mcp/config.ts +94 -146
  59. package/src/core/mcp/index.ts +0 -4
  60. package/src/core/mcp/loader.ts +26 -22
  61. package/src/core/mcp/manager.ts +18 -23
  62. package/src/core/mcp/tool-bridge.ts +9 -1
  63. package/src/core/mcp/types.ts +2 -0
  64. package/src/core/model-registry.ts +25 -8
  65. package/src/core/plugins/installer.ts +1 -1
  66. package/src/core/plugins/loader.ts +17 -11
  67. package/src/core/plugins/manager.ts +2 -2
  68. package/src/core/plugins/paths.ts +12 -7
  69. package/src/core/plugins/types.ts +3 -3
  70. package/src/core/sdk.ts +48 -27
  71. package/src/core/session-manager.ts +4 -4
  72. package/src/core/settings-manager.ts +45 -21
  73. package/src/core/skills.ts +208 -293
  74. package/src/core/slash-commands.ts +34 -165
  75. package/src/core/system-prompt.ts +58 -65
  76. package/src/core/timings.ts +2 -2
  77. package/src/core/tools/lsp/config.ts +38 -17
  78. package/src/core/tools/task/agents.ts +21 -0
  79. package/src/core/tools/task/artifacts.ts +1 -1
  80. package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
  81. package/src/core/tools/task/bundled-agents/task.md +1 -0
  82. package/src/core/tools/task/commands.ts +30 -107
  83. package/src/core/tools/task/discovery.ts +75 -66
  84. package/src/core/tools/task/executor.ts +25 -10
  85. package/src/core/tools/task/index.ts +35 -10
  86. package/src/core/tools/task/model-resolver.ts +27 -25
  87. package/src/core/tools/task/types.ts +6 -2
  88. package/src/core/tools/web-fetch.ts +3 -3
  89. package/src/core/tools/web-search/auth.ts +40 -34
  90. package/src/core/tools/web-search/index.ts +1 -1
  91. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  92. package/src/discovery/agents-md.ts +75 -0
  93. package/src/discovery/builtin.ts +646 -0
  94. package/src/discovery/claude.ts +623 -0
  95. package/src/discovery/cline.ts +102 -0
  96. package/src/discovery/codex.ts +571 -0
  97. package/src/discovery/cursor.ts +264 -0
  98. package/src/discovery/gemini.ts +368 -0
  99. package/src/discovery/github.ts +120 -0
  100. package/src/discovery/helpers.test.ts +127 -0
  101. package/src/discovery/helpers.ts +249 -0
  102. package/src/discovery/index.ts +84 -0
  103. package/src/discovery/mcp-json.ts +127 -0
  104. package/src/discovery/vscode.ts +99 -0
  105. package/src/discovery/windsurf.ts +216 -0
  106. package/src/main.ts +14 -13
  107. package/src/migrations.ts +24 -3
  108. package/src/modes/interactive/components/hook-editor.ts +1 -1
  109. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  110. package/src/modes/interactive/components/settings-defs.ts +38 -2
  111. package/src/modes/interactive/components/settings-selector.ts +1 -0
  112. package/src/modes/interactive/components/welcome.ts +2 -2
  113. package/src/modes/interactive/interactive-mode.ts +233 -16
  114. package/src/modes/interactive/theme/theme-schema.json +1 -1
  115. package/src/utils/clipboard.ts +1 -1
  116. package/src/utils/shell-snapshot.ts +2 -2
  117. package/src/utils/shell.ts +7 -7
@@ -1,34 +1,15 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, join, resolve } from "node:path";
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { basename, join } from "node:path";
4
3
  import { minimatch } from "minimatch";
5
- import { CONFIG_DIR_NAME, getAgentDir } from "../config";
4
+ import { skillCapability } from "../capability/skill";
5
+ import type { SourceMeta } from "../capability/types";
6
+ import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
7
+ import { loadSync } from "../discovery";
8
+ import { parseFrontmatter } from "../discovery/helpers";
6
9
  import type { SkillsSettings } from "./settings-manager";
7
10
 
8
- /**
9
- * Standard frontmatter fields per Agent Skills spec.
10
- * See: https://agentskills.io/specification#frontmatter-required
11
- */
12
- const ALLOWED_FRONTMATTER_FIELDS = new Set([
13
- "name",
14
- "description",
15
- "license",
16
- "compatibility",
17
- "metadata",
18
- "allowed-tools",
19
- ]);
20
-
21
- /** Max name length per spec */
22
- const MAX_NAME_LENGTH = 64;
23
-
24
- /** Max description length per spec */
25
- const MAX_DESCRIPTION_LENGTH = 1024;
26
-
27
- export interface SkillFrontmatter {
28
- name?: string;
29
- description?: string;
30
- [key: string]: unknown;
31
- }
11
+ // Re-export SkillFrontmatter for backward compatibility
12
+ export type { ImportedSkillFrontmatter as SkillFrontmatter };
32
13
 
33
14
  export interface Skill {
34
15
  name: string;
@@ -36,6 +17,8 @@ export interface Skill {
36
17
  filePath: string;
37
18
  baseDir: string;
38
19
  source: string;
20
+ /** Source metadata for display */
21
+ _source?: SourceMeta;
39
22
  }
40
23
 
41
24
  export interface SkillWarning {
@@ -48,108 +31,6 @@ export interface LoadSkillsResult {
48
31
  warnings: SkillWarning[];
49
32
  }
50
33
 
51
- type SkillFormat = "recursive" | "claude";
52
-
53
- function stripQuotes(value: string): string {
54
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
55
- return value.slice(1, -1);
56
- }
57
- return value;
58
- }
59
-
60
- function parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {
61
- const frontmatter: SkillFrontmatter = {};
62
- const allKeys: string[] = [];
63
-
64
- const normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
65
-
66
- if (!normalizedContent.startsWith("---")) {
67
- return { frontmatter, body: normalizedContent, allKeys };
68
- }
69
-
70
- const endIndex = normalizedContent.indexOf("\n---", 3);
71
- if (endIndex === -1) {
72
- return { frontmatter, body: normalizedContent, allKeys };
73
- }
74
-
75
- const frontmatterBlock = normalizedContent.slice(4, endIndex);
76
- const body = normalizedContent.slice(endIndex + 4).trim();
77
-
78
- for (const line of frontmatterBlock.split("\n")) {
79
- const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
80
- if (match) {
81
- const key = match[1];
82
- const value = stripQuotes(match[2].trim());
83
- allKeys.push(key);
84
- if (key === "name") {
85
- frontmatter.name = value;
86
- } else if (key === "description") {
87
- frontmatter.description = value;
88
- }
89
- }
90
- }
91
-
92
- return { frontmatter, body, allKeys };
93
- }
94
-
95
- /**
96
- * Validate skill name per Agent Skills spec.
97
- * Returns array of validation error messages (empty if valid).
98
- */
99
- function validateName(name: string, parentDirName: string): string[] {
100
- const errors: string[] = [];
101
-
102
- if (name !== parentDirName) {
103
- errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
104
- }
105
-
106
- if (name.length > MAX_NAME_LENGTH) {
107
- errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
108
- }
109
-
110
- if (!/^[a-z0-9-]+$/.test(name)) {
111
- errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
112
- }
113
-
114
- if (name.startsWith("-") || name.endsWith("-")) {
115
- errors.push(`name must not start or end with a hyphen`);
116
- }
117
-
118
- if (name.includes("--")) {
119
- errors.push(`name must not contain consecutive hyphens`);
120
- }
121
-
122
- return errors;
123
- }
124
-
125
- /**
126
- * Validate description per Agent Skills spec.
127
- */
128
- function validateDescription(description: string | undefined): string[] {
129
- const errors: string[] = [];
130
-
131
- if (!description || description.trim() === "") {
132
- errors.push(`description is required`);
133
- } else if (description.length > MAX_DESCRIPTION_LENGTH) {
134
- errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
135
- }
136
-
137
- return errors;
138
- }
139
-
140
- /**
141
- * Check for unknown frontmatter fields.
142
- */
143
- function validateFrontmatterFields(keys: string[]): string[] {
144
- const errors: string[] = [];
145
- for (const key of keys) {
146
- if (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {
147
- errors.push(`unknown frontmatter field "${key}"`);
148
- }
149
- }
150
- return errors;
151
- }
152
-
153
34
  export interface LoadSkillsFromDirOptions {
154
35
  /** Directory to scan for skills */
155
36
  dir: string;
@@ -160,133 +41,130 @@ export interface LoadSkillsFromDirOptions {
160
41
  /**
161
42
  * Load skills from a directory recursively.
162
43
  * Skills are directories containing a SKILL.md file with frontmatter including a description.
44
+ * @deprecated Use loadSync("skills") from discovery API instead
163
45
  */
164
46
  export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
165
- const { dir, source } = options;
166
- return loadSkillsFromDirInternal(dir, source, "recursive");
167
- }
168
-
169
- function loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {
170
47
  const skills: Skill[] = [];
171
48
  const warnings: SkillWarning[] = [];
172
-
173
- if (!existsSync(dir)) {
174
- return { skills, warnings };
175
- }
176
-
177
- try {
178
- const entries = readdirSync(dir, { withFileTypes: true });
179
-
180
- for (const entry of entries) {
181
- if (entry.name.startsWith(".")) {
182
- continue;
183
- }
184
-
185
- // Skip node_modules to avoid scanning dependencies
186
- if (entry.name === "node_modules") {
187
- continue;
188
- }
189
-
190
- const fullPath = join(dir, entry.name);
191
-
192
- // For symlinks, check if they point to a directory and follow them
193
- let isDirectory = entry.isDirectory();
194
- let isFile = entry.isFile();
195
- if (entry.isSymbolicLink()) {
196
- try {
197
- const stats = statSync(fullPath);
198
- isDirectory = stats.isDirectory();
199
- isFile = stats.isFile();
200
- } catch {
201
- // Broken symlink, skip it
202
- continue;
203
- }
49
+ const seenPaths = new Set<string>();
50
+
51
+ function addSkill(skillFile: string, skillDir: string, dirName: string) {
52
+ if (seenPaths.has(skillFile)) return;
53
+ try {
54
+ const content = readFileSync(skillFile, "utf-8");
55
+ const { frontmatter } = parseFrontmatter(content);
56
+ const name = (frontmatter.name as string) || dirName;
57
+ const description = frontmatter.description as string;
58
+
59
+ if (description) {
60
+ seenPaths.add(skillFile);
61
+ skills.push({
62
+ name,
63
+ description,
64
+ filePath: skillFile,
65
+ baseDir: skillDir,
66
+ source: options.source,
67
+ });
204
68
  }
69
+ } catch {
70
+ // Skip invalid skills
71
+ }
72
+ }
205
73
 
206
- if (format === "recursive") {
207
- // Recursive format: scan directories, look for SKILL.md files
208
- if (isDirectory) {
209
- const subResult = loadSkillsFromDirInternal(fullPath, source, format);
210
- skills.push(...subResult.skills);
211
- warnings.push(...subResult.warnings);
212
- } else if (isFile && entry.name === "SKILL.md") {
213
- const result = loadSkillFromFile(fullPath, source);
214
- if (result.skill) {
215
- skills.push(result.skill);
74
+ function scanDir(dir: string) {
75
+ try {
76
+ const entries = readdirSync(dir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
79
+
80
+ const fullPath = join(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ const skillFile = join(fullPath, "SKILL.md");
83
+ try {
84
+ const stat = statSync(skillFile);
85
+ if (stat.isFile()) {
86
+ addSkill(skillFile, fullPath, entry.name);
87
+ }
88
+ } catch {
89
+ // No SKILL.md in this directory
216
90
  }
217
- warnings.push(...result.warnings);
218
- }
219
- } else if (format === "claude") {
220
- // Claude format: only one level deep, each directory must contain SKILL.md
221
- if (!isDirectory) {
222
- continue;
91
+ scanDir(fullPath);
92
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
93
+ addSkill(fullPath, dir, basename(dir));
223
94
  }
224
-
225
- const skillFile = join(fullPath, "SKILL.md");
226
- if (!existsSync(skillFile)) {
227
- continue;
228
- }
229
-
230
- const result = loadSkillFromFile(skillFile, source);
231
- if (result.skill) {
232
- skills.push(result.skill);
233
- }
234
- warnings.push(...result.warnings);
235
95
  }
96
+ } catch (err) {
97
+ warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` });
236
98
  }
237
- } catch {}
99
+ }
100
+
101
+ scanDir(options.dir);
238
102
 
239
103
  return { skills, warnings };
240
104
  }
241
105
 
242
- function loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {
106
+ /**
107
+ * Scan a directory for SKILL.md files recursively.
108
+ * Used internally by loadSkills for custom directories.
109
+ */
110
+ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
111
+ const skills: Skill[] = [];
243
112
  const warnings: SkillWarning[] = [];
244
-
245
- try {
246
- const rawContent = readFileSync(filePath, "utf-8");
247
- const { frontmatter, allKeys } = parseFrontmatter(rawContent);
248
- const skillDir = dirname(filePath);
249
- const parentDirName = basename(skillDir);
250
-
251
- // Validate frontmatter fields
252
- const fieldErrors = validateFrontmatterFields(allKeys);
253
- for (const error of fieldErrors) {
254
- warnings.push({ skillPath: filePath, message: error });
255
- }
256
-
257
- // Validate description
258
- const descErrors = validateDescription(frontmatter.description);
259
- for (const error of descErrors) {
260
- warnings.push({ skillPath: filePath, message: error });
113
+ const seenPaths = new Set<string>();
114
+
115
+ function addSkill(skillFile: string, skillDir: string, dirName: string) {
116
+ if (seenPaths.has(skillFile)) return;
117
+ try {
118
+ const content = readFileSync(skillFile, "utf-8");
119
+ const { frontmatter } = parseFrontmatter(content);
120
+ const name = (frontmatter.name as string) || dirName;
121
+ const description = frontmatter.description as string;
122
+
123
+ if (description) {
124
+ seenPaths.add(skillFile);
125
+ skills.push({
126
+ name,
127
+ description,
128
+ filePath: skillFile,
129
+ baseDir: skillDir,
130
+ source: "custom",
131
+ });
132
+ }
133
+ } catch {
134
+ // Skip invalid skills
261
135
  }
136
+ }
262
137
 
263
- // Use name from frontmatter, or fall back to parent directory name
264
- const name = frontmatter.name || parentDirName;
265
-
266
- // Validate name
267
- const nameErrors = validateName(name, parentDirName);
268
- for (const error of nameErrors) {
269
- warnings.push({ skillPath: filePath, message: error });
138
+ function scanDir(currentDir: string) {
139
+ try {
140
+ const entries = readdirSync(currentDir, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
143
+
144
+ const fullPath = join(currentDir, entry.name);
145
+ if (entry.isDirectory()) {
146
+ const skillFile = join(fullPath, "SKILL.md");
147
+ try {
148
+ const stat = statSync(skillFile);
149
+ if (stat.isFile()) {
150
+ addSkill(skillFile, fullPath, entry.name);
151
+ }
152
+ } catch {
153
+ // No SKILL.md in this directory
154
+ }
155
+ scanDir(fullPath);
156
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
157
+ addSkill(fullPath, currentDir, basename(currentDir));
158
+ }
159
+ }
160
+ } catch (err) {
161
+ warnings.push({ skillPath: currentDir, message: `Failed to read directory: ${err}` });
270
162
  }
163
+ }
271
164
 
272
- // Still load the skill even with warnings (unless description is completely missing)
273
- if (!frontmatter.description || frontmatter.description.trim() === "") {
274
- return { skill: null, warnings };
275
- }
165
+ scanDir(dir);
276
166
 
277
- return {
278
- skill: {
279
- name,
280
- description: frontmatter.description,
281
- filePath,
282
- baseDir: skillDir,
283
- source,
284
- },
285
- warnings,
286
- };
287
- } catch {
288
- return { skill: null, warnings };
289
- }
167
+ return { skills, warnings };
290
168
  }
291
169
 
292
170
  /**
@@ -331,8 +209,6 @@ function escapeXml(str: string): string {
331
209
  export interface LoadSkillsOptions extends SkillsSettings {
332
210
  /** Working directory for project-local skills. Default: process.cwd() */
333
211
  cwd?: string;
334
- /** Agent config directory for global skills. Default: ~/.pi/agent */
335
- agentDir?: string;
336
212
  }
337
213
 
338
214
  /**
@@ -342,7 +218,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
342
218
  export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
343
219
  const {
344
220
  cwd = process.cwd(),
345
- agentDir,
221
+ enabled = true,
346
222
  enableCodexUser = true,
347
223
  enableClaudeUser = true,
348
224
  enableClaudeProject = true,
@@ -353,17 +229,33 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
353
229
  includeSkills = [],
354
230
  } = options;
355
231
 
356
- // Resolve agentDir - if not provided, use default from config
357
- const resolvedAgentDir = agentDir ?? getAgentDir();
232
+ // Early return if skills are disabled
233
+ if (!enabled) {
234
+ return { skills: [], warnings: [] };
235
+ }
236
+
237
+ // Helper to check if a source is enabled
238
+ function isSourceEnabled(source: SourceMeta): boolean {
239
+ const { provider, level } = source;
240
+ if (provider === "codex" && level === "user") return enableCodexUser;
241
+ if (provider === "claude" && level === "user") return enableClaudeUser;
242
+ if (provider === "claude" && level === "project") return enableClaudeProject;
243
+ if (provider === "native" && level === "user") return enablePiUser;
244
+ if (provider === "native" && level === "project") return enablePiProject;
245
+ // For other providers (gemini, cursor, etc.) or custom, default to enabled
246
+ return true;
247
+ }
248
+
249
+ // Use capability API to load all skills
250
+ const result = loadSync<CapabilitySkill>(skillCapability.id, { cwd });
358
251
 
359
252
  const skillMap = new Map<string, Skill>();
360
253
  const realPathSet = new Set<string>();
361
- const allWarnings: SkillWarning[] = [];
362
254
  const collisionWarnings: SkillWarning[] = [];
363
255
 
364
256
  // Check if skill name matches any of the include patterns
365
257
  function matchesIncludePatterns(name: string): boolean {
366
- if (includeSkills.length === 0) return true; // No filter = include all
258
+ if (includeSkills.length === 0) return true;
367
259
  return includeSkills.some((pattern) => minimatch(name, pattern));
368
260
  }
369
261
 
@@ -373,65 +265,88 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
373
265
  return ignoredSkills.some((pattern) => minimatch(name, pattern));
374
266
  }
375
267
 
376
- function addSkills(result: LoadSkillsResult) {
377
- allWarnings.push(...result.warnings);
378
- for (const skill of result.skills) {
379
- // Apply ignore filter (glob patterns) - takes precedence over include
380
- if (matchesIgnorePatterns(skill.name)) {
381
- continue;
382
- }
383
- // Apply include filter (glob patterns)
384
- if (!matchesIncludePatterns(skill.name)) {
385
- continue;
386
- }
268
+ // Helper to add a skill to the map
269
+ function addSkill(capSkill: CapabilitySkill, sourceProvider: string) {
270
+ // Apply ignore filter (glob patterns) - takes precedence over include
271
+ if (matchesIgnorePatterns(capSkill.name)) {
272
+ return;
273
+ }
274
+ // Apply include filter (glob patterns)
275
+ if (!matchesIncludePatterns(capSkill.name)) {
276
+ return;
277
+ }
387
278
 
388
- // Resolve symlinks to detect duplicate files
389
- let realPath: string;
390
- try {
391
- realPath = realpathSync(skill.filePath);
392
- } catch {
393
- realPath = skill.filePath;
394
- }
279
+ // Resolve symlinks to detect duplicate files
280
+ let realPath: string;
281
+ try {
282
+ realPath = realpathSync(capSkill.path);
283
+ } catch {
284
+ realPath = capSkill.path;
285
+ }
395
286
 
396
- // Skip silently if we've already loaded this exact file (via symlink)
397
- if (realPathSet.has(realPath)) {
398
- continue;
399
- }
287
+ // Skip silently if we've already loaded this exact file (via symlink)
288
+ if (realPathSet.has(realPath)) {
289
+ return;
290
+ }
400
291
 
401
- const existing = skillMap.get(skill.name);
402
- if (existing) {
403
- collisionWarnings.push({
404
- skillPath: skill.filePath,
405
- message: `name collision: "${skill.name}" already loaded from ${existing.filePath}, skipping this one`,
406
- });
407
- } else {
408
- skillMap.set(skill.name, skill);
409
- realPathSet.add(realPath);
410
- }
292
+ const existing = skillMap.get(capSkill.name);
293
+ if (existing) {
294
+ collisionWarnings.push({
295
+ skillPath: capSkill.path,
296
+ message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
297
+ });
298
+ } else {
299
+ // Transform capability skill to legacy format
300
+ const skill: Skill = {
301
+ name: capSkill.name,
302
+ description: capSkill.frontmatter?.description || "",
303
+ filePath: capSkill.path,
304
+ baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
305
+ source: `${sourceProvider}:${capSkill.level}`,
306
+ _source: capSkill._source,
307
+ };
308
+ skillMap.set(capSkill.name, skill);
309
+ realPathSet.add(realPath);
411
310
  }
412
311
  }
413
312
 
414
- if (enableCodexUser) {
415
- addSkills(loadSkillsFromDirInternal(join(homedir(), ".codex", "skills"), "codex-user", "recursive"));
416
- }
417
- if (enableClaudeUser) {
418
- addSkills(loadSkillsFromDirInternal(join(homedir(), ".claude", "skills"), "claude-user", "claude"));
419
- }
420
- if (enableClaudeProject) {
421
- addSkills(loadSkillsFromDirInternal(resolve(cwd, ".claude", "skills"), "claude-project", "claude"));
422
- }
423
- if (enablePiUser) {
424
- addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", "recursive"));
425
- }
426
- if (enablePiProject) {
427
- addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", "recursive"));
313
+ // Process skills from capability API
314
+ for (const capSkill of result.items) {
315
+ // Check if this source is enabled
316
+ if (!isSourceEnabled(capSkill._source)) {
317
+ continue;
318
+ }
319
+
320
+ addSkill(capSkill, capSkill._source.provider);
428
321
  }
429
- for (const customDir of customDirectories) {
430
- addSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\/])/, homedir()), "custom", "recursive"));
322
+
323
+ // Process custom directories - scan directly without using full provider system
324
+ for (const dir of customDirectories) {
325
+ const customSkills = scanDirectoryForSkills(dir);
326
+ for (const s of customSkills.skills) {
327
+ // Convert to capability format for addSkill processing
328
+ const capSkill: CapabilitySkill = {
329
+ name: s.name,
330
+ path: s.filePath,
331
+ content: "",
332
+ frontmatter: { description: s.description },
333
+ level: "user",
334
+ _source: {
335
+ provider: "custom",
336
+ providerName: "Custom",
337
+ path: s.filePath,
338
+ level: "user",
339
+ },
340
+ };
341
+ addSkill(capSkill, "custom");
342
+ }
343
+ for (const warning of customSkills.warnings) {
344
+ collisionWarnings.push(warning);
345
+ }
431
346
  }
432
347
 
433
348
  return {
434
349
  skills: Array.from(skillMap.values()),
435
- warnings: [...allWarnings, ...collisionWarnings],
350
+ warnings: [...result.warnings.map((w) => ({ skillPath: "", message: w })), ...collisionWarnings],
436
351
  };
437
352
  }