@oh-my-pi/pi-coding-agent 2.2.1337 → 3.0.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 (116) hide show
  1. package/CHANGELOG.md +64 -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 +16 -12
  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/export-html/index.ts +9 -9
  55. package/src/core/export-html/template.generated.ts +2 -0
  56. package/src/core/hooks/loader.ts +13 -42
  57. package/src/core/index.ts +0 -1
  58. package/src/core/logger.ts +7 -7
  59. package/src/core/mcp/client.ts +1 -1
  60. package/src/core/mcp/config.ts +94 -146
  61. package/src/core/mcp/index.ts +0 -4
  62. package/src/core/mcp/loader.ts +26 -22
  63. package/src/core/mcp/manager.ts +18 -23
  64. package/src/core/mcp/tool-bridge.ts +9 -1
  65. package/src/core/mcp/types.ts +2 -0
  66. package/src/core/model-registry.ts +25 -8
  67. package/src/core/plugins/installer.ts +1 -1
  68. package/src/core/plugins/loader.ts +17 -11
  69. package/src/core/plugins/manager.ts +2 -2
  70. package/src/core/plugins/paths.ts +12 -7
  71. package/src/core/plugins/types.ts +3 -3
  72. package/src/core/sdk.ts +48 -27
  73. package/src/core/session-manager.ts +4 -4
  74. package/src/core/settings-manager.ts +45 -21
  75. package/src/core/skills.ts +222 -293
  76. package/src/core/slash-commands.ts +34 -165
  77. package/src/core/system-prompt.ts +58 -65
  78. package/src/core/timings.ts +2 -2
  79. package/src/core/tools/lsp/config.ts +38 -17
  80. package/src/core/tools/task/artifacts.ts +1 -1
  81. package/src/core/tools/task/commands.ts +30 -107
  82. package/src/core/tools/task/discovery.ts +54 -66
  83. package/src/core/tools/task/executor.ts +9 -9
  84. package/src/core/tools/task/index.ts +10 -10
  85. package/src/core/tools/task/model-resolver.ts +27 -25
  86. package/src/core/tools/task/types.ts +2 -2
  87. package/src/core/tools/web-fetch.ts +3 -3
  88. package/src/core/tools/web-search/auth.ts +40 -34
  89. package/src/core/tools/web-search/index.ts +1 -1
  90. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  91. package/src/discovery/agents-md.ts +75 -0
  92. package/src/discovery/builtin.ts +646 -0
  93. package/src/discovery/claude.ts +623 -0
  94. package/src/discovery/cline.ts +102 -0
  95. package/src/discovery/codex.ts +571 -0
  96. package/src/discovery/cursor.ts +264 -0
  97. package/src/discovery/gemini.ts +368 -0
  98. package/src/discovery/github.ts +120 -0
  99. package/src/discovery/helpers.test.ts +127 -0
  100. package/src/discovery/helpers.ts +249 -0
  101. package/src/discovery/index.ts +84 -0
  102. package/src/discovery/mcp-json.ts +127 -0
  103. package/src/discovery/vscode.ts +99 -0
  104. package/src/discovery/windsurf.ts +216 -0
  105. package/src/main.ts +14 -13
  106. package/src/migrations.ts +24 -3
  107. package/src/modes/interactive/components/hook-editor.ts +1 -1
  108. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  109. package/src/modes/interactive/components/settings-defs.ts +38 -2
  110. package/src/modes/interactive/components/settings-selector.ts +1 -0
  111. package/src/modes/interactive/components/welcome.ts +2 -2
  112. package/src/modes/interactive/interactive-mode.ts +211 -16
  113. package/src/modes/interactive/theme/theme-schema.json +1 -1
  114. package/src/utils/clipboard.ts +1 -1
  115. package/src/utils/shell-snapshot.ts +2 -2
  116. 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,144 @@ 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
