// Extracted from newOS4 index.html — SkillsModule (v4.2 — Self-improving AI) class SkillsModule { constructor() { this.commands = {}; this.terminal = null; this._skillIndexCache = null; this._skillIndexDirty = false; this._skillIndexFlushTimer = null; } _readSkillIndex() { if (this._skillIndexCache !== null) return this._skillIndexCache; const fs = this.terminal.fs; this._skillIndexCache = fs.fsReadJSON('/skills/index.json') || {}; return this._skillIndexCache; } _markSkillIndexDirty() { this._skillIndexDirty = true; if (this._skillIndexFlushTimer) clearTimeout(this._skillIndexFlushTimer); this._skillIndexFlushTimer = setTimeout(() => this._flushSkillIndex(), 2000); } _flushSkillIndex() { if (!this._skillIndexDirty || this._skillIndexCache === null) return; const fs = this.terminal.fs; fs.fsWriteJSON('/skills/index.json', this._skillIndexCache); this._skillIndexDirty = false; this._skillIndexFlushTimer = null; } getHelpInfo() { return { 'skill-list': 'skill-list — List all installed skills', 'skill-view': 'skill-view — View the content of a skill', 'skill-create': 'skill-create [desc] [category] [tags] — Create a new skill', 'skill-patch': 'skill-patch [content...] — Update skill content (no args to view current)', 'skill-delete': 'skill-delete — Delete a skill', 'skill-export': 'skill-export — Export a skill to WebDAV', 'skill-import': 'skill-import — 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._readSkillIndex(); 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 ', 'warning'); return; } const content = this.terminal.fs.readFile(`/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 [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.readFile(`/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.writeFile(`/skills/${name}.md`, content); const index = this._readSkillIndex(); index[name] = { name, description, category, tags }; this._markSkillIndexDirty(); 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 [content...]', 'warning'); return; } const fs = this.terminal.fs; const existing = fs.readFile(`/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 ', 'warning'); return; } const newContent = args.slice(1).join(' '); fs.writeFile(`/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 ', 'warning'); return; } const fs = this.terminal.fs; if (!fs.readFile(`/skills/${name}.md`)) { this.terminal.print(`Skill '${name}' not found`, 'error'); return; } fs.writeFile(`/skills/${name}.md`, ''); const index = this._readSkillIndex(); delete index[name]; this._markSkillIndexDirty(); 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 ', '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.readFile(`/skills/${name}.md`); if (!content) { this.terminal.print(`Skill '${name}' not found in VFS`, 'error'); return; } const index = this._readSkillIndex(); 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 ', '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.writeFile(`/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 = this._readSkillIndex(); index[name] = meta; this._markSkillIndexDirty(); 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 = this._readSkillIndex(); 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.readFile(`/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.writeFile(`/skills/${name}.md`, content); localIndex[name] = remoteIndex[name]; imported++; } } } // Push updated local index back to remote this._markSkillIndexDirty(); 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'); } } }