@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
@@ -27,7 +27,15 @@ export declare const themeCss = "\n :root {\n /* Cool-gray neutral palette.
27
27
  * pole. Scales cleanly from favicon to hero size.
28
28
  */
29
29
  export declare const meridianLogoSvg = "<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>";
30
- export declare const profileBarCss = "\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 /* Build chip \u2014 two mutually exclusive states, and the colour is not a\n style choice: \"update available\" is a link to the releases page, so it\n is blue (interactive); \"local/dev build\" is a meta annotation with no\n href, so it is violet. Swapping them would break the DESIGN.md rule. */\n .meridian-header .mh-build {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; white-space: nowrap;\n padding: 3px 10px; border-radius: 20px; text-decoration: none;\n transition: background 0.15s;\n }\n .meridian-header .mh-build.visible { display: inline-flex; }\n .meridian-header .mh-build.update {\n color: var(--accent, #58a6ff);\n background: rgba(88,166,255,0.12);\n border: 1px solid rgba(88,166,255,0.35);\n }\n .meridian-header .mh-build.update:hover { background: rgba(88,166,255,0.18); }\n .meridian-header .mh-build.provenance {\n color: var(--accent2, #bc8cff);\n background: rgba(188,140,255,0.12);\n border: 1px solid rgba(188,140,255,0.35);\n cursor: default;\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";
30
+ export declare const profileBarCss = "\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 /* Build chip \u2014 two mutually exclusive states, and the colour is not a\n style choice: \"update available\" is a link to the releases page, so it\n is blue (interactive); \"local/dev build\" is a meta annotation with no\n href, so it is violet. Swapping them would break the DESIGN.md rule. */\n .meridian-header .mh-build {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; white-space: nowrap;\n padding: 3px 10px; border-radius: 20px; text-decoration: none;\n transition: background 0.15s;\n }\n .meridian-header .mh-build.visible { display: inline-flex; }\n .meridian-header .mh-build.update {\n color: var(--accent, #58a6ff);\n background: rgba(88,166,255,0.12);\n border: 1px solid rgba(88,166,255,0.35);\n }\n .meridian-header .mh-build.update:hover { background: rgba(88,166,255,0.18); }\n .meridian-header .mh-build.provenance {\n color: var(--accent2, #bc8cff);\n background: rgba(188,140,255,0.12);\n border: 1px solid rgba(188,140,255,0.35);\n cursor: default;\n .meridian-header .mh-profile.following { border-color: var(--accent2, #bc8cff); }\n .meridian-header .mh-profile .mh-profile-follow {\n color: var(--accent2, #bc8cff); font-size: 10px; }\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";
31
31
  export declare const profileBarHtml = "\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-build\" id=\"mhBuild\" target=\"_blank\" rel=\"noopener\"></a>\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";
32
- export declare const profileBarJs = "\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var buildChip = document.getElementById('mhBuild');\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 // Build provenance chip. Hidden entirely for a current npm install, which\n // is the case that needs no comment.\n function renderBuild(build) {\n if (!buildChip) return;\n if (!build) { buildChip.className = 'mh-build'; return; }\n if (build.source !== 'npm') {\n buildChip.textContent = (build.source === 'dev' ? 'dev build' : 'local build') + (build.dirty ? ' *' : '');\n buildChip.removeAttribute('href');\n buildChip.title = 'Not an npm release \u2014 the reported version ' + build.version +\n ' is the tree\\'s last release, not proof of what is running' +\n (build.branch ? '\\nbranch: ' + build.branch : '') +\n (build.sha ? '\\ncommit: ' + build.sha.slice(0, 8) : '') +\n (build.dirty ? '\\nuncommitted changes present' : '');\n buildChip.className = 'mh-build provenance visible';\n return;\n }\n if (build.updateAvailable) {\n buildChip.textContent = build.latest + ' available';\n buildChip.href = 'https://github.com/rynfar/meridian/releases';\n buildChip.title = 'Running ' + build.version + ' \u2014 update with:\\nnpm install -g @rynfar/meridian@latest';\n buildChip.className = 'mh-build update visible';\n return;\n }\n buildChip.className = 'mh-build';\n }\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 renderBuild(h.build);\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";
32
+ export declare const profileBarJs = "\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var buildChip = document.getElementById('mhBuild');\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 // Build provenance chip. Hidden entirely for a current npm install, which\n // is the case that needs no comment.\n function renderBuild(build) {\n if (!buildChip) return;\n if (!build) { buildChip.className = 'mh-build'; return; }\n if (build.source !== 'npm') {\n buildChip.textContent = (build.source === 'dev' ? 'dev build' : 'local build') + (build.dirty ? ' *' : '');\n buildChip.removeAttribute('href');\n buildChip.title = 'Not an npm release \u2014 the reported version ' + build.version +\n ' is the tree\\'s last release, not proof of what is running' +\n (build.branch ? '\\nbranch: ' + build.branch : '') +\n (build.sha ? '\\ncommit: ' + build.sha.slice(0, 8) : '') +\n (build.dirty ? '\\nuncommitted changes present' : '');\n buildChip.className = 'mh-build provenance visible';\n return;\n }\n if (build.updateAvailable) {\n buildChip.textContent = build.latest + ' available';\n buildChip.href = 'https://github.com/rynfar/meridian/releases';\n buildChip.title = 'Running ' + build.version + ' \u2014 update with:\\nnpm install -g @rynfar/meridian@latest';\n buildChip.className = 'mh-build update visible';\n return;\n }\n buildChip.className = 'mh-build';\n }\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 renderBuild(h.build);\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 // Follow mode: say so on every page. An instance quietly taking its\n // active profile from another one, with a picker that won't stick, is\n // otherwise an hour of confusion.\n var follow = data.follow;\n var followLabel = follow ? (follow.activeProfile ? 'following' : 'follow: local') : '';\n if (follow && follow.stale) followLabel += ' (stale)';\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>'\n + (follow ? ' <span class=\"mh-profile-follow\">' + esc(followLabel) + '</span>' : '');\n profileChip.classList.toggle('following', !!follow);\n profileChip.title = follow\n ? 'Active profile follows ' + follow.url + ' (MERIDIAN_FOLLOW_ACTIVE)'\n + (follow.activeProfile ? '' : ' \u2014 no usable value from it, using the local profile')\n + '. Switching here is refused; switch on the followed instance.'\n : 'Active profile \u2014 switch from the home page';\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";
33
+ /** Native desktop chrome: system appearance with the canonical Meridian hues.
34
+ * Web pages continue using themeCss and the shared profile header above. */
35
+ export declare const desktopThemeCss = "\n :root {\n color-scheme: light dark;\n --bg: #f4f5f7; --surface: #ffffff; --surface2: #edeff2;\n --border: #dde0e5; --text: #23272e; --muted: #69717e;\n --accent: #176bce; --accent2: #8250b5;\n --green: #237c40; --yellow: #976400; --red: #ca3b36;\n --sidebar: #e9edf1; --control: #ffffff; --control-border: #d0d5dc;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0d1117; --surface: #161b22; --surface2: #1c2128;\n --border: #30363d; --text: #e6edf3; --muted: #8b949e;\n --accent: #58a6ff; --accent2: #bc8cff;\n --green: #3fb950; --yellow: #d29922; --red: #f85149;\n --sidebar: #161b22; --control: #242b35; --control-border: #39424e;\n }\n }\n body { background: var(--bg); }\n html.native-glass body { background: transparent; }\n";
36
+ export declare const desktopWindowColors: {
37
+ readonly transparent: "#00000000";
38
+ readonly dark: "#0d1117";
39
+ readonly light: "#f4f5f7";
40
+ };
33
41
  //# sourceMappingURL=profileBar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"profileBar.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileBar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,6gDAwCpB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,syBAYrB,CAAA;AAEP,eAAO,MAAM,aAAa,ioHAgFzB,CAAA;AAED,eAAO,MAAM,cAAc,ypDAmB1B,CAAA;AAED,eAAO,MAAM,YAAY,4pGAqExB,CAAA"}
