diff options
Diffstat (limited to 'modules/core/module-skills.js')
| -rw-r--r-- | modules/core/module-skills.js | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/modules/core/module-skills.js b/modules/core/module-skills.js new file mode 100644 index 0000000..ed6e14f --- /dev/null +++ b/modules/core/module-skills.js @@ -0,0 +1,271 @@ +// Extracted from newOS4 index.html — SkillsModule (v4.2 — Self-improving AI) + class SkillsModule { + constructor() { this.commands = {}; this.terminal = null; } + getHelpInfo() { + return { + 'skill-list': 'skill-list — List all installed skills', + 'skill-view': 'skill-view <name> — View the content of a skill', + 'skill-create': 'skill-create <name> [desc] [category] [tags] — Create a new skill', + 'skill-patch': 'skill-patch <name> [content...] — Update skill content (no args to view current)', + 'skill-delete': 'skill-delete <name> — Delete a skill', + 'skill-export': 'skill-export <name> — Export a skill to WebDAV', + 'skill-import': 'skill-import <name> — Import a skill from WebDAV', + 'skill-sync': 'skill-sync — Bidirectional sync all skills with WebDAV', + }; + } + ensureSkillsDir() { + if (this.terminal && this.terminal.fs) { + const fs = this.terminal.fs; + // Ensure /skills/ directory exists + if (!fs.root.content['skills']) { + fs.root.content['skills'] = { type: 'dir', content: {} }; + fs.saveToStorage(); + } + } + } + registerCommands() { + this.commands['skill-list'] = () => this.listSkills(); + this.commands['skill-view'] = (args) => this.viewSkill(args); + this.commands['skill-create'] = (args) => this.createSkill(args); + this.commands['skill-patch'] = (args) => this.patchSkill(args); + this.commands['skill-delete'] = (args) => this.deleteSkill(args); + this.commands['skill-export'] = (args) => this.exportSkill(args); + this.commands['skill-import'] = (args) => this.importSkill(args); + this.commands['skill-sync'] = () => this.syncSkills(); + } + listSkills() { + this.ensureSkillsDir(); + const index = this.terminal.fs.fsReadJSON('/skills/index.json') || {}; + const names = Object.keys(index); + if (names.length === 0) { + this.terminal.print('No skills found.', 'info'); + return; + } + this.terminal.print(`=== Skills (${names.length}) ===`, 'info'); + names.forEach(name => { + const s = index[name]; + const tags = (s.tags || []).join(', '); + this.terminal.print(`📘 ${name} — ${s.description} [${s.category}] ${tags ? '(' + tags + ')' : ''}`); + }); + } + viewSkill(args) { + const name = (args[0] || '').replace(/\.md$/, ''); + if (!name) { + this.terminal.print('Usage: skill-view <name>', 'warning'); + return; + } + const content = this.terminal.fs.fsRead(`/skills/${name}.md`); + if (!content) { + this.terminal.print(`Skill '${name}' not found`, 'error'); + return; + } + this.terminal.print(`=== ${name} ===`, 'info'); + this.terminal.print(content, 'info'); + } + createSkill(args) { + this.ensureSkillsDir(); + const name = args[0]; + if (!name) { + this.terminal.print('Usage: skill-create <name> [description] [category] [tag1,tag2,...]', 'warning'); + return; + } + const description = args[1] || 'No description'; + const category = args[2] || 'general'; + const tags = (args[3] || '').split(',').filter(Boolean); + const fs = this.terminal.fs; + if (fs.fsRead(`/skills/${name}.md`)) { + this.terminal.print(`Skill '${name}' already exists. Use skill-patch to modify it.`, 'warning'); + return; + } + const content = '---\nname: ' + name + '\ndescription: ' + description + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + category + '\n---\n## Steps\n1. (to be filled)\n'; + fs.fsWrite(`/skills/${name}.md`, content); + const index = fs.fsReadJSON('/skills/index.json') || {}; + index[name] = { name, description, category, tags }; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`Skill '${name}' created. Edit its content with skill-patch ${name}`, 'success'); + } + patchSkill(args) { + const name = args[0]; + if (!name) { + this.terminal.print('Usage: skill-patch <name> [content...]', 'warning'); + return; + } + const fs = this.terminal.fs; + const existing = fs.fsRead(`/skills/${name}.md`); + if (!existing) { + this.terminal.print(`Skill '${name}' not found. Use skill-create first.`, 'error'); + return; + } + if (args.length < 2) { + // No new content — show the skill so user can copy-paste + this.terminal.print(`=== ${name} (current content) ===`, 'info'); + this.terminal.print(existing, 'info'); + this.terminal.print('---', 'info'); + this.terminal.print('Pass new content as additional arguments: skill-patch <name> <full content>', 'warning'); + return; + } + const newContent = args.slice(1).join(' '); + fs.fsWrite(`/skills/${name}.md`, newContent); + this.terminal.print(`Skill '${name}' patched`, 'success'); + } + deleteSkill(args) { + const name = (args[0] || '').replace(/\.md$/, ''); + if (!name) { + this.terminal.print('Usage: skill-delete <name>', 'warning'); + return; + } + const fs = this.terminal.fs; + if (!fs.fsRead(`/skills/${name}.md`)) { + this.terminal.print(`Skill '${name}' not found`, 'error'); + return; + } + fs.fsWrite(`/skills/${name}.md`, ''); + const index = fs.fsReadJSON('/skills/index.json') || {}; + delete index[name]; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`Skill '${name}' deleted`, 'success'); + } + getWebDAVModule() { + return this.terminal && this.terminal.moduleManager.modules['WebDAVModule']; + } + async exportSkill(args) { + const name = (args[0] || '').replace(/\.md$/, ''); + if (!name) { + this.terminal.print('Usage: skill-export <name>', 'warning'); + return; + } + const wd = this.getWebDAVModule(); + if (!wd || !wd.config || !wd.config.url) { + this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning'); + return; + } + this.ensureSkillsDir(); + const fs = this.terminal.fs; + const content = fs.fsRead(`/skills/${name}.md`); + if (!content) { + this.terminal.print(`Skill '${name}' not found in VFS`, 'error'); + return; + } + const index = fs.fsReadJSON('/skills/index.json') || {}; + const meta = index[name]; + try { + // Ensure remote skills dir + if (typeof wd._ensureDir === 'function') await wd._ensureDir('skills'); + // Push the skill file + const resp1 = await wd._webdavRequest('PUT', 'skills/' + name + '.md', { + body: content, + headers: { 'Content-Type': 'text/markdown' } + }); + if (!resp1.ok && resp1.status !== 201 && resp1.status !== 204) { + throw new Error('Upload failed: HTTP ' + resp1.status); + } + // Push updated index + const indexResp = await wd._webdavRequest('PUT', 'skills/index.json', { + body: JSON.stringify(index, null, 2), + headers: { 'Content-Type': 'application/json' } + }); + if (indexResp.ok || indexResp.status === 201 || indexResp.status === 204) { + this.terminal.print(`Skill '${name}' exported to WebDAV skills/`, 'success'); + } + } catch (e) { + this.terminal.print('Export failed: ' + e.message, 'error'); + } + } + async importSkill(args) { + const name = (args[0] || '').replace(/\.md$/, ''); + if (!name) { + this.terminal.print('Usage: skill-import <name>', 'warning'); + return; + } + const wd = this.getWebDAVModule(); + if (!wd || !wd.config || !wd.config.url) { + this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning'); + return; + } + try { + const resp = await wd._webdavRequest('GET', 'skills/' + name + '.md'); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const content = await resp.text(); + this.ensureSkillsDir(); + const fs = this.terminal.fs; + fs.fsWrite(`/skills/${name}.md`, content); + // Try to pull the index to get metadata; if fails, create minimal entry + let meta = { name, description: 'Imported from WebDAV', category: 'general', tags: [] }; + try { + const indexResp = await wd._webdavRequest('GET', 'skills/index.json'); + if (indexResp.ok) { + const remoteIndex = JSON.parse(await indexResp.text()); + if (remoteIndex && remoteIndex[name]) { + meta = remoteIndex[name]; + } + } + } catch (e) { /* use default meta */ } + const index = fs.fsReadJSON('/skills/index.json') || {}; + index[name] = meta; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`Skill '${name}' imported from WebDAV (${content.length} bytes)`, 'success'); + } catch (e) { + this.terminal.print('Import failed: ' + e.message, 'error'); + } + } + async syncSkills() { + const wd = this.getWebDAVModule(); + if (!wd || !wd.config || !wd.config.url) { + this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning'); + return; + } + this.ensureSkillsDir(); + const fs = this.terminal.fs; + const localIndex = fs.fsReadJSON('/skills/index.json') || {}; + const localNames = new Set(Object.keys(localIndex)); + this.terminal.print('Syncing skills with WebDAV...', 'info'); + let imported = 0, exported = 0; + try { + // Try to get remote index + let remoteIndex = {}; + try { + const resp = await wd._webdavRequest('GET', 'skills/index.json'); + if (resp.ok) { + remoteIndex = JSON.parse(await resp.text()) || {}; + } + } catch (e) { /* no remote index yet */ } + const remoteNames = new Set(Object.keys(remoteIndex)); + // Export local-only skills to remote + for (const name of localNames) { + if (!remoteNames.has(name)) { + const content = fs.fsRead(`/skills/${name}.md`); + if (content) { + await wd._ensureDir('skills'); + await wd._webdavRequest('PUT', 'skills/' + name + '.md', { + body: content, + headers: { 'Content-Type': 'text/markdown' } + }); + exported++; + } + } + } + // Import remote-only skills to local + for (const name of remoteNames) { + if (!localNames.has(name)) { + const resp = await wd._webdavRequest('GET', 'skills/' + name + '.md'); + if (resp.ok) { + const content = await resp.text(); + fs.fsWrite(`/skills/${name}.md`, content); + localIndex[name] = remoteIndex[name]; + imported++; + } + } + } + // Push updated local index back to remote + fs.fsWriteJSON('/skills/index.json', localIndex); + await wd._ensureDir('skills'); + await wd._webdavRequest('PUT', 'skills/index.json', { + body: JSON.stringify(localIndex, null, 2), + headers: { 'Content-Type': 'application/json' } + }); + this.terminal.print(`Sync complete: ${exported} exported, ${imported} imported`, 'success'); + } catch (e) { + this.terminal.print('Sync failed: ' + e.message, 'error'); + } + } + } |