gyrus 0.1.2 → 0.1.3
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/commands/skill.ts +432 -0
- package/src/config/index.ts +10 -7
- package/src/formatters/skill.ts +301 -0
- package/src/formatters/workspace.ts +14 -4
- package/src/index.ts +19 -0
- package/src/operations/index.ts +19 -6
- package/src/operations/skill.ts +308 -0
- package/src/operations/types.ts +74 -0
- package/src/operations/workspace.ts +10 -6
- package/src/server/mcp-stdio.ts +215 -0
- package/src/services/markdown.ts +115 -2
- package/src/services/skill.ts +458 -0
- package/src/services/workspace.ts +45 -4
- package/src/tools/gyrus-list.ts +1 -1
- package/src/tools/index.ts +36 -0
- package/src/tools/skill-create.ts +86 -0
- package/src/tools/skill-list.ts +55 -0
- package/src/tools/skill-read.ts +54 -0
- package/src/tools/skill-search.ts +67 -0
- package/src/tools/skill-update.ts +90 -0
- package/src/types/index.ts +129 -0
package/package.json
CHANGED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill CLI Command
|
|
3
|
+
* Handles all skill-related CLI operations
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* gyrus skill list [--workspace <name>] [--tags <t1,t2>]
|
|
7
|
+
* gyrus skill read <name> [--workspace <name>]
|
|
8
|
+
* gyrus skill search <query> [--workspace <name>] [--tags <t1,t2>] [--trigger <t>]
|
|
9
|
+
* gyrus skill create <name> --title <title> --content <content> [options]
|
|
10
|
+
* gyrus skill update <name> [--title <title>] [--content <content>] [--tags <t1,t2>]
|
|
11
|
+
*
|
|
12
|
+
* Aliases: gyrus s
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readConfig } from "../config/index.ts";
|
|
16
|
+
import { WorkspaceManager } from "../services/workspace.ts";
|
|
17
|
+
import {
|
|
18
|
+
listSkills,
|
|
19
|
+
readSkill,
|
|
20
|
+
searchSkills,
|
|
21
|
+
createSkill,
|
|
22
|
+
updateSkill,
|
|
23
|
+
} from "../operations/skill.ts";
|
|
24
|
+
import {
|
|
25
|
+
formatSkillListCli,
|
|
26
|
+
formatSkillReadCli,
|
|
27
|
+
formatSkillSearchCli,
|
|
28
|
+
formatSkillCreateCli,
|
|
29
|
+
formatSkillUpdateCli,
|
|
30
|
+
} from "../formatters/skill.ts";
|
|
31
|
+
|
|
32
|
+
const HELP = `
|
|
33
|
+
Usage: gyrus skill <subcommand> [options]
|
|
34
|
+
|
|
35
|
+
Subcommands:
|
|
36
|
+
list List all skills
|
|
37
|
+
read <name> Read a specific skill by name
|
|
38
|
+
search <query> Search skills by text query
|
|
39
|
+
create Create a new skill (with flags)
|
|
40
|
+
update <name> Update an existing skill
|
|
41
|
+
|
|
42
|
+
Global Options:
|
|
43
|
+
-w, --workspace <name> Target workspace (default: active workspace)
|
|
44
|
+
-h, --help Show this help message
|
|
45
|
+
|
|
46
|
+
List Options:
|
|
47
|
+
-t, --tags <t1,t2,...> Filter by tags (comma-separated)
|
|
48
|
+
|
|
49
|
+
Search Options:
|
|
50
|
+
-t, --tags <t1,t2,...> Filter by tags
|
|
51
|
+
--trigger <trigger> Filter by trigger pattern
|
|
52
|
+
|
|
53
|
+
Create Options (name and title required):
|
|
54
|
+
--name <name> Skill name in kebab-case
|
|
55
|
+
--title <title> Skill title
|
|
56
|
+
--content <content> Skill content (or use --file)
|
|
57
|
+
--file <path> Read content from file
|
|
58
|
+
-t, --tags <t1,t2,...> Tags (comma-separated)
|
|
59
|
+
--triggers <t1,t2,...> Trigger patterns (comma-separated)
|
|
60
|
+
--apply-to <a1,a2,...> Apply to file patterns (comma-separated)
|
|
61
|
+
--related <id1,id2,...> Related skill names
|
|
62
|
+
--description <desc> Brief description
|
|
63
|
+
|
|
64
|
+
Update Options:
|
|
65
|
+
--title <title> New title
|
|
66
|
+
--content <content> New content (or use --file)
|
|
67
|
+
--file <path> Read content from file
|
|
68
|
+
-t, --tags <t1,t2,...> New tags (replaces existing)
|
|
69
|
+
--triggers <t1,t2,...> New triggers (replaces existing)
|
|
70
|
+
--apply-to <a1,a2,...> New apply-to patterns (replaces existing)
|
|
71
|
+
--related <id1,id2,...> New related names (replaces existing)
|
|
72
|
+
--description <desc> New description
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
gyrus skill list
|
|
76
|
+
gyrus skill list --tags typescript,testing
|
|
77
|
+
gyrus skill read code-review
|
|
78
|
+
gyrus skill search "testing" --tags quality
|
|
79
|
+
gyrus skill create --name code-review --title "Code Review Guide" --content "# Code Review..."
|
|
80
|
+
gyrus s list -w work
|
|
81
|
+
|
|
82
|
+
Aliases: s
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse command-line arguments into options object
|
|
87
|
+
*/
|
|
88
|
+
function parseArgs(args: string[]): {
|
|
89
|
+
subcommand: string;
|
|
90
|
+
positional: string[];
|
|
91
|
+
options: Record<string, string | boolean>;
|
|
92
|
+
} {
|
|
93
|
+
const options: Record<string, string | boolean> = {};
|
|
94
|
+
const positional: string[] = [];
|
|
95
|
+
let subcommand = "";
|
|
96
|
+
|
|
97
|
+
let i = 0;
|
|
98
|
+
|
|
99
|
+
// First non-flag argument is the subcommand
|
|
100
|
+
while (i < args.length && !subcommand) {
|
|
101
|
+
if (!args[i].startsWith("-")) {
|
|
102
|
+
subcommand = args[i];
|
|
103
|
+
i++;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse remaining arguments
|
|
110
|
+
while (i < args.length) {
|
|
111
|
+
const arg = args[i];
|
|
112
|
+
|
|
113
|
+
if (arg === "-h" || arg === "--help") {
|
|
114
|
+
options.help = true;
|
|
115
|
+
i++;
|
|
116
|
+
} else if (arg === "-w" || arg === "--workspace") {
|
|
117
|
+
options.workspace = args[++i] || "";
|
|
118
|
+
i++;
|
|
119
|
+
} else if (arg === "-t" || arg === "--tags") {
|
|
120
|
+
options.tags = args[++i] || "";
|
|
121
|
+
i++;
|
|
122
|
+
} else if (arg === "--name") {
|
|
123
|
+
options.name = args[++i] || "";
|
|
124
|
+
i++;
|
|
125
|
+
} else if (arg === "--title") {
|
|
126
|
+
options.title = args[++i] || "";
|
|
127
|
+
i++;
|
|
128
|
+
} else if (arg === "--content") {
|
|
129
|
+
options.content = args[++i] || "";
|
|
130
|
+
i++;
|
|
131
|
+
} else if (arg === "--file") {
|
|
132
|
+
options.file = args[++i] || "";
|
|
133
|
+
i++;
|
|
134
|
+
} else if (arg === "--triggers") {
|
|
135
|
+
options.triggers = args[++i] || "";
|
|
136
|
+
i++;
|
|
137
|
+
} else if (arg === "--trigger") {
|
|
138
|
+
options.trigger = args[++i] || "";
|
|
139
|
+
i++;
|
|
140
|
+
} else if (arg === "--apply-to") {
|
|
141
|
+
options.applyTo = args[++i] || "";
|
|
142
|
+
i++;
|
|
143
|
+
} else if (arg === "--related") {
|
|
144
|
+
options.related = args[++i] || "";
|
|
145
|
+
i++;
|
|
146
|
+
} else if (arg === "--description") {
|
|
147
|
+
options.description = args[++i] || "";
|
|
148
|
+
i++;
|
|
149
|
+
} else if (!arg.startsWith("-")) {
|
|
150
|
+
positional.push(arg);
|
|
151
|
+
i++;
|
|
152
|
+
} else {
|
|
153
|
+
// Unknown flag, skip
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { subcommand, positional, options };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse comma-separated string into array
|
|
163
|
+
*/
|
|
164
|
+
function parseList(value: string | undefined): string[] | undefined {
|
|
165
|
+
if (!value || typeof value !== "string") return undefined;
|
|
166
|
+
return value
|
|
167
|
+
.split(",")
|
|
168
|
+
.map((s) => s.trim())
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Read content from file if --file option is provided
|
|
174
|
+
*/
|
|
175
|
+
async function getContent(
|
|
176
|
+
options: Record<string, string | boolean>,
|
|
177
|
+
): Promise<string | undefined> {
|
|
178
|
+
if (options.file && typeof options.file === "string") {
|
|
179
|
+
try {
|
|
180
|
+
const file = Bun.file(options.file);
|
|
181
|
+
return await file.text();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`Error reading file: ${options.file}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return typeof options.content === "string" ? options.content : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle the list subcommand
|
|
192
|
+
*/
|
|
193
|
+
async function handleList(
|
|
194
|
+
manager: WorkspaceManager,
|
|
195
|
+
options: Record<string, string | boolean>,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const result = await listSkills(manager, {
|
|
198
|
+
workspace:
|
|
199
|
+
typeof options.workspace === "string" ? options.workspace : undefined,
|
|
200
|
+
tags: parseList(options.tags as string),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!result.success) {
|
|
204
|
+
console.error(`Error: ${result.error}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(formatSkillListCli(result.data!));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handle the read subcommand
|
|
213
|
+
*/
|
|
214
|
+
async function handleRead(
|
|
215
|
+
manager: WorkspaceManager,
|
|
216
|
+
positional: string[],
|
|
217
|
+
options: Record<string, string | boolean>,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const name = positional[0];
|
|
220
|
+
|
|
221
|
+
if (!name) {
|
|
222
|
+
console.error("Error: Please provide a skill name");
|
|
223
|
+
console.error("Usage: gyrus skill read <name>");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await readSkill(manager, {
|
|
228
|
+
workspace:
|
|
229
|
+
typeof options.workspace === "string" ? options.workspace : undefined,
|
|
230
|
+
name,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!result.success) {
|
|
234
|
+
console.error(`Error: ${result.error}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(formatSkillReadCli(result.data!));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handle the search subcommand
|
|
243
|
+
*/
|
|
244
|
+
async function handleSearch(
|
|
245
|
+
manager: WorkspaceManager,
|
|
246
|
+
positional: string[],
|
|
247
|
+
options: Record<string, string | boolean>,
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
const query = positional[0];
|
|
250
|
+
const tags = parseList(options.tags as string);
|
|
251
|
+
const trigger =
|
|
252
|
+
typeof options.trigger === "string" ? options.trigger : undefined;
|
|
253
|
+
|
|
254
|
+
if (!query && !tags?.length && !trigger) {
|
|
255
|
+
console.error("Error: Please provide a search query, tags, or trigger");
|
|
256
|
+
console.error(
|
|
257
|
+
"Usage: gyrus skill search <query> [--tags <t1,t2>] [--trigger <t>]",
|
|
258
|
+
);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = await searchSkills(manager, {
|
|
263
|
+
workspace:
|
|
264
|
+
typeof options.workspace === "string" ? options.workspace : undefined,
|
|
265
|
+
query,
|
|
266
|
+
tags,
|
|
267
|
+
trigger,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
console.error(`Error: ${result.error}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(formatSkillSearchCli(result.data!));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle the create subcommand
|
|
280
|
+
*/
|
|
281
|
+
async function handleCreate(
|
|
282
|
+
manager: WorkspaceManager,
|
|
283
|
+
options: Record<string, string | boolean>,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const name = typeof options.name === "string" ? options.name : undefined;
|
|
286
|
+
const title = typeof options.title === "string" ? options.title : undefined;
|
|
287
|
+
const content = await getContent(options);
|
|
288
|
+
|
|
289
|
+
if (!name || !title || !content) {
|
|
290
|
+
console.error("Error: Missing required options for create");
|
|
291
|
+
console.error("Required: --name, --title, --content (or --file)");
|
|
292
|
+
console.error("\nExample:");
|
|
293
|
+
console.error(
|
|
294
|
+
' gyrus skill create --name code-review --title "Code Review Guide" --content "# Instructions..."',
|
|
295
|
+
);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const result = await createSkill(manager, {
|
|
300
|
+
workspace:
|
|
301
|
+
typeof options.workspace === "string" ? options.workspace : undefined,
|
|
302
|
+
name,
|
|
303
|
+
title,
|
|
304
|
+
content,
|
|
305
|
+
description:
|
|
306
|
+
typeof options.description === "string" ? options.description : undefined,
|
|
307
|
+
tags: parseList(options.tags as string),
|
|
308
|
+
triggers: parseList(options.triggers as string),
|
|
309
|
+
applyTo: parseList(options.applyTo as string),
|
|
310
|
+
related: parseList(options.related as string),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!result.success) {
|
|
314
|
+
console.error(`Error: ${result.error}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log(formatSkillCreateCli(result.data!));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handle the update subcommand
|
|
323
|
+
*/
|
|
324
|
+
async function handleUpdate(
|
|
325
|
+
manager: WorkspaceManager,
|
|
326
|
+
positional: string[],
|
|
327
|
+
options: Record<string, string | boolean>,
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
const name = positional[0];
|
|
330
|
+
|
|
331
|
+
if (!name) {
|
|
332
|
+
console.error("Error: Please provide a skill name to update");
|
|
333
|
+
console.error("Usage: gyrus skill update <name> [options]");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const content = await getContent(options);
|
|
338
|
+
const hasUpdates =
|
|
339
|
+
options.title ||
|
|
340
|
+
content ||
|
|
341
|
+
options.tags ||
|
|
342
|
+
options.triggers ||
|
|
343
|
+
options.applyTo ||
|
|
344
|
+
options.related ||
|
|
345
|
+
options.description;
|
|
346
|
+
|
|
347
|
+
if (!hasUpdates) {
|
|
348
|
+
console.error("Error: No update options provided");
|
|
349
|
+
console.error(
|
|
350
|
+
"Available options: --title, --content, --file, --tags, --triggers, --apply-to, --related, --description",
|
|
351
|
+
);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const result = await updateSkill(manager, {
|
|
356
|
+
workspace:
|
|
357
|
+
typeof options.workspace === "string" ? options.workspace : undefined,
|
|
358
|
+
name,
|
|
359
|
+
title: typeof options.title === "string" ? options.title : undefined,
|
|
360
|
+
content,
|
|
361
|
+
description:
|
|
362
|
+
typeof options.description === "string" ? options.description : undefined,
|
|
363
|
+
tags: parseList(options.tags as string),
|
|
364
|
+
triggers: parseList(options.triggers as string),
|
|
365
|
+
applyTo: parseList(options.applyTo as string),
|
|
366
|
+
related: parseList(options.related as string),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (!result.success) {
|
|
370
|
+
console.error(`Error: ${result.error}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(formatSkillUpdateCli(result.data!));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Main skill command handler
|
|
379
|
+
*/
|
|
380
|
+
export async function skillCommand(args: string[]): Promise<void> {
|
|
381
|
+
const { subcommand, positional, options } = parseArgs(args);
|
|
382
|
+
|
|
383
|
+
// Show help if requested or no subcommand
|
|
384
|
+
if (options.help || !subcommand) {
|
|
385
|
+
console.log(HELP);
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Load config and create workspace manager
|
|
390
|
+
const config = readConfig();
|
|
391
|
+
if (!config) {
|
|
392
|
+
console.error("Error: No configuration found. Run `gyrus init` first.");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const manager = new WorkspaceManager(config);
|
|
397
|
+
|
|
398
|
+
// Route to subcommand handler
|
|
399
|
+
switch (subcommand) {
|
|
400
|
+
case "list":
|
|
401
|
+
case "ls":
|
|
402
|
+
await handleList(manager, options);
|
|
403
|
+
break;
|
|
404
|
+
|
|
405
|
+
case "read":
|
|
406
|
+
case "get":
|
|
407
|
+
case "show":
|
|
408
|
+
await handleRead(manager, positional, options);
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case "search":
|
|
412
|
+
case "find":
|
|
413
|
+
await handleSearch(manager, positional, options);
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case "create":
|
|
417
|
+
case "new":
|
|
418
|
+
case "add":
|
|
419
|
+
await handleCreate(manager, options);
|
|
420
|
+
break;
|
|
421
|
+
|
|
422
|
+
case "update":
|
|
423
|
+
case "edit":
|
|
424
|
+
await handleUpdate(manager, positional, options);
|
|
425
|
+
break;
|
|
426
|
+
|
|
427
|
+
default:
|
|
428
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
429
|
+
console.log("\nRun `gyrus skill --help` for usage information.");
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -131,7 +131,10 @@ export function getOrCreateConfig(): GyrusConfig {
|
|
|
131
131
|
/**
|
|
132
132
|
* Get a workspace by name
|
|
133
133
|
*/
|
|
134
|
-
export function getWorkspace(
|
|
134
|
+
export function getWorkspace(
|
|
135
|
+
config: GyrusConfig,
|
|
136
|
+
name: string,
|
|
137
|
+
): Workspace | null {
|
|
135
138
|
return config.workspaces.find((ws) => ws.name === name) ?? null;
|
|
136
139
|
}
|
|
137
140
|
|
|
@@ -148,7 +151,7 @@ export function getDefaultWorkspace(config: GyrusConfig): Workspace | null {
|
|
|
148
151
|
*/
|
|
149
152
|
export function addWorkspace(
|
|
150
153
|
config: GyrusConfig,
|
|
151
|
-
workspace: Workspace
|
|
154
|
+
workspace: Workspace,
|
|
152
155
|
): string | null {
|
|
153
156
|
// Validate name format
|
|
154
157
|
if (!isValidWorkspaceName(workspace.name)) {
|
|
@@ -162,9 +165,7 @@ export function addWorkspace(
|
|
|
162
165
|
|
|
163
166
|
// Check for duplicate path
|
|
164
167
|
const expandedPath = expandPath(workspace.path);
|
|
165
|
-
if (
|
|
166
|
-
config.workspaces.some((ws) => expandPath(ws.path) === expandedPath)
|
|
167
|
-
) {
|
|
168
|
+
if (config.workspaces.some((ws) => expandPath(ws.path) === expandedPath)) {
|
|
168
169
|
return `A workspace already exists at "${workspace.path}".`;
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -178,7 +179,7 @@ export function addWorkspace(
|
|
|
178
179
|
*/
|
|
179
180
|
export function removeWorkspace(
|
|
180
181
|
config: GyrusConfig,
|
|
181
|
-
name: string
|
|
182
|
+
name: string,
|
|
182
183
|
): string | null {
|
|
183
184
|
const index = config.workspaces.findIndex((ws) => ws.name === name);
|
|
184
185
|
|
|
@@ -207,7 +208,7 @@ export function removeWorkspace(
|
|
|
207
208
|
*/
|
|
208
209
|
export function setDefaultWorkspace(
|
|
209
210
|
config: GyrusConfig,
|
|
210
|
-
name: string
|
|
211
|
+
name: string,
|
|
211
212
|
): string | null {
|
|
212
213
|
if (!config.workspaces.some((ws) => ws.name === name)) {
|
|
213
214
|
return `Workspace "${name}" not found.`;
|
|
@@ -224,12 +225,14 @@ export function getWorkspacePaths(workspace: Workspace): {
|
|
|
224
225
|
root: string;
|
|
225
226
|
knowledge: string;
|
|
226
227
|
adrs: string;
|
|
228
|
+
skills: string;
|
|
227
229
|
} {
|
|
228
230
|
const root = expandPath(workspace.path);
|
|
229
231
|
return {
|
|
230
232
|
root,
|
|
231
233
|
knowledge: join(root, "knowledge"),
|
|
232
234
|
adrs: join(root, "adrs"),
|
|
235
|
+
skills: join(root, "skills"),
|
|
233
236
|
};
|
|
234
237
|
}
|
|
235
238
|
|