@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.
- package/CHANGELOG.md +72 -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 +208 -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/agents.ts +21 -0
- package/src/core/tools/task/artifacts.ts +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/commands.ts +30 -107
- package/src/core/tools/task/discovery.ts +75 -66
- package/src/core/tools/task/executor.ts +25 -10
- package/src/core/tools/task/index.ts +35 -10
- package/src/core/tools/task/model-resolver.ts +27 -25
- package/src/core/tools/task/types.ts +6 -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 +233 -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,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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
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
|
-
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
scanDir(options.dir);
|
|
238
102
|
|
|
239
103
|
return { skills, warnings };
|
|
240
104
|
}
|
|
241
105
|
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
274
|
-
return { skill: null, warnings };
|
|
275
|
-
}
|
|
165
|
+
scanDir(dir);
|
|
276
166
|
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
//
|
|
357
|
-
|
|
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;
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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"));
|
|
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
|
-
|
|
430
|
-
|
|
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: [...
|
|
350
|
+
warnings: [...result.warnings.map((w) => ({ skillPath: "", message: w })), ...collisionWarnings],
|
|
436
351
|
};
|
|
437
352
|
}
|