@rynfar/meridian 1.55.0 → 1.56.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.
@@ -1,12 +1,15 @@
1
1
  import {
2
+ ProfileExhaustion,
3
+ choosePriorityProfile,
2
4
  getActiveProfileId,
3
5
  getEffectiveProfiles,
4
6
  getRoutingMode,
5
7
  listProfiles,
8
+ resolvePriorityOrder,
6
9
  resolveProfile,
7
10
  restoreActiveProfile,
8
11
  setActiveProfile
9
- } from "./cli-f0yqy2d2.js";
12
+ } from "./cli-ngtexmne.js";
10
13
  import {
11
14
  isTrackedPlugin,
12
15
  recordError,
@@ -46,7 +49,8 @@ import {
46
49
  stripExtendedContext
47
50
  } from "./cli-jhm27q0x.js";
48
51
  import {
49
- getSetting
52
+ getSetting,
53
+ setSetting
50
54
  } from "./cli-340h1chz.js";
51
55
  import {
52
56
  checkPluginConfigured
@@ -1758,6 +1762,19 @@ ${profileBarHtml}
1758
1762
 
1759
1763
  <div id="adapters"></div>
1760
1764
 
1765
+ <h1 style="margin-top:40px">Routing</h1>
1766
+ <p class="subtitle" style="max-width:720px;line-height:1.6">
1767
+ How unpinned requests choose an account. <strong style="color:var(--text)">Active</strong> uses the manually
1768
+ selected profile. <strong style="color:var(--text)">Sticky</strong> distributes sessions across profiles evenly
1769
+ (cache-affine). <strong style="color:var(--text)">Priority</strong> drains the pool in order — highest first —
1770
+ and fails over per request when an account runs out; conversations keep their account, and new sessions
1771
+ return to the preferred account after its window resets. An explicit <code>x-meridian-profile</code> header
1772
+ always overrides. Changes apply to the next request — no restart needed.
1773
+ </p>
1774
+ <div class="adapter-card" id="routing-card">
1775
+ <div id="routing-body">Loading…</div>
1776
+ </div>
1777
+
1761
1778
  <h1 style="margin-top:40px">Model Pricing</h1>
1762
1779
  <p class="subtitle" style="max-width:720px;line-height:1.6">
1763
1780
  Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the
@@ -2036,8 +2053,45 @@ async function addPricingModel() {
2036
2053
  }
2037
2054
  }
2038
2055
 
2056
+ async function loadRouting() {
2057
+ const res = await fetch('/settings/api/routing');
2058
+ const cfg = await res.json();
2059
+ const el = document.getElementById('routing-body');
2060
+ const envNote = (on) => on ? ' <span style="font-size:11px;color:var(--yellow)">(env override active — setting saved but env wins)</span>' : '';
2061
+ let h = '<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">'
2062
+ + '<label style="color:var(--muted);font-size:13px;width:90px">Mode</label>'
2063
+ + '<select id="routing-mode" style="background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px">'
2064
+ + ['active','sticky','priority'].map(m => '<option value="'+m+'"'+(cfg.routing===m?' selected':'')+'>'+m+'</option>').join('')
2065
+ + '</select>' + envNote(cfg.envOverride.routing) + '</div>';
2066
+ h += '<div id="routing-order-wrap" style="'+(cfg.routing==='priority'?'':'display:none')+'">'
2067
+ + '<div style="color:var(--muted);font-size:13px;margin-bottom:8px">Pool order — highest priority first. Drained top to bottom.'
2068
+ + envNote(cfg.envOverride.profileOrder) + '</div>'
2069
+ + '<ol id="routing-order" style="margin:0;padding-left:22px">'
2070
+ + cfg.profileOrder.map((id, i) =>
2071
+ '<li style="padding:4px 0;color:var(--text)">'
2072
+ + '<span style="font-family:var(--mono, monospace)">'+id+'</span>'
2073
+ + ' <button data-move="up" data-i="'+i+'" style="margin-left:10px;background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer"'+(i===0?' disabled':'')+'>&uarr;</button>'
2074
+ + ' <button data-move="down" data-i="'+i+'" style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer"'+(i===cfg.profileOrder.length-1?' disabled':'')+'>&darr;</button>'
2075
+ + '</li>').join('')
2076
+ + '</ol></div>';
2077
+ el.innerHTML = h;
2078
+ document.getElementById('routing-mode').addEventListener('change', async (e) => {
2079
+ await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ routing: e.target.value }) });
2080
+ await loadRouting();
2081
+ });
2082
+ el.querySelectorAll('button[data-move]').forEach(btn => btn.addEventListener('click', async () => {
2083
+ const i = Number(btn.dataset.i);
2084
+ const j = btn.dataset.move === 'up' ? i - 1 : i + 1;
2085
+ const order = cfg.profileOrder.slice();
2086
+ const tmp = order[i]; order[i] = order[j]; order[j] = tmp;
2087
+ await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileOrder: order }) });
2088
+ await loadRouting();
2089
+ }));
2090
+ }
2091
+
2039
2092
  loadConfig();
2040
2093
  loadPricing();
2094
+ loadRouting();
2041
2095
  ${profileBarJs}
2042
2096
  </script>
2043
2097
  </body>
