@rynfar/meridian 1.71.1 → 1.73.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.
Files changed (125) hide show
  1. package/README.md +115 -162
  2. package/assets/desktop-dashboard.jpg +0 -0
  3. package/assets/trayTemplate.png +0 -0
  4. package/assets/trayTemplate@2x.png +0 -0
  5. package/assets/trayTemplate@3x.png +0 -0
  6. package/dist/cli-1jstkdj9.js +30 -0
  7. package/dist/{cli-9e5cxp89.js → cli-2h3w5n6g.js} +163 -65
  8. package/dist/cli-3zwfvexc.js +56 -0
  9. package/dist/{cli-hxxy0m1z.js → cli-82e2m44k.js} +2 -1
  10. package/dist/cli-8sw5wr9j.js +136 -0
  11. package/dist/{cli-khhjyk04.js → cli-chsp880k.js} +214 -21
  12. package/dist/cli-dh0a1bw4.js +619 -0
  13. package/dist/cli-eehk3052.js +57 -0
  14. package/dist/cli-grfp0s1j.js +330 -0
  15. package/dist/{cli-ryt69ryf.js → cli-p5zbk952.js} +2185 -358
  16. package/dist/{cli-8yp89fan.js → cli-p9ca4p13.js} +17 -2
  17. package/dist/cli-qpfky8wy.js +133 -0
  18. package/dist/cli.js +47 -12
  19. package/dist/cliDashboard-c0dvkj4y.js +325 -0
  20. package/dist/configDir.d.ts +30 -0
  21. package/dist/configDir.d.ts.map +1 -0
  22. package/dist/organizationName-90f0atkn.js +29 -0
  23. package/dist/{pluginPage-n3gdtbmj.js → pluginPage-3kxsrdpj.js} +1 -1
  24. package/dist/{profileCli-1eecbmgk.js → profileCli-1bfgc05h.js} +13 -3
  25. package/dist/{profilePage-vfj2dbhn.js → profilePage-etxqg3vn.js} +252 -60
  26. package/dist/profileRename-4a4km6g8.js +22 -0
  27. package/dist/{profiles-wch9h234.js → profiles-0mp32ek9.js} +16 -3
  28. package/dist/proxy/adapterInstances.d.ts +1 -1
  29. package/dist/proxy/adapterInstances.d.ts.map +1 -1
  30. package/dist/proxy/adapters/claudecode.d.ts.map +1 -1
  31. package/dist/proxy/adapters/droid.d.ts.map +1 -1
  32. package/dist/proxy/authDiscovery.d.ts +52 -0
  33. package/dist/proxy/authDiscovery.d.ts.map +1 -0
  34. package/dist/proxy/credentialsMode.d.ts +77 -0
  35. package/dist/proxy/credentialsMode.d.ts.map +1 -0
  36. package/dist/proxy/design.d.ts.map +1 -1
  37. package/dist/proxy/errors.d.ts.map +1 -1
  38. package/dist/proxy/followActive.d.ts +262 -0
  39. package/dist/proxy/followActive.d.ts.map +1 -0
  40. package/dist/proxy/limitDetection.d.ts +110 -0
  41. package/dist/proxy/limitDetection.d.ts.map +1 -0
  42. package/dist/proxy/models.d.ts.map +1 -1
  43. package/dist/proxy/oauthPlan.d.ts +63 -0
  44. package/dist/proxy/oauthPlan.d.ts.map +1 -0
  45. package/dist/proxy/oauthUsage.d.ts +65 -2
  46. package/dist/proxy/oauthUsage.d.ts.map +1 -1
  47. package/dist/proxy/openai.d.ts +4 -2
  48. package/dist/proxy/openai.d.ts.map +1 -1
  49. package/dist/proxy/organizationName.d.ts +71 -0
  50. package/dist/proxy/organizationName.d.ts.map +1 -0
  51. package/dist/proxy/passthroughEarlyStop.d.ts +10 -0
  52. package/dist/proxy/passthroughEarlyStop.d.ts.map +1 -1
  53. package/dist/proxy/planAllowance.d.ts +75 -0
  54. package/dist/proxy/planAllowance.d.ts.map +1 -0
  55. package/dist/proxy/probes.d.ts +85 -0
  56. package/dist/proxy/probes.d.ts.map +1 -0
  57. package/dist/proxy/profileCli.d.ts +53 -1
  58. package/dist/proxy/profileCli.d.ts.map +1 -1
  59. package/dist/proxy/profileHealth.d.ts +127 -0
  60. package/dist/proxy/profileHealth.d.ts.map +1 -0
  61. package/dist/proxy/profileRename.d.ts +83 -0
  62. package/dist/proxy/profileRename.d.ts.map +1 -0
  63. package/dist/proxy/profiles.d.ts +72 -6
  64. package/dist/proxy/profiles.d.ts.map +1 -1
  65. package/dist/proxy/query.d.ts +7 -0
  66. package/dist/proxy/query.d.ts.map +1 -1
  67. package/dist/proxy/routing.d.ts +59 -1
  68. package/dist/proxy/routing.d.ts.map +1 -1
  69. package/dist/proxy/sdkFeatures.d.ts +1 -1
  70. package/dist/proxy/sdkFeatures.d.ts.map +1 -1
  71. package/dist/proxy/server.d.ts.map +1 -1
  72. package/dist/proxy/session/fingerprint.d.ts +5 -0
  73. package/dist/proxy/session/fingerprint.d.ts.map +1 -1
  74. package/dist/proxy/session/lineage.d.ts.map +1 -1
  75. package/dist/proxy/session/processIncarnation.d.ts +7 -0
  76. package/dist/proxy/session/processIncarnation.d.ts.map +1 -1
  77. package/dist/proxy/sessionLifecycle.d.ts +8 -1
  78. package/dist/proxy/sessionLifecycle.d.ts.map +1 -1
  79. package/dist/proxy/sessionStore.d.ts.map +1 -1
  80. package/dist/proxy/setup.d.ts.map +1 -1
  81. package/dist/proxy/supervision.d.ts +41 -0
  82. package/dist/proxy/supervision.d.ts.map +1 -0
  83. package/dist/proxy/tokenRefresh.d.ts +38 -4
  84. package/dist/proxy/tokenRefresh.d.ts.map +1 -1
  85. package/dist/server.js +11 -7
  86. package/dist/settings.d.ts +83 -0
  87. package/dist/settings.d.ts.map +1 -0
  88. package/dist/{setup-ndmjpy23.js → setup-7fr49z92.js} +1 -1
  89. package/dist/statusProbe-9x9q261e.js +151 -0
  90. package/dist/telemetry/dashboard.d.ts.map +1 -1
  91. package/dist/telemetry/index.d.ts +20 -2
  92. package/dist/telemetry/index.d.ts.map +1 -1
  93. package/dist/telemetry/landing.d.ts +1 -1
  94. package/dist/telemetry/landing.d.ts.map +1 -1
  95. package/dist/telemetry/logStore.d.ts +11 -0
  96. package/dist/telemetry/logStore.d.ts.map +1 -1
  97. package/dist/telemetry/pricingStore.d.ts +2 -2
  98. package/dist/telemetry/pricingStore.d.ts.map +1 -1
  99. package/dist/telemetry/profileBar.d.ts +10 -2
  100. package/dist/telemetry/profileBar.d.ts.map +1 -1
  101. package/dist/telemetry/profileFacts.d.ts +17 -0
  102. package/dist/telemetry/profileFacts.d.ts.map +1 -0
  103. package/dist/telemetry/profileOrder.d.ts +68 -0
  104. package/dist/telemetry/profileOrder.d.ts.map +1 -0
  105. package/dist/telemetry/profilePage.d.ts.map +1 -1
  106. package/dist/telemetry/profileSort.d.ts +35 -0
  107. package/dist/telemetry/profileSort.d.ts.map +1 -0
  108. package/dist/telemetry/profileSpent.d.ts +68 -0
  109. package/dist/telemetry/profileSpent.d.ts.map +1 -0
  110. package/dist/telemetry/routeChain.d.ts +32 -0
  111. package/dist/telemetry/routeChain.d.ts.map +1 -0
  112. package/dist/telemetry/routes.d.ts.map +1 -1
  113. package/dist/telemetry/settingsPage.d.ts +1 -1
  114. package/dist/telemetry/settingsPage.d.ts.map +1 -1
  115. package/dist/telemetry/sqlite.d.ts.map +1 -1
  116. package/dist/telemetry/store.d.ts +6 -1
  117. package/dist/telemetry/store.d.ts.map +1 -1
  118. package/dist/telemetry/types.d.ts +132 -2
  119. package/dist/telemetry/types.d.ts.map +1 -1
  120. package/dist/{tokenRefresh-kzz08kea.js → tokenRefresh-eg9h83wd.js} +6 -1
  121. package/package.json +2 -1
  122. package/dist/cli-5jxyma6z.js +0 -277
  123. package/dist/cli-vj9cv18n.js +0 -38
  124. package/dist/proxy/settings.d.ts +0 -30
  125. package/dist/proxy/settings.d.ts.map +0 -1
