token-larper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +334 -0
  2. package/Start-TokenLarper.vbs +5 -0
  3. package/bin/cli.js +34 -0
  4. package/llms.txt +59 -0
  5. package/package.json +61 -0
  6. package/scripts/docs-demo-server.ts +141 -0
  7. package/scripts/launch-on-default-desktop.ps1 +42 -0
  8. package/scripts/launch-silent.vbs +6 -0
  9. package/scripts/run-server.ps1 +59 -0
  10. package/scripts/tray-host.ps1 +181 -0
  11. package/src/ccusage.ts +1075 -0
  12. package/src/client/App.tsx +560 -0
  13. package/src/client/charts.tsx +341 -0
  14. package/src/client/components/Calendar.tsx +207 -0
  15. package/src/client/components/DateRangePicker.tsx +131 -0
  16. package/src/client/components/KpiCards.tsx +122 -0
  17. package/src/client/components/RankChip.tsx +22 -0
  18. package/src/client/components/SelectMenu.tsx +116 -0
  19. package/src/client/components/SettingsDialog.tsx +207 -0
  20. package/src/client/components/ThemeSettings.tsx +131 -0
  21. package/src/client/context.tsx +60 -0
  22. package/src/client/index.html +54 -0
  23. package/src/client/lib/aggregate.ts +266 -0
  24. package/src/client/lib/range.ts +87 -0
  25. package/src/client/lib/rank.ts +323 -0
  26. package/src/client/lib/shareCard.ts +139 -0
  27. package/src/client/logo.svg +8 -0
  28. package/src/client/main.tsx +12 -0
  29. package/src/client/styles.css +1113 -0
  30. package/src/client/themes.ts +268 -0
  31. package/src/client/utils.ts +61 -0
  32. package/src/client/views/Models.tsx +71 -0
  33. package/src/client/views/Overview.tsx +280 -0
  34. package/src/client/views/Projects.tsx +88 -0
  35. package/src/client/views/Rank.tsx +255 -0
  36. package/src/client/views/Sessions.tsx +108 -0
  37. package/src/client/views/Tools.tsx +114 -0
  38. package/src/projects.ts +214 -0
  39. package/src/server.ts +173 -0
  40. package/src/startup.ts +110 -0
  41. package/src/tray.ts +293 -0
  42. package/src/types.ts +155 -0
