nex-code 0.3.4 → 0.3.7

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/cli/skills.js DELETED
@@ -1,412 +0,0 @@
1
- /**
2
- * cli/skills.js — Skills System
3
- * Load .md and .js skill files from .nex/skills/ to extend the system.
4
- * - Prompt Skills (.md): inject instructions into system prompt
5
- * - Script Skills (.js): provide instructions, commands, and tools
6
- */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
-
11
- // Loaded skills registry
12
- let loadedSkills = [];
13
-
14
- function getSkillsDir() {
15
- return path.join(process.cwd(), '.nex', 'skills');
16
- }
17
-
18
- function getConfigPath() {
19
- return path.join(process.cwd(), '.nex', 'config.json');
20
- }
21
-
22
- /**
23
- * Initialize the skills directory
24
- * @returns {string} path to .nex/skills/
25
- */
26
- function initSkillsDir() {
27
- const dir = getSkillsDir();
28
- if (!fs.existsSync(dir)) {
29
- fs.mkdirSync(dir, { recursive: true });
30
- }
31
- return dir;
32
- }
33
-
34
- /**
35
- * Load disabled list from .nex/config.json
36
- * @returns {string[]}
37
- */
38
- function getDisabledSkills() {
39
- const configPath = getConfigPath();
40
- if (!fs.existsSync(configPath)) return [];
41
- try {
42
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
43
- return (config.skills && Array.isArray(config.skills.disabled)) ? config.skills.disabled : [];
44
- } catch {
45
- return [];
46
- }
47
- }
48
-
49
- /**
50
- * Save disabled list to .nex/config.json
51
- * @param {string[]} disabled
52
- */
53
- function saveDisabledSkills(disabled) {
54
- const configPath = getConfigPath();
55
- let config = {};
56
- if (fs.existsSync(configPath)) {
57
- try {
58
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
59
- } catch {
60
- config = {};
61
- }
62
- }
63
- if (!config.skills) config.skills = {};
64
- config.skills.disabled = disabled;
65
- const dir = path.dirname(configPath);
66
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
67
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
68
- }
69
-
70
- /**
71
- * Validate a script skill module
72
- * @param {Object} mod - require()'d module
73
- * @param {string} filePath - for error messages
74
- * @returns {{valid: boolean, errors: string[]}}
75
- */
76
- function validateScriptSkill(mod, filePath) {
77
- const errors = [];
78
-
79
- if (typeof mod !== 'object' || mod === null) {
80
- return { valid: false, errors: ['Module must export an object'] };
81
- }
82
-
83
- if (mod.name !== undefined && typeof mod.name !== 'string') {
84
- errors.push('name must be a string');
85
- }
86
-
87
- if (mod.description !== undefined && typeof mod.description !== 'string') {
88
- errors.push('description must be a string');
89
- }
90
-
91
- if (mod.instructions !== undefined && typeof mod.instructions !== 'string') {
92
- errors.push('instructions must be a string');
93
- }
94
-
95
- if (mod.commands !== undefined) {
96
- if (!Array.isArray(mod.commands)) {
97
- errors.push('commands must be an array');
98
- } else {
99
- for (let i = 0; i < mod.commands.length; i++) {
100
- const c = mod.commands[i];
101
- if (!c.cmd || typeof c.cmd !== 'string') {
102
- errors.push(`commands[${i}].cmd must be a non-empty string`);
103
- }
104
- if (c.handler !== undefined && typeof c.handler !== 'function') {
105
- errors.push(`commands[${i}].handler must be a function`);
106
- }
107
- }
108
- }
109
- }
110
-
111
- if (mod.tools !== undefined) {
112
- if (!Array.isArray(mod.tools)) {
113
- errors.push('tools must be an array');
114
- } else {
115
- for (let i = 0; i < mod.tools.length; i++) {
116
- const t = mod.tools[i];
117
- if (!t.function || !t.function.name || typeof t.function.name !== 'string') {
118
- errors.push(`tools[${i}].function.name must be a non-empty string`);
119
- }
120
- if (t.execute !== undefined && typeof t.execute !== 'function') {
121
- errors.push(`tools[${i}].execute must be a function`);
122
- }
123
- }
124
- }
125
- }
126
-
127
- return { valid: errors.length === 0, errors };
128
- }
129
-
130
- /**
131
- * Load a single .md skill
132
- * @param {string} filePath
133
- * @returns {Object|null}
134
- */
135
- function loadMarkdownSkill(filePath) {
136
- try {
137
- const content = fs.readFileSync(filePath, 'utf-8').trim();
138
- if (!content) return null;
139
- const name = path.basename(filePath, '.md');
140
- return {
141
- name,
142
- type: 'prompt',
143
- filePath,
144
- instructions: content,
145
- commands: [],
146
- tools: [],
147
- };
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
- /**
154
- * Load a single .js skill
155
- * @param {string} filePath
156
- * @returns {Object|null}
157
- */
158
- function loadScriptSkill(filePath) {
159
- try {
160
- const mod = require(filePath);
161
- const { valid, errors } = validateScriptSkill(mod, filePath);
162
- if (!valid) {
163
- console.error(`Skill validation failed: ${filePath}\n ${errors.join('\n ')}`);
164
- return null;
165
- }
166
- const name = mod.name || path.basename(filePath, '.js');
167
- return {
168
- name,
169
- type: 'script',
170
- filePath,
171
- description: mod.description || '',
172
- instructions: mod.instructions || '',
173
- commands: (mod.commands || []).map((c) => ({
174
- cmd: c.cmd.startsWith('/') ? c.cmd : `/${c.cmd}`,
175
- desc: c.desc || c.description || '',
176
- handler: c.handler || null,
177
- })),
178
- tools: (mod.tools || []).map((t) => ({
179
- type: t.type || 'function',
180
- function: {
181
- name: t.function.name,
182
- description: t.function.description || '',
183
- parameters: t.function.parameters || { type: 'object', properties: {} },
184
- },
185
- execute: t.execute || null,
186
- })),
187
- };
188
- } catch (err) {
189
- console.error(`Failed to load skill: ${filePath}: ${err.message}`);
190
- return null;
191
- }
192
- }
193
-
194
- /**
195
- * Load all skills from .nex/skills/
196
- * @returns {Object[]} array of loaded skills
197
- */
198
- function loadAllSkills() {
199
- loadedSkills = [];
200
- const dir = getSkillsDir();
201
- if (!fs.existsSync(dir)) return loadedSkills;
202
-
203
- const disabled = getDisabledSkills();
204
- let entries;
205
- try {
206
- entries = fs.readdirSync(dir);
207
- } catch {
208
- return loadedSkills;
209
- }
210
-
211
- for (const entry of entries) {
212
- const filePath = path.join(dir, entry);
213
- let stat;
214
- try {
215
- stat = fs.statSync(filePath);
216
- } catch {
217
- continue;
218
- }
219
- if (!stat.isFile()) continue;
220
-
221
- let skill = null;
222
- if (entry.endsWith('.md')) {
223
- skill = loadMarkdownSkill(filePath);
224
- } else if (entry.endsWith('.js')) {
225
- skill = loadScriptSkill(filePath);
226
- }
227
-
228
- if (skill) {
229
- skill.enabled = !disabled.includes(skill.name);
230
- loadedSkills.push(skill);
231
- }
232
- }
233
-
234
- return loadedSkills;
235
- }
236
-
237
- /**
238
- * Get combined instructions from all enabled skills
239
- * @returns {string}
240
- */
241
- function getSkillInstructions() {
242
- const parts = [];
243
- for (const skill of loadedSkills) {
244
- if (!skill.enabled || !skill.instructions) continue;
245
- parts.push(`[Skill: ${skill.name}]\n${skill.instructions}`);
246
- }
247
- if (parts.length === 0) return '';
248
- return `SKILL INSTRUCTIONS:\n${parts.join('\n\n')}`;
249
- }
250
-
251
- /**
252
- * Get all commands from enabled skills
253
- * @returns {Array<{cmd: string, desc: string}>}
254
- */
255
- function getSkillCommands() {
256
- const cmds = [];
257
- for (const skill of loadedSkills) {
258
- if (!skill.enabled) continue;
259
- for (const c of skill.commands) {
260
- cmds.push({ cmd: c.cmd, desc: c.desc || `[skill: ${skill.name}]` });
261
- }
262
- }
263
- return cmds;
264
- }
265
-
266
- /**
267
- * Get tool definitions from enabled skills (OpenAI format with skill_ prefix)
268
- * @returns {Array}
269
- */
270
- function getSkillToolDefinitions() {
271
- const defs = [];
272
- for (const skill of loadedSkills) {
273
- if (!skill.enabled) continue;
274
- for (const t of skill.tools) {
275
- defs.push({
276
- type: 'function',
277
- function: {
278
- name: `skill_${t.function.name}`,
279
- description: `[Skill:${skill.name}] ${t.function.description}`,
280
- parameters: t.function.parameters,
281
- },
282
- });
283
- }
284
- }
285
- return defs;
286
- }
287
-
288
- /**
289
- * Route a skill_ tool call to its execute function
290
- * @param {string} fnName
291
- * @param {Object} args
292
- * @returns {Promise<string|null>} null if not a skill tool
293
- */
294
- async function routeSkillCall(fnName, args) {
295
- if (!fnName.startsWith('skill_')) return null;
296
- const toolName = fnName.substring(6);
297
-
298
- for (const skill of loadedSkills) {
299
- if (!skill.enabled) continue;
300
- for (const t of skill.tools) {
301
- if (t.function.name === toolName && t.execute) {
302
- try {
303
- const result = await t.execute(args);
304
- return typeof result === 'string' ? result : JSON.stringify(result);
305
- } catch (err) {
306
- return `ERROR: Skill tool '${toolName}' failed: ${err.message}`;
307
- }
308
- }
309
- }
310
- }
311
-
312
- return `ERROR: Skill tool '${toolName}' not found`;
313
- }
314
-
315
- /**
316
- * Check if input matches a skill command and run its handler
317
- * @param {string} input - full user input (e.g. "/deploy staging")
318
- * @returns {boolean} true if handled
319
- */
320
- function handleSkillCommand(input) {
321
- const [cmd, ...rest] = input.split(/\s+/);
322
- const args = rest.join(' ').trim();
323
-
324
- for (const skill of loadedSkills) {
325
- if (!skill.enabled) continue;
326
- for (const c of skill.commands) {
327
- if (c.cmd === cmd && c.handler) {
328
- try {
329
- c.handler(args);
330
- } catch (err) {
331
- console.error(`Skill command error (${cmd}): ${err.message}`);
332
- }
333
- return true;
334
- }
335
- }
336
- }
337
- return false;
338
- }
339
-
340
- /**
341
- * List all loaded skills with their status
342
- * @returns {Array<{name: string, type: string, enabled: boolean, commands: number, tools: number}>}
343
- */
344
- function listSkills() {
345
- return loadedSkills.map((s) => ({
346
- name: s.name,
347
- type: s.type,
348
- enabled: s.enabled,
349
- description: s.description || '',
350
- commands: s.commands.length,
351
- tools: s.tools.length,
352
- filePath: s.filePath,
353
- }));
354
- }
355
-
356
- /**
357
- * Enable a skill by name
358
- * @param {string} name
359
- * @returns {boolean}
360
- */
361
- function enableSkill(name) {
362
- const skill = loadedSkills.find((s) => s.name === name);
363
- if (!skill) return false;
364
- skill.enabled = true;
365
- const disabled = getDisabledSkills().filter((n) => n !== name);
366
- saveDisabledSkills(disabled);
367
- return true;
368
- }
369
-
370
- /**
371
- * Disable a skill by name
372
- * @param {string} name
373
- * @returns {boolean}
374
- */
375
- function disableSkill(name) {
376
- const skill = loadedSkills.find((s) => s.name === name);
377
- if (!skill) return false;
378
- skill.enabled = false;
379
- const disabled = getDisabledSkills();
380
- if (!disabled.includes(name)) {
381
- disabled.push(name);
382
- saveDisabledSkills(disabled);
383
- }
384
- return true;
385
- }
386
-
387
- /**
388
- * Get loaded skills array (for testing)
389
- * @returns {Object[]}
390
- */
391
- function getLoadedSkills() {
392
- return loadedSkills;
393
- }
394
-
395
- module.exports = {
396
- initSkillsDir,
397
- loadAllSkills,
398
- getSkillInstructions,
399
- getSkillCommands,
400
- getSkillToolDefinitions,
401
- routeSkillCall,
402
- handleSkillCommand,
403
- listSkills,
404
- enableSkill,
405
- disableSkill,
406
- getLoadedSkills,
407
- // exported for testing
408
- _getSkillsDir: getSkillsDir,
409
- _validateScriptSkill: validateScriptSkill,
410
- _loadMarkdownSkill: loadMarkdownSkill,
411
- _loadScriptSkill: loadScriptSkill,
412
- };