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 { 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 { 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 { 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 { 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); });