diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
| commit | ccc7d9956559d2195e20345e7a794662b52cd403 (patch) | |
| tree | 757354fbfec5c75b4577b2cb620d5ab1582e19b1 /modules/core/module-aiassist.js | |
| parent | b538577cd0a4292e4cd4ca1c9f9829fc7e133caa (diff) | |
auto: 2026-06-06_06:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 503 |
1 files changed, 439 insertions, 64 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index b93b12d..5a4341a 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -20,6 +20,16 @@ this._toolDefs = null; this._toolCallLog = []; this._debugPanelEnabled = false; + // š Guardrail Layer + this._guardrailPolicy = null; + this._shadowSnapshot = null; + this._guardrailViolations = {}; + this._confirmTimeoutMs = 120000; + this._skillDebounceMs = 1800000; + this._chatLockTimeoutMs = 10000; + this._loadGuardrailPolicy(); + // Restore AI config from localStorage (maxContext, MAX_TOOL_TURNS, etc.) + try { const cfg = localStorage.getItem('webx11_ai_config'); if (cfg) { const p = JSON.parse(cfg); if (p.maxTurns) this.MAX_TOOL_TURNS = p.maxTurns; if (p.maxContext) this.maxContext = p.maxContext; if (p.maxContextTokens) this.maxContextTokens = p.maxContextTokens; if (p.confirmTimeoutMs) this._confirmTimeoutMs = p.confirmTimeoutMs; if (p.skillDebounceMs) this._skillDebounceMs = p.skillDebounceMs; if (p.chatLockTimeoutMs) this._chatLockTimeoutMs = p.chatLockTimeoutMs; } } catch (e) {} try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {} try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {} // Restore AI conversation context from localStorage @@ -455,11 +465,11 @@ RULES: required: [] } } - }, { +}, { type: 'function', function: { name: 'smart_context_set', - description: 'Set the active AI context profile. Changes the AI\'s behavior (temperature, style) for subsequent responses.', + description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.", parameters: { type: 'object', properties: { @@ -468,6 +478,20 @@ RULES: required: ['name'] } } + }, { + type: 'function', + function: { + name: 'create_mindmap', + description: 'Create a mindmap with the given topic and child nodes, then render it as inline SVG in the terminal. Creates root and all children in one call. Use this when the user asks for a mindmap or visual architecture.', + parameters: { + type: 'object', + properties: { + topic: { type: 'string', description: 'Root topic of the mindmap (e.g. "WebX11 Architecture")' }, + children: { type: 'string', description: 'Comma-separated list of child node names (e.g. "AI Engine, VFS System, Modules")' } + }, + required: ['topic'] + } + } }]; } getHelpInfo() { @@ -481,7 +505,8 @@ RULES: 'ai-deny': 'ai-deny ā Deny pending AI action', 'ai-ignore': 'ai-ignore ā Suppress validator warnings for this session', 'ai-stop': 'ai-stop ā Cancel current AI operation', - 'ai-model': 'ai-model [model] ā Show or set AI model' + 'ai-model': 'ai-model [model] ā Show or set AI model', + 'ai-config': 'ai-config [key] [value] ā View or set AI loop config (max-turns, max-context)' }; } registerCommands() { @@ -496,7 +521,14 @@ RULES: this.commands['ai-stop'] = () => this.cancelAi(); this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); + this.commands['ai-config'] = (args) => this.aiConfig(args); this.commands['debug'] = () => this.toggleDebug(); + this.commands['guardrail'] = (args) => this.guardrailCmd(args); + this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]); + this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]); + this.commands['shadow'] = (args) => this.shadowExec(args); + this.commands['shadow-commit'] = () => this.shadowCommit(); + this.commands['shadow-rollback'] = () => this.shadowRollback(); } async chat(args) { const userMessage = args.join(' '); @@ -507,7 +539,7 @@ RULES: this.terminal.setAiBusy(true); const lockStart = Date.now(); while (this._chatLock) { - if (Date.now() - lockStart > 10000) { + if (Date.now() - lockStart > this._chatLockTimeoutMs) { this._chatLock = false; this.terminal.print('ā ļø Chat lock timed out ā force released', 'warning'); break; @@ -528,13 +560,6 @@ RULES: this.context.slice().forEach(msg => messages.push(msg)); messages.push({ role: 'user', content: userMessage }); this.terminal.print('š¤ WebX11AI thinking...', 'info'); - // Ensure skills directory exists and seed defaults on first run - this.terminal.fs.mkdirRecursive('/skills'); - if (!this.terminal.fs.readFile('/skills/_seeded')) { - try { - await this._seedDefaultSkills(); - } catch (e) { /* non-critical */ } - } // Auto-load skills relevant to the user's query (awaited, not fire-and-forget) // Skip skill loading for trivial queries (< 10 chars, no actionable keywords) const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false; @@ -775,6 +800,17 @@ RULES: })); for (const { fn, toolResult, t1 } of safeResults) { this.terminal.print(`š§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); + // Memory graph: record tool call + file access + try { + const _mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + if (_mg) { + _mg.recordToolCall(fn.name, !this._isToolError(toolResult)); + if (fn.name === 'read_file' || fn.name === 'write_file' || fn.name === 'view_module' || fn.name === 'edit_module') { + const _path = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); + if (_path) _mg.recordFileAccess(_path); + } + } + } catch (_e) { /* memory graph not available */ } const obs = this._summarizeObservation(toolResult, 150); const err = this._isToolError(toolResult); if (err) { @@ -793,7 +829,20 @@ RULES: } // Only block on destructive tools if (destructiveTools.length > 0) { - const approved = await this.confirmActionMulti(destructiveTools); + // šÆ CONFIDENCE ESCALATION ā auto-approve only if confidence >= 90% + let autoApprove = false; + if (pendingText) { + const confidence = this._parseConfidence(pendingText); + if (confidence !== null && confidence >= 90) { + autoApprove = true; + this.terminal.print(`šÆ AI confidence: ${confidence}% ā auto-approved`, 'success'); + } else if (confidence !== null) { + this.terminal.print(`šÆ AI confidence: ${confidence}% ā below 90% threshold, requiring manual approval`, 'warning'); + } else { + this.terminal.print('šÆ No confidence declared for destructive action ā requiring manual approval', 'warning'); + } + } + const approved = autoApprove || await this.confirmActionMulti(destructiveTools); if (approved) { for (const tc of destructiveTools) { const fn = tc.function; @@ -813,6 +862,17 @@ RULES: const sm2 = this.terminal.moduleManager.modules['ServerMonitorModule']; if (sm2 && sm2.logToolCall) sm2.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult)); if (this._debugPanelEnabled) this._updateDebugPanel(); + // Memory graph: record destructive tool call + try { + const _mg2 = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + if (_mg2) { + _mg2.recordToolCall(fn.name, !this._isToolError(toolResult)); + if (fn.name === 'write_file' || fn.name === 'edit_module') { + const _path2 = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); + if (_path2) _mg2.recordFileAccess(_path2); + } + } + } catch (_e2) { /* memory graph not available */ } const dObs = this._summarizeObservation(toolResult, 150); const dErr = this._isToolError(toolResult); if (dErr) { @@ -865,7 +925,7 @@ RULES: async afterActionReflection(query, toolTurns) { // Debounce: only suggest skill creation once per 30 minutes const now = Date.now(); - if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < 1800000) { + if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < this._skillDebounceMs) { return; } const suggestions = []; @@ -955,7 +1015,7 @@ RULES: // Countdown timer updates with escalating visibility const timerInterval = setInterval(() => { const elapsed = Math.round((Date.now() - startTime) / 1000); - const remaining = Math.max(0, 120 - elapsed); + const remaining = Math.max(0, (this._confirmTimeoutMs / 1000) - elapsed); const timerEl = document.getElementById('ai-confirm-timer'); if (timerEl) { timerEl.textContent = `ā± ${remaining}s`; @@ -984,7 +1044,7 @@ RULES: this._cleanupConfirmBar(); this.terminal.print('ā± Confirmation timed out ā tool not executed', 'warning'); } - }, 120000); + }, this._confirmTimeoutMs); }); } _resolveConfirm(approved) { @@ -1065,12 +1125,288 @@ RULES: this.terminal.print('š§ Debug panel disabled', 'info'); } } + // š === GUARDRAIL LAYER === + _loadGuardrailPolicy() { + try { + const raw = localStorage.getItem('webx11_guardrail_policy'); + if (raw) { + this._guardrailPolicy = JSON.parse(raw); + } else { + // Default policy ā seed on first run + this._guardrailPolicy = { + version: 1, + rules: [ + { + id: 'rm-rf-block', + pattern: '/rm\\s+-rf/', + description: 'Prevent recursive force deletes outside trash', + severity: 'deny', + tools: ['run_command', 'write_file'] + }, + { + id: 'curl-pipe-bash', + pattern: '/(curl|wget)\\s+.*\\|\\s*(bash|sh)/', + description: 'Block curl-to-bash execution', + severity: 'deny', + tools: ['run_command'] + }, + { + id: 'rm-outer-vfs', + pattern: '/delete_file.*\\.(user\\.json|token|session|key)/', + description: 'Block deletion of sensitive metadata files', + severity: 'deny', + tools: ['delete_file'] + }, + { + id: 'mass-delete', + pattern: '/rm\\s+-rf\\s+\\/|delete_file.*\\*|\\.*\\/\\*/', + description: 'Block mass-delete operations', + severity: 'deny', + tools: ['run_command', 'delete_file'] + }, + { + id: 'chmod-777', + pattern: '/chmod\\s+777/', + description: 'Block world-writable permissions', + severity: 'deny', + tools: ['run_command'] + }, + { + id: 'external-dl', + pattern: '/(wget|curl)\\s+-[oO]\\s+/', + description: 'External file download ā requires high confidence', + severity: 'warn', + tools: ['run_command'] + }, + { + id: 'env-overwrite', + pattern: '/export\\s+\\w+\\s*=\\s*/', + description: 'Modifying environment variables', + severity: 'warn', + tools: ['run_command'] + }, + { + id: 'eval-danger', + pattern: '/(eval|exec|system)\\s*\\(/', + description: 'Dynamic code execution', + severity: 'deny', + tools: ['run_command'] + } + ], + descriptions: {} + }; + localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); + } + } catch (e) { + this._guardrailPolicy = { version: 1, rules: [] }; + } + } + _saveGuardrailPolicy() { + try { + localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); + } catch (e) {} + } + checkGuardrail(toolName, args) { + if (!this._guardrailPolicy || !this._guardrailPolicy.rules) return null; + const argsStr = typeof args === 'object' ? JSON.stringify(args) : String(args); + for (const rule of this._guardrailPolicy.rules) { + if (!rule.tools.includes(toolName)) continue; + try { + const re = new RegExp(rule.pattern.slice(1, -1), 'i'); + if (re.test(argsStr)) { + this._guardrailViolations[rule.id] = (this._guardrailViolations[rule.id] || 0) + 1; + return rule; // { id, pattern, description, severity, tools } + } + } catch (e) { /* skip malformed regex */ } + } + return null; + } + // šÆ === CONFIDENCE ESCALATION === + _parseConfidence(text) { + if (!text || typeof text !== 'string') return null; + // Look for patterns like "Confidence: 85%" or "confidence=95%" or "I am 72% confident" + const patterns = [ + /confidence[:\s]*(\d+)%/i, + /confidence[=:]?\s*(\d+)\s*%/i, + /(\d+)%\s*confiden/i, + /confiden[ts]\s+(\d+)%/i + ]; + for (const re of patterns) { + const m = text.match(re); + if (m) return parseInt(m[1], 10); + } + return null; + } + // šø === SHADOW VFS (Sandboxed Execution) === + _snapshotShadowVFS() { + // Deep-clone the VFS root state into a snapshot + try { + const root = this.terminal.fs.root; + this._shadowSnapshot = JSON.parse(JSON.stringify(root)); + return true; + } catch (e) { + this.terminal.print('ā ļø Shadow VFS snapshot failed: ' + e.message, 'error'); + return false; + } + } + _diffShadowVFS(consume = true) { + if (!this._shadowSnapshot) return null; + try { + const current = this.terminal.fs.root; + const changes = []; + this._walkDiff(this._shadowSnapshot, current, '', changes); + if (consume) { + this._shadowSnapshot = null; + } + return changes; + } catch (e) { + this._shadowSnapshot = null; + return [{ type: 'diff-error', message: e.message }]; + } + } + _walkDiff(snapshot, live, path, changes) { + const snapKeys = snapshot && snapshot.content ? Object.keys(snapshot.content) : []; + const liveKeys = live && live.content ? Object.keys(live.content) : []; + // New files in live + for (const key of liveKeys) { + const livePath = path ? path + '/' + key : key; + const liveEntry = live.content[key]; + const snapEntry = snapshot.content ? snapshot.content[key] : undefined; + if (!snapEntry) { + const entrySize = liveEntry.type === 'file' ? (liveEntry.content || '').length : 0; + changes.push({ type: 'created', path: livePath, entryType: liveEntry.type, size: entrySize }); + } else if (liveEntry.type === 'file' && liveEntry.content !== snapEntry.content) { + const oldLen = (snapEntry.content || '').length; + const newLen = (liveEntry.content || '').length; + changes.push({ type: 'modified', path: livePath, sizeBefore: oldLen, sizeAfter: newLen }); + } + // Recurse into directories + if (liveEntry.type === 'dir' && snapEntry && snapEntry.type === 'dir') { + this._walkDiff(snapEntry, liveEntry, livePath, changes); + } + } + // Deleted files + for (const key of snapKeys) { + if (!liveKeys.includes(key)) { + changes.push({ type: 'deleted', path: path ? path + '/' + key : key }); + } + } + } + // š === GUARDRAIL COMMANDS === + guardrailCmd(args) { + if (args.length === 0) { + this.terminal.print('=== Guardrail Policy ===', 'info'); + if (!this._guardrailPolicy) { + this.terminal.print('No guardrail policy loaded', 'warning'); + return; + } + this.terminal.print(`Rules: ${this._guardrailPolicy.rules.length}`, 'info'); + for (const rule of this._guardrailPolicy.rules) { + const icon = rule.severity === 'deny' ? 'š“' : 'š”'; + this.terminal.print(`${icon} ${rule.id}: ${rule.description}`, 'info'); + } + const violations = Object.values(this._guardrailViolations).reduce((a, b) => a + b, 0); + if (violations > 0) { + this.terminal.print(`ā ļø ${violations} violation(s) this session`, 'warning'); + } + this.terminal.print('Usage: guardrail-add <id> <severity> <pattern> <tools...> | guardrail-remove <id>', 'info'); + return; + } + const sub = args[0].toLowerCase(); + if (sub === 'add' && args.length >= 4) { + const id = args[1]; + const severity = args[2]; + const pattern = args[3]; + const tools = args.slice(4); + this._guardrailPolicy.rules.push({ id, severity, pattern, tools, description: 'Custom rule' }); + this._saveGuardrailPolicy(); + this.terminal.print(`ā
Guardrail rule '${id}' added`, 'success'); + } else if (sub === 'remove' && args.length >= 2) { + const idx = this._guardrailPolicy.rules.findIndex(r => r.id === args[1]); + if (idx >= 0) { + this._guardrailPolicy.rules.splice(idx, 1); + this._saveGuardrailPolicy(); + this.terminal.print(`ā
Guardrail rule '${args[1]}' removed`, 'success'); + } else { + this.terminal.print(`ā Rule '${args[1]}' not found`, 'error'); + } + } else { + this.terminal.print('Usage: guardrail-add <id> <severity> <pattern> <tools...> | guardrail-remove <id>', 'warning'); + } + } + // šÆ SHADOW COMMAND + shadowExec(args) { + // Usage: shadow <command> + // Snapshot VFS, run command, diff output + if (args.length === 0) { + this.terminal.print('Usage: shadow <command> ā Execute command in shadow VFS (sandbox preview)', 'warning'); + return; + } + const cmd = args.join(' '); + this._snapshotShadowVFS(); + const cmdName = cmd.split(' ')[0].toLowerCase(); + const cmdArgs = cmd.split(' ').slice(1); + const originalPrint = this.terminal.print.bind(this.terminal); + let output = ''; + this.terminal.print = (text, type) => { output += text + '\n'; }; + this.terminal.dispatchCommand(cmdName, cmdArgs).then(() => { + this.terminal.print = originalPrint; + const diff = this._diffShadowVFS(false); + if (!diff || diff.length === 0) { + this.terminal.print('š¦ Shadow: No filesystem changes detected.', 'success'); + } else { + this.terminal.print(`š¦ Shadow: ${diff.length} change(s) detected:`, 'info'); + for (const c of diff) { + const icon = c.type === 'created' ? 'ā' : c.type === 'modified' ? 'āļø' : 'šļø'; + const sizeStr = c.size !== undefined ? c.size + 'B' : ''; + const detail = c.type === 'created' ? `(${c.entryType}${sizeStr ? ', ' + sizeStr : ''})` : + c.type === 'modified' ? `(${c.sizeBefore}ā${c.sizeAfter}B)` : ''; + this.terminal.print(`${icon} ${c.path} ${detail}`, c.type === 'deleted' ? 'warning' : 'info'); + } + // Ask to commit or rollback + this.terminal.print('š VFS modified ā type "shadow-commit" to accept or "shadow-rollback" to undo.', 'warning'); + } + }).catch(e => { + this.terminal.print = originalPrint; + this.terminal.print('ā ļø Shadow command error: ' + e.message, 'error'); + }); + } + shadowCommit() { + this._shadowSnapshot = null; + this.terminal.print('ā
Shadow changes committed to VFS', 'success'); + } + shadowRollback() { + if (!this._shadowSnapshot) { + this.terminal.print('ā No shadow snapshot to rollback to', 'error'); + return; + } + try { + this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot)); + this.terminal.fs.saveToLocalStorage(); + this._shadowSnapshot = null; + this.terminal.print('ā©ļø VFS rolled back to pre-shadow state', 'success'); + } catch (e) { + this.terminal.print('ā ļø Rollback failed: ' + e.message, 'error'); + } + } + // š OVERRIDES for isDestructive ā adds runTool intercept isDestructive(name) { return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module'; } async runTool(name, argsStr) { let a = {}; try { + // š GUARDRAIL CHECK ā intercept before any tool executes + const guardrail = this.checkGuardrail(name, argsStr); + if (guardrail && guardrail.severity === 'deny') { + const errMsg = `š GUARDRAIL BLOCKED: ${guardrail.description} (rule: ${guardrail.id})`; + this.terminal.print(errMsg, 'error'); + this.terminal.log('WARN', 'GUARDRAIL', errMsg); + return JSON.stringify({ error: errMsg }); + } + if (guardrail && guardrail.severity === 'warn') { + this.terminal.print(`š” GUARDRAIL WARNING: ${guardrail.description} (rule: ${guardrail.id})`, 'warning'); + } a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'list_files': { @@ -1200,10 +1536,19 @@ RULES: case 'run_command': { const cmd = a.command || ''; if (!cmd) return JSON.stringify({ error: 'No command provided' }); + // šø Shadow VFS auto-snapshot before run_command + this._snapshotShadowVFS(); const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; try { await this.terminal.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } finally { this.terminal.print = originalPrint; } + // šø Diff and append shadow report + const shadowDiff = this._diffShadowVFS(); + if (shadowDiff && shadowDiff.length > 0) { + const diffSummary = shadowDiff.map(c => `${c.type}:${c.path}`).join(', '); + output += `\nš¦ [Shadow VFS: ${shadowDiff.length} change(s) ā ${diffSummary}]`; + this._shadowSnapshot = null; // Changes considered "committed" since AI explicitly ran command + } return output.trim() || '(no output)'; } case 'check_updates': { @@ -1486,6 +1831,27 @@ RULES: const profile = scm.contextProfiles[name]; return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature }); } + case 'create_mindmap': { + const mindmap = this.terminal.moduleManager.modules['MindmapModule']; + if (!mindmap) return JSON.stringify({ error: 'MindmapModule not loaded' }); + const topic = a.topic || 'Mindmap'; + const children = a.children || ''; + mindmap.root = null; + const args = topic.split(' '); + mindmap._new(args); + if (!mindmap.root) return JSON.stringify({ error: 'Failed to create root' }); + const rootId = mindmap.root.id; + const added = []; + if (children) { + const childNames = children.split(',').map(c => c.trim()).filter(Boolean); + for (const child of childNames) { + mindmap._add([rootId, child]); + added.push(child); + } + } + mindmap._svg([]); + return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(mindmap.root), rendered: 'inline SVG in terminal' }); + } case 'view_module': case 'edit_module': case 'test_module': @@ -1731,6 +2097,64 @@ RULES: this.terminal.print('No models available. Use ollamaconnect first.', 'warning'); } } + aiConfig(args) { + if (args.length === 0) { + this.terminal.print('=== AI Config ===', 'info'); + this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); + this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); + this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); + this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled !== false) ? 'on' : 'off'}`, ''); + this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs / 1000}s (default: 120s)`, ''); + this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); + this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); + this.terminal.print('Usage: ai-config <key> <value>', 'info'); + this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); + return; + } + const key = args[0]; + const val = args[1]; + if (key === 'max-turns') { + const n = parseInt(val); + if (isNaN(n) || n < 1 || n > 50) return this.terminal.print('max-turns must be 1-50', 'warning'); + this.MAX_TOOL_TURNS = n; + } else if (key === 'max-context') { + const n = parseInt(val); + if (isNaN(n) || n < 1 || n > 100) return this.terminal.print('max-context must be 1-100', 'warning'); + this.maxContext = n; + } else if (key === 'max-context-tokens') { + const n = parseInt(val); + if (isNaN(n) || n < 100 || n > 10000) return this.terminal.print('max-context-tokens must be 100-10000', 'warning'); + this.maxContextTokens = n; + } else if (key === 'confirm-timeout') { + const n = parseInt(val); + if (isNaN(n) || n < 5 || n > 600) return this.terminal.print('confirm-timeout must be 5-600 seconds', 'warning'); + this._confirmTimeoutMs = n * 1000; + } else if (key === 'chat-lock-timeout') { + const n = parseInt(val); + if (isNaN(n) || n < 1 || n > 60) return this.terminal.print('chat-lock-timeout must be 1-60 seconds', 'warning'); + this._chatLockTimeoutMs = n * 1000; + } else if (key === 'skill-debounce') { + const n = parseInt(val); + if (isNaN(n) || n < 1 || n > 240) return this.terminal.print('skill-debounce must be 1-240 minutes', 'warning'); + this._skillDebounceMs = n * 60000; + } else if (key === 'trivial-optimization') { + if (this.terminal?.ollamaManager) { + this.terminal.ollamaManager.optimizationEnabled = val === 'on'; + } + } else { + return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization`, 'warning'); + } + // Persist + localStorage.setItem('webx11_ai_config', JSON.stringify({ + maxTurns: this.MAX_TOOL_TURNS, + maxContext: this.maxContext, + maxContextTokens: this.maxContextTokens, + confirmTimeoutMs: this._confirmTimeoutMs, + chatLockTimeoutMs: this._chatLockTimeoutMs, + skillDebounceMs: this._skillDebounceMs + })); + this.terminal.print(`ai-config ${key} set to ${val}`, 'success'); + } showContext() { if (this.context.length === 0) { this.terminal.print('No conversation context', 'info'); return; @@ -1767,55 +2191,6 @@ RULES: try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} } } - async _seedDefaultSkills() { - const fs = this.terminal.fs; - const mindmapSkill = `--- -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 ---- -## Steps - -1. **Create a new mindmap:** - - \`mindmap new "<topic>" -t tag1,tag2\` ā creates a root node - - Tags are optional (e.g. planning, research, dev) - - Root gets auto-assigned a color - -2. **Add child nodes:** - - \`mindmap add <parent-id> "<idea>" -t tag\` ā add a child - - Get the parent-id from \`mindmap show\` - - IDs are 4-char random strings (e.g. a3f8, x7k2) - -3. **View the mindmap:** - - \`mindmap show\` ā full tree with Unicode box-drawing - - \`mindmap show <id>\` ā subtree starting at that node - -4. **Edit nodes:** - - \`mindmap rename <id> "<new text>"\` ā change text - - \`mindmap color <id> <color>\` ā set color (red/green/yellow/blue/cyan/magenta/white) - - \`mindmap tag <id> <tag>\` ā add a tag - -5. **Persistence:** - - \`mindmap save <name>\` ā save to localStorage - - \`mindmap load <name>\` ā restore - - \`mindmap list\` ā show saved mindmaps - - \`mindmap delete <name>\` ā remove saved - -6. **Export:** - - \`mindmap export [id]\` ā Markdown bullet list output - -## When to use -- User wants to brainstorm, organize ideas, plan features -- User says "map out", "diagram", "brainstorm", "organize thoughts" -- Visualizing project structure or task decomposition`; - - 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.writeFile('/skills/_seeded', '1'); - } _trackPattern(userMessage) { const lower = userMessage.toLowerCase(); const patterns = ['font size','bigger text','smaller text','export','backup','save','install','load module','search','find','color','theme','skill','learn','remember','mindmap','brainstorm']; |