mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-16 19:44:14 +00:00
GNOME 45+ extension that shows live token costs in the top bar panel with a dropdown for provider breakdown, top activities/models, cache stats, and budget alerts. Polls `codeburn status --format menubar-json` every 30s — same data contract as the macOS menubar app. Includes GSettings preferences (refresh interval, compact mode, budget threshold, per-provider enable/disable toggles) with Libadwaita UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
141 lines
3.5 KiB
JavaScript
141 lines
3.5 KiB
JavaScript
import GLib from 'gi://GLib';
|
|
import Gio from 'gi://Gio';
|
|
|
|
const TIMEOUT_SECONDS = 45;
|
|
const SAFE_ARG_RE = /^[A-Za-z0-9 ._/\-]+$/;
|
|
const ADDITIONAL_PATH_ENTRIES = ['/usr/local/bin', `${GLib.get_home_dir()}/.local/bin`, `${GLib.get_home_dir()}/.npm-global/bin`];
|
|
|
|
export class DataClient {
|
|
_cache = new Map();
|
|
_inFlight = null;
|
|
_codeburnPath;
|
|
|
|
constructor(codeburnPath) {
|
|
this._codeburnPath = codeburnPath || '';
|
|
}
|
|
|
|
setCodeburnPath(path) {
|
|
this._codeburnPath = path || '';
|
|
}
|
|
|
|
cancelInFlight() {
|
|
if (this._inFlight) {
|
|
this._inFlight.cancellable.cancel();
|
|
this._inFlight = null;
|
|
}
|
|
}
|
|
|
|
getCached(period, provider) {
|
|
const key = `${period}:${provider}`;
|
|
return this._cache.get(key) ?? null;
|
|
}
|
|
|
|
async fetch(period, provider) {
|
|
this.cancelInFlight();
|
|
|
|
const cancellable = new Gio.Cancellable();
|
|
this._inFlight = { cancellable };
|
|
|
|
try {
|
|
const payload = await this._spawn(period, provider, cancellable);
|
|
const key = `${period}:${provider}`;
|
|
this._cache.set(key, payload);
|
|
return payload;
|
|
} finally {
|
|
if (this._inFlight?.cancellable === cancellable)
|
|
this._inFlight = null;
|
|
}
|
|
}
|
|
|
|
_buildArgv(period, provider) {
|
|
let base;
|
|
if (this._codeburnPath && SAFE_ARG_RE.test(this._codeburnPath)) {
|
|
base = this._codeburnPath.split(' ').filter(s => s.length > 0);
|
|
} else {
|
|
base = ['codeburn'];
|
|
}
|
|
|
|
const args = [
|
|
...base,
|
|
'status',
|
|
'--format', 'menubar-json',
|
|
'--period', period,
|
|
'--no-optimize',
|
|
];
|
|
|
|
if (provider && provider !== 'all')
|
|
args.push('--provider', provider);
|
|
|
|
return args;
|
|
}
|
|
|
|
_augmentedEnv() {
|
|
const currentPath = GLib.getenv('PATH') || '/usr/bin:/bin';
|
|
const parts = currentPath.split(':');
|
|
for (const extra of ADDITIONAL_PATH_ENTRIES) {
|
|
if (!parts.includes(extra))
|
|
parts.push(extra);
|
|
}
|
|
return [`PATH=${parts.join(':')}`];
|
|
}
|
|
|
|
_spawn(period, provider, cancellable) {
|
|
return new Promise((resolve, reject) => {
|
|
const argv = this._buildArgv(period, provider);
|
|
|
|
let proc;
|
|
try {
|
|
const launcher = Gio.SubprocessLauncher.new(
|
|
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
|
|
);
|
|
for (const entry of this._augmentedEnv()) {
|
|
const [key, val] = entry.split('=', 2);
|
|
launcher.setenv(key, val, true);
|
|
}
|
|
proc = launcher.spawnv(argv);
|
|
} catch (e) {
|
|
reject(new Error(`CLI not found: ${e.message}`));
|
|
return;
|
|
}
|
|
|
|
let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS, () => {
|
|
timeoutId = 0;
|
|
proc.force_exit();
|
|
reject(new Error('CLI timeout'));
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
|
|
proc.communicate_utf8_async(null, cancellable, (_proc, res) => {
|
|
if (timeoutId) {
|
|
GLib.Source.remove(timeoutId);
|
|
timeoutId = 0;
|
|
}
|
|
|
|
try {
|
|
const [, stdout, stderr] = _proc.communicate_utf8_finish(res);
|
|
|
|
if (!_proc.get_successful()) {
|
|
const msg = stderr?.trim() || 'CLI exited with error';
|
|
reject(new Error(msg));
|
|
return;
|
|
}
|
|
|
|
if (!stdout || stdout.trim().length === 0) {
|
|
reject(new Error('CLI returned empty output'));
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.parse(stdout);
|
|
resolve(payload);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
destroy() {
|
|
this.cancelInFlight();
|
|
this._cache.clear();
|
|
}
|
|
}
|