Read Claude Code usage from local session logs, redesign panel as separate cards
This commit is contained in:
@@ -38,7 +38,7 @@ The installer automatically exposes scripts from the `scripts/` directory as glo
|
|||||||
- **`blob_glass [on|off|toggle]`**: A quick toggle to enable or disable window transparency on the fly.
|
- **`blob_glass [on|off|toggle]`**: A quick toggle to enable or disable window transparency on the fly.
|
||||||
- **`blob_boot [path]`**: Safely updates your Plymouth boot splash image (defaults to `branding/boot_flash.png`) and rebuilds the `initramfs` (GRUB compatible via `mkinitcpio`).
|
- **`blob_boot [path]`**: Safely updates your Plymouth boot splash image (defaults to `branding/boot_flash.png`) and rebuilds the `initramfs` (GRUB compatible via `mkinitcpio`).
|
||||||
- **`blob_wifi`**: A streamlined script to connect to the GMU Eduroam Wi-Fi network using `iwd` and `systemd-resolved` (replaces NetworkManager).
|
- **`blob_wifi`**: A streamlined script to connect to the GMU Eduroam Wi-Fi network using `iwd` and `systemd-resolved` (replaces NetworkManager).
|
||||||
- **`blob_key <set|show|clear|list> [NAME] [VALUE]`**: Stores secrets/env values for widgets and services, e.g. `blob_key set ANTHROPIC_ADMIN_KEY sk-ant-admin01-...` for the Claude Code Usage card in the quick settings panel.
|
- **`blob_key <set|show|clear|list> [NAME] [VALUE]`**: Stores secrets/env values for widgets and services, e.g. `blob_key set SOME_TOKEN value`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
+15
-24
@@ -1,34 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
KEY_FILE="$HOME/.config/ags/secrets/ANTHROPIC_ADMIN_KEY"
|
PROJECTS_DIR="$HOME/.claude/projects"
|
||||||
|
TODAY=$(date -u '+%Y-%m-%d')
|
||||||
|
|
||||||
if [ ! -f "$KEY_FILE" ]; then
|
if [ ! -d "$PROJECTS_DIR" ]; then
|
||||||
echo '{"available":false,"tokens":0,"costUsd":0}'
|
echo '{"available":false,"tokens":0,"messages":0}'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ADMIN_KEY=$(cat "$KEY_FILE")
|
FILES=$(find "$PROJECTS_DIR" -name "*.jsonl" -newermt "$TODAY")
|
||||||
TODAY_START=$(date -u '+%Y-%m-%dT00:00:00Z')
|
|
||||||
TODAY_END=$(date -u -d tomorrow '+%Y-%m-%dT00:00:00Z')
|
|
||||||
|
|
||||||
USAGE_JSON=$(curl -fsS --max-time 6 \
|
if [ -z "$FILES" ]; then
|
||||||
-H "anthropic-version: 2023-06-01" \
|
echo '{"available":true,"tokens":0,"messages":0}'
|
||||||
-H "x-api-key: $ADMIN_KEY" \
|
|
||||||
"https://api.anthropic.com/v1/organizations/usage_report/messages?starting_at=${TODAY_START}&ending_at=${TODAY_END}&bucket_width=1d")
|
|
||||||
|
|
||||||
COST_JSON=$(curl -fsS --max-time 6 \
|
|
||||||
-H "anthropic-version: 2023-06-01" \
|
|
||||||
-H "x-api-key: $ADMIN_KEY" \
|
|
||||||
"https://api.anthropic.com/v1/organizations/cost_report?starting_at=${TODAY_START}&ending_at=${TODAY_END}&bucket_width=1d")
|
|
||||||
|
|
||||||
if [ -z "$USAGE_JSON" ] || [ -z "$COST_JSON" ]; then
|
|
||||||
echo '{"available":false,"tokens":0,"costUsd":0}'
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TOKENS=$(echo "$USAGE_JSON" | jq '[(.data // [])[].results[]? | (.uncached_input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation.ephemeral_1h_input_tokens // 0) + (.cache_creation.ephemeral_5m_input_tokens // 0) + (.output_tokens // 0)] | add // 0')
|
jq -s --arg today "$TODAY" '
|
||||||
|
[.[] | select(.timestamp != null and (.timestamp | startswith($today)) and .message.usage != null) | .message.usage] as $usages
|
||||||
COST_CENTS=$(echo "$COST_JSON" | jq '[(.data // [])[].results[]?.amount | tonumber] | add // 0')
|
| {
|
||||||
|
available: true,
|
||||||
jq -n --argjson tokens "$TOKENS" --argjson costCents "$COST_CENTS" \
|
tokens: ([$usages[] | (.input_tokens // 0) + (.output_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)] | add // 0),
|
||||||
'{available: true, tokens: $tokens, costUsd: ($costCents / 100)}'
|
messages: ($usages | length)
|
||||||
|
}
|
||||||
|
' $FILES 2>/dev/null || echo '{"available":false,"tokens":0,"messages":0}'
|
||||||
|
|||||||
+7
-14
@@ -349,44 +349,37 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qs-divider {
|
|
||||||
min-width: 1px;
|
|
||||||
background-color: alpha(@foreground, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.qs-side-panel {
|
.qs-side-panel {
|
||||||
min-width: 160px;
|
min-width: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card {
|
.widget-card {
|
||||||
background-color: alpha(@color0, 0.6);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card-icon {
|
.widget-card-icon {
|
||||||
color: @accent;
|
color: @accent;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card-title {
|
.widget-card-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @foreground;
|
color: @foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card-label {
|
.widget-card-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card-value {
|
.widget-card-value {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @foreground;
|
color: @foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-card-empty {
|
.widget-card-empty {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -5,18 +5,18 @@ import { shell } from "../lib/utils"
|
|||||||
type ClaudeUsage = {
|
type ClaudeUsage = {
|
||||||
available: boolean
|
available: boolean
|
||||||
tokens: number
|
tokens: number
|
||||||
costUsd: number
|
messages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage = createPoll<ClaudeUsage>(
|
const usage = createPoll<ClaudeUsage>(
|
||||||
{ available: false, tokens: 0, costUsd: 0 },
|
{ available: false, tokens: 0, messages: 0 },
|
||||||
60000,
|
60000,
|
||||||
shell("bash $HOME/.config/ags/lib/claude-usage.sh"),
|
shell("bash $HOME/.config/ags/lib/claude-usage.sh"),
|
||||||
(stdout) => {
|
(stdout) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(stdout.trim())
|
return JSON.parse(stdout.trim())
|
||||||
} catch {
|
} catch {
|
||||||
return { available: false, tokens: 0, costUsd: 0 }
|
return { available: false, tokens: 0, messages: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -29,26 +29,26 @@ function formatTokens(tokens: number) {
|
|||||||
|
|
||||||
export function ClaudeUsageCard() {
|
export function ClaudeUsageCard() {
|
||||||
return (
|
return (
|
||||||
<box class="side-card" vertical spacing={6}>
|
<box class="panel widget-card" vertical spacing={6}>
|
||||||
<box spacing={8}>
|
<box spacing={8}>
|
||||||
<label class="side-card-icon" label={""} />
|
<label class="widget-card-icon" label={""} />
|
||||||
<label class="side-card-title" label="Claude Code" xalign={0} halign={Gtk.Align.START} hexpand />
|
<label class="widget-card-title" label="Claude Code" xalign={0} halign={Gtk.Align.START} hexpand />
|
||||||
</box>
|
</box>
|
||||||
<label
|
<label
|
||||||
class="side-card-empty"
|
class="widget-card-empty"
|
||||||
label="No admin key set"
|
label="No usage logs found"
|
||||||
xalign={0}
|
xalign={0}
|
||||||
halign={Gtk.Align.START}
|
halign={Gtk.Align.START}
|
||||||
visible={usage.as((u) => !u.available)}
|
visible={usage.as((u) => !u.available)}
|
||||||
/>
|
/>
|
||||||
<box vertical spacing={4} visible={usage.as((u) => u.available)}>
|
<box vertical spacing={4} visible={usage.as((u) => u.available)}>
|
||||||
<box spacing={8}>
|
<box spacing={8}>
|
||||||
<label class="side-card-label" label="Tokens today" xalign={0} halign={Gtk.Align.START} hexpand />
|
<label class="widget-card-label" label="Tokens today" xalign={0} halign={Gtk.Align.START} hexpand />
|
||||||
<label class="side-card-value" label={usage.as((u) => formatTokens(u.tokens))} />
|
<label class="widget-card-value" label={usage.as((u) => formatTokens(u.tokens))} />
|
||||||
</box>
|
</box>
|
||||||
<box spacing={8}>
|
<box spacing={8}>
|
||||||
<label class="side-card-label" label="Cost today" xalign={0} halign={Gtk.Align.START} hexpand />
|
<label class="widget-card-label" label="Messages today" xalign={0} halign={Gtk.Align.START} hexpand />
|
||||||
<label class="side-card-value" label={usage.as((u) => `$${u.costUsd.toFixed(2)}`)} />
|
<label class="widget-card-value" label={usage.as((u) => `${u.messages}`)} />
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -313,12 +313,12 @@ const uptime = createPoll("", 60000, shell("uptime -p"), (stdout) =>
|
|||||||
|
|
||||||
function UptimeCard() {
|
function UptimeCard() {
|
||||||
return (
|
return (
|
||||||
<box class="side-card" vertical spacing={6}>
|
<box class="panel widget-card" vertical spacing={6}>
|
||||||
<box spacing={8}>
|
<box spacing={8}>
|
||||||
<label class="side-card-icon" label={""} />
|
<label class="widget-card-icon" label={""} />
|
||||||
<label class="side-card-title" label="Uptime" xalign={0} halign={Gtk.Align.START} hexpand />
|
<label class="widget-card-title" label="Uptime" xalign={0} halign={Gtk.Align.START} hexpand />
|
||||||
</box>
|
</box>
|
||||||
<label class="side-card-value" label={uptime} xalign={0} halign={Gtk.Align.START} wrap />
|
<label class="widget-card-value" label={uptime} xalign={0} halign={Gtk.Align.START} wrap />
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -329,19 +329,19 @@ const weather = createPoll("Loading...", 600000, shell("omarchy-weather-status")
|
|||||||
|
|
||||||
function WeatherCard() {
|
function WeatherCard() {
|
||||||
return (
|
return (
|
||||||
<box class="side-card" vertical spacing={6}>
|
<box class="panel widget-card" vertical spacing={6}>
|
||||||
<box spacing={8}>
|
<box spacing={8}>
|
||||||
<label class="side-card-icon" label={""} />
|
<label class="widget-card-icon" label={""} />
|
||||||
<label class="side-card-title" label="Weather" xalign={0} halign={Gtk.Align.START} hexpand />
|
<label class="widget-card-title" label="Weather" xalign={0} halign={Gtk.Align.START} hexpand />
|
||||||
</box>
|
</box>
|
||||||
<label class="side-card-value" label={weather} xalign={0} halign={Gtk.Align.START} wrap />
|
<label class="widget-card-value" label={weather} xalign={0} halign={Gtk.Align.START} wrap />
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LeftPanel() {
|
function LeftPanel() {
|
||||||
return (
|
return (
|
||||||
<box class="qs-side-panel" vertical spacing={10}>
|
<box class="qs-side-panel" vertical spacing={12}>
|
||||||
<UptimeCard />
|
<UptimeCard />
|
||||||
<WeatherCard />
|
<WeatherCard />
|
||||||
</box>
|
</box>
|
||||||
@@ -350,7 +350,7 @@ function LeftPanel() {
|
|||||||
|
|
||||||
function RightPanel() {
|
function RightPanel() {
|
||||||
return (
|
return (
|
||||||
<box class="qs-side-panel" vertical spacing={10}>
|
<box class="qs-side-panel" vertical spacing={12}>
|
||||||
<ClaudeUsageCard />
|
<ClaudeUsageCard />
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
@@ -371,10 +371,9 @@ export default function QuickSettings() {
|
|||||||
visible={false}
|
visible={false}
|
||||||
application={app}
|
application={app}
|
||||||
>
|
>
|
||||||
<box class="panel quick-settings-shell" spacing={12}>
|
<box spacing={12}>
|
||||||
<LeftPanel />
|
<LeftPanel />
|
||||||
<box class="qs-divider" vexpand />
|
<box class="panel quick-settings control-center" vertical spacing={12}>
|
||||||
<box class="quick-settings control-center" vertical spacing={12}>
|
|
||||||
<Header />
|
<Header />
|
||||||
<CalendarSection />
|
<CalendarSection />
|
||||||
<Toggles />
|
<Toggles />
|
||||||
@@ -382,7 +381,6 @@ export default function QuickSettings() {
|
|||||||
<BrightnessSlider />
|
<BrightnessSlider />
|
||||||
<MediaPlayer />
|
<MediaPlayer />
|
||||||
</box>
|
</box>
|
||||||
<box class="qs-divider" vexpand />
|
|
||||||
<RightPanel />
|
<RightPanel />
|
||||||
</box>
|
</box>
|
||||||
</window>
|
</window>
|
||||||
|
|||||||
Reference in New Issue
Block a user