aiden-runtime 3.16.2 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+ // api/dashboard.ts — Aiden local web dashboard
3
+ // Served at GET /ui — single self-contained HTML, no external build step.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.getDashboardHTML = getDashboardHTML;
6
+ function getDashboardHTML() {
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8"/>
11
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
12
+ <title>Aiden Dashboard</title>
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ <style>
15
+ :root {
16
+ --bg: #0D0D0D;
17
+ --surface: #1A1A1A;
18
+ --surface2: #222222;
19
+ --border: #2A2A2A;
20
+ --orange: #FF6B35;
21
+ --orange-dim: #c0521e;
22
+ --text: #E8E8E8;
23
+ --text-dim: #888;
24
+ --green: #4CAF50;
25
+ --red: #F44336;
26
+ --yellow: #FFC107;
27
+ --radius: 8px;
28
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ }
30
+ *{box-sizing:border-box;margin:0;padding:0}
31
+ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;height:100vh;display:flex;flex-direction:column;overflow:hidden}
32
+
33
+ /* ── Top bar ── */
34
+ #topbar{display:flex;align-items:center;gap:12px;padding:0 20px;height:48px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}
35
+ #logo{font-weight:700;font-size:16px;color:var(--orange);letter-spacing:.5px}
36
+ #status-dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .4s}
37
+ #status-dot.ok{background:var(--green)}
38
+ #status-text{color:var(--text-dim);font-size:12px}
39
+ #topbar-right{margin-left:auto;display:flex;gap:8px;align-items:center}
40
+ #version-label{color:var(--text-dim);font-size:11px}
41
+
42
+ /* ── Tab bar ── */
43
+ #tabbar{display:flex;gap:2px;padding:8px 20px 0;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}
44
+ .tab{padding:8px 18px;border-radius:6px 6px 0 0;cursor:pointer;font-size:13px;font-weight:500;color:var(--text-dim);border:1px solid transparent;border-bottom:none;background:transparent;transition:all .15s}
45
+ .tab:hover{color:var(--text);background:var(--surface2)}
46
+ .tab.active{color:var(--orange);background:var(--bg);border-color:var(--border)}
47
+
48
+ /* ── Main content ── */
49
+ #panels{flex:1;overflow:hidden;display:flex;flex-direction:column}
50
+ .panel{display:none;flex:1;overflow:hidden;flex-direction:column}
51
+ .panel.active{display:flex}
52
+
53
+ /* ── Shared card ── */
54
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
55
+ .card-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin-bottom:10px}
56
+
57
+ /* ── Scrollable inner ── */
58
+ .scroll{overflow-y:auto;flex:1;padding:16px 20px}
59
+ .scroll::-webkit-scrollbar{width:6px}
60
+ .scroll::-webkit-scrollbar-track{background:transparent}
61
+ .scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
62
+
63
+ /* ─────────────────── CHAT ─────────────────── */
64
+ #chat-wrap{display:flex;flex:1;flex-direction:column;overflow:hidden}
65
+ #chat-messages{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:10px}
66
+ #chat-messages::-webkit-scrollbar{width:6px}
67
+ #chat-messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
68
+ .msg{max-width:80%;border-radius:10px;padding:10px 14px;line-height:1.55;font-size:13.5px}
69
+ .msg.user{align-self:flex-end;background:var(--orange);color:#fff;border-bottom-right-radius:2px}
70
+ .msg.aiden{align-self:flex-start;background:var(--surface);border:1px solid var(--border);color:var(--text);border-bottom-left-radius:2px}
71
+ .msg.aiden pre{background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:8px 10px;overflow-x:auto;font-size:12px;margin-top:8px}
72
+ .msg.aiden code{font-size:12px;background:var(--bg);padding:1px 4px;border-radius:3px}
73
+ .msg.system{align-self:center;color:var(--text-dim);font-size:12px;font-style:italic}
74
+ .msg.thinking{align-self:flex-start;color:var(--text-dim);font-size:12px;font-style:italic;padding:6px 10px}
75
+ #chat-input-bar{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;flex-shrink:0;background:var(--surface)}
76
+ #chat-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 14px;color:var(--text);font-size:14px;resize:none;min-height:40px;max-height:120px;line-height:1.4;font-family:var(--font)}
77
+ #chat-input:focus{outline:none;border-color:var(--orange)}
78
+ #send-btn{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;flex-shrink:0;transition:background .15s}
79
+ #send-btn:hover{background:var(--orange-dim)}
80
+ #send-btn:disabled{opacity:.5;cursor:not-allowed}
81
+ #session-bar{padding:6px 20px;font-size:11px;color:var(--text-dim);border-bottom:1px solid var(--border);display:flex;gap:16px;align-items:center;flex-shrink:0;background:var(--surface)}
82
+ .sess-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer}
83
+ .sess-btn:hover{border-color:var(--orange);color:var(--orange)}
84
+
85
+ /* ─────────────────── PROVIDERS ─────────────────── */
86
+ .provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
87
+ .pcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
88
+ .pcard-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
89
+ .pcard-name{font-weight:600;font-size:13px}
90
+ .badge{font-size:10px;padding:2px 7px;border-radius:20px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}
91
+ .badge.ok{background:#1a3a1a;color:var(--green)}
92
+ .badge.err{background:#3a1a1a;color:var(--red)}
93
+ .badge.warn{background:#3a2a00;color:var(--yellow)}
94
+ .pcard-model{font-size:11px;color:var(--text-dim);margin-top:2px}
95
+ .pcard-latency{font-size:11px;color:var(--text-dim);margin-top:4px}
96
+
97
+ /* ─────────────────── MEMORY ─────────────────── */
98
+ .mem-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start}
99
+ .mem-icon{color:var(--orange);font-size:16px;flex-shrink:0;margin-top:1px}
100
+ .mem-text{font-size:13px;line-height:1.5;color:var(--text);flex:1}
101
+ .mem-meta{font-size:10px;color:var(--text-dim);margin-top:3px}
102
+ #mem-search{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:8px 13px;color:var(--text);font-size:13px;margin-bottom:14px}
103
+ #mem-search:focus{outline:none;border-color:var(--orange)}
104
+ .mem-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin:14px 0 8px}
105
+
106
+ /* ─────────────────── SKILLS ─────────────────── */
107
+ .skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
108
+ .scard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
109
+ .scard-name{font-weight:600;font-size:13px;color:var(--orange);margin-bottom:4px}
110
+ .scard-desc{font-size:12px;color:var(--text-dim);line-height:1.4}
111
+ .scard-trigger{font-size:10px;color:var(--text-dim);margin-top:5px;font-family:monospace}
112
+
113
+ /* ─────────────────── REFRESH ─────────────────── */
114
+ .refresh-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;transition:all .15s}
115
+ .refresh-btn:hover{border-color:var(--orange);color:var(--orange)}
116
+ .panel-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 20px 0;flex-shrink:0}
117
+ .panel-toolbar h2{font-size:14px;font-weight:600;color:var(--text)}
118
+ </style>
119
+ </head>
120
+ <body>
121
+
122
+ <!-- Top bar -->
123
+ <div id="topbar">
124
+ <span id="logo">⬡ Aiden</span>
125
+ <span id="status-dot"></span>
126
+ <span id="status-text">connecting…</span>
127
+ <div id="topbar-right">
128
+ <span id="version-label"></span>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Tab bar -->
133
+ <div id="tabbar">
134
+ <div class="tab active" data-tab="chat">💬 Chat</div>
135
+ <div class="tab" data-tab="providers">⚡ Providers</div>
136
+ <div class="tab" data-tab="memory">🧠 Memory</div>
137
+ <div class="tab" data-tab="skills">🎯 Skills</div>
138
+ </div>
139
+
140
+ <!-- Panels -->
141
+ <div id="panels">
142
+
143
+ <!-- ── CHAT ── -->
144
+ <div class="panel active" id="panel-chat">
145
+ <div id="session-bar">
146
+ <span id="session-label">Session: —</span>
147
+ <button class="sess-btn" id="new-session-btn">+ New session</button>
148
+ <button class="sess-btn" id="clear-chat-btn">✕ Clear</button>
149
+ </div>
150
+ <div id="chat-wrap">
151
+ <div id="chat-messages">
152
+ <div class="msg system">Aiden is running locally. Ask anything.</div>
153
+ </div>
154
+ <div id="chat-input-bar">
155
+ <textarea id="chat-input" placeholder="Message Aiden…" rows="1"></textarea>
156
+ <button id="send-btn">Send</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- ── PROVIDERS ── -->
162
+ <div class="panel" id="panel-providers">
163
+ <div class="panel-toolbar">
164
+ <h2>LLM Providers</h2>
165
+ <button class="refresh-btn" id="refresh-providers">↻ Refresh</button>
166
+ </div>
167
+ <div class="scroll">
168
+ <div id="providers-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- ── MEMORY ── -->
173
+ <div class="panel" id="panel-memory">
174
+ <div class="panel-toolbar">
175
+ <h2>Memory</h2>
176
+ <button class="refresh-btn" id="refresh-memory">↻ Refresh</button>
177
+ </div>
178
+ <div class="scroll">
179
+ <input id="mem-search" type="text" placeholder="Search memory…" autocomplete="off"/>
180
+ <div id="memory-content"><p style="color:var(--text-dim)">Loading…</p></div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- ── SKILLS ── -->
185
+ <div class="panel" id="panel-skills">
186
+ <div class="panel-toolbar">
187
+ <h2>Skills</h2>
188
+ <button class="refresh-btn" id="refresh-skills">↻ Refresh</button>
189
+ </div>
190
+ <div class="scroll">
191
+ <div id="skills-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
192
+ </div>
193
+ </div>
194
+
195
+ </div><!-- /panels -->
196
+
197
+ <script>
198
+ // ── Config ──────────────────────────────────────────────────
199
+ const BASE = window.location.origin // e.g. http://localhost:4200
200
+
201
+ // ── Tabs ────────────────────────────────────────────────────
202
+ const tabs = document.querySelectorAll('.tab')
203
+ const panels = document.querySelectorAll('.panel')
204
+ tabs.forEach(tab => {
205
+ tab.addEventListener('click', () => {
206
+ const id = tab.dataset.tab
207
+ tabs.forEach(t => t.classList.remove('active'))
208
+ panels.forEach(p => p.classList.remove('active'))
209
+ tab.classList.add('active')
210
+ document.getElementById('panel-' + id).classList.add('active')
211
+ if (id === 'providers') loadProviders()
212
+ if (id === 'memory') loadMemory()
213
+ if (id === 'skills') loadSkills()
214
+ })
215
+ })
216
+
217
+ // ── Status bar ──────────────────────────────────────────────
218
+ async function ping() {
219
+ try {
220
+ const r = await fetch(BASE + '/api/ping', { cache: 'no-store' })
221
+ const d = await r.json()
222
+ document.getElementById('status-dot').className = 'ok'
223
+ document.getElementById('status-text').textContent = 'online'
224
+ document.getElementById('version-label').textContent = 'v' + (d.version || '')
225
+ } catch {
226
+ document.getElementById('status-dot').className = ''
227
+ document.getElementById('status-text').textContent = 'offline'
228
+ }
229
+ }
230
+ ping()
231
+ setInterval(ping, 10000)
232
+
233
+ // ── Chat ─────────────────────────────────────────────────────
234
+ let sessionId = null
235
+ const chatBox = document.getElementById('chat-messages')
236
+ const chatInput = document.getElementById('chat-input')
237
+ const sendBtn = document.getElementById('send-btn')
238
+
239
+ function addMsg(role, html) {
240
+ const el = document.createElement('div')
241
+ el.className = 'msg ' + role
242
+ el.innerHTML = html
243
+ chatBox.appendChild(el)
244
+ chatBox.scrollTop = chatBox.scrollHeight
245
+ return el
246
+ }
247
+
248
+ function mdToHtml(text) {
249
+ try { return marked.parse(text) } catch { return escHtml(text) }
250
+ }
251
+
252
+ function escHtml(s) {
253
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
254
+ }
255
+
256
+ chatInput.addEventListener('keydown', e => {
257
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() }
258
+ })
259
+
260
+ document.getElementById('send-btn').addEventListener('click', sendMessage)
261
+
262
+ document.getElementById('clear-chat-btn').addEventListener('click', () => {
263
+ chatBox.innerHTML = '<div class="msg system">Cleared.</div>'
264
+ })
265
+
266
+ document.getElementById('new-session-btn').addEventListener('click', () => {
267
+ sessionId = null
268
+ document.getElementById('session-label').textContent = 'Session: —'
269
+ chatBox.innerHTML = '<div class="msg system">New session started.</div>'
270
+ })
271
+
272
+ async function sendMessage() {
273
+ const text = chatInput.value.trim()
274
+ if (!text) return
275
+ chatInput.value = ''
276
+ chatInput.style.height = ''
277
+ sendBtn.disabled = true
278
+ addMsg('user', escHtml(text))
279
+
280
+ const thinkEl = addMsg('thinking', '⋯ thinking')
281
+ let aiEl = null
282
+ let buffer = ''
283
+
284
+ try {
285
+ const body = { message: text }
286
+ if (sessionId) body.sessionId = sessionId
287
+
288
+ const resp = await fetch(BASE + '/api/chat', {
289
+ method: 'POST',
290
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
291
+ body: JSON.stringify(body)
292
+ })
293
+
294
+ const reader = resp.body.getReader()
295
+ const dec = new TextDecoder()
296
+ let partial = ''
297
+
298
+ while (true) {
299
+ const { done, value } = await reader.read()
300
+ if (done) break
301
+ partial += dec.decode(value, { stream: true })
302
+ const lines = partial.split('\\n')
303
+ partial = lines.pop()
304
+ for (const line of lines) {
305
+ if (!line.startsWith('data:')) continue
306
+ const raw = line.slice(5).trim()
307
+ if (!raw || raw === '[DONE]') continue
308
+ let ev
309
+ try { ev = JSON.parse(raw) } catch { continue }
310
+
311
+ if (ev.sessionId && !sessionId) {
312
+ sessionId = ev.sessionId
313
+ document.getElementById('session-label').textContent = 'Session: ' + sessionId.slice(0,8)
314
+ }
315
+ // server sends { token, done: false } — no type field
316
+ if (ev.token || ev.delta) {
317
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
318
+ if (!aiEl) aiEl = addMsg('aiden', '')
319
+ buffer += (ev.delta || ev.token || '')
320
+ aiEl.innerHTML = mdToHtml(buffer)
321
+ chatBox.scrollTop = chatBox.scrollHeight
322
+ }
323
+ // server sends { tool: "name", message: "...", timestamp } for tool progress
324
+ if (ev.tool) {
325
+ if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '🔧 ' + escHtml(ev.tool)
326
+ }
327
+ // server sends { activity: { icon, message, style, rawTool? }, done: false }
328
+ if (ev.activity) {
329
+ const act = ev.activity
330
+ if (thinkEl && thinkEl.parentNode) {
331
+ thinkEl.innerHTML = (act.icon ? act.icon + ' ' : '🔧 ') + escHtml(act.message || act.rawTool || 'working…')
332
+ }
333
+ if (act.style === 'error' && !buffer) {
334
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
335
+ addMsg('system', '⚠ ' + escHtml(act.message || 'Error'))
336
+ }
337
+ }
338
+ // server sends { thinking: { stage, message } }
339
+ if (ev.thinking) {
340
+ if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '⋯ ' + escHtml(ev.thinking.message || 'thinking…')
341
+ }
342
+ // server sends { done: true } — no type field
343
+ if (ev.done === true) {
344
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
345
+ }
346
+ }
347
+ }
348
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
349
+ if (!aiEl && !buffer) addMsg('system', '(no response)')
350
+ } catch (err) {
351
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
352
+ addMsg('system', '⚠ ' + escHtml(err.message))
353
+ } finally {
354
+ sendBtn.disabled = false
355
+ chatInput.focus()
356
+ }
357
+ }
358
+
359
+ // Auto-resize textarea
360
+ chatInput.addEventListener('input', () => {
361
+ chatInput.style.height = 'auto'
362
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px'
363
+ })
364
+
365
+ // ── Providers ────────────────────────────────────────────────
366
+ async function loadProviders() {
367
+ const el = document.getElementById('providers-content')
368
+ try {
369
+ const [stateRes, statusRes] = await Promise.all([
370
+ fetch(BASE + '/api/providers/state').then(r => r.json()),
371
+ fetch(BASE + '/api/providers/status').then(r => r.json()).catch(() => ({}))
372
+ ])
373
+
374
+ const providers = Array.isArray(stateRes) ? stateRes
375
+ : (stateRes.providers || Object.entries(stateRes).map(([k,v]) => ({ name: k, ...v })))
376
+
377
+ if (!providers.length) { el.innerHTML = '<p style="color:var(--text-dim)">No providers configured.</p>'; return }
378
+
379
+ const statusMap = {}
380
+ if (statusRes && typeof statusRes === 'object') {
381
+ const arr = Array.isArray(statusRes) ? statusRes : Object.entries(statusRes).map(([k,v])=>({name:k,...v}))
382
+ arr.forEach(p => { statusMap[p.name || p.id] = p })
383
+ }
384
+
385
+ el.innerHTML = '<div class="provider-grid">' + providers.map(p => {
386
+ const name = p.name || p.id || '?'
387
+ const status = p.status || statusMap[name]?.status || 'unknown'
388
+ const badgeCls = status === 'ok' || status === 'healthy' || status === 'active' ? 'ok'
389
+ : status === 'error' || status === 'down' ? 'err' : 'warn'
390
+ const model = p.currentModel || p.model || p.defaultModel || ''
391
+ const latency = p.latency || p.avgLatency || statusMap[name]?.latency || ''
392
+ return \`<div class="pcard">
393
+ <div class="pcard-header">
394
+ <span class="pcard-name">\${escHtml(name)}</span>
395
+ <span class="badge \${badgeCls}">\${escHtml(status)}</span>
396
+ </div>
397
+ \${model ? \`<div class="pcard-model">Model: \${escHtml(model)}</div>\` : ''}
398
+ \${latency ? \`<div class="pcard-latency">Latency: \${typeof latency==='number' ? latency+'ms' : escHtml(String(latency))}</div>\` : ''}
399
+ </div>\`
400
+ }).join('') + '</div>'
401
+ } catch (err) {
402
+ el.innerHTML = '<p style="color:var(--red)">Failed to load providers: ' + escHtml(err.message) + '</p>'
403
+ }
404
+ }
405
+ document.getElementById('refresh-providers').addEventListener('click', loadProviders)
406
+
407
+ // ── Memory ───────────────────────────────────────────────────
408
+ let allMemories = []
409
+
410
+ async function loadMemory() {
411
+ const el = document.getElementById('memory-content')
412
+ try {
413
+ const data = await fetch(BASE + '/api/memory').then(r => r.json())
414
+ allMemories = Array.isArray(data) ? data : (data.memories || data.items || [])
415
+ renderMemory(allMemories)
416
+ } catch (err) {
417
+ el.innerHTML = '<p style="color:var(--red)">Failed to load memory: ' + escHtml(err.message) + '</p>'
418
+ }
419
+ }
420
+
421
+ function renderMemory(items) {
422
+ const el = document.getElementById('memory-content')
423
+ if (!items.length) { el.innerHTML = '<p style="color:var(--text-dim)">No memories stored yet.</p>'; return }
424
+ el.innerHTML = items.map(m => {
425
+ const text = m.content || m.text || m.value || JSON.stringify(m)
426
+ const meta = [m.type, m.category, m.created_at || m.createdAt].filter(Boolean).join(' · ')
427
+ return \`<div class="mem-item">
428
+ <span class="mem-icon">◈</span>
429
+ <div>
430
+ <div class="mem-text">\${escHtml(String(text))}</div>
431
+ \${meta ? \`<div class="mem-meta">\${escHtml(meta)}</div>\` : ''}
432
+ </div>
433
+ </div>\`
434
+ }).join('')
435
+ }
436
+
437
+ let memSearchTimer = null
438
+ document.getElementById('mem-search').addEventListener('input', e => {
439
+ clearTimeout(memSearchTimer)
440
+ const q = e.target.value.trim()
441
+ memSearchTimer = setTimeout(async () => {
442
+ if (!q) { renderMemory(allMemories); return }
443
+ try {
444
+ const data = await fetch(BASE + '/api/memory/search?q=' + encodeURIComponent(q)).then(r => r.json())
445
+ const results = Array.isArray(data) ? data : (data.results || data.memories || [])
446
+ renderMemory(results)
447
+ } catch { renderMemory(allMemories.filter(m => JSON.stringify(m).toLowerCase().includes(q.toLowerCase()))) }
448
+ }, 300)
449
+ })
450
+ document.getElementById('refresh-memory').addEventListener('click', loadMemory)
451
+
452
+ // ── Skills ───────────────────────────────────────────────────
453
+ async function loadSkills() {
454
+ const el = document.getElementById('skills-content')
455
+ try {
456
+ const data = await fetch(BASE + '/api/skills/learned').then(r => r.json())
457
+ const skills = Array.isArray(data) ? data : (data.skills || data.items || [])
458
+ if (!skills.length) { el.innerHTML = '<p style="color:var(--text-dim)">No skills loaded.</p>'; return }
459
+ el.innerHTML = '<div class="skill-grid">' + skills.map(s => {
460
+ const name = s.name || s.id || '?'
461
+ const desc = s.description || s.desc || ''
462
+ const trigger = s.trigger || s.command || ''
463
+ return \`<div class="scard">
464
+ <div class="scard-name">\${escHtml(name)}</div>
465
+ \${desc ? \`<div class="scard-desc">\${escHtml(desc)}</div>\` : ''}
466
+ \${trigger ? \`<div class="scard-trigger">trigger: \${escHtml(trigger)}</div>\` : ''}
467
+ </div>\`
468
+ }).join('') + '</div>'
469
+ } catch (err) {
470
+ el.innerHTML = '<p style="color:var(--red)">Failed to load skills: ' + escHtml(err.message) + '</p>'
471
+ }
472
+ }
473
+ document.getElementById('refresh-skills').addEventListener('click', loadSkills)
474
+
475
+ // ── Init ─────────────────────────────────────────────────────
476
+ chatInput.focus()
477
+ </script>
478
+ </body>
479
+ </html>`;
480
+ }
@@ -124,6 +124,7 @@ const costTracker_1 = require("../core/costTracker");
124
124
  const sessionMemory_1 = require("../core/sessionMemory");
125
125
  const memoryExtractor_1 = require("../core/memoryExtractor");
126
126
  const pluginSystem_1 = require("../core/pluginSystem");
127
+ const pluginLoader_1 = require("../core/pluginLoader");
127
128
  const aidenIdentity_1 = require("../core/aidenIdentity");
128
129
  const eventBus_1 = require("../core/eventBus");
129
130
  const workflowTracker_1 = require("../core/workflowTracker");
@@ -150,6 +151,7 @@ const signal_1 = require("../core/channels/signal");
150
151
  const twilio_1 = require("../core/channels/twilio");
151
152
  const imessage_1 = require("../core/channels/imessage");
152
153
  const email_1 = require("../core/channels/email");
154
+ const dashboard_1 = require("./dashboard");
153
155
  // —— Sprint 25: module-level WebSocket clients registry (shared between createApiServer routes and startApiServer WS setup)
154
156
  let wsBroadcastClients = new Set();
155
157
  let activeTelegramBot = null;
@@ -542,6 +544,15 @@ function createApiServer() {
542
544
  next();
543
545
  });
544
546
  // ── Core routes ──────────────────────────────────────────────
547
+ // GET /ui — local web dashboard
548
+ app.get('/ui', (_req, res) => {
549
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
550
+ res.send((0, dashboard_1.getDashboardHTML)());
551
+ });
552
+ // GET /api/ping — lightweight status probe for dashboard
553
+ app.get('/api/ping', (_req, res) => {
554
+ res.json({ ok: true, version: version_1.VERSION, ts: Date.now() });
555
+ });
545
556
  // GET /api/health — liveness probe (no auth required)
546
557
  app.get('/api/health', (_req, res) => {
547
558
  res.json({ status: 'ok', version: version_1.VERSION, timestamp: new Date().toISOString() });
@@ -1380,6 +1391,10 @@ function createApiServer() {
1380
1391
  // ── Callback system — additive layer alongside existing SSE sends ──
1381
1392
  const sid = sessionId || 'default';
1382
1393
  callbackSystem_1.callbacks.emit('session_start', sid, { message }).catch(() => { });
1394
+ // Fire flat-plugin session hooks
1395
+ for (const fn of pluginLoader_1.pluginHooks.onSessionStart) {
1396
+ fn(sid, { message }).catch(() => { });
1397
+ }
1383
1398
  // Forward callback events from other sessions to this SSE connection.
1384
1399
  // The sessionId guard prevents re-sending this session's own emitted events.
1385
1400
  const unsubscribeSSE = callbackSystem_1.callbacks.onAny((payload) => {
@@ -1393,6 +1408,9 @@ function createApiServer() {
1393
1408
  (0, toolRegistry_1.setProgressEmitter)(null);
1394
1409
  unsubscribeSSE();
1395
1410
  callbackSystem_1.callbacks.emit('session_end', sid, {}).catch(() => { });
1411
+ for (const fn of pluginLoader_1.pluginHooks.onSessionEnd) {
1412
+ fn(sid, {}).catch(() => { });
1413
+ }
1396
1414
  (0, memoryDistiller_1.distillSession)(sid).catch(() => { });
1397
1415
  });
1398
1416
  // Sprint 6: tiered model selection
@@ -2661,7 +2679,7 @@ function createApiServer() {
2661
2679
  res.status(500).json({ error: err.message });
2662
2680
  }
2663
2681
  });
2664
- // GET /api/plugins — list loaded community plugins
2682
+ // GET /api/plugins — list loaded community plugins (subdirectory format)
2665
2683
  app.get('/api/plugins', (_req, res) => {
2666
2684
  try {
2667
2685
  res.json({ plugins: pluginSystem_1.pluginManager.list() });
@@ -2670,6 +2688,29 @@ function createApiServer() {
2670
2688
  res.status(500).json({ error: e.message });
2671
2689
  }
2672
2690
  });
2691
+ // GET /api/plugins/list — list all loaded plugins (subdirectory + flat)
2692
+ app.get('/api/plugins/list', (_req, res) => {
2693
+ try {
2694
+ res.json({
2695
+ subdirectory: pluginSystem_1.pluginManager.list(),
2696
+ flat: (0, pluginLoader_1.listFlatPlugins)(),
2697
+ });
2698
+ }
2699
+ catch (e) {
2700
+ res.status(500).json({ error: e.message });
2701
+ }
2702
+ });
2703
+ // POST /api/plugins/reload — hot-reload all flat .js plugins
2704
+ app.post('/api/plugins/reload', async (_req, res) => {
2705
+ try {
2706
+ const dir = path.join(process.cwd(), 'workspace', 'plugins');
2707
+ await (0, pluginLoader_1.reloadPlugins)(dir);
2708
+ res.json({ ok: true, plugins: (0, pluginLoader_1.listFlatPlugins)() });
2709
+ }
2710
+ catch (e) {
2711
+ res.status(500).json({ error: e.message });
2712
+ }
2713
+ });
2673
2714
  // GET /api/telegram/config — load Telegram bot config
2674
2715
  app.get('/api/telegram/config', (_req, res) => {
2675
2716
  try {
@@ -5851,6 +5892,9 @@ function startApiServer(portArg) {
5851
5892
  }
5852
5893
  // Load community plugins from workspace/plugins/
5853
5894
  pluginSystem_1.pluginManager.loadAll().catch(e => console.error('[Plugins] Load failed:', e.message));
5895
+ // Load flat .js plugins from workspace/plugins/*.js
5896
+ const flatPluginDir = path.join(process.cwd(), 'workspace', 'plugins');
5897
+ (0, pluginLoader_1.loadPlugins)(flatPluginDir).catch(e => console.error('[PluginLoader] Load failed:', e.message));
5854
5898
  // Start background license refresh (12-hour interval, silent)
5855
5899
  (0, licenseManager_1.startLicenseRefresh)();
5856
5900
  // Log provider chain before listening so it's visible in startup log
@@ -80,6 +80,7 @@ const semanticMemory_1 = require("./semanticMemory");
80
80
  const sessionMemory_1 = require("./sessionMemory");
81
81
  const goalTracker_1 = require("./goalTracker");
82
82
  const hooks_1 = require("./hooks");
83
+ const pluginLoader_1 = require("./pluginLoader");
83
84
  const instinctSystem_1 = require("./instinctSystem");
84
85
  const workflowTracker_1 = require("./workflowTracker");
85
86
  const parallelExecutor_1 = require("./parallelExecutor");
@@ -1667,11 +1668,25 @@ const NO_RETRY_TOOLS = new Set([
1667
1668
  async function executeToolWithRetry(tool, input, maxRetries = 2) {
1668
1669
  const retryable = !NO_RETRY_TOOLS.has(tool);
1669
1670
  const effectiveMax = retryable ? maxRetries : 0;
1671
+ // ── Plugin preTool hooks ──────────────────────────────────────
1672
+ let effectiveInput = input;
1673
+ for (const hook of pluginLoader_1.pluginHooks.preTool) {
1674
+ try {
1675
+ const r = await hook(tool, effectiveInput);
1676
+ if (r.skip)
1677
+ return { success: true, output: '[skipped by plugin]', skippedByPlugin: true };
1678
+ if (r.input)
1679
+ effectiveInput = r.input;
1680
+ }
1681
+ catch (e) {
1682
+ console.warn(`[PluginHook] preTool error for ${tool}:`, e.message);
1683
+ }
1684
+ }
1670
1685
  for (let attempt = 0; attempt <= effectiveMax; attempt++) {
1671
1686
  try {
1672
- const result = await (0, toolRegistry_1.executeTool)(tool, input);
1687
+ const result = await (0, toolRegistry_1.executeTool)(tool, effectiveInput);
1673
1688
  if (result.success) {
1674
- const quality = validateResultQuality(tool, input, result.output || result);
1689
+ const quality = validateResultQuality(tool, effectiveInput, result.output || result);
1675
1690
  if (!quality.valid) {
1676
1691
  console.log(`[Quality] ${tool} returned but quality check failed: ${quality.reason}`);
1677
1692
  if (attempt < effectiveMax) {
@@ -1683,7 +1698,19 @@ async function executeToolWithRetry(tool, input, maxRetries = 2) {
1683
1698
  console.log(`[Quality] ${tool} — accepting low-quality result after ${effectiveMax} retries`);
1684
1699
  appendLesson(`${tool} produced low-quality output (${quality.reason}) after ${effectiveMax} retries — consider alternative approach for this tool.`);
1685
1700
  }
1686
- return result;
1701
+ // ── Plugin postTool hooks ─────────────────────────────
1702
+ let finalResult = result;
1703
+ for (const hook of pluginLoader_1.pluginHooks.postTool) {
1704
+ try {
1705
+ const r = await hook(tool, effectiveInput, finalResult);
1706
+ if (r.result)
1707
+ finalResult = r.result;
1708
+ }
1709
+ catch (e) {
1710
+ console.warn(`[PluginHook] postTool error for ${tool}:`, e.message);
1711
+ }
1712
+ }
1713
+ return finalResult;
1687
1714
  }
1688
1715
  if (attempt < effectiveMax) {
1689
1716
  const delay = Math.min(1000 * Math.pow(2, attempt), 5000);