onbuzz 4.8.0 → 4.8.2
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/package.json +1 -1
- package/src/core/__tests__/agentPool.test.js +185 -0
- package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -0
- package/src/core/__tests__/agentScheduler.taskListInjection.test.js +94 -0
- package/src/core/agentPool.js +319 -0
- package/src/core/agentScheduler.js +216 -2
- package/src/services/__tests__/conversationCompactionService.test.js +141 -0
- package/src/services/__tests__/modelRouterNaming.test.js +41 -23
- package/src/services/conversationCompactionService.js +120 -46
- package/src/tools/__tests__/baseTool.test.js +171 -0
- package/src/tools/__tests__/codeMapTool.test.js +179 -0
- package/src/tools/__tests__/taskManagerTool.test.js +141 -0
- package/src/tools/baseTool.js +89 -1
- package/src/tools/openaiFunctionSchemas.js +14 -0
- package/src/tools/skillsTool.js +282 -277
- package/src/tools/taskManagerTool.js +72 -2
- package/src/utilities/constants.js +19 -1
package/src/tools/skillsTool.js
CHANGED
|
@@ -1,277 +1,282 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills Tool - Global skills library for agents
|
|
3
|
-
*
|
|
4
|
-
* Purpose:
|
|
5
|
-
* - Allow agents to discover, browse, and read reusable skill instructions
|
|
6
|
-
* - Support progressive disclosure: list → describe → read-section → read
|
|
7
|
-
* - Support CRUD operations and importing skills from disk
|
|
8
|
-
* - Skills are global (shared across agents) and persist across package updates
|
|
9
|
-
*
|
|
10
|
-
* Actions:
|
|
11
|
-
* - list: List all skills with descriptions, section headings, and sizes
|
|
12
|
-
* - describe: Get full metadata for a skill without loading content
|
|
13
|
-
* - read: Read a skill's full content
|
|
14
|
-
* - read-section: Read only a specific section of a skill
|
|
15
|
-
* - read-file: Read a supporting file from a skill directory
|
|
16
|
-
* - create: Create a new skill
|
|
17
|
-
* - update: Update an existing skill
|
|
18
|
-
* - delete: Remove a skill
|
|
19
|
-
* - import: Import a skill from an external file or directory
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { BaseTool } from './baseTool.js';
|
|
23
|
-
import { getSkillsService } from '../services/skillsService.js';
|
|
24
|
-
import { SKILLS_ACTIONS } from '../utilities/toolConstants.js';
|
|
25
|
-
|
|
26
|
-
class SkillsTool extends BaseTool {
|
|
27
|
-
constructor(config = {}, logger = null) {
|
|
28
|
-
super(config, logger);
|
|
29
|
-
|
|
30
|
-
this.skillsService = null;
|
|
31
|
-
this.requiresProject = false;
|
|
32
|
-
this.isAsync = false;
|
|
33
|
-
this.timeout = 30000;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async _ensureSkillsService() {
|
|
37
|
-
if (!this.skillsService) {
|
|
38
|
-
this.skillsService = getSkillsService(this.logger);
|
|
39
|
-
await this.skillsService.initialize();
|
|
40
|
-
}
|
|
41
|
-
return this.skillsService;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getDescription() {
|
|
45
|
-
return `Skills Tool: Browse and manage a global library of reusable skill instructions.
|
|
46
|
-
|
|
47
|
-
Skills are structured knowledge packages containing instructions, checklists, templates, and reference files.
|
|
48
|
-
Each skill is a directory with a skill.md file and optional supporting files.
|
|
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
|
-
\`\`\`json
|
|
108
|
-
{ "toolId": "skills", "action": "
|
|
109
|
-
\`\`\`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
type: 'string',
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Skills Tool - Global skills library for agents
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Allow agents to discover, browse, and read reusable skill instructions
|
|
6
|
+
* - Support progressive disclosure: list → describe → read-section → read
|
|
7
|
+
* - Support CRUD operations and importing skills from disk
|
|
8
|
+
* - Skills are global (shared across agents) and persist across package updates
|
|
9
|
+
*
|
|
10
|
+
* Actions:
|
|
11
|
+
* - list: List all skills with descriptions, section headings, and sizes
|
|
12
|
+
* - describe: Get full metadata for a skill without loading content
|
|
13
|
+
* - read: Read a skill's full content
|
|
14
|
+
* - read-section: Read only a specific section of a skill
|
|
15
|
+
* - read-file: Read a supporting file from a skill directory
|
|
16
|
+
* - create: Create a new skill
|
|
17
|
+
* - update: Update an existing skill
|
|
18
|
+
* - delete: Remove a skill
|
|
19
|
+
* - import: Import a skill from an external file or directory
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { BaseTool } from './baseTool.js';
|
|
23
|
+
import { getSkillsService } from '../services/skillsService.js';
|
|
24
|
+
import { SKILLS_ACTIONS } from '../utilities/toolConstants.js';
|
|
25
|
+
|
|
26
|
+
class SkillsTool extends BaseTool {
|
|
27
|
+
constructor(config = {}, logger = null) {
|
|
28
|
+
super(config, logger);
|
|
29
|
+
|
|
30
|
+
this.skillsService = null;
|
|
31
|
+
this.requiresProject = false;
|
|
32
|
+
this.isAsync = false;
|
|
33
|
+
this.timeout = 30000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _ensureSkillsService() {
|
|
37
|
+
if (!this.skillsService) {
|
|
38
|
+
this.skillsService = getSkillsService(this.logger);
|
|
39
|
+
await this.skillsService.initialize();
|
|
40
|
+
}
|
|
41
|
+
return this.skillsService;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getDescription() {
|
|
45
|
+
return `Skills Tool: Browse and manage a global library of reusable skill instructions.
|
|
46
|
+
|
|
47
|
+
Skills are structured knowledge packages containing instructions, checklists, templates, and reference files.
|
|
48
|
+
Each skill is a directory with a skill.md file and optional supporting files.
|
|
49
|
+
|
|
50
|
+
★ PROACTIVE USE — When a new task arrives or your focus shifts, your FIRST move should be:
|
|
51
|
+
\`{ "toolId": "skills", "action": "list" }\`
|
|
52
|
+
Skim the names + descriptions. If anything looks relevant, "describe" it and follow its
|
|
53
|
+
checklist instead of improvising. The team curates skills specifically so you don't have
|
|
54
|
+
to re-derive recurring playbooks from scratch.
|
|
55
|
+
|
|
56
|
+
PROGRESSIVE DISCLOSURE — Use this flow to minimize context usage:
|
|
57
|
+
1. "list" → See all skills with section headings and sizes
|
|
58
|
+
2. "describe" → Inspect a specific skill's structure in detail
|
|
59
|
+
3. "read-section" → Load only the section you need
|
|
60
|
+
4. "read" → Load full content only when necessary
|
|
61
|
+
|
|
62
|
+
ACTIONS:
|
|
63
|
+
|
|
64
|
+
1. LIST all skills:
|
|
65
|
+
\`\`\`json
|
|
66
|
+
{ "toolId": "skills", "action": "list" }
|
|
67
|
+
\`\`\`
|
|
68
|
+
Returns: name, description, section headings, line count, file count for each skill.
|
|
69
|
+
|
|
70
|
+
2. DESCRIBE a skill (metadata only, no content):
|
|
71
|
+
\`\`\`json
|
|
72
|
+
{ "toolId": "skills", "action": "describe", "name": "code-review" }
|
|
73
|
+
\`\`\`
|
|
74
|
+
Returns: description, sections with line ranges, file list, size.
|
|
75
|
+
|
|
76
|
+
3. READ full skill content:
|
|
77
|
+
\`\`\`json
|
|
78
|
+
{ "toolId": "skills", "action": "read", "name": "code-review" }
|
|
79
|
+
\`\`\`
|
|
80
|
+
Returns: full skill.md content + list of files in the skill directory.
|
|
81
|
+
|
|
82
|
+
4. READ a specific SECTION:
|
|
83
|
+
\`\`\`json
|
|
84
|
+
{ "toolId": "skills", "action": "read-section", "name": "code-review", "section": "Checklist" }
|
|
85
|
+
\`\`\`
|
|
86
|
+
Returns: only the content under the specified ## heading.
|
|
87
|
+
|
|
88
|
+
5. READ a supporting FILE:
|
|
89
|
+
\`\`\`json
|
|
90
|
+
{ "toolId": "skills", "action": "read-file", "name": "email-templates", "file": "templates/welcome.html" }
|
|
91
|
+
\`\`\`
|
|
92
|
+
Returns: content of a specific file within the skill directory.
|
|
93
|
+
|
|
94
|
+
6. CREATE a new skill:
|
|
95
|
+
\`\`\`json
|
|
96
|
+
{ "toolId": "skills", "action": "create", "name": "my-skill", "content": "# My Skill\\n\\nInstructions here...\\n\\n## Section One\\n..." }
|
|
97
|
+
\`\`\`
|
|
98
|
+
Optional: "files" array of { "path": "relative/path.ext", "content": "..." } for supporting files.
|
|
99
|
+
|
|
100
|
+
7. UPDATE an existing skill:
|
|
101
|
+
\`\`\`json
|
|
102
|
+
{ "toolId": "skills", "action": "update", "name": "my-skill", "content": "# Updated content..." }
|
|
103
|
+
\`\`\`
|
|
104
|
+
Optional: "files" array to add/update supporting files.
|
|
105
|
+
|
|
106
|
+
8. DELETE a skill:
|
|
107
|
+
\`\`\`json
|
|
108
|
+
{ "toolId": "skills", "action": "delete", "name": "my-skill" }
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
9. IMPORT a skill from disk:
|
|
112
|
+
\`\`\`json
|
|
113
|
+
{ "toolId": "skills", "action": "import", "source": "/path/to/skill-dir-or-file" }
|
|
114
|
+
\`\`\`
|
|
115
|
+
Optional: "name" to override the derived skill name. If source is a directory, it must contain a skill.md file.
|
|
116
|
+
|
|
117
|
+
SKILL NAMING: Names must be kebab-case (lowercase, hyphens). Example: "code-review", "email-templates".`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parseParameters(content) {
|
|
121
|
+
return content;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getRequiredParameters() {
|
|
125
|
+
return ['action'];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getSupportedActions() {
|
|
129
|
+
return Object.values(SKILLS_ACTIONS);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
validateParameterTypes(params) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (params.action && typeof params.action !== 'string') {
|
|
135
|
+
errors.push('action must be a string');
|
|
136
|
+
}
|
|
137
|
+
if (params.name !== undefined && typeof params.name !== 'string') {
|
|
138
|
+
errors.push('name must be a string');
|
|
139
|
+
}
|
|
140
|
+
if (params.content !== undefined && typeof params.content !== 'string') {
|
|
141
|
+
errors.push('content must be a string');
|
|
142
|
+
}
|
|
143
|
+
if (params.section !== undefined && typeof params.section !== 'string') {
|
|
144
|
+
errors.push('section must be a string');
|
|
145
|
+
}
|
|
146
|
+
if (params.file !== undefined && typeof params.file !== 'string') {
|
|
147
|
+
errors.push('file must be a string');
|
|
148
|
+
}
|
|
149
|
+
if (params.source !== undefined && typeof params.source !== 'string') {
|
|
150
|
+
errors.push('source must be a string');
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
customValidateParameters(params) {
|
|
156
|
+
const errors = [];
|
|
157
|
+
const { action, name, content, section, file, source } = params;
|
|
158
|
+
|
|
159
|
+
const validActions = this.getSupportedActions();
|
|
160
|
+
if (!validActions.includes(action)) {
|
|
161
|
+
errors.push(`Invalid action: "${action}". Valid actions: ${validActions.join(', ')}`);
|
|
162
|
+
return errors;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Action-specific required params
|
|
166
|
+
const needsName = [SKILLS_ACTIONS.DESCRIBE, SKILLS_ACTIONS.READ, SKILLS_ACTIONS.READ_SECTION, SKILLS_ACTIONS.READ_FILE, SKILLS_ACTIONS.CREATE, SKILLS_ACTIONS.UPDATE, SKILLS_ACTIONS.DELETE];
|
|
167
|
+
if (needsName.includes(action) && !name) {
|
|
168
|
+
errors.push(`"name" is required for action "${action}"`);
|
|
169
|
+
}
|
|
170
|
+
if (action === SKILLS_ACTIONS.CREATE && !content) {
|
|
171
|
+
errors.push('"content" is required for action "create"');
|
|
172
|
+
}
|
|
173
|
+
if (action === SKILLS_ACTIONS.READ_SECTION && !section) {
|
|
174
|
+
errors.push('"section" is required for action "read-section"');
|
|
175
|
+
}
|
|
176
|
+
if (action === SKILLS_ACTIONS.READ_FILE && !file) {
|
|
177
|
+
errors.push('"file" is required for action "read-file"');
|
|
178
|
+
}
|
|
179
|
+
if (action === SKILLS_ACTIONS.IMPORT && !source) {
|
|
180
|
+
errors.push('"source" is required for action "import"');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return errors;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async execute(params, context = {}) {
|
|
187
|
+
const service = await this._ensureSkillsService();
|
|
188
|
+
const { action } = params;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
switch (action) {
|
|
192
|
+
case SKILLS_ACTIONS.LIST:
|
|
193
|
+
return this._formatResult(await service.listSkills(), 'Skills listed');
|
|
194
|
+
|
|
195
|
+
case SKILLS_ACTIONS.DESCRIBE:
|
|
196
|
+
return this._formatResult(await service.describeSkill(params.name), `Skill described: ${params.name}`);
|
|
197
|
+
|
|
198
|
+
case SKILLS_ACTIONS.READ:
|
|
199
|
+
return this._formatResult(await service.readSkill(params.name), `Skill read: ${params.name}`);
|
|
200
|
+
|
|
201
|
+
case SKILLS_ACTIONS.READ_SECTION:
|
|
202
|
+
return this._formatResult(await service.readSkillSection(params.name, params.section), `Section read: ${params.section}`);
|
|
203
|
+
|
|
204
|
+
case SKILLS_ACTIONS.READ_FILE:
|
|
205
|
+
return this._formatResult(await service.readSkillFile(params.name, params.file), `File read: ${params.file}`);
|
|
206
|
+
|
|
207
|
+
case SKILLS_ACTIONS.CREATE:
|
|
208
|
+
return this._formatResult(await service.createSkill(params.name, params.content, params.files || [], params.description || null), `Skill created: ${params.name}`);
|
|
209
|
+
|
|
210
|
+
case SKILLS_ACTIONS.UPDATE:
|
|
211
|
+
return this._formatResult(await service.updateSkill(params.name, params.content || null, params.files || [], params.description || null), `Skill updated: ${params.name}`);
|
|
212
|
+
|
|
213
|
+
case SKILLS_ACTIONS.DELETE:
|
|
214
|
+
await service.deleteSkill(params.name);
|
|
215
|
+
return this._formatResult({ deleted: params.name }, `Skill deleted: ${params.name}`);
|
|
216
|
+
|
|
217
|
+
case SKILLS_ACTIONS.IMPORT:
|
|
218
|
+
return this._formatResult(await service.importSkill(params.source, params.name || null, params.description || null), `Skill imported: ${params.name || params.source}`);
|
|
219
|
+
|
|
220
|
+
default:
|
|
221
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return { success: false, error: error.message };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_formatResult(data, message) {
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
result: data,
|
|
232
|
+
message
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getParameterSchema() {
|
|
237
|
+
return {
|
|
238
|
+
type: 'object',
|
|
239
|
+
required: ['action'],
|
|
240
|
+
properties: {
|
|
241
|
+
action: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
enum: Object.values(SKILLS_ACTIONS),
|
|
244
|
+
description: 'The skill action to perform'
|
|
245
|
+
},
|
|
246
|
+
name: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
description: 'Skill name (kebab-case)'
|
|
249
|
+
},
|
|
250
|
+
content: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Skill content (markdown)'
|
|
253
|
+
},
|
|
254
|
+
section: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Section heading to read (for read-section action)'
|
|
257
|
+
},
|
|
258
|
+
file: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: 'Relative file path within skill directory (for read-file action)'
|
|
261
|
+
},
|
|
262
|
+
source: {
|
|
263
|
+
type: 'string',
|
|
264
|
+
description: 'Source file or directory path (for import action)'
|
|
265
|
+
},
|
|
266
|
+
files: {
|
|
267
|
+
type: 'array',
|
|
268
|
+
items: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
path: { type: 'string' },
|
|
272
|
+
content: { type: 'string' }
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
description: 'Additional supporting files (for create/update actions)'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default SkillsTool;
|