claude-memory-admin 1.7.0 → 1.8.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 CHANGED
@@ -307,13 +307,18 @@ writes a setting.
307
307
 
308
308
  ## Tools: what a session saved, next to what it cost
309
309
 
310
- Every other meter here counts what a session *costs*. [rtk](https://github.com/rtk-ai/rtk)
311
- counts the other half: it proxies commands like `grep`, `git` and `cargo test`,
312
- strips their output down before it reaches the model, and keeps a ledger of what
313
- it removed.
310
+ Environment grows a fourth segment, **Tools**, for each companion CLI it finds on
311
+ the PATH the server was started with. Nothing appears if none is installed.
314
312
 
315
- If `rtk` is on the PATH the server was started with, Environment grows a fourth
316
- segment, **Tools**, showing three things:
313
+ Each tool is one collapsible row carrying its own headline - *17.7% saved here*,
314
+ *$317 here, 20 sessions* - so the answer is readable without opening anything,
315
+ and which row you left open is remembered.
316
+
317
+ ### rtk - what never reached the model
318
+
319
+ [rtk](https://github.com/rtk-ai/rtk) proxies commands like `grep`, `git` and
320
+ `cargo test`, strips their output down before it reaches the model, and keeps a
321
+ ledger of what it removed. Its panel shows three things:
317
322
 
318
323
  - **This project** - what share of everything rtk read here never reached the
319
324
  model. rtk scopes its ledger by working directory, so this is exactly the
@@ -326,11 +331,38 @@ segment, **Tools**, showing three things:
326
331
  estimates count what the filter would have stripped, not what the model would
327
332
  have ignored.
328
333
 
329
- The segment is the one place this app runs a program that is not itself, so it
330
- stays off until you press **Read rtk stats**, and the answer is remembered. rtk
331
- is only ever handed a working directory the app resolved on its own, never one
332
- named in a request, and only its read-only `gain` and `discover` subcommands are
333
- called. If rtk is not installed, the segment does not appear at all.
334
+ ### ccusage - what it actually cost
335
+
336
+ [ccusage](https://github.com/ccusage/ccusage) prices the same session transcripts
337
+ this app lists under **Sessions**, which makes it the other half of rtk's
338
+ arithmetic: rtk reports what it stopped you paying for, ccusage reports the bill
339
+ that arrived anyway. Its panel shows:
340
+
341
+ - **This project** - what it cost, and what share of your whole Claude Code
342
+ spend that is. Sessions are attributed by the project slug ccusage reports,
343
+ which is the same slug this app names each store by.
344
+ - **Every project** - the machine-wide total, with the bar showing how much of
345
+ it fell in the last thirty days.
346
+ - **Where the money went** - spend per model, so an expensive habit is visible
347
+ as a model choice rather than as a number.
348
+ - **Most expensive sessions** - the costliest sessions in this project by id.
349
+ These are the same ids the Sessions segment lists, so clicking one opens that
350
+ transcript there, scrolled to and unfolded. A session Claude Code has already
351
+ swept says so rather than failing quietly.
352
+
353
+ It runs `ccusage claude session --json --offline`; `--offline` forces the cached
354
+ price table, so no network call is made.
355
+
356
+ ### What the segment will and will not do
357
+
358
+ This is the one place the app runs a program that is not itself, so it stays off
359
+ until you press **Read tool stats**, and the answer is remembered. Only
360
+ read-only subcommands are called, each tool is looked up by a fixed name in a
361
+ small registry rather than by anything a request names, and rtk is only ever
362
+ handed a working directory the app resolved on its own.
363
+
364
+ Savings figures come from the tools themselves and are their own estimates; the
365
+ cost figures come from the transcripts and are not.
334
366
 
335
367
  ## Everything it writes is reversible
336
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-admin",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Browse, audit and prune the auto memory Claude Code keeps under ~/.claude/projects",
5
5
  "keywords": [
6
6
  "claude",
@@ -60,8 +60,8 @@
60
60
  "prepublishOnly": "npm run build:css"
61
61
  },
62
62
  "dependencies": {
63
- "dompurify": "^3.2.7",
64
- "marked": "^16.4.0"
63
+ "dompurify": "^3.4.14",
64
+ "marked": "^18.0.10"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@tailwindcss/cli": "^4.3.3"
package/public/app.mjs CHANGED
@@ -46,7 +46,7 @@ function paintTheme() {
46
46
  document.documentElement.dataset.theme = state.theme;
47
47
  const dark = state.theme === 'dark';
48
48
  const button = el('theme-toggle');
49
- button.textContent = dark ? '☀' : '☾';
49
+ button.textContent = dark ? '☀️' : '🌙';
50
50
  button.title = dark ? 'Switch to the light theme ( t )' : 'Switch to the dark theme ( t )';
51
51
  button.setAttribute('aria-label', dark ? 'Switch to the light theme' : 'Switch to the dark theme');
52
52
  button.setAttribute('aria-pressed', String(dark));
@@ -130,6 +130,7 @@ async function init() {
130
130
 
131
131
  try {
132
132
  const data = await reloadStores();
133
+ if (data.version) el('source-link').textContent = `claude-memory-admin v${data.version}`;
133
134
  el('root-path').textContent = data.root;
134
135
  if (data.rootSource && data.rootSource !== 'default') {
135
136
  el('root-path').title = data.rootFile
package/public/dom.mjs CHANGED
@@ -6,6 +6,7 @@ export function node(tag, props = {}, children = []) {
6
6
  if (key === 'class') element.className = value;
7
7
  else if (key === 'text') element.textContent = value;
8
8
  else if (key.startsWith('on')) element.addEventListener(key.slice(2).toLowerCase(), value);
9
+ else if (value === false) element.removeAttribute(key);
9
10
  else if (value !== null && value !== undefined) element.setAttribute(key, value);
10
11
  }
11
12
  for (const child of [].concat(children)) {
package/public/index.html CHANGED
@@ -27,12 +27,13 @@
27
27
  <nav id="project-list" data-ui="sidebarList" aria-label="Projects"></nav>
28
28
  <footer data-ui="sidebarFoot">
29
29
  <div data-ui="sidebarFootRow">
30
- <a href="https://bitspire.ch/" target="_blank" rel="noopener noreferrer" data-ui="sidebarCredit">
31
- <span>Brought to you by</span>
32
- <img src="/assets/bitspire-logo.webp" alt="BitSpire" width="96" data-ui="sidebarLogo">
33
- </a>
34
- <button id="theme-toggle" data-ui="themeToggle" title="Switch to the dark theme ( t )" aria-label="Switch to the dark theme" aria-pressed="false">&#9790;</button>
30
+ <a id="source-link" href="https://github.com/linkdotnet/claude-memory-admin" target="_blank" rel="noopener noreferrer" data-ui="sidebarSource" title="Source and issue tracker on GitHub">claude-memory-admin</a>
31
+ <button id="theme-toggle" data-ui="themeToggle" title="Switch to the dark theme ( t )" aria-label="Switch to the dark theme" aria-pressed="false">🌙</button>
35
32
  </div>
33
+ <a href="https://bitspire.ch/" target="_blank" rel="noopener noreferrer" data-ui="sidebarCredit">
34
+ <span>Brought to you by</span>
35
+ <img src="/assets/bitspire-logo.webp" alt="BitSpire" width="64" data-ui="sidebarLogo">
36
+ </a>
36
37
  </footer>
37
38
  </aside>
38
39
 
package/public/state.mjs CHANGED
@@ -22,6 +22,7 @@ const storedListSort = () => {
22
22
  export const state = {
23
23
  stores: [],
24
24
  tools: [],
25
+ version: null,
25
26
  storeId: null,
26
27
  store: null,
27
28
  tab: 'memory',
@@ -40,7 +41,7 @@ export const state = {
40
41
  listSelection: new Set(),
41
42
  selecting: false,
42
43
  indexView: localStorage.getItem('memoryIndexView') === 'source' ? 'source' : 'rendered',
43
- aux: { instructions: null, settings: null, sessions: null, sessionDay: null, rtk: null },
44
+ aux: { instructions: null, settings: null, sessions: null, sessionDay: null, sessionFocus: null, tools: null },
44
45
  pathCheck: null,
45
46
  };
46
47
 
package/public/store.mjs CHANGED
@@ -10,7 +10,7 @@ export async function openStore(id, { keepTab = false } = {}) {
10
10
  state.selecting = false;
11
11
  }
12
12
  state.storeId = id;
13
- state.aux = { instructions: null, settings: null, sessions: null, sessionDay: null, rtk: null };
13
+ state.aux = { instructions: null, settings: null, sessions: null, sessionDay: null, sessionFocus: null, tools: null };
14
14
  state.pathCheck = null;
15
15
  if (!keepTab) {
16
16
  const opening = state.stores.find((store) => store.id === id);
@@ -65,6 +65,7 @@ export async function reloadStores() {
65
65
  const data = await api('/api/stores');
66
66
  state.stores = data.stores;
67
67
  state.tools = data.tools || [];
68
+ state.version = data.version || null;
68
69
  paint('stores');
69
70
  sweepIssues();
70
71
  return data;
package/public/styles.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--font-mono:ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--color-red-50:oklch(97.1% .013 17.4);--color-red-300:oklch(80.8% .114 19.6);--color-red-400:oklch(70.4% .16 22.2);--color-red-500:oklch(63.7% .208 25.3);--color-red-600:oklch(57.7% .215 27.3);--color-red-950:oklch(25.8% .092 26);--color-amber-50:oklch(97.7% .021 84.6);--color-amber-300:oklch(87.9% .153 91.6);--color-amber-400:oklch(82.8% .171 84.4);--color-amber-600:oklch(66.6% .15 58.3);--color-amber-700:oklch(55.5% .13 49);--color-amber-950:oklch(27.9% .077 45.6);--color-green-50:oklch(98.2% .018 155.8);--color-green-300:oklch(87.1% .12 154.4);--color-green-400:oklch(79.2% .15 151.7);--color-green-600:oklch(58.5% .145 149.2);--color-green-700:oklch(52.7% .132 150.1);--color-green-950:oklch(26.6% .065 152.9);--color-blue-50:oklch(97% .014 253.3);--color-blue-200:oklch(88% .06 253.3);--color-blue-300:oklch(78.4% .124 239.1);--color-blue-400:oklch(75.2% .138 243.7);--color-blue-500:oklch(65.8% .152 248.5);--color-blue-600:oklch(56.5% .163 253.3);--color-blue-700:oklch(48.5% .15 253.3);--color-blue-800:oklch(41% .125 253.3);--color-blue-900:oklch(35.5% .1 253.3);--color-blue-950:oklch(24% .07 253.3);--color-violet-50:oklch(96.9% .016 300);--color-violet-300:oklch(82.7% .104 300);--color-violet-700:oklch(49.6% .19 300);--color-violet-950:oklch(28.3% .109 300);--color-slate-50:oklch(98.4% .003 253.3);--color-slate-100:oklch(96.4% .005 253.3);--color-slate-200:oklch(92.6% .009 253.3);--color-slate-300:oklch(86.6% .014 253.3);--color-slate-400:oklch(70.4% .021 253.3);--color-slate-500:oklch(55.6% .026 253.3);--color-slate-600:oklch(44.8% .028 253.3);--color-slate-700:oklch(36.8% .028 253.3);--color-slate-800:oklch(26.4% .026 253.3);--color-slate-900:oklch(20.4% .024 253.3);--color-slate-950:oklch(14.2% .022 253.3);--spacing:.25rem;--container-2xl:42rem;--text-xs:.6875rem;--text-xs--line-height:1.45;--text-sm:.8125rem;--text-sm--line-height:1.5;--text-base:.875rem;--text-base--line-height:1.6;--text-lg:1rem;--text-lg--line-height:1.5;--text-xl:1.25rem;--text-xl--line-height:1.3;--text-2xl:1.5rem;--text-2xl--line-height:1.2;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.12em;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.12s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-slate-0:oklch(100% 0 253.3);--spacing-control-x-sm:calc(var(--spacing) * 2);--spacing-control-y-sm:calc(var(--spacing) * 1);--spacing-control-x-md:calc(var(--spacing) * 3);--spacing-control-y-md:calc(var(--spacing) * 2);--spacing-control-x-lg:calc(var(--spacing) * 3.5);--spacing-control-y-lg:calc(var(--spacing) * 2.5);--spacing-control-h-sm:calc(var(--spacing) * 6.5);--spacing-control-h-md:calc(var(--spacing) * 9.5);--spacing-control-h-lg:calc(var(--spacing) * 9.5);--text-2xs:.625rem;--text-2xs--line-height:1.4;--tracking-tightest:-.03em}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{color-scheme:light}:root[data-theme=dark]{color-scheme:dark}html,body{height:100%}body{background-color:var(--ui-canvas);color:var(--ui-fg);font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--text-base--line-height)}::selection{background-color:var(--ui-accent-surface);color:var(--ui-accent)}:focus-visible{outline:2px solid var(--ui-accent);outline-offset:1px}}@layer components{.prose{overflow-wrap:anywhere}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose h1,.prose h2,.prose h3,.prose h4{margin:calc(var(--spacing) * 6) 0 calc(var(--spacing) * 2);letter-spacing:var(--tracking-tightest);font-weight:650;line-height:1.25}.prose h1{font-size:var(--text-lg)}.prose h2{font-size:var(--text-base);padding-bottom:calc(var(--spacing) * 1.5);border-bottom:1px solid var(--ui-line)}.prose h3,.prose h4{font-size:var(--text-base);color:var(--ui-fg-muted)}.prose p{margin:calc(var(--spacing) * 3) 0}.prose ul,.prose ol{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 5)}.prose ul{list-style-type:"– "}.prose ol{list-style-type:decimal}.prose li{margin:calc(var(--spacing) * 1) 0}.prose li::marker{color:var(--ui-accent);font-family:var(--font-mono)}.prose li>ul,.prose li>ol{margin:calc(var(--spacing) * 1) 0}.prose a{color:var(--ui-accent);text-underline-offset:2px;text-decoration:underline}.prose a:hover{color:var(--ui-accent-hover)}.prose strong{color:var(--ui-fg);font-weight:650}.prose pre{margin:calc(var(--spacing) * 3) 0;padding:calc(var(--spacing) * 3);border:1px solid var(--ui-line);border-radius:var(--radius-md);background-color:var(--ui-surface-sunken);font-size:var(--text-xs);line-height:1.6;overflow-x:auto}.prose code{font-family:var(--font-mono);font-size:.9em}.prose :not(pre)>code{border:1px solid var(--ui-line);border-radius:var(--radius-sm);background-color:var(--ui-surface-sunken);color:var(--ui-accent);padding:1px 4px}.prose blockquote{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 3);border-left:2px solid var(--ui-accent-line);color:var(--ui-fg-muted)}.prose hr{margin:calc(var(--spacing) * 5) 0;border:0;border-top:1px dashed var(--ui-line-strong)}.prose table{margin:calc(var(--spacing) * 3) 0;border-collapse:collapse;font-size:var(--text-sm);display:block;overflow-x:auto}.prose th,.prose td{padding:calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);border:1px solid var(--ui-line);text-align:left}.prose th{background-color:var(--ui-surface-sunken);font-weight:600}.graph svg{cursor:grab;width:100%;display:block}.graph .node circle{cursor:pointer;stroke-width:1.5px}.graph .node circle.hit{fill:#0000;stroke:none}.graph .node text{fill:var(--ui-fg-muted);font-family:var(--font-mono);pointer-events:none;font-size:9px}.graph .node.hi circle{stroke:var(--ui-accent);stroke-width:2.5px}.graph .node.hi text{fill:var(--ui-fg);font-weight:600}.graph .node.dim{opacity:.18}.graph .node.dim text{display:none}.graph .edge{stroke:var(--ui-line-strong)}.graph .edge.dim{opacity:.05}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-y-0{inset-block:0}.top-0{top:0}.top-2{top:calc(var(--spacing) * 2)}.top-3{top:calc(var(--spacing) * 3)}.top-3\.5{top:calc(var(--spacing) * 3.5)}.right-3\.5{right:calc(var(--spacing) * 3.5)}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:0}.left-1\/2{left:50%}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.z-20{z-index:20}.z-200{z-index:200}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.m-auto{margin:auto}.my-1{margin-block:var(--spacing)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-9{height:calc(var(--spacing) * 9)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[28rem\]{max-height:28rem}.max-h-\[82vh\]{max-height:82vh}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-control-h-lg{min-height:var(--spacing-control-h-lg)}.min-h-control-h-md{min-height:var(--spacing-control-h-md)}.min-h-control-h-sm{min-height:var(--spacing-control-h-sm)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-3{width:calc(var(--spacing) * 3)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-\[calc\(100\%-3rem\)\]{width:calc(100% - 3rem)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.min-w-0{min-width:0}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-control-h-sm{min-width:var(--spacing-control-h-sm)}.flex-1{flex:1}.flex-none{flex:none}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-flow-col{grid-auto-flow:column}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[3rem_1fr\]{grid-template-columns:3rem 1fr}.grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.grid-rows-7{grid-template-rows:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-px{gap:1px}.gap-x-3\.5{column-gap:calc(var(--spacing) * 3.5)}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-xs{border-radius:var(--radius-xs)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-accent{border-color:var(--ui-accent)}.border-accent-line{border-color:var(--ui-accent-line)}.border-current{border-color:currentColor}.border-danger{border-color:var(--ui-danger)}.border-danger-line{border-color:var(--ui-danger-line)}.border-line{border-color:var(--ui-line)}.border-transparent{border-color:#0000}.border-l-danger{border-left-color:var(--ui-danger)}.border-l-warn{border-left-color:var(--ui-warn)}.bg-accent{background-color:var(--ui-accent)}.bg-accent-surface{background-color:var(--ui-accent-surface)}.bg-canvas{background-color:var(--ui-canvas)}.bg-danger{background-color:var(--ui-danger)}.bg-danger-surface{background-color:var(--ui-danger-surface)}.bg-fg{background-color:var(--ui-fg)}.bg-heat-0{background-color:var(--ui-heat-0)}.bg-heat-1{background-color:var(--ui-heat-1)}.bg-heat-2{background-color:var(--ui-heat-2)}.bg-heat-3{background-color:var(--ui-heat-3)}.bg-heat-4{background-color:var(--ui-heat-4)}.bg-line-strong{background-color:var(--ui-line-strong)}.bg-ok{background-color:var(--ui-ok)}.bg-ok-surface{background-color:var(--ui-ok-surface)}.bg-scope-local-surface{background-color:var(--ui-scope-local-surface)}.bg-scope-managed-surface{background-color:var(--ui-scope-managed-surface)}.bg-scope-project-surface{background-color:var(--ui-scope-project-surface)}.bg-scope-user-surface{background-color:var(--ui-scope-user-surface)}.bg-surface{background-color:var(--ui-surface)}.bg-surface-sunken{background-color:var(--ui-surface-sunken)}.bg-transparent{background-color:#0000}.bg-type-feedback-surface{background-color:var(--ui-type-feedback-surface)}.bg-type-project-surface{background-color:var(--ui-type-project-surface)}.bg-type-reference-surface{background-color:var(--ui-type-reference-surface)}.bg-type-user-surface{background-color:var(--ui-type-user-surface)}.bg-warn{background-color:var(--ui-warn)}.bg-warn-surface{background-color:var(--ui-warn-surface)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-control-x-lg{padding-inline:var(--spacing-control-x-lg)}.px-control-x-md{padding-inline:var(--spacing-control-x-md)}.px-control-x-sm{padding-inline:var(--spacing-control-x-sm)}.px-px{padding-inline:1px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-\[15vh\]{padding-block:15vh}.py-control-y-lg{padding-block:var(--spacing-control-y-lg)}.py-control-y-md{padding-block:var(--spacing-control-y-md)}.py-control-y-sm{padding-block:var(--spacing-control-y-sm)}.py-px{padding-block:1px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-3\.5{padding-bottom:calc(var(--spacing) * 3.5)}.pb-4\.5{padding-bottom:calc(var(--spacing) * 4.5)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-2xs{font-size:var(--text-2xs);line-height:var(--tw-leading,var(--text-2xs--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-3{--tw-leading:calc(var(--spacing) * 3);line-height:calc(var(--spacing) * 3)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tightest{--tw-tracking:var(--tracking-tightest);letter-spacing:var(--tracking-tightest)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--ui-accent)}.text-canvas{color:var(--ui-canvas)}.text-danger{color:var(--ui-danger)}.text-fg{color:var(--ui-fg)}.text-fg-muted{color:var(--ui-fg-muted)}.text-fg-subtle{color:var(--ui-fg-subtle)}.text-ok{color:var(--ui-ok)}.text-on-accent{color:var(--ui-on-accent)}.text-scope-local{color:var(--ui-scope-local)}.text-scope-managed{color:var(--ui-scope-managed)}.text-scope-project{color:var(--ui-scope-project)}.text-scope-user{color:var(--ui-scope-user)}.text-type-feedback{color:var(--ui-type-feedback)}.text-type-project{color:var(--ui-type-project)}.text-type-reference{color:var(--ui-type-reference)}.text-type-user{color:var(--ui-type-user)}.text-warn{color:var(--ui-warn)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-danger{-webkit-text-decoration-color:var(--ui-danger);-webkit-text-decoration-color:var(--ui-danger);text-decoration-color:var(--ui-danger)}.decoration-line-strong{-webkit-text-decoration-color:var(--ui-line-strong);-webkit-text-decoration-color:var(--ui-line-strong);text-decoration-color:var(--ui-line-strong)}.accent-accent{accent-color:var(--ui-accent)}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-card{--tw-shadow:var(--ui-shadow-card);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--ui-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-offset-1{outline-offset:1px}.outline-fg{outline-color:var(--ui-fg)}.outline-transparent{outline-color:#0000}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[outline-color\]{transition-property:outline-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-fg-subtle::placeholder{color:var(--ui-fg-subtle)}.backdrop\:bg-scrim::backdrop{background-color:var(--ui-scrim)}.before\:flex-1:before{content:var(--tw-content);flex:1}.before\:border-t:before{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.before\:border-dashed:before{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.before\:border-danger:before{content:var(--tw-content);border-color:var(--ui-danger)}.after\:flex-1:after{content:var(--tw-content);flex:1}.after\:border-t:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.after\:border-dashed:after{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.after\:border-danger:after{content:var(--tw-content);border-color:var(--ui-danger)}.first\:mt-0:first-child{margin-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.open\:flex:is([open],:popover-open,:open){display:flex}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--ui-accent)}.hover\:border-accent-hover:hover{border-color:var(--ui-accent-hover)}.hover\:border-accent-line:hover{border-color:var(--ui-accent-line)}.hover\:border-line-strong:hover{border-color:var(--ui-line-strong)}.hover\:bg-accent-hover:hover{background-color:var(--ui-accent-hover)}.hover\:bg-danger:hover{background-color:var(--ui-danger)}.hover\:bg-surface-sunken:hover{background-color:var(--ui-surface-sunken)}.hover\:text-accent-hover:hover{color:var(--ui-accent-hover)}.hover\:text-fg:hover{color:var(--ui-fg)}.hover\:text-on-accent:hover{color:var(--ui-on-accent)}.hover\:underline:hover{text-decoration-line:underline}.hover\:outline-fg-subtle:hover{outline-color:var(--ui-fg-subtle)}}.focus\:border-accent:focus{border-color:var(--ui-accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-45:disabled{opacity:.45}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:gap-14{gap:calc(var(--spacing) * 14)}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-4{top:calc(var(--spacing) * 4)}.xl\:max-h-\[calc\(100vh-9rem\)\]{max-height:calc(100vh - 9rem)}.xl\:grid-cols-\[minmax\(280px\,380px\)_1fr\]{grid-template-columns:minmax(280px,380px) 1fr}.xl\:overflow-y-auto{overflow-y:auto}}.dark\:invert:where([data-theme=dark],[data-theme=dark] *){--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.\[\&\>span\]\:block>span{display:block}}:root{--ui-canvas:var(--color-slate-50);--ui-surface:var(--color-slate-0);--ui-surface-sunken:var(--color-slate-100);--ui-surface-raised:var(--color-slate-0);--ui-line:var(--color-slate-200);--ui-line-strong:var(--color-slate-300);--ui-fg:var(--color-slate-900);--ui-fg-muted:var(--color-slate-500);--ui-fg-subtle:var(--color-slate-400);--ui-accent:var(--color-blue-600);--ui-accent-hover:var(--color-blue-700);--ui-accent-surface:var(--color-blue-50);--ui-accent-line:var(--color-blue-200);--ui-on-accent:var(--color-slate-0);--ui-danger:var(--color-red-600);--ui-danger-surface:var(--color-red-50);--ui-danger-line:var(--color-red-300);--ui-warn:var(--color-amber-700);--ui-warn-surface:var(--color-amber-50);--ui-warn-line:var(--color-amber-300);--ui-ok:var(--color-green-600);--ui-ok-surface:var(--color-green-50);--ui-ok-line:var(--color-green-300);--ui-heat-0:var(--color-slate-200);--ui-heat-1:var(--color-blue-200);--ui-heat-2:var(--color-blue-400);--ui-heat-3:var(--color-blue-600);--ui-heat-4:var(--color-blue-800);--ui-type-project:var(--color-blue-700);--ui-type-project-surface:var(--color-blue-50);--ui-type-feedback:var(--color-amber-700);--ui-type-feedback-surface:var(--color-amber-50);--ui-type-user:var(--color-violet-700);--ui-type-user-surface:var(--color-violet-50);--ui-type-reference:var(--color-green-700);--ui-type-reference-surface:var(--color-green-50);--ui-scope-managed:var(--color-red-600);--ui-scope-managed-surface:var(--color-red-50);--ui-scope-user:var(--color-blue-700);--ui-scope-user-surface:var(--color-blue-50);--ui-scope-project:var(--color-amber-700);--ui-scope-project-surface:var(--color-amber-50);--ui-scope-local:var(--color-slate-600);--ui-scope-local-surface:var(--color-slate-100);--ui-shadow-card:0 1px 2px #0000000d;--ui-shadow-pop:0 1px 2px #0000000f, 0 12px 32px #0000001a;--ui-scrim:#0f172a73}:root[data-theme=dark]{--ui-canvas:var(--color-slate-950);--ui-surface:var(--color-slate-900);--ui-surface-sunken:var(--color-slate-950);--ui-surface-raised:var(--color-slate-800);--ui-line:var(--color-slate-800);--ui-line-strong:var(--color-slate-700);--ui-fg:var(--color-slate-100);--ui-fg-muted:var(--color-slate-400);--ui-fg-subtle:var(--color-slate-500);--ui-accent:var(--color-blue-400);--ui-accent-hover:var(--color-blue-300);--ui-accent-surface:var(--color-blue-950);--ui-accent-line:var(--color-blue-800);--ui-on-accent:var(--color-slate-950);--ui-danger:var(--color-red-400);--ui-danger-surface:var(--color-red-950);--ui-danger-line:var(--color-red-500);--ui-warn:var(--color-amber-400);--ui-warn-surface:var(--color-amber-950);--ui-warn-line:var(--color-amber-600);--ui-ok:var(--color-green-400);--ui-ok-surface:var(--color-green-950);--ui-ok-line:var(--color-green-600);--ui-heat-0:var(--color-slate-800);--ui-heat-1:var(--color-blue-900);--ui-heat-2:var(--color-blue-700);--ui-heat-3:var(--color-blue-500);--ui-heat-4:var(--color-blue-300);--ui-type-project:var(--color-blue-300);--ui-type-project-surface:var(--color-blue-950);--ui-type-feedback:var(--color-amber-300);--ui-type-feedback-surface:var(--color-amber-950);--ui-type-user:var(--color-violet-300);--ui-type-user-surface:var(--color-violet-950);--ui-type-reference:var(--color-green-300);--ui-type-reference-surface:var(--color-green-950);--ui-scope-managed:var(--color-red-400);--ui-scope-managed-surface:var(--color-red-950);--ui-scope-user:var(--color-blue-300);--ui-scope-user-surface:var(--color-blue-950);--ui-scope-project:var(--color-amber-300);--ui-scope-project-surface:var(--color-amber-950);--ui-scope-local:var(--color-slate-400);--ui-scope-local-surface:var(--color-slate-800);--ui-shadow-card:0 1px 2px #0006;--ui-shadow-pop:0 1px 2px #00000080, 0 12px 32px #00000073;--ui-scrim:#020610b3}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--font-mono:ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--color-red-50:oklch(97.1% .013 17.4);--color-red-300:oklch(80.8% .114 19.6);--color-red-400:oklch(70.4% .16 22.2);--color-red-500:oklch(63.7% .208 25.3);--color-red-600:oklch(57.7% .215 27.3);--color-red-950:oklch(25.8% .092 26);--color-amber-50:oklch(97.7% .021 84.6);--color-amber-300:oklch(87.9% .153 91.6);--color-amber-400:oklch(82.8% .171 84.4);--color-amber-600:oklch(66.6% .15 58.3);--color-amber-700:oklch(55.5% .13 49);--color-amber-950:oklch(27.9% .077 45.6);--color-green-50:oklch(98.2% .018 155.8);--color-green-300:oklch(87.1% .12 154.4);--color-green-400:oklch(79.2% .15 151.7);--color-green-600:oklch(58.5% .145 149.2);--color-green-700:oklch(52.7% .132 150.1);--color-green-950:oklch(26.6% .065 152.9);--color-blue-50:oklch(97% .014 253.3);--color-blue-200:oklch(88% .06 253.3);--color-blue-300:oklch(78.4% .124 239.1);--color-blue-400:oklch(75.2% .138 243.7);--color-blue-500:oklch(65.8% .152 248.5);--color-blue-600:oklch(56.5% .163 253.3);--color-blue-700:oklch(48.5% .15 253.3);--color-blue-800:oklch(41% .125 253.3);--color-blue-900:oklch(35.5% .1 253.3);--color-blue-950:oklch(24% .07 253.3);--color-violet-50:oklch(96.9% .016 300);--color-violet-300:oklch(82.7% .104 300);--color-violet-700:oklch(49.6% .19 300);--color-violet-950:oklch(28.3% .109 300);--color-slate-50:oklch(98.4% .003 253.3);--color-slate-100:oklch(96.4% .005 253.3);--color-slate-200:oklch(92.6% .009 253.3);--color-slate-300:oklch(86.6% .014 253.3);--color-slate-400:oklch(70.4% .021 253.3);--color-slate-500:oklch(55.6% .026 253.3);--color-slate-600:oklch(44.8% .028 253.3);--color-slate-700:oklch(36.8% .028 253.3);--color-slate-800:oklch(26.4% .026 253.3);--color-slate-900:oklch(20.4% .024 253.3);--color-slate-950:oklch(14.2% .022 253.3);--spacing:.25rem;--container-2xl:42rem;--text-xs:.6875rem;--text-xs--line-height:1.45;--text-sm:.8125rem;--text-sm--line-height:1.5;--text-base:.875rem;--text-base--line-height:1.6;--text-lg:1rem;--text-lg--line-height:1.5;--text-xl:1.25rem;--text-xl--line-height:1.3;--text-2xl:1.5rem;--text-2xl--line-height:1.2;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.12em;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.12s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-slate-0:oklch(100% 0 253.3);--spacing-control-x-sm:calc(var(--spacing) * 2);--spacing-control-y-sm:calc(var(--spacing) * 1);--spacing-control-x-md:calc(var(--spacing) * 3);--spacing-control-y-md:calc(var(--spacing) * 2);--spacing-control-x-lg:calc(var(--spacing) * 3.5);--spacing-control-y-lg:calc(var(--spacing) * 2.5);--spacing-control-h-sm:calc(var(--spacing) * 6.5);--spacing-control-h-md:calc(var(--spacing) * 9.5);--spacing-control-h-lg:calc(var(--spacing) * 9.5);--text-2xs:.625rem;--text-2xs--line-height:1.4;--tracking-tightest:-.03em}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{color-scheme:light}:root[data-theme=dark]{color-scheme:dark}html,body{height:100%}body{background-color:var(--ui-canvas);color:var(--ui-fg);font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--text-base--line-height)}::selection{background-color:var(--ui-accent-surface);color:var(--ui-accent)}:focus-visible{outline:2px solid var(--ui-accent);outline-offset:1px}}@layer components{.prose{overflow-wrap:anywhere}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose h1,.prose h2,.prose h3,.prose h4{margin:calc(var(--spacing) * 6) 0 calc(var(--spacing) * 2);letter-spacing:var(--tracking-tightest);font-weight:650;line-height:1.25}.prose h1{font-size:var(--text-lg)}.prose h2{font-size:var(--text-base);padding-bottom:calc(var(--spacing) * 1.5);border-bottom:1px solid var(--ui-line)}.prose h3,.prose h4{font-size:var(--text-base);color:var(--ui-fg-muted)}.prose p{margin:calc(var(--spacing) * 3) 0}.prose ul,.prose ol{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 5)}.prose ul{list-style-type:"– "}.prose ol{list-style-type:decimal}.prose li{margin:calc(var(--spacing) * 1) 0}.prose li::marker{color:var(--ui-accent);font-family:var(--font-mono)}.prose li>ul,.prose li>ol{margin:calc(var(--spacing) * 1) 0}.prose a{color:var(--ui-accent);text-underline-offset:2px;text-decoration:underline}.prose a:hover{color:var(--ui-accent-hover)}.prose strong{color:var(--ui-fg);font-weight:650}.prose pre{margin:calc(var(--spacing) * 3) 0;padding:calc(var(--spacing) * 3);border:1px solid var(--ui-line);border-radius:var(--radius-md);background-color:var(--ui-surface-sunken);font-size:var(--text-xs);line-height:1.6;overflow-x:auto}.prose code{font-family:var(--font-mono);font-size:.9em}.prose :not(pre)>code{border:1px solid var(--ui-line);border-radius:var(--radius-sm);background-color:var(--ui-surface-sunken);color:var(--ui-accent);padding:1px 4px}.prose blockquote{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 3);border-left:2px solid var(--ui-accent-line);color:var(--ui-fg-muted)}.prose hr{margin:calc(var(--spacing) * 5) 0;border:0;border-top:1px dashed var(--ui-line-strong)}.prose table{margin:calc(var(--spacing) * 3) 0;border-collapse:collapse;font-size:var(--text-sm);display:block;overflow-x:auto}.prose th,.prose td{padding:calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);border:1px solid var(--ui-line);text-align:left}.prose th{background-color:var(--ui-surface-sunken);font-weight:600}.graph svg{cursor:grab;width:100%;display:block}.graph .node circle{cursor:pointer;stroke-width:1.5px}.graph .node circle.hit{fill:#0000;stroke:none}.graph .node text{fill:var(--ui-fg-muted);font-family:var(--font-mono);pointer-events:none;font-size:9px}.graph .node.hi circle{stroke:var(--ui-accent);stroke-width:2.5px}.graph .node.hi text{fill:var(--ui-fg);font-weight:600}.graph .node.dim{opacity:.18}.graph .node.dim text{display:none}.graph .edge{stroke:var(--ui-line-strong)}.graph .edge.dim{opacity:.05}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-y-0{inset-block:0}.top-0{top:0}.top-2{top:calc(var(--spacing) * 2)}.top-3{top:calc(var(--spacing) * 3)}.top-3\.5{top:calc(var(--spacing) * 3.5)}.right-3\.5{right:calc(var(--spacing) * 3.5)}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:0}.left-1\/2{left:50%}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.z-20{z-index:20}.z-200{z-index:200}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.m-auto{margin:auto}.my-1{margin-block:var(--spacing)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-9{height:calc(var(--spacing) * 9)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[28rem\]{max-height:28rem}.max-h-\[82vh\]{max-height:82vh}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-control-h-lg{min-height:var(--spacing-control-h-lg)}.min-h-control-h-md{min-height:var(--spacing-control-h-md)}.min-h-control-h-sm{min-height:var(--spacing-control-h-sm)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-3{width:calc(var(--spacing) * 3)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-3rem\)\]{width:calc(100% - 3rem)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.min-w-0{min-width:0}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-control-h-sm{min-width:var(--spacing-control-h-sm)}.flex-1{flex:1}.flex-none{flex:none}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-flow-col{grid-auto-flow:column}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[3rem_1fr\]{grid-template-columns:3rem 1fr}.grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.grid-rows-7{grid-template-rows:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-px{gap:1px}.gap-x-3\.5{column-gap:calc(var(--spacing) * 3.5)}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-xs{border-radius:var(--radius-xs)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-accent{border-color:var(--ui-accent)}.border-accent-line{border-color:var(--ui-accent-line)}.border-current{border-color:currentColor}.border-danger{border-color:var(--ui-danger)}.border-danger-line{border-color:var(--ui-danger-line)}.border-line{border-color:var(--ui-line)}.border-transparent{border-color:#0000}.border-l-danger{border-left-color:var(--ui-danger)}.border-l-warn{border-left-color:var(--ui-warn)}.bg-accent{background-color:var(--ui-accent)}.bg-accent-surface{background-color:var(--ui-accent-surface)}.bg-canvas{background-color:var(--ui-canvas)}.bg-danger{background-color:var(--ui-danger)}.bg-danger-surface{background-color:var(--ui-danger-surface)}.bg-fg{background-color:var(--ui-fg)}.bg-heat-0{background-color:var(--ui-heat-0)}.bg-heat-1{background-color:var(--ui-heat-1)}.bg-heat-2{background-color:var(--ui-heat-2)}.bg-heat-3{background-color:var(--ui-heat-3)}.bg-heat-4{background-color:var(--ui-heat-4)}.bg-line-strong{background-color:var(--ui-line-strong)}.bg-ok{background-color:var(--ui-ok)}.bg-ok-surface{background-color:var(--ui-ok-surface)}.bg-scope-local-surface{background-color:var(--ui-scope-local-surface)}.bg-scope-managed-surface{background-color:var(--ui-scope-managed-surface)}.bg-scope-project-surface{background-color:var(--ui-scope-project-surface)}.bg-scope-user-surface{background-color:var(--ui-scope-user-surface)}.bg-surface{background-color:var(--ui-surface)}.bg-surface-sunken{background-color:var(--ui-surface-sunken)}.bg-transparent{background-color:#0000}.bg-type-feedback-surface{background-color:var(--ui-type-feedback-surface)}.bg-type-project-surface{background-color:var(--ui-type-project-surface)}.bg-type-reference-surface{background-color:var(--ui-type-reference-surface)}.bg-type-user-surface{background-color:var(--ui-type-user-surface)}.bg-warn{background-color:var(--ui-warn)}.bg-warn-surface{background-color:var(--ui-warn-surface)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-control-x-lg{padding-inline:var(--spacing-control-x-lg)}.px-control-x-md{padding-inline:var(--spacing-control-x-md)}.px-control-x-sm{padding-inline:var(--spacing-control-x-sm)}.px-px{padding-inline:1px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-\[15vh\]{padding-block:15vh}.py-control-y-lg{padding-block:var(--spacing-control-y-lg)}.py-control-y-md{padding-block:var(--spacing-control-y-md)}.py-control-y-sm{padding-block:var(--spacing-control-y-sm)}.py-px{padding-block:1px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-3\.5{padding-bottom:calc(var(--spacing) * 3.5)}.pb-4\.5{padding-bottom:calc(var(--spacing) * 4.5)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-2xs{font-size:var(--text-2xs);line-height:var(--tw-leading,var(--text-2xs--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-3{--tw-leading:calc(var(--spacing) * 3);line-height:calc(var(--spacing) * 3)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tightest{--tw-tracking:var(--tracking-tightest);letter-spacing:var(--tracking-tightest)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--ui-accent)}.text-canvas{color:var(--ui-canvas)}.text-danger{color:var(--ui-danger)}.text-fg{color:var(--ui-fg)}.text-fg-muted{color:var(--ui-fg-muted)}.text-fg-subtle{color:var(--ui-fg-subtle)}.text-ok{color:var(--ui-ok)}.text-on-accent{color:var(--ui-on-accent)}.text-scope-local{color:var(--ui-scope-local)}.text-scope-managed{color:var(--ui-scope-managed)}.text-scope-project{color:var(--ui-scope-project)}.text-scope-user{color:var(--ui-scope-user)}.text-type-feedback{color:var(--ui-type-feedback)}.text-type-project{color:var(--ui-type-project)}.text-type-reference{color:var(--ui-type-reference)}.text-type-user{color:var(--ui-type-user)}.text-warn{color:var(--ui-warn)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-danger{-webkit-text-decoration-color:var(--ui-danger);-webkit-text-decoration-color:var(--ui-danger);text-decoration-color:var(--ui-danger)}.decoration-line-strong{-webkit-text-decoration-color:var(--ui-line-strong);-webkit-text-decoration-color:var(--ui-line-strong);text-decoration-color:var(--ui-line-strong)}.accent-accent{accent-color:var(--ui-accent)}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-card{--tw-shadow:var(--ui-shadow-card);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--ui-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-offset-1{outline-offset:1px}.outline-fg{outline-color:var(--ui-fg)}.outline-transparent{outline-color:#0000}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[outline-color\]{transition-property:outline-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-fg-subtle::placeholder{color:var(--ui-fg-subtle)}.backdrop\:bg-scrim::backdrop{background-color:var(--ui-scrim)}.before\:flex-1:before{content:var(--tw-content);flex:1}.before\:border-t:before{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.before\:border-dashed:before{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.before\:border-danger:before{content:var(--tw-content);border-color:var(--ui-danger)}.after\:flex-1:after{content:var(--tw-content);flex:1}.after\:border-t:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.after\:border-dashed:after{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.after\:border-danger:after{content:var(--tw-content);border-color:var(--ui-danger)}.first\:mt-0:first-child{margin-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.open\:flex:is([open],:popover-open,:open){display:flex}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--ui-accent)}.hover\:border-accent-hover:hover{border-color:var(--ui-accent-hover)}.hover\:border-accent-line:hover{border-color:var(--ui-accent-line)}.hover\:border-line-strong:hover{border-color:var(--ui-line-strong)}.hover\:bg-accent-hover:hover{background-color:var(--ui-accent-hover)}.hover\:bg-danger:hover{background-color:var(--ui-danger)}.hover\:bg-surface-sunken:hover{background-color:var(--ui-surface-sunken)}.hover\:text-accent-hover:hover{color:var(--ui-accent-hover)}.hover\:text-fg:hover{color:var(--ui-fg)}.hover\:text-on-accent:hover{color:var(--ui-on-accent)}.hover\:underline:hover{text-decoration-line:underline}.hover\:outline-fg-subtle:hover{outline-color:var(--ui-fg-subtle)}}.focus\:border-accent:focus{border-color:var(--ui-accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-45:disabled{opacity:.45}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:gap-14{gap:calc(var(--spacing) * 14)}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-4{top:calc(var(--spacing) * 4)}.xl\:max-h-\[calc\(100vh-9rem\)\]{max-height:calc(100vh - 9rem)}.xl\:grid-cols-\[minmax\(280px\,380px\)_1fr\]{grid-template-columns:minmax(280px,380px) 1fr}.xl\:overflow-y-auto{overflow-y:auto}}.dark\:invert:where([data-theme=dark],[data-theme=dark] *){--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.\[\&\>span\]\:block>span{display:block}}:root{--ui-canvas:var(--color-slate-50);--ui-surface:var(--color-slate-0);--ui-surface-sunken:var(--color-slate-100);--ui-surface-raised:var(--color-slate-0);--ui-line:var(--color-slate-200);--ui-line-strong:var(--color-slate-300);--ui-fg:var(--color-slate-900);--ui-fg-muted:var(--color-slate-500);--ui-fg-subtle:var(--color-slate-400);--ui-accent:var(--color-blue-600);--ui-accent-hover:var(--color-blue-700);--ui-accent-surface:var(--color-blue-50);--ui-accent-line:var(--color-blue-200);--ui-on-accent:var(--color-slate-0);--ui-danger:var(--color-red-600);--ui-danger-surface:var(--color-red-50);--ui-danger-line:var(--color-red-300);--ui-warn:var(--color-amber-700);--ui-warn-surface:var(--color-amber-50);--ui-warn-line:var(--color-amber-300);--ui-ok:var(--color-green-600);--ui-ok-surface:var(--color-green-50);--ui-ok-line:var(--color-green-300);--ui-heat-0:var(--color-slate-200);--ui-heat-1:var(--color-blue-200);--ui-heat-2:var(--color-blue-400);--ui-heat-3:var(--color-blue-600);--ui-heat-4:var(--color-blue-800);--ui-type-project:var(--color-blue-700);--ui-type-project-surface:var(--color-blue-50);--ui-type-feedback:var(--color-amber-700);--ui-type-feedback-surface:var(--color-amber-50);--ui-type-user:var(--color-violet-700);--ui-type-user-surface:var(--color-violet-50);--ui-type-reference:var(--color-green-700);--ui-type-reference-surface:var(--color-green-50);--ui-scope-managed:var(--color-red-600);--ui-scope-managed-surface:var(--color-red-50);--ui-scope-user:var(--color-blue-700);--ui-scope-user-surface:var(--color-blue-50);--ui-scope-project:var(--color-amber-700);--ui-scope-project-surface:var(--color-amber-50);--ui-scope-local:var(--color-slate-600);--ui-scope-local-surface:var(--color-slate-100);--ui-shadow-card:0 1px 2px #0000000d;--ui-shadow-pop:0 1px 2px #0000000f, 0 12px 32px #0000001a;--ui-scrim:#0f172a73}:root[data-theme=dark]{--ui-canvas:var(--color-slate-950);--ui-surface:var(--color-slate-900);--ui-surface-sunken:var(--color-slate-950);--ui-surface-raised:var(--color-slate-800);--ui-line:var(--color-slate-800);--ui-line-strong:var(--color-slate-700);--ui-fg:var(--color-slate-100);--ui-fg-muted:var(--color-slate-400);--ui-fg-subtle:var(--color-slate-500);--ui-accent:var(--color-blue-400);--ui-accent-hover:var(--color-blue-300);--ui-accent-surface:var(--color-blue-950);--ui-accent-line:var(--color-blue-800);--ui-on-accent:var(--color-slate-950);--ui-danger:var(--color-red-400);--ui-danger-surface:var(--color-red-950);--ui-danger-line:var(--color-red-500);--ui-warn:var(--color-amber-400);--ui-warn-surface:var(--color-amber-950);--ui-warn-line:var(--color-amber-600);--ui-ok:var(--color-green-400);--ui-ok-surface:var(--color-green-950);--ui-ok-line:var(--color-green-600);--ui-heat-0:var(--color-slate-800);--ui-heat-1:var(--color-blue-900);--ui-heat-2:var(--color-blue-700);--ui-heat-3:var(--color-blue-500);--ui-heat-4:var(--color-blue-300);--ui-type-project:var(--color-blue-300);--ui-type-project-surface:var(--color-blue-950);--ui-type-feedback:var(--color-amber-300);--ui-type-feedback-surface:var(--color-amber-950);--ui-type-user:var(--color-violet-300);--ui-type-user-surface:var(--color-violet-950);--ui-type-reference:var(--color-green-300);--ui-type-reference-surface:var(--color-green-950);--ui-scope-managed:var(--color-red-400);--ui-scope-managed-surface:var(--color-red-950);--ui-scope-user:var(--color-blue-300);--ui-scope-user-surface:var(--color-blue-950);--ui-scope-project:var(--color-amber-300);--ui-scope-project-surface:var(--color-amber-950);--ui-scope-local:var(--color-slate-400);--ui-scope-local-surface:var(--color-slate-800);--ui-shadow-card:0 1px 2px #0006;--ui-shadow-pop:0 1px 2px #00000080, 0 12px 32px #00000073;--ui-scrim:#020610b3}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}
package/public/ui.mjs CHANGED
@@ -19,9 +19,10 @@ export const sidebarRoot = 'mt-1 mb-2.5 font-mono text-2xs break-all text-fg-mut
19
19
  export const sidebarList = 'flex-1 overflow-y-auto p-2';
20
20
  export const sidebarGroup = 'px-2 pt-3 pb-1 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle';
21
21
  export const sidebarFoot = 'border-t border-line p-4';
22
- export const sidebarCredit = 'flex flex-col gap-1.5 text-2xs text-fg-subtle no-underline transition-colors hover:text-fg';
23
- export const sidebarLogo = 'w-24 opacity-60 transition-opacity dark:invert';
24
- export const sidebarFootRow = 'flex items-end justify-between gap-3';
22
+ export const sidebarCredit = 'flex items-center gap-2 text-2xs whitespace-nowrap text-fg-subtle no-underline transition-colors hover:text-fg';
23
+ export const sidebarLogo = 'w-16 opacity-60 transition-opacity dark:invert';
24
+ export const sidebarFootRow = 'mb-3 flex items-center justify-between gap-3';
25
+ export const sidebarSource = 'min-w-0 truncate font-mono text-2xs text-fg-subtle no-underline transition-colors hover:text-fg';
25
26
  export const themeToggle = cx('inline-flex flex-none cursor-pointer items-center justify-center rounded-md border border-line bg-surface font-sans text-base leading-none text-fg-muted transition-colors hover:border-line-strong hover:text-fg', 'min-w-control-h-sm', controlPad.sm);
26
27
 
27
28
  export const toggleLabel = 'flex cursor-pointer items-center gap-1.5 text-xs text-fg-muted';
@@ -0,0 +1,150 @@
1
+ import * as ui from '/ui.mjs';
2
+ import { node } from '/dom.mjs';
3
+ import { focusSession } from '/views/sessions.mjs';
4
+
5
+ const money = (value) => (value >= 100 ? `$${Math.round(value).toLocaleString()}` : `$${value.toFixed(2)}`);
6
+
7
+ const compact = (value) => {
8
+ const rounded = Math.round(value);
9
+ if (Math.abs(rounded) >= 1000000000) return `${(rounded / 1000000000).toFixed(1)}B`;
10
+ if (Math.abs(rounded) >= 1000000) return `${(rounded / 1000000).toFixed(1)}M`;
11
+ if (Math.abs(rounded) >= 1000) return `${(rounded / 1000).toFixed(1)}K`;
12
+ return String(rounded);
13
+ };
14
+
15
+ const clamp = (percent) => Math.min(100, Math.max(0, percent));
16
+
17
+ const shortModel = (name) => name.replace(/^claude-/, '').replace(/-\d{8}$/, '');
18
+
19
+ const fact = (value, label) => node('span', {}, [
20
+ node('b', { class: ui.meterFactValue, text: value }),
21
+ document.createTextNode(` ${label}`),
22
+ ]);
23
+
24
+ function meter(percent, value, unit, note, facts) {
25
+ return node('div', { class: ui.meter }, [
26
+ node('div', { class: ui.meterTop }, [
27
+ node('span', { class: ui.meterValue, text: value }),
28
+ node('span', { class: ui.meterUnit, text: unit }),
29
+ ]),
30
+ node('div', { class: ui.meterBar }, [
31
+ node('div', { class: ui.meterFill('accent'), style: `width:${clamp(percent).toFixed(1)}%` }),
32
+ ]),
33
+ note ? node('p', { class: ui.meterNote, text: note }) : null,
34
+ facts.length ? node('div', { class: ui.meterFacts }, facts) : null,
35
+ ]);
36
+ }
37
+
38
+ const row = (name, detail, value, onclick) => node('div', { class: ui.settingsLayerRow }, [
39
+ onclick
40
+ ? node('button', { class: ui.linkButton, text: name, onclick })
41
+ : node('span', { class: ui.settingsKeyName, text: name }),
42
+ node('span', { class: ui.settingsLayerFile, text: detail }),
43
+ node('span', { class: ui.settingsEffective, text: value }),
44
+ ]);
45
+
46
+ function projectSection(report) {
47
+ const project = report.project;
48
+ if (!project) {
49
+ return [node('p', { class: ui.noteTight, text: 'This entry is not a project store, so ccusage has no transcripts to attribute to it.' })];
50
+ }
51
+ if (!project.sessions) {
52
+ return [node('p', { class: ui.noteTight, text: 'ccusage found no billed sessions for this project.' })];
53
+ }
54
+
55
+ const cacheShare = project.tokens ? (project.cacheRead / project.tokens) * 100 : 0;
56
+ return [
57
+ node('div', { class: ui.sectionLabel, text: 'This project' }),
58
+ meter(
59
+ project.share,
60
+ money(project.cost),
61
+ `spent here, ${project.share.toFixed(1)}% of everything Claude Code has cost you`,
62
+ 'Read from the same transcripts this app lists under Sessions, priced with the ccusage cached price table. No network call is made.',
63
+ [
64
+ fact(String(project.sessions), 'sessions'),
65
+ fact(compact(project.tokens), 'tokens'),
66
+ fact(compact(project.output), 'written by Claude'),
67
+ fact(`${cacheShare.toFixed(1)}%`, 'of it cache reads'),
68
+ ],
69
+ ),
70
+ ];
71
+ }
72
+
73
+ function modelSection(report) {
74
+ const models = report.project ? report.project.models : [];
75
+ if (!models.length) return [];
76
+ const top = models[0].cost;
77
+ return [
78
+ node('div', { class: ui.sectionLabel, text: 'Where the money went' }),
79
+ ...models.map((model) => row(
80
+ shortModel(model.name),
81
+ `${compact(model.tokens)} tokens written${model.cost === top ? ' · most expensive' : ''}`,
82
+ money(model.cost),
83
+ )),
84
+ ];
85
+ }
86
+
87
+ function sessionSection(report) {
88
+ const top = report.project ? report.project.top : [];
89
+ if (!top.length) return [];
90
+ return [
91
+ node('div', { class: ui.sectionLabel, text: 'Most expensive sessions' }),
92
+ ...top.map((session) => row(
93
+ session.id.slice(0, 8),
94
+ `${compact(session.tokens)} tokens · ${session.last.slice(0, 10)}`,
95
+ money(session.cost),
96
+ () => focusSession(session.id),
97
+ )),
98
+ node('p', { class: ui.note, text: 'Click an id to open that transcript under Sessions. A session Claude Code has already swept is gone from there, and says so.' }),
99
+ ];
100
+ }
101
+
102
+ function machineSection(report) {
103
+ const machine = report.machine;
104
+ if (!machine) return [node('p', { class: ui.noteTight, text: 'The ccusage ledger could not be read.' })];
105
+
106
+ const window = machine.window;
107
+ const share = machine.cost ? (window.cost / machine.cost) * 100 : 0;
108
+ return [
109
+ node('div', { class: ui.sectionLabel, text: report.project ? 'Every project' : 'This machine' }),
110
+ meter(
111
+ share,
112
+ money(machine.cost),
113
+ 'across every project Claude Code has a transcript for',
114
+ `The bar is how much of that fell in the last ${window.days} days, so a full one means the habit is recent rather than accumulated.`,
115
+ [
116
+ fact(String(machine.sessions), 'sessions'),
117
+ fact(compact(machine.tokens), 'tokens'),
118
+ fact(money(window.cost), `in the last ${window.days} days`),
119
+ ],
120
+ ),
121
+ ];
122
+ }
123
+
124
+ function errorNotes(report) {
125
+ if (!report.errors.length) return [];
126
+ return [
127
+ node('div', { class: ui.sectionLabel, text: 'Not read' }),
128
+ ...report.errors.map((error) => node('p', { class: ui.noteTight, text: `${error.step}: ${error.message}` })),
129
+ ];
130
+ }
131
+
132
+ export function ccusageSummary(report) {
133
+ const project = report.project;
134
+ if (project && project.sessions) {
135
+ return `${money(project.cost)} here · ${project.sessions} sessions`;
136
+ }
137
+ if (report.machine) return `${money(report.machine.cost)} across every project`;
138
+ return 'no ledger';
139
+ }
140
+
141
+ export function renderCcusage(container, report) {
142
+ const parts = [
143
+ ...projectSection(report),
144
+ ...machineSection(report),
145
+ ...modelSection(report),
146
+ ...sessionSection(report),
147
+ ...errorNotes(report),
148
+ ];
149
+ for (const part of parts) container.append(part);
150
+ }
@@ -29,16 +29,6 @@ function meter(percent, unit, note, facts) {
29
29
  ]);
30
30
  }
31
31
 
32
- function head(report, actions) {
33
- return node('div', { class: ui.sectionLabelRow }, [
34
- node('span', { class: ui.sectionLabelInline, text: report.version ? `rtk ${report.version}` : 'rtk' }),
35
- node('span', { class: ui.note, text: report.path || '' }),
36
- node('span', { class: ui.listSpacer }),
37
- node('button', { class: ui.buttonSmall, text: 'Re-read', onclick: actions.reread }),
38
- node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: actions.turnOff }),
39
- ]);
40
- }
41
-
42
32
  function projectSection(report) {
43
33
  const project = report.project;
44
34
  if (!project) return [];
@@ -132,8 +122,16 @@ function errorNotes(report) {
132
122
  ];
133
123
  }
134
124
 
135
- export function renderRtk(container, report, actions) {
136
- container.append(head(report, actions));
125
+ export function rtkSummary(report) {
126
+ const project = report.project;
127
+ if (project && project.commands) {
128
+ return `${project.savedPct.toFixed(1)}% saved here · ${compact(project.saved)} tokens`;
129
+ }
130
+ if (report.machine) return `${report.machine.savedPct.toFixed(1)}% saved on this machine`;
131
+ return 'no ledger';
132
+ }
133
+
134
+ export function renderRtk(container, report) {
137
135
  for (const part of projectSection(report)) container.append(part);
138
136
 
139
137
  container.append(node('div', { class: ui.sectionLabel, text: report.project ? 'Every project' : 'This machine' }));
@@ -5,6 +5,8 @@ import { state } from '/state.mjs';
5
5
  import { sizeLabel, sessionDate, isAt } from '/parts.mjs';
6
6
  import { selectMemory } from '/store.mjs';
7
7
  import { paint } from '/bus.mjs';
8
+ import { goTo } from '/parts.mjs';
9
+ import { toast } from '/api.mjs';
8
10
 
9
11
  const TITLE_SOURCE = {
10
12
  'ai-title': 'title Claude Code generated for this session',
@@ -12,9 +14,15 @@ const TITLE_SOURCE = {
12
14
  prompt: 'first prompt, no generated title found in the part read',
13
15
  };
14
16
 
15
- function sessionRow(session, memoriesFrom) {
17
+ export function focusSession(id) {
18
+ state.aux.sessionFocus = id;
19
+ state.aux.sessionDay = null;
20
+ goTo('environment', 'sessions');
21
+ }
22
+
23
+ function sessionRow(session, memoriesFrom, focused) {
16
24
  const soon = session.expiresInDays !== null && session.expiresInDays <= 7;
17
- const caret = node('span', { class: ui.contextCaret, text: '\u25b8' });
25
+ const caret = node('span', { class: ui.contextCaret, text: focused ? '\u25be' : '\u25b8' });
18
26
 
19
27
  const tags = [];
20
28
  if (session.gitBranch) tags.push(node('span', { class: ui.badge('neutral'), text: session.gitBranch }));
@@ -29,7 +37,7 @@ function sessionRow(session, memoriesFrom) {
29
37
  tags.push(node('span', { class: ui.badge('project'), text: memory.name }));
30
38
  }
31
39
 
32
- const body = node('div', { class: ui.contextBody, hidden: true });
40
+ const body = node('div', { class: ui.contextBody, hidden: !focused });
33
41
  const facts = node('dl', { class: ui.metaList });
34
42
  const rows = [
35
43
  ['session', session.id],
@@ -62,7 +70,7 @@ function sessionRow(session, memoriesFrom) {
62
70
 
63
71
  const button = node('button', {
64
72
  class: ui.contextRowButton,
65
- 'aria-expanded': 'false',
73
+ 'aria-expanded': String(Boolean(focused)),
66
74
  onclick: (event) => {
67
75
  const open = body.hidden;
68
76
  body.hidden = !open;
@@ -227,9 +235,21 @@ export async function renderSessions(container) {
227
235
  }));
228
236
  }
229
237
  container.append(listHead);
238
+ const focus = state.aux.sessionFocus;
239
+ state.aux.sessionFocus = null;
240
+
230
241
  const card = node('div', { class: ui.card });
242
+ let focused = null;
231
243
  for (const session of visible) {
232
- card.append(sessionRow(session, byOrigin.get(session.id) || []));
244
+ const row = sessionRow(session, byOrigin.get(session.id) || [], session.id === focus);
245
+ if (session.id === focus) focused = row;
246
+ card.append(row);
233
247
  }
234
248
  container.append(card);
249
+
250
+ if (focus && focused) {
251
+ requestAnimationFrame(() => focused.scrollIntoView({ block: 'center' }));
252
+ } else if (focus) {
253
+ toast('That transcript is no longer beside this store - Claude Code has swept it.');
254
+ }
235
255
  }
@@ -4,73 +4,141 @@ import { api } from '/api.mjs';
4
4
  import { state } from '/state.mjs';
5
5
  import { isAt } from '/parts.mjs';
6
6
  import { paint } from '/bus.mjs';
7
- import { renderRtk } from '/views/rtk.mjs';
7
+ import { renderRtk, rtkSummary } from '/views/rtk.mjs';
8
+ import { renderCcusage, ccusageSummary } from '/views/ccusage.mjs';
8
9
 
9
10
  const OPT_IN = 'toolsOptIn';
10
11
 
11
- const installedRtk = () => state.tools.find((tool) => tool.id === 'rtk' && tool.found) || null;
12
+ const PANELS = {
13
+ rtk: { render: renderRtk, summary: rtkSummary },
14
+ ccusage: { render: renderCcusage, summary: ccusageSummary },
15
+ };
12
16
 
13
- async function fetchRtk() {
14
- const id = state.storeId;
17
+ const installed = () => state.tools.filter((tool) => tool.found && PANELS[tool.id]);
18
+
19
+ const openKey = (id) => `toolOpen:${id}`;
20
+ const isOpen = (id) => localStorage.getItem(openKey(id)) === '1';
21
+
22
+ function setOpen(id, open) {
23
+ if (open) localStorage.setItem(openKey(id), '1');
24
+ else localStorage.removeItem(openKey(id));
25
+ }
26
+
27
+ async function fetchTool(id) {
28
+ const storeId = state.storeId;
15
29
  let next;
16
30
  try {
17
- next = { data: await api(`/api/stores/${encodeURIComponent(id)}/tools/rtk`) };
31
+ next = { data: await api(`/api/stores/${encodeURIComponent(storeId)}/tools/${encodeURIComponent(id)}`) };
18
32
  } catch (err) {
19
33
  next = { error: err.message };
20
34
  }
21
- if (state.storeId !== id) return;
22
- state.aux.rtk = next;
35
+ if (state.storeId !== storeId) return;
36
+ state.aux.tools = { ...state.aux.tools, [id]: next };
23
37
  if (isAt('environment', 'tools')) paint('tab');
24
38
  }
25
39
 
26
- function startRtk({ remember = true } = {}) {
40
+ function start({ remember = true } = {}) {
27
41
  if (remember) localStorage.setItem(OPT_IN, '1');
28
- state.aux.rtk = { running: true };
29
- fetchRtk();
42
+ const running = {};
43
+ for (const tool of installed()) running[tool.id] = { running: true };
44
+ state.aux.tools = running;
45
+ for (const tool of installed()) fetchTool(tool.id);
30
46
  }
31
47
 
32
- function readRtk() {
33
- startRtk();
48
+ function readAll() {
49
+ start();
34
50
  paint('tab');
35
51
  }
36
52
 
37
53
  function turnOff() {
38
54
  localStorage.removeItem(OPT_IN);
39
- state.aux.rtk = null;
55
+ state.aux.tools = null;
40
56
  paint('tab');
41
57
  }
42
58
 
43
- function gate(container, tool) {
59
+ function gate(container, tools) {
60
+ const names = tools.map((tool) => tool.label).join(' and ');
61
+ const verb = tools.length > 1 ? 'are installed' : 'is installed';
44
62
  container.append(node('div', { class: ui.issue(false) }, [
45
63
  node('div', { class: ui.issueBody }, [
46
- node('div', { class: ui.issueTitle, text: 'rtk is installed, and is not being read' }),
47
- node('div', { class: ui.issueDetail, text: `${tool.path} keeps a ledger of what its filters stripped before it reached the model. Reading it runs rtk on this machine, so it stays off until you ask.` }),
64
+ node('div', { class: ui.issueTitle, text: `${names} ${verb}, and not being read` }),
65
+ node('div', { class: ui.issueDetail, text: `Each keeps a ledger of what it saved or spent on your behalf. Reading them runs ${names} on this machine, so it stays off until you ask.` }),
48
66
  ]),
49
- node('button', { class: ui.buttonSmall, text: 'Read rtk stats', onclick: readRtk }),
67
+ node('button', { class: ui.buttonSmall, text: 'Read tool stats', onclick: readAll }),
50
68
  ]));
51
69
  }
52
70
 
53
- function failure(container, message) {
54
- container.append(node('div', { class: ui.issue(true) }, [
55
- node('div', { class: ui.issueBody }, [
56
- node('div', { class: ui.issueTitle, text: 'rtk could not be read' }),
57
- node('div', { class: ui.issueDetail, text: message }),
71
+ function headline(tool, slot) {
72
+ if (!slot || slot.running) return 'reading…';
73
+ if (slot.error) return 'could not be read';
74
+ return PANELS[tool.id].summary(slot.data);
75
+ }
76
+
77
+ function subtitle(tool, slot) {
78
+ const report = slot && slot.data;
79
+ if (report && report.version) return `${tool.label} ${report.version}`;
80
+ return tool.label;
81
+ }
82
+
83
+ function body(tool, slot) {
84
+ const host = node('div');
85
+ if (!slot || slot.running) {
86
+ host.append(node('p', { class: ui.note, text: `Reading the ${tool.label} ledger…` }));
87
+ return host;
88
+ }
89
+ if (slot.error) {
90
+ host.append(node('div', { class: ui.issue(true) }, [
91
+ node('div', { class: ui.issueBody }, [
92
+ node('div', { class: ui.issueTitle, text: `${tool.label} could not be read` }),
93
+ node('div', { class: ui.issueDetail, text: slot.error }),
94
+ ]),
95
+ node('button', { class: ui.buttonSmall, text: 'Try again', onclick: readAll }),
96
+ ]));
97
+ return host;
98
+ }
99
+ PANELS[tool.id].render(host, slot.data);
100
+ return host;
101
+ }
102
+
103
+ function accordion(container, tool, slot) {
104
+ const open = isOpen(tool.id);
105
+ const panel = body(tool, slot);
106
+ panel.hidden = !open;
107
+
108
+ const caret = node('span', { class: ui.contextCaret, text: open ? '▾' : '▸' });
109
+
110
+ container.append(node('button', {
111
+ class: ui.contextRowButton,
112
+ 'aria-expanded': String(open),
113
+ onclick: (event) => {
114
+ const next = panel.hidden;
115
+ panel.hidden = !next;
116
+ caret.textContent = next ? '▾' : '▸';
117
+ event.currentTarget.setAttribute('aria-expanded', String(next));
118
+ setOpen(tool.id, next);
119
+ },
120
+ }, [
121
+ node('div', { class: ui.contextMain }, [
122
+ node('div', { class: ui.contextFile }, [caret, document.createTextNode(subtitle(tool, slot))]),
58
123
  ]),
59
- node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: turnOff }),
60
- node('button', { class: ui.buttonSmall, text: 'Try again', onclick: readRtk }),
124
+ node('div', { class: ui.contextSize, text: headline(tool, slot) }),
61
125
  ]));
126
+ container.append(panel);
62
127
  }
63
128
 
64
129
  export function renderTools(container) {
65
- const tool = installedRtk();
66
- if (!tool) return;
130
+ const tools = installed();
131
+ if (!tools.length) return;
67
132
 
68
- if (!state.aux.rtk && localStorage.getItem(OPT_IN) === '1') startRtk({ remember: false });
133
+ if (!state.aux.tools && localStorage.getItem(OPT_IN) === '1') start({ remember: false });
134
+ if (!state.aux.tools) return gate(container, tools);
69
135
 
70
- const report = state.aux.rtk;
71
- if (!report) return gate(container, tool);
72
- if (report.running) return container.append(node('p', { class: ui.note, text: 'Reading the rtk ledger…' }));
73
- if (report.error) return failure(container, report.error);
136
+ container.append(node('div', { class: ui.listBar }, [
137
+ node('span', { class: ui.sectionLabelInline, text: 'Companion tools' }),
138
+ node('span', { class: ui.listSpacer }),
139
+ node('button', { class: ui.buttonSmall, text: 'Re-read', onclick: readAll }),
140
+ node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: turnOff }),
141
+ ]));
74
142
 
75
- renderRtk(container, report.data, { reread: readRtk, turnOff });
143
+ for (const tool of tools) accordion(container, tool, state.aux.tools[tool.id]);
76
144
  }
package/server.mjs CHANGED
@@ -16,7 +16,7 @@ import { listStores } from './src/stores.mjs';
16
16
  import { forgetPath, rememberPath } from './src/pathcache.mjs';
17
17
  import { searchAll } from './src/search.mjs';
18
18
  import { verifyPaths } from './src/pathcheck.mjs';
19
- import { findRtk, rtkReport } from './src/rtk.mjs';
19
+ import { detectTools, toolReport } from './src/tools.mjs';
20
20
  import { sessionsWithSummaries, transcriptDir } from './src/sessions.mjs';
21
21
  import {
22
22
  addIndexEntry,
@@ -129,6 +129,14 @@ function requireStore(id) {
129
129
  return store;
130
130
  }
131
131
 
132
+ const VERSION = (() => {
133
+ try {
134
+ return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version || null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ })();
139
+
132
140
  function storeProjectDir(store) {
133
141
  // The global store is the user scope itself, which no project owns.
134
142
  if (store.kind === 'global') return null;
@@ -218,7 +226,8 @@ async function handleApi(req, res, url) {
218
226
  rootSource: origin.path === ROOT ? origin.source : 'flag',
219
227
  rootFile: origin.path === ROOT ? origin.file : null,
220
228
  rootWarning: origin.invalid,
221
- tools: [findRtk()],
229
+ version: VERSION,
230
+ tools: detectTools(),
222
231
  stores: listStores(ROOT),
223
232
  });
224
233
  }
@@ -278,11 +287,12 @@ async function handleApi(req, res, url) {
278
287
  return sendJson(res, 200, sessionsWithSummaries(slugDir, { projectDir: storeProjectDir(store) }));
279
288
  }
280
289
 
281
- if (action === 'tools/rtk' && req.method === 'GET') {
282
- const rtk = findRtk();
283
- if (!rtk.found) throw new Error('rtk is not installed, or not on the PATH this server was started with.');
284
- const report = await rtkReport(storeProjectDir(store));
285
- return sendJson(res, 200, { ...report, path: rtk.path });
290
+ if (action.startsWith('tools/') && req.method === 'GET') {
291
+ const target = {
292
+ projectDir: store.kind === 'global' ? null : storeProjectDir(store),
293
+ slug: store.kind === 'auto' ? store.slug : null,
294
+ };
295
+ return sendJson(res, 200, await toolReport(action.slice('tools/'.length), target));
286
296
  }
287
297
 
288
298
  if (action === 'settings' && req.method === 'GET') {
@@ -0,0 +1,122 @@
1
+ import os from 'node:os';
2
+
3
+ import { collector, findBinary, num, parseJson, pct, runBinary, text } from './toolrun.mjs';
4
+
5
+ const BINARY = 'ccusage';
6
+ const WINDOW_DAYS = 30;
7
+ const SESSION_LIMIT = 8;
8
+ const MODEL_LIMIT = 6;
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+
11
+ export const findCcusage = (env = process.env) => ({ id: BINARY, ...findBinary(BINARY, env) });
12
+
13
+ const runCcusage = (args, options) => runBinary(BINARY, args, options);
14
+
15
+ const ZERO = { sessions: 0, cost: 0, tokens: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
16
+
17
+ function rowOf(session) {
18
+ const models = Array.isArray(session.modelBreakdowns) ? session.modelBreakdowns : [];
19
+ return {
20
+ id: text(session.sessionId),
21
+ slug: text(session.projectPath),
22
+ cost: num(session.totalCost),
23
+ tokens: num(session.totalTokens),
24
+ input: num(session.inputTokens),
25
+ output: num(session.outputTokens),
26
+ cacheRead: num(session.cacheReadTokens),
27
+ cacheWrite: num(session.cacheCreationTokens),
28
+ last: text(session.lastActivity),
29
+ models: models
30
+ .filter((model) => model && typeof model === 'object')
31
+ .map((model) => ({ name: text(model.modelName), cost: num(model.cost), tokens: num(model.outputTokens) }))
32
+ .filter((model) => model.name),
33
+ };
34
+ }
35
+
36
+ const totalsOf = (rows) => rows.reduce((sum, row) => ({
37
+ sessions: sum.sessions + 1,
38
+ cost: sum.cost + row.cost,
39
+ tokens: sum.tokens + row.tokens,
40
+ input: sum.input + row.input,
41
+ output: sum.output + row.output,
42
+ cacheRead: sum.cacheRead + row.cacheRead,
43
+ cacheWrite: sum.cacheWrite + row.cacheWrite,
44
+ }), ZERO);
45
+
46
+ function modelMix(rows) {
47
+ const byName = new Map();
48
+ for (const row of rows) {
49
+ for (const model of row.models) {
50
+ const seen = byName.get(model.name) || { name: model.name, cost: 0, tokens: 0 };
51
+ seen.cost += model.cost;
52
+ seen.tokens += model.tokens;
53
+ byName.set(model.name, seen);
54
+ }
55
+ }
56
+ return [...byName.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_LIMIT);
57
+ }
58
+
59
+ function windowOf(rows, now) {
60
+ const cutoff = now - (WINDOW_DAYS * DAY_MS);
61
+ const recent = rows.filter((row) => {
62
+ const stamp = Date.parse(row.last);
63
+ return Number.isFinite(stamp) && stamp >= cutoff;
64
+ });
65
+ return { days: WINDOW_DAYS, ...totalsOf(recent) };
66
+ }
67
+
68
+ function machineOf(raw, rows) {
69
+ const totals = raw && typeof raw.totals === 'object' && raw.totals ? raw.totals : {};
70
+ return {
71
+ sessions: rows.length,
72
+ cost: num(totals.totalCost),
73
+ tokens: num(totals.totalTokens),
74
+ input: num(totals.inputTokens),
75
+ output: num(totals.outputTokens),
76
+ cacheRead: num(totals.cacheReadTokens),
77
+ cacheWrite: num(totals.cacheCreationTokens),
78
+ };
79
+ }
80
+
81
+ export async function ccusageReport(target = {}, { run = runCcusage, home = os.homedir(), now = Date.now() } = {}) {
82
+ const { errors, step } = collector();
83
+
84
+ const [version, raw] = await Promise.all([
85
+ step('version', async () => text(await run(['--version'], { cwd: home })).trim().replace(/^ccusage\s+/, '')),
86
+ step('claude session', async () => parseJson(
87
+ await run(['claude', 'session', '--json', '--offline'], { cwd: home }),
88
+ 'ccusage claude session',
89
+ )),
90
+ ]);
91
+
92
+ if (!raw) {
93
+ return { tool: BINARY, version: version || null, slug: target.slug || null, machine: null, project: null, errors };
94
+ }
95
+
96
+ const rows = (Array.isArray(raw.sessions) ? raw.sessions : [])
97
+ .filter((session) => session && typeof session === 'object')
98
+ .map(rowOf)
99
+ .filter((row) => row.id);
100
+
101
+ const machine = machineOf(raw, rows);
102
+ const mine = target.slug ? rows.filter((row) => row.slug === target.slug) : null;
103
+ const mineTotals = mine ? totalsOf(mine) : null;
104
+
105
+ return {
106
+ tool: BINARY,
107
+ version: version || null,
108
+ slug: target.slug || null,
109
+ machine: { ...machine, window: windowOf(rows, now) },
110
+ project: mine ? {
111
+ ...mineTotals,
112
+ share: pct(mineTotals.cost, machine.cost),
113
+ window: windowOf(mine, now),
114
+ models: modelMix(mine),
115
+ top: [...mine]
116
+ .sort((a, b) => b.cost - a.cost)
117
+ .slice(0, SESSION_LIMIT)
118
+ .map(({ id, cost, tokens, last }) => ({ id, cost, tokens, last })),
119
+ } : null,
120
+ errors,
121
+ };
122
+ }
package/src/rtk.mjs CHANGED
@@ -1,66 +1,16 @@
1
- import { execFile } from 'node:child_process';
2
- import fs from 'node:fs';
3
1
  import os from 'node:os';
4
- import path from 'node:path';
2
+
3
+ import { collector, findBinary, num, parseJson, pct, runBinary, text, usableDir } from './toolrun.mjs';
5
4
 
6
5
  const BINARY = 'rtk';
7
- const TIMEOUT_MS = 15000;
8
- const MAX_BUFFER = 4 * 1024 * 1024;
9
6
  const DISCOVER_DAYS = 30;
10
7
  const DISCOVER_LIMIT = 10;
11
8
  const MISSED_LIMIT = 12;
12
9
  const TREND_DAYS = 30;
13
10
 
14
- const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
15
- const text = (value) => (typeof value === 'string' ? value : '');
16
- const pct = (part, whole) => (whole > 0 ? (part / whole) * 100 : 0);
17
-
18
- function candidateNames(env) {
19
- if (process.platform !== 'win32') return [BINARY];
20
- const exts = text(env.PATHEXT || '.EXE').split(';').filter(Boolean);
21
- return exts.map((ext) => BINARY + ext.toLowerCase());
22
- }
23
-
24
- export function findRtk(env = process.env) {
25
- const dirs = text(env.PATH).split(path.delimiter).filter(Boolean);
26
- const names = candidateNames(env);
27
- for (const dir of dirs) {
28
- for (const name of names) {
29
- const candidate = path.join(dir, name);
30
- try {
31
- if (!fs.statSync(candidate).isFile()) continue;
32
- fs.accessSync(candidate, fs.constants.X_OK);
33
- return { id: BINARY, found: true, path: candidate };
34
- } catch {
35
- continue;
36
- }
37
- }
38
- }
39
- return { id: BINARY, found: false, path: null };
40
- }
41
-
42
- function runRtk(args, { cwd, timeout = TIMEOUT_MS } = {}) {
43
- return new Promise((resolve, reject) => {
44
- const options = { cwd, timeout, maxBuffer: MAX_BUFFER, windowsHide: true, encoding: 'utf8' };
45
- execFile(BINARY, args, options, (err, stdout, stderr) => {
46
- if (!err) return resolve(stdout);
47
- if (err.code === 'ENOENT') return reject(new Error('rtk is not on this process PATH'));
48
- if (err.killed) return reject(new Error(`rtk ${args[0]} was still running after ${timeout}ms and was stopped`));
49
- const detail = text(stderr || stdout).trim().split('\n')[0];
50
- return reject(new Error(detail ? `rtk ${args[0]} failed: ${detail}` : `rtk ${args[0]} exited ${err.code}`));
51
- });
52
- });
53
- }
11
+ export const findRtk = (env = process.env) => ({ id: BINARY, ...findBinary(BINARY, env) });
54
12
 
55
- function parseJson(raw, label) {
56
- try {
57
- const value = JSON.parse(raw);
58
- if (!value || typeof value !== 'object') throw new Error('not an object');
59
- return value;
60
- } catch {
61
- throw new Error(`rtk ${label} did not answer with JSON`);
62
- }
63
- }
13
+ const runRtk = (args, options) => runBinary(BINARY, args, options);
64
14
 
65
15
  function summaryOf(raw) {
66
16
  const summary = raw && typeof raw.summary === 'object' && raw.summary ? raw.summary : {};
@@ -133,35 +83,17 @@ function discoverOf(raw) {
133
83
  };
134
84
  }
135
85
 
136
- function usableDir(dir) {
137
- if (!dir) return null;
138
- try {
139
- return fs.statSync(dir).isDirectory() ? dir : null;
140
- } catch {
141
- return null;
142
- }
143
- }
144
-
145
- export async function rtkReport(projectDir, { run = runRtk, home = os.homedir() } = {}) {
146
- const errors = [];
147
- const step = async (label, work) => {
148
- try {
149
- return await work();
150
- } catch (err) {
151
- errors.push({ step: label, message: err.message });
152
- return null;
153
- }
154
- };
155
-
156
- const dir = usableDir(projectDir);
86
+ export async function rtkReport(target = {}, { run = runRtk, home = os.homedir() } = {}) {
87
+ const { errors, step } = collector();
88
+ const dir = usableDir(target.projectDir);
157
89
 
158
90
  const [version, machine, project, discover] = await Promise.all([
159
91
  step('version', async () => text(await run(['--version'], { cwd: home })).trim().replace(/^rtk\s+/, '')),
160
- step('gain', async () => parseJson(await run(['gain', '-a', '--format', 'json'], { cwd: home }), 'gain')),
161
- dir ? step('gain --project', async () => parseJson(await run(['gain', '-p', '--format', 'json'], { cwd: dir }), 'gain --project')) : null,
92
+ step('gain', async () => parseJson(await run(['gain', '-a', '--format', 'json'], { cwd: home }), 'rtk gain')),
93
+ dir ? step('gain --project', async () => parseJson(await run(['gain', '-p', '--format', 'json'], { cwd: dir }), 'rtk gain --project')) : null,
162
94
  dir ? step('discover', async () => parseJson(
163
95
  await run(['discover', '--format', 'json', '-s', String(DISCOVER_DAYS), '-l', String(DISCOVER_LIMIT)], { cwd: dir }),
164
- 'discover',
96
+ 'rtk discover',
165
97
  )) : null,
166
98
  ]);
167
99
 
@@ -0,0 +1,78 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const TIMEOUT_MS = 15000;
6
+ const MAX_BUFFER = 4 * 1024 * 1024;
7
+
8
+ export const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
9
+ export const text = (value) => (typeof value === 'string' ? value : '');
10
+ export const pct = (part, whole) => (whole > 0 ? (part / whole) * 100 : 0);
11
+
12
+ function candidateNames(binary, env) {
13
+ if (process.platform !== 'win32') return [binary];
14
+ return text(env.PATHEXT || '.EXE').split(';').filter(Boolean).map((ext) => binary + ext.toLowerCase());
15
+ }
16
+
17
+ export function findBinary(binary, env = process.env) {
18
+ const dirs = text(env.PATH).split(path.delimiter).filter(Boolean);
19
+ const names = candidateNames(binary, env);
20
+ for (const dir of dirs) {
21
+ for (const name of names) {
22
+ const candidate = path.join(dir, name);
23
+ try {
24
+ if (!fs.statSync(candidate).isFile()) continue;
25
+ fs.accessSync(candidate, fs.constants.X_OK);
26
+ return { found: true, path: candidate };
27
+ } catch {
28
+ continue;
29
+ }
30
+ }
31
+ }
32
+ return { found: false, path: null };
33
+ }
34
+
35
+ export function runBinary(binary, args, { cwd, timeout = TIMEOUT_MS } = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ const options = { cwd, timeout, maxBuffer: MAX_BUFFER, windowsHide: true, encoding: 'utf8' };
38
+ execFile(binary, args, options, (err, stdout, stderr) => {
39
+ if (!err) return resolve(stdout);
40
+ if (err.code === 'ENOENT') return reject(new Error(`${binary} is not on this process PATH`));
41
+ if (err.killed) return reject(new Error(`${binary} ${args[0]} was still running after ${timeout}ms and was stopped`));
42
+ const detail = text(stderr || stdout).trim().split('\n')[0];
43
+ return reject(new Error(detail ? `${binary} ${args[0]} failed: ${detail}` : `${binary} ${args[0]} exited ${err.code}`));
44
+ });
45
+ });
46
+ }
47
+
48
+ export function parseJson(raw, label) {
49
+ try {
50
+ const value = JSON.parse(raw);
51
+ if (!value || typeof value !== 'object') throw new Error('not an object');
52
+ return value;
53
+ } catch {
54
+ throw new Error(`${label} did not answer with JSON`);
55
+ }
56
+ }
57
+
58
+ export function usableDir(dir) {
59
+ if (!dir) return null;
60
+ try {
61
+ return fs.statSync(dir).isDirectory() ? dir : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ export function collector() {
68
+ const errors = [];
69
+ const step = async (label, work) => {
70
+ try {
71
+ return await work();
72
+ } catch (err) {
73
+ errors.push({ step: label, message: err.message });
74
+ return null;
75
+ }
76
+ };
77
+ return { errors, step };
78
+ }
package/src/tools.mjs ADDED
@@ -0,0 +1,47 @@
1
+ import { findBinary } from './toolrun.mjs';
2
+ import { rtkReport } from './rtk.mjs';
3
+ import { ccusageReport } from './ccusage.mjs';
4
+
5
+ const REGISTRY = [
6
+ {
7
+ id: 'rtk',
8
+ binary: 'rtk',
9
+ label: 'rtk',
10
+ repo: 'https://github.com/rtk-ai/rtk',
11
+ blurb: 'filters command output before it reaches the model',
12
+ report: rtkReport,
13
+ },
14
+ {
15
+ id: 'ccusage',
16
+ binary: 'ccusage',
17
+ label: 'ccusage',
18
+ repo: 'https://github.com/ccusage/ccusage',
19
+ blurb: 'reads what Claude Code actually billed, from the same transcripts',
20
+ report: ccusageReport,
21
+ },
22
+ ];
23
+
24
+ export function detectTools(env = process.env) {
25
+ return REGISTRY.map(({ id, label, repo, blurb, binary }) => ({
26
+ id,
27
+ label,
28
+ repo,
29
+ blurb,
30
+ ...findBinary(binary, env),
31
+ }));
32
+ }
33
+
34
+ export const toolById = (id) => REGISTRY.find((tool) => tool.id === id) || null;
35
+
36
+ export async function toolReport(id, target, { env = process.env } = {}) {
37
+ const tool = toolById(id);
38
+ if (!tool) throw new Error(`Unknown tool: ${id}`);
39
+
40
+ const found = findBinary(tool.binary, env);
41
+ if (!found.found) {
42
+ throw new Error(`${tool.label} is not installed, or not on the PATH this server was started with.`);
43
+ }
44
+
45
+ const report = await tool.report(target);
46
+ return { ...report, id: tool.id, label: tool.label, repo: tool.repo, path: found.path };
47
+ }