natureco-cli 1.0.0
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/CHANGELOG.md +192 -0
- package/README.md +369 -0
- package/bin/natureco.js +109 -0
- package/package.json +33 -0
- package/skills/code-review/SKILL.md +48 -0
- package/skills/summarize/SKILL.md +46 -0
- package/skills/translate/SKILL.md +47 -0
- package/src/commands/ask.js +50 -0
- package/src/commands/bots.js +41 -0
- package/src/commands/chat.js +226 -0
- package/src/commands/config.js +68 -0
- package/src/commands/gateway.js +84 -0
- package/src/commands/help.js +55 -0
- package/src/commands/init.js +135 -0
- package/src/commands/login.js +45 -0
- package/src/commands/logout.js +13 -0
- package/src/commands/mcp.js +275 -0
- package/src/commands/run.js +69 -0
- package/src/commands/setup.js +222 -0
- package/src/commands/skills.js +161 -0
- package/src/commands/update.js +61 -0
- package/src/utils/agents.js +28 -0
- package/src/utils/api.js +64 -0
- package/src/utils/config.js +81 -0
- package/src/utils/history.js +87 -0
- package/src/utils/mcp.js +188 -0
- package/src/utils/skills.js +285 -0
package/src/utils/mcp.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { CONFIG_DIR, loadConfig, saveConfig } = require('./config');
|
|
5
|
+
|
|
6
|
+
// MCP templates
|
|
7
|
+
const MCP_TEMPLATES = {
|
|
8
|
+
filesystem: {
|
|
9
|
+
name: 'filesystem',
|
|
10
|
+
description: 'File system operations (read, write, list files)',
|
|
11
|
+
command: 'npx',
|
|
12
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
|
|
13
|
+
env: {},
|
|
14
|
+
},
|
|
15
|
+
github: {
|
|
16
|
+
name: 'github',
|
|
17
|
+
description: 'GitHub repository operations (issues, PRs, commits)',
|
|
18
|
+
command: 'npx',
|
|
19
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
20
|
+
env: {
|
|
21
|
+
GITHUB_TOKEN: '<your-github-token>',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
postgres: {
|
|
25
|
+
name: 'postgres',
|
|
26
|
+
description: 'PostgreSQL database operations',
|
|
27
|
+
command: 'npx',
|
|
28
|
+
args: ['-y', '@modelcontextprotocol/server-postgres'],
|
|
29
|
+
env: {
|
|
30
|
+
POSTGRES_CONNECTION_STRING: 'postgresql://user:password@localhost:5432/dbname',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
sqlite: {
|
|
34
|
+
name: 'sqlite',
|
|
35
|
+
description: 'SQLite database operations',
|
|
36
|
+
command: 'npx',
|
|
37
|
+
args: ['-y', '@modelcontextprotocol/server-sqlite', '--db-path', './database.db'],
|
|
38
|
+
env: {},
|
|
39
|
+
},
|
|
40
|
+
'brave-search': {
|
|
41
|
+
name: 'brave-search',
|
|
42
|
+
description: 'Web search using Brave Search API',
|
|
43
|
+
command: 'npx',
|
|
44
|
+
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
|
45
|
+
env: {
|
|
46
|
+
BRAVE_API_KEY: '<your-brave-api-key>',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Get MCP servers from config
|
|
52
|
+
function getMcpServers() {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
return config?.mcpServers || {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Save MCP servers to config
|
|
58
|
+
function saveMcpServers(servers) {
|
|
59
|
+
const config = loadConfig() || {};
|
|
60
|
+
config.mcpServers = servers;
|
|
61
|
+
saveConfig(config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add MCP server
|
|
65
|
+
function addMcpServer(name, template = null, customConfig = null) {
|
|
66
|
+
const servers = getMcpServers();
|
|
67
|
+
|
|
68
|
+
if (servers[name]) {
|
|
69
|
+
throw new Error(`MCP sunucu zaten mevcut: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let serverConfig;
|
|
73
|
+
|
|
74
|
+
if (template && MCP_TEMPLATES[template]) {
|
|
75
|
+
serverConfig = { ...MCP_TEMPLATES[template] };
|
|
76
|
+
delete serverConfig.name;
|
|
77
|
+
} else if (customConfig) {
|
|
78
|
+
serverConfig = customConfig;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error('Template veya custom config gerekli');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
servers[name] = {
|
|
84
|
+
...serverConfig,
|
|
85
|
+
disabled: false,
|
|
86
|
+
autoApprove: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
saveMcpServers(servers);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove MCP server
|
|
93
|
+
function removeMcpServer(name) {
|
|
94
|
+
const servers = getMcpServers();
|
|
95
|
+
|
|
96
|
+
if (!servers[name]) {
|
|
97
|
+
throw new Error(`MCP sunucu bulunamadı: ${name}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
delete servers[name];
|
|
101
|
+
saveMcpServers(servers);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Test MCP server connection
|
|
105
|
+
function testMcpServer(name) {
|
|
106
|
+
const servers = getMcpServers();
|
|
107
|
+
|
|
108
|
+
if (!servers[name]) {
|
|
109
|
+
throw new Error(`MCP sunucu bulunamadı: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const server = servers[name];
|
|
113
|
+
|
|
114
|
+
// Check if command exists
|
|
115
|
+
try {
|
|
116
|
+
if (server.command === 'npx') {
|
|
117
|
+
// Check if npx is available
|
|
118
|
+
execSync('npx --version', { stdio: 'ignore' });
|
|
119
|
+
} else {
|
|
120
|
+
// Check if custom command exists
|
|
121
|
+
execSync(`${server.command} --version`, { stdio: 'ignore' });
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(`Komut bulunamadı: ${server.command}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check environment variables
|
|
128
|
+
if (server.env) {
|
|
129
|
+
const missingEnvVars = [];
|
|
130
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
131
|
+
if (value.startsWith('<') && value.endsWith('>')) {
|
|
132
|
+
missingEnvVars.push(key);
|
|
133
|
+
} else if (!process.env[key] && value) {
|
|
134
|
+
missingEnvVars.push(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (missingEnvVars.length > 0) {
|
|
139
|
+
throw new Error(`Eksik environment variable'lar: ${missingEnvVars.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Enable/disable MCP server
|
|
147
|
+
function toggleMcpServer(name, enabled) {
|
|
148
|
+
const servers = getMcpServers();
|
|
149
|
+
|
|
150
|
+
if (!servers[name]) {
|
|
151
|
+
throw new Error(`MCP sunucu bulunamadı: ${name}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
servers[name].disabled = !enabled;
|
|
155
|
+
saveMcpServers(servers);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get MCP templates
|
|
159
|
+
function getMcpTemplates() {
|
|
160
|
+
return MCP_TEMPLATES;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update MCP server config
|
|
164
|
+
function updateMcpServer(name, updates) {
|
|
165
|
+
const servers = getMcpServers();
|
|
166
|
+
|
|
167
|
+
if (!servers[name]) {
|
|
168
|
+
throw new Error(`MCP sunucu bulunamadı: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
servers[name] = {
|
|
172
|
+
...servers[name],
|
|
173
|
+
...updates,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
saveMcpServers(servers);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
getMcpServers,
|
|
181
|
+
saveMcpServers,
|
|
182
|
+
addMcpServer,
|
|
183
|
+
removeMcpServer,
|
|
184
|
+
testMcpServer,
|
|
185
|
+
toggleMcpServer,
|
|
186
|
+
getMcpTemplates,
|
|
187
|
+
updateMcpServer,
|
|
188
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { CONFIG_DIR } = require('./config');
|
|
6
|
+
|
|
7
|
+
const BUILTIN_SKILLS_DIR = path.join(__dirname, '..', '..', 'skills');
|
|
8
|
+
const USER_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
9
|
+
const PROJECT_SKILLS_DIR = path.join(process.cwd(), '.natureco', 'skills');
|
|
10
|
+
|
|
11
|
+
// Skill metadata parser
|
|
12
|
+
function parseSkillMetadata(content) {
|
|
13
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
14
|
+
if (!frontmatterMatch) return null;
|
|
15
|
+
|
|
16
|
+
const frontmatter = frontmatterMatch[1];
|
|
17
|
+
const metadata = {};
|
|
18
|
+
|
|
19
|
+
// name
|
|
20
|
+
const nameMatch = frontmatter.match(/name:\s*(.+)/);
|
|
21
|
+
if (nameMatch) metadata.name = nameMatch[1].trim();
|
|
22
|
+
|
|
23
|
+
// description
|
|
24
|
+
const descMatch = frontmatter.match(/description:\s*(.+)/);
|
|
25
|
+
if (descMatch) metadata.description = descMatch[1].trim();
|
|
26
|
+
|
|
27
|
+
// metadata JSON
|
|
28
|
+
const metaMatch = frontmatter.match(/metadata:\s*(\{[\s\S]*?\})/);
|
|
29
|
+
if (metaMatch) {
|
|
30
|
+
try {
|
|
31
|
+
metadata.metadata = JSON.parse(metaMatch[1]);
|
|
32
|
+
} catch {
|
|
33
|
+
metadata.metadata = {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return metadata;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Skill gating - requirements check
|
|
41
|
+
function checkSkillRequirements(metadata) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
|
|
44
|
+
if (metadata?.metadata?.natureco?.requires?.bins) {
|
|
45
|
+
const bins = metadata.metadata.natureco.requires.bins;
|
|
46
|
+
for (const bin of bins) {
|
|
47
|
+
try {
|
|
48
|
+
execSync(`${bin} --version`, { stdio: 'ignore' });
|
|
49
|
+
} catch {
|
|
50
|
+
errors.push(`Binary gerekli: ${bin}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (metadata?.metadata?.natureco?.requires?.env) {
|
|
56
|
+
const envVars = metadata.metadata.natureco.requires.env;
|
|
57
|
+
for (const envVar of envVars) {
|
|
58
|
+
if (!process.env[envVar]) {
|
|
59
|
+
errors.push(`Environment variable gerekli: ${envVar}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (metadata?.metadata?.natureco?.os) {
|
|
65
|
+
const supportedOS = metadata.metadata.natureco.os;
|
|
66
|
+
if (!supportedOS.includes(os.platform())) {
|
|
67
|
+
errors.push(`Platform desteklenmiyor. Desteklenen: ${supportedOS.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return errors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get all skills from all sources
|
|
75
|
+
function getSkills() {
|
|
76
|
+
const skills = [];
|
|
77
|
+
|
|
78
|
+
// Builtin skills
|
|
79
|
+
if (fs.existsSync(BUILTIN_SKILLS_DIR)) {
|
|
80
|
+
const builtinDirs = fs.readdirSync(BUILTIN_SKILLS_DIR, { withFileTypes: true })
|
|
81
|
+
.filter(d => d.isDirectory());
|
|
82
|
+
|
|
83
|
+
for (const dir of builtinDirs) {
|
|
84
|
+
const skillFile = path.join(BUILTIN_SKILLS_DIR, dir.name, 'SKILL.md');
|
|
85
|
+
if (fs.existsSync(skillFile)) {
|
|
86
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
87
|
+
const metadata = parseSkillMetadata(content);
|
|
88
|
+
if (metadata) {
|
|
89
|
+
skills.push({
|
|
90
|
+
...metadata,
|
|
91
|
+
slug: dir.name,
|
|
92
|
+
source: 'builtin',
|
|
93
|
+
path: path.join(BUILTIN_SKILLS_DIR, dir.name),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// User skills
|
|
101
|
+
if (fs.existsSync(USER_SKILLS_DIR)) {
|
|
102
|
+
const userDirs = fs.readdirSync(USER_SKILLS_DIR, { withFileTypes: true })
|
|
103
|
+
.filter(d => d.isDirectory());
|
|
104
|
+
|
|
105
|
+
for (const dir of userDirs) {
|
|
106
|
+
const skillFile = path.join(USER_SKILLS_DIR, dir.name, 'SKILL.md');
|
|
107
|
+
if (fs.existsSync(skillFile)) {
|
|
108
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
109
|
+
const metadata = parseSkillMetadata(content);
|
|
110
|
+
if (metadata) {
|
|
111
|
+
skills.push({
|
|
112
|
+
...metadata,
|
|
113
|
+
slug: dir.name,
|
|
114
|
+
source: 'user',
|
|
115
|
+
path: path.join(USER_SKILLS_DIR, dir.name),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Project skills
|
|
123
|
+
if (fs.existsSync(PROJECT_SKILLS_DIR)) {
|
|
124
|
+
const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
|
|
125
|
+
.filter(d => d.isDirectory());
|
|
126
|
+
|
|
127
|
+
for (const dir of projectDirs) {
|
|
128
|
+
const skillFile = path.join(PROJECT_SKILLS_DIR, dir.name, 'SKILL.md');
|
|
129
|
+
if (fs.existsSync(skillFile)) {
|
|
130
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
131
|
+
const metadata = parseSkillMetadata(content);
|
|
132
|
+
if (metadata) {
|
|
133
|
+
skills.push({
|
|
134
|
+
...metadata,
|
|
135
|
+
slug: dir.name,
|
|
136
|
+
source: 'project',
|
|
137
|
+
path: path.join(PROJECT_SKILLS_DIR, dir.name),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return skills;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Install skill from NatureHub
|
|
148
|
+
async function installSkill(slug) {
|
|
149
|
+
const url = `https://natureco.me/hub/skills/${slug}/SKILL.md`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(url);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new Error(`Skill bulunamadı: ${slug}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const content = await response.text();
|
|
158
|
+
const metadata = parseSkillMetadata(content);
|
|
159
|
+
|
|
160
|
+
if (!metadata) {
|
|
161
|
+
throw new Error('Geçersiz skill formatı');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check requirements
|
|
165
|
+
const errors = checkSkillRequirements(metadata);
|
|
166
|
+
if (errors.length > 0) {
|
|
167
|
+
throw new Error(`Gereksinimler karşılanmadı:\n${errors.join('\n')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Save to user skills
|
|
171
|
+
if (!fs.existsSync(USER_SKILLS_DIR)) {
|
|
172
|
+
fs.mkdirSync(USER_SKILLS_DIR, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const skillDir = path.join(USER_SKILLS_DIR, slug);
|
|
176
|
+
if (!fs.existsSync(skillDir)) {
|
|
177
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(`Skill yüklenemedi: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove skill
|
|
187
|
+
function removeSkill(slug) {
|
|
188
|
+
const skillPath = path.join(USER_SKILLS_DIR, slug);
|
|
189
|
+
|
|
190
|
+
if (!fs.existsSync(skillPath)) {
|
|
191
|
+
throw new Error(`Skill bulunamadı: ${slug}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update all skills
|
|
198
|
+
async function updateAllSkills() {
|
|
199
|
+
const skills = getSkills().filter(s => s.source === 'user');
|
|
200
|
+
const updated = [];
|
|
201
|
+
|
|
202
|
+
for (const skill of skills) {
|
|
203
|
+
try {
|
|
204
|
+
await installSkill(skill.slug);
|
|
205
|
+
updated.push(skill.slug);
|
|
206
|
+
} catch {
|
|
207
|
+
// Skip failed updates
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return updated;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Create skill template
|
|
215
|
+
function createSkillTemplate(name) {
|
|
216
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
217
|
+
const skillDir = path.join(USER_SKILLS_DIR, slug);
|
|
218
|
+
|
|
219
|
+
if (fs.existsSync(skillDir)) {
|
|
220
|
+
throw new Error(`Skill zaten mevcut: ${slug}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(USER_SKILLS_DIR)) {
|
|
224
|
+
fs.mkdirSync(USER_SKILLS_DIR, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
228
|
+
|
|
229
|
+
const template = `---
|
|
230
|
+
name: ${name}
|
|
231
|
+
description: ${name} skill açıklaması
|
|
232
|
+
metadata: {"natureco": {"requires": {"bins": [], "env": []}, "os": ["win32","darwin","linux"]}}
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
# ${name} Skill
|
|
236
|
+
|
|
237
|
+
Bu skill ${name} işlemlerini yapar.
|
|
238
|
+
|
|
239
|
+
## Kullanım
|
|
240
|
+
|
|
241
|
+
[Kullanım talimatları]
|
|
242
|
+
|
|
243
|
+
## Örnekler
|
|
244
|
+
|
|
245
|
+
\`\`\`
|
|
246
|
+
[Örnek komutlar]
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
## Notlar
|
|
250
|
+
|
|
251
|
+
[Ek notlar]
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), template, 'utf8');
|
|
255
|
+
|
|
256
|
+
return path.join(skillDir, 'SKILL.md');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Get skill prompt injection content
|
|
260
|
+
function getSkillPrompts() {
|
|
261
|
+
const skills = getSkills();
|
|
262
|
+
const prompts = [];
|
|
263
|
+
|
|
264
|
+
for (const skill of skills) {
|
|
265
|
+
const skillFile = path.join(skill.path, 'SKILL.md');
|
|
266
|
+
if (fs.existsSync(skillFile)) {
|
|
267
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
268
|
+
// Remove frontmatter
|
|
269
|
+
const withoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
270
|
+
prompts.push(`\n## Skill: ${skill.name}\n${withoutFrontmatter}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return prompts.join('\n\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
getSkills,
|
|
279
|
+
installSkill,
|
|
280
|
+
removeSkill,
|
|
281
|
+
updateAllSkills,
|
|
282
|
+
createSkillTemplate,
|
|
283
|
+
getSkillPrompts,
|
|
284
|
+
checkSkillRequirements,
|
|
285
|
+
};
|