// 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');
}
}
}