49
 
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
- }
204
- }
205
-
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);
50
+ function scanDir(dir: string) {
51
+ try {
52
+ const entries = readdirSync(dir, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
55
+
56
+ const fullPath = join(dir, entry.name);
57
+ if (entry.isDirectory()) {
58
+ const skillFile = join(fullPath, "SKILL.md");
59
+ try {
60
+ const stat = statSync(skillFile);
61
+ if (stat.isFile()) {
62
+ const content = readFileSync(skillFile, "utf-8");
63
+ const { frontmatter } = parseFrontmatter(content);
64
+ const name = (frontmatter.name as string) || entry.name;
65
+ const description = frontmatter.description as string;
66
+
67
+ if (description) {
68
+ skills.push({
69
+ name,
70
+ description,
71
+ filePath: skillFile,
72
+ baseDir: fullPath,
73
+ source: options.source,
74
+ });
75
+ }
76
+ }
77
+ } catch {
78
+ // Skip invalid skills
216
79
  }
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;
223
- }
224
-
225
- const skillFile = join(fullPath, "SKILL.md");
226
- if (!existsSync(skillFile)) {
227
- continue;
228
- }
229
80
 
230
- const result = loadSkillFromFile(skillFile, source);
231
- if (result.skill) {
232
- skills.push(result.skill);
81
+ scanDir(fullPath);
82
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
83
+ try {
84
+ const content = readFileSync(fullPath, "utf-8");
85
+ const { frontmatter } = parseFrontmatter(content);
86
+ const name = (frontmatter.name as string) || basename(dir);
87
+ const description = frontmatter.description as string;
88
+
89
+ if (description) {
90
+ skills.push({
91
+ name,
92
+ description,
93
+ filePath: fullPath,
94
+ baseDir: dir,
95
+ source: options.source,
96
+ });
97
+ }
98
+ } catch {
99
+ // Skip invalid skills
100
+ }
233
101
  }
234
- warnings.push(...result.warnings);
235
102
  }
103
+ } catch (err) {
104
+ warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` });
236
105
  }
237
- } catch {}
106
+ }
107
+
108
+ scanDir(options.dir);
238
109
 
239
110
  return { skills, warnings };
240
111
  }
241
112
 
242
- function loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {
113
+ /**
114
+ * Scan a directory for SKILL.md files recursively.
115
+ * Used internally by loadSkills for custom directories.
116
+ */
117
+ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
118
+ const skills: Skill[] = [];
243
119
  const warnings: SkillWarning[] = [];
244
120
 
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 });
261
- }
262
-
263
- // Use name from frontmatter, or fall back to parent directory name
264
- const name = frontmatter.name || parentDirName;
121
+ function scanDir(currentDir: string) {
122
+ try {
123
+ const entries = readdirSync(currentDir, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
126
+
127
+ const fullPath = join(currentDir, entry.name);
128
+ if (entry.isDirectory()) {
129
+ const skillFile = join(fullPath, "SKILL.md");
130
+ try {
131
+ const stat = statSync(skillFile);
132
+ if (stat.isFile()) {
133
+ const content = readFileSync(skillFile, "utf-8");
134
+ const { frontmatter } = parseFrontmatter(content);
135
+ const name = (frontmatter.name as string) || entry.name;
136
+ const description = frontmatter.description as string;
137
+
138
+ if (description) {
139
+ skills.push({
140
+ name,
141
+ description,
142
+ filePath: skillFile,
143
+ baseDir: fullPath,
144
+ source: "custom",
145
+ });
146
+ }
147
+ }
148
+ } catch {
149
+ // Skip invalid skills
150
+ }
265
151
 
266
- // Validate name
267
- const nameErrors = validateName(name, parentDirName);
268
- for (const error of nameErrors) {
269
- warnings.push({ skillPath: filePath, message: error });
152
+ scanDir(fullPath);
153
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
154
+ try {
155
+ const content = readFileSync(fullPath, "utf-8");
156
+ const { frontmatter } = parseFrontmatter(content);
157
+ const name = (frontmatter.name as string) || basename(currentDir);
158
+ const description = frontmatter.description as string;
159
+
160
+ if (description) {
161
+ skills.push({
162
+ name,
163
+ description,
164
+ filePath: fullPath,
165
+ baseDir: currentDir,
166
+ source: "custom",
167
+ });
168
+ }
169
+ } catch {
170
+ // Skip invalid skills
171
+ }
172
+ }
173
+ }
174
+ } catch (err) {
175
+ warnings.push({ skillPath: currentDir, message: `Failed to read directory: ${err}` });
270
176
  }
177
+ }
271
178
 
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
- }
179
+ scanDir(dir);
276
180
 
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
- }
181
+ return { skills, warnings };
290
182
  }
291
183
 
292
184
  /**
@@ -331,8 +223,6 @@ function escapeXml(str: string): string {
331
223
  export interface LoadSkillsOptions extends SkillsSettings {
332
224
  /** Working directory for project-local skills. Default: process.cwd() */
333
225
  cwd?: string;
334
- /** Agent config directory for global skills. Default: ~/.pi/agent */
335
- agentDir?: string;
336
226
  }
337
227
 
338
228
  /**
@@ -342,7 +232,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
342
232
  export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
343
233
  const {
344
234
  cwd = process.cwd(),
345
- agentDir,
235
+ enabled = true,
346
236
  enableCodexUser = true,
347
237
  enableClaudeUser = true,
348
238
  enableClaudeProject = true,
@@ -353,17 +243,33 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
353
243
  includeSkills = [],
354
244
  } = options;
355
245
 
356
- // Resolve agentDir - if not provided, use default from config
357
- const resolvedAgentDir = agentDir ?? getAgentDir();
246
+ // Early return if skills are disabled
247
+ if (!enabled) {
248
+ return { skills: [], warnings: [] };
249
+ }
250
+
251
+ // Helper to check if a source is enabled
252
+ function isSourceEnabled(source: SourceMeta): boolean {
253
+ const { provider, level } = source;
254
+ if (provider === "codex" && level === "user") return enableCodexUser;
255
+ if (provider === "claude" && level === "user") return enableClaudeUser;
256
+ if (provider === "claude" && level === "project") return enableClaudeProject;
257
+ if (provider === "native" && level === "user") return enablePiUser;
258
+ if (provider === "native" && level === "project") return enablePiProject;
259
+ // For other providers (gemini, cursor, etc.) or custom, default to enabled
260
+ return true;
261
+ }
262
+
263
+ // Use capability API to load all skills
264
+ const result = loadSync<CapabilitySkill>(skillCapability.id, { cwd });
358
265
 
359
266
  const skillMap = new Map<string, Skill>();
360
267
  const realPathSet = new Set<string>();
361
- const allWarnings: SkillWarning[] = [];
362
268
  const collisionWarnings: SkillWarning[] = [];
363
269
 
364
270
  // Check if skill name matches any of the include patterns
365
271
  function matchesIncludePatterns(name: string): boolean {
366
- if (includeSkills.length === 0) return true; // No filter = include all
272
+ if (includeSkills.length === 0) return true;
367
273
  return includeSkills.some((pattern) => minimatch(name, pattern));
368
274
  }
369
275
 
@@ -373,65 +279,88 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
373
279
  return ignoredSkills.some((pattern) => minimatch(name, pattern));
374
280
  }
375
281
 
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
- }
282
+ // Helper to add a skill to the map
283
+ function addSkill(capSkill: CapabilitySkill, sourceProvider: string) {
284
+ // Apply ignore filter (glob patterns) - takes precedence over include
285
+ if (matchesIgnorePatterns(capSkill.name)) {
286
+ return;
287
+ }
288
+ // Apply include filter (glob patterns)
289
+ if (!matchesIncludePatterns(capSkill.name)) {
290
+ return;
291
+ }
387
292
 
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
- }
293
+ // Resolve symlinks to detect duplicate files
294
+ let realPath: string;
295
+ try {
296
+ realPath = realpathSync(capSkill.path);
297
+ } catch {
298
+ realPath = capSkill.path;
299
+ }
395
300
 
396
- // Skip silently if we've already loaded this exact file (via symlink)
397
- if (realPathSet.has(realPath)) {
398
- continue;
399
- }
301
+ // Skip silently if we've already loaded this exact file (via symlink)
302
+ if (realPathSet.has(realPath)) {
303
+ return;
304
+ }
400
305
 
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
- }
306
+ const existing = skillMap.get(capSkill.name);
307
+ if (existing) {
308
+ collisionWarnings.push({
309
+ skillPath: capSkill.path,
310
+ message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
311
+ });
312
+ } else {
313
+ // Transform capability skill to legacy format
314
+ const skill: Skill = {
315
+ name: capSkill.name,
316
+ description: capSkill.frontmatter?.description || "",
317
+ filePath: capSkill.path,
318
+ baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
319
+ source: `${sourceProvider}:${capSkill.level}`,
320
+ _source: capSkill._source,
321
+ };
322
+ skillMap.set(capSkill.name, skill);
323
+ realPathSet.add(realPath);
411
324
  }
412
325
  }
413
326
 
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"));
327
+ // Process skills from capability API
328
+ for (const capSkill of result.items) {
329
+ // Check if this source is enabled
330
+ if (!isSourceEnabled(capSkill._source)) {
331
+ continue;
332
+ }
333
+
334
+ addSkill(capSkill, capSkill._source.provider);
428
335
  }
429
- for (const customDir of customDirectories) {
430
- addSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\/])/, homedir()), "custom", "recursive"));
336
+
337
+ // Process custom directories - scan directly without using full provider system
338
+ for (const dir of customDirectories) {
339
+ const customSkills = scanDirectoryForSkills(dir);
340
+ for (const s of customSkills.skills) {
341
+ // Convert to capability format for addSkill processing
342
+ const capSkill: CapabilitySkill = {
343
+ name: s.name,
344
+ path: s.filePath,
345
+ content: "",
346
+ frontmatter: { description: s.description },
347
+ level: "user",
348
+ _source: {
349
+ provider: "custom",
350
+ providerName: "Custom",
351
+ path: s.filePath,
352
+ level: "user",
353
+ },
354
+ };
355
+ addSkill(capSkill, "custom");
356
+ }
357
+ for (const warning of customSkills.warnings) {
358
+ collisionWarnings.push(warning);
359
+ }
431
360
  }
432
361
 
433
362
  return {
434
363
  skills: Array.from(skillMap.values()),
435
- warnings: [...allWarnings, ...collisionWarnings],
364
+ warnings: [...result.warnings.map((w) => ({ skillPath: "", message: w })), ...collisionWarnings],
436
365
  };
437
366
  }