@rynfar/meridian 1.50.0 → 1.52.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.
- package/README.md +59 -19
- package/assets/banner.svg +28 -21
- package/assets/bot-icon-512.png +0 -0
- package/assets/bot-icon.svg +49 -43
- package/assets/how-it-works.svg +40 -40
- package/assets/icon-512.png +0 -0
- package/assets/icon.svg +16 -13
- package/assets/logo-512.png +0 -0
- package/assets/logo.svg +14 -12
- package/dist/{cli-yw8cx02m.js → cli-tq3vtp0q.js} +935 -189
- package/dist/{cli-q6dz9r5p.js → cli-vjeftz4z.js} +16 -2
- package/dist/cli-xmweegb1.js +179 -0
- package/dist/cli.js +9 -5
- package/dist/{pluginPage-85s6t6k8.js → pluginPage-03q5j753.js} +4 -10
- package/dist/{profileCli-fbhe9p5z.js → profileCli-s1h4vh8w.js} +1 -1
- package/dist/{profilePage-txxpbc44.js → profilePage-naychnb8.js} +6 -5
- package/dist/proxy/errors.d.ts +9 -0
- package/dist/proxy/errors.d.ts.map +1 -1
- package/dist/proxy/messages.d.ts +20 -0
- package/dist/proxy/messages.d.ts.map +1 -1
- package/dist/proxy/models.d.ts +16 -1
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/oauthUsage.d.ts.map +1 -1
- package/dist/proxy/openai.d.ts.map +1 -1
- package/dist/proxy/plugins/pluginPage.d.ts.map +1 -1
- package/dist/proxy/query.d.ts +4 -0
- package/dist/proxy/query.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/server.js +3 -3
- package/dist/telemetry/dashboard.d.ts.map +1 -1
- package/dist/telemetry/landing.d.ts +6 -2
- package/dist/telemetry/landing.d.ts.map +1 -1
- package/dist/telemetry/percentiles.d.ts +5 -1
- package/dist/telemetry/percentiles.d.ts.map +1 -1
- package/dist/telemetry/pricing.d.ts +63 -0
- package/dist/telemetry/pricing.d.ts.map +1 -0
- package/dist/telemetry/pricingStore.d.ts +32 -0
- package/dist/telemetry/pricingStore.d.ts.map +1 -0
- package/dist/telemetry/profileBar.d.ts +19 -9
- package/dist/telemetry/profileBar.d.ts.map +1 -1
- package/dist/telemetry/profileUsage.d.ts.map +1 -1
- package/dist/telemetry/settingsPage.d.ts +1 -1
- package/dist/telemetry/settingsPage.d.ts.map +1 -1
- package/dist/telemetry/sqlite.d.ts.map +1 -1
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/types.d.ts +31 -0
- package/dist/telemetry/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cli-4rqtm83g.js +0 -148
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* Shared percentile computation used by all telemetry store implementations.
|
|
3
3
|
*/
|
|
4
4
|
import type { RequestMetric, PhaseTiming, TelemetrySummary } from "./types";
|
|
5
|
+
import { type ModelPricing } from "./pricing";
|
|
5
6
|
export declare function computePercentiles(values: number[]): PhaseTiming;
|
|
6
7
|
/**
|
|
7
8
|
* Compute a TelemetrySummary from an array of RequestMetric.
|
|
8
9
|
* Both MemoryTelemetryStore and SqliteTelemetryStore use this
|
|
9
10
|
* to guarantee identical output.
|
|
11
|
+
*
|
|
12
|
+
* pricingOverrides: user-defined model rates (from pricingStore) applied
|
|
13
|
+
* to the cost estimate; omit for built-in pricing only.
|
|
10
14
|
*/
|
|
11
|
-
export declare function computeSummary(metrics: RequestMetric[], windowMs: number): TelemetrySummary;
|
|
15
|
+
export declare function computeSummary(metrics: RequestMetric[], windowMs: number, pricingOverrides?: Record<string, ModelPricing>): TelemetrySummary;
|
|
12
16
|
//# sourceMappingURL=percentiles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"percentiles.d.ts","sourceRoot":"","sources":["../../src/telemetry/percentiles.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"percentiles.d.ts","sourceRoot":"","sources":["../../src/telemetry/percentiles.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC3E,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA;AAElE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,CAchE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,aAAa,EAAE,EACxB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAC9C,gBAAgB,CA4GlB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Anthropic API list pricing and cost estimation (PURE, no I/O).
|
|
3
|
+
*
|
|
4
|
+
* Meridian proxies Claude Max, so requests are covered by the subscription
|
|
5
|
+
* and never billed per token. The numbers produced here answer a different
|
|
6
|
+
* question: "what would this usage have cost at API list prices?", useful
|
|
7
|
+
* for judging how much value the subscription is delivering.
|
|
8
|
+
*
|
|
9
|
+
* Rates are USD per million tokens, verified against the official pricing
|
|
10
|
+
* docs (platform.claude.com/docs/en/about-claude/pricing, snapshot 2026-07).
|
|
11
|
+
* Users can override any rate, or add models missing from this table, via
|
|
12
|
+
* the settings page (persisted by pricingStore.ts; overrides win in
|
|
13
|
+
* resolveModelPricing). Cache rates are derived from the input rate:
|
|
14
|
+
* - cache read = 0.10× input
|
|
15
|
+
* - cache write = 1.25× input (5-minute TTL, the TTL the SDK uses, see
|
|
16
|
+
* MONITORING.md; 1-hour TTL writes bill at 2×, so if a client opted into
|
|
17
|
+
* the long TTL this estimate is a floor)
|
|
18
|
+
*
|
|
19
|
+
* Models without a table entry are excluded from the total and surfaced via
|
|
20
|
+
* `unpricedRequestCount` instead of silently costing $0.
|
|
21
|
+
*/
|
|
22
|
+
import type { RequestMetric, CostEstimate } from "./types";
|
|
23
|
+
export interface ModelPricing {
|
|
24
|
+
/** USD per 1M uncached input tokens */
|
|
25
|
+
inputPerMTok: number;
|
|
26
|
+
/** USD per 1M output tokens */
|
|
27
|
+
outputPerMTok: number;
|
|
28
|
+
/** USD per 1M cache-read input tokens */
|
|
29
|
+
cacheReadPerMTok: number;
|
|
30
|
+
/** USD per 1M cache-write (creation) input tokens */
|
|
31
|
+
cacheWritePerMTok: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const CACHE_READ_MULTIPLIER = 0.1;
|
|
34
|
+
export declare const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
35
|
+
/** Normalize a model string for pricing lookup: trimmed, lowercase, [1m] suffix stripped. */
|
|
36
|
+
export declare function normalizeModelKey(model: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Exact-match table. Keys are lowercase, checked after stripping the [1m]
|
|
39
|
+
* suffix. Covers the SDK aliases meridian itself uses (opus, sonnet, ...)
|
|
40
|
+
* plus concrete API model IDs that clients commonly send. Dated snapshot
|
|
41
|
+
* IDs (e.g. claude-haiku-4-5-20251001) fall through to the family rules.
|
|
42
|
+
*
|
|
43
|
+
* Exported for the settings page, which lists these models with their
|
|
44
|
+
* default rates so users can override them.
|
|
45
|
+
*/
|
|
46
|
+
export declare const BUILTIN_MODEL_PRICING: Record<string, ModelPricing>;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a model string to pricing. User-defined overrides win, then the
|
|
49
|
+
* exact built-in table, then family fallback so versioned/dated IDs
|
|
50
|
+
* (claude-opus-4-8, claude-haiku-4-5-20251001) and future releases still
|
|
51
|
+
* price at their family's current rate.
|
|
52
|
+
* Returns null for unrecognized models; callers must not treat that as $0.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveModelPricing(model: string, overrides?: Record<string, ModelPricing>): ModelPricing | null;
|
|
55
|
+
/** Estimated USD for a single request's token usage at the given rates. */
|
|
56
|
+
export declare function estimateRequestCostUsd(metric: RequestMetric, pricing: ModelPricing): number;
|
|
57
|
+
/**
|
|
58
|
+
* Aggregate estimated cost per model across a set of metrics.
|
|
59
|
+
* Grouping key is requestModel || model, matching computeSummary's byModel.
|
|
60
|
+
* Pass overrides (from pricingStore) to apply user-defined rates.
|
|
61
|
+
*/
|
|
62
|
+
export declare function computeCostEstimate(metrics: RequestMetric[], overrides?: Record<string, ModelPricing>): CostEstimate;
|
|
63
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/telemetry/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAsB,MAAM,SAAS,CAAA;AAE9E,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAA;IACxB,qDAAqD;IACrD,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,eAAO,MAAM,qBAAqB,MAAM,CAAA;AACxC,eAAO,MAAM,sBAAsB,OAAO,CAAA;AAE1C,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAsBD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqB9D,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GACvC,YAAY,GAAG,IAAI,CAwBrB;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAO3F;AAOD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GACvC,YAAY,CA6Cd"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-defined model pricing overrides.
|
|
3
|
+
*
|
|
4
|
+
* Lets users correct a stale built-in rate or define pricing for models the
|
|
5
|
+
* static table in pricing.ts doesn't know about (custom routers, brand-new
|
|
6
|
+
* releases). Overrides win over the built-in table in resolveModelPricing.
|
|
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
|
|
10
|
+
* settings-page edits show up on the next dashboard refresh without a proxy
|
|
11
|
+
* restart. Mirrors the sdkFeatures.ts persistence pattern.
|
|
12
|
+
*/
|
|
13
|
+
import { type ModelPricing } from "./pricing";
|
|
14
|
+
export type PricingOverrides = Record<string, ModelPricing>;
|
|
15
|
+
/** Current user-defined pricing overrides, keyed by normalized model string. */
|
|
16
|
+
export declare function getPricingOverrides(): PricingOverrides;
|
|
17
|
+
/**
|
|
18
|
+
* Validate a pricing update body into a complete ModelPricing.
|
|
19
|
+
* inputPerMTok and outputPerMTok are required; cache rates are optional and
|
|
20
|
+
* derived from the input rate (read 0.1x, write 1.25x) when omitted.
|
|
21
|
+
* Throws on invalid input.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validatePricingUpdate(raw: unknown): ModelPricing;
|
|
24
|
+
/** Validate the model key for an override. Throws on invalid input. */
|
|
25
|
+
export declare function validateModelKey(model: string): string;
|
|
26
|
+
/** Create or replace the pricing override for a model. */
|
|
27
|
+
export declare function setPricingOverride(model: string, pricing: ModelPricing): void;
|
|
28
|
+
/** Remove the pricing override for a model (reverts to the built-in table). */
|
|
29
|
+
export declare function deletePricingOverride(model: string): void;
|
|
30
|
+
/** Reset the read cache — for testing only (path changes between tests). */
|
|
31
|
+
export declare function resetPricingOverridesCache(): void;
|
|
32
|
+
//# sourceMappingURL=pricingStore.d.ts.map
|
|
@@ -0,0 +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,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
2
|
+
* Shared site header — injected into all HTML pages.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One piece of chrome for the whole app: brand (logo + wordmark), site nav,
|
|
5
|
+
* live health pill, and the active-profile chip. Profile *switching* happens
|
|
6
|
+
* on the home page (click an account card) or the Profiles page — the header
|
|
7
|
+
* only shows which profile is active.
|
|
8
|
+
*
|
|
9
|
+
* CSS and JS are self-contained. Export names are kept from the old profile
|
|
10
|
+
* bar (profileBarCss/Html/Js) so every page picks the header up unchanged.
|
|
7
11
|
*/
|
|
8
12
|
/**
|
|
9
|
-
* Canonical Meridian theme
|
|
13
|
+
* Canonical Meridian theme.
|
|
10
14
|
*
|
|
11
15
|
* Every inline HTML page (landing, telemetry dashboard, profiles,
|
|
12
16
|
* settings, plugins) prepends this block before its own styles so
|
|
@@ -16,8 +20,14 @@
|
|
|
16
20
|
* so the telemetry waterfall and lineage-colored badges keep their
|
|
17
21
|
* semantic meaning without needing per-page overrides.
|
|
18
22
|
*/
|
|
19
|
-
export declare const themeCss = "\n :root {\n /* Cool-gray neutral palette
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export declare const themeCss = "\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";
|
|
24
|
+
/**
|
|
25
|
+
* The Meridian mark: a wireframe globe whose prime meridian runs the brand
|
|
26
|
+
* gradient from blue (north) to violet (south), pinned by a node at each
|
|
27
|
+
* pole. Scales cleanly from favicon to hero size.
|
|
28
|
+
*/
|
|
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 .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-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 statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n";
|
|
23
33
|
//# sourceMappingURL=profileBar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profileBar.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileBar.ts"],"names":[],"mappings":"AAAA
|
|
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,glFAyDzB,CAAA;AAED,eAAO,MAAM,cAAc,ukDAkB1B,CAAA;AAED,eAAO,MAAM,YAAY,k0DAyCxB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profileUsage.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uDAAuD;AACvD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"profileUsage.d.ts","sourceRoot":"","sources":["../../src/telemetry/profileUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uDAAuD;AACvD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAShD,CAAA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnD;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAA;AAChD,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,CAKvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAY1G;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;CACpB;AACD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE;IACnC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI,CAiB9C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAA;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB;kEAC8D;IAC9D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,yCAAyC;IACzC,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAA;IAChC,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAA;CACxB;AAQD,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,GAAG,GAAE,MAAmB,GACvB,UAAU,GAAG,IAAI,CAoBnB"}
|
|
@@ -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 \u2014 matches the original dashboard/profiles/\n settings look. Better for data-dense dashboards: high contrast,\n surface/border separation, no color cast muddying the text. Purple\n accents are retained as secondary brand color for hover states,\n gradients, 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, purple 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\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n background: var(--bg); color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-profile-bar {\n position: sticky; top: 0; z-index: 100;\n display: none; align-items: center; gap: 12px;\n padding: 8px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n font-size: 12px;\n color: var(--muted, #8b949e);\n }\n .meridian-profile-bar.visible { display: flex; }\n .meridian-profile-bar .profile-label {\n font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\n font-size: 10px; color: var(--muted, #8b949e);\n }\n .meridian-profile-bar select {\n background: var(--surface, #161b22); color: var(--text, #e6edf3);\n border: 1px solid var(--border, #30363d); border-radius: 6px;\n padding: 4px 24px 4px 10px; font-size: 12px; cursor: pointer;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%238b949e' stroke-width='1.5'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 6px center;\n }\n .meridian-profile-bar select:hover { border-color: var(--accent, #58a6ff); }\n .meridian-profile-bar select:focus { outline: none; border-color: var(--accent, #58a6ff); box-shadow: 0 0 0 1px var(--accent, #58a6ff); }\n .meridian-profile-bar .profile-status {\n font-size: 11px; color: var(--green, #3fb950); opacity: 0;\n transition: opacity 0.3s;\n }\n .meridian-profile-bar .profile-status.show { opacity: 1; }\n .meridian-profile-bar .profile-type {\n font-size: 10px; padding: 2px 8px; border-radius: 4px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n }\n .meridian-profile-bar .spacer { flex: 1; }\n .meridian-profile-bar .profile-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 11px;\n padding: 4px 8px; border-radius: 4px; transition: color 0.15s;\n }\n .meridian-profile-bar .profile-nav a:hover { color: var(--text, #e6edf3); }\n .meridian-profile-bar .profile-nav a.active { color: var(--accent, #58a6ff); }\n\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</style>\n</head>\n<body>\n\n<div class=\"meridian-profile-bar\" id=\"meridianProfileBar\">\n <span class=\"profile-label\">Profile</span>\n <select id=\"meridianProfileSelect\"></select>\n <span class=\"profile-type\" id=\"meridianProfileType\"></span>\n <span class=\"profile-status\" id=\"meridianProfileStatus\">\u2713 Switched</span>\n <div class=\"spacer\"></div>\n <div class=\"profile-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </div>\n</div>\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</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '"');\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\nloadConfig();\n\n(function() {\n var profileBar = document.getElementById('meridianProfileBar');\n var profileSelect = document.getElementById('meridianProfileSelect');\n var profileType = document.getElementById('meridianProfileType');\n var profileStatus = document.getElementById('meridianProfileStatus');\n var statusTimeout;\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.profile-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadProfiles() {\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n if (!data.profiles || data.profiles.length === 0) {\n profileBar.classList.remove('visible');\n return;\n }\n profileBar.classList.add('visible');\n var current = data.profiles.find(function(p) { return p.isActive; });\n profileSelect.innerHTML = data.profiles.map(function(p) {\n return '<option value=\"' + esc(p.id) + '\"' + (p.isActive ? ' selected' : '') + '>' + esc(p.id) + '</option>';\n }).join('');\n if (current) profileType.textContent = current.type;\n }).catch(function() {});\n }\n\n profileSelect.onchange = function() {\n fetch('/profiles/active', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ profile: profileSelect.value })\n }).then(function(r) { return r.json(); }).then(function(data) {\n if (data.success) {\n profileStatus.classList.add('show');\n clearTimeout(statusTimeout);\n statusTimeout = setTimeout(function() { profileStatus.classList.remove('show'); }, 2000);\n loadProfiles();\n }\n }).catch(function() {});\n };\n\n loadProfiles();\n setInterval(loadProfiles, 10000);\n})();\n\n</script>\n</body>\n</html>";
|
|
5
|
+
export declare const settingsPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Meridian \u2014 SDK Features</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n \n :root {\n /* Cool-gray neutral palette. High contrast, surface/border separation,\n no color cast muddying the text. Blue is the primary accent; violet\n is the fixed secondary \u2014 used together in the brand gradient and\n individually for hover states and a handful of telemetry badges. */\n --bg: #0d1117;\n --surface: #161b22;\n --surface2: #1c2128;\n --border: #30363d;\n /* Text */\n --text: #e6edf3;\n --muted: #8b949e;\n /* Brand \u2014 blue primary, violet secondary */\n --accent: #58a6ff;\n --accent2: #bc8cff;\n --violet: #bc8cff;\n --lavender: #d2a8ff;\n /* Semantic */\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n /* Telemetry-specific aliases (waterfall + lineage badges) */\n --blue: #58a6ff;\n --purple: #bc8cff;\n --queue: #d29922;\n --ttfb: #58a6ff;\n --upstream: #3fb950;\n }\n /* Banner backsplash \u2014 the brand look: a gentle diagonal wash with soft\n blue (top-left) and violet (bottom-right) glows. Pages must not set\n their own body background so this shows through everywhere. */\n body {\n background:\n radial-gradient(1200px 800px at 12% -8%, rgba(88,166,255,0.07), transparent 60%),\n radial-gradient(1100px 800px at 92% 108%, rgba(188,140,255,0.06), transparent 60%),\n linear-gradient(135deg, #0d1117 0%, #10151d 55%, #161b22 100%);\n background-attachment: fixed;\n background-color: var(--bg);\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-header {\n position: sticky; top: 0; z-index: 100;\n display: flex; align-items: center; gap: 20px;\n padding: 10px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n }\n .meridian-header .mh-brand {\n display: flex; align-items: center; gap: 10px;\n text-decoration: none; color: var(--text, #e6edf3);\n }\n .meridian-header .mh-logo { display: block; }\n .meridian-header .mh-name {\n font-size: 15px; font-weight: 700; letter-spacing: 2px;\n text-transform: uppercase;\n }\n .meridian-header .mh-nav { display: flex; align-items: center; gap: 2px; }\n .meridian-header .mh-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 12px;\n font-weight: 500; padding: 5px 10px; border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n }\n .meridian-header .mh-nav a:hover { color: var(--text, #e6edf3); background: var(--surface, #161b22); }\n .meridian-header .mh-nav a.active { color: var(--accent, #58a6ff); background: var(--surface, #161b22); }\n .meridian-header .mh-right {\n margin-left: auto; display: flex; align-items: center; gap: 10px;\n }\n .meridian-header .mh-profile {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; color: var(--text, #e6edf3);\n padding: 3px 10px; border-radius: 20px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n text-decoration: none; transition: border-color 0.15s;\n }\n .meridian-header .mh-profile:hover { border-color: var(--accent, #58a6ff); }\n .meridian-header .mh-profile.visible { display: inline-flex; }\n .meridian-header .mh-profile .mh-profile-type {\n color: var(--muted, #8b949e); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n\n .content { max-width: 900px; margin: 0 auto; padding: 24px; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .nav { display: flex; gap: 16px; margin-bottom: 24px; font-size: 13px; }\n .nav a { color: var(--muted); text-decoration: none; }\n .nav a:hover { color: var(--accent); }\n .nav a.active { color: var(--accent); }\n\n .adapter-card {\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 20px; margin-bottom: 16px;\n }\n .adapter-header {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 16px;\n }\n .adapter-name { font-size: 16px; font-weight: 600; }\n .adapter-badge {\n font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .badge-active { background: rgba(63, 185, 80, 0.15); color: var(--green); }\n .badge-inactive { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n\n .feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n @media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }\n\n .feature-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: 6px;\n background: var(--bg); border: 1px solid var(--border);\n }\n .feature-info { display: flex; flex-direction: column; }\n .feature-label { font-size: 13px; font-weight: 500; }\n .feature-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n\n /* Toggle switch */\n .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .toggle-track {\n position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;\n background: var(--border); border-radius: 10px; transition: background 0.2s;\n }\n .toggle-track::after {\n content: \"\"; position: absolute; height: 14px; width: 14px;\n left: 3px; bottom: 3px; background: var(--muted); border-radius: 50%;\n transition: transform 0.2s, background 0.2s;\n }\n .toggle input:checked + .toggle-track { background: var(--accent); }\n .toggle input:checked + .toggle-track::after {\n transform: translateX(16px); background: var(--text);\n }\n\n /* Select dropdown */\n .feature-select {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer;\n }\n\n .save-indicator {\n position: fixed; bottom: 24px; right: 24px;\n background: var(--green); color: #000; padding: 8px 16px;\n border-radius: 6px; font-size: 13px; font-weight: 500;\n opacity: 0; transition: opacity 0.3s; pointer-events: none;\n }\n .save-indicator.visible { opacity: 1; }\n\n .reset-btn {\n background: none; border: 1px solid var(--border); color: var(--muted);\n border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;\n }\n .reset-btn:hover { border-color: var(--red); color: var(--red); }\n\n /* Model pricing */\n .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }\n .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }\n .pricing-table tr:last-child td { border-bottom: none; }\n .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }\n .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;\n font-variant-numeric: tabular-nums; }\n .pricing-input:focus { border-color: var(--accent); outline: none; }\n .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }\n .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }\n .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;\n padding-top: 14px; border-top: 1px solid var(--border); }\n .pricing-add input[type=\"text\"] { width: 240px; text-align: left; }\n .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;\n padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }\n .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }\n</style>\n</head>\n<body>\n\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\">\n <svg class=\"mh-logo\" width=\"24\" height=\"24\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <defs>\n <linearGradient id=\"mhGrad\" x1=\"32\" y1=\"4\" x2=\"32\" y2=\"60\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#58a6ff\"/>\n <stop offset=\"1\" stop-color=\"#bc8cff\"/>\n </linearGradient>\n </defs>\n <circle cx=\"32\" cy=\"32\" r=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"3.5\"/>\n <ellipse cx=\"32\" cy=\"32\" rx=\"10.5\" ry=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"2.5\" opacity=\"0.8\"/>\n <path d=\"M7 32h50\" stroke=\"url(#mhGrad)\" stroke-width=\"2\" opacity=\"0.4\"/>\n <circle cx=\"32\" cy=\"7\" r=\"4.5\" fill=\"#58a6ff\"/>\n <circle cx=\"32\" cy=\"57\" r=\"4.5\" fill=\"#bc8cff\"/>\n</svg>\n <span class=\"mh-name\">Meridian</span>\n </a>\n <nav class=\"mh-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </nav>\n <div class=\"mh-right\">\n <a class=\"mh-profile\" id=\"mhProfile\" href=\"/\" title=\"Active profile \u2014 switch from the home page\"></a>\n <span class=\"mh-status\" id=\"mhStatus\"><span class=\"mh-dot\" id=\"mhDot\"></span><span class=\"mh-status-text\" id=\"mhStatusText\"></span></span>\n </div>\n</header>\n\n<div class=\"content\">\n <h1>SDK Features <span style=\"font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(210,153,34,0.15);color:var(--yellow);vertical-align:middle;margin-left:8px\">Experimental</span></h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Unlock Claude Code features for any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md \u2014 normally\n exclusive to Claude Code \u2014 become available to OpenCode, Crush, Droid, and any other harness routed through Meridian.\n Each agent keeps its own toolchain while gaining access to these additional features.<br><br>\n <strong style=\"color:var(--text)\">System prompts:</strong> For these features to work correctly, both the Claude Code prompt and your client prompt\n should be enabled. When both are active, they are appended together \u2014 Claude Code's base instructions come first,\n followed by your agent's specific instructions.\n </p>\n\n <div id=\"adapters\"></div>\n\n <h1 style=\"margin-top:40px\">Model Pricing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the\n built-in rate, or add models the built-in table doesn't know about (they show as \"no pricing\" on the\n dashboard until defined here). Changes apply on the next dashboard refresh.\n </p>\n <div class=\"adapter-card\">\n <table class=\"pricing-table\">\n <thead><tr><th>Model</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Source</th><th></th></tr></thead>\n <tbody id=\"pricingRows\"></tbody>\n </table>\n <div class=\"pricing-add\">\n <input type=\"text\" class=\"pricing-input\" id=\"newModelName\" placeholder=\"model id (e.g. claude-opus-9)\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelInput\" placeholder=\"input\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelOutput\" placeholder=\"output\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheRead\" placeholder=\"cache read\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheWrite\" placeholder=\"cache write\" min=\"0\" step=\"0.01\">\n <button class=\"add-btn\" onclick=\"addPricingModel()\">Add Model</button>\n </div>\n <div class=\"pricing-note\">\n Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the\n input rate (the 5-minute cache TTL multipliers). Verify current list prices at\n <a href=\"https://claude.com/pricing\" target=\"_blank\" rel=\"noreferrer\" style=\"color:var(--accent)\">claude.com/pricing</a>.\n </div>\n </div>\n</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '"');\n row.innerHTML = info +\n '<input type=\"text\" class=\"feature-select\" style=\"width:180px\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">';\n }\n\n grid.appendChild(row);\n }\n\n card.appendChild(grid);\n container.appendChild(card);\n }\n}\n\n// ---- Model pricing (telemetry cost estimate) ----\nlet pricingData = { builtin: {}, overrides: {} };\n\nfunction fmtRate(v) { return String(Math.round(v * 10000) / 10000); }\n\nasync function loadPricing() {\n const res = await fetch('/settings/api/pricing');\n pricingData = await res.json();\n renderPricing();\n}\n\nasync function putPricing(model, rates) {\n const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(rates),\n });\n if (!res.ok) {\n const err = await res.json().catch(function () { return {}; });\n alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));\n return false;\n }\n showSaved();\n return true;\n}\n\nasync function removePricing(model) {\n await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });\n showSaved();\n await loadPricing();\n}\n\nfunction rateCell(value, onCommit) {\n const td = document.createElement('td');\n const input = document.createElement('input');\n input.type = 'number';\n input.min = '0';\n input.step = '0.01';\n input.className = 'pricing-input';\n input.value = fmtRate(value);\n input.addEventListener('change', onCommit);\n td.appendChild(input);\n return { td: td, input: input };\n}\n\nfunction renderPricing() {\n const tbody = document.getElementById('pricingRows');\n tbody.innerHTML = '';\n const models = Array.from(new Set(\n Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))\n )).sort();\n\n for (const model of models) {\n const override = pricingData.overrides[model];\n const effective = override || pricingData.builtin[model];\n const tr = document.createElement('tr');\n\n const nameTd = document.createElement('td');\n nameTd.className = 'pricing-model';\n nameTd.textContent = model;\n tr.appendChild(nameTd);\n\n const cells = [];\n const commit = async function () {\n const rates = {\n inputPerMTok: parseFloat(cells[0].input.value),\n outputPerMTok: parseFloat(cells[1].input.value),\n cacheReadPerMTok: parseFloat(cells[2].input.value),\n cacheWritePerMTok: parseFloat(cells[3].input.value),\n };\n for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }\n if (await putPricing(model, rates)) await loadPricing();\n };\n ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {\n const cell = rateCell(effective[key], commit);\n cells.push(cell);\n tr.appendChild(cell.td);\n });\n\n const badgeTd = document.createElement('td');\n const badge = document.createElement('span');\n badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');\n badge.textContent = override ? 'Override' : 'Built-in';\n badgeTd.appendChild(badge);\n tr.appendChild(badgeTd);\n\n const actionTd = document.createElement('td');\n if (override) {\n const btn = document.createElement('button');\n btn.className = 'reset-btn';\n btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';\n btn.addEventListener('click', function () { removePricing(model); });\n actionTd.appendChild(btn);\n }\n tr.appendChild(actionTd);\n\n tbody.appendChild(tr);\n }\n}\n\nasync function addPricingModel() {\n const name = document.getElementById('newModelName').value.trim();\n const inputRate = parseFloat(document.getElementById('newModelInput').value);\n const outputRate = parseFloat(document.getElementById('newModelOutput').value);\n if (!name) { alert('Enter a model id'); return; }\n if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }\n const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };\n const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);\n const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);\n if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;\n if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;\n if (await putPricing(name, rates)) {\n ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {\n document.getElementById(id).value = '';\n });\n await loadPricing();\n }\n}\n\nloadConfig();\nloadPricing();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n\n</script>\n</body>\n</html>";
|
|
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,
|
|
1
|
+
{"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,+p0BAuZrB,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;
|
|
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;AA4WnH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;eAGV,eAAe;iBACxB,mBAAmB;;EAGvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAc/E,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,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,yCAAyC;IACzC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAMnC,8CAA8C;IAC9C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAiB5F;kEAC8D;IAC9D,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAWlE;;;OAGG;IACH,SAAS,CAAC,QAAQ,GAAE,MAAuB,GAAG,gBAAgB;IAM9D,gCAAgC;IAChC,KAAK,IAAI,IAAI;CAKd;AAED,kDAAkD;AAClD,eAAO,MAAM,cAAc,sBAA6B,CAAA"}
|
|
@@ -24,6 +24,9 @@ export interface RequestMetric {
|
|
|
24
24
|
model: string;
|
|
25
25
|
/** Original model string from the client request (e.g. "claude-sonnet-4-6-20250312") */
|
|
26
26
|
requestModel?: string;
|
|
27
|
+
/** Profile that served the request (multi-account); absent on early
|
|
28
|
+
* parse-error records where profile resolution never ran. */
|
|
29
|
+
profileId?: string;
|
|
27
30
|
/** Streaming or non-streaming */
|
|
28
31
|
mode: "stream" | "non-stream";
|
|
29
32
|
/** Envelope-integrity violations detected on this response (dangling_block,
|
|
@@ -95,6 +98,32 @@ export interface PhaseTiming {
|
|
|
95
98
|
max: number;
|
|
96
99
|
avg: number;
|
|
97
100
|
}
|
|
101
|
+
/** Per-model token totals and estimated cost at static API list prices. */
|
|
102
|
+
export interface ModelCostBreakdown {
|
|
103
|
+
requests: number;
|
|
104
|
+
inputTokens: number;
|
|
105
|
+
outputTokens: number;
|
|
106
|
+
cacheReadTokens: number;
|
|
107
|
+
cacheCreationTokens: number;
|
|
108
|
+
/** Estimated USD for this model's requests; null when the model has no pricing entry */
|
|
109
|
+
estimatedUsd: number | null;
|
|
110
|
+
}
|
|
111
|
+
/** Aggregate cost estimate across a window. Estimates only: Claude Max
|
|
112
|
+
* usage is covered by the subscription; this is the equivalent API cost. */
|
|
113
|
+
export interface CostEstimate {
|
|
114
|
+
/** Sum across all priced models (unpriced models excluded) */
|
|
115
|
+
totalUsd: number;
|
|
116
|
+
/** Keyed by requestModel || model, matching TelemetrySummary.byModel */
|
|
117
|
+
byModel: Record<string, ModelCostBreakdown>;
|
|
118
|
+
/** Requests whose model had no pricing entry, excluded from totalUsd */
|
|
119
|
+
unpricedRequestCount: number;
|
|
120
|
+
/** Per-profile rollup (multi-account): estimated USD + request count.
|
|
121
|
+
* Keyed by RequestMetric.profileId, "default" when absent. */
|
|
122
|
+
byProfile: Record<string, {
|
|
123
|
+
requests: number;
|
|
124
|
+
estimatedUsd: number;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
98
127
|
export interface TelemetrySummary {
|
|
99
128
|
/** Time window these stats cover */
|
|
100
129
|
windowMs: number;
|
|
@@ -133,6 +162,8 @@ export interface TelemetrySummary {
|
|
|
133
162
|
/** Requests where cache hit rate was 0 despite being a resume */
|
|
134
163
|
cacheMissOnResumeCount: number;
|
|
135
164
|
};
|
|
165
|
+
/** Estimated cost of the window's usage at static API list prices */
|
|
166
|
+
costEstimate: CostEstimate;
|
|
136
167
|
}
|
|
137
168
|
/** Storage backend for request metrics. */
|
|
138
169
|
export interface ITelemetryStore {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/telemetry/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/telemetry/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/dist/cli-4rqtm83g.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__esm
|
|
3
|
-
} from "./cli-p9swy5t3.js";
|
|
4
|
-
|
|
5
|
-
// src/telemetry/profileBar.ts
|
|
6
|
-
var themeCss = `
|
|
7
|
-
:root {
|
|
8
|
-
/* Cool-gray neutral palette — matches the original dashboard/profiles/
|
|
9
|
-
settings look. Better for data-dense dashboards: high contrast,
|
|
10
|
-
surface/border separation, no color cast muddying the text. Purple
|
|
11
|
-
accents are retained as secondary brand color for hover states,
|
|
12
|
-
gradients, and a handful of telemetry badges. */
|
|
13
|
-
--bg: #0d1117;
|
|
14
|
-
--surface: #161b22;
|
|
15
|
-
--surface2: #1c2128;
|
|
16
|
-
--border: #30363d;
|
|
17
|
-
/* Text */
|
|
18
|
-
--text: #e6edf3;
|
|
19
|
-
--muted: #8b949e;
|
|
20
|
-
/* Brand — blue primary, purple secondary */
|
|
21
|
-
--accent: #58a6ff;
|
|
22
|
-
--accent2: #bc8cff;
|
|
23
|
-
--violet: #bc8cff;
|
|
24
|
-
--lavender: #d2a8ff;
|
|
25
|
-
/* Semantic */
|
|
26
|
-
--green: #3fb950;
|
|
27
|
-
--yellow: #d29922;
|
|
28
|
-
--red: #f85149;
|
|
29
|
-
/* Telemetry-specific aliases (waterfall + lineage badges) */
|
|
30
|
-
--blue: #58a6ff;
|
|
31
|
-
--purple: #bc8cff;
|
|
32
|
-
--queue: #d29922;
|
|
33
|
-
--ttfb: #58a6ff;
|
|
34
|
-
--upstream: #3fb950;
|
|
35
|
-
}
|
|
36
|
-
`, profileBarCss = `
|
|
37
|
-
.meridian-profile-bar {
|
|
38
|
-
position: sticky; top: 0; z-index: 100;
|
|
39
|
-
display: none; align-items: center; gap: 12px;
|
|
40
|
-
padding: 8px 24px;
|
|
41
|
-
background: rgba(13, 17, 23, 0.92);
|
|
42
|
-
backdrop-filter: blur(12px);
|
|
43
|
-
border-bottom: 1px solid var(--border, #30363d);
|
|
44
|
-
font-size: 12px;
|
|
45
|
-
color: var(--muted, #8b949e);
|
|
46
|
-
}
|
|
47
|
-
.meridian-profile-bar.visible { display: flex; }
|
|
48
|
-
.meridian-profile-bar .profile-label {
|
|
49
|
-
font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;
|
|
50
|
-
font-size: 10px; color: var(--muted, #8b949e);
|
|
51
|
-
}
|
|
52
|
-
.meridian-profile-bar select {
|
|
53
|
-
background: var(--surface, #161b22); color: var(--text, #e6edf3);
|
|
54
|
-
border: 1px solid var(--border, #30363d); border-radius: 6px;
|
|
55
|
-
padding: 4px 24px 4px 10px; font-size: 12px; cursor: pointer;
|
|
56
|
-
appearance: none;
|
|
57
|
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%238b949e' stroke-width='1.5'/%3E%3C/svg%3E");
|
|
58
|
-
background-repeat: no-repeat; background-position: right 6px center;
|
|
59
|
-
}
|
|
60
|
-
.meridian-profile-bar select:hover { border-color: var(--accent, #58a6ff); }
|
|
61
|
-
.meridian-profile-bar select:focus { outline: none; border-color: var(--accent, #58a6ff); box-shadow: 0 0 0 1px var(--accent, #58a6ff); }
|
|
62
|
-
.meridian-profile-bar .profile-status {
|
|
63
|
-
font-size: 11px; color: var(--green, #3fb950); opacity: 0;
|
|
64
|
-
transition: opacity 0.3s;
|
|
65
|
-
}
|
|
66
|
-
.meridian-profile-bar .profile-status.show { opacity: 1; }
|
|
67
|
-
.meridian-profile-bar .profile-type {
|
|
68
|
-
font-size: 10px; padding: 2px 8px; border-radius: 4px;
|
|
69
|
-
background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);
|
|
70
|
-
}
|
|
71
|
-
.meridian-profile-bar .spacer { flex: 1; }
|
|
72
|
-
.meridian-profile-bar .profile-nav a {
|
|
73
|
-
color: var(--muted, #8b949e); text-decoration: none; font-size: 11px;
|
|
74
|
-
padding: 4px 8px; border-radius: 4px; transition: color 0.15s;
|
|
75
|
-
}
|
|
76
|
-
.meridian-profile-bar .profile-nav a:hover { color: var(--text, #e6edf3); }
|
|
77
|
-
.meridian-profile-bar .profile-nav a.active { color: var(--accent, #58a6ff); }
|
|
78
|
-
`, profileBarHtml = `
|
|
79
|
-
<div class="meridian-profile-bar" id="meridianProfileBar">
|
|
80
|
-
<span class="profile-label">Profile</span>
|
|
81
|
-
<select id="meridianProfileSelect"></select>
|
|
82
|
-
<span class="profile-type" id="meridianProfileType"></span>
|
|
83
|
-
<span class="profile-status" id="meridianProfileStatus">✓ Switched</span>
|
|
84
|
-
<div class="spacer"></div>
|
|
85
|
-
<div class="profile-nav">
|
|
86
|
-
<a href="/" id="nav-home">Home</a>
|
|
87
|
-
<a href="/settings" id="nav-settings">Settings</a>
|
|
88
|
-
<a href="/profiles" id="nav-profiles">Profiles</a>
|
|
89
|
-
<a href="/telemetry" id="nav-telemetry">Telemetry</a>
|
|
90
|
-
<a href="/plugins" id="nav-plugins">Plugins</a>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
`, profileBarJs = `
|
|
94
|
-
(function() {
|
|
95
|
-
var profileBar = document.getElementById('meridianProfileBar');
|
|
96
|
-
var profileSelect = document.getElementById('meridianProfileSelect');
|
|
97
|
-
var profileType = document.getElementById('meridianProfileType');
|
|
98
|
-
var profileStatus = document.getElementById('meridianProfileStatus');
|
|
99
|
-
var statusTimeout;
|
|
100
|
-
|
|
101
|
-
// Highlight active nav link
|
|
102
|
-
var path = location.pathname;
|
|
103
|
-
var navLinks = document.querySelectorAll('.profile-nav a');
|
|
104
|
-
navLinks.forEach(function(a) {
|
|
105
|
-
if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {
|
|
106
|
-
a.classList.add('active');
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
111
|
-
|
|
112
|
-
function loadProfiles() {
|
|
113
|
-
fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {
|
|
114
|
-
if (!data.profiles || data.profiles.length === 0) {
|
|
115
|
-
profileBar.classList.remove('visible');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
profileBar.classList.add('visible');
|
|
119
|
-
var current = data.profiles.find(function(p) { return p.isActive; });
|
|
120
|
-
profileSelect.innerHTML = data.profiles.map(function(p) {
|
|
121
|
-
return '<option value="' + esc(p.id) + '"' + (p.isActive ? ' selected' : '') + '>' + esc(p.id) + '</option>';
|
|
122
|
-
}).join('');
|
|
123
|
-
if (current) profileType.textContent = current.type;
|
|
124
|
-
}).catch(function() {});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
profileSelect.onchange = function() {
|
|
128
|
-
fetch('/profiles/active', {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: { 'Content-Type': 'application/json' },
|
|
131
|
-
body: JSON.stringify({ profile: profileSelect.value })
|
|
132
|
-
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
133
|
-
if (data.success) {
|
|
134
|
-
profileStatus.classList.add('show');
|
|
135
|
-
clearTimeout(statusTimeout);
|
|
136
|
-
statusTimeout = setTimeout(function() { profileStatus.classList.remove('show'); }, 2000);
|
|
137
|
-
loadProfiles();
|
|
138
|
-
}
|
|
139
|
-
}).catch(function() {});
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
loadProfiles();
|
|
143
|
-
setInterval(loadProfiles, 10000);
|
|
144
|
-
})();
|
|
145
|
-
`;
|
|
146
|
-
var init_profileBar = () => {};
|
|
147
|
-
|
|
148
|
-
export { themeCss, profileBarCss, profileBarHtml, profileBarJs, init_profileBar };
|