@rynfar/meridian 1.27.6 → 1.29.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.
@@ -2,5 +2,5 @@
2
2
  * Inline HTML dashboard for telemetry.
3
3
  * No framework, no build step, no CDN. Single self-contained page.
4
4
  */
5
- export declare const dashboardHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Meridian \u2014 Telemetry</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n :root {\n --bg: #0d1117; --surface: #161b22; --border: #30363d;\n --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;\n --green: #3fb950; --yellow: #d29922; --red: #f85149;\n --blue: #58a6ff; --purple: #bc8cff;\n --queue: #d29922; --ttfb: #58a6ff; --upstream: #3fb950; --total: #bc8cff;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n background: var(--bg); color: var(--text); padding: 24px; line-height: 1.5; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }\n .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }\n .card-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }\n .card-value { font-size: 28px; font-weight: 600; margin-top: 4px; font-variant-numeric: tabular-nums; }\n .card-detail { font-size: 12px; color: var(--muted); margin-top: 2px; }\n .section { margin-bottom: 24px; }\n .section-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--muted);\n text-transform: uppercase; letter-spacing: 0.5px; }\n table { width: 100%; border-collapse: collapse; background: var(--surface);\n border: 1px solid var(--border); border-radius: 8px; overflow: hidden; font-size: 13px; }\n th { text-align: left; padding: 10px 12px; background: var(--bg); color: var(--muted);\n font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }\n td { padding: 8px 12px; border-top: 1px solid var(--border); font-variant-numeric: tabular-nums; }\n tr:hover td { background: rgba(88,166,255,0.04); }\n .waterfall { display: flex; align-items: center; height: 18px; min-width: 200px; position: relative; }\n .waterfall-seg { height: 100%; border-radius: 2px; min-width: 2px; }\n .waterfall-seg.queue { background: var(--queue); }\n .waterfall-seg.overhead { background: var(--yellow); }\n .waterfall-seg.ttfb { background: var(--ttfb); }\n .waterfall-seg.response { background: var(--upstream); }\n .legend { display: flex; gap: 16px; margin-bottom: 12px; font-size: 12px; color: var(--muted); }\n .legend-dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; margin-right: 4px; vertical-align: middle; }\n .status-ok { color: var(--green); }\n .status-err { color: var(--red); }\n .pct-table td:first-child { font-weight: 500; }\n .pct-table .phase-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; }\n .mono { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; }\n .refresh-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }\n .refresh-bar select, .refresh-bar button {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer;\n }\n .refresh-bar button:hover { border-color: var(--accent); }\n .refresh-indicator { font-size: 11px; color: var(--muted); }\n .empty { text-align: center; padding: 48px; color: var(--muted); }\n\n /* Tabs */\n .tabs { display: flex; gap: 0; margin-bottom: 20px; border-bottom: 1px solid var(--border); }\n .tab { padding: 10px 20px; font-size: 13px; font-weight: 500; color: var(--muted); cursor: pointer;\n border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s, border-color 0.15s;\n user-select: none; }\n .tab:hover { color: var(--text); }\n .tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n .tab-badge { font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-left: 6px;\n background: var(--border); color: var(--muted); font-variant-numeric: tabular-nums; }\n .tab.active .tab-badge { background: rgba(88,166,255,0.15); color: var(--accent); }\n .tab-panel { display: none; }\n .tab-panel.active { display: block; }\n\n /* Log filters */\n .log-filters { display: flex; gap: 8px; margin-bottom: 12px; }\n .log-filter { font-size: 11px; padding: 3px 10px; border-radius: 12px; cursor: pointer;\n border: 1px solid var(--border); background: var(--surface); color: var(--muted);\n transition: all 0.15s; }\n .log-filter:hover { border-color: var(--accent); color: var(--text); }\n .log-filter.active { background: rgba(88,166,255,0.1); border-color: var(--accent); color: var(--accent); }\n</style>\n</head>\n<body>\n<h1>Meridian</h1>\n<div class=\"subtitle\">Request Performance Telemetry</div>\n\n<div class=\"refresh-bar\">\n <select id=\"window\">\n <option value=\"300000\">Last 5 min</option>\n <option value=\"900000\">Last 15 min</option>\n <option value=\"3600000\" selected>Last 1 hour</option>\n <option value=\"86400000\">Last 24 hours</option>\n </select>\n <button onclick=\"refresh()\">Refresh</button>\n <label><input type=\"checkbox\" id=\"autoRefresh\" checked> Auto (5s)</label>\n <span class=\"refresh-indicator\" id=\"lastUpdate\"></span>\n</div>\n\n<div id=\"content\"><div class=\"empty\">Loading\u2026</div></div>\n\n<script>\nconst $ = s => document.querySelector(s);\nconst $$ = s => document.querySelectorAll(s);\nlet timer;\nlet activeTab = 'requests';\nlet activeLogFilter = 'all';\n\nfunction ms(v) {\n if (v == null) return '\u2014';\n if (v < 1000) return v + 'ms';\n return (v / 1000).toFixed(1) + 's';\n}\n\nfunction ago(ts) {\n const s = Math.floor((Date.now() - ts) / 1000);\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.floor(s/60) + 'm ago';\n return Math.floor(s/3600) + 'h ago';\n}\n\nfunction pctRow(label, color, phase) {\n return '<tr>'\n + '<td><span class=\"phase-dot\" style=\"background:' + color + '\"></span>' + label + '</td>'\n + '<td class=\"mono\">' + ms(phase.p50) + '</td>'\n + '<td class=\"mono\">' + ms(phase.p95) + '</td>'\n + '<td class=\"mono\">' + ms(phase.p99) + '</td>'\n + '<td class=\"mono\">' + ms(phase.min) + '</td>'\n + '<td class=\"mono\">' + ms(phase.max) + '</td>'\n + '<td class=\"mono\">' + ms(phase.avg) + '</td>'\n + '</tr>';\n}\n\nfunction switchTab(tab) {\n activeTab = tab;\n $$('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));\n $$('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + tab));\n}\n\nfunction setLogFilter(filter) {\n activeLogFilter = filter;\n $$('.log-filter').forEach(f => f.classList.toggle('active', f.dataset.filter === filter));\n $$('.log-row').forEach(r => {\n r.style.display = (filter === 'all' || r.dataset.category === filter) ? '' : 'none';\n });\n}\n\nasync function refresh() {\n const w = $('#window').value;\n try {\n const [summary, reqs, logs] = await Promise.all([\n fetch('/telemetry/summary?window=' + w).then(r => r.json()),\n fetch('/telemetry/requests?limit=50&since=' + (Date.now() - Number(w))).then(r => r.json()),\n fetch('/telemetry/logs?limit=200&since=' + (Date.now() - Number(w))).then(r => r.json()),\n ]);\n render(summary, reqs, logs);\n $('#lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString();\n } catch (e) {\n $('#content').innerHTML = '<div class=\"empty\">Failed to load telemetry</div>';\n }\n}\n\nfunction render(s, reqs, logs) {\n if (s.totalRequests === 0 && (!logs || logs.length === 0)) {\n $('#content').innerHTML = '<div class=\"empty\">No requests recorded yet. Send a request through the proxy to see telemetry.</div>';\n return;\n }\n\n // Count lineage types for badges\n const lineageCounts = {};\n for (const r of reqs) { const t = r.lineageType || 'unknown'; lineageCounts[t] = (lineageCounts[t] || 0) + 1; }\n const logCounts = { session: 0, lineage: 0, error: 0 };\n for (const l of logs) { if (logCounts[l.category] !== undefined) logCounts[l.category]++; }\n\n // Tabs\n let html = '<div class=\"tabs\">'\n + '<div class=\"tab' + (activeTab === 'overview' ? ' active' : '') + '\" data-tab=\"overview\" onclick=\"switchTab(&apos;overview&apos;)\">Overview</div>'\n + '<div class=\"tab' + (activeTab === 'requests' ? ' active' : '') + '\" data-tab=\"requests\" onclick=\"switchTab(&apos;requests&apos;)\">'\n + 'Requests<span class=\"tab-badge\">' + reqs.length + '</span></div>'\n + '<div class=\"tab' + (activeTab === 'logs' ? ' active' : '') + '\" data-tab=\"logs\" onclick=\"switchTab(&apos;logs&apos;)\">'\n + 'Logs<span class=\"tab-badge\">' + logs.length + '</span></div>'\n + '</div>';\n\n // ==================== Overview tab ====================\n html += '<div id=\"panel-overview\" class=\"tab-panel' + (activeTab === 'overview' ? ' active' : '') + '\">';\n\n // Summary cards\n html += '<div class=\"cards\">'\n + card('Requests', s.totalRequests, s.requestsPerMinute.toFixed(1) + ' req/min')\n + card('Errors', s.errorCount, s.totalRequests > 0 ? ((s.errorCount/s.totalRequests)*100).toFixed(1) + '% error rate' : '')\n + card('Median Total', ms(s.totalDuration.p50), 'p95: ' + ms(s.totalDuration.p95))\n + card('Median TTFB', ms(s.ttfb.p50), 'p95: ' + ms(s.ttfb.p95))\n + card('Proxy Overhead', ms(s.proxyOverhead.p50), 'p95: ' + ms(s.proxyOverhead.p95))\n + card('Queue Wait', ms(s.queueWait.p50), 'p95: ' + ms(s.queueWait.p95))\n + '</div>';\n\n // Model breakdown\n const models = Object.entries(s.byModel);\n if (models.length > 0) {\n html += '<div class=\"cards\">';\n for (const [name, data] of models) {\n html += card(name, data.count + ' reqs', 'avg ' + ms(data.avgTotalMs));\n }\n html += '</div>';\n }\n\n // Lineage breakdown\n if (Object.keys(lineageCounts).length > 0) {\n html += '<div class=\"cards\">';\n const lineageColors = {continuation:'var(--green)',compaction:'var(--yellow)',undo:'var(--purple)',diverged:'var(--red)',new:'var(--muted)'};\n for (const [type, count] of Object.entries(lineageCounts)) {\n html += '<div class=\"card\"><div class=\"card-label\">Lineage: ' + type + '</div>'\n + '<div class=\"card-value\" style=\"color:' + (lineageColors[type] || 'var(--text)') + '\">' + count + '</div></div>';\n }\n html += '</div>';\n }\n\n // Percentile table\n html += '<div class=\"section\"><div class=\"section-title\">Percentiles</div>'\n + '<table class=\"pct-table\"><thead><tr><th>Phase</th><th>p50</th><th>p95</th><th>p99</th><th>Min</th><th>Max</th><th>Avg</th></tr></thead><tbody>'\n + pctRow('Queue Wait', 'var(--queue)', s.queueWait)\n + pctRow('Proxy Overhead', 'var(--yellow)', s.proxyOverhead)\n + pctRow('TTFB', 'var(--ttfb)', s.ttfb)\n + pctRow('Upstream', 'var(--upstream)', s.upstreamDuration)\n + pctRow('Total', 'var(--purple)', s.totalDuration)\n + '</tbody></table></div>';\n\n html += '</div>'; // end overview panel\n\n // ==================== Requests tab ====================\n html += '<div id=\"panel-requests\" class=\"tab-panel' + (activeTab === 'requests' ? ' active' : '') + '\">';\n\n html += '<div class=\"legend\">'\n + '<span><span class=\"legend-dot\" style=\"background:var(--queue)\"></span>Queue</span>'\n + '<span><span class=\"legend-dot\" style=\"background:var(--yellow)\"></span>Proxy</span>'\n + '<span><span class=\"legend-dot\" style=\"background:var(--ttfb)\"></span>TTFB</span>'\n + '<span><span class=\"legend-dot\" style=\"background:var(--upstream)\"></span>Response</span>'\n + '</div>'\n + '<table><thead><tr><th>Time</th><th>Adapter</th><th>Model</th><th>Mode</th><th>Session</th><th>Status</th>'\n + '<th>Queue</th><th>Proxy</th><th>TTFB</th><th>Total</th><th>Waterfall</th></tr></thead><tbody>';\n\n const maxTotal = Math.max(...reqs.map(r => r.totalDurationMs), 1);\n\n for (const r of reqs) {\n const statusClass = r.error ? 'status-err' : 'status-ok';\n const statusText = r.error ? r.error : r.status;\n const scale = 280 / maxTotal;\n const qW = Math.max(r.queueWaitMs * scale, 2);\n const ohW = Math.max((r.proxyOverheadMs || 0) * scale, 0);\n const ttfbW = Math.max((r.ttfbMs || 0) * scale, 0);\n const respW = Math.max((r.upstreamDurationMs - (r.ttfbMs || 0)) * scale, 2);\n\n const lineageBadge = r.lineageType ? '<span style=\"font-size:10px;padding:1px 5px;border-radius:3px;background:' + ({continuation:'var(--green)',compaction:'var(--yellow)',undo:'var(--purple)',diverged:'var(--red)',new:'var(--muted)'}[r.lineageType] || 'var(--muted)') + ';color:var(--bg)\">' + r.lineageType + '</span>' : '';\n const sessionShort = r.sdkSessionId ? r.sdkSessionId.slice(0, 8) : '\u2014';\n const msgCount = r.messageCount != null ? r.messageCount : '?';\n\n html += '<tr>'\n + '<td class=\"mono\">' + ago(r.timestamp) + '</td>'\n + '<td>' + (r.adapter || '\u2014') + '</td>'\n + '<td>' + (r.requestModel || r.model) + '<br><span style=\"font-size:10px;color:var(--muted)\">' + r.model + '</span></td>'\n + '<td>' + r.mode + '</td>'\n + '<td class=\"mono\">' + sessionShort + ' ' + lineageBadge + '<br><span style=\"font-size:10px;color:var(--muted)\">' + msgCount + ' msgs</span></td>'\n + '<td class=\"' + statusClass + '\">' + statusText + '</td>'\n + '<td class=\"mono\">' + ms(r.queueWaitMs) + '</td>'\n + '<td class=\"mono\">' + ms(r.proxyOverheadMs) + '</td>'\n + '<td class=\"mono\">' + ms(r.ttfbMs) + '</td>'\n + '<td class=\"mono\">' + ms(r.totalDurationMs) + '</td>'\n + '<td><div class=\"waterfall\">'\n + '<div class=\"waterfall-seg queue\" style=\"width:' + qW + 'px\"></div>'\n + '<div class=\"waterfall-seg overhead\" style=\"width:' + ohW + 'px\"></div>'\n + '<div class=\"waterfall-seg ttfb\" style=\"width:' + ttfbW + 'px\"></div>'\n + '<div class=\"waterfall-seg response\" style=\"width:' + respW + 'px\"></div>'\n + '</div></td>'\n + '</tr>';\n }\n html += '</tbody></table>';\n html += '</div>'; // end requests panel\n\n // ==================== Logs tab ====================\n html += '<div id=\"panel-logs\" class=\"tab-panel' + (activeTab === 'logs' ? ' active' : '') + '\">';\n\n // Filter buttons\n html += '<div class=\"log-filters\">'\n + '<span class=\"log-filter' + (activeLogFilter === 'all' ? ' active' : '') + '\" data-filter=\"all\" onclick=\"setLogFilter(&apos;all&apos;)\">All<span class=\"tab-badge\">' + logs.length + '</span></span>'\n + '<span class=\"log-filter' + (activeLogFilter === 'session' ? ' active' : '') + '\" data-filter=\"session\" onclick=\"setLogFilter(&apos;session&apos;)\" style=\"--accent:var(--blue)\">Session<span class=\"tab-badge\">' + logCounts.session + '</span></span>'\n + '<span class=\"log-filter' + (activeLogFilter === 'lineage' ? ' active' : '') + '\" data-filter=\"lineage\" onclick=\"setLogFilter(&apos;lineage&apos;)\" style=\"--accent:var(--purple)\">Lineage<span class=\"tab-badge\">' + logCounts.lineage + '</span></span>'\n + '<span class=\"log-filter' + (activeLogFilter === 'error' ? ' active' : '') + '\" data-filter=\"error\" onclick=\"setLogFilter(&apos;error&apos;)\" style=\"--accent:var(--red)\">Error<span class=\"tab-badge\">' + logCounts.error + '</span></span>'\n + '</div>';\n\n if (logs.length === 0) {\n html += '<div class=\"empty\">No diagnostic logs in this time window.</div>';\n } else {\n html += '<table><thead><tr>'\n + '<th style=\"width:80px\">Time</th><th style=\"width:55px\">Level</th><th style=\"width:70px\">Category</th><th>Message</th>'\n + '</tr></thead><tbody>';\n\n for (const log of logs) {\n const levelColor = {info:'var(--green)',warn:'var(--yellow)',error:'var(--red)'}[log.level] || 'var(--muted)';\n const catColor = {session:'var(--blue)',lineage:'var(--purple)',error:'var(--red)',lifecycle:'var(--muted)'}[log.category] || 'var(--muted)';\n const display = (activeLogFilter === 'all' || log.category === activeLogFilter) ? '' : 'display:none';\n html += '<tr class=\"log-row\" data-category=\"' + log.category + '\" style=\"' + display + '\">'\n + '<td class=\"mono\">' + ago(log.timestamp) + '</td>'\n + '<td><span style=\"color:' + levelColor + '\">' + log.level + '</span></td>'\n + '<td><span style=\"color:' + catColor + '\">' + log.category + '</span></td>'\n + '<td class=\"mono\" style=\"word-break:break-all\">' + log.message + '</td>'\n + '</tr>';\n }\n html += '</tbody></table>';\n }\n html += '</div>'; // end logs panel\n\n $('#content').innerHTML = html;\n}\n\nfunction card(label, value, detail) {\n return '<div class=\"card\"><div class=\"card-label\">' + label + '</div>'\n + '<div class=\"card-value\">' + value + '</div>'\n + (detail ? '<div class=\"card-detail\">' + detail + '</div>' : '')\n + '</div>';\n}\n\n$('#autoRefresh').addEventListener('change', function() {\n clearInterval(timer);\n if (this.checked) timer = setInterval(refresh, 5000);\n});\n$('#window').addEventListener('change', refresh);\n\nrefresh();\ntimer = setInterval(refresh, 5000);\n</script>\n</body>\n</html>";
5
+ export declare const dashboardHtml: string;
6
6
  //# sourceMappingURL=dashboard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,aAAa,gsiBAsUlB,CAAA"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QA4UlB,CAAA"}
@@ -3,5 +3,5 @@
3
3
  * Shows system status, account info, quick stats, and agent setup snippets.
4
4
  * Fetches /health and /telemetry/summary client-side for live data.
5
5
  */
6
- export declare const landingHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Meridian</title>\n<style>\n :root {\n --bg: #0f0b1a; --surface: #1a1030; --surface2: #221840; --border: #2d2545;\n --text: #e0e7ff; --muted: #8b8aa0; --accent: #8b5cf6; --accent2: #6366f1;\n --green: #3fb950; --yellow: #d29922; --red: #f85149;\n --violet: #a78bfa; --lavender: #c4b5fd;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }\n .container { max-width: 960px; margin: 0 auto; padding: 32px 24px; }\n\n .header { display: flex; align-items: center; gap: 16px; margin-bottom: 6px; }\n .header h1 { font-size: 28px; font-weight: 700; letter-spacing: 3px; }\n .tagline { color: var(--muted); font-size: 14px; margin-bottom: 32px; letter-spacing: 0.5px; }\n\n .status-banner { display: flex; align-items: center; gap: 12px; padding: 16px 20px;\n background: var(--surface); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 24px; }\n .status-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }\n .status-dot.healthy { background: var(--green); box-shadow: 0 0 8px rgba(63,185,80,0.4); }\n .status-dot.degraded { background: var(--yellow); }\n .status-dot.unhealthy { background: var(--red); }\n .status-text { font-size: 14px; font-weight: 500; }\n .status-detail { font-size: 12px; color: var(--muted); margin-left: auto; }\n\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }\n .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }\n .card-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; font-weight: 500; }\n .card-value { font-size: 32px; font-weight: 700; margin-top: 4px; font-variant-numeric: tabular-nums; }\n .card-value.green { color: var(--green); }\n .card-value.violet { color: var(--violet); }\n .card-detail { font-size: 12px; color: var(--muted); margin-top: 4px; }\n\n .section { margin-bottom: 24px; }\n .section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase;\n letter-spacing: 1px; margin-bottom: 12px; }\n .info-grid { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; font-size: 13px;\n background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; }\n .info-label { color: var(--muted); }\n .info-value { color: var(--text); font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; }\n\n .snippet { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;\n padding: 16px 20px; margin-top: 12px; }\n .snippet code { display: block; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;\n font-size: 12px; color: var(--lavender); line-height: 1.8; white-space: pre-wrap; word-break: break-all; }\n .snippet-tabs { display: flex; gap: 0; margin-bottom: 12px; }\n .snippet-tab { padding: 6px 14px; font-size: 11px; font-weight: 500; cursor: pointer;\n color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-bottom: none; }\n .snippet-tab:first-child { border-radius: 8px 0 0 0; }\n .snippet-tab:last-child { border-radius: 0 8px 0 0; }\n .snippet-tab.active { color: var(--violet); background: var(--surface2); border-color: var(--accent); }\n\n .links { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; }\n .link { padding: 10px 20px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: 8px; color: var(--violet); text-decoration: none; font-size: 13px; font-weight: 500;\n transition: border-color 0.2s; }\n .link:hover { border-color: var(--accent); }\n\n .footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border);\n font-size: 11px; color: var(--muted); text-align: center; }\n .footer a { color: var(--violet); text-decoration: none; }\n</style>\n</head>\n<body>\n<div class=\"container\">\n <div class=\"header\">\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" rx=\"14\" fill=\"#1C1830\"/>\n <line x1=\"32\" y1=\"10\" x2=\"32\" y2=\"54\" stroke=\"#8B7CF6\" stroke-width=\"2.5\" stroke-linecap=\"round\"/>\n <path d=\"M16 20 A18 18 0 0 1 48 20\" fill=\"none\" stroke=\"#C4B5FD\" stroke-width=\"1.2\" opacity=\"0.4\"/>\n <path d=\"M16 44 A18 18 0 0 0 48 44\" fill=\"none\" stroke=\"#C4B5FD\" stroke-width=\"1.2\" opacity=\"0.4\"/>\n <path d=\"M20 30 A14 14 0 0 1 44 30\" fill=\"none\" stroke=\"#C4B5FD\" stroke-width=\"0.8\" opacity=\"0.2\"/>\n <path d=\"M20 34 A14 14 0 0 0 44 34\" fill=\"none\" stroke=\"#C4B5FD\" stroke-width=\"0.8\" opacity=\"0.2\"/>\n <circle cx=\"32\" cy=\"10\" r=\"3.5\" fill=\"#C4B5FD\"/><circle cx=\"32\" cy=\"54\" r=\"3.5\" fill=\"#C4B5FD\"/>\n <circle cx=\"32\" cy=\"32\" r=\"3\" fill=\"#8B7CF6\"/>\n </svg>\n <h1>MERIDIAN</h1>\n </div>\n <div class=\"tagline\">Harness Claude, your way.</div>\n <div id=\"content\"><div style=\"color:var(--muted);padding:40px;text-align:center\">Loading\u2026</div></div>\n</div>\n<script>\nfunction ms(v){if(v==null||v===0)return '\u2014';return v<1000?v+'ms':(v/1000).toFixed(1)+'s'}\nfunction card(l,v,d,c){return '<div class=\"card\"><div class=\"card-label\">'+l+'</div><div class=\"card-value '+(c||'')+'\">'+v+'</div>'+(d?'<div class=\"card-detail\">'+d+'</div>':'')+'</div>'}\n\nasync function refresh(){\n try{\n const [health,stats]=await Promise.all([fetch('/health').then(r=>r.json()),fetch('/telemetry/summary?window=86400000').then(r=>r.json())]);\n render(health,stats);\n }catch(e){document.getElementById('content').innerHTML='<div style=\"color:var(--red);padding:40px;text-align:center\">Could not connect</div>'}\n}\n\nfunction render(h,s){\n const st=h.status||'unknown',dot=st==='healthy'?'healthy':st==='degraded'?'degraded':'unhealthy';\n let o='';\n o+='<div class=\"status-banner\"><div class=\"status-dot '+dot+'\"></div><span class=\"status-text\">'+(st==='healthy'?'Operational':st==='degraded'?'Degraded':'Offline')+'</span><span class=\"status-detail\">Port '+location.port+' \u00B7 '+(h.mode||'internal')+' mode</span></div>';\n const er=s.totalRequests>0?((s.errorCount/s.totalRequests)*100).toFixed(1):'0';\n o+='<div class=\"grid\">'+card('Requests (24h)',s.totalRequests,'','violet')+card('Median Response',ms(s.totalDuration?.p50),'p95: '+ms(s.totalDuration?.p95),'')+card('Median TTFB',ms(s.ttfb?.p50),'p95: '+ms(s.ttfb?.p95),'')+card('Error Rate',er+'%',s.errorCount+' errors',parseFloat(er)>5?'':'green')+'</div>';\n o+='<div class=\"section\"><div class=\"section-title\">Account</div>';\n if(h.auth?.loggedIn){o+='<div class=\"info-grid\"><span class=\"info-label\">Email</span><span class=\"info-value\">'+(h.auth.email||'\u2014')+'</span><span class=\"info-label\">Subscription</span><span class=\"info-value\">'+(h.auth.subscriptionType||'\u2014')+'</span><span class=\"info-label\">Mode</span><span class=\"info-value\">'+(h.mode||'internal')+'</span><span class=\"info-label\">Endpoint</span><span class=\"info-value\">http://'+location.host+'</span></div>'}\n else{o+='<div class=\"info-grid\"><span class=\"info-label\">Status</span><span class=\"info-value\" style=\"color:var(--yellow)\">'+(h.error||'Not authenticated')+'</span></div>'}\n o+='</div>';\n if(s.byModel&&Object.keys(s.byModel).length>0){o+='<div class=\"section\"><div class=\"section-title\">Models (24h)</div><div class=\"grid\">';for(const[n,d]of Object.entries(s.byModel))o+=card(n,d.count,'avg '+ms(d.avgTotalMs),'');o+='</div></div>'}\n o+='<div class=\"section\"><div class=\"section-title\">Connect an Agent</div><div class=\"snippet\"><div class=\"snippet-tabs\"><div class=\"snippet-tab active\" onclick=\"showTab(this,&apos;opencode&apos;)\">OpenCode</div><div class=\"snippet-tab\" onclick=\"showTab(this,&apos;crush&apos;)\">Crush</div><div class=\"snippet-tab\" onclick=\"showTab(this,&apos;generic&apos;)\">Any Tool</div></div><div id=\"tab-opencode\"><code>ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://'+location.host+' opencode</code></div><div id=\"tab-crush\" style=\"display:none\"><code>'+JSON.stringify({providers:{meridian:{type:\"anthropic\",base_url:\"http://\"+location.host,api_key:\"x\",models:[{id:\"claude-sonnet-4-5-20250514\",name:\"Sonnet 4.5\"}]}}},null,2)+'</code></div><div id=\"tab-generic\" style=\"display:none\"><code>export ANTHROPIC_API_KEY=x\\nexport ANTHROPIC_BASE_URL=http://'+location.host+'</code></div></div></div>';\n o+='<div class=\"links\"><a href=\"/telemetry\" class=\"link\">\uD83D\uDCCA Telemetry</a><a href=\"/health\" class=\"link\">\uD83E\uDE7A Health</a><a href=\"/telemetry/summary\" class=\"link\">\uD83D\uDCC8 Stats API</a><a href=\"https://github.com/rynfar/meridian\" class=\"link\">\u2699\uFE0F GitHub</a></div>';\n o+='<div class=\"footer\">Meridian \u00B7 Built on the <a href=\"https://github.com/anthropics/claude-code-sdk-js\">Claude Code SDK</a></div>';\n document.getElementById('content').innerHTML=o;\n}\nfunction showTab(el,id){document.querySelectorAll('.snippet-tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');document.querySelectorAll('[id^=\"tab-\"]').forEach(t=>t.style.display='none');document.getElementById('tab-'+id).style.display='block'}\nrefresh();setInterval(refresh,10000);\n</script>\n</body>\n</html>";
6
+ export declare const landingHtml: string;
7
7
  //# sourceMappingURL=landing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,WAAW,0jTAsHhB,CAAA"}
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,WAAW,QAyHhB,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared profile switcher bar — injected into all HTML pages.
3
+ *
4
+ * CSS and JS are self-contained. The bar auto-hides when no profiles
5
+ * are configured. Profile changes take effect immediately via
6
+ * POST /profiles/active — no page reload needed.
7
+ */
8
+ export declare const profileBarCss = "\n .meridian-profile-bar {\n position: sticky; top: 0; z-index: 100;\n display: none; align-items: center; gap: 12px;\n padding: 8px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n font-size: 12px;\n color: var(--muted, #8b949e);\n }\n .meridian-profile-bar.visible { display: flex; }\n .meridian-profile-bar .profile-label {\n font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\n font-size: 10px; color: var(--muted, #8b949e);\n }\n .meridian-profile-bar select {\n background: var(--surface, #161b22); color: var(--text, #e6edf3);\n border: 1px solid var(--border, #30363d); border-radius: 6px;\n padding: 4px 24px 4px 10px; font-size: 12px; cursor: pointer;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%238b949e' stroke-width='1.5'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 6px center;\n }\n .meridian-profile-bar select:hover { border-color: var(--accent, #58a6ff); }\n .meridian-profile-bar select:focus { outline: none; border-color: var(--accent, #58a6ff); box-shadow: 0 0 0 1px var(--accent, #58a6ff); }\n .meridian-profile-bar .profile-status {\n font-size: 11px; color: var(--green, #3fb950); opacity: 0;\n transition: opacity 0.3s;\n }\n .meridian-profile-bar .profile-status.show { opacity: 1; }\n .meridian-profile-bar .profile-type {\n font-size: 10px; padding: 2px 8px; border-radius: 4px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n }\n .meridian-profile-bar .spacer { flex: 1; }\n .meridian-profile-bar .profile-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 11px;\n padding: 4px 8px; border-radius: 4px; transition: color 0.15s;\n }\n .meridian-profile-bar .profile-nav a:hover { color: var(--text, #e6edf3); }\n .meridian-profile-bar .profile-nav a.active { color: var(--accent, #58a6ff); }\n";
9
+ export declare const profileBarHtml = "\n<div class=\"meridian-profile-bar\" id=\"meridianProfileBar\">\n <span class=\"profile-label\">Profile</span>\n <select id=\"meridianProfileSelect\"></select>\n <span class=\"profile-type\" id=\"meridianProfileType\"></span>\n <span class=\"profile-status\" id=\"meridianProfileStatus\">\u2713 Switched</span>\n <div class=\"spacer\"></div>\n <div class=\"profile-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n </div>\n</div>\n";
10
+ export declare const profileBarJs = "\n(function() {\n var profileBar = document.getElementById('meridianProfileBar');\n var profileSelect = document.getElementById('meridianProfileSelect');\n var profileType = document.getElementById('meridianProfileType');\n var profileStatus = document.getElementById('meridianProfileStatus');\n var statusTimeout;\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.profile-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadProfiles() {\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n if (!data.profiles || data.profiles.length === 0) {\n profileBar.classList.remove('visible');\n return;\n }\n profileBar.classList.add('visible');\n var current = data.profiles.find(function(p) { return p.isActive; });\n profileSelect.innerHTML = data.profiles.map(function(p) {\n return '<option value=\"' + esc(p.id) + '\"' + (p.isActive ? ' selected' : '') + '>' + esc(p.id) + '</option>';\n }).join('');\n if (current) profileType.textContent = current.type;\n }).catch(function() {});\n }\n\n profileSelect.onchange = function() {\n fetch('/profiles/active', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ profile: profileSelect.value })\n }).then(function(r) { return r.json(); }).then(function(data) {\n if (data.success) {\n profileStatus.classList.add('show');\n clearTimeout(statusTimeout);\n statusTimeout = setTimeout(function() { profileStatus.classList.remove('show'); }, 2000);\n loadProfiles();\n }\n }).catch(function() {});\n };\n\n loadProfiles();\n setInterval(loadProfiles, 10000);\n})();\n";
11
+ //# sourceMappingURL=profileBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileBar.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileBar.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,aAAa,wmEA0CzB,CAAA;AAED,eAAO,MAAM,cAAc,0jBAa1B,CAAA;AAED,eAAO,MAAM,YAAY,yiEAoDxB,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Profile management page.
3
+ * Shows all configured profiles, their auth status, and setup instructions.
4
+ */
5
+ export declare const profilePageHtml: string;
6
+ //# sourceMappingURL=profilePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profilePage.d.ts","sourceRoot":"","sources":["../../src/telemetry/profilePage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,eAAe,QAuQpB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.27.6",
3
+ "version": "1.29.0",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -24,7 +24,7 @@
24
24
  "build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js' && tsc -p tsconfig.build.json",
25
25
  "postbuild": "node --check dist/cli.js && node --check dist/server.js && test -f dist/proxy/server.d.ts",
26
26
  "prepublishOnly": "bun run build",
27
- "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' --path-ignore-patterns '**/*proxy-context-usage-store*' --path-ignore-patterns '**/*proxy-passthrough-thinking*' && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/proxy-context-usage-store.test.ts && bun test src/__tests__/models-auth-status.test.ts && bun test src/__tests__/proxy-passthrough-thinking.test.ts",
27
+ "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' --path-ignore-patterns '**/*proxy-context-usage-store*' --path-ignore-patterns '**/*proxy-passthrough-thinking*' --path-ignore-patterns '**/*profile-switch-integration*' --path-ignore-patterns '**/*session-recovery*' && bun test src/__tests__/profile-switch-integration.test.ts && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/proxy-context-usage-store.test.ts && bun test src/__tests__/models-auth-status.test.ts && bun test src/__tests__/proxy-passthrough-thinking.test.ts && bun test src/__tests__/proxy-session-recovery.test.ts",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "proxy:direct": "bun run ./bin/cli.ts"
30
30
  },
@@ -81,4 +81,4 @@
81
81
  "license": "MIT",
82
82
  "private": false,
83
83
  "packageManager": "bun@1.3.11"
84
- }
84
+ }