@@ -4,7 +4,7 @@ import {
4
4
  profileBarHtml,
5
5
  profileBarJs,
6
6
  themeCss
7
- } from "./cli-8yp89fan.js";
7
+ } from "./cli-p9ca4p13.js";
8
8
  import"./cli-p9swy5t3.js";
9
9
 
10
10
  // src/proxy/plugins/pluginPage.ts
@@ -3,31 +3,41 @@ import {
3
3
  OAUTH_REDIRECT_URI,
4
4
  OAUTH_TOKEN_URL,
5
5
  buildAuthLoginEnv,
6
+ buildLoginCredentials,
6
7
  createManualOAuthSession,
7
8
  dirsToRemoveOnProfileRemove,
9
+ isValidProfileId,
8
10
  parseAuthorizationCodeInput,
11
+ planProfileLogin,
9
12
  profileAdd,
10
13
  profileAddOauthToken,
11
14
  profileHelp,
12
15
  profileList,
13
16
  profileLogin,
14
17
  profileRemove,
18
+ profileRename,
15
19
  profileSwitch
16
- } from "./cli-9e5cxp89.js";
17
- import"./cli-khhjyk04.js";
18
- import"./cli-vj9cv18n.js";
20
+ } from "./cli-2h3w5n6g.js";
21
+ import"./cli-chsp880k.js";
22
+ import"./cli-1jstkdj9.js";
23
+ import"./cli-qpfky8wy.js";
24
+ import"./cli-eehk3052.js";
19
25
  import"./cli-p9swy5t3.js";
