From aafb0fbde5289a45f9f818a00141f80b149be283 Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 2 Jun 2026 18:00:37 +0200 Subject: auto: 2026-06-02_18:00 changes --- modules/ext/skill-pack-core.js | 284 +++++++++++++++++++---- modules/ext/webdav.js | 515 ++++++++++++++++++++++++++++++++++------- 2 files changed, 666 insertions(+), 133 deletions(-) (limited to 'modules/ext') diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index cc8ae88..f731594 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -20,37 +20,51 @@ class SkillPackCoreModule { getHelpInfo() { return { - 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, mindmap, etc.)', + 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, agents, webdav, security, etc.)', }; } _buildSkills() { const s = (name, desc, tags, cat, steps) => ({ name, description: desc, tags, category: cat, - content: `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: ${cat}\n---\n## Steps\n${steps}` + content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n## Steps\n' + steps + }); + + const sk = (name, desc, tags, cat, content) => ({ + name, description: desc, tags, category: cat, + content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n' + content }); return [ s('navigating-the-terminal', 'How to explore the WebX11 terminal, find commands, check status, and navigate the filesystem', ['terminal', 'navigation', 'help', 'filesystem'], 'system', `1. **Get an overview of available commands:** - - Call get_help() to get a full command list sorted by category - - Call get_status() to see filesystem state, loaded modules, Ollama status - - For a specific command, use help or check the help map + - Type "help" for a full command list sorted by category + - Type "status" to see filesystem state, loaded modules, Ollama status + - For a specific command, use help + - Type "commands" for a compact list with module source info + - Type "check" for system health including module requirements 2. **Explore the filesystem:** - - Use list_files(path?) to browse directories (omit path for current dir) - - Use read_file(path) to view file contents - - Use get_current_dir to see your working directory - - Use run_command("ls") or run_command("tree") for visual output + - Use ls, ll, tree to browse directories + - Use cd to navigate, pwd to show current path + - Use cat or edit to view file contents + - Use find or grep to search files + - Use du to see disk usage 3. **Understand system state:** - - run_command("mem") — memory usage - - run_command("status") — network, files, modules, Ollama - - run_command("modules") — module manager GUI - - get_module_status() — loaded modules, URLs, local edits - -4. **When user asks a vague question:** - - First call get_status() and get_help() to understand the current state + - mem — memory usage (auto-shown in header on boot) + - status — network, files, modules, Ollama + - modules — module manager GUI + - health-check — VFS size, modules, logs, trash, skills, tool failures + - checkpoint-list — VFS checkpoint snapshots + +4. **Agent / Orchestration state:** + - agent-list — registered agents (type, capabilities, status) + - orchestrate — list active orchestrated agents + - lease-list — show active leases with TTL + +5. **When user asks a vague question:** + - First run status and help to understand the current state - Then ask clarifying questions based on what you found`), s('memory-and-profile', 'How to use the user profile system, session memory, and pattern tracking for persistent context', ['memory', 'profile', 'session', 'persistence'], 'system', @@ -67,26 +81,36 @@ class SkillPackCoreModule { - session_recent(count) — show last N session summaries - session_stats — aggregate totals across all sessions - Recent sessions auto-load into context on first message + - systemd-style journal: use logs and logrotate commands for persistent logging 3. **Conversation Patterns:** - Pattern tracking runs automatically — frequent topics surface as hints - my-set, my-get, my-list store additional personal data - AI conversation context persists within a session (use ai-clear to reset) + - debug command toggles real-time debug panel with Copy/Export/VFS buttons -4. **When to use:** +4. **Checkpoints (VFS snapshots):** + - checkpoint [name] — snapshot current VFS state + - checkpoint-list — list all checkpoints + - rollback — restore from checkpoint + - checkpoint-del — delete a checkpoint + +5. **When to use:** - At start: call session_recent(3) to see what was worked on before - After identifying user preferences: call user_set to store them - - After complex tasks: call session_save with a brief summary`), + - After complex tasks: call session_save with a brief summary + - Before risky edits: call checkpoint to create a restore point`), s('web-search-and-fetch', 'How to search the web and fetch URL content using the WebX11 proxy API', ['search', 'fetch', 'web', 'proxy', 'api'], 'research', `1. **Search the web:** - - Call web_search(query) — uses /api/search which queries SearXNG + DDG fallback + - Use search or web_search(query) — uses /api/search which queries SearXNG + DDG fallback - Results include: title, url, content snippet, source engine - Always available (same-origin, no CORS issues) - 5-minute server-side cache for repeated queries + - Alternative: use ddg or duck for DuckDuckGo direct, or s as alias 2. **Fetch URL content:** - - Call web_fetch(url) — uses /api/extract which strips HTML, extracts clean text + - Use fetch or web_fetch(url) — uses /api/extract which strips HTML, extracts clean text - Returns: title, content (up to 4000 chars), word_count, extraction method - SSRF protection blocks internal network addresses - 15-minute server-side cache @@ -96,19 +120,21 @@ class SkillPackCoreModule { - search --fetch — fetches content snippets for top 3 results 4. **When to use:** - - User asks about current events, facts, research → call web_search - - User says "read this article" or provides a URL → call web_fetch + - User asks about current events, facts, research → use search + - User says "read this article" or provides a URL → use fetch - For in-depth analysis: search first, then fetch top results`), s('module-management', 'How to view, edit, test, reload modules and manage startup configuration', ['modules', 'editing', 'code', 'self-improvement'], 'dev', `1. **View module source:** - view_module(name) — fetches original source or reads from _mods/ if edited - Supports all loaded modules: SystemModule, FileSystemModule, SearXNGModule, etc. + - Or use modules command for GUI module manager 2. **Edit a module:** - edit_module(name) — copies source to _mods/.js and opens code editor - Editor shows the full source in a textarea - Click "Save & Test" to save and auto-load the edited version + - Checkpoint before editing: checkpoint mod-edit- 3. **Test and reload:** - test_module(name) — loads edited module from _mods/ for live testing @@ -121,8 +147,15 @@ class SkillPackCoreModule { 5. **Manage startup:** - startup-modules — opens the Startup Modules UI to select which modules load - load-local — load a module from VFS or URL without editing + - loadmod — alternative: load module from URL + - removemod — remove a loaded module -6. **When to use:** +6. **Agent modules (Phase 2):** + - module-agent-registry.js — Agent registry with agent-list, agent-register, agent-unregister, agent-status + - module-leases.js — Time-bound contracts with 5s watchdog, auto-expiry + - module-orchestrator.js — Spawn, delegate, kill, purge with heartbeat monitoring + +7. **When to use:** - User reports a bug in a module → view_module, edit_module, test_module - User asks "can you add X feature?" → same workflow - Always save edits before testing. If test fails, the original module stays loaded.`), @@ -142,42 +175,68 @@ class SkillPackCoreModule { - config-export — dump all config as JSON - config-reset — restore defaults (use before factory-reset if you want to save data) -4. **Health checks (run automatically after complex tasks):** +4. **Security level:** + - security-level <0-3> — set security level (0=Permissive, 1=Standard, 2=Paranoid, 3=Audit) + - security — show current security level, blocked count, and stats + - cmd-allow — whitelist a command from security checks + - cmd-deny — blacklist a command + +5. **Health checks (run automatically after complex tasks):** - Check module loading status: loaded vs total - Check for pending module edits - Check memory usage (high if >100MB) - Check skill count (0 skills after 2+ tool calls → suggest create_skill) + - health-check — full audit: VFS size, modules, logs, trash, skills, tool failures -5. **Font size and display:** +6. **Font size and display:** - font-size <8-32> — set terminal font size, persists across sessions - setfont — alias for font-size -6. **When to use:** - - System is slow or modules fail to load → check check_updates, memory +7. **Checkpoints:** + - checkpoint [name] — snapshot current VFS state + - checkpoint-list — list all checkpoints + - rollback — restore from checkpoint + +8. **When to use:** + - System is slow or modules fail to load → check check_updates, mem - User says "reset everything" → factory-reset --confirm - - User says "clear my files but keep settings" → reset-vfs --confirm`), + - User says "clear my files but keep settings" → reset-vfs --confirm + - User reports security concerns → security-level or cmd-allow/cmd-deny`), s('backup-and-restore', 'How to backup, restore, and export WebX11 data', ['backup', 'restore', 'export', 'import', 'persistence'], 'system', `1. **Backup VFS:** - vfs-export — downloads the entire virtual filesystem as a dated JSON file - backup — creates a full backup including VFS, modules, config (saves to _backups/) -2. **Export/Import personal data:** +2. **WebDAV cloud backup:** + - webdav-backup — push VFS snapshot to WebDAV server (auto-archive) + - webdav-backup-list — list remote WebDAV backups + - webdav-restore — restore VFS from remote WebDAV snapshot + - webdav-push — push file to WebDAV space + - webdav-pull — pull file from WebDAV space + - webdav-ls [path] — list remote WebDAV directory contents + - webdav-status — check WebDAV connection and space usage + - webdav-explorer — GUI file browser for WebDAV + - webdav-browser — simpler file listing for WebDAV + - webdav-config — configure WebDAV endpoint and credentials + +3. **Export/Import personal data:** - my-export — prints all my-set data as formatted JSON - my-import — bulk-import personal data from JSON string -3. **Export/Import modules:** +4. **Export/Import modules:** - export-mods — exports module registry as JSON - import-mods — imports modules from JSON backup -4. **Restore:** +5. **Restore:** - restore — restores terminal state from a backup JSON - Session logs are saved to /memory/ automatically on page unload -5. **When to use:** +6. **When to use:** - Before factory-reset: call my-export, vfs-export, export-mods to save data - When switching browsers or devices: export all, then import - - User says "backup my data" or "save my files"`), + - User says "backup my data" or "save my files" + - For cloud backup: use webdav-backup (requires WebDAV server configured)`), s('mindmap-workflow', 'How to create, view, edit, and manage brainstorming mindmaps', ['mindmap', 'brainstorming', 'planning', 'organization'], 'productivity', `1. **Create a new mindmap:** @@ -215,18 +274,19 @@ class SkillPackCoreModule { s('skills-system-guide', 'How to use the skills system: load, create, patch, list, and audit skills for persistent procedural memory', ['skills', 'system', 'meta'], 'system', `1. **Always load skills at task start:** - - At the START of every task, call load_skills(query) to check for stored procedures - - Example: load_skills("search the web") returns matching skill content - - Example: load_skills("edit module") returns the module editing workflow + - At the START of every task, type or call "skill-view " or "skill-list" + - Example: skill-view "module-management" — returns matching skill content + - Example: skill-list system — browse skills by category 2. **Create skills after complex tasks:** - - After any task with 3+ tool calls, call create_skill to save the approach + - After any task with 3+ tool calls, call "skill-create" - Required fields: name (kebab-case), description, content (markdown with YAML frontmatter) - Optional: tags (comma-separated), category (e.g. research, system, dev) - Skills persist in /skills/ as .md files with /skills/index.json for fast lookup + - Skill pack auto-seeds 8 core skills on first boot 3. **Patch skills when they're outdated:** - - If a stored procedure has wrong steps, call patch_skill(name, newContent) + - If a stored procedure has wrong steps, call "skill-patch " - This replaces the entire skill content and updates the index - Do NOT create a duplicate — always patch the existing skill @@ -235,13 +295,139 @@ class SkillPackCoreModule { - Named tool--workaround (e.g. tool-write_file-workaround) 5. **Audit and maintenance:** - - list_skills(category?) — browse available skills, optionally filter + - skill-list — browse available skills, optionally filter + - skill-export — download a skill as markdown + - skill-import — bulk import skills + - skill-sync — re-index skills from /skills/ directory - Skills are audited every 20 messages for staleness (empty content, >30 days old) 6. **YAML frontmatter format:** - Each skill file must start with: ---\\nname: \\ndescription: \\ntags: [...]\\ncategory: \\n---\\n - Steps follow under ## Steps heading - Use numbered lists for sequential steps`), + + sk('agent-orchestration', 'How to use the agent registry, leases, and orchestrator for multi-agent workflows', ['agents', 'orchestration', 'delegation', 'leases'], 'system', +`## Steps +1. **Agent Registry:** + - agent-list — list all registered agents (ID, type, capabilities, status) + - agent-register — register a new agent (capabilities: comma-separated like "code,research") + - agent-unregister — remove an agent + - agent-status — detailed status including uptime, task history + +2. **Leases (time-bound contracts):** + - lease-create — create a lease with watchdog timer + - lease-list — show active leases with remaining TTL + - lease-revoke — terminate a lease early + - lease-renew — extend a lease + +3. **Orchestrator (spawn, delegate, kill):** + - orchestrator-spawn — spawn a new agent automatically (registers + creates lease) + - orchestrator-kill — kill an agent (revokes lease + unregisters) + - orchestrate — list all active agents with their orchestrator state + - orchestrator-purge — kill all idle/expired agents + - orchestrator-status — full system status: active, idle, dead agents + +4. **AI tool integration:** + - AI tools list_agents, delegate_task, spawn_agent are available in the AI system prompt + - The AI can auto-spawn worker agents for sub-tasks + - Each agent gets its own VFS workspace at /agents// + +5. **When to use:** + - User needs parallel task execution → orchestrator-spawn coder + researcher + - Time-bound automation → lease-create with short TTL + - System cleanup → orchestrator-purge to kill stale agents`), + + sk('debug-panel-and-logs', 'How to use the debug panel, session logging, and terminal diagnostics', ['debug', 'logging', 'diagnostics', 'troubleshooting'], 'system', +`## Steps +1. **Toggle debug panel:** + - debug — opens real-time debug panel showing last AI tool call, response time, and context + - Debug panel includes Copy, Export, and Write to VFS buttons + - Export: saves tool call log as timestamped JSON to downloads + - Write to VFS: appends debug data to /debug.log (dedup by turn ID) + +2. **Session logging:** + - logs [n] [LEVEL] — view last n log entries (optionally filtered by INFO/WARN/ERROR) + - logrotate — rotate current.log to dated archive (auto-cleans old logs beyond 10) + - Logs stored in /logs/ directory: current.log + dated archives + - Health check runs automatically after complex tasks + +3. **Diagnostic commands:** + - health-check — full audit: VFS size, modules, logs, trash, skills + - check — quick system health including module requirement status + - mem — real-time memory usage (also in header) + - status — network, files, modules, Ollama connectivity + +4. **System journals:** + - debug.log — appended by debug panel VFS button (dedup by turn ID) + - current.log — system activity log with rotation + - Auto-save: terminal state auto-saves every 60s + +5. **When to use:** + - AI is behaving unexpectedly → debug to see tool calls + - User reports errors → logs 50 ERROR to see error entries + - Memory concerns → mem to check, health-check for full audit`), + + sk('webdav-operations', 'How to use WebDAV cloud storage for backup, file sync, and remote file access', ['webdav', 'cloud', 'backup', 'sync', 'storage'], 'system', +`## Steps +1. **Configuration:** + - webdav-config — configure WebDAV endpoint URL and credentials + - Connection auto-tested on config; webdav-status shows connection health + - Uses per-user token stored in .token file on the server side + +2. **File operations:** + - webdav-ls [path] — list remote directory contents + - webdav-push — upload file to WebDAV + - webdav-pull [local-path] — download file from WebDAV + - webdav-explorer — GUI file browser for WebDAV + - webdav-browser — CLI file browser for WebDAV + +3. **Backup & Restore:** + - webdav-backup — push full VFS snapshot to WebDAV (creates /backups/newos4-.json) + - webdav-backup-list — list all remote backup snapshots + - webdav-restore — restore VFS from a remote backup snapshot + +4. **Status & Diagnostics:** + - webdav-status — connection status, last sync, space information + - If uploads fail: check webdav-config, ensure .token file is valid + - WebDAV runs on the same server (nginx with WebDAV module), no external dependency + +5. **When to use:** + - Before factory-reset: webdav-backup to store VFS remotely + - Cross-browser: webdav-push/pull to move files between instances + - User says "cloud backup" or "remote save" → webdav-backup`), + + sk('security-and-safety', 'How to use WebX11 security levels, command blocking, and safety features', ['security', 'safety', 'blocking', 'permissions'], 'system', +`## Steps +1. **Security levels:** + - security-level <0-3> — set security level: + 0 = Permissive (no blocks, no confirms — anything runs) + 1 = Standard (unknown cmds → AI route, destructive → confirm, system → block) + 2 = Paranoid (unknown cmds blocked entirely, destructive blocked) + 3 = Audit (everything allowed, everything logged) + - Default: level 1 + - security — show current level, blocked count, and stats + +2. **Command control:** + - cmd-allow — whitelist a command from security checks + - cmd-deny — blacklist/block a command entirely + - whitelist built-in: help, clear, ls, cat, pwd, whoami, df, free, top, ps, echo, date, env, history + +3. **What gets blocked at each level:** + - Unknown commands: Level 1 → AI routed, Level 2+ → blocked + - Destructive commands (delete, write, save, format, etc.): Level 1 → confirmation, Level 2+ → blocked + - System commands (reboot, shutdown, format, kill): Level 1+ → blocked + - Suspicious flags (--execute, --eval, --shell): Level 1+ → blocked + +4. **Safety checklist:** + - Check security: security shows your current level + - Before running risky commands: create a checkpoint first + - For temporary relax: security-level 0, then restore to 1 + - For total lockdown before leaving: security-level 2 + +5. **When to use:** + - User wants to lock down the terminal → security-level 2 + - User wants to allow a blocked command → cmd-allow + - Debugging strange behavior → verify security-level (user may have set it higher)`) ]; } registerCommands() { @@ -264,8 +450,9 @@ class SkillPackCoreModule { console.log('[SkillPackCore] existing index keys:', Object.keys(index)); let seeded = 0; for (const skill of this._skills) { - if (!index[skill.name]) { - fs.writeFile(`/skills/${skill.name}.md`, skill.content); + const existing = index[skill.name]; + if (!existing) { + fs.writeFile('/skills/' + skill.name + '.md', skill.content); index[skill.name] = { name: skill.name, description: skill.description, @@ -275,14 +462,25 @@ class SkillPackCoreModule { updated: new Date().toISOString() }; seeded++; + } else { + // Update existing skill content on fresh load (cache-bust version bump triggers this) + fs.writeFile('/skills/' + skill.name + '.md', skill.content); + index[skill.name] = { + ...existing, + description: skill.description, + tags: skill.tags, + category: skill.category, + updated: new Date().toISOString() + }; } } if (seeded > 0) { - fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print('📚 Seeded ' + seeded + ' new skill(s)', 'success'); } + fs.fsWriteJSON('/skills/index.json', index); this._seeded = true; } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js'); -} +} \ No newline at end of file diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index b4dfa6f..eea513b 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -46,8 +46,11 @@ class WebDAVModule { 'webdav-backup': 'Backup entire VFS to WebDAV as a dated JSON snapshot', 'webdav-restore': 'Restore VFS from a WebDAV backup file', 'webdav-backup-list': 'List available backup files on WebDAV', - 'webdav-explorer': 'webdav-explorer [path] — Interactive remote file browser', - 'webdav-browser': 'webdav-browser [path] — Visual WebDAV file explorer modal', + 'webdav-browser': 'webdav-browser [path] — Modern WebDAV file-tree browser with bulk operations', + 'webdav-rm': 'webdav-rm — Delete a remote file or directory', + 'webdav-mkdir': 'webdav-mkdir — Create a remote directory', + 'webdav-rename': 'webdav-rename — Rename or move a remote file/dir', + 'webdav-move': 'webdav-move — Alias for webdav-rename', }; } @@ -127,7 +130,11 @@ class WebDAVModule { } async _listDir(path) { - // Try PROPFIND first (works with WebDAV servers), fallback to nginx autoindex + // For WebX11 spaces, skip PROPFIND (nginx returns 302) and use __list__ directly + if (this._spaceName) { + return this._listDirViaAutoIndex(path); + } + // Non-nginx: try PROPFIND first, fallback to autoindex try { const response = await this._webdavRequest('PROPFIND', path, { headers: { 'Depth': '1' } @@ -137,15 +144,24 @@ class WebDAVModule { return this._parsePropfind(text); } } catch (e) { /* PROPFIND failed, try autoindex */ } - // Fallback: use nginx __list__ endpoint return this._listDirViaAutoIndex(path); } async _listDirViaAutoIndex(path) { - const baseUrl = this.config.url.replace(/\/+$/, '') + '/'; - const urlPath = path ? (path.startsWith('/') ? path : '/' + path) : '/'; - // Use __list__ for directory listing - const listUrl = baseUrl.replace(/\/spaces\/([^/]+)\//, '/spaces/$1/__list__/') + urlPath.replace(/^\//, ''); + if (!this._spaceName) { + throw new Error('WebDAV: cannot use __list__ without space name'); + } + // Derive host from configured URL to support different environments + let host = 'https://webx11.duckdns.org'; + if (this.config && this.config.url) { + try { + const u = new URL(this.config.url); + host = u.origin; + } catch (e) {} + } + const cleanPath = path ? (path.startsWith('/') ? path : '/' + path) : '/'; + // Avoid double slash when path starts with / + const listUrl = host + '/spaces/' + this._spaceName + '/__list__' + (cleanPath === '/' ? '' : cleanPath.replace(/\/{2,}/g, '/')); try { const resp = await fetch(listUrl, { method: 'GET' }); if (!resp.ok) throw new Error('Autoindex failed: HTTP ' + resp.status); @@ -158,15 +174,14 @@ class WebDAVModule { _parseAutoindex(html) { const entries = []; - // Parse nginx autoindex HTML format - const linkRegex = /([^<]+)<\/a>\s+(\d{2}-[A-Za-z]{3}-\d{4}\s+\d{2}:\d{2})\s+([\d\w.-]+)/g; + // Parse nginx autoindex HTML format — flexible regex that doesn't depend on date locale + const linkRegex = /([^<]+)<\/a>\s+([\w]{2}-[\w]{3}-\d{4}\s+\d{2}:\d{2})\s+([\d\w.-]+)/g; let match; while ((match = linkRegex.exec(html)) !== null) { const href = match[1]; const name = match[2].replace(/\/$/, ''); const sizeStr = match[4]; - if (name === '..' || name === '../') continue; - if (name === '__list__') continue; + if (name === '..' || name === '../' || name === '__list__') continue; const isDir = href.endsWith('/'); let size = 0; if (!isDir && sizeStr !== '-') { @@ -181,22 +196,40 @@ class WebDAVModule { } // Fallback: if regex failed, try simpler parse if (entries.length === 0) { - const simpleRegex = //g; + // Try broadest: extract all hrefs + const broadRegex = /]*>/g; const names = []; - while ((match = simpleRegex.exec(html)) !== null) { + while ((match = broadRegex.exec(html)) !== null) { const href = match[1]; if (href === '../' || href === '/' || href === './') continue; if (href.startsWith('?')) continue; names.push(href); } - for (const href of names) { - const isDir = href.endsWith('/'); - entries.push({ - name: decodeURIComponent(href.replace(/\/$/, '').split('/').pop() || href), - type: isDir ? 'dir' : 'file', - size: 0, - href: href - }); + if (names.length > 0) { + for (const href of names) { + const isDir = href.endsWith('/'); + entries.push({ + name: decodeURIComponent(href.replace(/\/$/, '').split('/').pop() || href), + type: isDir ? 'dir' : 'file', + size: 0, + href: href + }); + } + } else { + // Last resort: match any + const lastRegex = /5MB + if (serialized.length > 5 * 1024 * 1024) { + self.terminal.print('Backup too large: ' + self._formatSize(serialized.length) + ' (>5MB limit). Try reducing VFS size first.', 'error'); + return; + } // Gzip payload let payload = serialized; let isGzipped = false; @@ -550,7 +594,9 @@ class WebDAVModule { const buf = await resp.arrayBuffer(); payload = new Uint8Array(buf); isGzipped = true; - } catch (e) { /* fallback to plain */ } + } catch (e) { self.terminal.print('Gzip unavailable, falling back to plain JSON', 'info'); } + } else { + self.terminal.print('CompressionStream not supported, saving as plain JSON', 'info'); } // Encrypt if requested if (encryptPass) { @@ -671,37 +717,74 @@ class WebDAVModule { } }; - // --- webdav-browser: visual modal file browser --- + // --- webdav-browser: modern visual file browser --- this.commands['webdav-browser'] = async (args) => { const startPath = args[0] || self._browsingPath || '/'; await self.openWebdavBrowser(startPath); }; - // --- webdav-explorer: interactive file browser via WebDAV --- - this.commands['webdav-explorer'] = async (args) => { - const startPath = args[0] || '/'; + // --- webdav-rm: delete remote file/dir --- + this.commands['webdav-rm'] = async (args) => { + if (!args[0]) { + self.terminal.print('Usage: webdav-rm ', 'warning'); + return; + } try { - const entries = await self._listDir(startPath); - self.terminal.print('📁 WebDAV: ' + startPath, 'info'); - if (entries.length === 0) { - self.terminal.print('(empty)', 'info'); - return; + const ok = await self._deleteFile(args[0]); + if (ok) { + self.terminal.print('Deleted: ' + args[0], 'success'); + } else { + self.terminal.print('Delete failed: resource may not exist', 'error'); } - entries.forEach(e => { - const icon = e.type === 'dir' ? '📁' : '📄'; - const size = e.type === 'file' ? ' (' + self._formatSize(e.size) + ')' : '/'; - self.terminal.print(icon + ' ' + e.name + (e.type === 'dir' ? '/' : '') + size); + } catch (e) { + self.terminal.print('Delete error: ' + e.message, 'error'); + } + }; + + // --- webdav-mkdir: create remote directory --- + this.commands['webdav-mkdir'] = async (args) => { + if (!args[0]) { + self.terminal.print('Usage: webdav-mkdir ', 'warning'); + return; + } + try { + const ok = await self._ensureDir(args[0]); + if (ok) { + self.terminal.print('Created directory: ' + args[0], 'success'); + } else { + self.terminal.print('Failed to create directory', 'error'); + } + } catch (e) { + self.terminal.print('Mkdir error: ' + e.message, 'error'); + } + }; + + // --- webdav-rename: rename/move remote file or dir (MOVE method) --- + this.commands['webdav-rename'] = async (args) => { + if (!args[0] || !args[1]) { + self.terminal.print('Usage: webdav-rename ', 'warning'); + self.terminal.print('Renames or moves a file/directory using WebDAV MOVE', 'info'); + return; + } + try { + const resp = await self._webdavRequest('MOVE', args[0], { + headers: { 'Destination': args[1] } }); - self.terminal.print('---'); - self.terminal.print('↳ Navigate: webdav-explorer | Pull: webdav-pull ', 'text-muted'); - self.terminal.print(entries.length + ' item(s)', 'text-muted'); + if (resp.ok || resp.status === 201 || resp.status === 204) { + self.terminal.print('Renamed: ' + args[0] + ' → ' + args[1], 'success'); + } else { + self.terminal.print('Rename failed: HTTP ' + resp.status, 'error'); + } } catch (e) { - self.terminal.print('WebDAV error: ' + e.message, 'error'); + self.terminal.print('Rename error: ' + e.message, 'error'); } }; + + // --- webdav-move: alias for webdav-rename --- + this.commands['webdav-move'] = this.commands['webdav-rename']; } - /* ── Visual WebDAV Browser Modal ── */ + /* ── Modern WebDAV File-Tree Browser with Bulk Ops ── */ async openWebdavBrowser(path) { if (!this.config || !this.config.url) { @@ -710,6 +793,7 @@ class WebDAVModule { } this._browsingPath = path; const self = this; + this._selected = {}; // Remove existing modal if any const existing = document.getElementById('webdav-browser-modal'); @@ -724,6 +808,12 @@ class WebDAVModule { errorMsg = e.message; } + // Sort: dirs first, then files, alphabetically + entries.sort(function(a, b) { + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; + return a.name.localeCompare(b.name); + }); + // Build modal const overlay = document.createElement('div'); overlay.id = 'webdav-browser-modal'; @@ -733,73 +823,237 @@ class WebDAVModule { const content = document.createElement('div'); content.className = 'modal-content'; - content.style.cssText = 'max-width:650px; max-height:80vh; width:92%;'; + content.style.cssText = 'max-width:750px; max-height:85vh; width:94%;'; // Header const header = document.createElement('div'); header.className = 'modal-header'; - header.innerHTML = '📁 WebDAV Browser✕'; + header.innerHTML = '📁 WebDAV File Tree' + + '' + + '' + + '✕'; content.appendChild(header); // Body const body = document.createElement('div'); body.className = 'modal-body'; - body.style.cssText = 'padding:8px; overflow-y:auto; flex-grow:1;'; + body.style.cssText = 'padding:0; overflow-y:auto; flex-grow:1;'; if (errorMsg) { const errDiv = document.createElement('div'); errDiv.style.cssText = 'color:var(--error-color); padding:16px; text-align:center;'; errDiv.textContent = '❌ ' + errorMsg; body.appendChild(errDiv); - } else if (entries.length === 0) { - const emptyDiv = document.createElement('div'); - emptyDiv.className = 'text-center'; - emptyDiv.style.cssText = 'padding:20px; color:var(--text-muted);'; - emptyDiv.textContent = '(empty) — upload files using the button below'; - body.appendChild(emptyDiv); } else { - // Sort: dirs first, then files, alphabetically - entries.sort(function(a, b) { - if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; - return a.name.localeCompare(b.name); - }); + // Select-all row + const selectAllRow = document.createElement('div'); + selectAllRow.style.cssText = 'display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid #222;font-size:0.8em;color:var(--text-muted);gap:8px;'; + const selAllCheck = document.createElement('input'); + selAllCheck.type = 'checkbox'; + selAllCheck.style.cssText = 'accent-color:var(--success-color);width:16px;height:16px;cursor:pointer;'; + selAllCheck.onchange = function() { + const checked = this.checked; + entries.forEach(function(e) { self._selected[e.name] = checked; }); + document.querySelectorAll('.wdav-checkbox').forEach(function(c) { c.checked = checked; }); + self._updateSelectionCount(); + }; + selectAllRow.appendChild(selAllCheck); + selectAllRow.appendChild(document.createTextNode('Select All (' + entries.length + ' items)')); + body.appendChild(selectAllRow); - var frag = document.createDocumentFragment(); + // File rows + const listContainer = document.createElement('div'); + let rowIndex = 0; entries.forEach(function(entry) { - var item = document.createElement('div'); - item.className = entry.type === 'dir' ? 'file-item dir' : 'file-item'; - item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:8px; border-bottom:1px solid #111; cursor:pointer; min-height:36px;'; - item.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; }; - item.onmouseout = function() { this.style.background = ''; }; + var row = document.createElement('div'); + row.className = 'wdav-row'; + row.style.cssText = 'display:flex;align-items:center;padding:7px 10px;border-bottom:1px solid #1a1a1a;cursor:default;min-height:38px;gap:8px;'; + row.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; }; + row.onmouseout = function() { this.style.background = ''; }; + row.dataset.index = rowIndex++; + + // Checkbox + var cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.className = 'wdav-checkbox'; + cb.style.cssText = 'accent-color:var(--success-color);width:15px;height:15px;cursor:pointer;flex-shrink:0;'; + cb.onchange = function(e) { + self._selected[entry.name] = this.checked; + self._updateSelectionCount(); + e.stopPropagation(); + }; + row.appendChild(cb); + // Icon + var iconSpan = document.createElement('span'); + iconSpan.textContent = entry.type === 'dir' ? '📁' : '📄'; + iconSpan.style.cssText = 'flex-shrink:0;font-size:1.1em;'; + + // Filename (editable on dblclick) var nameSpan = document.createElement('span'); - var icon = entry.type === 'dir' ? '📁' : '📄'; - nameSpan.textContent = icon + ' ' + entry.name + (entry.type === 'dir' ? '/' : ''); + nameSpan.textContent = entry.name + (entry.type === 'dir' ? '/' : ''); + nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.9em;cursor:pointer;'; + nameSpan.title = entry.name; + nameSpan.ondblclick = function(e) { + e.stopPropagation(); + var input = document.createElement('input'); + input.type = 'text'; + input.value = entry.name; + input.style.cssText = 'width:100%;background:#1a1a1a;border:1px solid var(--success-color);color:var(--text-color);padding:2px 6px;border-radius:3px;font-size:0.9em;'; + input.onblur = function() { + var newName = this.value.trim(); + if (newName && newName !== entry.name) { + var parentPath = path.replace(/\/+$/, '') || ''; + var oldFull = parentPath + '/' + entry.name; + var newFull = parentPath + '/' + newName; + self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } }) + .then(function(r) { + if (r.ok || r.status === 201 || r.status === 204) { + self.terminal.print('Renamed: ' + entry.name + ' → ' + newName, 'success'); + self.openWebdavBrowser(path); + } else { + self.terminal.print('Rename failed: HTTP ' + r.status, 'error'); + nameSpan.textContent = entry.name; + } + }); + } else { + nameSpan.textContent = entry.name; + } + }; + input.onkeydown = function(ev) { + if (ev.key === 'Enter') this.blur(); + if (ev.key === 'Escape') { nameSpan.textContent = entry.name; } + }; + nameSpan.textContent = ''; + nameSpan.appendChild(input); + input.focus(); + input.select(); + }; + // Click = navigate dir / preview file + if (entry.type === 'dir') { + row.onclick = function(e) { + if (e.target.type === 'checkbox') return; + self.openWebdavBrowser(entry.href); + }; + } else { + row.onclick = function(e) { + if (e.target.type === 'checkbox') return; + self.previewWebdavFile(entry); + }; + } + // Size var sizeSpan = document.createElement('span'); - sizeSpan.style.cssText = 'font-size:0.8em; color:var(--text-muted);'; + sizeSpan.style.cssText = 'font-size:0.75em;color:var(--text-muted);flex-shrink:0;min-width:60px;text-align:right;'; if (entry.type === 'file') { sizeSpan.textContent = self._formatSize(entry.size); } - item.appendChild(nameSpan); - item.appendChild(sizeSpan); + // Context menu on right-click + row.oncontextmenu = function(e) { + e.preventDefault(); + self._showContextMenu(e, entry, path); + }; - if (entry.type === 'dir') { - item.onclick = function() { self.openWebdavBrowser(entry.href); }; - } else { - item.onclick = function() { self.previewWebdavFile(entry); }; - // Right-click or context menu could download — but for simplicity, long tap or regular click previews - // Double-click to download - item.ondblclick = function() { self.downloadFromWebdav(entry); }; - } - frag.appendChild(item); + row.appendChild(iconSpan); + row.appendChild(nameSpan); + row.appendChild(sizeSpan); + listContainer.appendChild(row); }); - body.appendChild(frag); + body.appendChild(listContainer); + + // Keyboard navigation + body.tabIndex = -1; + body.style.outline = 'none'; + var focusIdx = 0; + body.onkeydown = function(e) { + var rows = listContainer.querySelectorAll('.wdav-row'); + if (e.key === 'ArrowDown' || e.key === 'j') { + e.preventDefault(); + focusIdx = Math.min(focusIdx + 1, rows.length - 1); + rows[focusIdx].scrollIntoView({ block: 'nearest' }); + rows.forEach(function(r, i) { r.style.borderLeft = i === focusIdx ? '2px solid var(--success-color)' : ''; }); + } else if (e.key === 'ArrowUp' || e.key === 'k') { + e.preventDefault(); + focusIdx = Math.max(focusIdx - 1, 0); + rows[focusIdx].scrollIntoView({ block: 'nearest' }); + rows.forEach(function(r, i) { r.style.borderLeft = i === focusIdx ? '2px solid var(--success-color)' : ''; }); + } else if (e.key === ' ') { + e.preventDefault(); + var cb = rows[focusIdx]?.querySelector('.wdav-checkbox'); + if (cb) { cb.checked = !cb.checked; cb.onchange({ stopPropagation: function(){} }); } + } else if (e.key === 'Enter') { + e.preventDefault(); + rows[focusIdx]?.click(); + } + }; + setTimeout(function() { body.focus(); }, 100); } content.appendChild(body); + // Bulk action bar + const bulkBar = document.createElement('div'); + bulkBar.id = 'wdav-bulk-bar'; + bulkBar.style.cssText = 'display:none;padding:8px 10px;border-top:1px solid var(--success-color);background:var(--bg-tertiary);gap:6px;align-items:center;flex-wrap:wrap;'; + bulkBar.innerHTML = '0 selected' + + '' + + '' + + ''; + self._updateSelectionCount = function() { + var count = Object.values(self._selected).filter(Boolean).length; + var el = document.getElementById('sel-count'); + if (el) el.textContent = count > 0 ? count + ' selected' : ''; + var bar = document.getElementById('wdav-bulk-bar'); + var cnt = document.getElementById('bulk-count'); + if (bar) bar.style.display = count > 0 ? 'flex' : 'none'; + if (cnt) cnt.textContent = count + ' selected'; + }; + + // Wire bulk actions + setTimeout(function() { + document.getElementById('bulk-delete')?.addEventListener('click', function() { + var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; }); + if (!names.length || !confirm('Delete ' + names.length + ' item(s) from ' + path + '?')) return; + names.forEach(function(name) { + var fullPath = (path.replace(/\/+$/, '') || '') + '/' + name; + self._deleteFile(fullPath).then(function(ok) { + if (ok) self.terminal.print('Deleted: ' + name, 'success'); + else self.terminal.print('Delete failed: ' + name, 'error'); + }); + }); + setTimeout(function() { self.openWebdavBrowser(path); }, 500); + }); + document.getElementById('bulk-move')?.addEventListener('click', function() { + var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; }); + if (!names.length) return; + var target = prompt('Move ' + names.length + ' item(s) to directory:', '/'); + if (!target) return; + target = target.replace(/\/+$/, ''); + names.forEach(function(name) { + var oldFull = (path.replace(/\/+$/, '') || '') + '/' + name; + var newFull = target + '/' + name; + self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } }) + .then(function(r) { + if (r.ok || r.status === 201 || r.status === 204) self.terminal.print('Moved: ' + name, 'success'); + else self.terminal.print('Move failed: ' + name + ' (HTTP ' + r.status + ')', 'error'); + }); + }); + setTimeout(function() { self.openWebdavBrowser(path); }, 500); + }); + document.getElementById('bulk-download')?.addEventListener('click', function() { + var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; }); + names.forEach(function(name) { + var fullPath = (path.replace(/\/+$/, '') || '') + '/' + name; + // Find entry from entries + var entry = entries.find(function(e) { return e.name === name; }); + if (entry && entry.type === 'file') self.downloadFromWebdav(entry); + }); + }); + }, 50); + + content.appendChild(bulkBar); + // Footer with breadcrumb + buttons const footer = document.createElement('div'); footer.className = 'modal-footer'; @@ -815,17 +1069,30 @@ class WebDAVModule { for (var i = 0; i < parts.length; i++) { cumulative += '/' + parts[i]; var seg = parts[i]; + breadHtml += ' ▸ '; breadHtml += '' + seg + ''; - if (i < parts.length - 1) breadHtml += ' ▸ '; } breadDiv.innerHTML = breadHtml; - // Attach nav function to modal element for breadcrumb clicks overlay.__nav = function(p) { self.openWebdavBrowser(p); }; // Button group var btnGroup = document.createElement('div'); btnGroup.style.cssText = 'display:flex; gap:4px; flex-shrink:0;'; + var mkdirBtn = document.createElement('button'); + mkdirBtn.className = 'btn'; + mkdirBtn.textContent = '📁+'; + mkdirBtn.title = 'New Directory'; + mkdirBtn.onclick = function() { + var name = prompt('Directory name:'); + if (name) { + self._ensureDir((path.replace(/\/+$/, '') || '') + '/' + name).then(function(ok) { + if (ok) { self.terminal.print('Created: ' + name, 'success'); self.openWebdavBrowser(path); } + else self.terminal.print('Failed to create directory', 'error'); + }); + } + }; + var uploadBtn = document.createElement('button'); uploadBtn.className = 'btn'; uploadBtn.textContent = '⬆ Upload'; @@ -842,6 +1109,7 @@ class WebDAVModule { closeBtn.textContent = 'Close'; closeBtn.onclick = function() { self.closeWebdavBrowser(); }; + btnGroup.appendChild(mkdirBtn); btnGroup.appendChild(uploadBtn); btnGroup.appendChild(refreshBtn); btnGroup.appendChild(closeBtn); @@ -853,7 +1121,71 @@ class WebDAVModule { overlay.appendChild(content); document.body.appendChild(overlay); - this.terminal.print('📂 Opened WebDAV browser: ' + path, 'info'); + this.terminal.print('📂 Opened WebDAV file browser: ' + path, 'info'); + } + + _showContextMenu(e, entry, currentPath) { + var existing = document.getElementById('wdav-context-menu'); + if (existing) existing.remove(); + + var menu = document.createElement('div'); + menu.id = 'wdav-context-menu'; + menu.style.cssText = 'position:fixed;left:' + e.clientX + 'px;top:' + e.clientY + 'px;background:#111;border:1px solid #333;border-radius:6px;padding:4px 0;z-index:1001;min-width:160px;box-shadow:0 4px 12px rgba(0,0,0,0.5);'; + menu.onclick = function() { this.remove(); }; + + var items = []; + if (entry.type === 'file') { + items.push({ label: '📄 Preview', fn: function() { this.previewWebdavFile(entry); }.bind(this) }); + items.push({ label: '⬇ Download', fn: function() { this.downloadFromWebdav(entry); }.bind(this) }); + items.push({ label: '📋 Copy Path', fn: function() { + var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name; + navigator.clipboard.writeText(full).then(function() { this.terminal.print('Path copied: ' + full, 'success'); }.bind(this)); + }.bind(this) }); + } else { + items.push({ label: '📂 Open', fn: function() { this.openWebdavBrowser(entry.href); }.bind(this) }); + items.push({ label: '📋 Copy Path', fn: function() { + var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name + '/'; + navigator.clipboard.writeText(full).then(function() { this.terminal.print('Path copied: ' + full, 'success'); }.bind(this)); + }.bind(this) }); + } + items.push({ label: '✏️ Rename', fn: function() { + var newName = prompt('Rename "' + entry.name + '" to:'); + if (newName && newName !== entry.name) { + var oldFull = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name; + var newFull = (currentPath.replace(/\/+$/, '') || '') + '/' + newName; + this._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } }).then(function(r) { + if (r.ok || r.status === 201 || r.status === 204) { + this.terminal.print('Renamed: ' + entry.name + ' → ' + newName, 'success'); + this.openWebdavBrowser(currentPath); + } else { this.terminal.print('Rename failed: HTTP ' + r.status, 'error'); } + }.bind(this)); + } + }.bind(this) }); + items.push({ label: '🗑 Delete', fn: function() { + if (!confirm('Delete ' + entry.name + '?')) return; + var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name; + this._deleteFile(full).then(function(ok) { + if (ok) { this.terminal.print('Deleted: ' + entry.name, 'success'); this.openWebdavBrowser(currentPath); } + else { this.terminal.print('Delete failed', 'error'); } + }.bind(this)); + }.bind(this) }); + + items.forEach(function(item) { + var div = document.createElement('div'); + div.textContent = item.label; + div.style.cssText = 'padding:6px 14px;cursor:pointer;font-size:0.85em;'; + div.onmouseover = function() { this.style.background = '#222'; }; + div.onmouseout = function() { this.style.background = ''; }; + div.onclick = function(e) { e.stopPropagation(); menu.remove(); item.fn(); }; + menu.appendChild(div); + }); + + document.body.appendChild(menu); + // Close on click outside + var closeMenu = function(ev) { + if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', closeMenu); } + }; + setTimeout(function() { document.addEventListener('click', closeMenu); }, 10); } closeWebdavBrowser() { @@ -949,7 +1281,10 @@ class WebDAVModule { async downloadFromWebdav(entry) { try { - var resp = await this._webdavRequest('GET', entry.href); + // Construct path from current browsing path + entry name (avoid autoindex href fragility) + var basePath = (this._browsingPath || '/').replace(/\/+$/, ''); + var fullPath = basePath + '/' + entry.name; + var resp = await this._webdavRequest('GET', fullPath); if (!resp.ok) { this.terminal.print('Download failed: HTTP ' + resp.status, 'error'); return; } var content = await resp.text(); var blob = new Blob([content], { type: 'application/octet-stream' }); @@ -957,7 +1292,7 @@ class WebDAVModule { var a = document.createElement('a'); a.href = url; a.download = entry.name; - a.style.display = 'none'; + a.style.cssText = 'display:none;'; document.body.appendChild(a); a.click(); setTimeout(function() { -- cgit v1.2.3