feat: config-driven architecture, install wizard, live runtime switching, usage tracking, auto-failover

Major changes:
- Config-driven adapters: all channels (Slack, Discord, Telegram, WebChat, Webhooks) controlled via config.json with enabled flags and token auto-detection, no CLI flags required
- Runtime engine field: runtime.engine selects opencode/claude from config
- Interactive install script: 8-phase setup wizard with AI runtime detection/installation, token setup, identity file personalization (personality presets), aetheel CLI command, background service (launchd/systemd)
- Live runtime switching: /engine, /model, /provider commands hot-swap the AI runtime from chat without restart, changes persisted to config.json
- Usage tracking: per-request cost extraction from Claude Code JSON output, cumulative stats via /usage command
- Auto-failover: rate limit detection on both runtimes, automatic switch to other engine on quota errors with user notification
- Chat commands work without / prefix (Slack intercepts / in channels), commands: engine, model, provider, config, usage, reload, cron, subagents, status, help
- /config set for editing config.json from chat with dotted key notation
- Security audit saved to docs/security-audit.md
- Full command reference in docs/commands.md
- Future changes doc with NanoClaw agent teams analysis
- Logo added to README and WebChat UI
- README fully rewritten with all features documented
This commit is contained in:
2026-02-18 01:07:12 -05:00
parent 41b2f9a593
commit 6d73f74e0b
41 changed files with 11363 additions and 437 deletions

221
static/chat.html Normal file
View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aetheel Chat</title>
<link rel="icon" type="image/jpeg" href="/logo.jpeg">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
height: 100vh;
display: flex;
flex-direction: column;
}
#header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #16213e;
border-bottom: 1px solid #0f3460;
flex-shrink: 0;
}
#header .title { font-size: 1.1rem; font-weight: 600; }
#status {
font-size: 0.8rem;
padding: 3px 10px;
border-radius: 12px;
background: #333;
color: #aaa;
}
#status.connected { background: #0a3d2a; color: #4ade80; }
#status.disconnected { background: #3d0a0a; color: #f87171; }
#messages {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.msg {
max-width: 80%;
padding: 10px 14px;
border-radius: 12px;
line-height: 1.5;
word-wrap: break-word;
font-size: 0.95rem;
}
.msg.user {
align-self: flex-end;
background: #0f3460;
color: #e0e0e0;
border-bottom-right-radius: 4px;
}
.msg.ai {
align-self: flex-start;
background: #222244;
color: #d0d0d0;
border-bottom-left-radius: 4px;
}
.msg.ai strong { color: #93c5fd; }
.msg.ai code {
background: #1a1a2e;
padding: 1px 5px;
border-radius: 4px;
font-size: 0.88em;
font-family: 'Fira Code', 'Consolas', monospace;
}
.msg.ai pre {
background: #111128;
padding: 10px 12px;
border-radius: 6px;
overflow-x: auto;
margin: 6px 0;
font-size: 0.88em;
font-family: 'Fira Code', 'Consolas', monospace;
line-height: 1.4;
}
.msg.ai pre code { background: none; padding: 0; }
.msg.ai ul, .msg.ai ol { padding-left: 20px; margin: 4px 0; }
#input-area {
display: flex;
gap: 8px;
padding: 12px 20px;
background: #16213e;
border-top: 1px solid #0f3460;
flex-shrink: 0;
}
#input {
flex: 1;
padding: 10px 14px;
border: 1px solid #0f3460;
border-radius: 8px;
background: #1a1a2e;
color: #e0e0e0;
font-size: 0.95rem;
outline: none;
}
#input:focus { border-color: #3b82f6; }
#send {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #3b82f6;
color: #fff;
font-size: 0.95rem;
cursor: pointer;
font-weight: 500;
}
#send:hover { background: #2563eb; }
#send:disabled { background: #333; cursor: not-allowed; }
</style>
</head>
<body>
<div id="header">
<div style="display:flex;align-items:center;gap:8px">
<img src="/logo.jpeg" alt="Aetheel" width="28" height="28" style="border-radius:6px">
<span class="title">Aetheel</span>
</div>
<span id="status">connecting&#8230;</span>
</div>
<div id="messages" role="log" aria-live="polite"></div>
<div id="input-area">
<input type="text" id="input" placeholder="Type a message&#8230;" autofocus
aria-label="Chat message input">
<button id="send" aria-label="Send message">Send</button>
</div>
<script>
(function () {
var messagesEl = document.getElementById('messages');
var inputEl = document.getElementById('input');
var sendBtn = document.getElementById('send');
var statusEl = document.getElementById('status');
var ws = null;
var reconnectTimer = null;
function renderMarkdown(text) {
var html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
return '<pre><code>' + code.trimEnd() + '</code></pre>';
});
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>[\s\S]*?<\/li>\n?)+/g, function (m) {
return '<ul>' + m + '</ul>';
});
html = html.replace(/\n/g, '<br>');
html = html.replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, function (_, c) {
return '<pre><code>' + c.replace(/<br>/g, '\n') + '</code></pre>';
});
html = html.replace(/<ul>([\s\S]*?)<\/ul>/g, function (_, items) {
return '<ul>' + items.replace(/<br>/g, '') + '</ul>';
});
return html;
}
function addMessage(text, type) {
var div = document.createElement('div');
div.className = 'msg ' + type;
div.setAttribute('role', 'article');
if (type === 'ai') {
div.innerHTML = renderMarkdown(text);
} else {
div.textContent = text;
}
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function setStatus(state) {
statusEl.className = state;
var labels = { connected: 'connected', disconnected: 'disconnected', '': 'connecting\u2026' };
statusEl.textContent = labels[state] || state;
}
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
setStatus('');
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(proto + '//' + location.host + '/ws');
ws.onopen = function () {
setStatus('connected');
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
};
ws.onmessage = function (event) { addMessage(event.data, 'ai'); };
ws.onclose = function () {
setStatus('disconnected');
ws = null;
reconnectTimer = setTimeout(connect, 3000);
};
ws.onerror = function () { ws.close(); };
}
function send() {
var text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage(text, 'user');
ws.send(text);
inputEl.value = '';
inputEl.focus();
}
sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
connect();
})();
</script>
</body>
</html>

BIN
static/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB