aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-27 06:00:43 +0200
committerAgando <roduccomm@gmail.com>2026-05-27 06:00:43 +0200
commit212708b464c2bdbdd9670c00804623dca0729d2c (patch)
treeee190cbb0d1524350baea641df65aa19817744b5 /modules/core
parentf4d934d341056142da1212c845eeeb170fd452c2 (diff)
auto: 2026-05-27_06:00 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-aiassist.js49
-rw-r--r--modules/core/module-fs.js49
-rw-r--r--modules/core/module-loader.js7
-rw-r--r--modules/core/module-manager.js24
-rw-r--r--modules/core/module-skills.js68
-rw-r--r--modules/core/module-system.js95
-rw-r--r--modules/core/terminal.js452
-rw-r--r--modules/core/virtual-fs.js440
8 files changed, 908 insertions, 276 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index f9e1e98..e983e6a 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -538,6 +538,22 @@ RULES:
}
return null;
}
+ _trackToolFailure(toolName, error) {
+ try {
+ const key = 'webx11_tool_failures';
+ const raw = localStorage.getItem(key) || '{}';
+ const map = JSON.parse(raw);
+ if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] };
+ map[toolName].count++;
+ map[toolName].last = new Date().toISOString();
+ map[toolName].errors.push(error);
+ if (map[toolName].errors.length > 5) map[toolName].errors.shift();
+ localStorage.setItem(key, JSON.stringify(map));
+ if (map[toolName].count >= 3) {
+ this.terminal.print(`⚠️ Tool '${toolName}' has failed ${map[toolName].count} times. Consider creating a workaround skill.`, 'warning');
+ }
+ } catch (e) { /* ignore */ }
+ }
async agenticLoop(messages) {
let toolTurns = 0;
while (toolTurns < this.MAX_TOOL_TURNS) {
@@ -579,8 +595,11 @@ RULES:
const obs = this._summarizeObservation(toolResult, 150);
const err = this._isToolError(toolResult);
if (err) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
+ this._trackToolFailure(fn.name, err);
this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
} else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
}
messages.push({ role: 'tool', content: toolResult });
@@ -598,8 +617,11 @@ RULES:
const dObs = this._summarizeObservation(toolResult, 150);
const dErr = this._isToolError(toolResult);
if (dErr) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
+ this._trackToolFailure(fn.name, dErr);
this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning');
} else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
}
messages.push({ role: 'tool', content: toolResult });
@@ -803,8 +825,9 @@ RULES:
return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module';
}
async runTool(name, argsStr) {
+ let a = {};
try {
- const a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
+ a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
switch (name) {
case 'list_files': {
const originalPath = a.path || '';
@@ -1058,7 +1081,7 @@ RULES:
for (const [name, meta] of Object.entries(index)) {
const score = (name.includes(query) ? 3 : 0) + (meta.description.toLowerCase().includes(query) ? 2 : 0) + ((meta.tags || []).some(t => t.toLowerCase().includes(query)) ? 2 : 0);
if (score > 0) {
- const content = fs.fsRead(`/skills/${name}.md`) || '';
+ const content = fs.readFile(`/skills/${name}.md`) || '';
matches.push({ name, description: meta.description, category: meta.category, score, content: content.substring(0, 2000) });
}
}
@@ -1078,7 +1101,7 @@ RULES:
}
const fs = this.terminal.fs;
fs.mkdirRecursive('/skills');
- fs.fsWrite(`/skills/${name}.md`, content);
+ fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
index[name] = { name, description, tags, category, created: index[name]?.created || new Date().toISOString(), updated: new Date().toISOString() };
fs.fsWriteJSON('/skills/index.json', index);
@@ -1093,7 +1116,7 @@ RULES:
const fs = this.terminal.fs;
const index = fs.fsReadJSON('/skills/index.json') || {};
if (!index[pname]) return JSON.stringify({ error: `Skill "${pname}" not found` });
- fs.fsWrite(`/skills/${pname}.md`, pcontent);
+ fs.writeFile(`/skills/${pname}.md`, pcontent);
index[pname].updated = new Date().toISOString();
// Re-parse frontmatter for metadata updates
const metaMatch = pcontent.match(/^---\n([\s\S]*?)\n---/);
@@ -1134,8 +1157,10 @@ RULES:
}
}
async runModuleTool(name, argsStr) {
- const a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
- switch (name) {
+ let a = {};
+ try {
+ a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
+ switch (name) {
case 'view_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
@@ -1211,6 +1236,10 @@ RULES:
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
+ }
+ } catch (e) {
+ this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
+ return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
clearContext() {
@@ -1358,11 +1387,11 @@ category: productivity
- User says "map out", "diagram", "brainstorm", "organize thoughts"
- Visualizing project structure or task decomposition`;
- fs.fsWrite('/skills/mindmap-user-guide.md', mindmapSkill);
+ fs.writeFile('/skills/mindmap-user-guide.md', mindmapSkill);
const existingIndex = fs.fsReadJSON('/skills/index.json') || {};
existingIndex['mindmap-user-guide'] = { name: 'mindmap-user-guide', description: 'How to create, view, and manage brainstorming mindmaps using the mindmap terminal module', tags: ['mindmap', 'brainstorming', 'organization', 'planning'], category: 'productivity', created: existingIndex['mindmap-user-guide']?.created || new Date().toISOString(), updated: new Date().toISOString() };
fs.fsWriteJSON('/skills/index.json', existingIndex);
- fs.fsWrite('/skills/_seeded', '1');
+ fs.writeFile('/skills/_seeded', '1');
}
_trackPattern(userMessage) {
const lower = userMessage.toLowerCase();
@@ -1381,7 +1410,7 @@ category: productivity
for (const [tool, errors] of Object.entries(grouped)) {
if (errors.length >= 3) {
const skillName = 'tool-' + tool + '-workaround';
- const existing = this.terminal.fs.fsRead('/skills/' + skillName + '.md');
+ const existing = this.terminal.fs.readFile('/skills/' + skillName + '.md');
if (existing) continue;
const steps = errors.map((e, i) => (i + 1) + '. When calling ' + tool + ', avoid: "' + JSON.stringify(e.args) + '" which caused: ' + e.error).join('\n');
const content = '---\nname: ' + skillName + '\ndescription: Workarounds for ' + tool + ' failures\ntags: [' + tool + ', error, workaround]\ncategory: system\n---\n## Steps\n' + steps;
@@ -1397,7 +1426,7 @@ category: productivity
const now = Date.now();
const issues = [];
for (const [name, meta] of Object.entries(index)) {
- const content = fs.fsRead('/skills/' + name + '.md');
+ const content = fs.readFile('/skills/' + name + '.md');
if (!content || content.length < 50) {
issues.push({ name, issue: 'empty or too short' });
continue;
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index c4bc786..4ba34b0 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -5,7 +5,11 @@ class FileSystemModule {
'ls': 'ls [path] [-la] — List files and directories',
'cat': 'cat <file> — Display file contents',
'put': 'put <file> <content> — Create or overwrite a file',
- 'rm': 'rm <file> — Remove a file; use -r/-rf for directories',
+ 'rm': 'rm <file> — Remove a file (moved to trash); use -r/-rf for directories',
+ 'trash': 'trash <file> — Move a file to trash',
+ 'trash-dir': 'trash-dir <dir> — Move a directory to trash',
+ 'ls-trash': 'ls-trash — List items in trash',
+ 'empty-trash': 'empty-trash — Permanently delete all trash items',
'mkdir': 'mkdir <dir> [-p] — Create a directory; -p creates parent paths',
'rmdir': 'rmdir <dir> — Remove a directory',
'mv': 'mv <source> <dest> — Move file or directory',
@@ -80,13 +84,52 @@ class FileSystemModule {
if(!args[0]) return this.terminal.print('Usage: rm <filename>', 'warning');
if(args[0] === '-r' || args[0] === '-rf') {
const t = args[1]; if (!t) return this.terminal.print('Usage: rm -r <dirname>', 'warning');
- if (this.terminal.fs.deleteDirRecursive(t)) this.terminal.print('Removed: ' + t, 'success');
+ // Use trash for recursive dir removal too
+ if (this.terminal.fs.trashDir(t)) this.terminal.print('Moved to trash: ' + t, 'success');
else this.terminal.print('rm: ' + t + ': No such file or directory', 'error');
return;
}
- if(this.terminal.fs.deleteFile(args[0])) this.terminal.print('Removed: ' + args[0], 'success');
+ if(this.terminal.fs.deleteFile(args[0])) this.terminal.print('Moved to trash: ' + args[0], 'success');
else this.terminal.print('rm: ' + args[0] + ': No such file', 'error');
};
+ this.commands['trash'] = (args) => {
+ const fs = this.terminal.fs;
+ if(!args[0]) return this.terminal.print('Usage: trash <filename>', 'warning');
+ const node = fs.readFile(args[0]) !== null ? { type: 'file' } : fs.readDir(args[0]);
+ if (!node) return this.terminal.print('trash: ' + args[0] + ': No such file', 'error');
+ if (node.type === 'dir') {
+ if (fs.trashDir(args[0])) this.terminal.print('Moved to trash: ' + args[0], 'success');
+ else this.terminal.print('trash: ' + args[0] + ': Failed', 'error');
+ } else {
+ const content = fs.readFile(args[0]);
+ if (content !== null) {
+ fs.trashFile(args[0], { type: 'file', content, modified: new Date() });
+ this.terminal.print('Moved to trash: ' + args[0], 'success');
+ } else {
+ this.terminal.print('trash: ' + args[0] + ': No such file', 'error');
+ }
+ }
+ };
+ this.commands['trash-dir'] = (args) => {
+ if(!args[0]) return this.terminal.print('Usage: trash-dir <dirname>', 'warning');
+ if (this.terminal.fs.trashDir(args[0])) this.terminal.print('Moved to trash: ' + args[0], 'success');
+ else this.terminal.print('trash-dir: ' + args[0] + ': No such directory', 'error');
+ };
+ this.commands['ls-trash'] = () => {
+ const fs = this.terminal.fs;
+ const items = fs.listTrash ? fs.listTrash() : [];
+ if (!items.length) return this.terminal.print('Trash is empty', 'info');
+ this.terminal.print('Trash items:', 'info');
+ items.forEach(it => {
+ const date = new Date(it.ts).toLocaleString();
+ this.terminal.print(` ${it.name} [${it.type}] ${date}`);
+ });
+ };
+ this.commands['empty-trash'] = () => {
+ const fs = this.terminal.fs;
+ const removed = fs.emptyTrash ? fs.emptyTrash(0) : 0;
+ this.terminal.print(`Emptied trash: ${removed} item(s) permanently deleted`, removed > 0 ? 'warning' : 'info');
+ };
this.commands['mkdir'] = (args) => {
if(!args[0]) return this.terminal.print('Usage: mkdir <dirname>', 'warning');
if (args[0] === '-p') {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 850d627..2510fde 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -99,7 +99,10 @@
window.__moduleLoaderReload = reloadWithSelection;
- const CACHE = '?v=18';
+ const CACHE = '?v=' + (localStorage.getItem('webx11_cache_ver') || '2026-05-27-14');
+ if (!localStorage.getItem('webx11_cache_ver')) {
+ localStorage.setItem('webx11_cache_ver', '2026-05-27-14');
+ }
async function init() {
try {
@@ -153,7 +156,7 @@
// Phase 4: Ext modules (filtered)
if (typeof webx11._loadExtModules === 'function') {
- const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr'];
+ const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip'];
const extSelected = selected
? extKeys.filter(k => isSelected(k + '.js', selected))
: extKeys;
diff --git a/modules/core/module-manager.js b/modules/core/module-manager.js
index 8e9e911..c508778 100644
--- a/modules/core/module-manager.js
+++ b/modules/core/module-manager.js
@@ -42,10 +42,12 @@
}
createModuleClass(code, url) {
try {
- const classMatch = code.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/);
+ // Strip leading comments to avoid ASI: `return\n/**` → `return; /**`
+ const cleanCode = code.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, '');
+ const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/);
if (classMatch) {
const className = classMatch[1];
- const mod = new Function('return ' + code + '\nreturn ' + className)();
+ const mod = new Function('return ' + cleanCode + '\nreturn ' + className)();
return mod;
}
throw new Error('No valid module class found');
@@ -78,6 +80,24 @@
}
return false;
}
+ loadModuleFromCode(code, url) {
+ try {
+ this.terminal.print('Loading module from editor...', 'info');
+ const ModuleClass = this.createModuleClass(code, url);
+ if (ModuleClass) {
+ const instance = new ModuleClass();
+ this.registerModule(instance, url);
+ if (instance.init) {
+ instance.init();
+ }
+ this.terminal.print(`Module loaded from editor: ${instance.constructor.name}`, 'success');
+ return true;
+ }
+ } catch (error) {
+ this.terminal.print(`Failed to load module from code: ${error.message}`, 'error');
+ return false;
+ }
+ }
getModuleList() {
const allNames = new Set([...this.moduleRegistry.map(m => m.name), ...Object.keys(this.modules)]);
return Array.from(allNames).map(name => ({
diff --git a/modules/core/module-skills.js b/modules/core/module-skills.js
index ed6e14f..9d0adec 100644
--- a/modules/core/module-skills.js
+++ b/modules/core/module-skills.js
@@ -1,6 +1,30 @@
// Extracted from newOS4 index.html — SkillsModule (v4.2 — Self-improving AI)
class SkillsModule {
- constructor() { this.commands = {}; this.terminal = null; }
+ 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',
@@ -35,7 +59,7 @@
}
listSkills() {
this.ensureSkillsDir();
- const index = this.terminal.fs.fsReadJSON('/skills/index.json') || {};
+ const index = this._readSkillIndex();
const names = Object.keys(index);
if (names.length === 0) {
this.terminal.print('No skills found.', 'info');
@@ -54,7 +78,7 @@
this.terminal.print('Usage: skill-view <name>', 'warning');
return;
}
- const content = this.terminal.fs.fsRead(`/skills/${name}.md`);
+ const content = this.terminal.fs.readFile(`/skills/${name}.md`);
if (!content) {
this.terminal.print(`Skill '${name}' not found`, 'error');
return;
@@ -73,15 +97,15 @@
const category = args[2] || 'general';
const tags = (args[3] || '').split(',').filter(Boolean);
const fs = this.terminal.fs;
- if (fs.fsRead(`/skills/${name}.md`)) {
+ 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.fsWrite(`/skills/${name}.md`, content);
- const index = fs.fsReadJSON('/skills/index.json') || {};
+ fs.writeFile(`/skills/${name}.md`, content);
+ const index = this._readSkillIndex();
index[name] = { name, description, category, tags };
- fs.fsWriteJSON('/skills/index.json', index);
+ this._markSkillIndexDirty();
this.terminal.print(`Skill '${name}' created. Edit its content with skill-patch ${name}`, 'success');
}
patchSkill(args) {
@@ -91,7 +115,7 @@
return;
}
const fs = this.terminal.fs;
- const existing = fs.fsRead(`/skills/${name}.md`);
+ const existing = fs.readFile(`/skills/${name}.md`);
if (!existing) {
this.terminal.print(`Skill '${name}' not found. Use skill-create first.`, 'error');
return;
@@ -105,7 +129,7 @@
return;
}
const newContent = args.slice(1).join(' ');
- fs.fsWrite(`/skills/${name}.md`, newContent);
+ fs.writeFile(`/skills/${name}.md`, newContent);
this.terminal.print(`Skill '${name}' patched`, 'success');
}
deleteSkill(args) {
@@ -115,14 +139,14 @@
return;
}
const fs = this.terminal.fs;
- if (!fs.fsRead(`/skills/${name}.md`)) {
+ if (!fs.readFile(`/skills/${name}.md`)) {
this.terminal.print(`Skill '${name}' not found`, 'error');
return;
}
- fs.fsWrite(`/skills/${name}.md`, '');
- const index = fs.fsReadJSON('/skills/index.json') || {};
+ fs.writeFile(`/skills/${name}.md`, '');
+ const index = this._readSkillIndex();
delete index[name];
- fs.fsWriteJSON('/skills/index.json', index);
+ this._markSkillIndexDirty();
this.terminal.print(`Skill '${name}' deleted`, 'success');
}
getWebDAVModule() {
@@ -141,12 +165,12 @@
}
this.ensureSkillsDir();
const fs = this.terminal.fs;
- const content = fs.fsRead(`/skills/${name}.md`);
+ const content = fs.readFile(`/skills/${name}.md`);
if (!content) {
this.terminal.print(`Skill '${name}' not found in VFS`, 'error');
return;
}
- const index = fs.fsReadJSON('/skills/index.json') || {};
+ const index = this._readSkillIndex();
const meta = index[name];
try {
// Ensure remote skills dir
@@ -188,7 +212,7 @@
const content = await resp.text();
this.ensureSkillsDir();
const fs = this.terminal.fs;
- fs.fsWrite(`/skills/${name}.md`, content);
+ 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 {
@@ -200,9 +224,9 @@
}
}
} catch (e) { /* use default meta */ }
- const index = fs.fsReadJSON('/skills/index.json') || {};
+ const index = this._readSkillIndex();
index[name] = meta;
- fs.fsWriteJSON('/skills/index.json', index);
+ this._markSkillIndexDirty();
this.terminal.print(`Skill '${name}' imported from WebDAV (${content.length} bytes)`, 'success');
} catch (e) {
this.terminal.print('Import failed: ' + e.message, 'error');
@@ -216,7 +240,7 @@
}
this.ensureSkillsDir();
const fs = this.terminal.fs;
- const localIndex = fs.fsReadJSON('/skills/index.json') || {};
+ const localIndex = this._readSkillIndex();
const localNames = new Set(Object.keys(localIndex));
this.terminal.print('Syncing skills with WebDAV...', 'info');
let imported = 0, exported = 0;
@@ -233,7 +257,7 @@
// Export local-only skills to remote
for (const name of localNames) {
if (!remoteNames.has(name)) {
- const content = fs.fsRead(`/skills/${name}.md`);
+ const content = fs.readFile(`/skills/${name}.md`);
if (content) {
await wd._ensureDir('skills');
await wd._webdavRequest('PUT', 'skills/' + name + '.md', {
@@ -250,14 +274,14 @@
const resp = await wd._webdavRequest('GET', 'skills/' + name + '.md');
if (resp.ok) {
const content = await resp.text();
- fs.fsWrite(`/skills/${name}.md`, content);
+ fs.writeFile(`/skills/${name}.md`, content);
localIndex[name] = remoteIndex[name];
imported++;
}
}
}
// Push updated local index back to remote
- fs.fsWriteJSON('/skills/index.json', localIndex);
+ this._markSkillIndexDirty();
await wd._ensureDir('skills');
await wd._webdavRequest('PUT', 'skills/index.json', {
body: JSON.stringify(localIndex, null, 2),
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index f21946a..b561ef2 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -14,6 +14,9 @@
'factory-reset': 'factory-reset [--confirm] — Wipe all webx11_ data (VFS, config, AI profile, mindmaps, themes, all localStorage)',
'reset-vfs': 'reset-vfs [--confirm] — Clear only the virtual filesystem (keeps settings, modules, AI profile)',
'logs': 'logs [n] [LEVEL] — Show last n log entries (optionally filtered by level: INFO/WARN/ERROR)',
+ 'logrotate': 'logrotate — Rotate current.log to a dated archive (auto-cleans old)',
+ 'health-check': 'health-check — Audit VFS size, modules, logs, trash, skills, tool failures',
+ 'memory-map-export': 'memory-map-export — Generate memory map markdown from memory/index.json',
'font-size': 'font-size <8-32> — Set terminal font size (persists across sessions)',
'setfont': 'setfont <8-32> — Alias for font-size',
'smart-prompt': 'smart-prompt [on|off] — Toggle context-aware prompt (shows name, skills, sessions, memory, edits)',
@@ -92,6 +95,93 @@
this.terminal.print('--- Logs (last ' + tail.length + ') ---', 'info');
tail.forEach(l => this.terminal.print(l, l.includes('[ERROR]') ? 'error' : 'info'));
};
+ // Auto-rotate logs on startup (silently)
+ this._autoRotateLogs();
+ this.commands['logrotate'] = () => {
+ const rotated = this._autoRotateLogs(true);
+ if (rotated) {
+ this.terminal.print('Log rotated: ' + rotated, 'success');
+ } else {
+ this.terminal.print('Log rotation not needed (<50KB, <500 lines)', 'info');
+ }
+ };
+ this.commands['health-check'] = () => this._healthCheck();
+ this.commands['memory-map-export'] = () => this._memoryMapExport();
+ }
+ _autoRotateLogs(force = false) {
+ try {
+ const content = this.terminal.fs.readFile('/logs/current.log') || '';
+ const lines = content.split('\n').filter(l => l);
+ const size = content.length;
+ if (!force && size < 51200 && lines.length < 500) return null;
+ const now = new Date();
+ const stamp = now.getFullYear() + '-' +
+ String(now.getMonth()+1).padStart(2,'0') + '-' +
+ String(now.getDate()).padStart(2,'0') + '_' +
+ String(now.getHours()).padStart(2,'0') + '00';
+ this.terminal.fs.writeFile('/logs/' + stamp + '.log', content);
+ this.terminal.fs.writeFile('/logs/current.log', '');
+ // Clean oldest rotated logs (>10)
+ const logsDir = this.terminal.fs.readDir('/logs');
+ if (logsDir && logsDir.content) {
+ const rotated = Object.keys(logsDir.content)
+ .filter(k => k.endsWith('.log') && k !== 'current.log')
+ .sort();
+ while (rotated.length > 10) {
+ const oldest = rotated.shift();
+ this.terminal.fs.deleteFile('/logs/' + oldest);
+ }
+ }
+ return stamp + '.log';
+ } catch (e) { return null; }
+ }
+ _healthCheck() {
+ const fs = this.terminal.fs;
+ let warnings = 0;
+ this.terminal.print('╔══════════════════════════════════════╗', 'info');
+ this.terminal.print('║ 🔍 SYSTEM HEALTH CHECK ║', 'info');
+ this.terminal.print('╚══════════════════════════════════════╝', 'info');
+ // VFS size
+ const dump = JSON.stringify(fs.root).length;
+ const vfsOk = dump < 524288;
+ this.terminal.print(`VFS size: ${(dump/1024).toFixed(1)}KB ${vfsOk ? '✅' : '⚠️ > 512KB'}`, vfsOk ? 'info' : 'warning');
+ if (!vfsOk) warnings++;
+ // Modules
+ const modTotal = Object.keys(this.terminal.moduleManager.modules || {}).length;
+ this.terminal.print(`Modules loaded: ${modTotal} ✅`, 'info');
+ // Logs
+ const logContent = fs.readFile('/logs/current.log') || '';
+ const logLines = logContent.split('\n').filter(l => l).length;
+ this.terminal.print(`Current log: ${logLines} lines`, logLines > 500 ? 'warning' : 'info');
+ if (logLines > 500) warnings++;
+ // Trash
+ const trashList = fs.listTrash ? fs.listTrash() : [];
+ this.terminal.print(`Trash items: ${trashList.length}`, 'info');
+ // Skills
+ const skillIdx = fs.fsReadJSON ? (fs.fsReadJSON('/skills/index.json') || {}) : {};
+ const skillCount = Object.keys(skillIdx).length;
+ this.terminal.print(`Skills: ${skillCount}`, skillCount > 0 ? 'info' : 'warning');
+ if (skillCount === 0) warnings++;
+ // Summary
+ this.terminal.print('─'.repeat(40), 'text-muted');
+ if (warnings === 0) {
+ this.terminal.print('All checks passed ✅', 'success');
+ } else {
+ this.terminal.print(`${warnings} warning(s) found`, 'warning');
+ }
+ }
+ _memoryMapExport() {
+ const fs = this.terminal.fs;
+ const idx = fs.fsReadJSON ? (fs.fsReadJSON('/memory/index.json') || {}) : null;
+ if (!idx) {
+ this.terminal.print('No memory index found at /memory/index.json', 'error');
+ return;
+ }
+ this.terminal.print('---\nname: adaptive-memory-map\ndescription: Auto-generated from memory/index.json\ntags: [memory, context, tracking, adaptation]\ncategory: system\n---\n', 'info');
+ for (const [k,v] of Object.entries(idx)) {
+ this.terminal.print(`## ${k}\n- Tasks: ${v.tasks || 0}\n- Updated: ${v.updated || 'unknown'}\n- Summary: ${(v.summary || '').substring(0,100)}...`, 'info');
+ }
+ this.terminal.print('(Not saved — render to file with memory-map-export > /memory/reference.md if desired)', 'text-muted');
}
handleHelp(args) {
const helpMap = this.terminal.moduleManager.getAllHelp();
@@ -116,8 +206,9 @@
files: ['ls','ll','cat','put','rm','mv','cp','mkdir','rmdir','cd','pwd','find','grep','head','tail','sort','wc','touch','tree','du','upload','download','vfs-export','vfs-import','edit','diff'],
ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-stop','ai-model'],
search: ['search','s','ddg','duck','fetch','web','extract'],
- tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias'],
- system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs'],
+ tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias',
+ 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand'],
+ system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test'],
net: ['server','servers','server-check','server-remove','monitor'],
mods: ['modules','listmods','loadmod','removemod','load-local','loadlocal','startup-modules'],
ollama: ['ollama','ollamaconnect','ollamadisconnect','ollamamodels','ollamasetmodel','ollamasetproxy','models','model-info','model-pull','model-delete','model-run'],
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index ee1afa4..2617f79 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -1,4 +1,184 @@
// Extracted from newOS4 index.html — WebX11Terminal
+ class WebX11SecurityManager {
+ constructor(terminal, level = 1) {
+ this.terminal = terminal;
+ this.level = level;
+ this.commandMeta = new Map();
+ this.lastRouteTime = 0;
+ this.lastRouteCmd = '';
+ this.blockedCount = 0;
+ this.destructiveCmds = new Set(['rm','delete','del','edit','write','save','overwrite','format','kill','stop','shutdown','reboot']);
+ this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
+ this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
+ this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history']);
+ this._registerBuiltIn();
+ }
+ _registerBuiltIn() {
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','startup-modules','smart-prompt','cmd-allow','cmd-deny'];
+ internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
+ }
+ setLevel(n) {
+ if (n >= 0 && n <= 3) {
+ this.level = n;
+ this.terminal.print(`Security level set to ${n} (${this.getLevelName()})`, 'success');
+ if (n === 0) {
+ this.terminal.print('Warning: Permissive mode — commands will not be blocked or confirmed.', 'warning');
+ }
+ }
+ else { this.terminal.print('Level must be 0-3', 'error'); }
+ }
+ getLevel() { return this.level; }
+ getLevelName() {
+ return ['Permissive','Standard','Paranoid','Audit'][this.level] || 'Unknown';
+ }
+ registerCommand(name, meta = {}) {
+ this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ }
+ _collectAllCommands() {
+ const cmds = new Set([...this.commandMeta.keys()]);
+ for (const modName in this.terminal.moduleManager.modules) {
+ const mod = this.terminal.moduleManager.modules[modName];
+ if (mod.commands) {
+ for (const c in mod.commands) cmds.add(c);
+ }
+ }
+ const builtins = ['clear','help','ls','df','security','security-level'];
+ builtins.forEach(c => cmds.add(c));
+ return [...cmds];
+ }
+ _levenshtein(a, b) {
+ a = a.toLowerCase(); b = b.toLowerCase();
+ const m = a.length, n = b.length;
+ if (!m) return n; if (!n) return m;
+ let prev = new Uint16Array(n + 1);
+ let curr = new Uint16Array(n + 1);
+ for (let j = 0; j <= n; j++) prev[j] = j;
+ for (let i = 1; i <= m; i++) {
+ let next = new Uint16Array(n + 1);
+ next[0] = i;
+ for (let j = 1; j <= n; j++) {
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
+ next[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
+ }
+ [prev, curr] = [curr, next];
+ }
+ return prev[n];
+ }
+ suggest(input, maxDist = 2) {
+ const all = this._collectAllCommands();
+ const lower = input.toLowerCase();
+ let best = '', bestDist = Infinity;
+ for (const c of all) {
+ const d = this._levenshtein(lower, c);
+ if (d < bestDist && d <= maxDist) { bestDist = d; best = c; }
+ }
+ return best ? { cmd: best, dist: bestDist } : null;
+ }
+ _classify(raw) {
+ const parts = raw.trim().split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // Internal commands always allowed at terminal level, but security still classifies
+ const all = this._collectAllCommands();
+ const known = all.includes(cmd);
+ const hasSuspicious = args.some(a => this.suspiciousFlags.has(a.toLowerCase()));
+ const destructive = this.destructiveCmds.has(cmd) || hasSuspicious;
+ const system = this.systemCmds.has(cmd);
+ const unknown = !known;
+ return { cmd, args, known, destructive, system, unknown, hasSuspicious };
+ }
+ async checkAndRun(rawInput) {
+ const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
+ // Level 3 Audit: just log and allow
+ if (this.level === 3) {
+ this.terminal.print(`[AUDIT] ${rawInput} → known=${known} destructive=${destructive} system=${system}`, 'info');
+ return this._dispatch(cmd, args);
+ }
+ // Always allow builtins that are safe, regardless of level
+ if (['help','security','security-level'].includes(cmd)) {
+ return this._dispatch(cmd, args);
+ }
+ // Unknown command handling
+ if (unknown && !this.whitelist.has(cmd)) {
+ this.blockedCount++;
+ const sug = this.suggest(cmd);
+ if (sug) {
+ this.terminal.print(`Security: Unknown command "${cmd}". Did you mean: "${sug.cmd}"?`, 'warning');
+ } else {
+ this.terminal.print(`Security: Unknown command "${cmd}" — blocked. Use 'security-level 0' to disable.`, 'warning');
+ }
+ // At levels 0-1: suggest/fix but still route to AI (terminal is also a chat interface)
+ // At level 2+: block entirely (paranoid)
+ if (this.level >= 2) {
+ this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).`, 'error');
+ return;
+ }
+ if (this.level >= 1) {
+ if (sug) {
+ this.terminal.print(`Security: Did you mean "${sug.cmd}"? Routing to AI...`, 'warning');
+ } else {
+ this.terminal.print(`Security: Routing to AI...`, 'info');
+ }
+ }
+ return this._tryAIRoute(rawInput);
+ }
+ // Suspicious flags (e.g. --execute) at any level > 0
+ if (hasSuspicious && this.level > 0) {
+ this.terminal.print(`Security: Suspicious flag detected in "${rawInput}" — blocked.`, 'error');
+ this.blockedCount++;
+ return;
+ }
+ // Destructive commands
+ if (destructive) {
+ if (this.level === 0) {
+ this.terminal.print(`Warning: destructive command "${cmd}"`, 'warning');
+ return this._dispatch(cmd, args);
+ }
+ if (this.level >= 1) {
+ // Reuse your existing confirm UI if present, otherwise print confirm message
+ if (this.terminal.showConfirmationBar) {
+ this.terminal._pendingCommand = () => this._dispatch(cmd, args);
+ this.terminal.showConfirmationBar(`Run destructive: ${rawInput}`);
+ return;
+ } else {
+ this.terminal.print(`Security: Destructive command "${cmd}" requires confirmation.`, 'warning');
+ return;
+ }
+ }
+ }
+ // System commands at level >=1
+ if (system && this.level >= 1) {
+ this.terminal.print(`Security: System command "${cmd}" blocked (level ${this.level}).`, 'error');
+ this.blockedCount++;
+ return;
+ }
+ // Standard dispatch for everything else
+ return this._dispatch(cmd, args);
+ }
+ async _dispatch(cmd, args) {
+ return this.terminal.dispatchCommand(cmd, args);
+ }
+ async _tryAIRoute(rawInput) {
+ // AI routing cooldown guard
+ const now = Date.now();
+ if (this.lastRouteCmd === rawInput && (now - this.lastRouteTime) < 5000) {
+ this.terminal.print(`Security: Already routed "${rawInput}" to AI recently. Use 'security-level 0' to force.`, 'warning');
+ return;
+ }
+ this.lastRouteCmd = rawInput;
+ this.lastRouteTime = now;
+ try {
+ const aiMod = this.terminal.moduleManager.modules['WebX11AIAssistantModule'];
+ if (aiMod) {
+ this.terminal.print('→ Routing to AI assistant...', 'info');
+ await aiMod.chat([rawInput.trim()]);
+ }
+ } catch (e) {
+ this.terminal.print(`Security: AI error: ${e.message}`, 'error');
+ }
+ }
+ }
+
class WebX11Terminal {
constructor() {
this.outputContainer = document.getElementById('terminal-container');
@@ -15,6 +195,7 @@
}
} catch (e) { console.warn('SecretVault init:', e.message); }
this.moduleManager = new ModuleManager(this);
+ this.security = new WebX11SecurityManager(this, parseInt(localStorage.getItem('webx11_security_level')) || 1);
this.ollamaManager = new OllamaManager(this);
this._pipeInput = null;
this._pipeBufferSize = 5000;
@@ -267,6 +448,53 @@
this.outputContainer.appendChild(line);
this.scrollToBottom();
}
+ showConfirmationBar(desc) {
+ const area = document.getElementById('ai-confirm-area');
+ if (!area) {
+ this.print('Confirmation UI unavailable — command blocked.', 'error');
+ return;
+ }
+ this._pendingCommandDesc = desc;
+ area.innerHTML = `
+ <span class="confirm-desc">⚠️ ${desc}</span>
+ <button class="confirm-btn" id="cmd-confirm-allow" onclick="(function(){var a=document.getElementById('cmd-confirm-allow');if(a.__allow) a.__allow();}).call(this)">✅ Allow</button>
+ <button class="confirm-btn deny" id="cmd-confirm-deny" onclick="(function(){var d=document.getElementById('cmd-confirm-deny');if(d.__deny) d.__deny();}).call(this)">❌ Deny</button>
+ <span style="font-size:0.75em;color:var(--text-muted);width:100%;text-align:center">Type: cmd-allow or cmd-deny</span>
+ `;
+ area.classList.add('visible');
+ const allowBtn = document.getElementById('cmd-confirm-allow');
+ const denyBtn = document.getElementById('cmd-confirm-deny');
+ const onAllow = () => {
+ this._resolveCmdConfirm(true);
+ };
+ const onDeny = () => {
+ this._resolveCmdConfirm(false);
+ };
+ allowBtn.addEventListener('click', onAllow);
+ denyBtn.addEventListener('click', onDeny);
+ allowBtn.__allow = onAllow;
+ denyBtn.__deny = onDeny;
+ }
+ _resolveCmdConfirm(approved) {
+ const area = document.getElementById('ai-confirm-area');
+ if (area) {
+ area.classList.remove('visible');
+ area.innerHTML = '';
+ }
+ if (approved) {
+ this.print(`Allowed: ${this._pendingCommandDesc || 'command'}`, 'success');
+ if (this._pendingCommand) {
+ const fn = this._pendingCommand;
+ this._pendingCommand = null;
+ this._pendingCommandDesc = null;
+ fn();
+ }
+ } else {
+ this.print(`Denied: ${this._pendingCommandDesc || 'command'}`, 'warning');
+ this._pendingCommand = null;
+ this._pendingCommandDesc = null;
+ }
+ }
_loadHistory() {
try {
const saved = localStorage.getItem('webx11_terminal_output');
@@ -331,7 +559,7 @@
setTimeout(() => {
this._historySavePending = false;
try {
- const slice = this._historyBuffer.slice(-30);
+ const slice = this._historyBuffer.slice(-this._historyMaxLines);
localStorage.setItem('webx11_terminal_output', JSON.stringify(slice));
} catch (e) {
// Silently ignore storage errors (e.g. quota exceeded)
@@ -425,6 +653,55 @@
scrollToBottom() {
this.outputContainer.scrollTop = this.outputContainer.scrollHeight;
}
+ _getPathSuggestions(partial, type) {
+ // type: 'dir' = dirs only, 'file' = files + dirs (dirs get trailing /), 'session' = _sessions/*.json
+ if (type === 'session') {
+ this.fs.mkdirRecursive('_sessions');
+ const all = this.fs.ls('_sessions');
+ const names = all
+ .filter(e => e.type === 'file' && e.name.endsWith('.json'))
+ .map(e => e.name.replace('.json', ''));
+ return partial
+ ? names.filter(n => n.startsWith(partial)).map(n => n)
+ : names;
+ }
+ // Resolve the partial path to determine which directory to list
+ let dirPath = '';
+ let filterPrefix = '';
+ let entries = [];
+ let prefix = '';
+
+ if (partial.includes('/')) {
+ // "dir1/dir2" or "dir1/dir2/" or "dir1/"
+ const lastSlash = partial.lastIndexOf('/');
+ const beforeSlash = partial.slice(0, lastSlash); // dir1
+ filterPrefix = partial.slice(lastSlash + 1); // dir2 (or '' if ends with /)
+ // Resolve "dir1" relative to current path
+ const resolvedDir = this.fs._resolveDir(beforeSlash || '.');
+ if (!resolvedDir) return [];
+ entries = Object.keys(resolvedDir.content).map(name => ({
+ name,
+ type: resolvedDir.content[name].type,
+ size: resolvedDir.content[name].content ? resolvedDir.content[name].content.length : 0
+ }));
+ prefix = beforeSlash + '/';
+ } else {
+ // No slash — list current dir
+ entries = this.fs.ls();
+ filterPrefix = partial;
+ prefix = '';
+ }
+
+ if (type === 'dir') {
+ return entries
+ .filter(e => e.type === 'dir' && e.name.startsWith(filterPrefix))
+ .map(e => prefix + e.name + '/');
+ }
+ // 'file' — show files + dirs (dirs with trailing /)
+ return entries
+ .filter(e => e.name.startsWith(filterPrefix))
+ .map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
+ }
showAutocomplete() {
const input = this.inputField.value;
const trimmed = input.trim();
@@ -442,11 +719,24 @@
const cmdPart = parts[0].toLowerCase();
const lowerInput = input.toLowerCase();
- // Commands that take file/dir arguments for autocomplete
- const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view'];
- const dirArgCommands = ['ls', 'cd'];
- // Session commands that autocomplete from _sessions/*.json
- const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save'];
+ // Expanded command lists for autocomplete
+ const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list'];
+ const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir'];
+ const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save', 'session-list', 'session-import'];
+ const pathArgCommands = ['cat', 'preview', 'rm', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview'];
+
+ // Collect commands that take arguments from ext module metadata
+ const extFileCmds = new Set();
+ const extDirCmds = new Set();
+ for (const modName in this.moduleManager.modules) {
+ const mod = this.moduleManager.modules[modName];
+ if (mod.commandsMeta) {
+ for (const [c, meta] of Object.entries(mod.commandsMeta)) {
+ if (meta.argType === 'file' || meta.argType === 'path') extFileCmds.add(c);
+ if (meta.argType === 'dir') extDirCmds.add(c);
+ }
+ }
+ }
// If we have more than one word, we're past the command name — autocomplete file/dir/session names
if (parts.length > 1 && parts[1].length > 0) {
@@ -458,25 +748,13 @@
return;
}
- // Determine if we should show session names, dirs only, or both
if (sessionArgCommands.includes(cmdPart)) {
- this.fs.mkdirRecursive('_sessions');
- const all = this.fs.ls('_sessions');
- this.suggestions = all
- .filter(e => e.type === 'file' && e.name.endsWith('.json'))
- .map(e => e.name.replace('.json', ''))
- .filter(n => n.startsWith(partialTrimmed));
- } else if (cmdPart === 'cd' || cmdPart === 'rmdir' || dirArgCommands.includes(cmdPart)) {
- // Dir-only commands
- const entries = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = entries
- .filter(e => e.name.startsWith(partialTrimmed))
- .map(e => e.name + '/');
- } else if (fileArgCommands.includes(cmdPart)) {
- const all = this.fs.ls();
- this.suggestions = all
- .filter(e => e.name.startsWith(partialTrimmed))
- .map(e => e.type === 'dir' ? e.name + '/' : e.name);
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'session');
+ } else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'dir');
+ this._isCdAutocomplete = true;
+ } else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
} else {
this.hideAutocomplete();
return;
@@ -484,7 +762,6 @@
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
- this._isCdAutocomplete = (cmdPart === 'cd' || cmdPart === 'rmdir');
this.renderAutocomplete();
} else {
this.hideAutocomplete();
@@ -495,24 +772,18 @@
// Just typed a space after command — show all matching entries
if (parts.length > 1) {
if (sessionArgCommands.includes(cmdPart)) {
- this.fs.mkdirRecursive('_sessions');
- const all = this.fs.ls('_sessions');
- this.suggestions = all
- .filter(e => e.type === 'file' && e.name.endsWith('.json'))
- .map(e => e.name.replace('.json', ''));
- } else if (cmdPart === 'cd' || cmdPart === 'rmdir' || dirArgCommands.includes(cmdPart)) {
- const dirs = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = dirs.map(d => d.name + '/');
- } else if (fileArgCommands.includes(cmdPart)) {
- const all = this.fs.ls();
- this.suggestions = all.map(e => e.type === 'dir' ? e.name + '/' : e.name);
+ this.suggestions = this._getPathSuggestions('', 'session');
+ } else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions('', 'dir');
+ this._isCdAutocomplete = true;
+ } else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions('', 'file');
} else {
this.hideAutocomplete();
return;
}
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
- this._isCdAutocomplete = (cmdPart === 'cd' || cmdPart === 'rmdir');
this.renderAutocomplete();
} else {
this.hideAutocomplete();
@@ -520,10 +791,9 @@
return;
}
- // Directory autocomplete for bare 'cd ' (no partial yet) — should be unreachable due to above block
+ // Directory autocomplete for bare 'cd ' (no partial yet)
if (lowerInput.startsWith('cd ')) {
- const dirs = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = dirs.map(d => d.name + '/');
+ this.suggestions = this._getPathSuggestions('', 'dir');
if (this.suggestions.length > 0) {
this._isCdAutocomplete = true;
this.renderAutocomplete();
@@ -623,15 +893,17 @@
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell'];
+ const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
try {
- const res = await fetch('packages.json');
+ const res = await fetch('packages.json?v=' + cacheVer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const index = await res.json();
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
- await this.moduleManager.loadModuleFromURL(pkg.url);
+ const url = pkg.url + (pkg.url.includes('?') ? '&v=' : '?v=') + cacheVer;
+ await this.moduleManager.loadModuleFromURL(url);
}
}
} catch (err) {
@@ -640,7 +912,7 @@
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder'];
+ const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -659,65 +931,8 @@
await this.executePipeChain(pipeParts);
return;
}
- const parts = rawInput.trim().split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- if (cmd === 'startup-modules') {
- this.openStartupModules();
- return;
- }
- if (cmd === 'font-size' || cmd === 'setfont') {
- this.setFontSize(args[0]);
- return;
- }
- if (cmd === 'load-local' || cmd === 'loadlocal') {
- this.loadLocalModule(args[0]);
- return;
- }
- if (cmd === 'smart-prompt') {
- const val = args[0] && args[0] === 'off' ? 'off' : 'on';
- localStorage.setItem('webx11_smart_prompt', val);
- this.print('Smart prompt ' + val, 'success');
- this.updatePrompt();
- return;
- }
- if (cmd === 'mem') {
- this.printMemoryStats();
- return;
- }
- if (cmd === 'clear') {
- this.outputContainer.innerHTML = '';
- this._historyBuffer = [];
- localStorage.removeItem('webx11_terminal_output');
- return;
- }
- let executed = false;
- for (const modName in this.moduleManager.modules) {
- const mod = this.moduleManager.modules[modName];
- if (mod.commands && mod.commands[cmd]) {
- try {
- await mod.commands[cmd](args);
- executed = true;
- } catch (err) {
- this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
- }
- break;
- }
- }
- if (!executed) {
- // Route unknown commands to the AI agent
- try {
- const aiMod = this.moduleManager.modules['WebX11AIAssistantModule'];
- if (aiMod) {
- this.print('→', 'info');
- await aiMod.chat([rawInput.trim()]);
- return;
- }
- } catch (e) {
- this.print(`Agent error: ${e.message}`, 'error');
- return;
- }
- }
+ // Security gate for all single commands and internal builtins
+ await this.security.checkAndRun(rawInput);
}
splitPipe(input) {
const parts = [];
@@ -761,6 +976,25 @@
}
async dispatchCommand(cmd, args) {
if (cmd === 'clear') { this.outputContainer.innerHTML = ''; this._historyBuffer = []; localStorage.removeItem('webx11_terminal_output'); return; }
+ if (cmd === 'startup-modules') { this.openStartupModules(); return; }
+ if (cmd === 'font-size' || cmd === 'setfont') { this.setFontSize(args[0]); return; }
+ if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
+ if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
+ if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'security') {
+ this.print(`Security level: ${this.security.getLevel()} (${this.security.getLevelName()})`, 'info');
+ this.print(`Blocked commands: ${this.security.blockedCount}`, 'info');
+ return;
+ }
+ if (cmd === 'security-level') {
+ const lv = parseInt(args[0]);
+ if (isNaN(lv)) { this.print('Usage: security-level 0|1|2|3', 'warning'); return; }
+ this.security.setLevel(lv);
+ localStorage.setItem('webx11_security_level', lv);
+ return;
+ }
+ if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
+ if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commands && mod.commands[cmd]) {
@@ -768,17 +1002,7 @@
catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
}
}
- // Route unknown commands to the AI agent
- try {
- const aiMod = this.moduleManager.modules['WebX11AIAssistantModule'];
- if (aiMod) {
- this.print('→ Routing to AI assistant...', 'info');
- await aiMod.chat([cmd + ' ' + args.join(' ')]);
- return;
- }
- } catch (e) {
- this.print(`Agent error: ${e.message}`, 'error');
- }
+ // No AI routing here — security manager handles that via checkAndRun
this.print(`Command not found: ${cmd}`, 'error');
}
openFileBrowser() {
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 68d02bf..d2d3103 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -2,33 +2,83 @@ class VirtualFileSystem {
constructor() {
this.root = { type: 'dir', content: {} };
this.currentPath = [];
- this.loadFromStorage();
- }
- loadFromStorage() {
+ this._ready = this.loadFromStorage();
+ }
+ _u8ToBase64(u8) {
+ let s = '';
+ for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
+ return btoa(s);
+ }
+ _base64ToU8(b64) {
+ const s = atob(b64);
+ const u8 = new Uint8Array(s.length);
+ for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i);
+ return u8;
+ }
+ async _compress(text) {
+ if (typeof CompressionStream === 'undefined') return null;
+ const blob = new Blob([text]);
+ const stream = blob.stream().pipeThrough(new CompressionStream('gzip'));
+ const resp = new Response(stream);
+ const buf = await resp.arrayBuffer();
+ return this._u8ToBase64(new Uint8Array(buf));
+ }
+ async _decompress(b64) {
+ if (typeof DecompressionStream === 'undefined') throw new Error('DecompressionStream not available');
+ const u8 = this._base64ToU8(b64);
+ const blob = new Blob([u8]);
+ const stream = blob.stream().pipeThrough(new DecompressionStream('gzip'));
+ const resp = new Response(stream);
+ return await resp.text();
+ }
+ async loadFromStorage() {
try {
- const saved = localStorage.getItem('webx11_filesystem');
- if (saved) {
- // Safety: refuse to load if data is excessively large (>512KB raw)
- if (saved.length > 524288) {
- console.warn('FS data too large (' + (saved.length/1024).toFixed(0) + 'KB), skipping load');
- localStorage.removeItem('webx11_filesystem');
- return;
+ const v2 = localStorage.getItem('webx11_filesystem_v2');
+ if (v2) {
+ try {
+ const text = await this._decompress(v2);
+ const data = JSON.parse(text);
+ this.root = data.root || this.root;
+ this.currentPath = data.currentPath || [];
+ return true;
+ } catch (e) {
+ console.error('FS v2 decompress error:', e);
+ return this._loadLegacy();
}
- const data = JSON.parse(saved);
- this.root = data.root || this.root;
- this.currentPath = data.currentPath || [];
}
- } catch (e) { console.error('FS load error:', e); }
+ return this._loadLegacy();
+ } catch (e) { console.error('FS load error:', e); return false; }
+ }
+ _loadLegacy() {
+ const saved = localStorage.getItem('webx11_filesystem');
+ if (saved) {
+ if (saved.length > 524288) {
+ console.warn('FS data too large (' + (saved.length/1024).toFixed(0) + 'KB), skipping load');
+ localStorage.removeItem('webx11_filesystem');
+ return false;
+ }
+ const data = JSON.parse(saved);
+ this.root = data.root || this.root;
+ this.currentPath = data.currentPath || [];
+ console.info('Loaded legacy FS; will migrate to compressed on next save.');
+ return true;
+ }
+ return false;
}
saveToStorage() {
if (this._storageSavePending) return;
this._storageSavePending = true;
- setTimeout(() => {
+ setTimeout(async () => {
this._storageSavePending = false;
try {
- localStorage.setItem('webx11_filesystem', JSON.stringify({
- root: this.root, currentPath: this.currentPath
- }));
+ const json = JSON.stringify({ root: this.root, currentPath: this.currentPath });
+ const compressed = await this._compress(json);
+ if (compressed) {
+ localStorage.setItem('webx11_filesystem_v2', compressed);
+ localStorage.removeItem('webx11_filesystem');
+ } else {
+ localStorage.setItem('webx11_filesystem', json);
+ }
} catch (e) { console.error('FS save error:', e); }
}, 2000);
}
@@ -122,25 +172,62 @@ class VirtualFileSystem {
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
if (node && node.type === 'file') {
- delete resolved.dir.content[resolved.name];
- this.saveToStorage();
+ this.trashFile(path, node);
return true;
}
return false;
}
- mkdir(path) {
+ trashFile(path, node) {
+ try {
+ const name = path.split('/').pop() || 'unknown';
+ const ts = Date.now();
+ if (!this.root.content['.trash']) {
+ this.root.content['.trash'] = { type: 'dir', content: {} };
+ }
+ const trash = this.root.content['.trash'];
+ const trashName = ts + '-' + name;
+ trash.content[trashName] = JSON.parse(JSON.stringify(node));
+ const resolved = this._resolvePath(path);
+ if (resolved) delete resolved.dir.content[resolved.name];
+ this.saveToStorage();
+ } catch (e) { console.error('Trash error:', e); }
+ }
+ trashDir(path) {
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
- const name = resolved.name;
- if (name === '.' || name === '..') return false;
- if (resolved.dir.content[name]) return false;
- resolved.dir.content[name] = { type: 'dir', content: {} };
- this.saveToStorage();
- return true;
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') {
+ try {
+ const name = resolved.name;
+ const ts = Date.now();
+ if (!this.root.content['.trash']) {
+ this.root.content['.trash'] = { type: 'dir', content: {} };
+ }
+ const trash = this.root.content['.trash'];
+ const flatten = (n, prefix) => {
+ if (n.type === 'file') {
+ const flatName = prefix + '-' + name;
+ let ctr = 0;
+ let unique = flatName;
+ while (trash.content[unique]) { unique = flatName + '-' + (++ctr); }
+ trash.content[unique] = JSON.parse(JSON.stringify(n));
+ } else if (n.type === 'dir') {
+ for (const [childName, child] of Object.entries(n.content || {})) {
+ flatten(child, prefix + '-' + childName);
+ }
+ }
+ };
+ flatten(node, ts + '-dir-' + name);
+ delete resolved.dir.content[resolved.name];
+ this.saveToStorage();
+ return true;
+ } catch (e) { console.error('Trash dir error:', e); }
+ }
+ return false;
}
mkdirRecursive(path) {
const segs = this._toAbsoluteSegments(path);
- if (!segs || segs.length === 0) return true;
+ if (!segs) return false;
let dir = this.root;
for (const seg of segs) {
if (!dir.content[seg]) {
@@ -153,122 +240,209 @@ class VirtualFileSystem {
this.saveToStorage();
return true;
}
- readDir(path) {
- const resolved = this._resolvePath(path);
- if (!resolved) return null;
- if (resolved.name) {
- const node = resolved.dir.content[resolved.name];
- return (node && node.type === 'dir') ? node : null;
- }
- return resolved.dir;
- }
- copyFile(srcPath, dstPath) {
- const src = this._resolvePath(srcPath);
+ rename(oldPath, newPath) {
+ const src = this._resolvePath(oldPath);
if (!src || !src.name) return false;
- const srcNode = src.dir.content[src.name];
- if (!srcNode || srcNode.type !== 'file') return false;
- const dst = this._resolvePath(dstPath);
+ const dst = this._resolvePath(newPath);
if (!dst || !dst.name) return false;
- const copy = JSON.parse(JSON.stringify(srcNode));
- dst.dir.content[dst.name] = copy;
+ const node = src.dir.content[src.name];
+ if (!node) return false;
+ dst.dir.content[dst.name] = node;
+ delete src.dir.content[src.name];
this.saveToStorage();
return true;
}
- copyDir(srcPath, dstPath) {
+ copy(srcPath, dstPath) {
const src = this._resolvePath(srcPath);
if (!src || !src.name) return false;
- const srcNode = src.dir.content[src.name];
- if (!srcNode || srcNode.type !== 'dir') return false;
const dst = this._resolvePath(dstPath);
if (!dst || !dst.name) return false;
- if (dst.dir.content[dst.name]) return false;
- const copy = JSON.parse(JSON.stringify(srcNode));
- dst.dir.content[dst.name] = copy;
+ const node = src.dir.content[src.name];
+ if (!node) return false;
+ dst.dir.content[dst.name] = JSON.parse(JSON.stringify(node));
this.saveToStorage();
return true;
}
- moveFile(srcPath, dstPath) {
- if (this.copyFile(srcPath, dstPath)) {
- const src = this._resolvePath(srcPath);
- if (src && src.name) {
- delete src.dir.content[src.name];
- this.saveToStorage();
- return true;
- }
+ move(srcPath, dstPath) {
+ return this.rename(srcPath, dstPath);
+ }
+ cp(srcPath, dstPath) {
+ return this.copy(srcPath, dstPath);
+ }
+ mv(srcPath, dstPath) {
+ return this.rename(srcPath, dstPath);
+ }
+ rm(path) {
+ const resolved = this._resolvePath(path);
+ if (!resolved || !resolved.name) return false;
+ const node = resolved.dir.content[resolved.name];
+ if (!node) return false;
+ if (node.type === 'file') {
+ this.trashFile(path, node);
+ return true;
+ }
+ if (node.type === 'dir') {
+ this.trashDir(path);
+ return true;
}
return false;
}
- moveDir(srcPath, dstPath) {
- if (this.copyDir(srcPath, dstPath)) {
- const src = this._resolvePath(srcPath);
- if (src && src.name) {
- delete src.dir.content[src.name];
- this.saveToStorage();
- return true;
- }
+ rmdir(path) {
+ const resolved = this._resolvePath(path);
+ if (!resolved || !resolved.name) return false;
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') {
+ if (Object.keys(node.content).length > 0) return false;
+ delete resolved.dir.content[resolved.name];
+ this.saveToStorage();
+ return true;
}
return false;
}
- // Old-compat aliases for agent tools and SkillsModule
- fsRead(path) {
- const content = this.readFile(path);
- return content !== null ? content : null;
+ touch(path) {
+ return this.writeFile(path, '');
}
- fsWrite(path, content) {
- return this.writeFile(path, content);
+ readDir(path) {
+ const resolved = this._resolvePath(path);
+ if (!resolved) return null;
+ let dir;
+ if (resolved.name) {
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') dir = node;
+ else return null;
+ } else {
+ dir = resolved.dir;
+ }
+ return Object.keys(dir.content).map(name => ({
+ name,
+ type: dir.content[name].type,
+ size: dir.content[name].content ? dir.content[name].content.length : 0
+ }));
+ }
+ fsWriteJSON(path, obj) {
+ return this.writeFile(path, JSON.stringify(obj));
}
fsReadJSON(path) {
- const c = this.fsRead(path);
- if (c === null) return null;
- try { return JSON.parse(c); } catch (e) { return null; }
+ const content = this.readFile(path);
+ if (!content) return null;
+ try {
+ return JSON.parse(content);
+ } catch (e) {
+ return null;
+ }
}
- fsWriteJSON(path, obj) {
- return this.fsWrite(path, JSON.stringify(obj));
+ tree(path = '', prefix = '') {
+ const resolved = this._resolvePath(path);
+ if (!resolved) return '';
+ let dir;
+ if (resolved.name) {
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') dir = node;
+ else return '';
+ } else {
+ dir = resolved.dir;
+ }
+ let result = '';
+ const entries = Object.keys(dir.content).sort();
+ for (let i = 0; i < entries.length; i++) {
+ const name = entries[i];
+ const isLast = i === entries.length - 1;
+ const connector = isLast ? '└── ' : '├── ';
+ result += prefix + connector + name + '\n';
+ if (dir.content[name].type === 'dir') {
+ const ext = isLast ? ' ' : '│ ';
+ result += this.tree(path + '/' + name, prefix + ext);
+ }
+ }
+ return result;
}
- findFiles(pattern) {
+ find(pattern, path = '') {
+ const resolved = this._resolvePath(path);
+ if (!resolved) return [];
+ let dir;
+ if (resolved.name) {
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') dir = node;
+ else return [];
+ } else {
+ dir = resolved.dir;
+ }
const results = [];
- const search = (node, path) => {
- if (node.type === 'dir' && node.content) {
- for (const k of Object.keys(node.content)) {
- const sub = node.content[k];
- const subPath = path ? path + '/' + k : k;
- if (sub.type === 'dir') search(sub, subPath);
- else if (sub.type === 'file' && k.toLowerCase().includes(pattern)) results.push(subPath);
- }
+ const regex = new RegExp(pattern, 'i');
+ for (const name of Object.keys(dir.content)) {
+ const fullPath = path ? path + '/' + name : name;
+ if (regex.test(name)) {
+ results.push({ path: fullPath, type: dir.content[name].type });
}
- };
- search(this.root, '');
- return results.sort();
+ if (dir.content[name].type === 'dir') {
+ results.push(...this.find(pattern, fullPath));
+ }
+ }
+ return results;
+ }
+ grep(pattern, path = '') {
+ const results = [];
+ const regex = new RegExp(pattern, 'i');
+ const files = this.find('.', path).filter(e => e.type === 'file');
+ for (const file of files) {
+ const content = this.readFile(file.path);
+ if (content && regex.test(content)) {
+ results.push(file.path);
+ }
+ }
+ return results;
}
- deleteDirRecursive(path) {
+ chmod(path, mode) {
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
- if (!node || node.type !== 'dir') return false;
- delete resolved.dir.content[resolved.name];
+ if (!node) return false;
+ node.mode = mode;
this.saveToStorage();
return true;
}
- // Full state serialization for session save/restore
- serializeState() {
- return JSON.stringify({
- root: this.root,
- currentPath: this.currentPath,
- version: 1
- });
+ du(path = '') {
+ const resolved = this._resolvePath(path);
+ if (!resolved) return 0;
+ let dir;
+ if (resolved.name) {
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') dir = node;
+ else return node.content ? node.content.length : 0;
+ } else {
+ dir = resolved.dir;
+ }
+ let size = 0;
+ for (const name of Object.keys(dir.content)) {
+ const node = dir.content[name];
+ if (node.type === 'file') {
+ size += node.content ? node.content.length : 0;
+ } else if (node.type === 'dir') {
+ size += this.du(path ? path + '/' + name : name);
+ }
+ }
+ return size;
+ }
+ getSize(path) {
+ const resolved = this._resolvePath(path);
+ if (!resolved || !resolved.name) return 0;
+ const node = resolved.dir.content[resolved.name];
+ if (!node) return 0;
+ if (node.type === 'file') return node.content ? node.content.length : 0;
+ return this.du(path);
}
- restoreState(json) {
- try {
- const data = JSON.parse(json);
- if (!data || !data.root || !data.root.type) return false;
- this.root = data.root;
- this.currentPath = data.currentPath || [];
- this.saveToStorage();
- return true;
- } catch (e) {
- console.error('FS restore error:', e);
- return false;
+ exportToJSON(path = '') {
+ const resolved = this._resolvePath(path);
+ if (!resolved) return null;
+ let dir;
+ if (resolved.name) {
+ const node = resolved.dir.content[resolved.name];
+ if (node && node.type === 'dir') dir = node;
+ else return JSON.stringify(node);
+ } else {
+ dir = resolved.dir;
}
+ return JSON.stringify(dir, null, 2);
}
// Restore VFS from an exported dump file (replaces entire root)
restoreFromExport(content) {
@@ -276,8 +450,15 @@ class VirtualFileSystem {
try { content = JSON.parse(content); } catch (e) { return false; }
}
if (!content || !content.type) return false;
- this.root = content;
- this.currentPath = [];
+ // Handle v2 structured export: { type, version, root, currentPath, ... }
+ if (content.version === 2 || content.root) {
+ this.root = content.root;
+ this.currentPath = content.currentPath || [];
+ } else {
+ // Legacy format: content is just the root object
+ this.root = content;
+ this.currentPath = [];
+ }
this.saveToStorage();
return true;
}
@@ -309,7 +490,10 @@ class VirtualFileSystem {
}
rollback(name) {
const raw = this.fsReadJSON('/_snapshots/checkpoint-' + name + '.json');
- if (!raw || !raw.root) return false;
+ if (!raw || !raw.root) {
+ console.error('Rollback failed: checkpoint not found or invalid');
+ return false;
+ }
this.root = raw.root;
this.currentPath = raw.currentPath || [];
this.saveToStorage();
@@ -317,15 +501,29 @@ class VirtualFileSystem {
}
listCheckpoints() {
const dir = this.readDir('/_snapshots');
- if (!dir || !dir.content) return [];
- const prefix = 'checkpoint-';
- const suffix = '.json';
- return Object.keys(dir.content)
- .filter(k => k.startsWith(prefix) && k.endsWith(suffix))
- .map(k => k.slice(prefix.length, k.length - suffix.length))
- .sort();
+ if (!dir) return [];
+ return dir
+ .filter(e => e.type === 'file' && e.name.startsWith('checkpoint-') && e.name.endsWith('.json'))
+ .map(e => e.name.replace('checkpoint-', '').replace('.json', ''));
}
deleteCheckpoint(name) {
- return this.deleteFile('/_snapshots/checkpoint-' + name + '.json');
+ const path = '/_snapshots/checkpoint-' + name + '.json';
+ const resolved = this._resolvePath(path);
+ if (resolved && resolved.name) {
+ delete resolved.dir.content[resolved.name];
+ this.saveToStorage();
+ return true;
+ }
+ return false;
}
+ // Old-compat aliases for agent tools and SkillsModule
+ read(path) { return this.readFile(path); }
+ write(path, content) { return this.writeFile(path, content); }
+ delete(path) { return this.deleteFile(path); }
+ mkdir(path) { return this.mkdirRecursive(path); }
+ list(path) { return this.ls(path); }
+ dir(path) { return this.ls(path); }
}
+
+// Also expose for module system
+window.VirtualFileSystem = VirtualFileSystem;