oricore 1.4.1 → 1.5.1
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/dist/{chunk-SXDGT4YB.js → chunk-D5X6YFSK.js} +1814 -457
- package/dist/chunk-D5X6YFSK.js.map +1 -0
- package/dist/{chunk-XKZSVWRX.js → chunk-MZNH54NB.js} +375 -171
- package/dist/chunk-MZNH54NB.js.map +1 -0
- package/dist/{chunk-AXJGNOSQ.js → chunk-XBRIUBK5.js} +2 -2
- package/dist/history-FS6CASR6.js +8 -0
- package/dist/index.cjs +2424 -585
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -4
- package/dist/index.d.ts +240 -4
- package/dist/index.js +298 -14
- package/dist/index.js.map +1 -1
- package/dist/{session-34VFUDZB.js → session-W73HJB5Q.js} +4 -4
- package/dist/undici-NSB7IUB7.js +5 -0
- package/package.json +2 -1
- package/src/core/loop.ts +79 -25
- package/src/core/model/models.ts +69 -0
- package/src/core/model/providers.ts +76 -37
- package/src/core/model/resolution.ts +13 -0
- package/src/index.ts +12 -0
- package/src/mcp/mcp.ts +4 -1
- package/src/skill/bundled.ts +225 -0
- package/src/skill/skill.ts +278 -7
- package/src/tools/tool.ts +14 -4
- package/src/tools/tools/skill.ts +86 -8
- package/src/utils/messageNormalization.ts +18 -0
- package/dist/chunk-SXDGT4YB.js.map +0 -1
- package/dist/chunk-XKZSVWRX.js.map +0 -1
- package/dist/history-3JS745YJ.js +0 -8
- package/dist/undici-DJO5UB2C.js +0 -5
- /package/dist/{chunk-AXJGNOSQ.js.map → chunk-XBRIUBK5.js.map} +0 -0
- /package/dist/{history-3JS745YJ.js.map → history-FS6CASR6.js.map} +0 -0
- /package/dist/{session-34VFUDZB.js.map → session-W73HJB5Q.js.map} +0 -0
- /package/dist/{undici-DJO5UB2C.js.map → undici-NSB7IUB7.js.map} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
import type { SkillMetadata, SkillSource } from './skill';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bundled skill definition for TypeScript-encoded skills.
|
|
6
|
+
* This allows skills to be defined programmatically with dynamic behavior.
|
|
7
|
+
*/
|
|
8
|
+
export interface BundledSkillDefinition {
|
|
9
|
+
/** Unique skill name */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Skill description */
|
|
12
|
+
description: string;
|
|
13
|
+
/**
|
|
14
|
+
* Tools that this skill is allowed to use.
|
|
15
|
+
* If specified, only these tools will be available when the skill executes.
|
|
16
|
+
*/
|
|
17
|
+
allowedTools?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Execution context for the skill.
|
|
20
|
+
* - 'inline': Skill content is injected into current conversation (default)
|
|
21
|
+
* - 'fork': Skill runs in an isolated sub-agent
|
|
22
|
+
*/
|
|
23
|
+
context?: 'inline' | 'fork';
|
|
24
|
+
/**
|
|
25
|
+
* Agent type to use when context is 'fork'.
|
|
26
|
+
* If not specified, defaults to 'general-purpose'.
|
|
27
|
+
*/
|
|
28
|
+
agent?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this skill can be invoked by users via slash commands.
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
userInvocable?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether this skill can be invoked by the model via SkillTool.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
modelInvocable?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Aliases for the skill name.
|
|
41
|
+
* Allows the skill to be invoked by alternative names.
|
|
42
|
+
*/
|
|
43
|
+
aliases?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* When to use this skill.
|
|
46
|
+
* Provides guidance to the model about when to invoke this skill.
|
|
47
|
+
*/
|
|
48
|
+
whenToUse?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Hint for arguments that can be passed to the skill.
|
|
51
|
+
* Example: '[file_path] [issue_description]'
|
|
52
|
+
*/
|
|
53
|
+
argumentHint?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Function to determine if this skill is enabled in the current context.
|
|
56
|
+
* Can be a boolean or a function that receives the context.
|
|
57
|
+
*/
|
|
58
|
+
isEnabled?: boolean | ((context: Context) => boolean);
|
|
59
|
+
/**
|
|
60
|
+
* Embedded reference files that will be extracted when the skill runs.
|
|
61
|
+
* Key is the filename, value is the file content.
|
|
62
|
+
*/
|
|
63
|
+
files?: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Generate the skill prompt content.
|
|
66
|
+
* This function is called when the skill is invoked.
|
|
67
|
+
* @param args Arguments passed to the skill
|
|
68
|
+
* @param context The execution context
|
|
69
|
+
* @returns Promise resolving to the skill prompt content
|
|
70
|
+
*/
|
|
71
|
+
getPrompt: (args: string, context: Context) => Promise<string> | string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Registry for bundled skills.
|
|
76
|
+
* Skills registered here are built into the engine binary.
|
|
77
|
+
*/
|
|
78
|
+
class BundledSkillRegistry {
|
|
79
|
+
private skills: Map<string, BundledSkillDefinition> = new Map();
|
|
80
|
+
private aliases: Map<string, string> = new Map();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register a bundled skill.
|
|
84
|
+
* @param definition The skill definition
|
|
85
|
+
*/
|
|
86
|
+
register(definition: BundledSkillDefinition): void {
|
|
87
|
+
if (!definition.name) {
|
|
88
|
+
throw new Error('Bundled skill must have a name');
|
|
89
|
+
}
|
|
90
|
+
if (!definition.description) {
|
|
91
|
+
throw new Error('Bundled skill must have a description');
|
|
92
|
+
}
|
|
93
|
+
if (!definition.getPrompt) {
|
|
94
|
+
throw new Error('Bundled skill must have a getPrompt function');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.skills.set(definition.name, definition);
|
|
98
|
+
|
|
99
|
+
// Register aliases
|
|
100
|
+
if (definition.aliases) {
|
|
101
|
+
for (const alias of definition.aliases) {
|
|
102
|
+
this.aliases.set(alias, definition.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get a bundled skill by name or alias.
|
|
109
|
+
* @param name Skill name or alias
|
|
110
|
+
* @returns The skill definition or undefined if not found
|
|
111
|
+
*/
|
|
112
|
+
get(name: string): BundledSkillDefinition | undefined {
|
|
113
|
+
// Check direct name first
|
|
114
|
+
const skill = this.skills.get(name);
|
|
115
|
+
if (skill) return skill;
|
|
116
|
+
|
|
117
|
+
// Check aliases
|
|
118
|
+
const aliasedName = this.aliases.get(name);
|
|
119
|
+
if (aliasedName) {
|
|
120
|
+
return this.skills.get(aliasedName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all registered bundled skills.
|
|
128
|
+
* @param context Optional context to filter by isEnabled
|
|
129
|
+
* @returns Array of skill definitions
|
|
130
|
+
*/
|
|
131
|
+
getAll(context?: Context): BundledSkillDefinition[] {
|
|
132
|
+
return Array.from(this.skills.values()).filter((skill) => {
|
|
133
|
+
if (skill.isEnabled === undefined) return true;
|
|
134
|
+
if (typeof skill.isEnabled === 'boolean') return skill.isEnabled;
|
|
135
|
+
if (typeof skill.isEnabled === 'function' && context) {
|
|
136
|
+
return skill.isEnabled(context);
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a skill is registered.
|
|
144
|
+
* @param name Skill name or alias
|
|
145
|
+
*/
|
|
146
|
+
has(name: string): boolean {
|
|
147
|
+
return this.skills.has(name) || this.aliases.has(name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Unregister a skill.
|
|
152
|
+
* @param name Skill name
|
|
153
|
+
*/
|
|
154
|
+
unregister(name: string): void {
|
|
155
|
+
const skill = this.skills.get(name);
|
|
156
|
+
if (skill && skill.aliases) {
|
|
157
|
+
for (const alias of skill.aliases) {
|
|
158
|
+
this.aliases.delete(alias);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.skills.delete(name);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clear all registered skills.
|
|
166
|
+
*/
|
|
167
|
+
clear(): void {
|
|
168
|
+
this.skills.clear();
|
|
169
|
+
this.aliases.clear();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Global registry instance
|
|
174
|
+
export const bundledSkillRegistry = new BundledSkillRegistry();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Convert a bundled skill definition to SkillMetadata format.
|
|
178
|
+
* This is used internally when loading bundled skills into the SkillManager.
|
|
179
|
+
*/
|
|
180
|
+
export function bundledSkillToMetadata(
|
|
181
|
+
definition: BundledSkillDefinition,
|
|
182
|
+
source: SkillSource = 'builtin' as SkillSource,
|
|
183
|
+
): SkillMetadata {
|
|
184
|
+
return {
|
|
185
|
+
name: definition.name,
|
|
186
|
+
description: definition.description,
|
|
187
|
+
path: `bundled://${definition.name}`,
|
|
188
|
+
source,
|
|
189
|
+
allowedTools: definition.allowedTools,
|
|
190
|
+
context: definition.context,
|
|
191
|
+
agent: definition.agent,
|
|
192
|
+
userInvocable: definition.userInvocable ?? true,
|
|
193
|
+
modelInvocable: definition.modelInvocable ?? true,
|
|
194
|
+
// Note: bundled skills don't support 'paths' conditional activation
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Helper function to create a simple bundled skill.
|
|
200
|
+
*/
|
|
201
|
+
export function createBundledSkill(
|
|
202
|
+
config: Omit<BundledSkillDefinition, 'getPrompt'> & {
|
|
203
|
+
prompt: string | ((args: string, context: Context) => Promise<string> | string);
|
|
204
|
+
},
|
|
205
|
+
): BundledSkillDefinition {
|
|
206
|
+
return {
|
|
207
|
+
...config,
|
|
208
|
+
getPrompt:
|
|
209
|
+
typeof config.prompt === 'string'
|
|
210
|
+
? () => config.prompt as string
|
|
211
|
+
: config.prompt,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Register a bundled skill using a simplified configuration.
|
|
217
|
+
*/
|
|
218
|
+
export function registerBundledSkill(
|
|
219
|
+
config: Omit<BundledSkillDefinition, 'getPrompt'> & {
|
|
220
|
+
prompt: string | ((args: string, context: Context) => Promise<string> | string);
|
|
221
|
+
},
|
|
222
|
+
): void {
|
|
223
|
+
const skill = createBundledSkill(config);
|
|
224
|
+
bundledSkillRegistry.register(skill);
|
|
225
|
+
}
|
package/src/skill/skill.ts
CHANGED
|
@@ -6,6 +6,26 @@ import type { Context } from '../core/context';
|
|
|
6
6
|
import type { Paths } from '../core/paths';
|
|
7
7
|
import { PluginHookType } from '../core/plugin';
|
|
8
8
|
import { safeFrontMatter } from '../utils/safeFrontMatter';
|
|
9
|
+
import {
|
|
10
|
+
bundledSkillRegistry,
|
|
11
|
+
bundledSkillToMetadata,
|
|
12
|
+
type BundledSkillDefinition,
|
|
13
|
+
} from './bundled';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a directory entry is a directory or a symlink pointing to a directory.
|
|
17
|
+
*/
|
|
18
|
+
function isDirOrSymlinkToDir(parentDir: string, entry: fs.Dirent): boolean {
|
|
19
|
+
if (entry.isDirectory()) return true;
|
|
20
|
+
if (entry.isSymbolicLink()) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.statSync(path.join(parentDir, entry.name)).isDirectory();
|
|
23
|
+
} catch {
|
|
24
|
+
// broken symlink, skip
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
export enum SkillSource {
|
|
11
31
|
Plugin = 'plugin',
|
|
@@ -13,13 +33,48 @@ export enum SkillSource {
|
|
|
13
33
|
Global = 'global',
|
|
14
34
|
ProjectClaude = 'project-claude',
|
|
15
35
|
Project = 'project',
|
|
36
|
+
Builtin = 'builtin',
|
|
16
37
|
}
|
|
17
38
|
|
|
39
|
+
export type SkillContext = 'inline' | 'fork';
|
|
40
|
+
|
|
18
41
|
export interface SkillMetadata {
|
|
19
42
|
name: string;
|
|
20
43
|
description: string;
|
|
21
44
|
path: string;
|
|
22
45
|
source: SkillSource;
|
|
46
|
+
/**
|
|
47
|
+
* Tools that this skill is allowed to use.
|
|
48
|
+
* If specified, only these tools will be available when the skill executes.
|
|
49
|
+
* Example: ['Read', 'Grep', 'Bash']
|
|
50
|
+
*/
|
|
51
|
+
allowedTools?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Execution context for the skill.
|
|
54
|
+
* - 'inline': Skill content is injected into current conversation (default)
|
|
55
|
+
* - 'fork': Skill runs in an isolated sub-agent
|
|
56
|
+
*/
|
|
57
|
+
context?: SkillContext;
|
|
58
|
+
/**
|
|
59
|
+
* Agent type to use when context is 'fork'.
|
|
60
|
+
* If not specified, defaults to a general-purpose agent.
|
|
61
|
+
*/
|
|
62
|
+
agent?: string;
|
|
63
|
+
/**
|
|
64
|
+
* File path patterns that trigger this skill's availability.
|
|
65
|
+
* Example: ['src/asterisk/asterisk.ts', '*.test.js']
|
|
66
|
+
*/
|
|
67
|
+
paths?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Whether this skill can be invoked by users via slash commands.
|
|
70
|
+
* @default true
|
|
71
|
+
*/
|
|
72
|
+
userInvocable?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Whether this skill can be invoked by the model via SkillTool.
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
77
|
+
modelInvocable?: boolean;
|
|
23
78
|
}
|
|
24
79
|
|
|
25
80
|
export interface SkillError {
|
|
@@ -71,26 +126,149 @@ export class SkillManager {
|
|
|
71
126
|
private errors: SkillError[] = [];
|
|
72
127
|
private paths: Paths;
|
|
73
128
|
private context: Context;
|
|
129
|
+
/**
|
|
130
|
+
* Tracks which skills are currently "active" based on file operations.
|
|
131
|
+
* Skills with `paths` frontmatter are activated when matching files are accessed.
|
|
132
|
+
*/
|
|
133
|
+
private activeSkillNames: Set<string> = new Set();
|
|
134
|
+
/**
|
|
135
|
+
* Maps alias names to original skill names for bundled skills.
|
|
136
|
+
*/
|
|
137
|
+
private aliasMap: Map<string, string> = new Map();
|
|
74
138
|
|
|
75
139
|
constructor(opts: SkillManagerOpts) {
|
|
76
140
|
this.context = opts.context;
|
|
77
141
|
this.paths = opts.context.paths;
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Get all loaded skills.
|
|
146
|
+
* @param options Filter options
|
|
147
|
+
* @returns Array of skill metadata
|
|
148
|
+
*/
|
|
149
|
+
getSkills(options?: {
|
|
150
|
+
/** Only return skills that are user-invocable */
|
|
151
|
+
userInvocable?: boolean;
|
|
152
|
+
/** Only return skills that are model-invocable */
|
|
153
|
+
modelInvocable?: boolean;
|
|
154
|
+
/** Only return active skills (those with matching paths) */
|
|
155
|
+
activeOnly?: boolean;
|
|
156
|
+
}): SkillMetadata[] {
|
|
157
|
+
let skills = Array.from(this.skillsMap.values());
|
|
158
|
+
|
|
159
|
+
if (options?.userInvocable !== undefined) {
|
|
160
|
+
skills = skills.filter(
|
|
161
|
+
(s) => (s.userInvocable ?? true) === options.userInvocable,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options?.modelInvocable !== undefined) {
|
|
166
|
+
skills = skills.filter(
|
|
167
|
+
(s) => (s.modelInvocable ?? true) === options.modelInvocable,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options?.activeOnly) {
|
|
172
|
+
skills = skills.filter((s) => this.activeSkillNames.has(s.name));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return skills;
|
|
82
176
|
}
|
|
83
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Get a specific skill by name.
|
|
180
|
+
* Automatically resolves aliases to the original skill.
|
|
181
|
+
* @param name Skill name or alias
|
|
182
|
+
* @returns Skill metadata or undefined if not found
|
|
183
|
+
*/
|
|
84
184
|
getSkill(name: string): SkillMetadata | undefined {
|
|
85
|
-
|
|
185
|
+
// Check direct name first
|
|
186
|
+
const skill = this.skillsMap.get(name);
|
|
187
|
+
if (skill) return skill;
|
|
188
|
+
|
|
189
|
+
// Check if it's an alias
|
|
190
|
+
const originalName = this.aliasMap.get(name);
|
|
191
|
+
if (originalName) {
|
|
192
|
+
return this.skillsMap.get(originalName);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if a skill is active (has matching paths for current context).
|
|
200
|
+
* @param name Skill name
|
|
201
|
+
*/
|
|
202
|
+
isSkillActive(name: string): boolean {
|
|
203
|
+
return this.activeSkillNames.has(name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Activate skills based on file paths.
|
|
208
|
+
* Skills with `paths` frontmatter that match the given paths will be activated.
|
|
209
|
+
* @param filePaths Array of file paths to check
|
|
210
|
+
* @returns Array of newly activated skill names
|
|
211
|
+
*/
|
|
212
|
+
activateSkillsForPaths(filePaths: string[]): string[] {
|
|
213
|
+
const newlyActivated: string[] = [];
|
|
214
|
+
const minimatch = require('minimatch');
|
|
215
|
+
|
|
216
|
+
for (const [name, skill] of this.skillsMap) {
|
|
217
|
+
if (skill.paths && skill.paths.length > 0) {
|
|
218
|
+
const isMatch = skill.paths.some((pattern) =>
|
|
219
|
+
filePaths.some((filePath) => minimatch(filePath, pattern)),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (isMatch && !this.activeSkillNames.has(name)) {
|
|
223
|
+
this.activeSkillNames.add(name);
|
|
224
|
+
newlyActivated.push(name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return newlyActivated;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Clear all active skill activations.
|
|
234
|
+
* Useful when switching contexts or projects.
|
|
235
|
+
*/
|
|
236
|
+
clearActiveSkills(): void {
|
|
237
|
+
this.activeSkillNames.clear();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get all skills that have path-based conditional activation.
|
|
242
|
+
* @returns Array of skills with paths defined
|
|
243
|
+
*/
|
|
244
|
+
getConditionalSkills(): SkillMetadata[] {
|
|
245
|
+
return Array.from(this.skillsMap.values()).filter(
|
|
246
|
+
(s) => s.paths && s.paths.length > 0,
|
|
247
|
+
);
|
|
86
248
|
}
|
|
87
249
|
|
|
88
250
|
getErrors(): SkillError[] {
|
|
89
251
|
return this.errors;
|
|
90
252
|
}
|
|
91
253
|
|
|
92
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Read the body content of a skill.
|
|
256
|
+
* For file-based skills, reads from disk.
|
|
257
|
+
* For bundled skills, generates content via getPrompt.
|
|
258
|
+
*/
|
|
259
|
+
async readSkillBody(skill: SkillMetadata, args: string = ''): Promise<string> {
|
|
93
260
|
try {
|
|
261
|
+
// Handle bundled skills
|
|
262
|
+
if (skill.path.startsWith('bundled://')) {
|
|
263
|
+
const bundledName = skill.path.replace('bundled://', '');
|
|
264
|
+
const bundledSkill = bundledSkillRegistry.get(bundledName);
|
|
265
|
+
if (!bundledSkill) {
|
|
266
|
+
throw new Error(`Bundled skill "${bundledName}" not found in registry`);
|
|
267
|
+
}
|
|
268
|
+
return await bundledSkill.getPrompt(args, this.context);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle file-based skills
|
|
94
272
|
const content = fs.readFileSync(skill.path, 'utf-8');
|
|
95
273
|
const { body } = safeFrontMatter(content, skill.path);
|
|
96
274
|
return body;
|
|
@@ -101,10 +279,32 @@ export class SkillManager {
|
|
|
101
279
|
}
|
|
102
280
|
}
|
|
103
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Get a bundled skill definition by name.
|
|
284
|
+
* @param name Bundled skill name
|
|
285
|
+
* @returns Bundled skill definition or undefined if not found
|
|
286
|
+
*/
|
|
287
|
+
getBundledSkill(name: string): BundledSkillDefinition | undefined {
|
|
288
|
+
return bundledSkillRegistry.get(name);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Register a bundled skill programmatically.
|
|
293
|
+
* @param definition Bundled skill definition
|
|
294
|
+
*/
|
|
295
|
+
registerBundledSkill(definition: BundledSkillDefinition): void {
|
|
296
|
+
bundledSkillRegistry.register(definition);
|
|
297
|
+
// Reload to include the new skill
|
|
298
|
+
this.loadBuiltinSkills();
|
|
299
|
+
}
|
|
300
|
+
|
|
104
301
|
async loadSkills(): Promise<void> {
|
|
105
302
|
this.skillsMap.clear();
|
|
106
303
|
this.errors = [];
|
|
107
304
|
|
|
305
|
+
// Load builtin/bundled skills first
|
|
306
|
+
this.loadBuiltinSkills();
|
|
307
|
+
|
|
108
308
|
const pluginSkills = await this.context.apply({
|
|
109
309
|
hook: 'skill',
|
|
110
310
|
args: [],
|
|
@@ -153,6 +353,27 @@ export class SkillManager {
|
|
|
153
353
|
this.loadSkillsFromDirectory(projectDir, SkillSource.Project);
|
|
154
354
|
}
|
|
155
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Load builtin/bundled skills from the registry.
|
|
358
|
+
*/
|
|
359
|
+
private loadBuiltinSkills(): void {
|
|
360
|
+
// Clear alias map when reloading
|
|
361
|
+
this.aliasMap.clear();
|
|
362
|
+
|
|
363
|
+
const bundledSkills = bundledSkillRegistry.getAll(this.context);
|
|
364
|
+
for (const skill of bundledSkills) {
|
|
365
|
+
const metadata = bundledSkillToMetadata(skill, SkillSource.Builtin);
|
|
366
|
+
this.skillsMap.set(skill.name, metadata);
|
|
367
|
+
|
|
368
|
+
// Register aliases in the alias map
|
|
369
|
+
if (skill.aliases) {
|
|
370
|
+
for (const alias of skill.aliases) {
|
|
371
|
+
this.aliasMap.set(alias, skill.name);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
156
377
|
private loadSkillsFromDirectory(
|
|
157
378
|
skillsDir: string,
|
|
158
379
|
source: SkillSource,
|
|
@@ -165,7 +386,7 @@ export class SkillManager {
|
|
|
165
386
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
166
387
|
|
|
167
388
|
for (const entry of entries) {
|
|
168
|
-
if (entry
|
|
389
|
+
if (isDirOrSymlinkToDir(skillsDir, entry)) {
|
|
169
390
|
const skillPath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
170
391
|
if (fs.existsSync(skillPath)) {
|
|
171
392
|
this.loadSkillFile(skillPath, source);
|
|
@@ -210,6 +431,12 @@ export class SkillManager {
|
|
|
210
431
|
const { attributes } = safeFrontMatter<{
|
|
211
432
|
name?: string;
|
|
212
433
|
description?: string;
|
|
434
|
+
allowedTools?: string | string[];
|
|
435
|
+
context?: string;
|
|
436
|
+
agent?: string;
|
|
437
|
+
paths?: string | string[];
|
|
438
|
+
userInvocable?: boolean;
|
|
439
|
+
modelInvocable?: boolean;
|
|
213
440
|
}>(content, skillPath);
|
|
214
441
|
|
|
215
442
|
if (!attributes.name) {
|
|
@@ -252,10 +479,28 @@ export class SkillManager {
|
|
|
252
479
|
return null;
|
|
253
480
|
}
|
|
254
481
|
|
|
482
|
+
// Parse allowedTools - supports both string and array formats
|
|
483
|
+
const allowedTools = this.parseStringArrayField(attributes.allowedTools);
|
|
484
|
+
|
|
485
|
+
// Parse context (inline or fork)
|
|
486
|
+
const context: SkillContext | undefined =
|
|
487
|
+
attributes.context === 'inline' || attributes.context === 'fork'
|
|
488
|
+
? attributes.context
|
|
489
|
+
: undefined;
|
|
490
|
+
|
|
491
|
+
// Parse paths - supports both string and array formats
|
|
492
|
+
const paths = this.parseStringArrayField(attributes.paths);
|
|
493
|
+
|
|
255
494
|
return {
|
|
256
495
|
name: attributes.name,
|
|
257
496
|
description: attributes.description,
|
|
258
497
|
path: skillPath,
|
|
498
|
+
allowedTools,
|
|
499
|
+
context,
|
|
500
|
+
agent: attributes.agent,
|
|
501
|
+
paths,
|
|
502
|
+
userInvocable: attributes.userInvocable ?? true,
|
|
503
|
+
modelInvocable: attributes.modelInvocable ?? true,
|
|
259
504
|
};
|
|
260
505
|
} catch (error) {
|
|
261
506
|
this.errors.push({
|
|
@@ -269,6 +514,32 @@ export class SkillManager {
|
|
|
269
514
|
}
|
|
270
515
|
}
|
|
271
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Parse a field that can be either a comma-separated string or an array of strings.
|
|
519
|
+
* @param value The value to parse
|
|
520
|
+
* @returns Array of strings or undefined if empty
|
|
521
|
+
*/
|
|
522
|
+
private parseStringArrayField(
|
|
523
|
+
value: string | string[] | undefined,
|
|
524
|
+
): string[] | undefined {
|
|
525
|
+
if (!value) return undefined;
|
|
526
|
+
|
|
527
|
+
if (Array.isArray(value)) {
|
|
528
|
+
return value
|
|
529
|
+
.map((item) => item.trim())
|
|
530
|
+
.filter((item) => item.length > 0);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (typeof value === 'string') {
|
|
534
|
+
return value
|
|
535
|
+
.split(',')
|
|
536
|
+
.map((item) => item.trim())
|
|
537
|
+
.filter((item) => item.length > 0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
272
543
|
async addSkill(
|
|
273
544
|
source: string,
|
|
274
545
|
options: AddSkillOptions = {},
|
|
@@ -457,7 +728,7 @@ export class SkillManager {
|
|
|
457
728
|
if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
|
|
458
729
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
459
730
|
for (const entry of entries) {
|
|
460
|
-
if (entry
|
|
731
|
+
if (isDirOrSymlinkToDir(skillsDir, entry)) {
|
|
461
732
|
const skillPath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
462
733
|
if (fs.existsSync(skillPath)) {
|
|
463
734
|
skills.push(skillPath);
|
|
@@ -471,7 +742,7 @@ export class SkillManager {
|
|
|
471
742
|
|
|
472
743
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
473
744
|
for (const entry of entries) {
|
|
474
|
-
if (entry
|
|
745
|
+
if (isDirOrSymlinkToDir(dir, entry)) {
|
|
475
746
|
const skillPath = path.join(dir, entry.name, 'SKILL.md');
|
|
476
747
|
if (fs.existsSync(skillPath)) {
|
|
477
748
|
skills.push(skillPath);
|
package/src/tools/tool.ts
CHANGED
|
@@ -51,9 +51,6 @@ export async function resolveTools(opts: ResolveToolsOpts) {
|
|
|
51
51
|
createGlobTool({ cwd }),
|
|
52
52
|
createGrepTool({ cwd }),
|
|
53
53
|
createFetchTool({ model, fetch: opts.context.fetch }),
|
|
54
|
-
...(hasSkills
|
|
55
|
-
? [createSkillTool({ skillManager: opts.context.skillManager! })]
|
|
56
|
-
: []),
|
|
57
54
|
];
|
|
58
55
|
const askUserQuestionTools = opts.askUserQuestion
|
|
59
56
|
? [createAskUserQuestionTool()]
|
|
@@ -90,7 +87,7 @@ export async function resolveTools(opts: ResolveToolsOpts) {
|
|
|
90
87
|
|
|
91
88
|
const mcpTools = await getMcpTools(opts.context);
|
|
92
89
|
|
|
93
|
-
|
|
90
|
+
let allTools = [
|
|
94
91
|
...readonlyTools,
|
|
95
92
|
...askUserQuestionTools,
|
|
96
93
|
...writeTools,
|
|
@@ -99,6 +96,19 @@ export async function resolveTools(opts: ResolveToolsOpts) {
|
|
|
99
96
|
...mcpTools,
|
|
100
97
|
];
|
|
101
98
|
|
|
99
|
+
// Add skill tool if skills are available
|
|
100
|
+
// Note: skill tool is added after initial tool list so it can reference allTools for fork execution
|
|
101
|
+
if (hasSkills) {
|
|
102
|
+
const skillTool = createSkillTool({
|
|
103
|
+
skillManager: opts.context.skillManager!,
|
|
104
|
+
context: opts.context,
|
|
105
|
+
tools: allTools,
|
|
106
|
+
sessionId: opts.sessionId,
|
|
107
|
+
signal: opts.signal,
|
|
108
|
+
});
|
|
109
|
+
allTools = [...allTools, skillTool];
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
// 1. First, execute plugin hook to allow plugins to add/modify tools
|
|
103
113
|
let availableTools = allTools;
|
|
104
114
|
try {
|