@rynfar/meridian 1.72.0 → 1.74.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 (177) hide show
  1. package/README.md +119 -107
  2. package/assets/banner.svg +18 -43
  3. package/assets/how-it-works.svg +40 -86
  4. package/assets/providers/README.md +10 -0
  5. package/assets/providers/antigravity.png +0 -0
  6. package/assets/providers/claude.png +0 -0
  7. package/dist/antigravity-clients/opencode.js +173 -0
  8. package/dist/antigravity-clients/pi.js +21 -0
  9. package/dist/antigravitySetup-35pd5xqn.js +191 -0
  10. package/dist/cli-1jstkdj9.js +30 -0
  11. package/dist/{cli-9e5cxp89.js → cli-2h3w5n6g.js} +163 -65
  12. package/dist/cli-3zwfvexc.js +56 -0
  13. package/dist/{cli-hxxy0m1z.js → cli-82e2m44k.js} +2 -1
  14. package/dist/cli-8sw5wr9j.js +136 -0
  15. package/dist/{cli-khhjyk04.js → cli-chsp880k.js} +214 -21
  16. package/dist/cli-dh0a1bw4.js +619 -0
  17. package/dist/cli-eehk3052.js +57 -0
  18. package/dist/{cli-ab7zkp1a.js → cli-fveyj34s.js} +32261 -18952
  19. package/dist/cli-grfp0s1j.js +330 -0
  20. package/dist/cli-qpfky8wy.js +133 -0
  21. package/dist/{cli-hj61zpt7.js → cli-wbsmhxcx.js} +26 -2
  22. package/dist/cli.js +111 -38
  23. package/dist/cliDashboard-c0dvkj4y.js +325 -0
  24. package/dist/configDir.d.ts +30 -0
  25. package/dist/configDir.d.ts.map +1 -0
  26. package/dist/organizationName-90f0atkn.js +29 -0
  27. package/dist/{pluginPage-f5gdmbxc.js → pluginPage-pp72d2j8.js} +1 -1
  28. package/dist/{profileCli-1eecbmgk.js → profileCli-1bfgc05h.js} +13 -3
  29. package/dist/{profilePage-mamej7kp.js → profilePage-vs319rvf.js} +252 -60
  30. package/dist/profileRename-4a4km6g8.js +22 -0
  31. package/dist/{profiles-wch9h234.js → profiles-0mp32ek9.js} +16 -3
  32. package/dist/proxy/adapterInstances.d.ts +1 -1
  33. package/dist/proxy/adapterInstances.d.ts.map +1 -1
  34. package/dist/proxy/adapters/claudecode.d.ts.map +1 -1
  35. package/dist/proxy/adapters/droid.d.ts.map +1 -1
  36. package/dist/proxy/auth.d.ts +2 -0
  37. package/dist/proxy/auth.d.ts.map +1 -1
  38. package/dist/proxy/authDiscovery.d.ts +52 -0
  39. package/dist/proxy/authDiscovery.d.ts.map +1 -0
  40. package/dist/proxy/backends/antigravity.d.ts +8 -0
  41. package/dist/proxy/backends/antigravity.d.ts.map +1 -0
  42. package/dist/proxy/backends/antigravityAttachments.d.ts +16 -0
  43. package/dist/proxy/backends/antigravityAttachments.d.ts.map +1 -0
  44. package/dist/proxy/backends/antigravityGrammar.d.ts +12 -0
  45. package/dist/proxy/backends/antigravityGrammar.d.ts.map +1 -0
  46. package/dist/proxy/backends/antigravityJobs.d.ts +19 -0
  47. package/dist/proxy/backends/antigravityJobs.d.ts.map +1 -0
  48. package/dist/proxy/backends/antigravityMedia.d.ts +9 -0
  49. package/dist/proxy/backends/antigravityMedia.d.ts.map +1 -0
  50. package/dist/proxy/backends/antigravityNative.d.ts +8 -0
  51. package/dist/proxy/backends/antigravityNative.d.ts.map +1 -0
  52. package/dist/proxy/backends/antigravityOpenai.d.ts +9 -0
  53. package/dist/proxy/backends/antigravityOpenai.d.ts.map +1 -0
  54. package/dist/proxy/backends/antigravityOpenaiMedia.d.ts +9 -0
  55. package/dist/proxy/backends/antigravityOpenaiMedia.d.ts.map +1 -0
  56. package/dist/proxy/backends/antigravityPlugins.d.ts +41 -0
  57. package/dist/proxy/backends/antigravityPlugins.d.ts.map +1 -0
  58. package/dist/proxy/backends/antigravityProbe.d.ts +16 -0
  59. package/dist/proxy/backends/antigravityProbe.d.ts.map +1 -0
  60. package/dist/proxy/backends/antigravityProcess.d.ts +4 -0
  61. package/dist/proxy/backends/antigravityProcess.d.ts.map +1 -0
  62. package/dist/proxy/backends/antigravityProtocol.d.ts +582 -0
  63. package/dist/proxy/backends/antigravityProtocol.d.ts.map +1 -0
  64. package/dist/proxy/backends/antigravityReplay.d.ts +41 -0
  65. package/dist/proxy/backends/antigravityReplay.d.ts.map +1 -0
  66. package/dist/proxy/backends/antigravityResponses.d.ts +35 -0
  67. package/dist/proxy/backends/antigravityResponses.d.ts.map +1 -0
  68. package/dist/proxy/backends/antigravityRuntime.d.ts +189 -0
  69. package/dist/proxy/backends/antigravityRuntime.d.ts.map +1 -0
  70. package/dist/proxy/backends/antigravitySchema.d.ts +10 -0
  71. package/dist/proxy/backends/antigravitySchema.d.ts.map +1 -0
  72. package/dist/proxy/backends/antigravitySessions.d.ts +28 -0
  73. package/dist/proxy/backends/antigravitySessions.d.ts.map +1 -0
  74. package/dist/proxy/backends/antigravityState.d.ts +22 -0
  75. package/dist/proxy/backends/antigravityState.d.ts.map +1 -0
  76. package/dist/proxy/backends/antigravityStops.d.ts +10 -0
  77. package/dist/proxy/backends/antigravityStops.d.ts.map +1 -0
  78. package/dist/proxy/backends/antigravityTokens.d.ts +14 -0
  79. package/dist/proxy/backends/antigravityTokens.d.ts.map +1 -0
  80. package/dist/proxy/backends/antigravityUrl.d.ts +7 -0
  81. package/dist/proxy/backends/antigravityUrl.d.ts.map +1 -0
  82. package/dist/proxy/backends/providerStatus.d.ts +15 -0
  83. package/dist/proxy/backends/providerStatus.d.ts.map +1 -0
  84. package/dist/proxy/credentialsMode.d.ts +77 -0
  85. package/dist/proxy/credentialsMode.d.ts.map +1 -0
  86. package/dist/proxy/design.d.ts.map +1 -1
  87. package/dist/proxy/followActive.d.ts +262 -0
  88. package/dist/proxy/followActive.d.ts.map +1 -0
  89. package/dist/proxy/limitDetection.d.ts +110 -0
  90. package/dist/proxy/limitDetection.d.ts.map +1 -0
  91. package/dist/proxy/models.d.ts.map +1 -1
  92. package/dist/proxy/oauthPlan.d.ts +63 -0
  93. package/dist/proxy/oauthPlan.d.ts.map +1 -0
  94. package/dist/proxy/oauthUsage.d.ts +65 -2
  95. package/dist/proxy/oauthUsage.d.ts.map +1 -1
  96. package/dist/proxy/openai.d.ts +6 -2
  97. package/dist/proxy/openai.d.ts.map +1 -1
  98. package/dist/proxy/organizationName.d.ts +71 -0
  99. package/dist/proxy/organizationName.d.ts.map +1 -0
  100. package/dist/proxy/passthroughEarlyStop.d.ts +10 -0
  101. package/dist/proxy/passthroughEarlyStop.d.ts.map +1 -1
  102. package/dist/proxy/planAllowance.d.ts +75 -0
  103. package/dist/proxy/planAllowance.d.ts.map +1 -0
  104. package/dist/proxy/probes.d.ts +85 -0
  105. package/dist/proxy/probes.d.ts.map +1 -0
  106. package/dist/proxy/profileCli.d.ts +53 -1
  107. package/dist/proxy/profileCli.d.ts.map +1 -1
  108. package/dist/proxy/profileHealth.d.ts +127 -0
  109. package/dist/proxy/profileHealth.d.ts.map +1 -0
  110. package/dist/proxy/profileRename.d.ts +83 -0
  111. package/dist/proxy/profileRename.d.ts.map +1 -0
  112. package/dist/proxy/profiles.d.ts +72 -6
  113. package/dist/proxy/profiles.d.ts.map +1 -1
  114. package/dist/proxy/query.d.ts +7 -0
  115. package/dist/proxy/query.d.ts.map +1 -1
  116. package/dist/proxy/routing.d.ts +59 -1
  117. package/dist/proxy/routing.d.ts.map +1 -1
  118. package/dist/proxy/sdkFeatures.d.ts +1 -1
  119. package/dist/proxy/sdkFeatures.d.ts.map +1 -1
  120. package/dist/proxy/server.d.ts.map +1 -1
  121. package/dist/proxy/session/fingerprint.d.ts +5 -0
  122. package/dist/proxy/session/fingerprint.d.ts.map +1 -1
  123. package/dist/proxy/session/lineage.d.ts.map +1 -1
  124. package/dist/proxy/setup.d.ts.map +1 -1
  125. package/dist/proxy/supervision.d.ts +41 -0
  126. package/dist/proxy/supervision.d.ts.map +1 -0
  127. package/dist/proxy/tokenRefresh.d.ts +38 -4
  128. package/dist/proxy/tokenRefresh.d.ts.map +1 -1
  129. package/dist/proxy/types.d.ts +32 -0
  130. package/dist/proxy/types.d.ts.map +1 -1
  131. package/dist/server.js +11 -7
  132. package/dist/settings.d.ts +83 -0
  133. package/dist/settings.d.ts.map +1 -0
  134. package/dist/{setup-ndmjpy23.js → setup-7fr49z92.js} +1 -1
  135. package/dist/statusProbe-9x9q261e.js +151 -0
  136. package/dist/telemetry/dashboard.d.ts.map +1 -1
  137. package/dist/telemetry/index.d.ts +20 -2
  138. package/dist/telemetry/index.d.ts.map +1 -1
  139. package/dist/telemetry/landing.d.ts +1 -1
  140. package/dist/telemetry/landing.d.ts.map +1 -1
  141. package/dist/telemetry/logStore.d.ts +11 -0
  142. package/dist/telemetry/logStore.d.ts.map +1 -1
  143. package/dist/telemetry/pricingStore.d.ts +2 -2
  144. package/dist/telemetry/pricingStore.d.ts.map +1 -1
  145. package/dist/telemetry/profileBar.d.ts +3 -3
  146. package/dist/telemetry/profileBar.d.ts.map +1 -1
  147. package/dist/telemetry/profileFacts.d.ts +17 -0
  148. package/dist/telemetry/profileFacts.d.ts.map +1 -0
  149. package/dist/telemetry/profileOrder.d.ts +68 -0
  150. package/dist/telemetry/profileOrder.d.ts.map +1 -0
  151. package/dist/telemetry/profilePage.d.ts.map +1 -1
  152. package/dist/telemetry/profileSort.d.ts +35 -0
  153. package/dist/telemetry/profileSort.d.ts.map +1 -0
  154. package/dist/telemetry/profileSpent.d.ts +68 -0
  155. package/dist/telemetry/profileSpent.d.ts.map +1 -0
  156. package/dist/telemetry/providerPage.d.ts +2 -0
  157. package/dist/telemetry/providerPage.d.ts.map +1 -0
  158. package/dist/telemetry/providerSetup.d.ts +5 -0
  159. package/dist/telemetry/providerSetup.d.ts.map +1 -0
  160. package/dist/telemetry/providerView.d.ts +45 -0
  161. package/dist/telemetry/providerView.d.ts.map +1 -0
  162. package/dist/telemetry/routeChain.d.ts +32 -0
  163. package/dist/telemetry/routeChain.d.ts.map +1 -0
  164. package/dist/telemetry/routes.d.ts.map +1 -1
  165. package/dist/telemetry/settingsPage.d.ts +1 -1
  166. package/dist/telemetry/settingsPage.d.ts.map +1 -1
  167. package/dist/telemetry/sqlite.d.ts.map +1 -1
  168. package/dist/telemetry/store.d.ts +6 -1
  169. package/dist/telemetry/store.d.ts.map +1 -1
  170. package/dist/telemetry/types.d.ts +132 -2
  171. package/dist/telemetry/types.d.ts.map +1 -1
  172. package/dist/{tokenRefresh-kzz08kea.js → tokenRefresh-eg9h83wd.js} +6 -1
  173. package/package.json +11 -4
  174. package/dist/cli-5jxyma6z.js +0 -277
  175. package/dist/cli-vj9cv18n.js +0 -38
  176. package/dist/proxy/settings.d.ts +0 -30
  177. package/dist/proxy/settings.d.ts.map +0 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Persistent server settings.