20
26
  export {
21
27
  profileSwitch,
28
+ profileRename,
22
29
  profileRemove,
23
30
  profileLogin,
24
31
  profileList,
25
32
  profileHelp,
26
33
  profileAddOauthToken,
27
34
  profileAdd,
35
+ planProfileLogin,
28
36
  parseAuthorizationCodeInput,
37
+ isValidProfileId,
29
38
  dirsToRemoveOnProfileRemove,
30
39
  createManualOAuthSession,
40
+ buildLoginCredentials,
31
41
  buildAuthLoginEnv,
32
42
  OAUTH_TOKEN_URL,
33
43
  OAUTH_REDIRECT_URI,
@@ -1,29 +1,23 @@
1
+ import {
2
+ WINDOW_LABELS
3
+ } from "./cli-3zwfvexc.js";
4
+ import {
5
+ profileFactsJs,
6
+ reorderClientJs,
7
+ reorderCss,
8
+ reorderLiveRegionHtml
9
+ } from "./cli-grfp0s1j.js";
1
10
  import {
2
11
  init_profileBar,
3
12
  profileBarCss,
4
13
  profileBarHtml,
5
14
  profileBarJs,
6
15
  themeCss
7
- } from "./cli-8yp89fan.js";
16
+ } from "./cli-p9ca4p13.js";
8
17
  import"./cli-p9swy5t3.js";
9
18
 
10
19
  // src/telemetry/profilePage.ts
11
20
  init_profileBar();
12
-
13
- // src/telemetry/profileUsage.ts
14
- var WINDOW_LABELS = {
15
- five_hour: "5h",
16
- seven_day: "7d",
17
- seven_day_opus: "7d Opus",
18
- seven_day_sonnet: "7d Sonnet",
19
- seven_day_fable: "7d Fable",
20
- seven_day_oauth_apps: "7d Apps",
21
- seven_day_cowork: "7d Cowork",
22
- seven_day_omelette: "7d Omelette"
23
- };
24
- var SEVEN_DAYS_MS = 7 * 86400000;
25
-
26
- // src/telemetry/profilePage.ts
27
21
  var profilePageHtml = `<!DOCTYPE html>
28
22
  <html lang="en">
29
23
  <head>
@@ -48,18 +42,40 @@ var profilePageHtml = `<!DOCTYPE html>
48
42
  }
49
43
  .profile-card.active { border-color: var(--accent); }
50
44
  .profile-card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
45
+ ${reorderCss}
51
46
  .profile-name { font-size: 16px; font-weight: 600; }
47
+ .profile-card-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; }
48
+ .icon-btn {
49
+ background: var(--bg); color: var(--muted); border: 1px solid var(--border);
50
+ border-radius: 4px; padding: 4px 6px; cursor: pointer; display: inline-flex;
51
+ align-items: center; transition: all 0.15s;
52
+ }
53
+ .icon-btn:hover { border-color: var(--accent); color: var(--accent); }
54
+ .rename-input {
55
+ background: var(--surface2); color: var(--text); border: 1px solid var(--accent);
56
+ border-radius: 6px; padding: 4px 8px; font-size: 14px; font-weight: 600;
57
+ font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; width: 200px;
58
+ }
59
+ .rename-input:focus { outline: none; }
60
+ .rename-hint { font-size: 11px; color: var(--muted); }
61
+ .rename-error { font-size: 12px; color: var(--red); margin-bottom: 12px; }
52
62
  .profile-badge {
53
63
  font-size: 10px; padding: 2px 8px; border-radius: 4px; text-transform: uppercase;
54
64
  letter-spacing: 0.5px; font-weight: 500;
55
65
  }
56
66
  .badge-active { background: rgba(88,166,255,0.15); color: var(--accent); }
57
67
  .badge-type { background: var(--bg); color: var(--muted); border: 1px solid var(--border); }
68
+ .badge-spent { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid rgba(248,81,73,0.35); }
69
+ .spent-note { margin: 10px 0; padding: 10px 14px; border-radius: 8px; font-size: 12px; line-height: 1.5;
70
+ background: rgba(248,81,73,0.08); border: 1px solid rgba(248,81,73,0.3); color: var(--text); }
71
+ .spent-note .spent-why { color: var(--muted); }
58
72
  .profile-details {
59
73
  display: grid; grid-template-columns: 120px 1fr; gap: 6px 16px; font-size: 13px;
60
74
  }
61
75
  .detail-label { color: var(--muted); }
62
76
  .detail-value { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; }
77
+ .cached-tag { color: var(--muted); font-size: 10px; font-style: italic; margin-left: 6px; white-space: nowrap; }
78
+ .detail-unknown { color: var(--muted); font-style: italic; }
63
79
  .status-ok { color: var(--green); }
64
80
  .status-err { color: var(--red); }
