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.
- package/README.md +334 -0
- package/Start-TokenLarper.vbs +5 -0
- package/bin/cli.js +34 -0
- package/llms.txt +59 -0
- package/package.json +61 -0
- package/scripts/docs-demo-server.ts +141 -0
- package/scripts/launch-on-default-desktop.ps1 +42 -0
- package/scripts/launch-silent.vbs +6 -0
- package/scripts/run-server.ps1 +59 -0
- package/scripts/tray-host.ps1 +181 -0
- package/src/ccusage.ts +1075 -0
- package/src/client/App.tsx +560 -0
- package/src/client/charts.tsx +341 -0
- package/src/client/components/Calendar.tsx +207 -0
- package/src/client/components/DateRangePicker.tsx +131 -0
- package/src/client/components/KpiCards.tsx +122 -0
- package/src/client/components/RankChip.tsx +22 -0
- package/src/client/components/SelectMenu.tsx +116 -0
- package/src/client/components/SettingsDialog.tsx +207 -0
- package/src/client/components/ThemeSettings.tsx +131 -0
- package/src/client/context.tsx +60 -0
- package/src/client/index.html +54 -0
- package/src/client/lib/aggregate.ts +266 -0
- package/src/client/lib/range.ts +87 -0
- package/src/client/lib/rank.ts +323 -0
- package/src/client/lib/shareCard.ts +139 -0
- package/src/client/logo.svg +8 -0
- package/src/client/main.tsx +12 -0
- package/src/client/styles.css +1113 -0
- package/src/client/themes.ts +268 -0
- package/src/client/utils.ts +61 -0
- package/src/client/views/Models.tsx +71 -0
- package/src/client/views/Overview.tsx +280 -0
- package/src/client/views/Projects.tsx +88 -0
- package/src/client/views/Rank.tsx +255 -0
- package/src/client/views/Sessions.tsx +108 -0
- package/src/client/views/Tools.tsx +114 -0
- package/src/projects.ts +214 -0
- package/src/server.ts +173 -0
- package/src/startup.ts +110 -0
- package/src/tray.ts +293 -0
- 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
|
+
}
|