', 'warning'); return;
}
this._cancelled = false;
this.terminal.setAiBusy(true);
const lockStart = Date.now();
while (this._chatLock) {
if (Date.now() - lockStart > this._chatLockTimeoutMs) {
this._chatLock = false;
this.terminal.print('ā ļø Chat lock timed out ā force released', 'warning');
break;
}
await new Promise(r => setTimeout(r, 50));
}
this._chatLock = true;
this.terminal.markDirty();
// Re-read context from localStorage (auto-restore may have written it after init)
this._loadContext();
// Compress context if it exceeds the token threshold
this._compressContext();
let loopResult;
try {
const messages = [
{ role: 'system', content: this.systemPrompt }
];
this.context.slice().forEach(msg => messages.push(msg));
messages.push({ role: 'user', content: userMessage });
this.terminal.print('š¤ WebX11AI thinking...', 'info');
// 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;
const isTrivial = optEnabled && userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage);
if (!isTrivial) {
try {
const skillRes = await this.runTool('load_skills', { query: userMessage });
const skillData = JSON.parse(skillRes);
if (skillData && skillData.matches && skillData.matches.length > 0) {
const skillNotes = skillData.matches.map(m => `[Skill: ${m.name}] ${m.content}`).join('\n\n---\n\n');
messages.push({ role: 'system', content: `Relevant skills for this task:\n\n${skillNotes}\n\nUse the steps above as guidance. If they're wrong or outdated, use patch_skill to fix them after the task.` });
this.terminal.print(`š Loaded ${skillData.count} relevant skill(s)`, 'info');
}
} catch (e) { /* skills not available yet ā first run */ }
}
// Track conversation patternsfor learning
this._trackPattern(userMessage);
// Extract facts from user message
this._extractFacts(userMessage, '');
// On first message of session, auto-load recent memory
if (!this._sessionMemoryLoaded) {
this._sessionMemoryLoaded = true;
this.terminal.fs.mkdirRecursive('/memory');
try {
const recentRes = await this.runTool('session_recent', { count: 3 });
const rd = JSON.parse(recentRes);
if (rd.sessions && rd.sessions.length > 0) {
const memNotes = rd.sessions.map(s => '[' + s.date + '] ' + (s.tasks || 1) + ' tasks: ' + (s.summary || 'No summary')).join('\n');
messages.push({ role: 'system', content: 'Recent sessions:\n' + memNotes + '\nThis provides continuity. Refer to past work when relevant.' });
this.terminal.print('š Loaded ' + rd.sessions.length + ' recent session(s)', 'info');
}
} catch (e) {}
}
// Inject memory graph context for cross-session awareness
try {
const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule'];
if (memgraph) {
const mctx = memgraph.getContextForPrompt();
if (mctx) {
messages.push({ role: 'system', content: mctx });
}
}
} catch (e) { /* memory graph not available */ }
// Inject world model context for system self-awareness
try {
const wm = this.terminal.moduleManager.modules['WorldModelModule'];
if (wm) {
const wctx = wm.getContextForPrompt();
if (wctx) {
messages.push({ role: 'system', content: wctx });
}
}
} catch (e) { /* world model not available */ }
loopResult = await this.agenticLoop(messages);
this.context.push({ role: 'user', content: userMessage });
if (loopResult.finalAnswer) {
this.context.push({ role: 'assistant', content: loopResult.finalAnswer });
}
if (this.context.length > this.maxContext * 2) {
this.context = this.context.slice(-this.maxContext * 2);
}
// Reflection: after complex tasks, run health scan and auto-skill creation
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
// Auto-create skills from repeated errors
if (this._errorLog.length >= 3) {
await this._autoCreateErrorSkill();
}
// Skill quality audit every 20 messages
this._messageCount++;
if (this._messageCount % 20 === 0) {
await this._auditSkills();
}
// Sound notification on AI response
if (this.terminal.playNotificationSound) {
setTimeout(() => this.terminal.playNotificationSound(), 100);
}
} finally {
this._chatLock = false;
this.terminal.setAiBusy(false);
// Persist conversation context after each exchange
this._saveContext();
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)');
}
}
// Register session save on page unload (runs once per session)
if (!this._sessionSaved) {
this._sessionSaved = true;
window.addEventListener('beforeunload', () => {
this.terminal.fs.mkdirRecursive('/memory');
const today = new Date().toISOString().split('T')[0];
const index = this.terminal.fs.fsReadJSON('/memory/index.json') || {};
index[today] = index[today] || { summary: 'Session in progress', tasks: 0 };
index[today].tasks = (index[today].tasks || 0) + 1;
index[today].updated = new Date().toISOString();
this.terminal.fs.fsWriteJSON('/memory/index.json', index);
// Also persist AI context on page close
this._saveContext();
});
}
}
_summarizeObservation(result, maxLen) {
if (!result || typeof result !== 'string' || result.length <= maxLen) return result || '';
const lines = result.split('\n');
if (lines.length > 5) {
return lines.slice(0, 2).join('\n') + '\n...(' + (lines.length - 4) + ' more lines)...\n' + lines.slice(-2).join('\n');
}
return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)';
}
_escapeHtml(str) {
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
_updateDebugPanel() {
if (!this._debugPanelEnabled) return;
const panel = document.getElementById('debug-panel');
if (!panel) return;
const recent = this._toolCallLog.slice(-20);
let html = '';
for (const entry of recent) {
const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok';
const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60);
html += '' +
'#' + entry.turn + ' ' +
'' + this._escapeHtml(entry.name) + ' ' +
'' + this._escapeHtml(argsStr) + ' ' +
'' + entry.duration + 's' +
(entry.error ? ' ā ļø' : '') +
'
';
}
panel.innerHTML = '' + html;
}
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
const trimmed = result.trim();
if (trimmed.startsWith('{"error":') || trimmed.startsWith("{'error':")) {
try { const parsed = JSON.parse(trimmed); return parsed.error || null; }
catch (e) { return null; }
}
return null;
}
_trackToolFailure(toolName, error) {
try {
if (!this._toolFailuresBuffer) this._toolFailuresBuffer = {};
if (!this._toolFailuresBuffer[toolName]) this._toolFailuresBuffer[toolName] = { count: 0, last: '', errors: [] };
const entry = this._toolFailuresBuffer[toolName];
entry.count++;
entry.last = new Date().toISOString();
entry.errors.push(error);
if (entry.errors.length > 5) entry.errors.shift();
if (entry.count >= 3) {
this.terminal.print(`ā ļø Tool '${toolName}' has failed ${entry.count} times. Consider creating a workaround skill.`, 'warning');
}
if (this._toolFailureFlushTimer) clearTimeout(this._toolFailureFlushTimer);
this._toolFailureFlushTimer = setTimeout(() => this._flushToolFailures(), 2000);
} catch (e) { /* ignore */ }
}
_flushToolFailures() {
try {
if (!this._toolFailuresBuffer || Object.keys(this._toolFailuresBuffer).length === 0) return;
const key = 'webx11_tool_failures';
const raw = localStorage.getItem(key) || '{}';
const map = JSON.parse(raw);
for (const [toolName, entry] of Object.entries(this._toolFailuresBuffer)) {
if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] };
map[toolName].count += entry.count;
map[toolName].last = entry.last;
map[toolName].errors.push(...entry.errors);
if (map[toolName].errors.length > 10) map[toolName].errors = map[toolName].errors.slice(-10);
}
localStorage.setItem(key, JSON.stringify(map));
this._toolFailuresBuffer = {};
} catch (e) { /* ignore */ }
}
async agenticLoop(messages) {
let toolTurns = 0;
while (toolTurns < this.MAX_TOOL_TURNS) {
if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; }
toolTurns++;
// On last turn, inject wrap-up prompt
if (toolTurns === this.MAX_TOOL_TURNS) {
this.terminal.print(`ā± Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) ā AI must provide final answer`, 'warning');
messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' });
}
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`ā ļø AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
const text = res.message.content || '';
this.terminal.printAIResponse(text || '(no response)');
if (text) this.terminal.ollamaManager.fireResponseHooks(text);
this.terminal.print(`š¬ AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');
return { finalAnswer: text, toolTurns };
}
if (res.tool_calls && res.tool_calls.length > 0) {
this.terminal.print(`š§ AI wants ${res.tool_calls.length} tool(s) ā turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
messages.push(res.message);
let pendingText = res.message?.content || null;
const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments));
const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name, tc.function.arguments));
// Execute non-destructive tools in parallel (independent read-only operations)
const safeResults = await Promise.all(safeTools.map(async (tc) => {
const fn = tc.function;
const t0 = Date.now();
const toolResult = await this.runTool(fn.name, fn.arguments);
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
name: fn.name,
args: fn.arguments,
result: toolResult,
duration: t1,
error: this._isToolError(toolResult) ? true : false
});
// Telemetry
const sm = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
if (this._debugPanelEnabled) this._updateDebugPanel();
return { fn, toolResult, t1 };
}));
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) {
this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
this._trackToolFailure(fn.name, err);
this.terminal.print(`š Result (${t1}s): ā ļø Error: ${err}`, 'warning');
} else if (toolResult && toolResult.length > 150) {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
const safeId = 'toolresult-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
this.terminal.printHTML(`š Result (${t1}s): ${this._escapeHtml(obs)} [+] show full (${toolResult.length} chars)${this._escapeHtml(toolResult)} [-] collapse`);
} else {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`š Result (${t1}s): ${obs}`, 'info');
}
messages.push({ role: 'tool', content: toolResult });
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
// šÆ 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;
const t0 = Date.now();
this.terminal.print(`š§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
const toolResult = await this.runTool(fn.name, fn.arguments);
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
name: fn.name,
args: fn.arguments,
result: toolResult,
duration: t1,
error: this._isToolError(toolResult) ? true : false
});
// Telemetry
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) {
this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
this._trackToolFailure(fn.name, dErr);
this.terminal.print(`š Result (${t1}s): ā ļø Error: ${dErr}`, 'warning');
} else {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`š Result (${t1}s): ${dObs}`, 'info');
}
messages.push({ role: 'tool', content: toolResult });
}
} else {
messages.push({
role: 'tool',
content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied by user. Non-destructive actions already executed.`
});
}
}
// Validator: check safe tool results for consistency
if (safeResults.length > 0 && pendingText && !this._validatorSuppressed) {
const om = this.terminal.ollamaManager;
if (om && om.validatorEnabled && om.validatorModel) {
let invalidTools = [];
for (const { fn, toolResult } of safeResults) {
const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || '');
if (!valid && !this._cancelled) {
invalidTools.push(fn.name);
this.terminal.print('š Validator: ' + fn.name + ' result ā AI note may not match (logged)', 'info');
}
}
if (invalidTools.length > 0) {
pendingText += '\n\nā ļø **Validator flagged** ' + invalidTools.join(', ') + ' ā the AI analysis may not match what was returned. Use `ai-ignore` to dismiss and proceed.';
}
}
}
if (pendingText) {
messages.push({ role: 'assistant', content: pendingText });
}
continue;
}
return { finalAnswer: null, toolTurns };
}
// Loop exited without final answer ā auto-summary
const toolNames = Object.keys(this._commandFreq).slice(-3).join(', ');
this.terminal.print(`š¤ AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning');
this.terminal.print('Tools were executed but no final answer was produced ā try a more specific query.', 'info');
return { finalAnswer: null, toolTurns };
}
async afterActionReflection(query, toolTurns) {
// Debounce: only suggest skill creation once per 30 minutes
const now = Date.now();
if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < this._skillDebounceMs) {
return;
}
const suggestions = [];
const fs = this.terminal.fs;
try {
// Batch: read all status info in one VFS pass
const reg = this.terminal.moduleManager.moduleRegistry || [];
const total = reg.length;
const loaded = Object.keys(this.terminal.moduleManager.modules).length;
if (loaded < total) {
suggestions.push(loaded + '/' + total + ' modules loaded ā use loadmod to restore');
}
} catch (e) {}
try {
const skillIdx = fs.fsReadJSON('/skills/index.json');
const skillCount = skillIdx ? Object.keys(skillIdx).length : 0;
if (skillCount === 0 && toolTurns >= 4) {
suggestions.push('Save this approach as a skill with create_skill');
this._lastSkillSuggestion = now;
}
} catch (e) {}
try {
const modsDir = fs.getCurrentDir();
const mc = modsDir.content['_mods'];
const modCount = mc && mc.content ? Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')).length : 0;
if (modCount > 0) {
suggestions.push(modCount + ' pending module edit(s) in /_mods/ ā use test_module to verify');
}
} catch (e) {}
try {
if (performance.memory && performance.memory.usedJSHeapSize > 100 * 1048576) {
suggestions.push('High memory (' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + 'MB) ā consider clearing terminal');
}
} catch (e) {}
if (toolTurns >= 4 && suggestions.length === 0) {
suggestions.push('Used ' + toolTurns + ' tools ā create a skill to save this workflow');
this._lastSkillSuggestion = now;
}
if (suggestions.length > 0) {
this.terminal.print('š” ' + suggestions.join(' | '), 'info');
}
}
async confirmActionMulti(toolCalls) {
// No-confirm mode: auto-approve
if (this._confirmTimeoutMs === 0) {
this.terminal.print(`ā” No-confirm mode: auto-approving ${toolCalls.map(tc => tc.function.name).join(', ')}`, 'info');
return true;
}
const names = toolCalls.map(tc => tc.function.name).join(', ');
const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | ');
const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args;
const timeoutLabel = this._confirmTimeoutMs === 0
? ''
: ` (Auto-denies in ${this._confirmTimeoutMs / 1000}s)`;
this.terminal.printHTML(`ā ļø AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse.${timeoutLabel}`);
// Show confirm bar OUTSIDE scrollable output (always visible)
const area = document.getElementById('ai-confirm-area');
if (!area) {
// Fallback to promise resolve
return true;
}
area.innerHTML = `
ā ļø ${names}(${truncatedArgs})
Type: ai-allow or ai-deny
`;
area.classList.add('visible');
this._confirmArea = area;
this._confirmBar = area;
// Attach button handlers via both addEventListener + DOM property (inline onclick fallback)
const allowBtn = document.getElementById('ai-confirm-allow');
const denyBtn = document.getElementById('ai-confirm-deny');
const onAllow = () => {
this._resolveConfirm(true);
};
const onDeny = () => {
this._resolveConfirm(false);
};
allowBtn.addEventListener('click', onAllow);
denyBtn.addEventListener('click', onDeny);
allowBtn.__allow = onAllow;
denyBtn.__deny = onDeny;
const startTime = Date.now();
return new Promise((resolve) => {
this._pendingConfirmation = { names, args };
this._confirmResolve = resolve;
this._confirmAllowBtn = allowBtn;
this._confirmDenyBtn = denyBtn;
this._confirmOnAllow = onAllow;
this._confirmOnDeny = onDeny;
// Countdown timer updates with escalating visibility
const timerInterval = setInterval(() => {
const elapsed = Math.round((Date.now() - startTime) / 1000);
const remaining = Math.max(0, (this._confirmTimeoutMs / 1000) - elapsed);
const timerEl = document.getElementById('ai-confirm-timer');
if (timerEl) {
timerEl.textContent = `ā± ${remaining}s`;
timerEl.className = 'confirm-timer';
if (remaining > 30) {
timerEl.style.color = 'var(--warning-color)';
} else if (remaining > 10) {
timerEl.classList.add('urgent');
} else {
timerEl.classList.add('critical');
}
}
if (remaining === 60) {
this.terminal.print(`ā± 60s remaining to respond to AI confirmation for: ${names}`, 'warning');
} else if (remaining === 30) {
this.terminal.print(`ā± 30s ā ${names} will be auto-denied soon!`, 'warning');
} else if (remaining === 10) {
this.terminal.print(`ā± 10s remaining! Type ai-allow or click Allow now!`, 'warning');
}
}, 500);
this._confirmTimerInterval = timerInterval;
this._confirmTimeout = setTimeout(() => {
clearInterval(timerInterval);
if (this._confirmResolve) {
this._confirmResolve(false);
this._cleanupConfirmBar();
this.terminal.print('ā± Confirmation timed out ā tool not executed', 'warning');
}
}, this._confirmTimeoutMs);
});
}
_resolveConfirm(approved) {
if (this._confirmResolve) {
this._confirmResolve(approved);
this._cleanupConfirmBar();
if (approved) {
this.terminal.print('Action allowed', 'success');
} else {
this.terminal.print('Action denied', 'warning');
}
}
}
_cleanupConfirmBar() {
clearInterval(this._confirmTimerInterval);
clearTimeout(this._confirmTimeout);
this._confirmTimeout = null;
this._confirmTimerInterval = null;
this._confirmResolve = null;
this._pendingConfirmation = null;
this._confirmAllowBtn = null;
this._confirmDenyBtn = null;
this._confirmOnAllow = null;
this._confirmOnDeny = null;
const area = document.getElementById('ai-confirm-area');
if (area) {
area.classList.remove('visible');
area.innerHTML = '';
}
this._confirmArea = null;
this._confirmBar = null;
const timerEl = document.getElementById('ai-confirm-timer');
if (timerEl) timerEl.style.animation = '';
}
confirmPending() {
this._resolveConfirm(true);
}
denyPending() {
this._resolveConfirm(false);
}
ignoreValidator() {
this._validatorSuppressed = true;
this.terminal.print('š Validator suppressed for this session', 'info');
}
cancelAi() {
this._cancelled = true;
this.terminal.print('ā¹ Cancelling AI...', 'warning');
// Abort any in-flight HTTP request
if (this.terminal.ollamaManager) {
this.terminal.ollamaManager.abort();
}
// Clear any pending confirmation
if (this._confirmResolve) {
this._resolveConfirm(false);
}
// Immediately reset UI so user knows it stopped
this.terminal.setAiBusy(false);
}
toggleDebug() {
this._debugPanelEnabled = !this._debugPanelEnabled;
let panel = document.getElementById('debug-panel');
if (!panel) {
// Create the debug panel
panel = document.createElement('div');
panel.id = 'debug-panel';
panel.style.cssText = 'position:fixed;right:10px;top:50px;width:340px;max-height:350px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:100;font-family:monospace;font-size:0.75em;display:none;';
document.body.appendChild(panel);
// Add close button handler
panel.addEventListener('click', function() {
// Toggle collapsed/expanded state
});
}
panel.style.display = this._debugPanelEnabled ? 'block' : 'none';
if (this._debugPanelEnabled) {
this._updateDebugPanel();
this.terminal.print('š§ Debug panel enabled ā shows tool call log in bottom-right corner. Close with: debug', 'info');
} else {
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 | guardrail-remove ', '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 | guardrail-remove ', 'warning');
}
}
// šÆ SHADOW COMMAND
shadowExec(args) {
// Usage: shadow
// Snapshot VFS, run command, diff output
if (args.length === 0) {
this.terminal.print('Usage: shadow ā 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
// Now command-aware: run_command is only destructive for mutating commands
isDestructive(name, args = {}) {
if (name === 'run_command') {
const cmdStr = typeof args === 'object' ? (args?.command || '') : String(args);
const cmd = cmdStr.split(' ')[0].toLowerCase();
const safeRead = new Set(['ls','cat','pwd','echo','date','env','whoami','which','hostname',
'cal','tree','find','grep','head','tail','sort','wc','du','diff','basename','dirname',
'yes','seq','cut','uniq','expand','printf','base64','help','clear','set','export',
'unalias','history','ll','h','mem','status','df','free','top','ps','health-check',
'system-health','modules','listmods','modules-list','get-help-info','alias']);
return !safeRead.has(cmd);
}
return ['write_file','delete_file','make_directory','edit_module'].includes(name);
}
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': {
const originalPath = a.path || '';
const p = originalPath.replace(/^\//, '');
const saved = [...this.terminal.fs.currentPath];
if (originalPath.startsWith('/')) {
this.terminal.fs.currentPath = [];
}
if (p) {
const parts = p.split('/');
const dir = this.terminal.fs.root;
let cur = dir;
let found = true;
for (const part of parts) {
if (part === '..') { saved.pop(); continue; }
if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part];
else { found = false; break; }
}
if (!found) {
// Retry from root ā cwd may be stale
this.terminal.fs.currentPath = [];
cur = this.terminal.fs.root;
found = true;
for (const part of parts) {
if (part === '..') continue;
if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part];
else { found = false; break; }
}
if (!found) {
this.terminal.fs.currentPath = saved;
return JSON.stringify({ error: `Directory not found: ${p}` });
}
}
this.terminal.fs.currentPath = saved.concat(parts.filter(x => x !== '..'));
}
const files = this.terminal.fs.ls();
this.terminal.fs.currentPath = saved;
return JSON.stringify(files.map(f => ({ name: f.name, type: f.type, size: f.size })));
}
case 'read_file': {
const rp = a.path || '';
const savedR = [...this.terminal.fs.currentPath];
if (rp.startsWith('/')) this.terminal.fs.currentPath = [];
const path = rp.replace(/^\//, '');
const content = this.terminal.fs.readFile(path);
this.terminal.fs.currentPath = savedR;
if (content === null) return JSON.stringify({ error: `File not found: ${path}` });
return content.length > 8000 ? content.substring(0, 8000) + '\n...(truncated)' : content;
}
case 'write_file': {
const wp = a.path || '';
const savedW = [...this.terminal.fs.currentPath];
if (wp.startsWith('/')) this.terminal.fs.currentPath = [];
const path = wp.replace(/^\//, '');
const content = a.content || '';
this.terminal.fs.writeFile(path, content);
this.terminal.fs.currentPath = savedW;
return `File written: ${path} (${content.length} chars)`;
}
case 'make_directory': {
const mp = a.path || '';
const savedM = [...this.terminal.fs.currentPath];
if (mp.startsWith('/')) this.terminal.fs.currentPath = [];
const path = mp.replace(/^\//, '');
const ok = this.terminal.fs.mkdir(path);
this.terminal.fs.currentPath = savedM;
if (ok) return `Directory created: ${path}`;
return JSON.stringify({ error: `Cannot create: ${path}` });
}
case 'delete_file': {
const dp = a.path || '';
const savedD = [...this.terminal.fs.currentPath];
if (dp.startsWith('/')) this.terminal.fs.currentPath = [];
const path = dp.replace(/^\//, '');
const delOk = this.terminal.fs.deleteFile(path);
this.terminal.fs.currentPath = savedD;
if (delOk) return `Deleted: ${path}`;
return JSON.stringify({ error: `Cannot delete: ${path}` });
}
case 'get_status': {
const fs = this.terminal.fs;
const mm = this.terminal.moduleManager;
const om = this.terminal.ollamaManager;
return JSON.stringify({
files: fs.ls().length,
dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/',
modules: Object.keys(mm.modules).length,
ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected',
memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
});
}
case 'get_help': {
const cmds = this.terminal.moduleManager.getAllCommands();
return JSON.stringify(Object.keys(cmds).sort());
}
case 'get_current_dir': {
return '/' + this.terminal.fs.currentPath.join('/');
}
case 'web_search': {
const query = a.query || '';
if (!query) return JSON.stringify({ error: 'No query provided' });
try {
const resp = await fetch('/api/search?q=' + encodeURIComponent(query));
if (!resp.ok) { return JSON.stringify({ error: 'Search API returned HTTP ' + resp.status }); }
const data = await resp.json();
const results = data.results || [];
if (results.length === 0) return JSON.stringify({ message: 'No results found' });
return JSON.stringify({ count: results.length, source: data.source, results: results.map(r => ({ title: r.title, url: r.url, content: (r.content || '').substring(0, 300) })) });
} catch (e) { return JSON.stringify({ error: 'Search failed: ' + e.message }); }
}
case 'web_fetch': {
const url = a.url || '';
if (!url) return JSON.stringify({ error: 'No URL provided' });
try {
const resp = await fetch('/api/extract?url=' + encodeURIComponent(url));
if (resp.status === 403) return JSON.stringify({ error: 'URL blocked ā internal network' });
if (!resp.ok) { return JSON.stringify({ error: 'Fetch API returned HTTP ' + resp.status }); }
const data = await resp.json();
return JSON.stringify({ title: data.title || '', content: (data.content || '').substring(0, 4000), word_count: data.word_count || 0, method: data.method || 'extract' });
} catch (e) { return JSON.stringify({ error: 'Fetch failed: ' + e.message }); }
}
case 'wpm_install': {
const pkg = a.package || '';
if (!pkg) return JSON.stringify({ error: 'No package name' });
const mod = this.terminal.moduleManager.modules['WpmModule'];
if (mod) { await mod.install(pkg); return `Package "${pkg}" installation attempted`; }
return JSON.stringify({ error: 'WPM module not available' });
}
case 'wpm_search': {
const kw = a.keyword || '';
if (!kw) return JSON.stringify({ error: 'No keyword' });
const mod = this.terminal.moduleManager.modules['WpmModule'];
if (mod) {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
try { await mod.search(kw); } finally { this.terminal.print = originalPrint; }
return output.trim() || 'Search completed';
}
return JSON.stringify({ error: 'WPM module not available' });
}
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.security.checkAndRun(cmd); } 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': {
const reg = this.terminal.moduleManager.moduleRegistry;
const results = [];
for (const mod of reg) {
if (mod.url && mod.url.startsWith('http')) {
try {
const resp = await fetch(mod.url, { method: 'HEAD' });
results.push({ name: mod.name, url: mod.url, status: resp.ok ? 'reachable' : 'error (' + resp.status + ')' });
} catch (e) {
results.push({ name: mod.name, url: mod.url, status: 'unreachable' });
}
}
}
return JSON.stringify({ checked: results.length, modules: results });
}
case 'user_get': {
const key = a.key || '';
if (!key) return JSON.stringify({ error: 'Key is required' });
const val = key.split('.').reduce((o, k) => (o && typeof o === 'object') ? o[k] : undefined, this._userProfile);
return JSON.stringify({ key, value: val !== undefined ? val : null });
}
case 'user_set': {
const ukey = a.key || '';
if (!ukey) return JSON.stringify({ error: 'Key and value are required' });
const uval = a.value || '';
const keys = ukey.split('.');
let obj = this._userProfile;
for (let i = 0; i < keys.length - 1; i++) {
if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') obj[keys[i]] = {};
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = uval;
try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {}
return JSON.stringify({ success: true, key: ukey, value: uval });
}
case 'user_list': {
return JSON.stringify({ profile: this._userProfile, keys: Object.keys(this._userProfile) });
}
case 'user_forget': {
const fkey = a.key || '';
if (!fkey) return JSON.stringify({ error: 'Key is required' });
if (fkey.includes('.')) {
const parts = fkey.split('.');
let obj = this._userProfile;
for (let i = 0; i < parts.length - 1; i++) {
if (!obj[parts[i]]) return JSON.stringify({ error: 'Key not found' });
obj = obj[parts[i]];
}
delete obj[parts[parts.length - 1]];
} else {
delete this._userProfile[fkey];
}
try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {}
return JSON.stringify({ success: true, key: fkey });
}
case 'session_save': {
const summary = a.summary || 'No summary provided';
const fs = this.terminal.fs;
fs.mkdirRecursive('/memory');
const today = new Date().toISOString().split('T')[0];
const log = { date: today, summary, toolCallCount: 0, skillsCreated: [], modulesEdited: [], timestamp: new Date().toISOString() };
fs.fsWriteJSON('/memory/' + today + '.json', log);
const index = fs.fsReadJSON('/memory/index.json') || {};
index[today] = { summary, tasks: (index[today]?.tasks || 0) + 1, updated: new Date().toISOString() };
fs.fsWriteJSON('/memory/index.json', index);
return JSON.stringify({ success: true, date: today });
}
case 'session_load': {
const sdate = a.date || '';
if (!sdate) return JSON.stringify({ error: 'Date required (YYYY-MM-DD)' });
const sfs = this.terminal.fs;
const slog = sfs.fsReadJSON('/memory/' + sdate + '.json');
if (!slog) return JSON.stringify({ error: 'No session log for ' + sdate });
return JSON.stringify(slog);
}
case 'session_recent': {
const count = a.count || 3;
const rfs = this.terminal.fs;
const rindex = rfs.fsReadJSON('/memory/index.json') || {};
const dates = Object.keys(rindex).sort().reverse().slice(0, count);
const sessions = dates.map(d => ({ date: d, ...rindex[d] }));
return JSON.stringify({ sessions, count: sessions.length });
}
case 'session_terminal': {
const tfs = this.terminal.fs;
try {
const raw = tfs.readFile('_sessions/_auto.json');
if (!raw) return JSON.stringify({ error: 'No auto-saved session found' });
const data = JSON.parse(raw);
const lines = (data.terminalLines || []).slice(-50);
const text = lines.map(l => {
const ts = l.t ? '[' + l.t + '] ' : '';
return ts + l.l;
}).join('\n');
return JSON.stringify({ lines: lines.length, output: text, savedAt: data.savedAt || '' });
} catch (e) {
return JSON.stringify({ error: 'Could not read terminal history: ' + e.message });
}
}
case 'session_stats': {
const sfs = this.terminal.fs;
const sindex = sfs.fsReadJSON('/memory/index.json') || {};
const entries = Object.entries(sindex);
let totalTasks = 0;
for (const [, meta] of entries) totalTasks += meta.tasks || 0;
return JSON.stringify({ totalSessions: entries.length, totalTasks, dates: entries.map(([d]) => d) });
}
case 'list_agents': {
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
if (!registry) return JSON.stringify({ error: 'Agent registry not available' });
const agents = this.terminal.fs.ls('/agents/');
const results = [];
if (agents) {
agents.forEach(a => {
if (a.type === 'dir' && a.name !== 'configured') {
const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`);
if (data) results.push(JSON.parse(data));
}
});
}
return JSON.stringify({ agents: results, count: results.length });
}
case 'delegate_task': {
const task = a.task || '';
if (!task) return JSON.stringify({ error: 'No task provided' });
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
orch.delegateTask(task);
return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` });
}
case 'spawn_agent': {
const role = a.role || 'worker';
const caps = a.capabilities ? a.capabilities.split(',').map(s => s.trim()) : ['list_files', 'read_file'];
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
const agent = orch.spawnAgent(role, caps);
return JSON.stringify({ message: `Agent spawned: ${agent.id}`, agent });
}
case 'load_skills': {
const query = (a && a.query || '').toLowerCase();
const fs = this.terminal.fs;
const index = fs.fsReadJSON('/skills/index.json') || {};
if (!query) {
const names = Object.keys(index);
return JSON.stringify({ skills: names, count: names.length, message: 'No query provided ā showing all skill names. Call again with a query to load content.' });
}
const matches = [];
for (const [name, meta] of Object.entries(index)) {
const score = (name.includes(query) ? 3 : 0) + (meta.description.toLowerCase().includes(query) ? 2 : 0) + ((meta.tags || []).some(t => t.toLowerCase().includes(query)) ? 2 : 0);
if (score > 0) {
const content = fs.readFile(`/skills/${name}.md`) || '';
matches.push({ name, description: meta.description, category: meta.category, score, content: content.substring(0, 2000) });
}
}
matches.sort((a, b) => b.score - a.score);
const top = matches.slice(0, 3);
return JSON.stringify(top.length > 0 ? { matches: top.map(m => ({ name: m.name, description: m.description, content: m.content })), count: top.length } : { matches: [], count: 0, message: 'No matching skills found. You can create one after this task.' });
}
case 'create_skill': {
const name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase();
if (!name) return JSON.stringify({ error: 'Skill name is required' });
const description = a.description || '';
let content = a.content || '';
const tags = (a.tags || '').split(',').map(t => t.trim()).filter(Boolean);
const category = a.category || 'general';
if (!content.startsWith('---')) {
content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n${content}`;
}
const fs = this.terminal.fs;
fs.mkdirRecursive('/skills');
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
index[name] = { name, description, tags, category, created: index[name]?.created || new Date().toISOString(), updated: new Date().toISOString() };
fs.fsWriteJSON('/skills/index.json', index);
this.terminal.print(`š” Skill saved: ${name}`, 'success');
return JSON.stringify({ success: true, name, message: `Skill "${name}" created (${content.length} chars)` });
}
case 'patch_skill': {
const pname = (a.name || '').trim();
if (!pname) return JSON.stringify({ error: 'Skill name is required' });
const pcontent = a.content || '';
if (!pcontent) return JSON.stringify({ error: 'Skill content is required' });
const fs = this.terminal.fs;
const index = fs.fsReadJSON('/skills/index.json') || {};
if (!index[pname]) return JSON.stringify({ error: `Skill "${pname}" not found` });
fs.writeFile(`/skills/${pname}.md`, pcontent);
index[pname].updated = new Date().toISOString();
// Re-parse frontmatter for metadata updates
const metaMatch = pcontent.match(/^---\n([\s\S]*?)\n---/);
if (metaMatch) {
const metaLines = metaMatch[1].split('\n');
metaLines.forEach(line => {
const [k, ...v] = line.split(':');
if (k === 'description') index[pname].description = v.join(':').trim();
if (k === 'tags') index[pname].tags = v.join(':').trim().replace(/[\[\]]/g, '').split(',').map(t => t.trim());
if (k === 'category') index[pname].category = v.join(':').trim();
});
}
fs.fsWriteJSON('/skills/index.json', index);
this.terminal.print(`š” Skill patched: ${pname}`, 'success');
return JSON.stringify({ success: true, name: pname, message: `Skill "${pname}" updated` });
}
case 'list_skills': {
const filterCat = (a.category || '').toLowerCase();
const fs = this.terminal.fs;
const index = fs.fsReadJSON('/skills/index.json') || {};
const list = Object.entries(index).map(([name, meta]) => ({ name, description: meta.description, category: meta.category || 'general', tags: meta.tags || [], updated: meta.updated }))
.filter(s => !filterCat || (s.category || '').toLowerCase() === filterCat);
return JSON.stringify({ skills: list, count: list.length });
}
case 'nvfs_resolve': {
const concept = (a.concept || '').toLowerCase();
if (!concept) return JSON.stringify({ error: 'Concept is required' });
const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
if (!nvfs) return JSON.stringify({ error: 'Neural VFS module not loaded ā run nvfs-index first' });
// Build index if empty
if (Object.keys(nvfs.index).length === 0) {
const saved = this.terminal.fs.fsReadJSON('/.semantic_index/map.json');
if (saved) { nvfs.index = saved; }
else { return JSON.stringify({ error: 'No semantic index found ā run nvfs-index first' }); }
}
const exact = nvfs.index[concept];
const fuzzy = Object.keys(nvfs.index).filter(k =>
k.includes(concept) || concept.includes(k)
).filter(k => k !== concept);
const results = [];
if (exact) results.push(...exact.map(e => ({ concept, path: e.path, summary: e.summary })));
for (const f of fuzzy.slice(0, 5)) {
results.push(...nvfs.index[f].slice(0, 3).map(e => ({ concept: f, path: e.path, summary: e.summary })));
}
return JSON.stringify({ concept, exact: !!exact, count: results.length, results: results.slice(0, 20) });
}
case 'list_available_modules': {
try {
const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || ''));
if (!resp.ok) return JSON.stringify({ error: 'Module index not available' });
const idx = await resp.json();
const filterType = (a.type || '').toLowerCase();
let mods = Object.values(idx.modules);
if (filterType) mods = mods.filter(m => m.type === filterType);
return JSON.stringify({
total: mods.length,
available: idx.total,
type: filterType || 'all',
modules: mods.map(m => ({
name: m.name,
type: m.type,
class: m.class,
commands: m.commands.slice(0, 8),
description: (m.description || '').substring(0, 80),
auto_load: m.auto_load
}))
});
} catch (e) { return JSON.stringify({ error: 'Failed to load module index: ' + e.message }); }
}
case 'smart_context_list': {
const scm = this.terminal.moduleManager.modules['SmartContextModule'];
if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded', profiles: [] });
const profiles = scm.contextProfiles || {};
return JSON.stringify({
profiles: Object.entries(profiles).map(([key, p]) => ({
name: key,
displayName: p.name || key,
systemPrompt: (p.systemPrompt || '').substring(0, 80),
temperature: p.temperature || 0.7,
active: scm.activeProfile === key
}))
});
}
case 'smart_context_set': {
const scm = this.terminal.moduleManager.modules['SmartContextModule'];
if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded' });
const name = (a.name || '').toLowerCase();
if (!scm.contextProfiles[name]) {
return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) });
}
scm.activeProfile = name;
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':
case 'reload_module':
case 'list_modified_modules':
case 'get_module_status':
return await this.runModuleTool(name, argsStr);
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
async runModuleTool(name, argsStr) {
let a = {};
try {
a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
switch (name) {
case 'view_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (code) return code;
const url = entry ? entry.url : null;
// Try registry URL first
if (url) {
try {
const resp = await fetch(url);
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
return src;
}
} catch (e) { /* fall through */ }
}
// Fallback: use modules-index.json to map class name to filename
try {
const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || ''));
if (idxResp.ok) {
const idx = await idxResp.json();
// modName is the class name (e.g. "WebX11AIAssistantModule")
// or already a filename (e.g. "module-aiassist")
// Try matching by class name first
let match = Object.values(idx.modules).find(m => m.class === modName);
// Then by name
if (!match) match = idx.modules[modName];
if (match) {
const filePath = match.file; // e.g. "modules/core/module-aiassist.js"
const fetchUrl = `/spaces/newos4/${filePath}`;
const resp = await fetch(fetchUrl);
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
return src;
}
}
}
} catch (e) { /* fall through */ }
return JSON.stringify({ error: `Cannot find source for ${modName} ā not in registry, not in modules-index.json` });
}
case 'edit_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
const url = entry ? entry.url : null;
let code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code && url) {
try {
const resp = await fetch(url);
if (resp.ok) {
code = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code);
this.terminal.fs.writeFile(`_mods/${modName}.js`, code);
this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: url, modifiedAt: new Date().toISOString(), editCount: 0 }));
}
} catch (e) {}
}
// Fallback: use modules-index.json for core modules (not in registry)
if (!code) {
try {
const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || ''));
if (idxResp.ok) {
const idx = await idxResp.json();
let match = Object.values(idx.modules).find(m => m.class === modName);
if (!match) match = idx.modules[modName];
if (match) {
const filePath = match.file;
const fetchUrl = `/spaces/newos4/${filePath}`;
const resp = await fetch(fetchUrl);
if (resp.ok) {
code = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code);
this.terminal.fs.writeFile(`_mods/${modName}.js`, code);
this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: fetchUrl, modifiedAt: new Date().toISOString(), editCount: 0 }));
}
}
}
} catch (e) {}
}
if (code) this.terminal.openCodeEditor(modName, url || '', code);
return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` });
}
case 'test_module': {
const modName = a.name || '';
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code) return JSON.stringify({ error: `No local edit for ${modName}` });
this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
return `Module "${modName}" loaded from local edit for testing`;
}
case 'reload_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
this.terminal.fs.deleteFile(`_mods/${modName}.js`);
this.terminal.fs.deleteFile(`_mods/${modName}.orig.js`);
this.terminal.fs.deleteFile(`_mods/${modName}.meta.json`);
if (entry && entry.url) {
this.terminal.moduleManager.unloadModule(modName);
await this.terminal.moduleManager.loadModuleFromURL(entry.url);
return `Module "${modName}" reloaded from ${entry.url}`;
}
return JSON.stringify({ error: `No original URL for ${modName}` });
}
case 'list_modified_modules': {
const modsDir = this.terminal.fs.getCurrentDir();
const mc = modsDir.content['_mods'];
if (!mc || Object.keys(mc.content).length === 0) return JSON.stringify({ modified: [], message: 'No local edits' });
const files = Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js'));
return JSON.stringify({ modified: files.map(f => f.replace('.js', '')), count: files.length });
}
case 'get_module_status': {
const mm = this.terminal.moduleManager;
const modsDir = this.terminal.fs.getCurrentDir();
const hasMods = modsDir.content['_mods'] ? !!Object.keys(modsDir.content['_mods'].content).length : false;
// Load module index for file path lookup
let idxMap = {};
try {
const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || ''));
if (resp.ok) {
const idx = await resp.json();
for (const [name, meta] of Object.entries(idx.modules)) {
idxMap[meta.class] = '/spaces/newos4/' + meta.file;
}
}
} catch (e) {}
const modules = [];
const seen = new Set();
for (const [className, instance] of Object.entries(mm.modules)) {
const name = instance.constructor ? instance.constructor.name : className;
if (seen.has(name)) continue;
seen.add(name);
modules.push({
name: name,
url: idxMap[name] || null,
enabled: true,
loaded: true,
modified: !!this.terminal.fs.readFile(`_mods/${name}.js`),
source: 'loaded'
});
}
for (const m of mm.moduleRegistry) {
if (seen.has(m.name)) continue;
seen.add(m.name);
modules.push({
name: m.name, url: m.url || idxMap[m.name] || null, enabled: m.enabled,
loaded: !!mm.modules[m.name],
modified: !!this.terminal.fs.readFile(`_mods/${m.name}.js`),
source: m.url ? 'ext' : 'core'
});
}
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
clearContext() {
this.context = [];
localStorage.removeItem('webx11_ai_context');
this.terminal.print('AI conversation context cleared', 'success');
}
_saveContext() {
try {
const data = JSON.stringify({
context: this.context.slice(-20),
messageCount: this._messageCount,
savedAt: Date.now()
});
localStorage.setItem('webx11_ai_context', data);
// Also persist to VFS for crash survival
try {
const fs = this.terminal.fs;
if (fs) {
fs.mkdirRecursive('_sessions');
fs.writeFile('_sessions/_auto-context.json', data);
}
} catch (e) {}
} catch (e) {
// localStorage quota exceeded ā silently ignore
}
}
_loadContext() {
try {
const saved = localStorage.getItem('webx11_ai_context');
if (saved) {
const data = JSON.parse(saved);
if (data && data.context) {
this.context = data.context.slice(-20);
this._messageCount = data.messageCount || 0;
}
}
} catch (e) {}
}
setSystemPrompt(args) {
if (!args[0]) {
this.terminal.print(`Current system prompt: ${this.systemPrompt.substring(0, 50)}...`, 'info'); return;
}
this.systemPrompt = args.join(' ');
this.terminal.print('System prompt updated', 'success');
}
showModel(args) {
const om = this.terminal.ollamaManager;
if (args.length > 0) {
const name = args.join(' ');
if (om.models && om.models.some(m => m.name === name)) {
om.currentModel = name;
localStorage.setItem('webx11_ollama_model', name);
this.terminal.print(`AI model set to: ${name}`, 'success');
} else {
this.terminal.print(`Model "${name}" not found in available models. Use ai-model to list.`, 'warning');
}
return;
}
this.terminal.print(`Current AI model: ${om.currentModel || 'none'}`, 'info');
if (om.models && om.models.length > 0) {
const maxShow = 20;
const names = om.models.slice(0, maxShow).map(m => m.name).join(', ');
this.terminal.print(`Available (${om.models.length}): ${names}${om.models.length > maxShow ? `... +${om.models.length - maxShow} more` : ''}`, 'info');
} else {
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 > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (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 ', 'info');
this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), 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 < 0) return this.terminal.print('confirm-timeout must be 0+ seconds (0 = no confirm / auto-approve all)', '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;
}
this.terminal.print(`=== Context (${this.context.length} messages) ===`, 'info');
this.context.forEach(msg => {
const icon = msg.role === 'user' ? 'š¤' : msg.role === 'assistant' ? 'š¤' : 'š§';
const content = typeof msg.content === 'string' ? msg.content.substring(0, 60) : JSON.stringify(msg).substring(0, 60);
this.terminal.print(`${icon} ${content}...`);
});
}
_extractFacts(userMsg, aiResp) {
const patterns = [
{ re: /my name is (\w+)/i, key: 'name' },
{ re: /call me (\w+)/i, key: 'name' },
{ re: /i(?:'m| am) working on (.+)/i, key: 'project' },
{ re: /(?:my|the) project is (.+)/i, key: 'project' },
{ re: /i have a server at (.+)/i, key: 'server' },
{ re: /i use (\w+).*?(?:editor|ide|text)/i, key: 'preferredEditor' },
{ re: /i (?:like|prefer|love) (.+)/i, key: 'preference' },
];
let changed = false;
for (const { re, key } of patterns) {
const m = (userMsg + ' ' + (aiResp || '')).match(re);
if (m && m[1]) {
if (!this._userProfile.knownFacts) this._userProfile.knownFacts = {};
if (this._userProfile.knownFacts[key] !== m[1].trim()) {
this._userProfile.knownFacts[key] = m[1].trim();
changed = true;
}
}
}
if (changed) {
try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {}
}
}
_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'];
for (const p of patterns) {
if (lower.includes(p)) this._commandFreq[p] = (this._commandFreq[p] || 0) + 1;
}
try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {}
}
async _autoCreateErrorSkill() {
const grouped = {};
for (const e of this._errorLog) {
if (!grouped[e.tool]) grouped[e.tool] = [];
grouped[e.tool].push(e);
}
for (const [tool, errors] of Object.entries(grouped)) {
if (errors.length >= 3) {
const skillName = 'tool-' + tool + '-workaround';
const existing = this.terminal.fs.readFile('/skills/' + skillName + '.md');
if (existing) continue;
const steps = errors.map((e, i) => (i + 1) + '. When calling ' + tool + ', avoid: "' + JSON.stringify(e.args) + '" which caused: ' + e.error).join('\n');
const content = '---\nname: ' + skillName + '\ndescription: Workarounds for ' + tool + ' failures\ntags: [' + tool + ', error, workaround]\ncategory: system\n---\n## Steps\n' + steps;
await this.runTool('create_skill', { name: skillName, description: 'Workarounds for ' + tool + ' failures', content: content, tags: tool + ',error,workaround', category: 'system' });
this.terminal.print('š¤ Auto-created skill from repeated errors: ' + skillName, 'info');
}
}
this._errorLog = [];
}
async _auditSkills() {
const fs = this.terminal.fs;
const index = fs.fsReadJSON('/skills/index.json') || {};
const now = Date.now();
const issues = [];
for (const [name, meta] of Object.entries(index)) {
const content = fs.readFile('/skills/' + name + '.md');
if (!content || content.length < 50) {
issues.push({ name, issue: 'empty or too short' });
continue;
}
const updated = new Date(meta.updated || 0).getTime();
const ageDays = (now - updated) / 86400000;
if (ageDays > 30) issues.push({ name, issue: 'not updated in ' + Math.round(ageDays) + ' days' });
}
if (issues.length > 0) {
this.terminal.print('š Skill audit: ' + issues.length + ' issue(s) found', 'info');
issues.slice(0, 3).forEach(i => this.terminal.print(' - ' + i.name + ': ' + i.issue, 'warning'));
}
}
_estimateTokens(text) {
if (!text || typeof text !== 'string') return 0;
return Math.ceil(text.length / 4);
}
_estimateContextTokens() {
let total = 0;
for (const msg of this.context) {
total += this._estimateTokens(msg.content || '');
}
return total;
}
async _compressContext() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
if (totalTokens < this.maxContextTokens) return;
const keepCount = 3;
const oldMsgs = this.context.slice(0, -keepCount * 2);
if (oldMsgs.length < 2) return;
const recentMsgs = this.context.slice(-keepCount * 2);
const summaryText = oldMsgs.map(m =>
(m.role === 'user' ? 'š¤' : m.role === 'assistant' ? 'š¤' : 'āļø') + ': ' +
(m.content || '').substring(0, 150)
).join(' | ');
let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText}]`;
// Try AI summarization, fallback to truncation
try {
if (this.terminal.ollamaManager && this.terminal.ollamaManager.connected) {
const sumPrompt = 'Summarize these conversation exchanges into a single compact sentence. Focus on what the user asked and what actions were taken. Keep it under 200 characters.\n\n' + JSON.stringify(oldMsgs.map(m => ({ role: m.role, content: (m.content || '').substring(0, 200) })));
const sumRes = await this.terminal.ollamaManager.chat(sumPrompt);
if (sumRes && sumRes.length < 300) {
compressed = '[Earlier context: ' + sumRes.trim() + ']';
}
}
} catch (e) { /* fallback to truncation */ }
this.context = [{ role: 'system', content: compressed }, ...recentMsgs];
this.terminal.print('š¦ Context compressed (' + oldMsgs.length + ' messages ā 1 summary, ~' + totalTokens + ' tokens)', 'info');
}
_manualCompress() {
this._compressContext().then(() => {
this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info');
}).catch(e => {
this.terminal.print('Compression error: ' + e.message, 'error');
});
}
}