65
81
  .switch-btn {
@@ -115,6 +131,7 @@ var profilePageHtml = `<!DOCTYPE html>
115
131
  margin-bottom: 10px; display: flex; align-items: center; gap: 8px;
116
132
  }
117
133
  .usage-as-of { font-size: 10px; color: var(--muted); text-transform: none; letter-spacing: 0; opacity: 0.7; }
134
+ .usage-stale-note { font-size: 11px; color: var(--yellow); line-height: 1.4; margin: -2px 0 10px; }
118
135
  .usage-grid {
119
136
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
120
137
  gap: 8px;
@@ -159,6 +176,7 @@ var profilePageHtml = `<!DOCTYPE html>
159
176
  <div class="subtitle">Manage Claude account profiles</div>
160
177
 
161
178
  <div id="content"><div style="color:var(--muted);padding:40px;text-align:center">Loading…</div></div>
179
+ ${reorderLiveRegionHtml}
162
180
 
163
181
  <div class="section" style="margin-top:32px">
164
182
  <div class="section-title">Setup Guide</div>
@@ -195,13 +213,20 @@ var profilePageHtml = `<!DOCTYPE html>
195
213
  <div style="font-size:13px;margin-top:8px">
196
214
  <code>meridian profile list</code> — show all profiles and auth status<br>
197
215
  <code>meridian profile login &lt;name&gt;</code> — re-authenticate an expired profile<br>
216
+ <code>meridian profile rename &lt;old&gt; &lt;new&gt;</code> — rename a profile (or use the pencil above)<br>
198
217
  <code>meridian profile remove &lt;name&gt;</code> — remove a profile
199
218
  </div>
219
+ <p style="font-size:13px;color:var(--muted);margin-top:12px">
220
+ Renaming keeps the old name working: requests still naming it are served by
221
+ the renamed profile, so nothing breaks mid-flight. That redirect is dropped
222
+ as soon as the old name is taken again by a new profile.
223
+ </p>
200
224
  </div>
201
225
  </div>
202
226
  </div>
203
227
 
204
228
  <script>
229
+ ` + profileFactsJs + `
205
230
  // Inlined from src/telemetry/profileUsage.ts. The TS source is unit-tested
206
231
  // (see profile-usage.test.ts) and the labels object is interpolated here so
207
232
  // the browser script and TS module share their data.
@@ -252,15 +277,80 @@ function formatExtraUsage(eu) {
252
277
  };
253
278
  }
254
279
 
280
+ ${reorderClientJs}
281
+
255
282
  // Cache the last seen quota response so the /profiles/list refresh can
256
283
  // keep showing usage even if a single /v1/usage/quota/all call fails.
257
284
  var lastQuota = null;
285
+ // Last profile payload, so a rename can redraw from cache without refetching.
286
+ var lastProfiles = null;
287
+ // Profile whose name is being edited in place, and the error from the last
288
+ // rejected attempt. While editing, the poll is suspended — it rewrites
289
+ // innerHTML, which would blank the input mid-keystroke.
290
+ var editingProfile = null;
291
+ var renameError = null;
292
+
293
+ var ICON_PENCIL = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"/></svg>';
294
+ var ICON_CHECK = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg>';
295
+ var ICON_X = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>';
296
+
297
+ function focusRenameInput() {
298
+ var el = document.getElementById('rename-input');
299
+ if (el) { el.focus(); el.select(); }
300
+ }
301
+
302
+ function redraw() {
303
+ if (lastProfiles) render(lastProfiles, lastQuota);
304
+ }
305
+
306
+ function startRename(id) {
307
+ editingProfile = id;
308
+ renameError = null;
309
+ redraw();
310
+ focusRenameInput();
311
+ }
312
+
313
+ function cancelRename() {
314
+ editingProfile = null;
315
+ renameError = null;
316
+ redraw();
317
+ }
318
+
319
+ async function commitRename(from) {
320
+ var input = document.getElementById('rename-input');
321
+ if (!input) return;
322
+ var to = input.value.trim();
323
+ if (!to || to === from) { cancelRename(); return; }
324
+ var data;
325
+ try {
326
+ var res = await fetch('/profiles/rename', {
327
+ method: 'POST',
328
+ headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ from: from, to: to })
330
+ });
331
+ data = await res.json();
332
+ } catch (err) {
333
+ data = { error: 'Could not reach Meridian.' };
334
+ }
335
+ if (data && data.success) {
336
+ editingProfile = null;
337
+ renameError = null;
338
+ await refresh();
339
+ if (window.meridianHeaderRefresh) window.meridianHeaderRefresh();
340
+ return;
341
+ }
342
+ renameError = (data && data.error) || 'Rename failed.';
343
+ redraw();
344
+ focusRenameInput();
345
+ }
258
346
 
259
347
  async function refresh() {
348
+ if (editingProfile) return;
260
349
  try {
261
- var [profilesRes, quotaRes] = await Promise.all([
350
+ var [profilesRes, quotaRes, routingRes] = await Promise.all([
262
351
  fetch('/profiles/list'),
263
352
  fetch('/v1/usage/quota/all').catch(function () { return null; }),
353
+ fetch('/settings/api/routing').catch(function () { return null; }),
264
354
  ]);
265
355
  var profiles = await profilesRes.json();
266
356
  var quota = null;
@@ -268,6 +358,10 @@ async function refresh() {
268
358
  try { quota = await quotaRes.json(); } catch (_) { quota = null; }
269
359
  }
270
360
  if (quota) lastQuota = quota;
361
+ if (routingRes && routingRes.ok) {
362
+ try { meridianReorder.adopt(await routingRes.json()); } catch (_) { /* keep the last good order */ }
363
+ }
364
+ lastProfiles = profiles;
271
365
  render(profiles, lastQuota);
272
366
  } catch {
273
367
  document.getElementById('content').innerHTML = '<div class="empty-state"><h2>Could not load profiles</h2><p>Is Meridian running?</p></div>';
@@ -276,6 +370,88 @@ async function refresh() {
276
370
 
277
371
  function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
278
372
 
373
+ function factRows(facts) {
374
+ return facts.map(function (f) {
375
+ var tone = f.tone === 'ok' ? ' status-ok' : f.tone === 'err' ? ' status-err' : '';
376
+ var title = f.title ? ' title="' + esc(f.title) + '"' : '';
377
+ var cached = f.cached ? ' <span class="cached-tag">(cached)</span>' : '';
378
+ return '<span class="detail-label">' + esc(f.label) + '</span>'
379
+ + '<span class="detail-value' + tone + '"' + title + '>' + esc(f.value) + cached + '</span>';
380
+ }).join('');
381
+ }
382
+
383
+ // Mirrors src/telemetry/cachedFacts.ts (unit-tested there). Marked per value
384
+ // rather than per card: a card mixes a live status with a remembered email, so
385
+ // one banner across it would mislabel whichever half it doesn't apply to.
386
+ function factProvenance(value, stale) {
387
+ if (value == null || value === '') return 'never';
388
+ return stale ? 'cached' : 'live';
389
+ }
390
+ function cachedTag(provenance) {
391
+ return provenance === 'cached' ? '<span class="cached-tag">(cached)</span>' : '';
392
+ }
393
+ function renderFactValue(value, stale, extraClass) {
394
+ var provenance = factProvenance(value, stale);
395
+ if (provenance === 'never' && !stale) return null;
396
+ var classes = 'detail-value' + (extraClass ? ' ' + extraClass : '');
397
+ if (provenance === 'never') {
398
+ return '<span class="' + classes + ' detail-unknown">never read</span>';
399
+ }
400
+ return '<span class="' + classes + '">' + esc(String(value)) + cachedTag(provenance) + '</span>';
401
+ }
402
+
403
+ // "last 4 checks failed (rate limited upstream)" — the run of failed checks
404
+ // since the last good reading, or '' when the last check succeeded. Reasons map
405
+ // through a fixed vocabulary rather than being echoed, so nothing the server
406
+ // sends reaches the DOM.
407
+ function describeFailedRun(failure) {
408
+ if (!failure) return '';
409
+ var why = failure.reason === 'rate_limited' ? 'rate limited upstream'
410
+ : failure.reason === 'no_token' ? 'no credentials readable'
411
+ : 'usage endpoint unavailable';
412
+ var n = Math.floor(Number(failure.consecutiveFailures));
413
+ if (!isFinite(n) || n < 1) n = 1;
414
+ return (n > 1 ? 'last ' + n + ' checks failed' : 'last check failed') + ' (' + why + ')';
415
+ }
416
+
417
+ // A refusal and the cached percentages are different kinds of fact, so they
418
+ // are rendered as different things: the badge states what the API is doing
419
+ // now, the bars below stay as the last successful read. Measured: an account
420
+ // showed 5h 67% / 7d 7% while every request through it was refused.
421
+ function spentSummary(spent) {
422
+ if (!spent) return null;
423
+ var bucket = spent.diagnosis && spent.diagnosis.bucket
424
+ ? labelForWindow(spent.diagnosis.bucket)
425
+ : 'unknown limit';
426
+ var reported = !!(spent.diagnosis && spent.diagnosis.reported);
427
+ var reset = formatResetCountdown(spent.until);
428
+ return {
429
+ bucket: bucket,
430
+ reported: reported,
431
+ label: bucket + (reported ? '' : ' (guess)'),
432
+ reset: reset,
433
+ why: (spent.diagnosis && spent.diagnosis.rationale) || '',
434
+ at: spent.at,
435
+ };
436
+ }
437
+
438
+ function renderSpentBadge(spent) {
439
+ var s = spentSummary(spent);
440
+ if (!s) return '';
441
+ return '<span class="profile-badge badge-spent" title="' + esc(s.why) + '">out of ' + esc(s.label) + '</span>';
442
+ }
443
+
444
+ function renderSpentNote(spent) {
445
+ var s = spentSummary(spent);
446
+ if (!s) return '';
447
+ return '<div class="spent-note">'
448
+ + '<strong style="color:var(--red)">⚠ Anthropic is refusing this account</strong> - '
449
+ + 'out of <strong>' + esc(s.label) + '</strong>'
450
+ + (s.reset ? ', expected back ' + esc(s.reset) : '')
451
+ + '. Refused ' + esc(timeAgo(s.at)) + '.'
452
+ + '<div class="spent-why">' + esc(s.why) + '. The percentages below are the last successful read, not live.</div>'
453
+ + '</div>';
454
+
279
455
  function renderUsageSection(profileQuota) {
280
456
  // No quota data for this profile yet (cold start or fetch failed) — hide
281
457
  // entirely so we don't render an empty box.
@@ -288,6 +464,13 @@ function renderUsageSection(profileQuota) {
288
464
  });
289
465
  var extra = formatExtraUsage(profileQuota.extraUsage);
290
466
 
467
+ var failedRun = describeFailedRun(profileQuota.failure);
468
+ var usageTag = cachedTag(profileQuota.stale ? 'cached' : 'live');
469
+
470
+ // No figures at all means this profile has never been read successfully —
471
+ // the route serves the last good reading at any age, so an empty windows
472
+ // array is no longer "the stale window lapsed". Saying so keeps it distinct
473
+ // from a profile genuinely sitting at 0%.
291
474
  if (windows.length === 0 && !extra) {
292
475
  if (profileQuota.error === 'no_token') {
293
476
  return '<div class="usage-section">'
@@ -296,12 +479,13 @@ function renderUsageSection(profileQuota) {
296
479
  + '</div>';
297
480
  }
298
481
  // Credentials are fine here — Anthropic is throttling the usage endpoint
299
- // and the backoff has no snapshot left to serve. Saying "run claude login"
300
- // would send the user chasing a problem they don't have.
482
+ // and there has never been a reading to fall back on. Saying "run claude
483
+ // login" would send the user chasing a problem they don't have.
301
484
  if (profileQuota.error === 'rate_limited') {
302
485
  return '<div class="usage-section">'
303
486
  + '<div class="usage-section-title">Usage</div>'
304
- + '<div class="usage-empty">Usage data is rate limited upstream retrying shortly.</div>'
487
+ + '<div class="usage-empty">No reading yet — '
488
+ + esc(failedRun || 'rate limited upstream') + ', retrying.</div>'
305
489
  + '</div>';
306
490
  }
307
491
  return ''; // nothing fetched yet
@@ -311,6 +495,15 @@ function renderUsageSection(profileQuota) {
311
495
  ? '<span class="usage-as-of">updated ' + timeAgo(profileQuota.fetchedAt) + '</span>'
312
496
  : '';
313
497
 
498
+ // Figures are only ever this old because a later check failed, so the note
499
+ // and the "updated Xm ago" beside the title answer the two halves of the
500
+ // same question: how old these numbers are, and why they haven't moved.
501
+ var staleNote = failedRun
502
+ ? '<div class="usage-stale-note">'
503
+ + esc(failedRun.charAt(0).toUpperCase() + failedRun.slice(1))
504
+ + ' — figures below are the last successful read.</div>'
505
+ : '';
506
+
314
507
  var cards = windows.map(function (w) {
315
508
  var pct = Math.max(0, Math.min(1, w.utilization));
316
509
  var pctRound = Math.round(pct * 100);
@@ -321,7 +514,7 @@ function renderUsageSection(profileQuota) {
321
514
  return '<div class="usage-card status-' + esc(status) + '" title="' + esc(tip) + '">'
322
515
  + '<div class="usage-row">'
323
516
  + '<span class="usage-label">' + esc(label) + '</span>'
324
- + '<span class="usage-pct">' + pctRound + '%</span>'
517
+ + '<span class="usage-pct">' + pctRound + '%' + usageTag + '</span>'
325
518
  + '</div>'
326
519
  + '<div class="usage-bar"><div class="usage-fill" style="width:' + (pct * 100).toFixed(1) + '%"></div></div>'
327
520
  + (reset ? '<div class="usage-reset">' + esc(reset) + '</div>' : '')
@@ -333,7 +526,7 @@ function renderUsageSection(profileQuota) {
333
526
  extraBlock = '<div class="usage-extra status-' + esc(extra.status) + '">'
334
527
  + '<div class="usage-extra-row">'
335
528
  + '<span class="usage-label">Extra usage</span>'
336
- + '<span class="usage-pct">' + extra.utilizationPct + '%</span>'
529
+ + '<span class="usage-pct">' + extra.utilizationPct + '%' + usageTag + '</span>'
337
530
  + '</div>'
338
531
  + '<div class="usage-bar"><div class="usage-fill" style="width:' + extra.utilizationPct + '%"></div></div>'
339
532
  + '<div class="usage-extra-row" style="margin-top:4px">'
@@ -344,14 +537,16 @@ function renderUsageSection(profileQuota) {
344
537
 
345
538
  return '<div class="usage-section">'
346
539
  + '<div class="usage-section-title">Usage' + asOf + '</div>'
540
+ + staleNote
347
541
  + (cards ? '<div class="usage-grid">' + cards + '</div>' : '')
348
542
  + extraBlock
349
543
  + '</div>';
350
544
  }
351
545
 
352
546
  function render(data, quotaData) {
353
- const profiles = data.profiles || [];
547
+ const profiles = meridianReorder.sortProfiles(data.profiles || []);
354
548
  const active = data.activeProfile;
549
+ const refocusId = meridianReorder.focusAnchor();
355
550
  // Build quick lookup: profileId -> per-profile quota entry from
356
551
  // /v1/usage/quota/all. Endpoint may be unavailable (older Meridian)
357
552
  // or have errored — in that case quotaById is empty and the per-card
@@ -371,39 +566,43 @@ function render(data, quotaData) {
371
566
  return;
372
567
  }
373
568
 
569
+ const reorderable = profiles.length > 1 && !meridianReorder.envPinned();
570
+
374
571
  let html = '<div class="section"><div class="section-title">Configured Profiles</div>';
572
+ if (profiles.length > 1) html += meridianReorder.noteHtml(reorderable);
375
573
 
376
- for (const p of profiles) {
574
+ for (let idx = 0; idx < profiles.length; idx++) {
575
+ const p = profiles[idx];
377
576
  const isActive = p.id === active;
378
- html += '<div class="profile-card' + (isActive ? ' active' : '') + '">';
577
+ html += '<div class="profile-card' + (isActive ? ' active' : '') + '" data-id="' + esc(p.id) + '" data-index="' + idx + '">';
379
578
  html += '<div class="profile-card-header">';
380
- html += '<span class="profile-name">' + esc(p.id) + '</span>';
381
- if (isActive) html += '<span class="profile-badge badge-active">active</span>';
382
- html += '<span class="profile-badge badge-type">' + esc(p.type || 'claude-max') + '</span>';
579
+ if (editingProfile === p.id) {
580
+ html += "<input class="rename-input" id="rename-input" value="" + esc(p.id) + "" spellcheck="false" autocomplete="off""
581
+ + " onkeydown="if(event.key===&quot;Enter&quot;){event.preventDefault();commitRename(&quot;" + esc(p.id) + "&quot;)}"
582
+ + "else if(event.key===&quot;Escape&quot;){cancelRename()}">";
583
+ html += "<span class="rename-hint">Enter to save · Esc to cancel</span>";
584
+ html += "<span class="profile-card-actions">";
585
+ html += "<button class="icon-btn" title="Save new name" onclick="commitRename(&quot;"+esc(p.id)+"&quot;)">" + ICON_CHECK + "</button>";
586
+ html += "<button class="icon-btn" title="Cancel" onclick="cancelRename()">" + ICON_X + "</button>";
587
+ html += "</span>";
588
+ } else {
589
+ if (reorderable) html += meridianReorder.handleHtml(p.id, idx, profiles.length);
590
+ html += "<span class="profile-name">" + esc(p.id) + "</span>";
591
+ if (isActive) html += "<span class="profile-badge badge-active">active</span>";
592
+ html += "<span class="profile-badge badge-type">" + esc(p.type || "claude-max") + "</span>";
593
+ html += renderSpentBadge((quotaById[p.id] || {}).spent);
594
+ html += "<span class="profile-card-actions">";
595
+ html += "<button class="icon-btn" title="Rename profile" onclick="startRename(&quot;"+esc(p.id)+"&quot;)">" + ICON_PENCIL + "</button>";
596
+ html += "</span>";
597
+ }
383
598
  html += '</div>';
599
+ html += renderSpentNote((quotaById[p.id] || {}).spent);
384
600
 
385
- html += '<div class="profile-details">';
386
- html += '<span class="detail-label">Status</span>';
387
- html += '<span class="detail-value ' + (p.loggedIn ? 'status-ok' : 'status-err') + '">'
388
- + (p.loggedIn ? '✓ Authenticated' : '✗ Not logged in') + '</span>';
389
-
390
- if (p.email) {
391
- html += '<span class="detail-label">Email</span>';
392
- html += '<span class="detail-value">' + esc(p.email) + '</span>';
601
+ if (editingProfile === p.id && renameError) {
602
+ html += '<div class="rename-error">' + esc(renameError) + '</div>';
393
603
  }
394
- if (p.subscriptionType) {
395
- html += '<span class="detail-label">Plan</span>';
396
- html += '<span class="detail-value">' + esc(p.subscriptionType) + '</span>';
397
- }
398
- if (p.lastSuccessAt) {
399
- html += '<span class="detail-label">Last Verified</span>';
400
- html += '<span class="detail-value" style="color:var(--green)">' + timeAgo(p.lastSuccessAt) + '</span>';
401
- }
402
- if (p.lastCheckedAt && (!p.lastSuccessAt || p.lastCheckedAt !== p.lastSuccessAt)) {
403
- html += '<span class="detail-label">Last Checked</span>';
404
- html += '<span class="detail-value">' + timeAgo(p.lastCheckedAt) + '</span>';
405
- }
406
- html += '</div>';
604
+
605
+ html += '<div class="profile-details">' + factRows(profileFacts(p)) + '</div>';
407
606
 
408
607
  if (!p.loggedIn) {
409
608
  html += '<div style="margin-top:12px;padding:10px 14px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:8px;font-size:12px">';
@@ -432,16 +631,7 @@ function render(data, quotaData) {
432
631
 
433
632
  html += '</div>';
434
633
  document.getElementById('content').innerHTML = html;
435
- }
436
-
437
- function timeAgo(ts) {
438
- if (!ts) return '—';
439
- var s = Math.floor((Date.now() - ts) / 1000);
440
- if (s < 5) return 'just now';
441
- if (s < 60) return s + 's ago';
442
- if (s < 3600) return Math.floor(s/60) + 'm ago';
443
- if (s < 86400) return Math.floor(s/3600) + 'h ago';
444
- return new Date(ts).toLocaleString();
634
+ meridianReorder.restoreFocus(refocusId);
445
635
  }
446
636
 
447
637
  function copyCmd(btn) {
@@ -463,10 +653,12 @@ async function switchProfile(id) {
463
653
  });
464
654
  const data = await res.json();
465
655
  if (data.success) refresh();
656
+ else if (data.error) alert(data.error);
466
657
  }
467
658
 
659
+ meridianReorder.init({ onSaved: refresh });
468
660
  refresh();
469
- setInterval(refresh, 10000);
661
+ setInterval(function () { if (!meridianReorder.dragging()) refresh(); }, 10000);
470
662
  ` + profileBarJs + `
471
663
  </script>
472
664
  </body>
@@ -0,0 +1,22 @@
1
+ import {
2
+ applyProfileRename,
3
+ defaultProfilesConfigFile,
4
+ defaultProfilesDir,
5
+ isValidProfileId,
6
+ loadProfileConfigFrom,
7
+ planProfileRename,
8
+ reclaimAlias,
9
+ saveProfileConfigTo
10
+ } from "./cli-qpfky8wy.js";
11
+ import"./cli-eehk3052.js";
12
+ import"./cli-p9swy5t3.js";
13
+ export {
14
+ saveProfileConfigTo,
15
+ reclaimAlias,
16
+ planProfileRename,
17
+ loadProfileConfigFrom,
18
+ isValidProfileId,
19
+ defaultProfilesDir,
20
+ defaultProfilesConfigFile,
21
+ applyProfileRename
22
+ };
@@ -1,26 +1,39 @@
1
1
  import {
2
2
  enableDiskProfileDiscovery,
3
+ findProfileByIdOrAlias,
3
4
  getActiveProfileId,
5
+ getEffectiveActiveProfileId,
4
6
  getEffectiveProfiles,
5
7
  hasProfiles,
8
+ invalidateDiskProfileCache,
6
9
  listProfiles,
7
10
  loadProfilesFromDisk,
11
+ profilesRelocationNotice,
8
12
  resetActiveProfile,
13
+ resolveActiveProfileId,
9
14
  resolveProfile,
10
15
  restoreActiveProfile,
11
- setActiveProfile
12
- } from "./cli-5jxyma6z.js";
13
- import"./cli-vj9cv18n.js";
16
+ setActiveProfile,
17
+ shareableCredentialDir
18
+ } from "./cli-dh0a1bw4.js";
19
+ import"./cli-1jstkdj9.js";
20
+ import"./cli-eehk3052.js";
14
21
  import"./cli-p9swy5t3.js";
15
22
  export {
23
+ shareableCredentialDir,
16
24
  setActiveProfile,
17
25
  restoreActiveProfile,
18
26
  resolveProfile,
27
+ resolveActiveProfileId,
19
28
  resetActiveProfile,
29
+ profilesRelocationNotice,
20
30
  loadProfilesFromDisk,
21
31
  listProfiles,
32
+ invalidateDiskProfileCache,
22
33
  hasProfiles,
23
34
  getEffectiveProfiles,
35
+ getEffectiveActiveProfileId,
24
36
  getActiveProfileId,
37
+ findProfileByIdOrAlias,
25
38
  enableDiskProfileDiscovery
26
39
  };
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Config sources (first wins):
19
19
  * 1. MERIDIAN_ADAPTER_INSTANCES env var (JSON string)
20
- * 2. ~/.config/meridian/adapter-instances.json (5s TTL cache)
20
+ * 2. adapter-instances.json in the config directory (5s TTL cache)
21
21
  *
22
22
  * With no instances configured, adapter detection is byte-identical to the
23
23
  * built-in chain. Built-in adapter names cannot be shadowed.
@@ -1 +1 @@
1
- {"version":3,"file":"adapterInstances.d.ts","sourceRoot":"","sources":["../../src/proxy/adapterInstances.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAIpD,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,+BAA+B;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;IACnC,iFAAiF;IACjF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,oBAAoB,CAAA;CAC7B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;AAEnE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,kBAAkB,CAkCjF;AAOD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAazD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,kBAAkB,EACvB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,OAAO,CAiBT"}
1
+ {"version":3,"file":"adapterInstances.d.ts","sourceRoot":"","sources":["../../src/proxy/adapterInstances.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,+BAA+B;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;IACnC,iFAAiF;IACjF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,oBAAoB,CAAA;CAC7B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;AAEnE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,kBAAkB,CAkCjF;AAUD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAezD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,kBAAkB,EACvB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,OAAO,CAiBT"}
@@ -1 +1 @@
1
- {"version":3,"file":"claudecode.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/claudecode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAiC9C;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,OAAO,GAAG,yBAAyB,GAAG,SAAS,CA+BrG;AAED,sFAAsF;AACtF,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE5E;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED,eAAO,MAAM,iBAAiB,EAAE,YAqG/B,CAAA"}
1
+ {"version":3,"file":"claudecode.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/claudecode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAiC9C;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,OAAO,GAAG,yBAAyB,GAAG,SAAS,CA+BrG;AAED,sFAAsF;AACtF,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE5E;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED,eAAO,MAAM,iBAAiB,EAAE,YAkH/B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"droid.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/droid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAyC9C,eAAO,MAAM,YAAY,EAAE,YAkF1B,CAAA;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,CAAA"}
1
+ {"version":3,"file":"droid.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/droid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA0D9C,eAAO,MAAM,YAAY,EAAE,YAkF1B,CAAA;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,CAAA"}