@@ -4418,6 +4472,7 @@ function linkRequestAbort(signal) {
4418
4472
  var OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
4419
4473
  var OAUTH_BETA_HEADER = "oauth-2025-04-20";
4420
4474
  var CACHE_TTL_MS_DEFAULT = 30000;
4475
+ var STALE_MAX_MS_DEFAULT = 15 * 60000;
4421
4476
  var cacheByProfile = new Map;
4422
4477
  var inflightByProfile = new Map;
4423
4478
  var DEFAULT_KEY = "__default__";
@@ -4522,34 +4577,45 @@ async function fetchOAuthUsageImpl(opts) {
4522
4577
  if (existing)
4523
4578
  return existing;
4524
4579
  const store = opts?.store ?? createPlatformCredentialStore({ claudeConfigDir: opts?.claudeConfigDir });
4580
+ const staleMaxMs = opts?.staleMaxMs ?? STALE_MAX_MS_DEFAULT;
4581
+ const staleOr = (reason) => {
4582
+ const last = cacheByProfile.get(cacheKey2);
4583
+ if (last && Date.now() - last.fetchedAt < staleMaxMs) {
4584
+ claudeLog("oauth_usage.serving_stale", { profile: cacheKey2, reason, ageMs: Date.now() - last.fetchedAt });
4585
+ return { ...last, stale: true };
4586
+ }
4587
+ return null;
4588
+ };
4525
4589
  const promise = (async () => {
4526
4590
  try {
4527
4591
  const token = await readAccessToken(store);
4528
- if (!token)
4529
- return null;
4592
+ if (!token) {
4593
+ claudeLog("oauth_usage.no_token", { profile: cacheKey2 });
4594
+ return staleOr("no_token");
4595
+ }
4530
4596
  let result = await callAnthropic(token, fetchImpl);
4531
4597
  if ("__status" in result && result.__status === 401) {
4532
4598
  claudeLog("oauth_usage.token_refresh_attempt", { profile: cacheKey2 });
4533
4599
  const refreshed = await refreshOAuthToken(store);
4534
4600
  if (!refreshed) {
4535
4601
  claudeLog("oauth_usage.refresh_failed", { profile: cacheKey2 });
4536
- return null;
4602
+ return staleOr("refresh_failed");
4537
4603
  }
4538
4604
  const newToken = await readAccessToken(store);
4539
4605
  if (!newToken)
4540
- return null;
4606
+ return staleOr("no_token_after_refresh");
4541
4607
  result = await callAnthropic(newToken, fetchImpl);
4542
4608
  }
4543
4609
  if ("__status" in result) {
4544
4610
  claudeLog("oauth_usage.upstream_error", { profile: cacheKey2, status: result.__status });
4545
- return null;
4611
+ return staleOr(`upstream_${result.__status}`);
4546
4612
  }
4547
4613
  const snapshot = buildSnapshot(result);
4548
4614
  cacheByProfile.set(cacheKey2, snapshot);
4549
4615
  return snapshot;
4550
4616
  } catch (err) {
4551
4617
  claudeLog("oauth_usage.fetch_failed", { profile: cacheKey2, error: err instanceof Error ? err.message : String(err) });
4552
- return null;
4618
+ return staleOr("exception");
4553
4619
  } finally {
4554
4620
  inflightByProfile.delete(cacheKey2);
4555
4621
  }
@@ -9081,9 +9147,15 @@ var KNOWN_ALIASES = {
9081
9147
  executor: "build"
9082
9148
  };
9083
9149
  var STRIP_SUFFIXES = ["-agent", "-tool", "-worker", "-task", " agent", " tool"];
9084
- function resolveAgentAlias(input) {
9150
+ function resolveAgentAlias(input, validAgents) {
9085
9151
  const lowered = input.toLowerCase();
9086
- return KNOWN_ALIASES[lowered] ?? lowered;
9152
+ const exact = validAgents.find((a) => a.toLowerCase() === lowered);
9153
+ if (exact)
9154
+ return exact;
9155
+ const alias = KNOWN_ALIASES[lowered];
9156
+ if (alias && validAgents.includes(alias))
9157
+ return alias;
9158
+ return lowered;
9087
9159
  }
9088
9160
  function fuzzyMatchAgentName(input, validAgents) {
9089
9161
  if (!input)
@@ -9388,27 +9460,8 @@ var dashboardHtml = `<!DOCTYPE html>
9388
9460
  transition: all 0.15s; }
9389
9461
  .log-filter:hover { border-color: var(--accent); color: var(--text); }
9390
9462
  .log-filter.active { background: rgba(88,166,255,0.1); border-color: var(--accent); color: var(--accent); }
9391
-
9392
- /* Usage tab */
9393
- .usage-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
9394
- .ucard { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
9395
- .ucard-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
9396
- .ucard-title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
9397
- .ucard-reset { font-size: 11px; color: var(--muted); white-space: nowrap; }
9398
- .ucard-pct { font-size: 32px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 8px; color: var(--green); }
9399
- .ucard.warn .ucard-pct { color: var(--yellow); }
9400
- .ucard.high .ucard-pct { color: var(--red); }
9401
- .ucard-sub { font-size: 12px; color: var(--muted); margin-top: 8px; min-height: 16px; }
9402
- .ubar { position: relative; height: 8px; border-radius: 4px; background: var(--border); overflow: visible; margin-top: 12px; }
9403
- .ubar-fill { height: 100%; border-radius: 4px; background: var(--green); transition: width 0.4s ease; max-width: 100%; }
9404
- .ucard.warn .ubar-fill { background: var(--yellow); }
9405
- .ucard.high .ubar-fill { background: var(--red); }
9406
- .ubar-marker { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); opacity: 0.55; border-radius: 1px; }
9407
- .pace-pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
9408
- .pace-pill.on, .pace-pill.under { background: rgba(63,185,80,0.15); color: var(--green); }
9409
- .pace-pill.ahead { background: rgba(210,153,34,0.18); color: var(--yellow); }
9410
- .pace-pill.over { background: rgba(248,81,73,0.15); color: var(--red); }
9411
9463
  .usage-note { font-size: 11px; color: var(--muted); }
9464
+
9412
9465
  ` + profileBarCss + `
9413
9466
  </style>
9414
9467
  </head>
@@ -9501,22 +9554,20 @@ function setLogFilter(filter) {
9501
9554
  async function refresh() {
9502
9555
  const w = $('#window').value;
9503
9556
  try {
9504
- const [summary, reqs, logs, quota] = await Promise.all([
9557
+ const [summary, reqs, logs] = await Promise.all([
9505
9558
  fetch('/telemetry/summary?window=' + w).then(r => r.json()),
9506
9559
  fetch('/telemetry/requests?limit=50&since=' + (Date.now() - Number(w))).then(r => r.json()),
9507
9560
  fetch('/telemetry/logs?limit=200&since=' + (Date.now() - Number(w))).then(r => r.json()),
9508
- fetch('/v1/usage/quota').then(r => r.json()).catch(() => null),
9509
9561
  ]);
9510
- render(summary, reqs, logs, quota);
9562
+ render(summary, reqs, logs);
9511
9563
  $('#lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString();
9512
9564
  } catch (e) {
9513
9565
  $('#content').innerHTML = '<div class="empty">Failed to load telemetry</div>';
9514
9566
  }
9515
9567
  }
9516
9568
 
9517
- function render(s, reqs, logs, quota) {
9518
- const hasUsage = quota && quota.buckets && quota.buckets.some(b => b.utilization != null);
9519
- if (s.totalRequests === 0 && (!logs || logs.length === 0) && !hasUsage) {
9569
+ function render(s, reqs, logs) {
9570
+ if (s.totalRequests === 0 && (!logs || logs.length === 0)) {
9520
9571
  $('#content').innerHTML = '<div class="empty">No requests recorded yet. Send a request through the proxy to see telemetry.</div>';
9521
9572
  return;
9522
9573
  }
@@ -9534,7 +9585,6 @@ function render(s, reqs, logs, quota) {
9534
9585
  + 'Requests<span class="tab-badge">' + reqs.length + '</span></div>'
9535
9586
  + '<div class="tab' + (activeTab === 'logs' ? ' active' : '') + '" data-tab="logs" onclick="switchTab(&apos;logs&apos;)">'
9536
9587
  + 'Logs<span class="tab-badge">' + logs.length + '</span></div>'
9537
- + '<div class="tab' + (activeTab === 'usage' ? ' active' : '') + '" data-tab="usage" onclick="switchTab(&apos;usage&apos;)">Usage</div>'
9538
9588
  + '</div>';
9539
9589
 
9540
9590
  // ==================== Overview tab ====================
@@ -9724,11 +9774,6 @@ function render(s, reqs, logs, quota) {
9724
9774
  }
9725
9775
  html += '</div>'; // end logs panel
9726
9776
 
9727
- // ==================== Usage tab ====================
9728
- html += '<div id="panel-usage" class="tab-panel' + (activeTab === 'usage' ? ' active' : '') + '">';
9729
- html += renderUsage(quota);
9730
- html += '</div>'; // end usage panel
9731
-
9732
9777
  $('#content').innerHTML = html;
9733
9778
  }
9734
9779
 
@@ -9739,101 +9784,6 @@ function card(label, value, detail) {
9739
9784
  + '</div>';
9740
9785
  }
9741
9786
 
9742
- // ---- Usage tab helpers (mirror src/telemetry/profileUsage.ts; unit-tested there) ----
9743
- function classifyUtil(u) {
9744
- if (u == null || !isFinite(u)) return '';
9745
- if (u >= 0.85) return 'high';
9746
- if (u >= 0.6) return 'warn';
9747
- return '';
9748
- }
9749
- function resetIn(resetsAt) {
9750
- if (resetsAt == null || !isFinite(resetsAt)) return '';
9751
- var ms = resetsAt - Date.now();
9752
- if (ms <= 0) return 'resetting…';
9753
- var m = Math.floor(ms / 60000);
9754
- if (m < 60) return 'resets in ' + Math.max(1, m) + 'm';
9755
- var h = Math.floor(m / 60), rm = m % 60;
9756
- if (h < 24) return 'resets in ' + h + 'h' + (rm ? ' ' + rm + 'm' : '');
9757
- var d = Math.floor(h / 24), rh = h % 24;
9758
- return 'resets in ' + d + 'd' + (rh ? ' ' + rh + 'h' : '');
9759
- }
9760
- function pct(u) { return Math.round(Math.max(0, u) * 100); }
9761
-
9762
- function usageCard(title, bucket) {
9763
- if (!bucket || bucket.utilization == null) {
9764
- return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">' + title + '</span></div>'
9765
- + '<div class="ucard-pct" style="color:var(--muted)">—</div>'
9766
- + '<div class="ucard-sub">No data yet</div></div>';
9767
- }
9768
- var u = bucket.utilization;
9769
- var cls = classifyUtil(u);
9770
- var fill = Math.min(100, pct(u));
9771
- return '<div class="ucard ' + cls + '">'
9772
- + '<div class="ucard-head"><span class="ucard-title">' + title + '</span>'
9773
- + '<span class="ucard-reset">' + resetIn(bucket.resetsAt) + '</span></div>'
9774
- + '<div class="ucard-pct">' + pct(u) + '<span style="font-size:16px;font-weight:500;color:var(--muted)">%</span></div>'
9775
- + '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%"></div></div>'
9776
- + '<div class="ucard-sub">of your ' + title.split('·')[1].trim() + ' allowance used</div>'
9777
- + '</div>';
9778
- }
9779
-
9780
- // Weekly pace: actual vs. expected (even) consumption at this point in the 7-day window.
9781
- function paceCard(weekly) {
9782
- if (!weekly || weekly.utilization == null || weekly.resetsAt == null) {
9783
- return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">Weekly Pace</span></div>'
9784
- + '<div class="ucard-pct" style="color:var(--muted)">—</div>'
9785
- + '<div class="ucard-sub">Needs weekly usage data</div></div>';
9786
- }
9787
- var WEEK = 7 * 86400000;
9788
- var start = weekly.resetsAt - WEEK;
9789
- var elapsed = Math.max(0, Math.min(1, (Date.now() - start) / WEEK));
9790
- var actual = pct(weekly.utilization);
9791
- var expected = Math.round(elapsed * 100);
9792
- var delta = actual - expected;
9793
- var projected = elapsed >= 0.1 ? Math.round((Math.max(0, weekly.utilization) / elapsed) * 100) : null;
9794
-
9795
- var pill, label;
9796
- if (delta > 7) { pill = 'ahead'; label = '+' + delta + '% ahead of pace'; }
9797
- else if (delta < -7) { pill = 'under'; label = Math.abs(delta) + '% under pace'; }
9798
- else { pill = 'on'; label = 'On pace'; }
9799
- if (projected != null && projected >= 100) { pill = 'over'; label = 'On track to run out'; }
9800
-
9801
- var fill = Math.min(100, actual);
9802
- var mark = Math.min(100, expected);
9803
- var proj = projected == null ? '—' : projected + '%';
9804
- return '<div class="ucard">'
9805
- + '<div class="ucard-head"><span class="ucard-title">Weekly Pace</span>'
9806
- + '<span class="ucard-reset">' + Math.round(elapsed * 100) + '% through week</span></div>'
9807
- + '<div style="margin-top:8px"><span class="pace-pill ' + pill + '">' + label + '</span></div>'
9808
- + '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%;background:' + (pill === 'over' ? 'var(--red)' : pill === 'ahead' ? 'var(--yellow)' : 'var(--green)') + '"></div>'
9809
- + '<div class="ubar-marker" style="left:' + mark + '%" title="Expected at even pace"></div></div>'
9810
- + '<div class="ucard-sub">' + actual + '% used vs ' + expected + '% expected · at this rate ~' + proj + ' by reset</div>'
9811
- + '</div>';
9812
- }
9813
-
9814
- function renderUsage(quota) {
9815
- if (!quota || !quota.buckets) {
9816
- return '<div class="empty">Usage data unavailable.</div>';
9817
- }
9818
- var by = {};
9819
- quota.buckets.forEach(function (b) { by[b.type] = b; });
9820
- var session = by['five_hour'], weekly = by['seven_day'];
9821
- if ((!session || session.utilization == null) && (!weekly || weekly.utilization == null)) {
9822
- return '<div class="empty">No usage data yet — Anthropic reports it after your first request through Meridian.</div>';
9823
- }
9824
- var h = '<div class="usage-cards">'
9825
- + usageCard('Session · 5h', session)
9826
- + usageCard('Weekly · 7d', weekly)
9827
- + paceCard(weekly)
9828
- + '</div>';
9829
- var asOf = quota.asOf ? new Date(quota.asOf).toLocaleTimeString() : '';
9830
- h += '<div class="usage-note">'
9831
- + (quota.profile ? 'Profile: ' + quota.profile + ' · ' : '')
9832
- + 'Reported by Anthropic' + (asOf ? ' · as of ' + asOf : '')
9833
- + '</div>';
9834
- return h;
9835
- }
9836
-
9837
9787
  $('#autoRefresh').addEventListener('change', function() {
9838
9788
  clearInterval(timer);
9839
9789
  if (this.checked) timer = setInterval(refresh, 5000);
@@ -9941,6 +9891,10 @@ var landingHtml = `<!DOCTYPE html>
9941
9891
  .usage-row .w-label { color: var(--muted); width: 64px; flex-shrink: 0; }
9942
9892
  .usage-row .w-bar { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
9943
9893
  .usage-row .w-fill { height: 100%; border-radius: 3px; }
9894
+ .pace-row { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 8px; }
9895
+ .pace-row .pace-text { flex: 1; font-weight: 600; font-size: 11px; }
9896
+ .pool-chip { font-size: 10px; padding: 2px 8px; border-radius: 10px; background: var(--surface2); color: var(--muted); margin-left: 6px; vertical-align: middle; }
9897
+ .pool-chip.exhausted { color: var(--red); background: rgba(248,81,73,0.12); }
9944
9898
  .usage-row .w-pct { width: 38px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
9945
9899
  .usage-row .w-reset { color: var(--muted); font-size: 11px; width: 76px; text-align: right; }
9946
9900
  .no-usage { font-size: 12px; color: var(--muted); padding: 4px 0; }
@@ -9955,6 +9909,7 @@ var landingHtml = `<!DOCTYPE html>
9955
9909
  .strip-value.green { color: var(--green); }
9956
9910
  .strip-value.red { color: var(--red); }
9957
9911
  .strip-detail { font-size: 11px; color: var(--muted); }
9912
+ .strip-detail.red { color: var(--red); }
9958
9913
 
9959
9914
  .section { margin-bottom: 24px; }
9960
9915
  .section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase;
@@ -9979,6 +9934,29 @@ function usd(v){if(v==null)return '—';if(v>0&&v<0.01)return '$'+v.toFixed(4);i
9979
9934
  var WIN_LABELS={five_hour:'5h',seven_day:'7d',seven_day_opus:'7d Opus',seven_day_sonnet:'7d Sonnet',seven_day_fable:'7d Fable',seven_day_oauth_apps:'7d Apps',seven_day_cowork:'7d Cowork',seven_day_omelette:'7d Omelette'};
9980
9935
  function winLabel(t){if(WIN_LABELS[t])return WIN_LABELS[t];return t.replace(/^seven_day_/,'7d ').replace(/_/g,' ').replace(/\bw/g,function(c){return c.toUpperCase()})}
9981
9936
  function utilColor(u){return u>=0.85?'var(--red)':u>=0.6?'var(--yellow)':'var(--green)'}
9937
+ // Mirrors computeWeeklyPace in src/telemetry/profileUsage.ts (unit-tested
9938
+ // there): actual vs expected (even) consumption at this point in the 7-day
9939
+ // window, with the dashboard's over-promotion when the projection hits 100%.
9940
+ function weeklyPace(u,resetsAt){
9941
+ var WEEK=7*86400000;
9942
+ if(u==null||resetsAt==null)return null;
9943
+ var el=Math.max(0,Math.min(1,(Date.now()-(resetsAt-WEEK))/WEEK));
9944
+ var actual=Math.round(Math.max(0,u)*100);
9945
+ var expected=Math.round(el*100);
9946
+ var delta=actual-expected;
9947
+ var proj=el>=0.1?Math.round((Math.max(0,u)/el)*100):null;
9948
+ var st=delta>7?'ahead':delta<-7?'under':'on';
9949
+ if(proj!=null&&proj>=100)st='over';
9950
+ return {actual:actual,expected:expected,delta:delta,proj:proj,status:st};
9951
+ }
9952
+ function paceText(pc){
9953
+ if(pc.status==='over')return 'on track to run out';
9954
+ if(pc.status==='ahead')return '+'+pc.delta+'% ahead of pace';
9955
+ if(pc.status==='under')return Math.abs(pc.delta)+'% under pace';
9956
+ return 'on pace';
9957
+ }
9958
+ function paceColor(pc){return pc.status==='over'?'var(--red)':pc.status==='ahead'?'var(--yellow)':'var(--green)'}
9959
+
9982
9960
  function resetIn(ts){if(ts==null)return '';var d=ts-Date.now();if(d<=0)return 'resetting…';var m=Math.ceil(d/60000);if(m<60)return 'in '+m+'m';var h=Math.floor(m/60);if(h<24)return 'in '+h+'h'+(m%60?' '+(m%60)+'m':'');var days=Math.floor(h/24);return 'in '+days+'d'+(h%24?' '+(h%24)+'h':'')}
9983
9961
 
9984
9962
  function introSection(h){
@@ -10025,9 +10003,22 @@ function profileSection(q,s,pl,h){
10025
10003
  +'<span class="w-pct" style="color:'+utilColor(w.utilization)+'">'+pct+'%</span>'
10026
10004
  +'<span class="w-reset">'+resetIn(w.resetsAt)+'</span></div>';
10027
10005
  }
10006
+ var weekly=null;
10007
+ for(var j=0;j<wins.length;j++){if(wins[j].type==='seven_day')weekly=wins[j]}
10008
+ var pc=weekly?weeklyPace(weekly.utilization,weekly.resetsAt):null;
10009
+ if(pc)rows+='<div class="usage-row pace-row"><span class="w-label">pace</span>'
10010
+ +'<span class="pace-text" style="color:'+paceColor(pc)+'">'+paceText(pc)+'</span>'
10011
+ +'<span class="w-reset">'+(pc.proj!=null?'~'+pc.proj+'% by reset':'')+'</span></div>';
10028
10012
  if(!rows)rows='<div class="no-usage">no usage data yet</div>';
10029
- var switchable=multi&&p.configured&&!p.isActive;
10030
- var badge=p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
10013
+ var isPriority=pl&&pl.routing==='priority';
10014
+ var switchable=multi&&p.configured&&!p.isActive&&!isPriority;
10015
+ var badge=isPriority?'':p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
10016
+ if(isPriority){
10017
+ var orderIdx=(pl.profileOrder||[]).indexOf(p.id);
10018
+ if(orderIdx>=0)badge+='<span class="pool-chip">#'+(orderIdx+1)+' in pool</span>';
10019
+ var exh=(pl.exhausted||[]).filter(function(e){return e.id===p.id})[0];
10020
+ if(exh)badge+=' <span class="pool-chip exhausted">exhausted · resets '+resetIn(exh.until)+'</span>';
10021
+ }
10031
10022
  cards+='<div class="profile-card'+(p.isActive?' active':'')+(switchable?' switchable':'')+'"'+(switchable?' data-profile="'+esc(p.id)+'" role="button" tabindex="0"':'')+'>'
10032
10023
  +'<div class="profile-head"><span class="profile-name"><span class="prof-dot"></span>'+esc(p.label||p.id)+' '+badge+'</span>'
10033
10024
  +'<span class="profile-cost">'+usd(cost?cost.estimatedUsd:0)+'</span></div>'
@@ -10041,7 +10032,7 @@ function profileSection(q,s,pl,h){
10041
10032
  function strip(items){
10042
10033
  var o='<div class="strip">';
10043
10034
  for(var i=0;i<items.length;i++){var it=items[i];
10044
- o+='<div class="strip-item"><div class="strip-label">'+it[0]+'</div><div class="strip-value '+(it[2]||'')+'">'+it[1]+'</div>'+(it[3]?'<div class="strip-detail">'+it[3]+'</div>':'')+'</div>';
10035
+ o+='<div class="strip-item"><div class="strip-label">'+it[0]+'</div><div class="strip-value '+(it[2]||'')+'">'+it[1]+'</div>'+(it[3]?'<div class="strip-detail '+(it[4]||'')+'">'+it[3]+'</div>':'')+'</div>';
10045
10036
  }
10046
10037
  return o+'</div>';
10047
10038
  }
@@ -10072,7 +10063,9 @@ function render(h,s,q,pl){
10072
10063
  var tu=s.tokenUsage||{};
10073
10064
  var cache=tu.avgCacheHitRate!=null?Math.round(tu.avgCacheHitRate*100)+'%':'—';
10074
10065
  var items=[
10075
- ['Requests',String(s.totalRequests),s.errorCount>0?'red':'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors'],
10066
+ // The big number is the TOTAL — never error-colored (a red 1714 reads as
10067
+ // 1714 failures). The error signal lives on the detail line only.
10068
+ ['Requests',String(s.totalRequests),'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors',s.errorCount>0?'red':''],
10076
10069
  ['Tokens Out',tokens(tu.totalOutputTokens),'',tokens(tu.totalInputTokens)+' in'],
10077
10070
  ['Cache Hit',cache,tu.avgCacheHitRate>=0.5?'green':'','prompt cache'],
10078
10071
  ['Est. API Value',usd(s.costEstimate?.totalUsd),'','list prices'],
@@ -10211,7 +10204,7 @@ function classifyError(errMsg) {
10211
10204
  message: "Claude authentication expired or invalid. Run 'claude login' in your terminal to re-authenticate, then restart the proxy."
10212
10205
  };
10213
10206
  }
10214
- if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests")) {
10207
+ if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("hit your session limit") || lower.includes("usage limit reached")) {
10215
10208
  const hint = lower.includes("1m") || lower.includes("context") ? " If you're frequently hitting this, set MERIDIAN_SONNET_MODEL=sonnet to use the 200k model instead." : "";
10216
10209
  return {
10217
10210
  status: 429,
@@ -19848,6 +19841,127 @@ function createProxyServer(config = {}) {
19848
19841
  app.use("/settings/*", requireAuth);
19849
19842
  app.use("/settings", requireAuth);
19850
19843
  app.use("/design-login", requireAuth);
19844
+ const priorityExhaustion = new ProfileExhaustion;
19845
+ const priorityAssignments = new Map;
19846
+ const PRIORITY_ASSIGNMENTS_MAX = 5000;
19847
+ const PRIORITY_DEFAULT_COOLDOWN_MS = 10 * 60000;
19848
+ const PRIORITY_COOLDOWN_CAP_MS = 6 * 60 * 60000;
19849
+ function priorityProfileOrderSetting() {
19850
+ const env2 = process.env.MERIDIAN_PROFILE_ORDER;
19851
+ if (env2 && env2.trim())
19852
+ return env2.split(",").map((s) => s.trim()).filter(Boolean);
19853
+ const setting = getSetting("profileOrder");
19854
+ return Array.isArray(setting) && setting.length > 0 ? setting : undefined;
19855
+ }
19856
+ function priorityCooldownUntil(now) {
19857
+ const fiveHour = rateLimitStore.getAll().find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now);
19858
+ const until = fiveHour?.resetsAt ?? now + PRIORITY_DEFAULT_COOLDOWN_MS;
19859
+ return Math.min(until, now + PRIORITY_COOLDOWN_CAP_MS);
19860
+ }
19861
+ async function sniffQuotaFailure(res) {
19862
+ const contentType = res.headers.get("content-type") ?? "";
19863
+ if (!contentType.includes("text/event-stream")) {
19864
+ if (res.status === 429) {
19865
+ const body = await res.clone().json().catch(() => null);
19866
+ if (body?.error?.type === "rate_limit_error")
19867
+ return { failed: true, errorPayload: body, response: res };
19868
+ }
19869
+ return { failed: false, errorPayload: null, response: res };
19870
+ }
19871
+ const reader = res.body?.getReader();
19872
+ if (!reader)
19873
+ return { failed: false, errorPayload: null, response: res };
19874
+ const decoder = new TextDecoder;
19875
+ const consumed = [];
19876
+ let text = "";
19877
+ let failedPayload = null;
19878
+ while (true) {
19879
+ const { done, value } = await reader.read();
19880
+ if (done)
19881
+ break;
19882
+ consumed.push(value);
19883
+ text += decoder.decode(value, { stream: true });
19884
+ const frameEnd = text.indexOf(`
19885
+
19886
+ `);
19887
+ if (frameEnd === -1)
19888
+ continue;
19889
+ const frame = text.slice(0, frameEnd);
19890
+ if (/^event: error$/m.test(frame)) {
19891
+ const dataLine = frame.split(`
19892
+ `).find((l) => l.startsWith("data: "));
19893
+ try {
19894
+ const parsed = dataLine ? JSON.parse(dataLine.slice(6)) : null;
19895
+ if (parsed?.error?.type === "rate_limit_error") {
19896
+ failedPayload = parsed;
19897
+ }
19898
+ } catch {}
19899
+ }
19900
+ break;
19901
+ }
19902
+ if (failedPayload) {
19903
+ await reader.cancel().catch(() => {});
19904
+ return { failed: true, errorPayload: failedPayload, response: res };
19905
+ }
19906
+ const rest = new ReadableStream({
19907
+ start(ctrl) {
19908
+ for (const chunk of consumed)
19909
+ ctrl.enqueue(chunk);
19910
+ },
19911
+ async pull(ctrl) {
19912
+ const { done, value } = await reader.read();
19913
+ if (done)
19914
+ ctrl.close();
19915
+ else
19916
+ ctrl.enqueue(value);
19917
+ },
19918
+ cancel(reason) {
19919
+ reader.cancel(reason).catch(() => {});
19920
+ }
19921
+ });
19922
+ return { failed: false, errorPayload: null, response: new Response(rest, { status: res.status, headers: res.headers }) };
19923
+ }
19924
+ async function dispatchPriority(c, orderedCandidateIds, sessionKey, wantsStream) {
19925
+ const bodyBuf = await c.req.arrayBuffer();
19926
+ let lastError = null;
19927
+ let previous = null;
19928
+ for (const candidate of orderedCandidateIds) {
19929
+ const headers = new Headers(c.req.raw.headers);
19930
+ headers.set("x-meridian-profile", candidate);
19931
+ headers.set("x-meridian-priority-dispatch", "1");
19932
+ const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
19933
+ const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
19934
+ if (!failed) {
19935
+ if (sessionKey) {
19936
+ priorityAssignments.set(sessionKey, candidate);
19937
+ if (priorityAssignments.size > PRIORITY_ASSIGNMENTS_MAX) {
19938
+ const oldest = priorityAssignments.keys().next().value;
19939
+ if (oldest !== undefined)
19940
+ priorityAssignments.delete(oldest);
19941
+ }
19942
+ }
19943
+ if (previous) {
19944
+ claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
19945
+ plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
19946
+ }
19947
+ return response;
19948
+ }
19949
+ priorityExhaustion.mark(candidate, priorityCooldownUntil(Date.now()), "rate_limit_error");
19950
+ claudeLog("priority.exhausted", { profile: candidate, until: priorityCooldownUntil(Date.now()) });
19951
+ lastError = errorPayload;
19952
+ previous = candidate;
19953
+ }
19954
+ if (wantsStream) {
19955
+ return new Response(`event: error
19956
+ data: ${JSON.stringify(lastError)}
19957
+
19958
+ `, {
19959
+ status: 200,
19960
+ headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" }
19961
+ });
19962
+ }
19963
+ return new Response(JSON.stringify(lastError), { status: 429, headers: { "content-type": "application/json" } });
19964
+ }
19851
19965
  app.use("/auth/*", requireAuth);
19852
19966
  app.get("/", (c) => {
19853
19967
  const accept = c.req.header("accept") || "";
@@ -19915,6 +20029,25 @@ function createProxyServer(config = {}) {
19915
20029
  }
19916
20030
  const outputFormat = parsedOutputFormat.value;
19917
20031
  const routingMode = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
20032
+ if (routingMode === "priority" && !c.req.header("x-meridian-profile")) {
20033
+ const effectivePool = getEffectiveProfiles(finalConfig.profiles);
20034
+ if (effectivePool.length > 1) {
20035
+ const { order, unknown } = resolvePriorityOrder(effectivePool.map((p) => p.id), priorityProfileOrderSetting());
20036
+ if (unknown.length > 0)
20037
+ claudeLog("priority.unknown_order_ids", { unknown });
20038
+ const sessionKey = adapter.getSessionId(c, body) || null;
20039
+ const assigned = sessionKey ? priorityAssignments.get(sessionKey) : undefined;
20040
+ let first;
20041
+ if (assigned && order.includes(assigned) && !priorityExhaustion.isExhausted(assigned)) {
20042
+ first = assigned;
20043
+ } else {
20044
+ const pick = choosePriorityProfile(order, (id) => priorityExhaustion.isExhausted(id));
20045
+ first = pick?.id ?? order[0];
20046
+ }
20047
+ const candidates = [first, ...order.filter((id) => id !== first && !priorityExhaustion.isExhausted(id))];
20048
+ return dispatchPriority(c, candidates, sessionKey, body.stream === true);
20049
+ }
20050
+ }
19918
20051
  const profile = resolveProfile(finalConfig.profiles, finalConfig.defaultProfile, c.req.header("x-meridian-profile") || undefined, routingMode === "sticky" ? { routingMode, stickySessionKey: adapter.getSessionId(c, body) } : undefined);
19919
20052
  const authStatus = await getClaudeAuthStatusAsync(profile.id !== "default" ? profile.id : undefined, Object.keys(profile.env).length > 0 ? profile.env : undefined);
19920
20053
  const agentMode = c.req.header("x-opencode-agent-mode") ?? null;
@@ -20044,7 +20177,7 @@ function createProxyServer(config = {}) {
20044
20177
  const lineageType = lineageResult.type === "diverged" && !cachedSession ? "new" : lineageResult.type;
20045
20178
  const msgCount = Array.isArray(body.messages) ? body.messages.length : 0;
20046
20179
  const toolCount = body.tools?.length ?? 0;
20047
- const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""}${profile.id !== "default" ? ` profile=${profile.id}${routingMode === "sticky" ? "(sticky)" : ""}` : ""} model=${model} stream=${stream3} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
20180
+ const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""}${profile.id !== "default" ? ` profile=${profile.id}${routingMode === "sticky" ? "(sticky)" : c.req.header("x-meridian-priority-dispatch") ? "(priority)" : ""}` : ""} model=${model} stream=${stream3} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
20048
20181
  plog(`[PROXY] ${requestLogLine} msgs=${msgSummary}`);
20049
20182
  diagnosticLog2.session(`${requestLogLine}`, requestMeta.requestId);
20050
20183
  if (lineageResult.type === "diverged" && profileSessionId && !isIndependentSession) {
@@ -20241,7 +20374,7 @@ function createProxyServer(config = {}) {
20241
20374
  const clientTool = requestTools.find((t) => t.name === toolName);
20242
20375
  let toolInput = normalizeToolInput(input.tool_input, clientTool?.input_schema);
20243
20376
  if (toolName.toLowerCase() === "task" && toolInput?.subagent_type && typeof toolInput.subagent_type === "string") {
20244
- toolInput = { ...toolInput, subagent_type: resolveAgentAlias(toolInput.subagent_type) };
20377
+ toolInput = { ...toolInput, subagent_type: resolveAgentAlias(toolInput.subagent_type, validAgentNames) };
20245
20378
  }
20246
20379
  const signature = toolUseSignature(toolName, toolInput);
20247
20380
  const isExactDuplicate = capturedSignatures.has(signature);
@@ -21356,7 +21489,7 @@ data: ${JSON.stringify({ type: "message_stop" })}
21356
21489
  try {
21357
21490
  const parsed = JSON.parse(buffered);
21358
21491
  if (typeof parsed.subagent_type === "string") {
21359
- parsed.subagent_type = resolveAgentAlias(parsed.subagent_type);
21492
+ parsed.subagent_type = resolveAgentAlias(parsed.subagent_type, validAgentNames);
21360
21493
  }
21361
21494
  fixed = JSON.stringify(parsed);
21362
21495
  } catch {}
@@ -21942,6 +22075,44 @@ data: ${JSON.stringify({
21942
22075
  resetAdapterFeatures2(adapter);
21943
22076
  return c.json({ ok: true });
21944
22077
  });
22078
+ app.get("/settings/api/routing", (c) => {
22079
+ const profiles = listProfiles(finalConfig.profiles, finalConfig.defaultProfile);
22080
+ return c.json({
22081
+ routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing")),
22082
+ profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
22083
+ profiles: profiles.map((p) => p.id),
22084
+ envOverride: {
22085
+ routing: Boolean(process.env.MERIDIAN_ROUTING),
22086
+ profileOrder: Boolean(process.env.MERIDIAN_PROFILE_ORDER)
22087
+ }
22088
+ });
22089
+ });
22090
+ app.put("/settings/api/routing", async (c) => {
22091
+ let body;
22092
+ try {
22093
+ body = await c.req.json();
22094
+ } catch {
22095
+ return c.json({ error: "Invalid JSON" }, 400);
22096
+ }
22097
+ if (body.routing !== undefined) {
22098
+ if (typeof body.routing !== "string" || !["active", "sticky", "priority"].includes(body.routing)) {
22099
+ return c.json({ error: 'routing must be "active", "sticky", or "priority"' }, 400);
22100
+ }
22101
+ setSetting("routing", body.routing);
22102
+ }
22103
+ if (body.profileOrder !== undefined) {
22104
+ if (!Array.isArray(body.profileOrder) || body.profileOrder.some((x) => typeof x !== "string")) {
22105
+ return c.json({ error: "profileOrder must be an array of profile ids" }, 400);
22106
+ }
22107
+ const known = new Set(listProfiles(finalConfig.profiles, finalConfig.defaultProfile).map((p) => p.id));
22108
+ const unknown = body.profileOrder.filter((id) => !known.has(id));
22109
+ if (unknown.length > 0)
22110
+ return c.json({ error: `Unknown profiles: ${unknown.join(", ")}` }, 400);
22111
+ setSetting("profileOrder", body.profileOrder);
22112
+ }
22113
+ plog(`[PROXY] Routing settings updated: routing=${getSetting("routing") ?? "active"} order=${(getSetting("profileOrder") ?? []).join(",") || "(config order)"}`);
22114
+ return c.json({ success: true });
22115
+ });
21945
22116
  app.get("/settings/api/pricing", (c) => {
21946
22117
  const { BUILTIN_MODEL_PRICING: BUILTIN_MODEL_PRICING2 } = (init_pricing(), __toCommonJS(exports_pricing));
21947
22118
  const { getPricingOverrides: getPricingOverrides2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
@@ -22032,10 +22203,16 @@ data: ${JSON.stringify({
22032
22203
  lastSuccessAt: cacheInfo.lastSuccessAt || null
22033
22204
  };
22034
22205
  }));
22206
+ const routingModeNow = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
22207
+ const priorityInfo = routingModeNow === "priority" ? {
22208
+ profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
22209
+ exhausted: priorityExhaustion.snapshot()
22210
+ } : {};
22035
22211
  return c.json({
22036
22212
  profiles: enriched,
22037
22213
  activeProfile: getActiveProfileId() || finalConfig.defaultProfile || profiles[0]?.id || "default",
22038
- routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"))
22214
+ routing: routingModeNow,
22215
+ ...priorityInfo
22039
22216
  });
22040
22217
  });
22041
22218
  app.get("/profiles", async (c) => {
@@ -22059,10 +22236,17 @@ data: ${JSON.stringify({
22059
22236
  if (!effective.find((p) => p.id === body.profile)) {
22060
22237
  return c.json({ error: `Unknown profile: ${body.profile}. Available: ${effective.map((p) => p.id).join(", ")}` }, 400);
22061
22238
  }
22239
+ const previousProfile = getActiveProfileId() ?? null;
22062
22240
  setActiveProfile(body.profile);
22063
22241
  clearSessionCache();
22064
22242
  rateLimitStore.clear();
22065
- plog(`[PROXY] Active profile switched to: ${body.profile} (session + rate-limit caches cleared)`);
22243
+ claudeLog("profile.switched", {
22244
+ from: previousProfile,
22245
+ to: body.profile,
22246
+ userAgent: c.req.header("user-agent")?.slice(0, 120) ?? null,
22247
+ origin: c.req.header("origin") ?? c.req.header("referer")?.slice(0, 120) ?? null
22248
+ });
22249
+ plog(`[PROXY] Active profile switched to: ${body.profile} (from ${previousProfile ?? "unset"}, ua: ${(c.req.header("user-agent") || "unknown").slice(0, 60)}) (session + rate-limit caches cleared)`);
22066
22250
  return c.json({ success: true, activeProfile: body.profile });
22067
22251
  });
22068
22252
  app.get("/plugins/list", async (c) => {
@@ -11,7 +11,12 @@ import { homedir } from "node:os";
11
11
  // src/proxy/routing.ts
12
12
  import { createHash } from "node:crypto";
13
13
  function getRoutingMode(raw) {
14
- return raw?.toLowerCase() === "sticky" ? "sticky" : "active";
14
+ const lower = raw?.toLowerCase();
15
+ if (lower === "sticky")
16
+ return "sticky";
17
+ if (lower === "priority")
18
+ return "priority";
19
+ return "active";
15
20
  }
16
21
  function rendezvousScore(sessionKey, profileId) {
17
22
  const digest = createHash("sha256").update(`${sessionKey}\x00${profileId}`).digest();
@@ -31,6 +36,67 @@ function pickStickyProfile(sessionKey, profileIds) {
31
36
  }
32
37
  return best;
33
38
  }
39
+ function resolvePriorityOrder(configuredIds, orderSetting) {
40
+ const existing = new Set(configuredIds);
41
+ const order = [];
42
+ const unknown = [];
43
+ for (const id of orderSetting ?? []) {
44
+ if (!existing.has(id)) {
45
+ unknown.push(id);
46
+ continue;
47
+ }
48
+ if (!order.includes(id))
49
+ order.push(id);
50
+ }
51
+ for (const id of configuredIds)
52
+ if (!order.includes(id))
53
+ order.push(id);
54
+ return { order, unknown };
55
+ }
56
+ function choosePriorityProfile(order, isExhausted) {
57
+ if (order.length === 0)
58
+ return;
59
+ for (const id of order) {
60
+ if (!isExhausted(id))
61
+ return { id, allExhausted: false };
62
+ }
63
+ return { id: order[0], allExhausted: true };
64
+ }
65
+
66
+ class ProfileExhaustion {
67
+ now;
68
+ marks = new Map;
69
+ constructor(now = Date.now) {
70
+ this.now = now;
71
+ }
72
+ mark(id, until, reason) {
73
+ const existing = this.marks.get(id);
74
+ if (existing && existing.until >= until)
75
+ return;
76
+ this.marks.set(id, { until, reason });
77
+ }
78
+ isExhausted(id) {
79
+ const entry = this.marks.get(id);
80
+ if (!entry)
81
+ return false;
82
+ if (entry.until <= this.now()) {
83
+ this.marks.delete(id);
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+ snapshot() {
89
+ const out = [];
90
+ for (const [id, entry] of this.marks) {
91
+ if (entry.until <= this.now()) {
92
+ this.marks.delete(id);
93
+ continue;
94
+ }
95
+ out.push({ id, until: entry.until, reason: entry.reason });
96
+ }
97
+ return out;
98
+ }
99
+ }
34
100
 
35
101
  // src/proxy/profiles.ts
36
102
  var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
@@ -147,4 +213,4 @@ function listProfiles(profiles, defaultProfile) {
147
213
  }));
148
214
  }
149
215
 
150
- export { getRoutingMode, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
216
+ export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-w63v1ftp.js";
5
- import"./cli-f0yqy2d2.js";
4
+ } from "./cli-aefk7t3r.js";
5
+ import"./cli-ngtexmne.js";
6
6
  import"./cli-sry5aqdj.js";
7
7
  import"./cli-xmweegb1.js";
8
8
  import {
@@ -171,7 +171,7 @@ async function runCli(start = startProxyServer, runAuthCheck = async () => {
171
171
  console.error("\x1B[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1B[0m");
172
172
  }
173
173
  if (!profiles) {
174
- const { enableDiskProfileDiscovery } = await import("./profiles-84hnbd6c.js");
174
+ const { enableDiskProfileDiscovery } = await import("./profiles-sq9t3fh9.js");
175
175
  enableDiskProfileDiscovery();
176
176
  }
177
177
  const proxy = await start({ port, host, idleTimeoutSeconds, pluginDir, pluginConfigPath, profiles, defaultProfile, version, installProcessErrorHandlers: true });
@@ -9,7 +9,7 @@ import {
9
9
  resolveProfile,
10
10
  restoreActiveProfile,
11
11
  setActiveProfile
12
- } from "./cli-f0yqy2d2.js";
12
+ } from "./cli-ngtexmne.js";
13
13
  import"./cli-340h1chz.js";
14
14
  import"./cli-p9swy5t3.js";
15
15
  export {
@@ -22,7 +22,12 @@
22
22
  * the SDK may validate against registered alias variants (e.g., "general-purpose"
23
23
  * is registered by `addCaseVariants`), but the client expects the canonical
24
24
  * agent name from its config ("general").
25
+ *
26
+ * The alias table exists to REPAIR invalid names, never to remap valid ones
27
+ * (#671): a name that already matches a registered agent is returned in the
28
+ * config's canonical casing, and an alias is applied only when its target is
29
+ * itself registered — renaming to a nonexistent agent can only ever fail.
25
30
  */
26
- export declare function resolveAgentAlias(input: string): string;
31
+ export declare function resolveAgentAlias(input: string, validAgents: string[]): string;
27
32
  export declare function fuzzyMatchAgentName(input: string, validAgents: string[]): string;
28
33
  //# sourceMappingURL=agentMatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agentMatch.d.ts","sourceRoot":"","sources":["../../src/proxy/agentMatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0CH;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAsChF"}
1
+ {"version":3,"file":"agentMatch.d.ts","sourceRoot":"","sources":["../../src/proxy/agentMatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0CH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAO9E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAsChF"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA+G7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAA;IAC5D,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAuCpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA,MAAM,CAYR"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAoH7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAA;IAC5D,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAuCpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA,MAAM,CAYR"}
@@ -36,12 +36,17 @@ export interface OAuthUsageSnapshot {
36
36
  windows: OAuthUsageWindow[];
37
37
  extraUsage: OAuthExtraUsageInfo | null;
38
38
  fetchedAt: number;
39
+ /** Set when this is a previous snapshot served because a fresh fetch
40
+ * failed transiently (credential-read blip, upstream error). */
41
+ stale?: boolean;
39
42
  }
40
43
  /** Minimal fetch shape used by callAnthropic. Avoids `typeof fetch`'s
41
44
  * `preconnect` property, which makes test casts unwieldy. */
42
45
  type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
43
46
  export interface FetchOAuthUsageOpts {
44
47
  ttlMs?: number;
48
+ /** Max age of a last-good snapshot served on fetch failure (default 15 min). */
49
+ staleMaxMs?: number;
45
50
  force?: boolean;
46
51
  store?: CredentialStore;
47
52
  profileId?: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"oauthUsage.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAoD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AA6CvG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AAyFD;8DAC8D;AAC9D,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAmBzE,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAcD,8CAA8C;AAC9C,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,GAC9E,IAAI,CAEN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpG;AAqDD,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
1
+ {"version":3,"file":"oauthUsage.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAoD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AA6CvG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB;qEACiE;IACjE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AA6FD;8DAC8D;AAC9D,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAmBzE,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAcD,8CAA8C;AAC9C,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,GAC9E,IAAI,CAEN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpG;AAwED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * This is a leaf module — pure functions, no I/O.
21
21
  */
22
- export type RoutingMode = "active" | "sticky";
22
+ export type RoutingMode = "active" | "sticky" | "priority";
23
23
  /**
24
24
  * Parse a routing mode string (from settings or MERIDIAN_ROUTING).
25
25
  * Unknown values fall back to "active" — a typo must never change
@@ -41,4 +41,45 @@ export declare function pickStickyProfile(sessionKey: string, profileIds: readon
41
41
  * to update casually.
42
42
  */
43
43
  export declare const RENDEZVOUS_STABLE_GUARD: ReadonlyArray<readonly [string, readonly string[], string]>;
44
+ /**
45
+ * Resolve the effective pool order: the configured order (settings
46
+ * "profileOrder" / MERIDIAN_PROFILE_ORDER) filtered to profiles that exist,
47
+ * with unlisted profiles appended in config order. Unknown ids are returned
48
+ * for a startup warning — a typo must never silently drop an account.
49
+ */
50
+ export declare function resolvePriorityOrder(configuredIds: readonly string[], orderSetting: readonly string[] | undefined): {
51
+ order: string[];
52
+ unknown: string[];
53
+ };
54
+ /**
55
+ * Pick the highest-priority profile that isn't exhausted. When every pool
56
+ * member is exhausted, return the preferred (first) profile with the flag
57
+ * set — callers still attempt it (marks may be stale), and per the design
58
+ * decision the LAST tried profile's error is what ultimately surfaces.
59
+ */
60
+ export declare function choosePriorityProfile(order: readonly string[], isExhausted: (id: string) => boolean): {
61
+ id: string;
62
+ allExhausted: boolean;
63
+ } | undefined;
64
+ export interface ExhaustionEntry {
65
+ id: string;
66
+ until: number;
67
+ reason: string;
68
+ }
69
+ /**
70
+ * In-memory per-profile exhaustion marks with expiry. Deliberately not
71
+ * persisted: this is routing hygiene, not durable truth — after a restart
72
+ * the first failing request re-marks. A later mark may extend an entry but
73
+ * an earlier one never shortens it (two concurrent failures shouldn't
74
+ * un-learn the longer reset).
75
+ */
76
+ export declare class ProfileExhaustion {
77
+ private readonly now;
78
+ private readonly marks;
79
+ constructor(now?: () => number);
80
+ mark(id: string, until: number, reason: string): void;
81
+ isExhausted(id: string): boolean;
82
+ /** Live entries only — expired marks are dropped on read. */
83
+ snapshot(): ExhaustionEntry[];
84
+ }
44
85
  //# sourceMappingURL=routing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE7C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAEnE;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAYvG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAM/F,CAAA"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAA;AAE1D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAKnE;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAYvG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAM/F,CAAA;AAOD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAC1C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAUxC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACnC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAMnD;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuD;gBAChD,GAAG,GAAE,MAAM,MAAiB;IAEzD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMrD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAUhC,6DAA6D;IAC7D,QAAQ,IAAI,eAAe,EAAE;CAQ9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAysHhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAs5HhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
@@ -10,9 +10,12 @@
10
10
  export interface MeridianSettings {
11
11
  /** Last active profile ID — restored on proxy startup */
12
12
  activeProfile?: string;
13
- /** Profile routing mode (#383): "active" (default) or "sticky".
14
- * MERIDIAN_ROUTING env var takes precedence when set. */
13
+ /** Profile routing mode (#383, priority spec): "active" (default),
14
+ * "sticky", or "priority". MERIDIAN_ROUTING env var takes precedence. */
15
15
  routing?: string;
16
+ /** Priority-mode pool order (highest priority first). Falls back to
17
+ * profiles.json order. MERIDIAN_PROFILE_ORDER env var takes precedence. */
18
+ profileOrder?: string[];
16
19
  }
17
20
  /** Read settings from disk. Returns empty object if file doesn't exist or is invalid. */
18
21
  export declare function loadSettings(): MeridianSettings;
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAO/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASrE;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAErG"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8EAC0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;gFAC4E;IAC5E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAO/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASrE;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAErG"}
package/dist/server.js CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  runObserveHook,
12
12
  runTransformHook,
13
13
  startProxyServer
14
- } from "./cli-w63v1ftp.js";
15
- import"./cli-f0yqy2d2.js";
14
+ } from "./cli-aefk7t3r.js";
15
+ import"./cli-ngtexmne.js";
16
16
  import"./cli-sry5aqdj.js";
17
17
  import"./cli-xmweegb1.js";
18
18
  import"./cli-jhm27q0x.js";
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QAkhBlB,CAAA"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QAyZlB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,WAAW,QAkNhB,CAAA"}
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,WAAW,QA6PhB,CAAA"}
@@ -2,5 +2,5 @@
2
2
  * SDK Features settings page — per-adapter toggle UI.
3
3
  * Same dark theme as the telemetry dashboard. No framework, no CDN.
4
4
  */
5
- export declare const settingsPageHtml = "<!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 SDK Features</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n \n :root {\n /* Cool-gray neutral palette. High contrast, surface/border separation,\n no color cast muddying the text. Blue is the primary accent; violet\n is the fixed secondary \u2014 used together in the brand gradient and\n individually for hover states and a handful of telemetry badges. */\n --bg: #0d1117;\n --surface: #161b22;\n --surface2: #1c2128;\n --border: #30363d;\n /* Text */\n --text: #e6edf3;\n --muted: #8b949e;\n /* Brand \u2014 blue primary, violet secondary */\n --accent: #58a6ff;\n --accent2: #bc8cff;\n --violet: #bc8cff;\n --lavender: #d2a8ff;\n /* Semantic */\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n /* Telemetry-specific aliases (waterfall + lineage badges) */\n --blue: #58a6ff;\n --purple: #bc8cff;\n --queue: #d29922;\n --ttfb: #58a6ff;\n --upstream: #3fb950;\n }\n /* Banner backsplash \u2014 the brand look: a gentle diagonal wash with soft\n blue (top-left) and violet (bottom-right) glows. Pages must not set\n their own body background so this shows through everywhere. */\n body {\n background:\n radial-gradient(1200px 800px at 12% -8%, rgba(88,166,255,0.07), transparent 60%),\n radial-gradient(1100px 800px at 92% 108%, rgba(188,140,255,0.06), transparent 60%),\n linear-gradient(135deg, #0d1117 0%, #10151d 55%, #161b22 100%);\n background-attachment: fixed;\n background-color: var(--bg);\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-header {\n position: sticky; top: 0; z-index: 100;\n display: flex; align-items: center; gap: 20px;\n padding: 10px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n }\n .meridian-header .mh-brand {\n display: flex; align-items: center; gap: 10px;\n text-decoration: none; color: var(--text, #e6edf3);\n }\n .meridian-header .mh-logo { display: block; }\n .meridian-header .mh-name {\n font-size: 15px; font-weight: 700; letter-spacing: 2px;\n text-transform: uppercase;\n }\n .meridian-header .mh-nav { display: flex; align-items: center; gap: 2px; }\n .meridian-header .mh-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 12px;\n font-weight: 500; padding: 5px 10px; border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n }\n .meridian-header .mh-nav a:hover { color: var(--text, #e6edf3); background: var(--surface, #161b22); }\n .meridian-header .mh-nav a.active { color: var(--accent, #58a6ff); background: var(--surface, #161b22); }\n .meridian-header .mh-right {\n margin-left: auto; display: flex; align-items: center; gap: 10px;\n }\n .meridian-header .mh-profile {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; color: var(--text, #e6edf3);\n padding: 3px 10px; border-radius: 20px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n text-decoration: none; transition: border-color 0.15s;\n }\n .meridian-header .mh-profile:hover { border-color: var(--accent, #58a6ff); }\n .meridian-header .mh-profile.visible { display: inline-flex; }\n .meridian-header .mh-profile .mh-profile-type {\n color: var(--muted, #8b949e); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n\n .content { max-width: 900px; margin: 0 auto; padding: 24px; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .nav { display: flex; gap: 16px; margin-bottom: 24px; font-size: 13px; }\n .nav a { color: var(--muted); text-decoration: none; }\n .nav a:hover { color: var(--accent); }\n .nav a.active { color: var(--accent); }\n\n .adapter-card {\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 20px; margin-bottom: 16px;\n }\n .adapter-header {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 16px;\n }\n .adapter-name { font-size: 16px; font-weight: 600; }\n .adapter-badge {\n font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .badge-active { background: rgba(63, 185, 80, 0.15); color: var(--green); }\n .badge-inactive { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n\n .feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n @media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }\n\n .feature-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: 6px;\n background: var(--bg); border: 1px solid var(--border);\n }\n .feature-info { display: flex; flex-direction: column; }\n .feature-label { font-size: 13px; font-weight: 500; }\n .feature-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n\n /* Toggle switch */\n .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .toggle-track {\n position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;\n background: var(--border); border-radius: 10px; transition: background 0.2s;\n }\n .toggle-track::after {\n content: \"\"; position: absolute; height: 14px; width: 14px;\n left: 3px; bottom: 3px; background: var(--muted); border-radius: 50%;\n transition: transform 0.2s, background 0.2s;\n }\n .toggle input:checked + .toggle-track { background: var(--accent); }\n .toggle input:checked + .toggle-track::after {\n transform: translateX(16px); background: var(--text);\n }\n\n /* Select dropdown */\n .feature-select {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer;\n }\n\n .save-indicator {\n position: fixed; bottom: 24px; right: 24px;\n background: var(--green); color: #000; padding: 8px 16px;\n border-radius: 6px; font-size: 13px; font-weight: 500;\n opacity: 0; transition: opacity 0.3s; pointer-events: none;\n }\n .save-indicator.visible { opacity: 1; }\n\n .reset-btn {\n background: none; border: 1px solid var(--border); color: var(--muted);\n border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;\n }\n .reset-btn:hover { border-color: var(--red); color: var(--red); }\n\n /* Model pricing */\n .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }\n .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }\n .pricing-table tr:last-child td { border-bottom: none; }\n .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }\n .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;\n font-variant-numeric: tabular-nums; }\n .pricing-input:focus { border-color: var(--accent); outline: none; }\n .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }\n .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }\n .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;\n padding-top: 14px; border-top: 1px solid var(--border); }\n .pricing-add input[type=\"text\"] { width: 240px; text-align: left; }\n .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;\n padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }\n .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }\n</style>\n</head>\n<body>\n\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\">\n <svg class=\"mh-logo\" width=\"24\" height=\"24\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <defs>\n <linearGradient id=\"mhGrad\" x1=\"32\" y1=\"4\" x2=\"32\" y2=\"60\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#58a6ff\"/>\n <stop offset=\"1\" stop-color=\"#bc8cff\"/>\n </linearGradient>\n </defs>\n <circle cx=\"32\" cy=\"32\" r=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"3.5\"/>\n <ellipse cx=\"32\" cy=\"32\" rx=\"10.5\" ry=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"2.5\" opacity=\"0.8\"/>\n <path d=\"M7 32h50\" stroke=\"url(#mhGrad)\" stroke-width=\"2\" opacity=\"0.4\"/>\n <circle cx=\"32\" cy=\"7\" r=\"4.5\" fill=\"#58a6ff\"/>\n <circle cx=\"32\" cy=\"57\" r=\"4.5\" fill=\"#bc8cff\"/>\n</svg>\n <span class=\"mh-name\">Meridian</span>\n </a>\n <nav class=\"mh-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </nav>\n <div class=\"mh-right\">\n <a class=\"mh-profile\" id=\"mhProfile\" href=\"/\" title=\"Active profile \u2014 switch from the home page\"></a>\n <span class=\"mh-status\" id=\"mhStatus\"><span class=\"mh-dot\" id=\"mhDot\"></span><span class=\"mh-status-text\" id=\"mhStatusText\"></span></span>\n </div>\n</header>\n\n<div class=\"content\">\n <h1>SDK Features <span style=\"font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(210,153,34,0.15);color:var(--yellow);vertical-align:middle;margin-left:8px\">Experimental</span></h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Unlock Claude Code features for any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md \u2014 normally\n exclusive to Claude Code \u2014 become available to OpenCode, Crush, Droid, and any other harness routed through Meridian.\n Each agent keeps its own toolchain while gaining access to these additional features.<br><br>\n <strong style=\"color:var(--text)\">System prompts:</strong> For these features to work correctly, both the Claude Code prompt and your client prompt\n should be enabled. When both are active, they are appended together \u2014 Claude Code's base instructions come first,\n followed by your agent's specific instructions.\n </p>\n\n <div id=\"adapters\"></div>\n\n <h1 style=\"margin-top:40px\">Model Pricing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the\n built-in rate, or add models the built-in table doesn't know about (they show as \"no pricing\" on the\n dashboard until defined here). Changes apply on the next dashboard refresh.\n </p>\n <div class=\"adapter-card\">\n <table class=\"pricing-table\">\n <thead><tr><th>Model</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Source</th><th></th></tr></thead>\n <tbody id=\"pricingRows\"></tbody>\n </table>\n <div class=\"pricing-add\">\n <input type=\"text\" class=\"pricing-input\" id=\"newModelName\" placeholder=\"model id (e.g. claude-opus-9)\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelInput\" placeholder=\"input\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelOutput\" placeholder=\"output\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheRead\" placeholder=\"cache read\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheWrite\" placeholder=\"cache write\" min=\"0\" step=\"0.01\">\n <button class=\"add-btn\" onclick=\"addPricingModel()\">Add Model</button>\n </div>\n <div class=\"pricing-note\">\n Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the\n input rate (the 5-minute cache TTL multipliers). Verify current list prices at\n <a href=\"https://claude.com/pricing\" target=\"_blank\" rel=\"noreferrer\" style=\"color:var(--accent)\">claude.com/pricing</a>.\n </div>\n </div>\n</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '&quot;');\n row.innerHTML = info +\n '<input type=\"text\" class=\"feature-select\" style=\"width:180px\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">';\n }\n\n grid.appendChild(row);\n }\n\n card.appendChild(grid);\n container.appendChild(card);\n }\n}\n\n// ---- Model pricing (telemetry cost estimate) ----\nlet pricingData = { builtin: {}, overrides: {} };\n\nfunction fmtRate(v) { return String(Math.round(v * 10000) / 10000); }\n\nasync function loadPricing() {\n const res = await fetch('/settings/api/pricing');\n pricingData = await res.json();\n renderPricing();\n}\n\nasync function putPricing(model, rates) {\n const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(rates),\n });\n if (!res.ok) {\n const err = await res.json().catch(function () { return {}; });\n alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));\n return false;\n }\n showSaved();\n return true;\n}\n\nasync function removePricing(model) {\n await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });\n showSaved();\n await loadPricing();\n}\n\nfunction rateCell(value, onCommit) {\n const td = document.createElement('td');\n const input = document.createElement('input');\n input.type = 'number';\n input.min = '0';\n input.step = '0.01';\n input.className = 'pricing-input';\n input.value = fmtRate(value);\n input.addEventListener('change', onCommit);\n td.appendChild(input);\n return { td: td, input: input };\n}\n\nfunction renderPricing() {\n const tbody = document.getElementById('pricingRows');\n tbody.innerHTML = '';\n const models = Array.from(new Set(\n Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))\n )).sort();\n\n for (const model of models) {\n const override = pricingData.overrides[model];\n const effective = override || pricingData.builtin[model];\n const tr = document.createElement('tr');\n\n const nameTd = document.createElement('td');\n nameTd.className = 'pricing-model';\n nameTd.textContent = model;\n tr.appendChild(nameTd);\n\n const cells = [];\n const commit = async function () {\n const rates = {\n inputPerMTok: parseFloat(cells[0].input.value),\n outputPerMTok: parseFloat(cells[1].input.value),\n cacheReadPerMTok: parseFloat(cells[2].input.value),\n cacheWritePerMTok: parseFloat(cells[3].input.value),\n };\n for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }\n if (await putPricing(model, rates)) await loadPricing();\n };\n ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {\n const cell = rateCell(effective[key], commit);\n cells.push(cell);\n tr.appendChild(cell.td);\n });\n\n const badgeTd = document.createElement('td');\n const badge = document.createElement('span');\n badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');\n badge.textContent = override ? 'Override' : 'Built-in';\n badgeTd.appendChild(badge);\n tr.appendChild(badgeTd);\n\n const actionTd = document.createElement('td');\n if (override) {\n const btn = document.createElement('button');\n btn.className = 'reset-btn';\n btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';\n btn.addEventListener('click', function () { removePricing(model); });\n actionTd.appendChild(btn);\n }\n tr.appendChild(actionTd);\n\n tbody.appendChild(tr);\n }\n}\n\nasync function addPricingModel() {\n const name = document.getElementById('newModelName').value.trim();\n const inputRate = parseFloat(document.getElementById('newModelInput').value);\n const outputRate = parseFloat(document.getElementById('newModelOutput').value);\n if (!name) { alert('Enter a model id'); return; }\n if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }\n const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };\n const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);\n const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);\n if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;\n if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;\n if (await putPricing(name, rates)) {\n ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {\n document.getElementById(id).value = '';\n });\n await loadPricing();\n }\n}\n\nloadConfig();\nloadPricing();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-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 loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n\n</script>\n</body>\n</html>";
5
+ export declare const settingsPageHtml = "<!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 SDK Features</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n \n :root {\n /* Cool-gray neutral palette. High contrast, surface/border separation,\n no color cast muddying the text. Blue is the primary accent; violet\n is the fixed secondary \u2014 used together in the brand gradient and\n individually for hover states and a handful of telemetry badges. */\n --bg: #0d1117;\n --surface: #161b22;\n --surface2: #1c2128;\n --border: #30363d;\n /* Text */\n --text: #e6edf3;\n --muted: #8b949e;\n /* Brand \u2014 blue primary, violet secondary */\n --accent: #58a6ff;\n --accent2: #bc8cff;\n --violet: #bc8cff;\n --lavender: #d2a8ff;\n /* Semantic */\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n /* Telemetry-specific aliases (waterfall + lineage badges) */\n --blue: #58a6ff;\n --purple: #bc8cff;\n --queue: #d29922;\n --ttfb: #58a6ff;\n --upstream: #3fb950;\n }\n /* Banner backsplash \u2014 the brand look: a gentle diagonal wash with soft\n blue (top-left) and violet (bottom-right) glows. Pages must not set\n their own body background so this shows through everywhere. */\n body {\n background:\n radial-gradient(1200px 800px at 12% -8%, rgba(88,166,255,0.07), transparent 60%),\n radial-gradient(1100px 800px at 92% 108%, rgba(188,140,255,0.06), transparent 60%),\n linear-gradient(135deg, #0d1117 0%, #10151d 55%, #161b22 100%);\n background-attachment: fixed;\n background-color: var(--bg);\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-header {\n position: sticky; top: 0; z-index: 100;\n display: flex; align-items: center; gap: 20px;\n padding: 10px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n }\n .meridian-header .mh-brand {\n display: flex; align-items: center; gap: 10px;\n text-decoration: none; color: var(--text, #e6edf3);\n }\n .meridian-header .mh-logo { display: block; }\n .meridian-header .mh-name {\n font-size: 15px; font-weight: 700; letter-spacing: 2px;\n text-transform: uppercase;\n }\n .meridian-header .mh-nav { display: flex; align-items: center; gap: 2px; }\n .meridian-header .mh-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 12px;\n font-weight: 500; padding: 5px 10px; border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n }\n .meridian-header .mh-nav a:hover { color: var(--text, #e6edf3); background: var(--surface, #161b22); }\n .meridian-header .mh-nav a.active { color: var(--accent, #58a6ff); background: var(--surface, #161b22); }\n .meridian-header .mh-right {\n margin-left: auto; display: flex; align-items: center; gap: 10px;\n }\n .meridian-header .mh-profile {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; color: var(--text, #e6edf3);\n padding: 3px 10px; border-radius: 20px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n text-decoration: none; transition: border-color 0.15s;\n }\n .meridian-header .mh-profile:hover { border-color: var(--accent, #58a6ff); }\n .meridian-header .mh-profile.visible { display: inline-flex; }\n .meridian-header .mh-profile .mh-profile-type {\n color: var(--muted, #8b949e); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n\n .content { max-width: 900px; margin: 0 auto; padding: 24px; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .nav { display: flex; gap: 16px; margin-bottom: 24px; font-size: 13px; }\n .nav a { color: var(--muted); text-decoration: none; }\n .nav a:hover { color: var(--accent); }\n .nav a.active { color: var(--accent); }\n\n .adapter-card {\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 20px; margin-bottom: 16px;\n }\n .adapter-header {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 16px;\n }\n .adapter-name { font-size: 16px; font-weight: 600; }\n .adapter-badge {\n font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .badge-active { background: rgba(63, 185, 80, 0.15); color: var(--green); }\n .badge-inactive { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n\n .feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n @media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }\n\n .feature-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: 6px;\n background: var(--bg); border: 1px solid var(--border);\n }\n .feature-info { display: flex; flex-direction: column; }\n .feature-label { font-size: 13px; font-weight: 500; }\n .feature-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n\n /* Toggle switch */\n .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .toggle-track {\n position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;\n background: var(--border); border-radius: 10px; transition: background 0.2s;\n }\n .toggle-track::after {\n content: \"\"; position: absolute; height: 14px; width: 14px;\n left: 3px; bottom: 3px; background: var(--muted); border-radius: 50%;\n transition: transform 0.2s, background 0.2s;\n }\n .toggle input:checked + .toggle-track { background: var(--accent); }\n .toggle input:checked + .toggle-track::after {\n transform: translateX(16px); background: var(--text);\n }\n\n /* Select dropdown */\n .feature-select {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer;\n }\n\n .save-indicator {\n position: fixed; bottom: 24px; right: 24px;\n background: var(--green); color: #000; padding: 8px 16px;\n border-radius: 6px; font-size: 13px; font-weight: 500;\n opacity: 0; transition: opacity 0.3s; pointer-events: none;\n }\n .save-indicator.visible { opacity: 1; }\n\n .reset-btn {\n background: none; border: 1px solid var(--border); color: var(--muted);\n border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;\n }\n .reset-btn:hover { border-color: var(--red); color: var(--red); }\n\n /* Model pricing */\n .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }\n .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }\n .pricing-table tr:last-child td { border-bottom: none; }\n .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }\n .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;\n font-variant-numeric: tabular-nums; }\n .pricing-input:focus { border-color: var(--accent); outline: none; }\n .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }\n .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }\n .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;\n padding-top: 14px; border-top: 1px solid var(--border); }\n .pricing-add input[type=\"text\"] { width: 240px; text-align: left; }\n .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;\n padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }\n .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }\n</style>\n</head>\n<body>\n\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\">\n <svg class=\"mh-logo\" width=\"24\" height=\"24\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <defs>\n <linearGradient id=\"mhGrad\" x1=\"32\" y1=\"4\" x2=\"32\" y2=\"60\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#58a6ff\"/>\n <stop offset=\"1\" stop-color=\"#bc8cff\"/>\n </linearGradient>\n </defs>\n <circle cx=\"32\" cy=\"32\" r=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"3.5\"/>\n <ellipse cx=\"32\" cy=\"32\" rx=\"10.5\" ry=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"2.5\" opacity=\"0.8\"/>\n <path d=\"M7 32h50\" stroke=\"url(#mhGrad)\" stroke-width=\"2\" opacity=\"0.4\"/>\n <circle cx=\"32\" cy=\"7\" r=\"4.5\" fill=\"#58a6ff\"/>\n <circle cx=\"32\" cy=\"57\" r=\"4.5\" fill=\"#bc8cff\"/>\n</svg>\n <span class=\"mh-name\">Meridian</span>\n </a>\n <nav class=\"mh-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </nav>\n <div class=\"mh-right\">\n <a class=\"mh-profile\" id=\"mhProfile\" href=\"/\" title=\"Active profile \u2014 switch from the home page\"></a>\n <span class=\"mh-status\" id=\"mhStatus\"><span class=\"mh-dot\" id=\"mhDot\"></span><span class=\"mh-status-text\" id=\"mhStatusText\"></span></span>\n </div>\n</header>\n\n<div class=\"content\">\n <h1>SDK Features <span style=\"font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(210,153,34,0.15);color:var(--yellow);vertical-align:middle;margin-left:8px\">Experimental</span></h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Unlock Claude Code features for any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md \u2014 normally\n exclusive to Claude Code \u2014 become available to OpenCode, Crush, Droid, and any other harness routed through Meridian.\n Each agent keeps its own toolchain while gaining access to these additional features.<br><br>\n <strong style=\"color:var(--text)\">System prompts:</strong> For these features to work correctly, both the Claude Code prompt and your client prompt\n should be enabled. When both are active, they are appended together \u2014 Claude Code's base instructions come first,\n followed by your agent's specific instructions.\n </p>\n\n <div id=\"adapters\"></div>\n\n <h1 style=\"margin-top:40px\">Routing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n How unpinned requests choose an account. <strong style=\"color:var(--text)\">Active</strong> uses the manually\n selected profile. <strong style=\"color:var(--text)\">Sticky</strong> distributes sessions across profiles evenly\n (cache-affine). <strong style=\"color:var(--text)\">Priority</strong> drains the pool in order \u2014 highest first \u2014\n and fails over per request when an account runs out; conversations keep their account, and new sessions\n return to the preferred account after its window resets. An explicit <code>x-meridian-profile</code> header\n always overrides. Changes apply to the next request \u2014 no restart needed.\n </p>\n <div class=\"adapter-card\" id=\"routing-card\">\n <div id=\"routing-body\">Loading\u2026</div>\n </div>\n\n <h1 style=\"margin-top:40px\">Model Pricing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the\n built-in rate, or add models the built-in table doesn't know about (they show as \"no pricing\" on the\n dashboard until defined here). Changes apply on the next dashboard refresh.\n </p>\n <div class=\"adapter-card\">\n <table class=\"pricing-table\">\n <thead><tr><th>Model</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Source</th><th></th></tr></thead>\n <tbody id=\"pricingRows\"></tbody>\n </table>\n <div class=\"pricing-add\">\n <input type=\"text\" class=\"pricing-input\" id=\"newModelName\" placeholder=\"model id (e.g. claude-opus-9)\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelInput\" placeholder=\"input\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelOutput\" placeholder=\"output\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheRead\" placeholder=\"cache read\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheWrite\" placeholder=\"cache write\" min=\"0\" step=\"0.01\">\n <button class=\"add-btn\" onclick=\"addPricingModel()\">Add Model</button>\n </div>\n <div class=\"pricing-note\">\n Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the\n input rate (the 5-minute cache TTL multipliers). Verify current list prices at\n <a href=\"https://claude.com/pricing\" target=\"_blank\" rel=\"noreferrer\" style=\"color:var(--accent)\">claude.com/pricing</a>.\n </div>\n </div>\n</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '&quot;');\n row.innerHTML = info +\n '<input type=\"text\" class=\"feature-select\" style=\"width:180px\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">';\n }\n\n grid.appendChild(row);\n }\n\n card.appendChild(grid);\n container.appendChild(card);\n }\n}\n\n// ---- Model pricing (telemetry cost estimate) ----\nlet pricingData = { builtin: {}, overrides: {} };\n\nfunction fmtRate(v) { return String(Math.round(v * 10000) / 10000); }\n\nasync function loadPricing() {\n const res = await fetch('/settings/api/pricing');\n pricingData = await res.json();\n renderPricing();\n}\n\nasync function putPricing(model, rates) {\n const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(rates),\n });\n if (!res.ok) {\n const err = await res.json().catch(function () { return {}; });\n alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));\n return false;\n }\n showSaved();\n return true;\n}\n\nasync function removePricing(model) {\n await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });\n showSaved();\n await loadPricing();\n}\n\nfunction rateCell(value, onCommit) {\n const td = document.createElement('td');\n const input = document.createElement('input');\n input.type = 'number';\n input.min = '0';\n input.step = '0.01';\n input.className = 'pricing-input';\n input.value = fmtRate(value);\n input.addEventListener('change', onCommit);\n td.appendChild(input);\n return { td: td, input: input };\n}\n\nfunction renderPricing() {\n const tbody = document.getElementById('pricingRows');\n tbody.innerHTML = '';\n const models = Array.from(new Set(\n Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))\n )).sort();\n\n for (const model of models) {\n const override = pricingData.overrides[model];\n const effective = override || pricingData.builtin[model];\n const tr = document.createElement('tr');\n\n const nameTd = document.createElement('td');\n nameTd.className = 'pricing-model';\n nameTd.textContent = model;\n tr.appendChild(nameTd);\n\n const cells = [];\n const commit = async function () {\n const rates = {\n inputPerMTok: parseFloat(cells[0].input.value),\n outputPerMTok: parseFloat(cells[1].input.value),\n cacheReadPerMTok: parseFloat(cells[2].input.value),\n cacheWritePerMTok: parseFloat(cells[3].input.value),\n };\n for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }\n if (await putPricing(model, rates)) await loadPricing();\n };\n ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {\n const cell = rateCell(effective[key], commit);\n cells.push(cell);\n tr.appendChild(cell.td);\n });\n\n const badgeTd = document.createElement('td');\n const badge = document.createElement('span');\n badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');\n badge.textContent = override ? 'Override' : 'Built-in';\n badgeTd.appendChild(badge);\n tr.appendChild(badgeTd);\n\n const actionTd = document.createElement('td');\n if (override) {\n const btn = document.createElement('button');\n btn.className = 'reset-btn';\n btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';\n btn.addEventListener('click', function () { removePricing(model); });\n actionTd.appendChild(btn);\n }\n tr.appendChild(actionTd);\n\n tbody.appendChild(tr);\n }\n}\n\nasync function addPricingModel() {\n const name = document.getElementById('newModelName').value.trim();\n const inputRate = parseFloat(document.getElementById('newModelInput').value);\n const outputRate = parseFloat(document.getElementById('newModelOutput').value);\n if (!name) { alert('Enter a model id'); return; }\n if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }\n const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };\n const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);\n const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);\n if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;\n if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;\n if (await putPricing(name, rates)) {\n ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {\n document.getElementById(id).value = '';\n });\n await loadPricing();\n }\n}\n\nasync function loadRouting() {\n const res = await fetch('/settings/api/routing');\n const cfg = await res.json();\n const el = document.getElementById('routing-body');\n const envNote = (on) => on ? ' <span style=\"font-size:11px;color:var(--yellow)\">(env override active \u2014 setting saved but env wins)</span>' : '';\n let h = '<div style=\"display:flex;align-items:center;gap:12px;margin-bottom:14px\">'\n + '<label style=\"color:var(--muted);font-size:13px;width:90px\">Mode</label>'\n + '<select id=\"routing-mode\" style=\"background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px\">'\n + ['active','sticky','priority'].map(m => '<option value=\"'+m+'\"'+(cfg.routing===m?' selected':'')+'>'+m+'</option>').join('')\n + '</select>' + envNote(cfg.envOverride.routing) + '</div>';\n h += '<div id=\"routing-order-wrap\" style=\"'+(cfg.routing==='priority'?'':'display:none')+'\">'\n + '<div style=\"color:var(--muted);font-size:13px;margin-bottom:8px\">Pool order \u2014 highest priority first. Drained top to bottom.'\n + envNote(cfg.envOverride.profileOrder) + '</div>'\n + '<ol id=\"routing-order\" style=\"margin:0;padding-left:22px\">'\n + cfg.profileOrder.map((id, i) =>\n '<li style=\"padding:4px 0;color:var(--text)\">'\n + '<span style=\"font-family:var(--mono, monospace)\">'+id+'</span>'\n + ' <button data-move=\"up\" data-i=\"'+i+'\" style=\"margin-left:10px;background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer\"'+(i===0?' disabled':'')+'>&uarr;</button>'\n + ' <button data-move=\"down\" data-i=\"'+i+'\" style=\"background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer\"'+(i===cfg.profileOrder.length-1?' disabled':'')+'>&darr;</button>'\n + '</li>').join('')\n + '</ol></div>';\n el.innerHTML = h;\n document.getElementById('routing-mode').addEventListener('change', async (e) => {\n await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ routing: e.target.value }) });\n await loadRouting();\n });\n el.querySelectorAll('button[data-move]').forEach(btn => btn.addEventListener('click', async () => {\n const i = Number(btn.dataset.i);\n const j = btn.dataset.move === 'up' ? i - 1 : i + 1;\n const order = cfg.profileOrder.slice();\n const tmp = order[i]; order[i] = order[j]; order[j] = tmp;\n await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileOrder: order }) });\n await loadRouting();\n }));\n}\n\nloadConfig();\nloadPricing();\nloadRouting();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-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 loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n\n</script>\n</body>\n</html>";
6
6
  //# sourceMappingURL=settingsPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,+p0BAuZrB,CAAA"}
1
+ {"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,yw7BAycrB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.55.0",
3
+ "version": "1.56.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",