package/src/startup.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import type { StartupConfig } from "./types.ts";
4
+
5
+ const REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
6
+ const REG_VALUE_NAME = "TokenLarper";
7
+ const ROOT_DIR = resolve(import.meta.dir, "..");
8
+ const SETTINGS_FILE = join(ROOT_DIR, ".cache", "settings.json");
9
+ const VBS_LAUNCHER = join(ROOT_DIR, "scripts", "launch-silent.vbs");
10
+ const PS1_RUNNER = join(ROOT_DIR, "scripts", "run-server.ps1");
11
+
12
+ interface SavedSettings {
13
+ openBrowserOnBoot: boolean;
14
+ port: number;
15
+ }
16
+
17
+ function validPort(value: unknown, fallback: number): number {
18
+ const port = Number(value);
19
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
20
+ }
21
+
22
+ function loadSavedSettings(defaultPort = 4269): SavedSettings {
23
+ try {
24
+ if (existsSync(SETTINGS_FILE)) {
25
+ const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
26
+ return {
27
+ openBrowserOnBoot: raw.openBrowserOnBoot === true,
28
+ port: validPort(raw.port, defaultPort),
29
+ };
30
+ }
31
+ } catch {
32
+ // A missing or damaged settings file falls back to the default port.
33
+ }
34
+ return { openBrowserOnBoot: false, port: defaultPort };
35
+ }
36
+
37
+ function saveSettings(settings: SavedSettings): void {
38
+ mkdirSync(join(ROOT_DIR, ".cache"), { recursive: true });
39
+ writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf8");
40
+ }
41
+
42
+ async function runReg(args: string[]): Promise<{ code: number; output: string; error: string }> {
43
+ const proc = Bun.spawn(["reg.exe", ...args], { stdout: "pipe", stderr: "pipe" });
44
+ const [output, error, code] = await Promise.all([
45
+ new Response(proc.stdout).text(),
46
+ new Response(proc.stderr).text(),
47
+ proc.exited,
48
+ ]);
49
+ return { code, output, error };
50
+ }
51
+
52
+ async function readRegistryValue(): Promise<string | null> {
53
+ if (process.platform !== "win32") return null;
54
+ const result = await runReg(["query", REG_KEY, "/v", REG_VALUE_NAME]);
55
+ if (result.code !== 0) return null;
56
+ const line = result.output.split(/\r?\n/).find((part) =>
57
+ new RegExp(`^\\s*${REG_VALUE_NAME}\\s+REG_\\w+\\s+`, "i").test(part)
58
+ );
59
+ return line?.match(/^\s*\S+\s+REG_\w+\s+(.+)$/i)?.[1]?.trim() ?? null;
60
+ }
61
+
62
+ export async function getStartupStatus(defaultPort = 4269): Promise<StartupConfig> {
63
+ const saved = loadSavedSettings(defaultPort);
64
+ let registryValue: string | null = null;
65
+ try {
66
+ registryValue = await readRegistryValue();
67
+ } catch {
68
+ // Status remains readable if reg.exe is unavailable.
69
+ }
70
+ const expected = `wscript.exe //B //Nologo "${VBS_LAUNCHER}"`;
71
+ return {
72
+ enabled: registryValue?.toLowerCase() === expected.toLowerCase(),
73
+ openBrowserOnBoot: saved.openBrowserOnBoot,
74
+ port: defaultPort,
75
+ registryValue,
76
+ launcherPath: VBS_LAUNCHER,
77
+ };
78
+ }
79
+
80
+ export async function setStartupStatus(options: {
81
+ enabled: boolean;
82
+ openBrowserOnBoot?: boolean;
83
+ port?: number;
84
+ }): Promise<StartupConfig> {
85
+ const current = loadSavedSettings();
86
+ const next: SavedSettings = {
87
+ openBrowserOnBoot: options.openBrowserOnBoot ?? current.openBrowserOnBoot,
88
+ port: validPort(options.port ?? current.port, current.port),
89
+ };
90
+
91
+ if (options.enabled && (!existsSync(VBS_LAUNCHER) || !existsSync(PS1_RUNNER))) {
92
+ throw new Error("The Windows startup launcher is missing");
93
+ }
94
+
95
+ if (process.platform === "win32") {
96
+ if (options.enabled) {
97
+ const command = `wscript.exe //B //Nologo "${VBS_LAUNCHER}"`;
98
+ const result = await runReg([
99
+ "add", REG_KEY, "/v", REG_VALUE_NAME, "/t", "REG_SZ", "/d", command, "/f",
100
+ ]);
101
+ if (result.code !== 0) throw new Error(result.error.trim() || "Could not enable Windows startup");
102
+ } else if (await readRegistryValue()) {
103
+ const result = await runReg(["delete", REG_KEY, "/v", REG_VALUE_NAME, "/f"]);
104
+ if (result.code !== 0) throw new Error(result.error.trim() || "Could not disable Windows startup");
105
+ }
106
+ }
107
+
108
+ saveSettings(next);
109
+ return getStartupStatus(next.port);
110
+ }
package/src/tray.ts ADDED
@@ -0,0 +1,293 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ const ROOT_DIR = resolve(import.meta.dir, "..");
5
+ const SCRIPTS_DIR = join(ROOT_DIR, "scripts");
6
+ const TRAY_PS1_PATH = join(SCRIPTS_DIR, "tray-host.ps1");
7
+ const TRAY_LAUNCHER_PS1 = join(SCRIPTS_DIR, "launch-on-default-desktop.ps1");
8
+
9
+ let trayPid: number | null = null;
10
+
11
+ export function generateTrayScript(port: number, serverPid: number): string {
12
+ mkdirSync(SCRIPTS_DIR, { recursive: true });
13
+
14
+ const psScript = `
15
+ Add-Type -AssemblyName System.Windows.Forms
16
+ Add-Type -AssemblyName System.Drawing
17
+
18
+ $ServerPort = ${port}
19
+ $ServerPid = ${serverPid}
20
+ $BaseUrl = "http://127.0.0.1:$ServerPort"
21
+
22
+ # Opt into DPI awareness so the icon is drawn at the tray's real pixel size
23
+ # (20px at 125%, 24px at 150%) instead of being drawn at 16px and blurred up.
24
+ Add-Type -Namespace TokenLarper -Name Dpi -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();'
25
+ [TokenLarper.Dpi]::SetProcessDPIAware() | Out-Null
26
+
27
+ # The t. monogram from src/client/logo.svg, drawn in its 64-unit coordinates.
28
+ $iconSize = [Math]::Max(16, [System.Windows.Forms.SystemInformation]::SmallIconSize.Width)
29
+ $bmp = New-Object System.Drawing.Bitmap($iconSize, $iconSize)
30
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
31
+ $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
32
+ $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
33
+ $g.Clear([System.Drawing.Color]::Transparent)
34
+ $g.ScaleTransform($iconSize / 64, $iconSize / 64)
35
+
36
+ $green = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 47, 90, 67))
37
+ $cream = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 243, 227, 191))
38
+ $gold = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 225, 183, 92))
39
+
40
+ # Rounded square: x 3, y 3, size 58, corner radius 14.
41
+ $tile = New-Object System.Drawing.Drawing2D.GraphicsPath
42
+ $tile.AddArc(3, 3, 28, 28, 180, 90)
43
+ $tile.AddArc(33, 3, 28, 28, 270, 90)
44
+ $tile.AddArc(33, 33, 28, 28, 0, 90)
45
+ $tile.AddArc(3, 33, 28, 28, 90, 90)
46
+ $tile.CloseFigure()
47
+ $g.FillPath($green, $tile)
48
+
49
+ # Letter t: a stem with a curved foot, plus the crossbar.
50
+ $stem = New-Object System.Drawing.Drawing2D.GraphicsPath
51
+ $stem.AddLine(20, 12, 28, 12)
52
+ $stem.AddLine(28, 12, 28, 43.5)
53
+ $stem.AddBezier(28, 43.5, 28, 45.2, 28.8, 46, 30.5, 46)
54
+ $stem.AddLine(30.5, 46, 35, 46)
55
+ $stem.AddLine(35, 46, 35, 53)
56
+ $stem.AddLine(35, 53, 28.5, 53)
57
+ $stem.AddBezier(28.5, 53, 22.8, 53, 20, 50.2, 20, 44.5)
58
+ $stem.CloseFigure()
59
+ $g.FillPath($cream, $stem)
60
+ $g.FillRectangle($cream, 13, 21, 22, 7)
61
+
62
+ # The dot.
63
+ $g.FillEllipse($gold, 39.5, 42, 11, 11)
64
+
65
+ $hIcon = $bmp.GetHicon()
66
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
67
+
68
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
69
+ $notifyIcon.Icon = $icon
70
+ $notifyIcon.Text = "Token Larper - AI Coding Telemetry (Port $ServerPort)"
71
+ $notifyIcon.Visible = $true
72
+
73
+ # Context Menu
74
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
75
+
76
+ $itemHeader = New-Object System.Windows.Forms.ToolStripMenuItem("Token Larper Telemetry")
77
+ $itemHeader.Enabled = $false
78
+ $menu.Items.Add($itemHeader) | Out-Null
79
+
80
+ $itemStats = New-Object System.Windows.Forms.ToolStripMenuItem("Loading usage stats...")
81
+ $itemStats.Enabled = $false
82
+ $menu.Items.Add($itemStats) | Out-Null
83
+
84
+ $menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
85
+
86
+ $itemOpen = New-Object System.Windows.Forms.ToolStripMenuItem("Open Dashboard (http://localhost:$ServerPort)")
87
+ $itemOpen.Font = New-Object System.Drawing.Font($itemOpen.Font, [System.Drawing.FontStyle]::Bold)
88
+ $itemOpen.Add_Click({
89
+ Start-Process "$BaseUrl"
90
+ })
91
+ $menu.Items.Add($itemOpen) | Out-Null
92
+
93
+ $itemSync = New-Object System.Windows.Forms.ToolStripMenuItem("Sync Harnesses Now (ccusage)")
94
+ $itemSync.Add_Click({
95
+ try {
96
+ $notifyIcon.ShowBalloonTip(2000, "Token Larper", "Syncing coding harnesses via ccusage...", [System.Windows.Forms.ToolTipIcon]::Info)
97
+ Invoke-RestMethod -Uri "$BaseUrl/api/usage?refresh=1" -Method Get -TimeoutSec 20 | Out-Null
98
+ Update-TrayStatus
99
+ $notifyIcon.ShowBalloonTip(2500, "Token Larper Synced", $itemStats.Text, [System.Windows.Forms.ToolTipIcon]::Info)
100
+ } catch {
101
+ $notifyIcon.ShowBalloonTip(2500, "Token Larper", "Failed to sync harnesses.", [System.Windows.Forms.ToolTipIcon]::Warning)
102
+ }
103
+ })
104
+ $menu.Items.Add($itemSync) | Out-Null
105
+
106
+ $menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
107
+
108
+ $itemBoot = New-Object System.Windows.Forms.ToolStripMenuItem("Start on Windows Boot")
109
+ $itemBoot.CheckOnClick = $false
110
+ $itemBoot.Add_Click({
111
+ try {
112
+ $nextState = -not $itemBoot.Checked
113
+ $body = @{ enabled = $nextState; openBrowserOnBoot = $itemOpenOnBoot.Checked } | ConvertTo-Json
114
+ $res = Invoke-RestMethod -Uri "$BaseUrl/api/startup" -Method Post -Body $body -ContentType "application/json" -TimeoutSec 5
115
+ $itemBoot.Checked = [bool]$res.enabled
116
+ $itemOpenOnBoot.Checked = [bool]$res.openBrowserOnBoot
117
+ } catch {}
118
+ })
119
+ $menu.Items.Add($itemBoot) | Out-Null
120
+
121
+ $itemOpenOnBoot = New-Object System.Windows.Forms.ToolStripMenuItem("Auto-Open Browser on Boot")
122
+ $itemOpenOnBoot.CheckOnClick = $false
123
+ $itemOpenOnBoot.Add_Click({
124
+ try {
125
+ $nextOpen = -not $itemOpenOnBoot.Checked
126
+ $body = @{ enabled = $itemBoot.Checked; openBrowserOnBoot = $nextOpen } | ConvertTo-Json
127
+ $res = Invoke-RestMethod -Uri "$BaseUrl/api/startup" -Method Post -Body $body -ContentType "application/json" -TimeoutSec 5
128
+ $itemBoot.Checked = [bool]$res.enabled
129
+ $itemOpenOnBoot.Checked = [bool]$res.openBrowserOnBoot
130
+ } catch {}
131
+ })
132
+ $menu.Items.Add($itemOpenOnBoot) | Out-Null
133
+
134
+ $menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
135
+
136
+ $itemQuit = New-Object System.Windows.Forms.ToolStripMenuItem("Quit Token Larper")
137
+ $itemQuit.Add_Click({
138
+ $notifyIcon.Visible = $false
139
+ try {
140
+ Invoke-RestMethod -Uri "$BaseUrl/api/shutdown" -Method Post -ContentType "application/json" -TimeoutSec 3 | Out-Null
141
+ } catch {}
142
+ try {
143
+ Stop-Process -Id $ServerPid -Force -ErrorAction SilentlyContinue
144
+ } catch {}
145
+ $notifyIcon.Dispose()
146
+ [System.Windows.Forms.Application]::Exit()
147
+ })
148
+ $menu.Items.Add($itemQuit) | Out-Null
149
+
150
+ $notifyIcon.ContextMenuStrip = $menu
151
+
152
+ # Double-click tray icon opens dashboard
153
+ $notifyIcon.Add_DoubleClick({
154
+ Start-Process "$BaseUrl"
155
+ })
156
+
157
+ function Update-TrayStatus {
158
+ try {
159
+ $st = Invoke-RestMethod -Uri "$BaseUrl/api/tray-status" -Method Get -TimeoutSec 4
160
+ if ($null -ne $st) {
161
+ $itemStats.Text = $st.summaryText
162
+ $tooltip = "Token Larper: $($st.shortTooltip)"
163
+ if ($tooltip.Length -gt 63) {
164
+ $tooltip = $tooltip.Substring(0, 63)
165
+ }
166
+ $notifyIcon.Text = $tooltip
167
+ $itemBoot.Checked = [bool]$st.bootEnabled
168
+ $itemOpenOnBoot.Checked = [bool]$st.openBrowserOnBoot
169
+ }
170
+ } catch {}
171
+ }
172
+
173
+ # Timer to check parent Bun server health & update stats
174
+ $timer = New-Object System.Windows.Forms.Timer
175
+ $timer.Interval = 5000
176
+ $script:tickCount = 0
177
+ $timer.Add_Tick({
178
+ $proc = Get-Process -Id $ServerPid -ErrorAction SilentlyContinue
179
+ if ($null -eq $proc) {
180
+ $notifyIcon.Visible = $false
181
+ $notifyIcon.Dispose()
182
+ [System.Windows.Forms.Application]::Exit()
183
+ return
184
+ }
185
+ $script:tickCount++
186
+ if ($script:tickCount -eq 1 -or ($script:tickCount % 3 -eq 0)) {
187
+ Update-TrayStatus
188
+ }
189
+ })
190
+ $timer.Start()
191
+
192
+ Update-TrayStatus
193
+ $notifyIcon.ShowBalloonTip(2500, "Token Larper Running", "Right-click or double-click the green t. icon in your system tray.", [System.Windows.Forms.ToolTipIcon]::Info)
194
+
195
+ [System.Windows.Forms.Application]::Run()
196
+ `.trim();
197
+
198
+ writeFileSync(TRAY_PS1_PATH, psScript, "utf8");
199
+
200
+ // Generate WinSta0\Default desktop trampoline so even sandboxed terminals attach to Explorer's tray
201
+ const launcherScript = `
202
+ Add-Type @"
203
+ using System;
204
+ using System.Runtime.InteropServices;
205
+ public class DefaultDesktopLauncher {
206
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
207
+ public struct STARTUPINFO {
208
+ public int cb;
209
+ public string lpReserved;
210
+ public string lpDesktop;
211
+ public string lpTitle;
212
+ public int dwX; public int dwY; public int dwXSize; public int dwYSize;
213
+ public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute;
214
+ public int dwFlags; public short wShowWindow; public short cbReserved2;
215
+ public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError;
216
+ }
217
+ [StructLayout(LayoutKind.Sequential)]
218
+ public struct PROCESS_INFORMATION {
219
+ public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId;
220
+ }
221
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
222
+ public static extern bool CreateProcess(
223
+ string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
224
+ bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
225
+ ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
226
+
227
+ public static int LaunchOnDefaultDesktop(string cmdLine, string workDir) {
228
+ STARTUPINFO si = new STARTUPINFO();
229
+ si.cb = Marshal.SizeOf(si);
230
+ si.lpDesktop = "WinSta0\\\\Default";
231
+ si.dwFlags = 1;
232
+ si.wShowWindow = 0;
233
+ PROCESS_INFORMATION pi;
234
+ uint CREATE_NO_WINDOW = 0x08000000;
235
+ bool ok = CreateProcess(null, cmdLine, IntPtr.Zero, IntPtr.Zero, false, CREATE_NO_WINDOW, IntPtr.Zero, workDir, ref si, out pi);
236
+ if (!ok) return -Marshal.GetLastWin32Error();
237
+ return pi.dwProcessId;
238
+ }
239
+ }
240
+ "@
241
+ $cmd = "powershell.exe -NoProfile -ExecutionPolicy Bypass -Sta -File \`"${TRAY_PS1_PATH}\`""
242
+ $launchedPid = [DefaultDesktopLauncher]::LaunchOnDefaultDesktop($cmd, "${ROOT_DIR}")
243
+ Write-Output $launchedPid
244
+ `.trim();
245
+
246
+ writeFileSync(TRAY_LAUNCHER_PS1, launcherScript, "utf8");
247
+ return TRAY_LAUNCHER_PS1;
248
+ }
249
+
250
+ export async function startSystemTray(port: number): Promise<void> {
251
+ if (process.platform !== "win32") return;
252
+ if (process.env.NO_TRAY === "1") return;
253
+
254
+ try {
255
+ const launcherPath = generateTrayScript(port, process.pid);
256
+ const proc = Bun.spawn(
257
+ [
258
+ "powershell.exe",
259
+ "-NoProfile",
260
+ "-NonInteractive",
261
+ "-ExecutionPolicy",
262
+ "Bypass",
263
+ "-File",
264
+ launcherPath,
265
+ ],
266
+ {
267
+ stdout: "pipe",
268
+ stderr: "ignore",
269
+ }
270
+ );
271
+ const out = (await new Response(proc.stdout).text()).trim();
272
+ const parsedPid = Number(out);
273
+ if (Number.isFinite(parsedPid) && parsedPid > 0) {
274
+ trayPid = parsedPid;
275
+ }
276
+ } catch (err) {
277
+ console.error("Failed to launch system tray icon:", err);
278
+ }
279
+ }
280
+
281
+ export function stopSystemTray(): void {
282
+ if (trayPid && trayPid > 0) {
283
+ try {
284
+ Bun.spawnSync(["taskkill.exe", "/PID", String(trayPid), "/F"], {
285
+ stdout: "ignore",
286
+ stderr: "ignore",
287
+ });
288
+ } catch {
289
+ // ignore
290
+ }
291
+ trayPid = null;
292
+ }
293
+ }
package/src/types.ts ADDED
@@ -0,0 +1,155 @@
1
+ export type HarnessId =
2
+ | "claude"
3
+ | "codex"
4
+ | "antigravity"
5
+ | "pi"
6
+ | "opencode"
7
+ | "gemini"
8
+ | "copilot"
9
+ | "amp"
10
+ | "droid"
11
+ | "codebuff"
12
+ | "hermes"
13
+ | "goose"
14
+ | "kilo"
15
+ | "kimi"
16
+ | "qwen"
17
+ | "openclaw"
18
+ | "grok"
19
+ | "zcode";
20
+
21
+ export interface HarnessMetadata {
22
+ id: HarnessId;
23
+ name: string;
24
+ vendor: string;
25
+ color: string;
26
+ accentBg: string;
27
+ description: string;
28
+ configPathHint: string;
29
+ installed: boolean;
30
+ hasUsage: boolean;
31
+ syncStatus: "ready" | "syncing" | "idle" | "error";
32
+ lastSyncMs?: number;
33
+ }
34
+
35
+ export interface ModelMetric {
36
+ modelName: string;
37
+ harness: HarnessId;
38
+ inputTokens: number;
39
+ outputTokens: number;
40
+ cacheCreationTokens: number;
41
+ cacheReadTokens: number;
42
+ reasoningOutputTokens: number;
43
+ totalTokens: number;
44
+ verifiedCost: number;
45
+ estimatedCost: number;
46
+ missingPricing: boolean;
47
+ }
48
+
49
+ export interface PeriodHarnessBreakdown {
50
+ harness: HarnessId;
51
+ inputTokens: number;
52
+ outputTokens: number;
53
+ cacheCreationTokens: number;
54
+ cacheReadTokens: number;
55
+ reasoningOutputTokens: number;
56
+ totalTokens: number;
57
+ verifiedCost: number;
58
+ estimatedCost: number;
59
+ models: ModelMetric[];
60
+ }
61
+
62
+ export interface TimePeriodRow {
63
+ period: string; // YYYY-MM-DD, YYYY-Www, YYYY-MM, or YYYY
64
+ label: string;
65
+ inputTokens: number;
66
+ outputTokens: number;
67
+ cacheCreationTokens: number;
68
+ cacheReadTokens: number;
69
+ reasoningOutputTokens: number;
70
+ totalTokens: number;
71
+ verifiedCost: number;
72
+ estimatedCost: number;
73
+ byHarness: Record<string, PeriodHarnessBreakdown>;
74
+ modelsUsed: string[];
75
+ }
76
+
77
+ export interface SessionEntry {
78
+ id: string;
79
+ harness: HarnessId;
80
+ projectPath: string;
81
+ /** Working directory read from the harness's own session files, when it records one. */
82
+ cwd?: string;
83
+ /** Repository root (nearest folder with .git) or the cwd; sessions are grouped by this. */
84
+ projectRoot?: string;
85
+ projectName?: string;
86
+ firstActivity?: string;
87
+ lastActivity?: string;
88
+ date: string;
89
+ inputTokens: number;
90
+ outputTokens: number;
91
+ cacheCreationTokens: number;
92
+ cacheReadTokens: number;
93
+ reasoningOutputTokens: number;
94
+ totalTokens: number;
95
+ verifiedCost: number;
96
+ estimatedCost: number;
97
+ modelsUsed: string[];
98
+ modelBreakdowns: ModelMetric[];
99
+ }
100
+
101
+ export interface HarnessSummary {
102
+ meta: HarnessMetadata;
103
+ inputTokens: number;
104
+ outputTokens: number;
105
+ cacheCreationTokens: number;
106
+ cacheReadTokens: number;
107
+ reasoningOutputTokens: number;
108
+ totalTokens: number;
109
+ verifiedCost: number;
110
+ estimatedCost: number;
111
+ activeDays: number;
112
+ sessionCount: number;
113
+ lastActiveDate: string | null;
114
+ topModel: string | null;
115
+ cacheHitRate: number;
116
+ models: ModelMetric[];
117
+ unpricedModels: string[];
118
+ }
119
+
120
+ export interface DashboardPayload {
121
+ generatedAt: string;
122
+ ccusageVersion: string;
123
+ syncingHarnesses: HarnessId[];
124
+ totals: {
125
+ inputTokens: number;
126
+ outputTokens: number;
127
+ cacheCreationTokens: number;
128
+ cacheReadTokens: number;
129
+ reasoningOutputTokens: number;
130
+ totalTokens: number;
131
+ verifiedCost: number;
132
+ estimatedCost: number;
133
+ activeHarnesses: number;
134
+ installedHarnesses: number;
135
+ totalHarnesses: number;
136
+ activeDays: number;
137
+ totalSessions: number;
138
+ cacheHitRate: number;
139
+ };
140
+ harnesses: HarnessSummary[];
141
+ daily: TimePeriodRow[];
142
+ weekly: TimePeriodRow[];
143
+ monthly: TimePeriodRow[];
144
+ yearly: TimePeriodRow[];
145
+ models: ModelMetric[];
146
+ sessions: SessionEntry[];
147
+ }
148
+
149
+ export interface StartupConfig {
150
+ enabled: boolean;
151
+ openBrowserOnBoot: boolean;
152
+ port: number;
153
+ registryValue: string | null;
154
+ launcherPath: string;
155
+ }