aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js133
-rw-r--r--modules/core/module-fs.js6
-rw-r--r--modules/core/module-loader.js5
-rw-r--r--modules/core/module-neural-vfs.js5
-rw-r--r--modules/core/module-system.js92
-rw-r--r--modules/core/terminal.js37
-rw-r--r--modules/core/virtual-fs.js7
-rw-r--r--modules/ext/cryptogzip.js321
-rw-r--r--modules/ext/filemgr.js3
-rw-r--r--modules/ext/floating.js50
-rw-r--r--modules/ext/skill-pack-core.js6
-rw-r--r--modules/ext/webdav.js8
-rw-r--r--modules/ext/wpm.js9
13 files changed, 519 insertions, 163 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 7f99386..ecd4637 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -60,114 +60,37 @@
}
}
} catch (e) {}
- const CR = String.fromCharCode(10);
- const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR;
- return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n${nvfsNote}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE ==
-You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE):
+ return `Current date: ${dateStr}
+Current time: ${timeStr}${nvfsNote}
-FILESYSTEM:
-- list_files(path?) — List files in VFS directory (omit for current dir)
-- read_file(path) — Read file content (truncated past 8000 chars)
-- write_file(path, content) — Write to file (DESTRUCTIVE — confirm with user first)
-- make_directory(path) — Create a directory (DESTRUCTIVE — confirm first)
-- delete_file(path) — Delete file (DESTRUCTIVE — confirm first)
-- get_current_dir — Show current working directory
+You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS → Modules → AI. You can edit your own modules via edit_module/test_module/reload_module.
-SYSTEM:
-- get_status — Show filesystem, modules, Ollama, memory status
-- get_help — List all available terminal commands
-- run_command(command) — Execute any terminal command and capture output
-- font-size <8-32> — Change terminal font size (persists in localStorage)
+=== WORKFLOW ===
+1. Tool → Observe → Answer. Don't re-verify what a tool just told you.
+2. find_files("about X") → nvfs_resolve(X) FIRST. NVFS is built-in core, never WPM.
+3. load_skills("anti-loop-protocol") BEFORE tool #4 to prevent loops.
+4. If editing a module: load_skills("module-editing-workflow") FIRST.
+5. run_command may be blocked by security-level/guardrails — read the output.
+6. Concise. Bullets. If a tool fails, try another.
-SEARCH:
-- web_search(query) — Search the web via proxy API (SearXNG + DDG fallback, results with title/url/snippet)
-- web_fetch(url) — Fetch and extract readable content from a URL via proxy API (returns title + clean text + word count)
-
-PACKAGES:
-- wpm_install(package) — Install WPM package by name (e.g. "widget", "calc")
-- wpm_search(keyword) — Search available WPM packages
-
-SKILLS (PERSISTENT PROCEDURAL MEMORY):
-- list_skills(category?) — Browse available skills, optionally filtered by category
-- load_skills(query) — Search skills relevant to current task. Call at START of every task.
-- create_skill(name, description, content, tags, category) — Save a new stored procedure. Call after completing complex multi-step tasks (3+ tool calls).
-- patch_skill(name, content) — Update an existing skill when the approach changes
-
-FLOATING UI (PERSISTENT UI ELEMENTS): Use 'run_command' tool with these terminal commands:
-- 'floating-add btn <id> <label> <command>' — Create a floating button that runs a terminal command when clicked
-- 'floating-add menu <id> <label> <btn1>,<btn2>...' — Create a collapsible floating menu (buttons: label:command pairs)
-- 'floating-add panel <id> <title> <content>' — Create a floating text/status panel
-- 'floating-remove <id>' — Remove a floating element by ID
-- 'floating-toggle <id>' — Show/hide a floating element without deleting
-- 'floating-list' — List all floating elements
-- 'floating-clear' — Remove all floating elements
-- NOTE: These are terminal commands, run via 'run_command'. Use SPACES not hyphens between subcommands.
-MODULE SELF-IMPROVEMENT:
-- view_module(name) — View source code of any loaded module
-- edit_module(name) — Copy module into editor. Opens editor for modifications.
-- test_module(name) — Load edited module from /_mods/ for live testing
-- reload_module(name) — Discard edits and reload from original URL
-- list_modified_modules — List modules with local edits pending
-- get_module_status — Full status of all modules (loaded, modified, URLs)
-
-== SKILLS WORKFLOW ==
-1. At the START of every task, call load_skills(query) to check if a stored procedure exists
-2. Follow the skill's steps. If they're wrong, call patch_skill to fix them.
-3. After a complex multi-step task (3+ tool calls), call create_skill to save the approach
-4. Skills persist in /skills/ as markdown files with YAML frontmatter
-
-== MEMORY SYSTEM ==
-- I maintain a conversation context (this.context) that persists across messages in this session
-- The last ${this.maxContext} exchanges are remembered
-- Type "ai-clear" to reset conversation memory
-- Type "ai-context" to see what I remember
-- Type "ai-model" to see/switch models
-- Type "ai-stop" to cancel a running AI request
-- Smart context profiles (set via "context <name>") adjust my temperature and system prompt style
-- Saved prompts (via "prompt", "prompt-save") let you reuse template with {variable} substitution
-
-== USER PROFILE ==
-- Use user_get, user_set to read/write persistent user preferences across sessions
-- user_set dot-notation supported: user_set knownFacts.name "Alex"
-- Profile auto-stores known facts (name, project, preferences)
-- Use session_save after complex tasks to log what was done
-- Use session_load to review past sessions
-- Use session_recent at start of session to remember what was worked on before
-
-== SELF-IMPROVEMENT WORKFLOW ==
-1. User asks for module changes → call get_module_status to list modules
-2. Call view_module(name) to read the module source
-3. Call edit_module(name) to copy source into editor for modification
-4. After editing, call test_module(name) to load and test the edit
-5. If it works, the user keeps it. If not, call reload_module(name).
-
-== AUTO-IMPROVEMENT (SYSTEM-DRIVEN) ==
-- If I encounter the same tool error 3+ times, a workaround skill is created automatically
-- After complex tasks (3+ tool calls), I run health checks and suggest improvements
-- I track conversation patterns and surface frequent topics
-- Every 20 messages, skills are audited for staleness
-- Use check_updates to verify module source URLs are reachable
-
-== RESPONSE STYLE ==
-- Be concise. Use bullet points for lists.
-- When using tools, explain what you're doing briefly.
-- If a tool fails, try an alternative approach.
-- For web research tasks: search first, then analyze the results, then present findings.
-|- When the user says "update" or "improve" without specifics, use get_status + get_help to understand the current state first.
-
-== REACT PROTOCOL ==
-For every task, follow this cycle:
-1. **REASON** — Think about what you need next. What information is missing?
-2. **ACT** — Call ONE tool at a time (unless multiple independent lookups are needed).
-3. **OBSERVE** — Study the tool result carefully. Did it succeed? Is the data useful?
-4. **DECIDE** — Do you have enough to answer? If not, go back to step 1.
-
-RULES:
-- Call tools in sequence when the second depends on the first result
-- Independent lookups (e.g. reading two unrelated files) can run in parallel
-- If a tool returns an error, try a different approach before giving up
-- After 6 tool turns, start synthesizing a final answer
-- Always provide a complete, helpful final answer — don't leave the user hanging`;
+=== CAPABILITY MAP ===
+[FS] list_files, read_file, write_file, make_directory, delete_file, get_current_dir
+[NVFS] nvfs_resolve(concept), run_command: nvfs-status, nvfs-index, nvfs-tree. Skills: vfs-content-search, neural-vfs-management.
+[MODULES] view_module, edit_module, test_module, reload_module, list_modified_modules, get_module_status. CORE modules (never WPM): NVFS, Floating, Config, Vault, Sandbox, Orchestrator, Memory-Graph, World-Model, Predictor, Neural-VFS.
+[WPM] wpm_install, wpm_search, list_available_modules
+[SEARCH] web_search, web_fetch
+[AGENTS] delegate_task, list_agents, spawn_agent. A2A: a2a-send, a2a-list. Leases: lease-create, lease-list, lease-revoke.
+[SANDBOX] run_command: sandbox-status, sandbox-run, sandbox-config
+[CONFIG] run_command: config, config-set, ai-config
+[SKILLS] load_skills, create_skill, patch_skill, list_skills
+[PROFILE/SESSION] user_get/set/list/forget, session_save/load/recent/stats/terminal
+[SMART-CTX] smart_context_list, smart_context_set
+[MINDS] create_mindmap
+[SAFETY] run_command: guardrail, guardrail-add/remove, shadow, shadow-commit, shadow-rollback
+[VAULT] run_command: secret-set/get/list/del, secret-setpin, secret-status
+[CHECKPOINTS] run_command: checkpoint, checkpoint-list, checkpoint-del, rollback
+[WEBDAV] run_command: webdav-config/ls/push/pull/backup/restore
+[ALL OTHERS] cron, theme, voice, edit, diff, alias, mindmap, ll, cls, trash, gui, server, modules, ollama, context, world-model, memgraph, model, webxai, predict, predictor-config, memgraph-export, tar, untar, wget`;
}
getToolDefinitions() {
if (!this._toolDefs) this._toolDefs = this._buildToolDefs();
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index c2f29c4..309010d 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -1,5 +1,9 @@
class FileSystemModule {
- constructor() { this.commands = {}; this.terminal = null; }
+ constructor() { this.commands = {}; this.commandsMeta = {
+ 'trash': { argType: 'file' }, 'ls-trash': { argType: 'file' },
+ 'upload': { argType: 'dir' }, 'vfs-export': { argType: 'file' },
+ 'vfs-import': { argType: 'file' }, 'download': { argType: 'file' },
+ }; this.terminal = null; }
getHelpInfo() {
return {
'ls': 'ls [path] [-la] — List files and directories',
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 96fee00..9b8d892 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -13,7 +13,6 @@
const BUILTIN = [
'module-system.js',
'module-fs.js',
- 'module-modmgr.js',
'module-ollama.js',
'module-my.js',
'module-searxng.js',
@@ -105,8 +104,8 @@
window.__moduleLoaderReload = reloadWithSelection;
-const LATEST_VER = '2026-06-08-v4.5.22';
-const CACHE = '?v=' + LATEST_VER;
+ const LATEST_VER = '2026-06-08-v4.5.25';
+ const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
localStorage.setItem('webx11_cache_ver', LATEST_VER);
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index e216656..59618c8 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -1,6 +1,11 @@
class NeuralVFSModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'nvfs-index': { argType: 'file' }, 'nvfs-resolve': { argType: 'file' },
+ 'nvfs-tree': { argType: 'file' }, 'nvfs-alias': { argType: 'file' },
+ 'nvfs-sync': { argType: 'file' },
+ };
this.terminal = null;
this.index = {};
this.indexPath = '/.semantic_index/map.json';
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index 77525c7..cfb7cc4 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -277,39 +277,73 @@
}
return;
}
- // Categorize all commands
- const cats = {
- 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','trash','trash-dir','ls-trash','empty-trash'],
- ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-ignore','ai-stop','ai-model','ai-compress','debug'],
- search: ['search','s','ddg','duck','fetch','web','extract'],
- 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','tutorial','gzip','gunzip','encrypt','decrypt','files','session-save','session-load','session-list','session-delete','session-export','session-import'],
- system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','sh','set','test','logrotate','health-check','memory-map-export','commands','check','checkpoint','checkpoint-list','rollback','checkpoint-del'],
- 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'],
- skills: ['skill-list','skill-view','skill-create','skill-patch','skill-delete','skill-export','skill-import','skill-sync','skill-pack-core'],
- config: ['config','config-set','config-list','config-delete','config-export','config-reset'],
- webx: ['webxai','wx','webx','context','contexts','context-new','context-delete','smart-ask','prompt','prompt-save','prompt-list','prompt-delete','prompt-var','prompt-vars'],
- data: ['my','my-set','my-get','my-list','my-export','my-import','my-clear','export-mods','import-mods','backup','restore','share-mod'],
- ext: ['chat','chat-setapi','chat-setmodel','chat-ask','webdav-config','webdav-status','webdav-ls','webdav-push','webdav-pull','webdav-backup','webdav-restore','webdav-backup-list','webdav-explorer','webdav-browser'],
- gui: ['gui','gui-button','gui-input','gui-text','gui-clear','gui-run'],
- vault: ['secret-set','secret-get','secret-list','secret-del','secret-lock','secret-unlock','secret-setpin','secret-status'],
- };
- const labels = {
- files:'📁 Filesystem', ai:'🤖 AI', search:'🔍 Search & Web', tools:'🔧 Tools',
- system:'⚙️ System', net:'📡 Network', mods:'🧩 Modules', ollama:'🦙 Ollama',
- skills:'📚 Skills', config:'⚙️ Config', webx:'🌐 WebX', data:'💾 Data',
- ext:'🧰 Extras', gui:'🖥️ GUI', vault:'🔒 Vault'
+ // Dynamically group commands by owning module
+ const cmdToModule = this.terminal.moduleManager.getAllCommands();
+ const moduleGroups = {};
+ for (const [cmd, modName] of Object.entries(cmdToModule)) {
+ if (!moduleGroups[modName]) moduleGroups[modName] = [];
+ moduleGroups[modName].push(cmd);
+ }
+ const modLabels = {
+ FileSystemModule: '📁 Filesystem',
+ WebX11AIAssistantModule: '🤖 AI',
+ WebXAIAliasModule: '🌐 WebX',
+ DuckDuckGoModule: '🔍 Search & Web',
+ SearXNGModule: '🔍 Search & Web',
+ WebFetcherModule: '🔍 Search & Web',
+ AliasModule: '🔧 Tools',
+ BusyBoxModule: '🔧 Tools',
+ CalcModule: '🔧 Tools',
+ CronModule: '🔧 Tools',
+ CryptoGzipModule: '🔧 Tools',
+ DiffModule: '🔧 Tools',
+ EditorModule: '🔧 Tools',
+ FileManagerModule: '🔧 Tools',
+ FinderModule: '🔧 Tools',
+ MindmapModule: '🔧 Tools',
+ ThemeModule: '🔧 Tools',
+ TutorialModule: '🔧 Tools',
+ VoiceModule: '🔧 Tools',
+ WpmModule: '🔧 Tools',
+ SystemModule: '⚙️ System',
+ ShellModule: '⚙️ System',
+ ServerMonitorModule: '📡 Network',
+ ModuleManagerModule: '🧩 Modules',
+ OllamaModule: '🦙 Ollama',
+ NeuralVFSModule: '🧠 Neural VFS',
+ SkillPackCoreModule: '📚 Skills',
+ ConfigManagerModule: '⚙️ Config',
+ ModuleIOModule: '🔌 I/O',
+ SandboxModule: '🏖️ Sandbox',
+ MyModule: '💾 Data',
+ WebDAVModule: '🧰 Extras',
+ ChatStudioModule: '🧰 Extras',
+ SimpleGUIModule: '🖥️ GUI',
+ SecretVault: '🔒 Vault',
+ OrchestratorModule: '🧠 Orchestrator',
+ PredictorModule: '🔮 Prediction',
+ SmartContextModule: '🧠 Context',
+ MemoryGraphModule: '📊 Memory',
+ DynamicPromptsModule: '💬 Prompts',
+ WorldModelModule: '🌍 World',
+ LeaseModule: '🔐 Leases',
+ AgentRegistryModule: '🤖 Agents',
};
+ // Merge modules with the same label
+ const grouped = {};
+ const usedLabels = new Set();
+ for (const [modName, cmds] of Object.entries(moduleGroups)) {
+ const label = modLabels[modName] || `🧩 ${modName.replace(/Module$/, '')}`;
+ if (!grouped[label]) grouped[label] = [];
+ grouped[label].push(...cmds);
+ usedLabels.add(label);
+ }
let result = '╔══════════════════════════════════════╗\n║ 🔷 WebX11 ' + (window.NEWOS4_VERSION || 'v4.0') + ' — All Commands ║\n╚══════════════════════════════════════╝\n\n';
const total = Object.keys(helpMap).length;
result += `📊 ${total} commands loaded\n\n`;
- for (const [key, cmds] of Object.entries(cats)) {
- const present = cmds.filter(c => helpMap[c]);
- if (present.length === 0) continue;
- result += labels[key] + '\n';
- present.forEach(c => {
+ for (const [label, cmds] of Object.entries(grouped)) {
+ result += label + '\n';
+ cmds.forEach(c => {
const desc = (helpMap[c] || c).split(' — ')[1] || '';
const short = desc.substring(0, 55);
result += ` ${c.padEnd(16)}${short}\n`;
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 1fb44aa..3651713 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -896,6 +896,25 @@
.filter(e => e.name.startsWith(filterPrefix))
.map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
}
+
+ // ── Enum autocomplete from commandsMeta ──
+
+ _tryEnumAutocomplete(cmdName) {
+ this.suggestions = [];
+ for (const modName in this.moduleManager.modules) {
+ const mod = this.moduleManager.modules[modName];
+ const meta = mod.commandsMeta && mod.commandsMeta[cmdName];
+ if (meta && meta.argType === 'enum' && meta.enumFn && typeof mod[meta.enumFn] === 'function') {
+ const values = mod[meta.enumFn]();
+ if (Array.isArray(values) && values.length > 0) {
+ this.suggestions.push(...values);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
showAutocomplete() {
if (!this.moduleManager || !this.security) return;
const input = this.inputField.value;
@@ -919,8 +938,8 @@
const lowerInput = input.toLowerCase();
// Expanded command lists for autocomplete
- const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list'];
- const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir'];
+ const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'trash', 'ls-trash', 'upload', 'vfs-export', 'vfs-import', 'files', 'preview', 'skill-list', 'skill-view', 'skill-create', 'skill-patch', 'skill-delete', 'skill-sync', 'webdav-ls', 'webdav-push', 'webdav-pull', 'webdav-rm', 'webdav-mkdir', 'webdav-rename', 'webdav-move', 'webdav-config', 'nvfs-index', 'nvfs-resolve', 'nvfs-tree', 'nvfs-alias', 'nvfs-sync'];
+ const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir', 'upload', 'webdav-ls', 'webdav-mkdir', 'vfs-export', 'vfs-import', 'skill-list', 'skill-create'];
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', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview'];
@@ -954,9 +973,11 @@
this._isCdAutocomplete = true;
} else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
+ } else if (this._tryEnumAutocomplete(cmdPart)) {
+ this.suggestions = this.suggestions.filter(s => s.startsWith(partialTrimmed));
} else {
- this.hideAutocomplete();
- return;
+ // Fallback: show files anyway
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
}
this._autocompletePrefix = cmdPart + ' ';
@@ -977,9 +998,11 @@
this._isCdAutocomplete = true;
} else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions('', 'file');
+ } else if (this._tryEnumAutocomplete(cmdPart)) {
+ // suggestions already populated
} else {
- this.hideAutocomplete();
- return;
+ // Fallback: show files
+ this.suggestions = this._getPathSuggestions('', 'file');
}
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
@@ -2048,7 +2071,7 @@
const foundation = ['virtual-fs.js','module-manager.js','ollama-manager.js','terminal.js'];
const builtin = [
- 'module-system.js','module-fs.js','module-modmgr.js','module-ollama.js',
+ 'module-system.js','module-fs.js','module-ollama.js',
'module-my.js','module-searxng.js','module-servermon.js','module-ollamamodels.js',
'module-config.js','module-simplegui.js','module-io.js','module-dynamicprompts.js',
'module-aiassist.js','module-aiasm.js','module-smartctx.js','module-duckduckgo.js',
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index cb8020a..f6e2826 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -545,6 +545,13 @@ class VirtualFileSystem {
mkdir(path) { return this.mkdirRecursive(path); }
list(path) { return this.ls(path); }
dir(path) { return this.ls(path); }
+ reset() {
+ this.root = { type: 'dir', content: {} };
+ this.currentPath = [];
+ // Wipe all localStorage keys
+ ['webx11_filesystem', 'webx11_filesystem_v2'].forEach(k => localStorage.removeItem(k));
+ return true;
+ }
}
// Also expose for module system
diff --git a/modules/ext/cryptogzip.js b/modules/ext/cryptogzip.js
index b67e067..200abdd 100644
--- a/modules/ext/cryptogzip.js
+++ b/modules/ext/cryptogzip.js
@@ -1,6 +1,7 @@
class CryptoGzipModule {
- // CryptoGzipModule — VFS-level gzip/encrypt commands for WebX11 newOS4
- // Commands: gzip <path>, gunzip <path.gz>, encrypt <path> <passphrase>, decrypt <path.enc> <passphrase>
+ // CryptoGzipModule — VFS-level gzip/encrypt/tar commands for WebX11 newOS4
+ // Commands: gzip <path>, gunzip <path.gz>, encrypt <path> <passphrase>, decrypt <path.enc> <passphrase>,
+ // tar <src-dir> <out.tar[.gz]>, untar <path.tar[.gz]> [dest-dir]
constructor() {
this.commands = {};
this.terminal = null;
@@ -9,9 +10,12 @@ class CryptoGzipModule {
getHelpInfo() {
return {
'gzip': 'gzip <path> — Compress a VFS file to .gz',
- 'gunzip': 'gunzip <path.gz> — Decompress a .gz file',
- 'encrypt': 'encrypt <path> <passphrase> — AES-256-GCM encrypt a VFS file',
- 'decrypt': 'decrypt <path.enc> <passphrase> — Decrypt a .enc file'
+ 'gunzip': 'gunzip <path.gz> — Decompress a .gz file (binary-safe)',
+ 'encrypt': 'encrypt <path> <passphrase> — AES-256-GCM encrypt a file',
+ 'decrypt': 'decrypt <path.enc> <passphrase> — Decrypt a .enc file',
+ 'tar': 'tar <src-dir> <out.tar[.gz]> — Bundle a VFS directory into a tar archive (gzip if .gz)',
+ 'untar': 'untar <path.tar[.gz]> [dest-dir] — Extract a tar archive into VFS (auto-detects gzip)',
+ 'wget': 'wget <url> [filename] — Download a URL and save to VFS'
};
}
@@ -20,6 +24,17 @@ class CryptoGzipModule {
const normalized = path.replace(/^\/+/, '');
return this.terminal.fs.readFile(normalized) !== null;
}
+ _vfsDirExists(path) {
+ // Check if a path is a directory (or empty string for root)
+ const normalized = path.replace(/^\/+/, '').replace(/\/+$/, '');
+ if (!normalized) return true; // root always exists
+ try {
+ const items = this.terminal.fs.readDir(normalized);
+ return Array.isArray(items);
+ } catch (e) {
+ return false;
+ }
+ }
_vfsRead(path) {
const normalized = path.replace(/^\/+/, '');
@@ -152,6 +167,290 @@ class CryptoGzipModule {
return new TextDecoder().decode(decompressed);
}
+ // Binary-safe variants: return raw Uint8Array, store as base64 in VFS
+ async _gzipBytes(bytes) {
+ const cs = new CompressionStream('gzip');
+ const writer = cs.writable.getWriter();
+ writer.write(bytes);
+ writer.close();
+ const compressed = await new Response(cs.readable).arrayBuffer();
+ return new Uint8Array(compressed);
+ }
+ async _gunzipBytes(bytes) {
+ const ds = new DecompressionStream('gzip');
+ const writer = ds.writable.getWriter();
+ writer.write(bytes);
+ writer.close();
+ const decompressed = await new Response(ds.readable).arrayBuffer();
+ return new Uint8Array(decompressed);
+ }
+ _bytesToBase64(bytes) {
+ let s = '';
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
+ return btoa(s);
+ }
+ _base64ToBytes(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;
+ }
+// Save binary file to VFS (stored as Latin-1 string — each byte → one char, round-trips via charCodeAt)
+ _vfsWriteBytes(path, bytes) {
+ const normalized = path.replace(/^\/+/, '');
+ let s = '';
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
+ this.terminal.fs.writeFile(normalized, s);
+ }
+ // Load binary file from VFS (auto-detects format: base64 via heuristic, or raw Latin-1 bytes)
+ _vfsReadBytes(path) {
+ const normalized = path.replace(/^\/+/, '');
+ const content = this.terminal.fs.readFile(normalized);
+ if (content === null) throw new Error(`File not found: ${path}`);
+// Heuristic: if content looks like base64 (only [A-Za-z0-9+/\n\r]) and length is multiple of 4, decode
+ const looksLikeBase64 = /^[A-Za-z0-9+/\n\r]+=*$/.test(content.slice(0, 200));
+ if (looksLikeBase64 && content.length % 4 === 0) {
+ try { return this._base64ToBytes(content); } catch (e) {}
+ }
+ // Fallback: treat as Latin-1 bytes (each charCode is a byte)
+ const u8 = new Uint8Array(content.length);
+ for (let i = 0; i < content.length; i++) u8[i] = content.charCodeAt(i);
+ return u8;
+ }
+ // Detect gzip magic bytes (1f 8b)
+ _isGzip(bytes) {
+ return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
+ }
+
+ // ============================================================
+ // TAR / UNTAR — POSIX ustar parser (pure JS, ~120 lines)
+ // Supports: ustar + gnu tar formats, gzip auto-detect
+ // ============================================================
+
+ _parseOctal(str, len) {
+ // Parse octal string (null/space-padded) from tar header field
+ const s = (str || '').replace(/[\x00\s]+$/g, '').replace(/^0+/, '') || '0';
+ return parseInt(s, 8) || 0;
+ }
+ _padOctal(num, len) {
+ // Format number as octal, null-terminated, len bytes
+ const oct = num.toString(8);
+ return oct + '\0'.repeat(len - oct.length - 1);
+ }
+
+ // Parse a tar buffer (Uint8Array) into [{name, type, size, data, mode, mtime}]
+ _tarParse(bytes) {
+ const entries = [];
+ const decoder = new TextDecoder();
+ let off = 0;
+ while (off < bytes.length) {
+ // Read 512-byte header
+ if (off + 512 > bytes.length) break;
+ const header = bytes.slice(off, off + 512);
+ // Empty block = end of archive
+ if (header.every(b => b === 0)) break;
+ const nameRaw = decoder.decode(header.slice(0, 100)).replace(/\0+$/, '');
+ const size = this._parseOctal(decoder.decode(header.slice(124, 136)), 12);
+ const typeFlag = String.fromCharCode(header[156]);
+ const magic = decoder.decode(header.slice(257, 263)).replace(/\0+$/, '');
+ // ustar prefix (POSIX): name split across header.name + header.prefix
+ let name = nameRaw;
+ if (magic === 'ustar') {
+ const prefix = decoder.decode(header.slice(345, 500)).replace(/\0+$/, '');
+ if (prefix) name = prefix + '/' + nameRaw;
+ }
+ off += 512;
+ // Read file data (padded to 512)
+ const data = bytes.slice(off, off + size);
+ off += Math.ceil(size / 512) * 512;
+ entries.push({ name, type: typeFlag, size, data, mode: this._parseOctal(decoder.decode(header.slice(100, 108)), 8) });
+ }
+ return entries;
+ }
+
+ // Build a tar buffer from a list of {name, type, size, data, mode} entries
+ _tarBuild(entries) {
+ const blocks = [];
+ const encoder = new TextEncoder();
+ for (const e of entries) {
+ const header = new Uint8Array(512);
+ // Name (100 bytes, null-padded)
+ const nameBytes = encoder.encode(e.name.slice(0, 99));
+ header.set(nameBytes, 0);
+ // Mode (8 bytes, octal null-terminated) - default 0644
+ const modeStr = (e.mode || 0o644).toString(8).padStart(7, '0') + '\0';
+ header.set(encoder.encode(modeStr), 100);
+ // uid/gid (8 bytes each, "0000000\0")
+ header.set(encoder.encode('0000000\0'), 108);
+ header.set(encoder.encode('0000000\0'), 116);
+ // Size (12 bytes, octal)
+ const sizeStr = e.size.toString(8).padStart(11, '0') + '\0';
+ header.set(encoder.encode(sizeStr), 124);
+ // Mtime (12 bytes, octal) - epoch
+ const mtimeStr = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0') + '\0';
+ header.set(encoder.encode(mtimeStr), 136);
+ // Type flag at offset 156
+ header[156] = e.type.charCodeAt(0);
+ // Magic "ustar\0" at 257
+ header.set(encoder.encode('ustar\0'), 257);
+ // Version "00" at 263
+ header.set(encoder.encode('00'), 263);
+ blocks.push(header);
+ // Data block(s), padded to 512
+ const data = e.data;
+ if (data && data.length > 0) {
+ const padded = new Uint8Array(Math.ceil(data.length / 512) * 512);
+ padded.set(data, 0);
+ blocks.push(padded);
+ }
+ }
+ // End-of-archive: two 512-byte zero blocks
+ blocks.push(new Uint8Array(512));
+ blocks.push(new Uint8Array(512));
+ // Concatenate
+ const total = blocks.reduce((s, b) => s + b.length, 0);
+ const out = new Uint8Array(total);
+ let pos = 0;
+ for (const b of blocks) { out.set(b, pos); pos += b.length; }
+ return out;
+ }
+
+ // Collect all files in a VFS directory recursively
+ _vfsCollectFiles(dirPath) {
+ const normalized = dirPath.replace(/^\/+/, '').replace(/\/+$/, '');
+ const entries = [];
+ const walk = (rel, absParts) => {
+ const items = this.terminal.fs.readDir(absParts.join('/'));
+ for (const item of items) {
+ const itemPath = absParts.concat([item.name]).join('/');
+ const relPath = rel ? `${rel}/${item.name}` : item.name;
+ if (item.type === 'dir') {
+ walk(relPath, absParts.concat([item.name]));
+ } else {
+ const content = this.terminal.fs.readFile(itemPath);
+ entries.push({ vfsPath: itemPath, archivePath: relPath, content: content || '' });
+ }
+ }
+ };
+ // Handle root case
+ if (!normalized) {
+ walk('', []);
+ } else {
+ walk(normalized.split('/').pop() || '', normalized.split('/'));
+ }
+ return entries;
+ }
+
+ // ============================================================
+ // WGET — fetch URL and save to VFS (binary-safe)
+ // ============================================================
+
+ async _cmdWget(args) {
+ if (args.length < 1) {
+ this.terminal.print('Usage: wget <url> [filename] — Download a URL and save to VFS', 'warning');
+ return;
+ }
+ const url = args[0];
+ const filename = args[1] || url.split('/').pop() || 'download.bin';
+ this.terminal.print(`Downloading ${url}...`, 'info');
+ try {
+ const resp = await fetch(url);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
+ const buf = await resp.arrayBuffer();
+ const bytes = new Uint8Array(buf);
+ this._vfsWriteBytes(filename, bytes);
+ const sizeKb = (bytes.length / 1024).toFixed(1);
+ this.terminal.print(`✅ wget: ${filename} (${sizeKb} KB, ${bytes.length} bytes)`, 'success');
+ } catch (e) {
+ this.terminal.print(`wget error: ${e.message}`, 'error');
+ }
+ }
+
+ // ============================================================
+ // TAR / UNTAR COMMANDS
+ // ============================================================
+
+ async _cmdTar(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: tar <src-dir> <out.tar[.gz]>', 'warning');
+ return;
+ }
+ const [srcPath, outPath] = args;
+ if (!this._vfsDirExists(srcPath)) {
+ this.terminal.print(`Source not found (or not a directory): ${srcPath}`, 'error');
+ return;
+ }
+ this.terminal.print(`Bundling ${srcPath} → ${outPath}...`, 'info');
+ const files = this._vfsCollectFiles(srcPath);
+ if (files.length === 0) {
+ this.terminal.print('No files found in source directory', 'warning');
+ return;
+ }
+ // Build tar entries (all '0' = regular file)
+ const entries = files.map(f => ({
+ name: f.archivePath,
+ type: '0',
+ size: f.content.length,
+ data: new TextEncoder().encode(f.content),
+ mode: 0o644
+ }));
+ let tarBytes = this._tarBuild(entries);
+ // Gzip if .gz suffix
+ if (outPath.endsWith('.gz')) {
+ tarBytes = await this._gzipBytes(tarBytes);
+ }
+ this._vfsWriteBytes(outPath, tarBytes);
+ const sizeKb = (tarBytes.length / 1024).toFixed(1);
+ this.terminal.print(`✅ tar: ${files.length} files → ${outPath} (${sizeKb} KB)`, 'success');
+ }
+
+ async _cmdUntar(args) {
+ if (args.length < 1) {
+ this.terminal.print('Usage: untar <path.tar[.gz]> [dest-dir]', 'warning');
+ return;
+ }
+ const srcPath = args[0];
+ const destDir = (args[1] || '/').replace(/^\/+/, '').replace(/\/+$/, '');
+ if (!this._vfsExists(srcPath)) {
+ this.terminal.print(`File not found: ${srcPath}`, 'error');
+ return;
+ }
+ this.terminal.print(`Extracting ${srcPath} → /${destDir}...`, 'info');
+ let bytes = this._vfsReadBytes(srcPath);
+ // Auto-detect and strip gzip
+ if (this._isGzip(bytes)) {
+ this.terminal.print(' detected gzip wrapper, decompressing...', 'info');
+ bytes = await this._gunzipBytes(bytes);
+ }
+ const entries = this._tarParse(bytes);
+ if (entries.length === 0) {
+ this.terminal.print('No entries found in tar (empty or invalid format)', 'error');
+ return;
+ }
+ // Write to VFS
+ let fileCount = 0, dirCount = 0;
+ for (const e of entries) {
+ // Clean name (strip leading ./, normalize slashes)
+ let cleanName = e.name.replace(/^\.?\//, '').replace(/\/+$/, '');
+ const targetPath = destDir ? `${destDir}/${cleanName}` : cleanName;
+ if (e.type === '5' || e.type === 'd') {
+ // Directory
+ if (cleanName) {
+ this.terminal.fs.mkdirRecursive(targetPath);
+ dirCount++;
+ }
+ } else {
+ // Regular file (or type '0' or '\0')
+ const dir = targetPath.split('/').slice(0, -1).join('/');
+ if (dir) this.terminal.fs.mkdirRecursive(dir);
+ const content = new TextDecoder('utf-8', { fatal: false }).decode(e.data);
+ this.terminal.fs.writeFile(targetPath, content);
+ fileCount++;
+ }
+ }
+ this.terminal.print(`✅ untar: ${fileCount} files, ${dirCount} dirs from ${srcPath}`, 'success');
+ }
+
// ============================================================
// ENCRYPT / DECRYPT (AES-256-GCM)
// ============================================================
@@ -280,14 +579,20 @@ class CryptoGzipModule {
this.commands['gunzip'] = async (args) => self._cmdGunzip(args);
this.commands['encrypt'] = async (args) => self._cmdEncrypt(args);
this.commands['decrypt'] = async (args) => self._cmdDecrypt(args);
+ this.commands['tar'] = async (args) => self._cmdTar(args);
+ this.commands['untar'] = async (args) => self._cmdUntar(args);
+ this.commands['wget'] = async (args) => self._cmdWget(args);
this.commandsMeta = {
gzip: { argType: 'file', description: 'Compress a VFS file to .gz' },
- gunzip: { argType: 'file', description: 'Decompress a .gz file' },
+ gunzip: { argType: 'file', description: 'Decompress a .gz file (binary-safe)' },
encrypt: { argType: 'file', description: 'Encrypt a file with passphrase' },
- decrypt: { argType: 'file', description: 'Decrypt a .enc file' }
+ decrypt: { argType: 'file', description: 'Decrypt a .enc file' },
+ tar: { argType: 'file', description: 'Bundle a VFS directory into a tar archive' },
+ untar: { argType: 'file', description: 'Extract a tar archive into VFS' },
+ wget: { argType: 'url', description: 'Download a URL and save to VFS' }
};
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
- webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js');
+ webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js?v=2026-06-08-v4.5.26');
}
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js
index 4074f06..3cd2687 100644
--- a/modules/ext/filemgr.js
+++ b/modules/ext/filemgr.js
@@ -1,6 +1,9 @@
class FileManagerModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'files': { argType: 'file' }, 'preview': { argType: 'file' },
+ };
this.terminal = null;
this.currentPath = [];
this._overlay = null;
diff --git a/modules/ext/floating.js b/modules/ext/floating.js
index b6f5a93..f59aa29 100644
--- a/modules/ext/floating.js
+++ b/modules/ext/floating.js
@@ -5,6 +5,11 @@
class FloatingModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'floating-remove': { argType: 'enum', enumFn: '_listElementIds' },
+ 'floating-toggle': { argType: 'enum', enumFn: '_listElementIds' },
+ 'floating-pos': { argType: 'enum', enumFn: '_listElementIds' },
+ };
this.terminal = null;
this._elements = {};
this._storageKey = 'webx11_floating_elements';
@@ -14,7 +19,7 @@ class FloatingModule {
getHelpInfo() {
return {
- 'floating-add': 'floating-add btn|menu|panel <id> <label> [command|buttons...] — Add a floating UI element',
+ 'floating-add': 'floating-add btn|menu|panel <id> <label> [cmd|btn:Label:cmd...] — Add a floating UI element',
'floating-remove': 'floating-remove <id> — Remove a floating element',
'floating-list': 'floating-list — Show all floating elements',
'floating-pos': 'floating-pos <id> <x|auto> <y|auto> — Reposition a floating element',
@@ -30,10 +35,11 @@ class FloatingModule {
this.commands['floating-pos'] = (args) => this._cmdPos(args);
this.commands['floating-clear'] = () => this._cmdClear();
this.commands['floating-toggle'] = (args) => this._cmdToggle(args);
- }
-
- seed() {
- this._restore();
+ // Restore persisted elements at boot — registerCommands() is the only hook called by ModuleManager
+ // but defer to next tick so terminal/fs are ready
+ setTimeout(() => {
+ this._restore();
+ }, 0);
}
// ── Persistence ──
@@ -202,7 +208,7 @@ class FloatingModule {
_cmdAdd(args) {
if (args.length < 3) {
- this.terminal.print('Usage: floating-add btn|menu|panel <id> <label> [command|btn:Label|cmd...]', 'warning');
+ this.terminal.print('Usage: floating-add btn|menu|panel <id> <label> [cmd|btn:Label:cmd...]', 'warning');
return;
}
const type = args[0];
@@ -230,14 +236,32 @@ class FloatingModule {
el.type = 'menu';
el.collapsed = true;
el.buttons = [];
+ // Remaining args are btn:Label:command pairs. Quotes or colons separate them.
+ // Each btn: entry runs until next btn: or end
+ let btnBuf = '';
for (let i = 3; i < args.length; i++) {
- const m = args[i].match(/^btn:(.+?)\|(.+)$/);
- if (m) {
- el.buttons.push({ label: m[1], command: m[2] });
+ if (btnBuf) {
+ btnBuf += ' ' + args[i];
+ } else {
+ btnBuf = args[i];
+ }
+ // Check if this completes a btn: entry
+ // btn:Label: remaining text — the label is between first : and second :
+ const firstColon = btnBuf.indexOf(':');
+ if (firstColon === -1) { btnBuf = ''; continue; } // prefix doesn't start with btn:
+ const secondColon = btnBuf.indexOf(':', firstColon + 1);
+ if (secondColon === -1) continue; // need more args to find command
+ const prefix = btnBuf.substring(0, firstColon);
+ if (prefix !== 'btn') { btnBuf = ''; continue; }
+ const label = btnBuf.substring(firstColon + 1, secondColon);
+ const command = btnBuf.substring(secondColon + 1);
+ if (label && command) {
+ el.buttons.push({ label, command });
}
+ btnBuf = '';
}
if (el.buttons.length === 0) {
- this.terminal.print('No valid sub-buttons. Use btn:Label|command format', 'warning');
+ this.terminal.print('No valid sub-buttons. Use btn:Label:command format', 'warning');
return;
}
break;
@@ -328,4 +352,10 @@ class FloatingModule {
this._renderAll();
this.terminal.print('All floating elements cleared', 'success');
}
+
+ // ── Enum helpers for autocomplete ──
+
+ _listElementIds() {
+ return Object.keys(this._elements);
+ }
} \ No newline at end of file
diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js
index abbc7ab..bb00672 100644
--- a/modules/ext/skill-pack-core.js
+++ b/modules/ext/skill-pack-core.js
@@ -1,6 +1,12 @@
class SkillPackCoreModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'skill-list': { argType: 'dir' }, 'skill-view': { argType: 'file' },
+ 'skill-create': { argType: 'dir' }, 'skill-patch': { argType: 'file' },
+ 'skill-delete': { argType: 'file' }, 'skill-export': { argType: 'file' },
+ 'skill-sync': { argType: 'file' },
+ };
this.terminal = null;
this._seeded = false;
this._skills = this._buildSkills();
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index a025cb2..8096b2e 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -1,6 +1,14 @@
class WebDAVModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'webdav-ls': { argType: 'file' }, 'webdav-push': { argType: 'file' },
+ 'webdav-pull': { argType: 'file' }, 'webdav-rm': { argType: 'file' },
+ 'webdav-mkdir': { argType: 'dir' }, 'webdav-rename': { argType: 'file' },
+ 'webdav-move': { argType: 'file' }, 'webdav-backup': { argType: 'file' },
+ 'webdav-restore': { argType: 'file' }, 'webdav-browser': { argType: 'file' },
+ 'webdav-config': { argType: 'file' },
+ };
this.terminal = null;
this.config = null; // { url, username, password }
this._browsingPath = '/';
diff --git a/modules/ext/wpm.js b/modules/ext/wpm.js
index 4dc9c29..314e124 100644
--- a/modules/ext/wpm.js
+++ b/modules/ext/wpm.js
@@ -1,6 +1,9 @@
class WpmModule {
constructor() {
this.commands = {};
+ this.commandsMeta = {
+ 'wpm': { argType: 'enum', enumFn: '_listSubcommands' },
+ };
this.terminal = null;
this.indexUrl = 'https://webx11.duckdns.org/spaces/newos4/packages.json';
this.index = null;
@@ -143,6 +146,12 @@ class WpmModule {
if (installed) this.terminal.print(`Installed: ${installed.installed}`, 'success');
else this.terminal.print('Not installed. Use: wpm install ' + name, 'info');
}
+
+ // ── Enum helpers for autocomplete ──
+
+ _listSubcommands() {
+ return ['install', 'list', 'search', 'update', 'remove', 'info'];
+ }
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new WpmModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/wpm.js');