1
+ {"version":3,"file":"profileBar.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileBar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,6gDAwCpB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,syBAYrB,CAAA;AAEP,eAAO,MAAM,aAAa,i0HAkFzB,CAAA;AAED,eAAO,MAAM,cAAc,ypDAmB1B,CAAA;AAED,eAAO,MAAM,YAAY,6hIAkFxB,CAAA;AAED;4EAC4E;AAC5E,eAAO,MAAM,eAAe,wyBAoB3B,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;CAA2E,CAAA"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What a profile card states about an account, in one place.
3
+ *
4
+ * Two surfaces render the same list — the detail grid on /profiles and the
5
+ * hover overlay on the landing page — so the list lives here instead of being
6
+ * written twice and drifting apart the first time a row is added.
7
+ *
8
+ * Emitted as browser source rather than a TypeScript function because the
9
+ * pages are string templates concatenated at import time, the same arrangement
10
+ * `profileBarJs` uses. The unit tests evaluate this exact text, so what they
11
+ * assert is what the browser runs.
12
+ *
13
+ * Values are plain text; escaping is the page's job, since only the page knows
14
+ * whether it is filling a grid cell or an overlay row.
15
+ */
16
+ export declare const profileFactsJs = "\nfunction timeAgo(ts) {\n if (!ts) return '\\u2014';\n var s = Math.floor((Date.now() - ts) / 1000);\n if (s < 5) return 'just now';\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.floor(s / 60) + 'm ago';\n if (s < 86400) return Math.floor(s / 3600) + 'h ago';\n return new Date(ts).toLocaleString();\n}\n\nfunction profileFacts(p) {\n var facts = [];\n var authStale = p.authProvenance && p.authProvenance !== 'live';\n var statusValue = p.authProvenance === 'never' ? 'never read' : (p.loggedIn ? '\\u2713 Authenticated' : '\\u2717 Not logged in');\n facts.push({\n label: 'Status',\n value: statusValue,\n tone: p.loggedIn ? 'ok' : 'err',\n cached: authStale && p.authProvenance !== 'never'\n });\n if (p.email) facts.push({ label: 'Email', value: p.email, tone: '', cached: authStale });\n if (p.organizationName) facts.push({ label: 'Organization', value: p.organizationName, tone: '' });\n if (p.accountType) facts.push({ label: 'Account', value: p.accountType, tone: '' });\n var plan = p.planName || p.planLabel || p.subscriptionType;\n if (plan) facts.push({ label: 'Plan', value: plan, tone: '', cached: authStale, title: p.seatTier || p.rateLimitTier || '' });\n if (p.allowance) facts.push({ label: 'Allowance', value: p.allowance + ' of a Pro plan\\u2019s Claude Code usage', shortValue: p.allowance, tone: '', title: p.rateLimitTier || '' });\n if (p.aliases && p.aliases.length > 0) facts.push({ label: 'Former names', value: p.aliases.join(', '), tone: '', title: 'Requests naming these are served by this profile, until the name is added again' });\n if (p.lastSuccessAt) facts.push({ label: 'Last Verified', value: timeAgo(p.lastSuccessAt), tone: 'ok' });\n if (p.lastCheckedAt && p.lastCheckedAt !== p.lastSuccessAt) {\n facts.push({ label: 'Last Checked', value: timeAgo(p.lastCheckedAt), tone: '' });\n }\n return facts;\n}\n";
17
+ //# sourceMappingURL=profileFacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileFacts.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileFacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,i3DAkC1B,CAAA"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Profile ordering, shared by every page that renders the account list.
3
+ *
4
+ * The order itself already has a home: `PUT /settings/api/routing` persists
5
+ * it as the `profileOrder` setting, and `resolvePriorityOrder()` in
6
+ * src/proxy/routing.ts is what consumes it. This module owns the *display*
7
+ * half — applying that saved order, computing the next order from a drag or
8
+ * a keyboard move, and the drag gesture itself.
9
+ *
10
+ * The pure functions are the tested source of truth (profile-order.test.ts).
11
+ * `reorderCss` / `reorderClientJs` are the single browser-side copy: the
12
+ * pages are template literals that cannot import at runtime, so instead of
13
+ * each pasting its own transcription they interpolate these, and there is
14
+ * exactly one implementation of the gesture in the codebase.
15
+ */
16
+ /**
17
+ * Move the item at `from` to index `to`, returning a new array.
18
+ *
19
+ * Out-of-range indices (and a no-op move) return an unchanged copy rather
20
+ * than throwing — the caller is a drag handler, and a drop outside the list
21
+ * must not corrupt the order.
22
+ */
23
+ export declare function moveInOrder<T>(order: readonly T[], from: number, to: number): T[];
24
+ /**
25
+ * Sort `items` by the id sequence in `order`.
26
+ *
27
+ * Ids in `order` come first, in that order. Anything not listed keeps its
28
+ * original relative position at the end — a profile added since the order
29
+ * was saved must still render, and it must render somewhere predictable.
30
+ * This mirrors resolvePriorityOrder()'s "unlisted profiles appended in
31
+ * config order" rule so every page and the router agree on what the order is.
32
+ */
33
+ export declare function sortByOrder<T extends {
34
+ id: string;
35
+ }>(items: readonly T[], order: readonly string[]): T[];
36
+ /** Distance from a viewport edge at which a drag starts scrolling the page. */
37
+ export declare const EDGE_ZONE_PX = 96;
38
+ /** Scroll step at the very edge, in px per animation frame (~60/s). */
39
+ export declare const MAX_SCROLL_PX_PER_FRAME = 20;
40
+ /**
41
+ * How far to scroll the page during a drag, given the pointer's viewport
42
+ * position. Negative scrolls up, positive down, zero holds still.
43
+ *
44
+ * HTML5 drag-and-drop does not scroll the page for you once the list is
45
+ * taller than the viewport, so a drag can only reach targets that were
46
+ * already on screen. Ramping the speed with depth into the edge zone is
47
+ * what makes both a nudge and a fast travel possible from one gesture.
48
+ *
49
+ * A pointer dragged past the edge reports a clientY outside the viewport;
50
+ * that clamps to full speed rather than accelerating without bound. The
51
+ * zone is capped at half the viewport so the two zones cannot overlap and
52
+ * fight on a short window.
53
+ */
54
+ export declare function edgeScrollVelocity(clientY: number, viewportHeight: number, edge?: number, maxSpeed?: number): number;
55
+ /** Reorder affordances. Interpolated by every page that renders the list. */
56
+ export declare const reorderCss = "\n .profile-card.dragging { opacity: 0.45; border-color: var(--accent); }\n .profile-card.drop-target { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }\n .drag-handle {\n background: none; border: 1px solid transparent; border-radius: 6px;\n color: var(--muted); cursor: grab; padding: 2px 6px; font-size: 15px;\n line-height: 1; user-select: none; flex-shrink: 0;\n }\n .drag-handle:hover { color: var(--accent); border-color: var(--border); }\n .drag-handle:focus-visible { outline: none; color: var(--accent); border-color: var(--accent); }\n .drag-handle:active { cursor: grabbing; }\n .order-index {\n font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 11px;\n color: var(--muted); min-width: 16px; text-align: right; flex-shrink: 0;\n }\n .order-note { font-size: 12px; color: var(--muted); margin-bottom: 12px; max-width: 620px; }\n .order-note code {\n font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 11px;\n background: var(--bg); padding: 1px 5px; border-radius: 4px; color: var(--accent2);\n }\n .order-note.locked { color: var(--yellow); }\n .sr-only {\n position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;\n overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;\n }\n";
57
+ /** The live region the gesture announces moves into. Both pages embed it. */
58
+ export declare const reorderLiveRegionHtml = "<div id=\"orderStatus\" class=\"sr-only\" role=\"status\" aria-live=\"polite\"></div>";
59
+ /**
60
+ * The browser half, as `window.meridianReorder`. Interpolated verbatim into
61
+ * each page's inline script; see the module doc for why it is a string.
62
+ *
63
+ * Contract with the page: cards are `.profile-card[data-id]` inside
64
+ * `#content`, rendered in the order `sortProfiles()` returned, and
65
+ * `init({ onSaved })` is called once with a re-render callback.
66
+ */
67
+ export declare const reorderClientJs = "\nwindow.meridianReorder = (function () {\n function moveInOrder(order, from, to) {\n var next = order.slice();\n if (from < 0 || from >= next.length) return next;\n if (to < 0 || to >= next.length) return next;\n if (from === to) return next;\n var moved = next.splice(from, 1)[0];\n next.splice(to, 0, moved);\n return next;\n }\n\n function sortByOrder(items, order) {\n // Null-prototype: a profile called \"constructor\" or \"toString\" would\n // otherwise test true against Object.prototype and rank as position 0.\n var rank = Object.create(null);\n for (var i = 0; i < order.length; i++) {\n if (!(order[i] in rank)) rank[order[i]] = i;\n }\n return items\n .map(function (item, index) { return { item: item, index: index }; })\n .sort(function (a, b) {\n var ra = a.item.id in rank ? rank[a.item.id] : Number.MAX_SAFE_INTEGER;\n var rb = b.item.id in rank ? rank[b.item.id] : Number.MAX_SAFE_INTEGER;\n if (ra !== rb) return ra - rb;\n return a.index - b.index;\n })\n .map(function (entry) { return entry.item; });\n }\n\n // Escapes the attribute delimiter too. The profiles page's own esc() goes\n // through textContent, which leaves a double quote intact and would break\n // out of the aria-label this builds.\n function escAttr(s) {\n return String(s == null ? '' : s).replace(/[&<>\"']/g, function (ch) {\n return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[ch];\n });\n }\n\n var EDGE_ZONE_PX = 96;\n var MAX_SCROLL_PX_PER_FRAME = 20;\n\n // Mirrors edgeScrollVelocity in src/telemetry/profileOrder.ts, unit-tested\n // in profile-order.test.ts.\n function edgeScrollVelocity(clientY, viewportHeight) {\n if (!isFinite(clientY) || !isFinite(viewportHeight) || viewportHeight <= 0) return 0;\n var zone = Math.min(EDGE_ZONE_PX, viewportHeight / 2);\n if (zone <= 0) return 0;\n if (clientY < zone) return -Math.round(MAX_SCROLL_PX_PER_FRAME * Math.min(1, (zone - clientY) / zone));\n var fromBottom = viewportHeight - clientY;\n if (fromBottom < zone) return Math.round(MAX_SCROLL_PX_PER_FRAME * Math.min(1, (zone - fromBottom) / zone));\n return 0;\n }\n\n var velocity = 0;\n var frame = null;\n var pointerSeenAt = 0;\n // A drag that leaves the window stops producing dragover events while the\n // last velocity is still in force, which would scroll to the end on its\n // own. dragend covers the ordinary paths; this covers the rest.\n var POINTER_STALE_MS = 700;\n\n function step() {\n frame = null;\n if (velocity === 0) return;\n if (Date.now() - pointerSeenAt > POINTER_STALE_MS) { velocity = 0; return; }\n window.scrollBy(0, velocity);\n frame = window.requestAnimationFrame(step);\n }\n\n function trackPointer(e) {\n if (fromIndex === null) return;\n pointerSeenAt = Date.now();\n velocity = edgeScrollVelocity(e.clientY, window.innerHeight);\n if (velocity !== 0 && frame === null) frame = window.requestAnimationFrame(step);\n }\n\n function stopAutoScroll() {\n velocity = 0;\n if (frame !== null) { window.cancelAnimationFrame(frame); frame = null; }\n }\n\n var state = { order: [], envPinned: false };\n var fromIndex = null;\n var pendingFocusId = null;\n var onSaved = function () {};\n\n function adopt(cfg) {\n if (!cfg) return;\n if (Array.isArray(cfg.profileOrder)) state.order = cfg.profileOrder;\n state.envPinned = !!(cfg.envOverride && cfg.envOverride.profileOrder);\n }\n\n function sortProfiles(items) { return sortByOrder(items || [], state.order || []); }\n\n function announce(message) {\n var live = document.getElementById('orderStatus');\n if (live) live.textContent = message;\n }\n\n function noteHtml(reorderable) {\n return reorderable\n ? '<div class=\"order-note\">Drag a card by its handle to reorder, or focus a handle and press \\u2191 / \\u2193. '\n + 'The order is saved, and drives <a href=\"/settings\" style=\"color:var(--accent)\">Priority routing</a> \\u2014 the pool drains from the top.</div>'\n : '<div class=\"order-note locked\">Order is pinned by the <code>MERIDIAN_PROFILE_ORDER</code> environment variable. Unset it to reorder from here.</div>';\n }\n\n function handleHtml(id, index, total) {\n return '<span class=\"order-index\">' + (index + 1) + '</span>'\n + '<button type=\"button\" class=\"drag-handle\" draggable=\"true\" data-index=\"' + index + '\"'\n + ' title=\"Drag to reorder, or press \\u2191 / \\u2193\"'\n + ' aria-label=\"Reorder ' + escAttr(id) + ', position ' + (index + 1) + ' of ' + total\n + '. Press arrow up or arrow down to move.\">\\u283f</button>';\n }\n\n // The rendered order is the truth the server must agree with \u2014 read it back\n // off the DOM rather than from the cached config, which may list ids the\n // current /profiles/list no longer has (PUT rejects unknown ids outright).\n function currentOrderIds() {\n var ids = [];\n var cards = document.querySelectorAll('.profile-card[data-id]');\n for (var i = 0; i < cards.length; i++) ids.push(cards[i].dataset.id);\n return ids;\n }\n\n function clearMarks() {\n var marked = document.querySelectorAll('.profile-card.dragging, .profile-card.drop-target');\n for (var i = 0; i < marked.length; i++) marked[i].classList.remove('dragging', 'drop-target');\n }\n\n function save(order, message) {\n var previous = state.order;\n state.order = order;\n return fetch('/settings/api/routing', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ profileOrder: order })\n }).catch(function () { return null; }).then(function (res) {\n if (!res || !res.ok) {\n state.order = previous;\n announce('Could not save the new order.');\n } else if (message) {\n announce(message);\n }\n return onSaved();\n });\n }\n\n function moveTo(from, to) {\n var order = currentOrderIds();\n if (to < 0 || to >= order.length || from === to) return;\n save(moveInOrder(order, from, to), order[from] + ' moved to position ' + (to + 1) + ' of ' + order.length + '.');\n }\n\n function init(opts) {\n if (opts && opts.onSaved) onSaved = opts.onSaved;\n var content = document.getElementById('content');\n\n content.addEventListener('dragstart', function (e) {\n var handle = e.target.closest && e.target.closest('.drag-handle');\n if (!handle) return;\n var card = handle.closest('.profile-card');\n fromIndex = Number(handle.dataset.index);\n pointerSeenAt = Date.now();\n e.dataTransfer.effectAllowed = 'move';\n // Firefox will not start a drag at all unless the payload is set.\n e.dataTransfer.setData('text/plain', card.dataset.id || '');\n if (e.dataTransfer.setDragImage) e.dataTransfer.setDragImage(card, 24, 24);\n card.classList.add('dragging');\n });\n\n content.addEventListener('dragover', function (e) {\n if (fromIndex === null) return;\n var card = e.target.closest && e.target.closest('.profile-card');\n if (!card) return;\n // preventDefault is what makes an element a drop target at all.\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n var marked = document.querySelectorAll('.profile-card.drop-target');\n for (var i = 0; i < marked.length; i++) marked[i].classList.remove('drop-target');\n if (Number(card.dataset.index) !== fromIndex) card.classList.add('drop-target');\n });\n\n content.addEventListener('drop', function (e) {\n if (fromIndex === null) return;\n var card = e.target.closest && e.target.closest('.profile-card');\n if (!card) return;\n e.preventDefault();\n var from = fromIndex;\n var to = Number(card.dataset.index);\n fromIndex = null;\n stopAutoScroll();\n clearMarks();\n moveTo(from, to);\n });\n\n content.addEventListener('keydown', function (e) {\n if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;\n var handle = e.target.closest && e.target.closest('.drag-handle');\n if (!handle) return;\n e.preventDefault();\n var order = currentOrderIds();\n var from = Number(handle.dataset.index);\n var to = e.key === 'ArrowUp' ? from - 1 : from + 1;\n if (to < 0 || to >= order.length) return;\n pendingFocusId = order[from];\n moveTo(from, to);\n });\n\n // The page scrolls only while the pointer sits near a viewport edge, and\n // the pointer is over the header or the gap between cards as often as it\n // is over one \u2014 so this listens on the document, not on #content.\n document.addEventListener('dragover', trackPointer);\n document.addEventListener('dragend', function () {\n fromIndex = null;\n stopAutoScroll();\n clearMarks();\n });\n document.addEventListener('drop', stopAutoScroll);\n }\n\n // A poll landing mid-drag would replace the very cards being dragged.\n function dragging() { return fromIndex !== null; }\n\n // A re-render replaces the focused handle; without this the keyboard path\n // loses focus mid-reorder and the next arrow key goes nowhere.\n function focusAnchor() {\n var pending = pendingFocusId;\n pendingFocusId = null;\n if (pending) return pending;\n var active = document.activeElement;\n if (!active || !active.closest || !active.closest('.drag-handle')) return null;\n var card = active.closest('.profile-card');\n return card ? card.dataset.id : null;\n }\n\n function restoreFocus(id) {\n if (!id) return;\n var cards = document.querySelectorAll('.profile-card[data-id]');\n for (var i = 0; i < cards.length; i++) {\n if (cards[i].dataset.id !== id) continue;\n var handle = cards[i].querySelector('.drag-handle');\n if (handle) handle.focus();\n return;\n }\n }\n\n return {\n adopt: adopt,\n sortProfiles: sortProfiles,\n envPinned: function () { return state.envPinned; },\n noteHtml: noteHtml,\n handleHtml: handleHtml,\n init: init,\n dragging: dragging,\n focusAnchor: focusAnchor,\n restoreFocus: restoreFocus,\n announce: announce\n };\n})();\n";
68
+ //# sourceMappingURL=profileOrder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileOrder.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileOrder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,EAAE,CAQjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,EAAE,CAcxG;AAED,+EAA+E;AAC/E,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,MAAqB,EAC3B,QAAQ,GAAE,MAAgC,GACzC,MAAM,CAaR;AAED,6EAA6E;AAC7E,eAAO,MAAM,UAAU,gzCAyBtB,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,0FAAkF,CAAA;AAEpH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,ukUAoQ3B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"profilePage.d.ts","sourceRoot":"","sources":["../../src/telemetry/profilePage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,eAAO,MAAM,eAAe,QA8bpB,CAAA"}
1
+ {"version":3,"file":"profilePage.d.ts","sourceRoot":"","sources":["../../src/telemetry/profilePage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,eAAO,MAAM,eAAe,QAooBpB,CAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * View-only ordering for the home page's account cards.
3
+ *
4
+ * Distinct from the `profileOrder` setting, which is durable and decides
5
+ * which account Priority routing drains first. This one answers "show me
6
+ * the ones with capacity" while looking at the page, and is deliberately
7
+ * not persisted anywhere.
8
+ *
9
+ * Mirrored by the inline browser script in landing.ts, the same house
10
+ * pattern profileUsage.ts uses: the page is one template literal that runs
11
+ * in the browser and cannot import at runtime, so this TS module is the
12
+ * tested source of truth (profile-sort.test.ts).
13
+ */
14
+ export type ProfileSortMode = "configured" | "spent-desc" | "spent-asc";
15
+ export declare const DEFAULT_PROFILE_SORT: ProfileSortMode;
16
+ /** Rendered as the control, in this order. `id` is what the comparator takes. */
17
+ export declare const PROFILE_SORT_MODES: ReadonlyArray<{
18
+ id: ProfileSortMode;
19
+ label: string;
20
+ title: string;
21
+ }>;
22
+ export declare function parseProfileSortMode(raw: string | null | undefined): ProfileSortMode;
23
+ /**
24
+ * Order `items` for display. `spentOf` returns 0..1 for a profile, or null
25
+ * when nothing is known about it.
26
+ *
27
+ * A profile with no reading sorts last in BOTH directions. Null is absence
28
+ * of evidence, not a low number — sorting it to the front of "least spent"
29
+ * would recommend the one account we cannot vouch for.
30
+ *
31
+ * Ties and the "configured" mode keep the incoming order, which is the
32
+ * order the profiles are configured in.
33
+ */
34
+ export declare function sortProfilesForView<T>(items: readonly T[], mode: ProfileSortMode, spentOf: (item: T) => number | null): T[];
35
+ //# sourceMappingURL=profileSort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileSort.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileSort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,YAAY,GAAG,WAAW,CAAA;AAEvE,eAAO,MAAM,oBAAoB,EAAE,eAA8B,CAAA;AAEjE,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAInG,CAAA;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,CAEpF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAClC,CAAC,EAAE,CAeL"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * How spent a Claude account is — one definition, so every surface that
3
+ * asks "can this account still do work?" gets the same answer.
4
+ *
5
+ * Mirrored by the inline browser script in landing.ts, the same house
6
+ * pattern profileUsage.ts uses: the page is one template literal that runs
7
+ * in the browser and cannot import at runtime, so this TS module is the
8
+ * tested source of truth (profile-spent.test.ts) and the page carries a
9
+ * copy of the arithmetic.
10
+ */
11
+ /**
12
+ * Windows that describe the account as a whole. Anthropic also reports
13
+ * per-model caps (seven_day_opus, seven_day_sonnet, seven_day_fable, …);
14
+ * those are deliberately excluded, because hitting one leaves the rest of
15
+ * the account working. seven_day_fable is the concrete case: a profile on
16
+ * the author's fleet sat at 94% Fable with its general windows nearly
17
+ * untouched, and folding that in would have greyed out a usable account.
18
+ */
19
+ export declare const GENERAL_WINDOW_TYPES: readonly string[];
20
+ /** Where the gradient starts — below this a profile reads as untouched. */
21
+ export declare const FADE_FROM = 0.85;
22
+ /** At or above this a profile is spent, whatever the exact number. */
23
+ export declare const SPENT_AT = 0.95;
24
+ export interface SpendWindow {
25
+ type: string;
26
+ utilization?: number | null;
27
+ }
28
+ export interface SpendInput {
29
+ /** `windows` for this profile from /v1/usage/quota/all. */
30
+ windows?: readonly SpendWindow[] | null;
31
+ /** `error` for this profile from /v1/usage/quota/all. */
32
+ error?: string | null;
33
+ /** `loggedIn` for this profile from /profiles/list. */
34
+ loggedIn?: boolean | null;
35
+ }
36
+ export type SpendState = "unknown" | "available" | "fading" | "spent";
37
+ export interface ProfileSpend {
38
+ /** 0..1 — the worse of the general windows; null when nothing is known. */
39
+ fraction: number | null;
40
+ state: SpendState;
41
+ /**
42
+ * 0..1 — how far to grey the account out. Zero for an unusable one even
43
+ * though it is fully spent: fading is for an account that will come back
44
+ * on its own, and that is the opposite of what a missing login needs.
45
+ * Callers give `reason: "unusable"` its own, louder treatment.
46
+ */
47
+ fade: number;
48
+ /** Why it is spent. "unusable" needs a human; "usage" only needs time. */
49
+ reason: "usage" | "unusable" | null;
50
+ }
51
+ /**
52
+ * A profile that cannot serve a request at all, measured rather than
53
+ * assumed: a request carrying `x-meridian-profile` for a profile whose
54
+ * quota entry reports `no_token` comes back 401.
55
+ *
56
+ * `not_oauth` is explicitly NOT this. It is how /v1/usage/quota/all reports
57
+ * an API-key profile, which has no OAuth quota to report and works fine —
58
+ * treating that error like the others would grey out a healthy account.
59
+ */
60
+ export declare function isUnusable(input: SpendInput): boolean;
61
+ /**
62
+ * The worse of the general windows, or null when none of them carries a
63
+ * number. Null means "no evidence", which is not the same as zero and must
64
+ * not render as a pristine account.
65
+ */
66
+ export declare function generalUtilization(windows: readonly SpendWindow[] | null | undefined): number | null;
67
+ export declare function computeProfileSpend(input: SpendInput): ProfileSpend;
68
+ //# sourceMappingURL=profileSpent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileSpent.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileSpent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAA+B,CAAA;AAEjF,2EAA2E;AAC3E,eAAO,MAAM,SAAS,OAAO,CAAA;AAC7B,sEAAsE;AACtE,eAAO,MAAM,QAAQ,OAAO,CAAA;AAE5B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,CAAA;IACvC,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAA;AAErE,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,EAAE,UAAU,CAAA;IACjB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,0EAA0E;IAC1E,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAAA;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAGrD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUpG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CAoBnE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Fold priority-dispatch hops back into one row per client request.
3
+ *
4
+ * Priority routing serves one client request by re-entering the proxy once
5
+ * per candidate account, so a failover already produces several telemetry
6
+ * rows — one per attempt — that share a `routeGroupId`. Nothing correlates
7
+ * them at write time on purpose: the request path may not spend a cycle on
8
+ * anything the dashboard wants. The join happens HERE, on the read path,
9
+ * when somebody actually asks for the data.
10
+ *
11
+ * Pure — no I/O, no store access, no clock.
12
+ */
13
+ import type { RequestMetric, RouteSummary } from "./types";
14
+ /**
15
+ * Collapse each `routeGroupId` group down to the hop that answered the
16
+ * client — the last one attempted, whether it succeeded or the pool ran
17
+ * out — carrying the whole chain on it.
18
+ *
19
+ * @param metrics Newest first, as every ITelemetryStore.getRecent returns.
20
+ * Output keeps that order and the group's newest position.
21
+ */
22
+ export declare function collapseRouteChains(metrics: RequestMetric[]): RequestMetric[];
23
+ /**
24
+ * Tally where a window's requests went and which accounts refused them.
25
+ *
26
+ * Pure, and read-path only — same as everything else in this module.
27
+ *
28
+ * @param collapsed Rows from collapseRouteChains, i.e. one per CLIENT request.
29
+ * Passing raw hops would count a failover once per account.
30
+ */
31
+ export declare function summarizeRoutes(collapsed: RequestMetric[]): RouteSummary;
32
+ //# sourceMappingURL=routeChain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeChain.d.ts","sourceRoot":"","sources":["../../src/telemetry/routeChain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAA0C,YAAY,EAAE,MAAM,SAAS,CAAA;AA8BlG;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAuB7E;AA+BD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,YAAY,CA6BxE"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/telemetry/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAMjD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,kBAAkB,CAAA;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,kBAAuB,8EAyDlE"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/telemetry/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAWjD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,kBAAkB,CAAA;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,kBAAuB,8EA6ElE"}
@@ -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 /* Build chip \u2014 two mutually exclusive states, and the colour is not a\n style choice: \"update available\" is a link to the releases page, so it\n is blue (interactive); \"local/dev build\" is a meta annotation with no\n href, so it is violet. Swapping them would break the DESIGN.md rule. */\n .meridian-header .mh-build {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; white-space: nowrap;\n padding: 3px 10px; border-radius: 20px; text-decoration: none;\n transition: background 0.15s;\n }\n .meridian-header .mh-build.visible { display: inline-flex; }\n .meridian-header .mh-build.update {\n color: var(--accent, #58a6ff);\n background: rgba(88,166,255,0.12);\n border: 1px solid rgba(88,166,255,0.35);\n }\n .meridian-header .mh-build.update:hover { background: rgba(88,166,255,0.18); }\n .meridian-header .mh-build.provenance {\n color: var(--accent2, #bc8cff);\n background: rgba(188,140,255,0.12);\n border: 1px solid rgba(188,140,255,0.35);\n cursor: default;\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-build\" id=\"mhBuild\" target=\"_blank\" rel=\"noopener\"></a>\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: 'webFetchPreflight', label: 'WebFetch Preflight', desc: 'Check each WebFetch hostname against the Anthropic blocklist before fetching \u2014 off keeps hostnames local but fetches any URL unchecked. Only affects Cherry Studio; other adapters never run the SDK\\'s built-in WebFetch', type: 'toggle' },\n { key: 'claudeAiConnectors', label: 'claude.ai Connectors', desc: 'Load the MCP connectors attached to your claude.ai account (Drive, Gmail, Calendar). Off by default; always off in passthrough mode', 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\n// Display names only \u2014 NOT the list of adapters to render. The page renders\n// whatever /settings/api/features returns, so a new adapter shows up here the\n// moment it exists, using its raw name until someone gives it a pretty one.\n// A hardcoded list is what hid codex and cherry from this page entirely.\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 codex: 'Codex CLI (/v1/responses)',\n cherry: 'Cherry Studio',\n 'claude-code': 'Claude Code',\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.webFetchPreflight === false ||\n features.claudeAiConnectors || 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 of Object.keys(currentConfig)) {\n const label = ADAPTER_LABELS[adapter] || adapter;\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 buildChip = document.getElementById('mhBuild');\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 // Build provenance chip. Hidden entirely for a current npm install, which\n // is the case that needs no comment.\n function renderBuild(build) {\n if (!buildChip) return;\n if (!build) { buildChip.className = 'mh-build'; return; }\n if (build.source !== 'npm') {\n buildChip.textContent = (build.source === 'dev' ? 'dev build' : 'local build') + (build.dirty ? ' *' : '');\n buildChip.removeAttribute('href');\n buildChip.title = 'Not an npm release \u2014 the reported version ' + build.version +\n ' is the tree\\'s last release, not proof of what is running' +\n (build.branch ? '\\nbranch: ' + build.branch : '') +\n (build.sha ? '\\ncommit: ' + build.sha.slice(0, 8) : '') +\n (build.dirty ? '\\nuncommitted changes present' : '');\n buildChip.className = 'mh-build provenance visible';\n return;\n }\n if (build.updateAvailable) {\n buildChip.textContent = build.latest + ' available';\n buildChip.href = 'https://github.com/rynfar/meridian/releases';\n buildChip.title = 'Running ' + build.version + ' \u2014 update with:\\nnpm install -g @rynfar/meridian@latest';\n buildChip.className = 'mh-build update visible';\n return;\n }\n buildChip.className = 'mh-build';\n }\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 renderBuild(h.build);\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 /* Build chip \u2014 two mutually exclusive states, and the colour is not a\n style choice: \"update available\" is a link to the releases page, so it\n is blue (interactive); \"local/dev build\" is a meta annotation with no\n href, so it is violet. Swapping them would break the DESIGN.md rule. */\n .meridian-header .mh-build {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; white-space: nowrap;\n padding: 3px 10px; border-radius: 20px; text-decoration: none;\n transition: background 0.15s;\n }\n .meridian-header .mh-build.visible { display: inline-flex; }\n .meridian-header .mh-build.update {\n color: var(--accent, #58a6ff);\n background: rgba(88,166,255,0.12);\n border: 1px solid rgba(88,166,255,0.35);\n }\n .meridian-header .mh-build.update:hover { background: rgba(88,166,255,0.18); }\n .meridian-header .mh-build.provenance {\n color: var(--accent2, #bc8cff);\n background: rgba(188,140,255,0.12);\n border: 1px solid rgba(188,140,255,0.35);\n cursor: default;\n .meridian-header .mh-profile.following { border-color: var(--accent2, #bc8cff); }\n .meridian-header .mh-profile .mh-profile-follow {\n color: var(--accent2, #bc8cff); font-size: 10px; }\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 /* Harness tabs \u2014 dashboard.ts's tab rules, kept identical on purpose; wraps\n because there are more harnesses than the dashboard's three panels. */\n .tabs { display: flex; flex-wrap: wrap; gap: 0; margin-bottom: 20px; border-bottom: 1px solid var(--border); }\n .tab { padding: 10px 20px; font-size: 13px; font-weight: 500; color: var(--muted); cursor: pointer;\n border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s, border-color 0.15s;\n user-select: none; }\n .tab:hover { color: var(--text); }\n .tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n .tab-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; margin-left: 7px;\n vertical-align: middle; background: var(--green); }\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-build\" id=\"mhBuild\" target=\"_blank\" rel=\"noopener\"></a>\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>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.\n <strong style=\"color:var(--text)\">Active+priority</strong> keeps you in charge - every request goes to the\n profile you selected - but the moment that account is refused, the request is re-sent to the next healthy\n account in the pool order and answered from there, so the client never sees the error. Switching the active\n profile moves running conversations too, which is the difference from priority. It reuses the same pool\n order below; the active profile always goes first, wherever it sits in that list.\n 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\">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 class=\"tabs\" id=\"adapterTabs\"></div>\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\n <h1 style=\"margin-top:40px\">Telemetry Storage</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n What <a href=\"/telemetry\" style=\"color:var(--accent)\">/telemetry</a> remembers, and for how long. By default\n telemetry lives in a fixed-size ring in memory: fast, free, and gone the moment the proxy restarts \u2014 which is\n also why a window longer than an hour or so usually shows less than it names. Persisting moves it to SQLite,\n where rows survive a restart and are deleted by age instead of by count.\n <strong style=\"color:var(--text)\">These settings apply on the next start, not to the running proxy.</strong>\n The stores are built once at startup and cannot be swapped out from under requests already in flight.\n </p>\n <div class=\"adapter-card\" id=\"telemetry-card\">\n <div id=\"telemetry-body\">Loading\u2026</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: 'webFetchPreflight', label: 'WebFetch Preflight', desc: 'Check each WebFetch hostname against the Anthropic blocklist before fetching \u2014 off keeps hostnames local but fetches any URL unchecked. Only affects Cherry Studio; other adapters never run the SDK\\'s built-in WebFetch', type: 'toggle' },\n { key: 'claudeAiConnectors', label: 'claude.ai Connectors', desc: 'Load the MCP connectors attached to your claude.ai account (Drive, Gmail, Calendar). Off by default; always off in passthrough mode', 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\n// Display names only \u2014 NOT the list of adapters to render. The page renders\n// whatever /settings/api/features returns, so a new adapter shows up here the\n// moment it exists, using its raw name until someone gives it a pretty one.\n// A hardcoded list is what hid codex and cherry from this page entirely.\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 codex: 'Codex CLI (/v1/responses)',\n cherry: 'Cherry Studio',\n 'claude-code': 'Claude Code',\n};\n\nlet currentConfig = {};\nlet selectedAdapter = null;\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.webFetchPreflight === false ||\n features.claudeAiConnectors || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction selectAdapter(adapter) {\n selectedAdapter = adapter;\n render();\n}\n\nfunction renderTabs() {\n const tabs = document.getElementById('adapterTabs');\n tabs.innerHTML = '';\n\n for (const adapter of Object.keys(currentConfig)) {\n const tabLabel = ADAPTER_LABELS[adapter] || adapter;\n const tab = document.createElement('div');\n tab.className = 'tab' + (adapter === selectedAdapter ? ' active' : '');\n tab.dataset.adapter = adapter;\n tab.textContent = tabLabel;\n // Stacked cards showed every harness's state at once and one body at a\n // time cannot, so a harness that is off its defaults says so on its tab.\n if (hasAnyEnabled(currentConfig[adapter] || {})) {\n const dot = document.createElement('span');\n dot.className = 'tab-dot';\n dot.title = 'Customized \u2014 not on defaults';\n tab.appendChild(dot);\n }\n tab.addEventListener('click', function () { selectAdapter(adapter); });\n tabs.appendChild(tab);\n }\n}\n\nfunction adapterCard(adapter) {\n const label = ADAPTER_LABELS[adapter] || adapter;\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 return card;\n}\n\nfunction render() {\n const names = Object.keys(currentConfig);\n if (names.indexOf(selectedAdapter) === -1) selectedAdapter = names[0] || null;\n\n renderTabs();\n\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n if (selectedAdapter) container.appendChild(adapterCard(selectedAdapter));\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 + (cfg.modes || ['active','sticky','priority']).map(m => '<option value=\"'+m+'\"'+(cfg.routing===m?' selected':'')+'>'+m+'</option>').join('')\n + '</select>' + envNote(cfg.envOverride.routing) + '</div>';\n var usesPool = cfg.routing === 'priority' || cfg.routing === 'active+priority';\n h += '<div id=\"routing-order-wrap\" style=\"'+(usesPool?'':'display:none')+'\">'\n + '<div style=\"color:var(--muted);font-size:13px;margin-bottom:8px\">'\n + (cfg.routing === 'active+priority'\n ? 'Fallback order - used when the active profile runs out. The active profile always goes first, wherever it sits in this list.'\n : '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\nfunction telemetryBytes(n) {\n if (n == null) return null;\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(0) + ' KB';\n if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';\n return (n / (1024 * 1024 * 1024)).toFixed(1) + ' GB';\n}\n\nfunction telemetryEsc(s) {\n return String(s).replace(/[&<>\"']/g, function (ch) {\n return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[ch];\n });\n}\n\n// The saved value drives the form; the EFFECTIVE one is what the proxy is\n// actually doing. Rendering only the former is the failure mode this whole card\n// exists to avoid \u2014 a form that reads back what you typed and implies it landed.\nfunction telemetryRow(label, control, effective, note) {\n return '<div style=\"display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap\">'\n + '<label style=\"color:var(--muted);font-size:13px;width:150px\">' + label + '</label>'\n + control\n + '<span style=\"color:var(--muted);font-size:12px\">now: ' + effective + '</span>'\n + (note || '')\n + '</div>';\n}\n\nasync function loadTelemetry() {\n const res = await fetch('/settings/api/telemetry');\n const cfg = await res.json();\n const el = document.getElementById('telemetry-body');\n const live = cfg.effective || {};\n const lim = cfg.limits || {};\n const envNote = (on) => on ? ' <span style=\"font-size:11px;color:var(--yellow)\">(env override active \u2014 setting saved but env wins)</span>' : '';\n const persisting = live.kind === 'sqlite';\n\n const num = (key, value, suffix) => {\n const l = lim[key] || { min: 1, max: 1000000 };\n return '<input type=\"number\" id=\"tel-' + key + '\" value=\"' + (value == null ? '' : value) + '\"'\n + ' min=\"' + l.min + '\" max=\"' + l.max + '\" step=\"1\" placeholder=\"default\"'\n + ' style=\"width:110px;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px\">'\n + (suffix ? ' <span style=\"color:var(--muted);font-size:12px\">' + suffix + '</span>' : '');\n };\n\n let h = telemetryRow('Persist to SQLite',\n '<input type=\"checkbox\" id=\"tel-persist\"' + (cfg.wanted.persist ? ' checked' : '') + '>',\n persisting ? 'SQLite' : 'memory',\n envNote(cfg.envOverride.telemetryPersist));\n\n h += telemetryRow('Retention',\n num('telemetryRetentionDays', cfg.saved.telemetryRetentionDays, 'days'),\n persisting ? (live.retentionDays + 'd') : 'n/a while in memory',\n envNote(cfg.envOverride.telemetryRetentionDays));\n\n h += telemetryRow('Request ring size',\n num('telemetrySize', cfg.saved.telemetrySize, 'rows'),\n persisting ? 'n/a while persisting' : (live.capacity == null ? '\u2014' : live.capacity.toLocaleString()),\n envNote(cfg.envOverride.telemetrySize));\n\n h += telemetryRow('Diagnostic log size',\n num('diagnosticLogSize', cfg.saved.diagnosticLogSize, 'entries'),\n persisting ? 'n/a while persisting' : (live.diagnosticLogCapacity == null ? '\u2014' : live.diagnosticLogCapacity.toLocaleString()),\n envNote(cfg.envOverride.diagnosticLogSize));\n\n const held = (live.held == null ? 0 : live.held).toLocaleString();\n let state = 'Holding ' + held + ' request' + (live.held === 1 ? '' : 's');\n if (persisting) {\n state += ' in ' + telemetryEsc(live.dbPath || 'SQLite');\n const size = telemetryBytes(live.dbBytes);\n if (size) state += ' (' + size + ')';\n } else {\n state += ' in memory \u2014 lost on restart';\n }\n h += '<div class=\"pricing-note\" style=\"margin-top:4px\">' + state + '</div>';\n\n // The only honest thing a page can offer for a restart-scoped setting: name\n // the exact command for how THIS proxy was started. Guessing systemctl at a\n // process started by hand sends someone chasing a unit that does not exist.\n if (cfg.pendingRestart && cfg.pendingRestart.length > 0) {\n const sup = cfg.supervision || {};\n h += '<div class=\"pricing-note\" style=\"margin-top:10px;color:var(--yellow)\">'\n + 'Saved, but not in effect: <strong>' + cfg.pendingRestart.map(telemetryEsc).join('</strong>, <strong>') + '</strong>. '\n + (sup.restartCommand\n ? 'Restart to apply: <code style=\"color:var(--text)\">' + telemetryEsc(sup.restartCommand) + '</code>'\n : 'Restart Meridian to apply.')\n + '</div>';\n }\n\n h += '<div class=\"pricing-note\" style=\"margin-top:10px\">'\n + 'Leave a number blank to use the default. The database path is set with '\n + '<code>MERIDIAN_TELEMETRY_DB</code> and is deliberately not editable here.'\n + '</div>';\n\n el.innerHTML = h;\n\n document.getElementById('tel-persist').addEventListener('change', async (e) => {\n await putTelemetry({ telemetryPersist: e.target.checked });\n });\n ['telemetryRetentionDays', 'telemetrySize', 'diagnosticLogSize'].forEach((key) => {\n const input = document.getElementById('tel-' + key);\n if (!input) return;\n input.addEventListener('change', async () => {\n const raw = input.value.trim();\n const body = {};\n body[key] = raw === '' ? null : Number(raw);\n await putTelemetry(body);\n });\n });\n}\n\nasync function putTelemetry(body) {\n const res = await fetch('/settings/api/telemetry', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => ({}));\n alert(err.error || 'Failed to save telemetry settings');\n } else {\n showSaved();\n }\n await loadTelemetry();\n}\n\nloadConfig();\nloadPricing();\nloadRouting();\nloadTelemetry();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var buildChip = document.getElementById('mhBuild');\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 // Build provenance chip. Hidden entirely for a current npm install, which\n // is the case that needs no comment.\n function renderBuild(build) {\n if (!buildChip) return;\n if (!build) { buildChip.className = 'mh-build'; return; }\n if (build.source !== 'npm') {\n buildChip.textContent = (build.source === 'dev' ? 'dev build' : 'local build') + (build.dirty ? ' *' : '');\n buildChip.removeAttribute('href');\n buildChip.title = 'Not an npm release \u2014 the reported version ' + build.version +\n ' is the tree\\'s last release, not proof of what is running' +\n (build.branch ? '\\nbranch: ' + build.branch : '') +\n (build.sha ? '\\ncommit: ' + build.sha.slice(0, 8) : '') +\n (build.dirty ? '\\nuncommitted changes present' : '');\n buildChip.className = 'mh-build provenance visible';\n return;\n }\n if (build.updateAvailable) {\n buildChip.textContent = build.latest + ' available';\n buildChip.href = 'https://github.com/rynfar/meridian/releases';\n buildChip.title = 'Running ' + build.version + ' \u2014 update with:\\nnpm install -g @rynfar/meridian@latest';\n buildChip.className = 'mh-build update visible';\n return;\n }\n buildChip.className = 'mh-build';\n }\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 renderBuild(h.build);\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 // Follow mode: say so on every page. An instance quietly taking its\n // active profile from another one, with a picker that won't stick, is\n // otherwise an hour of confusion.\n var follow = data.follow;\n var followLabel = follow ? (follow.activeProfile ? 'following' : 'follow: local') : '';\n if (follow && follow.stale) followLabel += ' (stale)';\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>'\n + (follow ? ' <span class=\"mh-profile-follow\">' + esc(followLabel) + '</span>' : '');\n profileChip.classList.toggle('following', !!follow);\n profileChip.title = follow\n ? 'Active profile follows ' + follow.url + ' (MERIDIAN_FOLLOW_ACTIVE)'\n + (follow.activeProfile ? '' : ' \u2014 no usable value from it, using the local profile')\n + '. Switching here is refused; switch on the followed instance.'\n : 'Active profile \u2014 switch from the home page';\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,sxiCAodrB,CAAA"}
1
+ {"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,692CAupBrB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/telemetry/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmC,eAAe,EAAE,mBAAmB,EAAiB,MAAM,SAAS,CAAA;AAoXnH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;eAGV,eAAe;iBACxB,mBAAmB;;EAGvE"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/telemetry/sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmC,eAAe,EAAE,mBAAmB,EAAqC,MAAM,SAAS,CAAA;AAuavI,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;eAGF,eAAe;iBAChC,mBAAmB;;EAGvE"}