-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathBundledSkillLoader.ts
More file actions
118 lines (106 loc) · 3.71 KB
/
Copy pathBundledSkillLoader.ts
File metadata and controls
118 lines (106 loc) · 3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '@qwen-code/qwen-code-core';
import {
createDebugLogger,
appendToLastTextPart,
buildSkillLlmContent,
applySkillAllowedTools,
} from '@qwen-code/qwen-code-core';
import { dirname } from 'node:path';
import type { ICommandLoader } from './types.js';
import type {
SlashCommand,
SlashCommandActionReturn,
} from '../ui/commands/types.js';
import { CommandKind } from '../ui/commands/types.js';
import { t } from '../i18n/index.js';
const debugLogger = createDebugLogger('BUNDLED_SKILL_LOADER');
/**
* Loads bundled skills as slash commands, making them directly invocable
* via /<skill-name> (e.g., /review).
*/
export class BundledSkillLoader implements ICommandLoader {
constructor(private readonly config: Config | null) {}
async loadCommands(_signal: AbortSignal): Promise<SlashCommand[]> {
if (this.config?.getBareMode?.()) {
debugLogger.debug('Bare mode enabled, skipping bundled skills');
return [];
}
const skillManager = this.config?.getSkillManager();
if (!skillManager) {
debugLogger.debug('SkillManager not available, skipping bundled skills');
return [];
}
try {
const allSkills = await skillManager.listSkills({ level: 'bundled' });
// Hide skills whose allowedTools require cron when cron is disabled
const cronEnabled = this.config?.isCronEnabled() ?? false;
const skills = allSkills.filter((skill) => {
if (
!cronEnabled &&
skill.allowedTools?.some((t) => t.startsWith('cron_'))
) {
debugLogger.debug(
`Hiding skill "${skill.name}" because cron is not enabled`,
);
return false;
}
return true;
});
debugLogger.debug(
`Loaded ${skills.length} bundled skill(s) as slash commands`,
);
return skills.map((skill) => ({
name: skill.name,
description: skill.description,
modelDescription: skill.description,
kind: CommandKind.SKILL,
source: 'bundled-skill' as const,
sourceLabel: t('Skill'),
modelInvocable: !skill.disableModelInvocation,
argumentHint: skill.argumentHint,
whenToUse: skill.whenToUse,
action: async (context, _args): Promise<SlashCommandActionReturn> => {
// Auto-approve the skill's declared allowedTools before its body is submitted.
applySkillAllowedTools(
this.config?.getPermissionManager(),
skill.allowedTools,
);
// Resolve template variables in skill body
let body = skill.body;
const modelId = this.config?.getModel()?.trim() || '';
if (body.includes('{{model}}') || body.includes('YOUR_MODEL_ID')) {
body = body.replaceAll('{{model}}', modelId);
body = body.replaceAll('YOUR_MODEL_ID', modelId);
// Prepend model identity as a top-level declaration so the LLM
// cannot miss it even if it doesn't copy the template exactly.
if (modelId) {
body = `YOUR_MODEL_ID="${modelId}"\n\n${body}`;
}
}
const skillPrompt = buildSkillLlmContent(
dirname(skill.filePath),
body,
);
const content = context.invocation?.args
? appendToLastTextPart(
[{ text: skillPrompt }],
context.invocation.raw,
)
: [{ text: skillPrompt }];
return {
type: 'submit_prompt',
content,
};
},
}));
} catch (error) {
debugLogger.error('Failed to load bundled skills:', error);
return [];
}
}
}