3
+ *
4
+ * Stored in ~/.config/meridian/settings.json (MERIDIAN_CONFIG_DIR moves the
5
+ * directory). Survives proxy restarts.
6
+ * Shared between CLI, UI, and API — browser localStorage is only used
7
+ * for client-only preferences (theme, collapsed sections, etc.).
8
+ *
9
+ * Lives at the src root, beside env.ts, rather than under proxy/, because
10
+ * telemetry reads it too and telemetry may not import from proxy —
11
+ * dependencies flow proxy → telemetry only. env.ts is the existing precedent
12
+ * for "configuration both layers need"; a copy of the path logic in each
13
+ * would be two things to keep agreeing about MERIDIAN_CONFIG_DIR.
14
+ *
15
+ * Leaf module: node builtins only, no imports from anywhere in the tree.
16
+ */
17
+ import type { PriorityFailbackPolicy } from "./proxy/routing";
18
+ export interface MeridianSettings {
19
+ /** Last active profile ID — restored on proxy startup */
20
+ activeProfile?: string;
21
+ /** Profile routing mode (#383, priority spec): "active" (default),
22
+ * "sticky", or "priority". MERIDIAN_ROUTING env var takes precedence. */
23
+ routing?: string;
24
+ /** Priority-mode pool order (highest priority first). Falls back to
25
+ * profiles.json order. MERIDIAN_PROFILE_ORDER env var takes precedence. */
26
+ profileOrder?: string[];
27
+ priorityFailback?: PriorityFailbackPolicy;
28
+ /**
29
+ * Keep telemetry in SQLite instead of memory, so it survives a restart.
30
+ * MERIDIAN_TELEMETRY_PERSIST takes precedence.
31
+ *
32
+ * The four telemetry keys below differ from every other setting here in one
33
+ * way that the UI must not hide: the stores are built once at startup, so a
34
+ * change to any of them does nothing until the proxy is restarted. Routing
35
+ * is re-read per request; these are not, and cannot be without swapping a
36
+ * store out from under in-flight writes.
37
+ */
38
+ telemetryPersist?: boolean;
39
+ /** Days before persisted telemetry is deleted. MERIDIAN_TELEMETRY_RETENTION_DAYS wins. */
40
+ telemetryRetentionDays?: number;
41
+ /** Rows the in-memory ring holds. Ignored when persisting. MERIDIAN_TELEMETRY_SIZE wins. */
42
+ telemetrySize?: number;
43
+ /** Entries the in-memory diagnostic log ring holds. MERIDIAN_DIAGNOSTIC_LOG_SIZE wins. */
44
+ diagnosticLogSize?: number;
45
+ }
46
+ /**
47
+ * Accepted range for each numeric telemetry setting, and the reason for it.
48
+ *
49
+ * Lives here rather than in the route so the form and the validator read the
50
+ * same numbers: a page that offers a value the API rejects is a worse bug than
51
+ * either bound being slightly wrong.
52
+ *
53
+ * The upper bounds guard a value that only takes effect on the next start, so
54
+ * a typo is not discovered until a restart that then fails or thrashes. Both
55
+ * ring sizes are whole request/log records held in memory; a million rows is
56
+ * hundreds of megabytes of resident heap, which is not a setting anyone means
57
+ * to type. Retention is capped at ten years because SQLite cleanup deletes by
58
+ * a cutoff, and a cutoff further out than the data's usefulness simply never
59
+ * deletes anything.
60
+ */
61
+ export declare const TELEMETRY_SETTING_LIMITS: {
62
+ readonly telemetryRetentionDays: {
63
+ readonly min: 1;
64
+ readonly max: 3650;
65
+ };
66
+ readonly telemetrySize: {
67
+ readonly min: 10;
68
+ readonly max: 1000000;
69
+ };
70
+ readonly diagnosticLogSize: {
71
+ readonly min: 10;
72
+ readonly max: 1000000;
73
+ };
74
+ };
75
+ /** Read settings from disk. Returns empty object if file doesn't exist or is invalid. */
76
+ export declare function loadSettings(): MeridianSettings;
77
+ /** Write settings to disk. Merges with existing settings (doesn't clobber unknown keys). */
78
+ export declare function saveSettings(patch: Partial<MeridianSettings>): MeridianSettings;
79
+ /** Get a single setting value, or undefined if not set. */
80
+ export declare function getSetting<K extends keyof MeridianSettings>(key: K): MeridianSettings[K];
81
+ /** Update a single setting. */
82
+ export declare function setSetting<K extends keyof MeridianSettings>(key: K, value: MeridianSettings[K]): MeridianSettings;
83
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAc7D,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8EAC0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;gFAC4E;IAC5E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,gBAAgB,CAAC,EAAE,sBAAsB,CAAA;IAEzC;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,0FAA0F;IAC1F,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAI4B,CAAA;AAEjE,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAQ/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAO/E;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,+BAA+B;AAC/B,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAEjH"}
@@ -15,7 +15,7 @@ import {
15
15
  notePluginlessOpenCodeRequest,
16
16
  pluginPathForGeneration,
17
17
  runSetup
18
- } from "./cli-hxxy0m1z.js";
18
+ } from "./cli-82e2m44k.js";
19
19
  import"./cli-p9swy5t3.js";
