Rene Fichtmueller a290216183 feat: RTK integration — sync SQLite stats to TokenVault
- @tokenvault/rtk-bridge: Reads ~/Library/Application Support/rtk/history.db
  and syncs new commands to /v1/rtk/stats (stateful, batched, idempotent)
- Core: rtk_commands table + 4 endpoints (/v1/rtk/stats, /rtk/commands, /rtk/hosts)
- Dashboard: new RTK Savings tab with 6 KPI cards + top commands + host breakdown
- Multi-host aggregation: sync from Mac Studio, Mac Mini, MacBooks etc.
- First sync: 753 commands, 3.5M tokens saved aggregated

RTK is MIT-licensed (github.com/rtk-ai/rtk) — we use it as the compression
engine and add the missing pieces: central tracking, team attribution,
cross-machine dashboards, long-term trend analysis.
2026-04-14 21:08:59 +02:00

200 lines
6.5 KiB
TypeScript

import Database from 'better-sqlite3';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir, hostname, platform } from 'node:os';
/**
* TokenVault RTK Bridge
*
* Reads RTK's local SQLite history.db and syncs new command stats
* to TokenVault's central PostgreSQL via the /v1/rtk/stats endpoint.
*
* RTK is MIT-licensed (https://github.com/rtk-ai/rtk)
* This bridge enables:
* - Central aggregation of RTK savings across multiple machines
* - Cross-machine dashboards
* - Team/project attribution
* - Long-term cost trend analysis
*/
interface RtkCommand {
id: number;
timestamp: string;
original_cmd: string;
rtk_cmd: string;
input_tokens: number;
output_tokens: number;
saved_tokens: number;
savings_pct: number;
exec_time_ms: number;
project_path: string;
}
interface SyncState {
last_synced_id: number;
last_sync_at: string;
host: string;
}
// ─── Config ────────────────────────────────────────────────────────────────
const TOKENVAULT_URL = process.env['TOKENVAULT_URL'] ?? 'https://tokenvault.fichtmueller.org';
const RTK_DB_PATH = process.env['RTK_DB_PATH'] ?? defaultRtkDbPath();
const STATE_FILE = join(homedir(), '.tokenvault-rtk-sync-state.json');
const HOST = hostname();
const BATCH_SIZE = 100;
function defaultRtkDbPath(): string {
const home = homedir();
if (platform() === 'darwin') return join(home, 'Library', 'Application Support', 'rtk', 'history.db');
if (platform() === 'linux') return join(home, '.local', 'share', 'rtk', 'history.db');
return join(home, '.rtk', 'history.db');
}
// ─── State (tracks last synced ID) ─────────────────────────────────────────
async function loadState(): Promise<SyncState> {
try {
const fs = await import('node:fs/promises');
const raw = await fs.readFile(STATE_FILE, 'utf-8');
return JSON.parse(raw) as SyncState;
} catch {
return { last_synced_id: 0, last_sync_at: '', host: HOST };
}
}
async function saveState(state: SyncState): Promise<void> {
const fs = await import('node:fs/promises');
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
}
// ─── RTK DB reader ─────────────────────────────────────────────────────────
function fetchNewCommands(db: Database.Database, sinceId: number, limit: number): RtkCommand[] {
const stmt = db.prepare(
`SELECT id, timestamp, original_cmd, rtk_cmd, input_tokens, output_tokens,
saved_tokens, savings_pct, exec_time_ms, project_path
FROM commands
WHERE id > ?
ORDER BY id ASC
LIMIT ?`,
);
return stmt.all(sinceId, limit) as RtkCommand[];
}
function fetchStats(db: Database.Database): {
total_commands: number;
total_input: number;
total_output: number;
total_saved: number;
avg_savings_pct: number;
} {
const row = db.prepare(
`SELECT COUNT(*) as total_commands,
COALESCE(SUM(input_tokens), 0) as total_input,
COALESCE(SUM(output_tokens), 0) as total_output,
COALESCE(SUM(saved_tokens), 0) as total_saved,
COALESCE(AVG(savings_pct), 0) as avg_savings_pct
FROM commands`,
).get() as {
total_commands: number;
total_input: number;
total_output: number;
total_saved: number;
avg_savings_pct: number;
};
return row;
}
// ─── TokenVault pusher ─────────────────────────────────────────────────────
async function pushBatch(commands: RtkCommand[]): Promise<boolean> {
try {
const res = await fetch(`${TOKENVAULT_URL}/v1/rtk/stats`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
host: HOST,
commands: commands.map(c => ({
rtk_id: c.id,
timestamp: c.timestamp,
original_cmd: c.original_cmd,
rtk_cmd: c.rtk_cmd,
input_tokens: c.input_tokens,
output_tokens: c.output_tokens,
saved_tokens: c.saved_tokens,
savings_pct: c.savings_pct,
exec_time_ms: c.exec_time_ms,
project_path: c.project_path,
})),
}),
});
if (!res.ok) {
console.error(`TokenVault push failed: ${res.status} ${await res.text()}`);
return false;
}
return true;
} catch (err) {
console.error('TokenVault push error:', err instanceof Error ? err.message : err);
return false;
}
}
// ─── Main sync loop ────────────────────────────────────────────────────────
async function sync(): Promise<void> {
if (!existsSync(RTK_DB_PATH)) {
console.log(`RTK DB not found at ${RTK_DB_PATH} — skipping sync`);
return;
}
const db = new Database(RTK_DB_PATH, { readonly: true, fileMustExist: true });
const state = await loadState();
const stats = fetchStats(db);
console.log(`[${new Date().toISOString()}] TokenVault RTK sync starting`);
console.log(` Host: ${HOST}`);
console.log(` RTK DB: ${RTK_DB_PATH}`);
console.log(` TokenVault: ${TOKENVAULT_URL}`);
console.log(` Last ID: ${state.last_synced_id}`);
console.log(` Total cmds: ${stats.total_commands.toLocaleString()}`);
console.log(` Total saved: ${stats.total_saved.toLocaleString()} tokens (${stats.avg_savings_pct.toFixed(1)}% avg)`);
let totalSynced = 0;
let currentId = state.last_synced_id;
while (true) {
const batch = fetchNewCommands(db, currentId, BATCH_SIZE);
if (batch.length === 0) break;
const success = await pushBatch(batch);
if (!success) {
console.error('Sync aborted — TokenVault unreachable');
break;
}
totalSynced += batch.length;
currentId = batch[batch.length - 1]!.id;
process.stdout.write(`\r Synced: ${totalSynced} commands (up to ID ${currentId})`);
}
db.close();
console.log('');
if (totalSynced > 0) {
await saveState({
last_synced_id: currentId,
last_sync_at: new Date().toISOString(),
host: HOST,
});
console.log(`✓ Synced ${totalSynced} new commands to TokenVault`);
} else {
console.log('✓ Already up to date');
}
}
sync().catch((err) => {
console.error('Fatal sync error:', err);
process.exit(1);
});