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.
Files changed (34) hide show
  1. package/dist/{chunk-SXDGT4YB.js → chunk-D5X6YFSK.js} +1814 -457
  2. package/dist/chunk-D5X6YFSK.js.map +1 -0
  3. package/dist/{chunk-XKZSVWRX.js → chunk-MZNH54NB.js} +375 -171
  4. package/dist/chunk-MZNH54NB.js.map +1 -0
  5. package/dist/{chunk-AXJGNOSQ.js → chunk-XBRIUBK5.js} +2 -2
  6. package/dist/history-FS6CASR6.js +8 -0
  7. package/dist/index.cjs +2424 -585
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +240 -4
  10. package/dist/index.d.ts +240 -4
  11. package/dist/index.js +298 -14
  12. package/dist/index.js.map +1 -1
  13. package/dist/{session-34VFUDZB.js → session-W73HJB5Q.js} +4 -4
  14. package/dist/undici-NSB7IUB7.js +5 -0
  15. package/package.json +2 -1
  16. package/src/core/loop.ts +79 -25
  17. package/src/core/model/models.ts +69 -0
  18. package/src/core/model/providers.ts +76 -37
  19. package/src/core/model/resolution.ts +13 -0
  20. package/src/index.ts +12 -0
  21. package/src/mcp/mcp.ts +4 -1
  22. package/src/skill/bundled.ts +225 -0
  23. package/src/skill/skill.ts +278 -7
  24. package/src/tools/tool.ts +14 -4
  25. package/src/tools/tools/skill.ts +86 -8
  26. package/src/utils/messageNormalization.ts +18 -0
  27. package/dist/chunk-SXDGT4YB.js.map +0 -1
  28. package/dist/chunk-XKZSVWRX.js.map +0 -1
  29. package/dist/history-3JS745YJ.js +0 -8
  30. package/dist/undici-DJO5UB2C.js +0 -5
  31. /package/dist/{chunk-AXJGNOSQ.js.map → chunk-XBRIUBK5.js.map} +0 -0
  32. /package/dist/{history-3JS745YJ.js.map → history-FS6CASR6.js.map} +0 -0
  33. /package/dist/{session-34VFUDZB.js.map → session-W73HJB5Q.js.map} +0 -0
  34. /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
+ }
@@ -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
- getSkills(): SkillMetadata[] {
81
- return Array.from(this.skillsMap.values());
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
- return this.skillsMap.get(name);
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
- async readSkillBody(skill: SkillMetadata): Promise<string> {
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.isDirectory()) {
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.isDirectory()) {
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.isDirectory()) {
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
- const allTools = [
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 {