20
20
  export {
21
21
  runSetup,
@@ -0,0 +1,151 @@
1
+ import"./cli-p9swy5t3.js";
2
+
3
+ // src/proxy/statusProbe.ts
4
+ import { createServer } from "node:net";
5
+ var PROBE_TIMEOUT_MS = 2000;
6
+ function probeUrlBase(host, port) {
7
+ let target = host;
8
+ if (host === "0.0.0.0" || host === "")
9
+ target = "127.0.0.1";
10
+ else if (host === "::" || host === "[::]")
11
+ target = "::1";
12
+ const literal = target.includes(":") ? `[${target.replace(/^\[|\]$/g, "")}]` : target;
13
+ return `http://${literal}:${port}`;
14
+ }
15
+ var HEALTH_STATUSES = new Set(["healthy", "degraded", "unhealthy"]);
16
+ function isMeridianHealthBody(body) {
17
+ if (typeof body !== "object" || body === null)
18
+ return false;
19
+ const record = body;
20
+ if (typeof record["status"] === "string" && HEALTH_STATUSES.has(record["status"]) && typeof record["version"] === "string") {
21
+ return true;
22
+ }
23
+ const error = record["error"];
24
+ if (record["type"] === "error" && typeof error === "object" && error !== null) {
25
+ return error["type"] === "authentication_error";
26
+ }
27
+ return false;
28
+ }
29
+ function describeForeignResponse(status, statusText, headers) {
30
+ const parts = [`HTTP ${status}${statusText ? ` ${statusText}` : ""}`];
31
+ const server = headers.get("server");
32
+ if (server)
33
+ parts.push(`server: ${server}`);
34
+ const contentType = headers.get("content-type");
35
+ if (contentType)
36
+ parts.push(contentType.split(";")[0].trim());
37
+ return parts.join(", ");
38
+ }
39
+ function isPortAvailable(host, port) {
40
+ return new Promise((resolve) => {
41
+ const probe = createServer();
42
+ probe.once("error", () => resolve(false));
43
+ probe.once("listening", () => probe.close(() => resolve(true)));
44
+ probe.listen(port, host);
45
+ });
46
+ }
47
+ async function readJson(fetchImpl, url, timeoutMs, apiKey) {
48
+ try {
49
+ const response = await fetchImpl(url, {
50
+ signal: AbortSignal.timeout(timeoutMs),
51
+ headers: apiKey ? { "x-api-key": apiKey } : undefined
52
+ });
53
+ let body = null;
54
+ try {
55
+ body = await response.json();
56
+ } catch {
57
+ body = null;
58
+ }
59
+ return { ok: true, status: response.status, statusText: response.statusText, body, headers: response.headers };
60
+ } catch (err) {
61
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
62
+ }
63
+ }
64
+ async function readSection(fetchImpl, url, timeoutMs, apiKey) {
65
+ const result = await readJson(fetchImpl, url, timeoutMs, apiKey);
66
+ if (!result.ok)
67
+ return { data: null, reason: result.reason };
68
+ if (result.status === 401) {
69
+ return {
70
+ data: null,
71
+ reason: apiKey ? "the running instance rejected this MERIDIAN_API_KEY" : "the running instance requires an API key — set MERIDIAN_API_KEY"
72
+ };
73
+ }
74
+ if (result.status >= 400)
75
+ return { data: null, reason: `HTTP ${result.status}` };
76
+ if (result.body == null)
77
+ return { data: null, reason: "unreadable response" };
78
+ return { data: result.body };
79
+ }
80
+ async function probeMeridian(host, port, options = {}) {
81
+ const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
82
+ const apiKey = options.apiKey || undefined;
83
+ const fetchImpl = options.fetchImpl ?? fetch;
84
+ const base = probeUrlBase(host, port);
85
+ const health = await readJson(fetchImpl, `${base}/health`, timeoutMs, apiKey);
86
+ if (!health.ok)
87
+ return { kind: "unreachable", description: health.reason };
88
+ if (!isMeridianHealthBody(health.body)) {
89
+ return {
90
+ kind: "foreign",
91
+ description: describeForeignResponse(health.status, health.statusText, health.headers)
92
+ };
93
+ }
94
+ const authRejected = health.status === 401;
95
+ const [profileList, quota, summary] = await Promise.all([
96
+ readSection(fetchImpl, `${base}/profiles/list`, timeoutMs, apiKey),
97
+ readSection(fetchImpl, `${base}/v1/usage/quota/all`, timeoutMs, apiKey),
98
+ readSection(fetchImpl, `${base}/telemetry/summary?window=86400000`, timeoutMs, apiKey)
99
+ ]);
100
+ const unavailable = {};
101
+ const accountsReason = profileList.reason ?? quota.reason;
102
+ if (accountsReason)
103
+ unavailable.accounts = `accounts unavailable — ${accountsReason}`;
104
+ if (summary.reason)
105
+ unavailable.traffic = `traffic unavailable — ${summary.reason}`;
106
+ return {
107
+ kind: "meridian",
108
+ snapshot: {
109
+ health: authRejected ? null : health.body,
110
+ profileList: profileList.data,
111
+ quota: quota.data,
112
+ summary: summary.data,
113
+ ...Object.keys(unavailable).length > 0 ? { unavailable } : {}
114
+ }
115
+ };
116
+ }
117
+ function formatStatusMessage(result, host, port) {
118
+ if (result.kind === "foreign") {
119
+ return [
120
+ `Error: http://${host}:${port} is not Meridian (${result.description}).`,
121
+ ` Something else owns that port. Point at the right one: MERIDIAN_PORT=<port> meridian status`
122
+ ].join(`
123
+ `);
124
+ }
125
+ return [
126
+ `Error: no Meridian instance is listening on http://${host}:${port}.`,
127
+ ` Start one with: meridian`
128
+ ].join(`
129
+ `);
130
+ }
131
+ function formatConflictMessage(result, host, port) {
132
+ const found = result.kind === "foreign" ? `Something is listening there, and it is not Meridian (${result.description}).` : `Something is holding the port but did not answer (${result.description}).`;
133
+ return [
134
+ `Error: ${host}:${port} is already in use.`,
135
+ found,
136
+ "",
137
+ ` Identify it with: lsof -i :${port}`,
138
+ ` Or use a different port: MERIDIAN_PORT=4567 meridian`
139
+ ].join(`
140
+ `);
141
+ }
142
+ export {
143
+ probeUrlBase,
144
+ probeMeridian,
145
+ isPortAvailable,
146
+ isMeridianHealthBody,
147
+ formatStatusMessage,
148
+ formatConflictMessage,
149
+ describeForeignResponse,
150
+ PROBE_TIMEOUT_MS
151
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QAgalB,CAAA"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QA8mBlB,CAAA"}
@@ -1,12 +1,30 @@
1
- import type { ITelemetryStore, IDiagnosticLogStore } from "./types";
1
+ import type { ITelemetryStore, IDiagnosticLogStore, ResolvedTelemetryConfig } from "./types";
2
+ /**
3
+ * What this process WOULD use if it started right now.
4
+ *
5
+ * Called at startup to build the stores, and again by the settings API to
6
+ * detect a pending restart — the same function both times on purpose. A
7
+ * second copy of this precedence chain living in the settings route is how a
8
+ * page ends up confidently reporting a state the proxy is not in.
9
+ */
10
+ export declare function resolveTelemetryConfig(): ResolvedTelemetryConfig;
2
11
  export declare const telemetryStore: ITelemetryStore;
3
12
  export declare const diagnosticLog: IDiagnosticLogStore;
13
+ /**
14
+ * Entries the running diagnostic ring holds, null when persisting.
15
+ *
16
+ * The metric store reports its own shape through `describe()`; the log store
17
+ * has no such method, and adding one to IDiagnosticLogStore for a single
18
+ * number would widen an interface two classes implement.
19
+ */
20
+ export declare const diagnosticLogCapacity: number | null;
4
21
  export { MemoryTelemetryStore } from "./store";
5
22
  export { MemoryDiagnosticLogStore } from "./logStore";
6
23
  export { createTelemetryRoutes } from "./routes";
7
24
  export { landingHtml } from "./landing";
8
25
  export { computePercentiles, computeSummary } from "./percentiles";
26
+ export { collapseRouteChains, summarizeRoutes } from "./routeChain";
9
27
  export { renderPrometheusMetrics } from "./prometheus";
10
28
  export { createSqliteStores } from "./sqlite";
11
- export type { RequestMetric, TelemetrySummary, PhaseTiming, ITelemetryStore, IDiagnosticLogStore, DiagnosticLog, } from "./types";
29
+ export type { RequestMetric, RouteHop, RouteKind, RouteProfileTally, RouteSummary, ResolvedTelemetryConfig, TelemetryRetention, TelemetrySummary, PhaseTiming, ITelemetryStore, IDiagnosticLogStore, DiagnosticLog, } from "./types";
12
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/telemetry/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AA2CnE,eAAO,MAAM,cAAc,EAAE,eAAkC,CAAA;AAC/D,eAAO,MAAM,aAAa,EAAE,mBAAwC,CAAA;AAEpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,aAAa,GACd,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/telemetry/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AA6B5F;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,uBAAuB,CAWhE;AA6CD,eAAO,MAAM,cAAc,EAAE,eAAkC,CAAA;AAC/D,eAAO,MAAM,aAAa,EAAE,mBAAwC,CAAA;AAEpE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,GAAG,IAAmC,CAAA;AAEhF,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EACV,aAAa,EACb,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,aAAa,GACd,MAAM,SAAS,CAAA"}
@@ -5,7 +5,7 @@
5
5
  * (usage + est. cost, click to switch the active profile), and a compact
6
6
  * 24h traffic strip. Site chrome (logo, nav, status) lives in the shared
7
7
  * header from profileBar.ts. Fetches /health, /telemetry/summary,
8
- * /v1/usage/quota/all and /profiles/list client-side for live data.
8
+ * /v1/usage/quota/all, /profiles/list and /settings/api/routing client-side for live data.
9
9
  */
10
10
  export declare const landingHtml: string;
11
11
  //# sourceMappingURL=landing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,WAAW,QA8QhB,CAAA"}
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,eAAO,MAAM,WAAW,QAigBhB,CAAA"}
@@ -7,6 +7,17 @@
7
7
  */
8
8
  import type { DiagnosticLog, IDiagnosticLogStore } from "./types";
9
9
  export type { DiagnosticLog } from "./types";
10
+ /**
11
+ * Tunable for the same reason the metric ring is: 500 entries is the tightest
12
+ * window in the telemetry system - the metric ring holds twice as many rows of
13
+ * a rarer event - and it was the only one with no way to change it, so the
14
+ * logs a bug report needs had usually scrolled away by the time anyone looked.
15
+ *
16
+ * Env beats the saved setting, which beats the default. A value that is not
17
+ * positive is discarded rather than falling through, so a typo cannot produce
18
+ * a zero-length ring.
19
+ */
20
+ export declare function resolveDiagnosticLogCapacity(): number;
10
21
  export declare class MemoryDiagnosticLogStore implements IDiagnosticLogStore {
11
22
  private buffer;
12
23
  private head;
@@ -1 +1 @@
1
- {"version":3,"file":"logStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/logStore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AACjE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAI5C,qBAAa,wBAAyB,YAAW,mBAAmB;IAClE,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,GAAG,IAAI;IAMlD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIhD,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAgB/F,KAAK,IAAI,IAAI;CAKd;AAED,4CAA4C;AAC5C,eAAO,MAAM,aAAa,0BAAiC,CAAA"}
1
+ {"version":3,"file":"logStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/logStore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AACjE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAI5C;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CAQrD;AAED,qBAAa,wBAAyB,YAAW,mBAAmB;IAClE,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,GAAG,IAAI;IAMlD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIhD,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAgB/F,KAAK,IAAI,IAAI;CAKd;AAED,4CAA4C;AAC5C,eAAO,MAAM,aAAa,0BAAiC,CAAA"}
@@ -5,8 +5,8 @@
5
5
  * static table in pricing.ts doesn't know about (custom routers, brand-new
6
6
  * releases). Overrides win over the built-in table in resolveModelPricing.
7
7
  *
8
- * Persisted to ~/.config/meridian/model-pricing.json (override the path with
9
- * MERIDIAN_PRICING_CONFIG). Read at request time with a short cache, so
8
+ * Persisted to model-pricing.json in the config directory (override the path
9
+ * with MERIDIAN_PRICING_CONFIG). Read at request time with a short cache, so
10
10
  * settings-page edits show up on the next dashboard refresh without a proxy
11
11
  * restart. Mirrors the sdkFeatures.ts persistence pattern.
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"pricingStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/pricingStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,WAAW,CAAA;AAElB,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AA8D3D,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAwBhE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAI7E;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAKzD;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,IAAI,IAAI,CAIjD"}
1
+ {"version":3,"file":"pricingStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/pricingStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,WAAW,CAAA;AAElB,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AA6D3D,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAwBhE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAI7E;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAKzD;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,IAAI,IAAI,CAIjD"}
@@ -27,9 +27,9 @@ 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";
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";
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-profile.following { border-color: var(--accent2, #bc8cff); }\n .meridian-header .mh-profile .mh-profile-follow {\n color: var(--accent2, #bc8cff); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-nav { order: 3; flex-basis: 100%; min-width: 0; overflow-x: auto; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }\n .meridian-header .mh-nav a { flex-shrink: 0; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n";
31
+ export declare const profileBarHtml = "\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\" aria-label=\"Meridian home\">\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=\"/providers\" id=\"nav-providers\">Providers</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 if (h.backend === 'antigravity') {\n ['nav-telemetry','nav-profiles','nav-settings','nav-plugins'].forEach(function(id) { document.getElementById(id).hidden = true; });\n profileChip.removeAttribute('href');\n }\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
33
  /** Native desktop chrome: system appearance with the canonical Meridian hues.
34
34
  * Web pages continue using themeCss and the shared profile header above. */
35
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";
@@ -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;AAED;4EAC4E;AAC5E,eAAO,MAAM,eAAe,wyBAoB3B,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;CAA2E,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,2hIAsFzB,CAAA;AAED,eAAO,MAAM,cAAc,qvDAoB1B,CAAA;AAED,eAAO,MAAM,YAAY,2wIAsFxB,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"}