@oh-my-pi/pi-coding-agent 2.3.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.
- package/CHANGELOG.md +62 -34
- package/README.md +100 -100
- package/docs/compaction.md +8 -8
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +8 -8
- package/docs/extension-loading.md +58 -58
- package/docs/hooks.md +11 -11
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +14 -14
- package/docs/session-tree-plan.md +1 -1
- package/docs/session.md +2 -2
- package/docs/skills.md +16 -16
- package/docs/theme.md +9 -9
- package/docs/tui.md +1 -1
- package/examples/README.md +1 -1
- package/examples/custom-tools/README.md +4 -4
- package/examples/custom-tools/subagent/README.md +13 -13
- package/examples/custom-tools/subagent/agents.ts +2 -2
- package/examples/custom-tools/subagent/index.ts +5 -5
- package/examples/hooks/README.md +3 -3
- package/examples/hooks/auto-commit-on-exit.ts +1 -1
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +2 -2
- package/package.json +13 -11
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +52 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/plugin-cli.ts +24 -19
- package/src/cli/update-cli.ts +10 -10
- package/src/config.ts +290 -6
- package/src/core/auth-storage.ts +32 -9
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-commands/loader.ts +44 -50
- package/src/core/custom-tools/index.ts +1 -0
- package/src/core/custom-tools/loader.ts +67 -69
- package/src/core/custom-tools/types.ts +10 -1
- package/src/core/hooks/loader.ts +13 -42
- package/src/core/index.ts +0 -1
- package/src/core/logger.ts +7 -7
- package/src/core/mcp/client.ts +1 -1
- package/src/core/mcp/config.ts +94 -146
- package/src/core/mcp/index.ts +0 -4
- package/src/core/mcp/loader.ts +26 -22
- package/src/core/mcp/manager.ts +18 -23
- package/src/core/mcp/tool-bridge.ts +9 -1
- package/src/core/mcp/types.ts +2 -0
- package/src/core/model-registry.ts +25 -8
- package/src/core/plugins/installer.ts +1 -1
- package/src/core/plugins/loader.ts +17 -11
- package/src/core/plugins/manager.ts +2 -2
- package/src/core/plugins/paths.ts +12 -7
- package/src/core/plugins/types.ts +3 -3
- package/src/core/sdk.ts +48 -27
- package/src/core/session-manager.ts +4 -4
- package/src/core/settings-manager.ts +45 -21
- package/src/core/skills.ts +222 -293
- package/src/core/slash-commands.ts +34 -165
- package/src/core/system-prompt.ts +58 -65
- package/src/core/timings.ts +2 -2
- package/src/core/tools/lsp/config.ts +38 -17
- package/src/core/tools/task/artifacts.ts +1 -1
- package/src/core/tools/task/commands.ts +30 -107
- package/src/core/tools/task/discovery.ts +54 -66
- package/src/core/tools/task/executor.ts +9 -9
- package/src/core/tools/task/index.ts +10 -10
- package/src/core/tools/task/model-resolver.ts +27 -25
- package/src/core/tools/task/types.ts +2 -2
- package/src/core/tools/web-fetch.ts +3 -3
- package/src/core/tools/web-search/auth.ts +40 -34
- package/src/core/tools/web-search/index.ts +1 -1
- package/src/core/tools/web-search/providers/anthropic.ts +1 -1
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +646 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +102 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +264 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +216 -0
- package/src/main.ts +14 -13
- package/src/migrations.ts +24 -3
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/plugin-settings.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +38 -2
- package/src/modes/interactive/components/settings-selector.ts +1 -0
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +211 -16
- package/src/modes/interactive/theme/theme-schema.json +1 -1
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +2 -2
- package/src/utils/shell.ts +7 -7
package/src/core/skills.ts
CHANGED
|
@@ -1,34 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
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
|
-
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
scanDir(options.dir);
|
|
238
109
|
|
|
239
110
|
return { skills, warnings };
|
|
240
111
|
}
|
|
241
112
|
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
274
|
-
return { skill: null, warnings };
|
|
275
|
-
}
|
|
179
|
+
scanDir(dir);
|
|
276
180
|
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
357
|
-
|
|
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;
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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: [...
|
|
364
|
+
warnings: [...result.warnings.map((w) => ({ skillPath: "", message: w })), ...collisionWarnings],
|
|
436
365
|
};
|
|
437
366
|
}
|