From a2902161830d944cf1d8f8c24d13cfaf12ea0778 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Tue, 14 Apr 2026 21:08:59 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20RTK=20integration=20=E2=80=94=20sync=20?= =?UTF-8?q?SQLite=20stats=20to=20TokenVault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @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. --- .npmrc | 1 + .pnpmfile.cjs | 1 + ecosystem.config.cjs | 4 +- packages/core/src/config.ts | 1 + packages/core/src/db/rtk-migrate.ts | 38 ++ packages/core/src/providers/index.ts | 73 +++ packages/core/src/routes/rtk.ts | 140 ++++++ packages/core/src/server.ts | 4 + packages/core/src/types.ts | 2 +- packages/dashboard/public/index.html | 708 ++++++++++++++++++++++++++- packages/dashboard/src/server.ts | 16 + packages/rtk-bridge/package.json | 26 + packages/rtk-bridge/src/index.ts | 199 ++++++++ packages/rtk-bridge/tsconfig.json | 8 + packages/rtk-bridge/tsup.config.ts | 12 + pnpm-lock.yaml | 271 ++++++++++ 16 files changed, 1500 insertions(+), 4 deletions(-) create mode 100644 .npmrc create mode 100644 .pnpmfile.cjs create mode 100644 packages/core/src/db/rtk-migrate.ts create mode 100644 packages/core/src/routes/rtk.ts create mode 100644 packages/rtk-bridge/package.json create mode 100644 packages/rtk-bridge/src/index.ts create mode 100644 packages/rtk-bridge/tsconfig.json create mode 100644 packages/rtk-bridge/tsup.config.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..3e775ef --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=true diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 0000000..5beb845 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1 @@ +module.exports = { hooks: {} }; diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 46813f0..d456b01 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -6,7 +6,7 @@ module.exports = { cwd: '/opt/tokenvault', env: { NODE_ENV: 'production', - TOKENVAULT_PORT: '3300', + TOKENVAULT_PORT: '3350', TOKENVAULT_HOST: '0.0.0.0', DB_HOST: '127.0.0.1', DB_PORT: '5432', @@ -34,7 +34,7 @@ module.exports = { env: { NODE_ENV: 'production', PORT: '3301', - TOKENVAULT_CORE_URL: 'http://localhost:3300', + TOKENVAULT_CORE_URL: 'http://localhost:3350', }, instances: 1, exec_mode: 'fork', diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index f70a93a..377e3e3 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -24,5 +24,6 @@ export const config = { groq: { apiKey: process.env['GROQ_API_KEY'] ?? '' }, cerebras: { apiKey: process.env['CEREBRAS_API_KEY'] ?? '' }, ollama: { url: process.env['OLLAMA_URL'] ?? 'http://localhost:11434' }, + aiBridge: { url: process.env['AI_BRIDGE_URL'] ?? 'http://localhost:3250' }, }, } as const; diff --git a/packages/core/src/db/rtk-migrate.ts b/packages/core/src/db/rtk-migrate.ts new file mode 100644 index 0000000..4a33c32 --- /dev/null +++ b/packages/core/src/db/rtk-migrate.ts @@ -0,0 +1,38 @@ +import { query } from './client.js'; +import { logger } from '../observability/logger.js'; + +const RTK_MIGRATIONS = [ + `CREATE TABLE IF NOT EXISTS rtk_commands ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + rtk_id INTEGER NOT NULL, + host TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + original_cmd TEXT NOT NULL, + rtk_cmd TEXT NOT NULL, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + saved_tokens INTEGER NOT NULL DEFAULT 0, + savings_pct NUMERIC(5,2) NOT NULL DEFAULT 0, + exec_time_ms INTEGER NOT NULL DEFAULT 0, + project_path TEXT DEFAULT '', + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(host, rtk_id) + )`, + `CREATE INDEX IF NOT EXISTS idx_rtk_timestamp ON rtk_commands(timestamp DESC)`, + `CREATE INDEX IF NOT EXISTS idx_rtk_host ON rtk_commands(host)`, + `CREATE INDEX IF NOT EXISTS idx_rtk_project ON rtk_commands(project_path)`, +]; + +export async function runRtkMigrations(): Promise { + for (const sql of RTK_MIGRATIONS) { + try { + await query(sql); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (!msg.includes('already exists')) { + logger.error({ err, sql: sql.slice(0, 60) }, 'RTK migration failed'); + } + } + } + logger.info('RTK migrations complete'); +} diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts index 205797c..33a65e4 100644 --- a/packages/core/src/providers/index.ts +++ b/packages/core/src/providers/index.ts @@ -239,6 +239,77 @@ function createOllamaAdapter(): ProviderAdapter { }; } +// ─── AI-Bridge Adapter (Claude Max Flat Fee + ChatGPT + Copilot) ──────────── + +function createAiBridgeAdapter(): ProviderAdapter { + const baseUrl = config.providers.aiBridge.url; + const models: ProviderModel[] = [ + { id: 'claude-code', displayName: 'Claude Code (AI-Bridge)', contextLength: 200_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'premium' }, + { id: 'claude-opus', displayName: 'Claude Opus (AI-Bridge)', contextLength: 200_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'reasoning' }, + { id: 'claude-sonnet', displayName: 'Claude Sonnet (AI-Bridge)', contextLength: 200_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'standard' }, + { id: 'claude-haiku', displayName: 'Claude Haiku (AI-Bridge)', contextLength: 200_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'fast' }, + { id: 'chatgpt', displayName: 'ChatGPT (AI-Bridge)', contextLength: 128_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'standard' }, + { id: 'copilot', displayName: 'Copilot (AI-Bridge)', contextLength: 128_000, inputPricePerMTok: 0, outputPricePerMTok: 0, tier: 'fast' }, + ]; + + return { + name: 'ai-bridge', + displayName: 'AI-Bridge (Claude + ChatGPT + Copilot)', + models, + isConfigured: () => true, + supportsPromptCaching: () => false, + calculateCost: () => 0, // Flat fee — no per-token cost + async chat(request: ChatRequest): Promise { + const start = Date.now(); + const res = await fetch(`${baseUrl}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: request.messages.map(m => { + if (m.role === 'system') return `[System] ${m.content}`; + if (m.role === 'user') return m.content; + return `[Assistant] ${m.content}`; + }).join('\n\n'), + model: request.model, + }), + }); + + if (!res.ok) { + const errText = await res.text(); + throw new Error(`AI-Bridge error ${res.status}: ${errText.slice(0, 200)}`); + } + + const data = await res.json() as { + response: string; + model?: string; + tokens_used?: number; + }; + + // Estimate tokens from text length (AI-Bridge doesn't return exact counts) + const inputText = request.messages.map(m => m.content).join(' '); + const estimatedInputTokens = Math.ceil(inputText.length / 4); + const estimatedOutputTokens = Math.ceil(data.response.length / 4); + + return { + id: randomUUID(), + model: data.model ?? request.model, + provider: 'ai-bridge', + choices: [{ + index: 0, + message: { role: 'assistant', content: data.response }, + finish_reason: 'stop', + }], + usage: { + prompt_tokens: estimatedInputTokens, + completion_tokens: estimatedOutputTokens, + total_tokens: estimatedInputTokens + estimatedOutputTokens, + }, + latency_ms: Date.now() - start, + }; + }, + }; +} + // ─── Provider Registry ────────────────────────────────────────────────────── const adapters: Map = new Map(); @@ -247,10 +318,12 @@ export function initProviders(): void { const anthropic = createAnthropicAdapter(); const openai = createOpenAIAdapter(); const ollama = createOllamaAdapter(); + const aiBridge = createAiBridgeAdapter(); adapters.set('anthropic', anthropic); adapters.set('openai', openai); adapters.set('ollama', ollama); + adapters.set('ai-bridge', aiBridge); const configured = [...adapters.values()].filter(a => a.isConfigured()).map(a => a.name); logger.info({ configured }, 'Provider registry initialized'); diff --git a/packages/core/src/routes/rtk.ts b/packages/core/src/routes/rtk.ts new file mode 100644 index 0000000..3a53751 --- /dev/null +++ b/packages/core/src/routes/rtk.ts @@ -0,0 +1,140 @@ +import type { FastifyInstance } from 'fastify'; +import { query } from '../db/client.js'; +import { logger } from '../observability/logger.js'; + +interface RtkStatsBody { + host: string; + commands: Array<{ + rtk_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; + }>; +} + +export async function rtkRoutes(app: FastifyInstance): Promise { + // ─── Ingest RTK stats from client machines ─────────────────────────── + app.post<{ Body: RtkStatsBody }>('/v1/rtk/stats', async (req, reply) => { + const { host, commands } = req.body; + + if (!host || !Array.isArray(commands)) { + reply.code(400); + return { error: 'host and commands array required' }; + } + + let inserted = 0; + for (const cmd of commands) { + try { + const result = await query( + `INSERT INTO rtk_commands ( + rtk_id, host, timestamp, original_cmd, rtk_cmd, + input_tokens, output_tokens, saved_tokens, savings_pct, + exec_time_ms, project_path + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + ON CONFLICT (host, rtk_id) DO NOTHING`, + [ + cmd.rtk_id, host, cmd.timestamp, cmd.original_cmd, cmd.rtk_cmd, + cmd.input_tokens, cmd.output_tokens, cmd.saved_tokens, cmd.savings_pct, + cmd.exec_time_ms, cmd.project_path, + ], + ); + if (result.rowCount && result.rowCount > 0) inserted += 1; + } catch (err) { + logger.warn({ err, rtk_id: cmd.rtk_id }, 'Failed to insert RTK command'); + } + } + + logger.info({ host, received: commands.length, inserted }, 'RTK stats ingested'); + return { received: commands.length, inserted }; + }); + + // ─── Aggregate stats (all hosts) ────────────────────────────────────── + app.get<{ Querystring: { period?: string } }>('/v1/rtk/stats', async (req) => { + const intervals: Record = { + today: "timestamp >= CURRENT_DATE", + week: "timestamp >= CURRENT_DATE - INTERVAL '7 days'", + month: "timestamp >= CURRENT_DATE - INTERVAL '30 days'", + all: '1=1', + }; + const where = intervals[req.query.period ?? 'all'] ?? intervals['all']!; + + const result = await query<{ + total_commands: string; total_input: string; total_output: string; + total_saved: string; avg_savings: string; hosts: string; projects: string; + }>(`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, + COUNT(DISTINCT host) as hosts, + COUNT(DISTINCT project_path) as projects + FROM rtk_commands WHERE ${where}`); + + const row = result.rows[0]!; + return { + period: req.query.period ?? 'all', + total_commands: parseInt(row.total_commands, 10), + total_input_tokens: parseInt(row.total_input, 10), + total_output_tokens: parseInt(row.total_output, 10), + total_saved_tokens: parseInt(row.total_saved, 10), + avg_savings_pct: parseFloat(row.avg_savings), + unique_hosts: parseInt(row.hosts, 10), + unique_projects: parseInt(row.projects, 10), + }; + }); + + // ─── Top commands by savings ────────────────────────────────────────── + app.get('/v1/rtk/commands', async () => { + const result = await query<{ + cmd: string; count: string; saved: string; avg_pct: string; + }>(`SELECT + split_part(rtk_cmd, ' ', 1) || ' ' || split_part(rtk_cmd, ' ', 2) as cmd, + COUNT(*) as count, + COALESCE(SUM(saved_tokens), 0) as saved, + COALESCE(AVG(savings_pct), 0) as avg_pct + FROM rtk_commands + WHERE timestamp >= CURRENT_DATE - INTERVAL '30 days' + GROUP BY cmd + ORDER BY saved DESC + LIMIT 20`); + + return { + commands: result.rows.map(r => ({ + cmd: r.cmd, + count: parseInt(r.count, 10), + saved_tokens: parseInt(r.saved, 10), + avg_savings_pct: parseFloat(r.avg_pct), + })), + }; + }); + + // ─── Per-host breakdown ─────────────────────────────────────────────── + app.get('/v1/rtk/hosts', async () => { + const result = await query<{ + host: string; count: string; saved: string; last_seen: string; + }>(`SELECT + host, + COUNT(*) as count, + COALESCE(SUM(saved_tokens), 0) as saved, + MAX(timestamp) as last_seen + FROM rtk_commands + GROUP BY host + ORDER BY saved DESC`); + + return { + hosts: result.rows.map(r => ({ + host: r.host, + commands: parseInt(r.count, 10), + saved_tokens: parseInt(r.saved, 10), + last_seen: r.last_seen, + })), + }; + }); +} diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 20e460c..6427be0 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -3,11 +3,13 @@ import cors from '@fastify/cors'; import { config } from './config.js'; import { logger } from './observability/logger.js'; import { runMigrations } from './db/migrate.js'; +import { runRtkMigrations } from './db/rtk-migrate.js'; import { closePool } from './db/client.js'; import { initProviders } from './providers/index.js'; import { healthRoutes } from './routes/health.js'; import { proxyRoutes } from './routes/proxy.js'; import { ticketRoutes } from './routes/tickets.js'; +import { rtkRoutes } from './routes/rtk.js'; const app = Fastify({ logger: false, @@ -19,6 +21,7 @@ await app.register(cors, { origin: true }); await app.register(healthRoutes); await app.register(proxyRoutes); await app.register(ticketRoutes); +await app.register(rtkRoutes); // ─── Startup ───────────────────────────────────────────────────────────────── async function startup(): Promise { @@ -26,6 +29,7 @@ async function startup(): Promise { try { await runMigrations(); + await runRtkMigrations(); } catch (err) { logger.error({ err }, 'DB migration failed — proceeding in degraded mode'); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 31ad9da..aa61993 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,6 +1,6 @@ // ─── Provider Types ──────────────────────────────────────────────────────── -export type ProviderName = 'anthropic' | 'openai' | 'google' | 'mistral' | 'groq' | 'cerebras' | 'ollama'; +export type ProviderName = 'anthropic' | 'openai' | 'google' | 'mistral' | 'groq' | 'cerebras' | 'ollama' | 'ai-bridge'; export type TicketStatus = 'completed' | 'cached' | 'failed' | 'pending_review'; diff --git a/packages/dashboard/public/index.html b/packages/dashboard/public/index.html index a6a6423..457d242 100644 --- a/packages/dashboard/public/index.html +++ b/packages/dashboard/public/index.html @@ -91,6 +91,46 @@ tr:hover { background:#f8fafc; } /* Refresh button */ .refresh { padding:6px 14px; background:var(--primary); color:#fff; border:none; border-radius:8px; cursor:pointer; font-size:13px; font-weight:500; } .refresh:hover { background:var(--primary-dark); } + +/* Settings */ +.settings-grid { display:grid; grid-template-columns:240px 1fr; gap:0; background:var(--surface); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid var(--border); min-height:600px; } +@media (max-width: 768px) { .settings-grid { grid-template-columns:1fr; } } +.settings-nav { border-right:1px solid var(--border); padding:8px 0; } +.settings-nav-item { padding:10px 20px; cursor:pointer; font-size:13px; font-weight:500; color:var(--text-muted); display:flex; align-items:center; gap:8px; transition:all 0.15s; } +.settings-nav-item:hover { background:#f1f5f9; color:var(--text); } +.settings-nav-item.active { background:#eef2ff; color:var(--primary); border-right:2px solid var(--primary); font-weight:600; } +.settings-panel { padding:24px; } +.settings-panel h3 { font-size:16px; font-weight:600; margin-bottom:4px; } +.settings-panel .desc { font-size:13px; color:var(--text-muted); margin-bottom:20px; } +.form-group { margin-bottom:20px; } +.form-group label { display:block; font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); margin-bottom:6px; } +.form-group input, .form-group select, .form-group textarea { width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:8px; font-size:14px; background:var(--bg); transition:border-color 0.15s; } +.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline:none; border-color:var(--primary); box-shadow:0 0 0 3px rgba(99,102,241,0.1); } +.form-group .hint { font-size:11px; color:var(--text-muted); margin-top:4px; } +.form-group input[type="password"] { font-family:monospace; letter-spacing:2px; } +.form-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; } +@media (max-width: 768px) { .form-row { grid-template-columns:1fr; } } +.btn { padding:8px 16px; border:1px solid var(--border); border-radius:8px; font-size:13px; font-weight:500; cursor:pointer; transition:all 0.15s; } +.btn-primary { background:var(--primary); color:#fff; border-color:var(--primary); } +.btn-primary:hover { background:var(--primary-dark); } +.btn-danger { background:var(--red); color:#fff; border-color:var(--red); } +.btn-outline { background:var(--surface); color:var(--text); } +.btn-outline:hover { background:#f1f5f9; } +.btn-group { display:flex; gap:8px; margin-top:16px; } +.toggle { position:relative; display:inline-block; width:44px; height:24px; } +.toggle input { opacity:0; width:0; height:0; } +.toggle .slider { position:absolute; inset:0; background:#cbd5e1; border-radius:24px; cursor:pointer; transition:0.2s; } +.toggle .slider:before { content:''; position:absolute; height:18px; width:18px; left:3px; bottom:3px; background:#fff; border-radius:50%; transition:0.2s; } +.toggle input:checked + .slider { background:var(--primary); } +.toggle input:checked + .slider:before { transform:translateX(20px); } +.divider { height:1px; background:var(--border); margin:24px 0; } +.status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; } +.status-dot.green { background:var(--green); } +.status-dot.red { background:var(--red); } +.status-dot.amber { background:var(--amber); } +.key-mask { font-family:monospace; font-size:13px; color:var(--text-muted); } +.provider-card { background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); padding:16px; margin-bottom:12px; } +.provider-card h4 { font-size:14px; font-weight:600; margin-bottom:8px; display:flex; align-items:center; gap:6px; } @@ -111,6 +151,8 @@ tr:hover { background:#f8fafc; }
Tickets
Cost Analysis
Providers
+
RTK Savings
+
⚙ Settings
@@ -175,6 +217,69 @@ tr:hover { background:#f8fafc; }
+ + + + + + diff --git a/packages/dashboard/src/server.ts b/packages/dashboard/src/server.ts index ce789dc..3c58580 100644 --- a/packages/dashboard/src/server.ts +++ b/packages/dashboard/src/server.ts @@ -47,5 +47,21 @@ app.get('/api/cost/breakdown', async (req) => { return res.json(); }); +app.get('/api/rtk/stats', async (req) => { + const qs = new URL(req.url, 'http://localhost').search; + const res = await fetch(`${CORE_URL}/v1/rtk/stats${qs}`); + return res.json(); +}); + +app.get('/api/rtk/commands', async () => { + const res = await fetch(`${CORE_URL}/v1/rtk/commands`); + return res.json(); +}); + +app.get('/api/rtk/hosts', async () => { + const res = await fetch(`${CORE_URL}/v1/rtk/hosts`); + return res.json(); +}); + await app.listen({ port: PORT, host: '0.0.0.0' }); console.log(`TokenVault Dashboard running on http://localhost:${PORT}`); diff --git a/packages/rtk-bridge/package.json b/packages/rtk-bridge/package.json new file mode 100644 index 0000000..6cf7f51 --- /dev/null +++ b/packages/rtk-bridge/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tokenvault/rtk-bridge", + "version": "0.1.0", + "description": "TokenVault ↔ RTK bridge — syncs local RTK SQLite stats to TokenVault PostgreSQL", + "type": "module", + "bin": { + "tokenvault-rtk-sync": "dist/index.js" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsup", + "dev": "tsx watch src/index.ts", + "sync": "tsx src/index.ts", + "clean": "rm -rf dist" + }, + "dependencies": { + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "tsup": "^8.4.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "@types/node": "^22.0.0", + "@types/better-sqlite3": "^7.6.0" + } +} diff --git a/packages/rtk-bridge/src/index.ts b/packages/rtk-bridge/src/index.ts new file mode 100644 index 0000000..3f0d72e --- /dev/null +++ b/packages/rtk-bridge/src/index.ts @@ -0,0 +1,199 @@ +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); +}); diff --git a/packages/rtk-bridge/tsconfig.json b/packages/rtk-bridge/tsconfig.json new file mode 100644 index 0000000..a086b14 --- /dev/null +++ b/packages/rtk-bridge/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/rtk-bridge/tsup.config.ts b/packages/rtk-bridge/tsup.config.ts new file mode 100644 index 0000000..c1e24db --- /dev/null +++ b/packages/rtk-bridge/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'node20', + outDir: 'dist', + clean: true, + sourcemap: true, + dts: true, + banner: { js: '#!/usr/bin/env node' }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7bf518..8065337 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,8 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +pnpmfileChecksum: sha256-dS522kUCN9FHUHk8JODaJjlMeNKVaNv8hwk1JhcGjEY= + importers: .: {} @@ -120,6 +122,28 @@ importers: specifier: ^3.1.0 version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0) + packages/rtk-bridge: + dependencies: + better-sqlite3: + specifier: ^11.7.0 + version: 11.10.0 + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.0 + version: 7.6.13 + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + tsup: + specifier: ^8.4.0 + version: 8.5.1(postcss@8.5.9)(tsx@4.21.0)(typescript@5.9.3) + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + packages: '@esbuild/aix-ppc64@0.27.7': @@ -486,6 +510,9 @@ packages: cpu: [x64] os: [win32] + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -571,6 +598,18 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -579,6 +618,9 @@ packages: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -613,6 +655,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -665,10 +710,18 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -677,6 +730,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -688,6 +745,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -726,6 +786,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -773,6 +837,9 @@ packages: picomatch: optional: true + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -796,6 +863,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -815,6 +885,9 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -845,9 +918,15 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -938,14 +1017,24 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -965,10 +1054,17 @@ packages: engines: {node: ^18 || >=20} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1113,6 +1209,12 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} @@ -1123,6 +1225,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + qs@6.15.1: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} @@ -1138,6 +1243,14 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1244,6 +1357,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -1269,6 +1388,13 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -1277,6 +1403,13 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1348,6 +1481,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -1367,6 +1503,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1717,6 +1856,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 22.19.17 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1811,6 +1954,23 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -1829,6 +1989,11 @@ snapshots: dependencies: balanced-match: 4.0.4 + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -1862,6 +2027,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@1.1.4: {} + commander@4.1.1: {} confbox@0.1.8: {} @@ -1897,12 +2064,20 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} + depd@2.0.0: {} dequal@2.0.3: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1913,6 +2088,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -1966,6 +2145,8 @@ snapshots: dependencies: eventsource-parser: 3.0.6 + expand-template@2.0.3: {} + expect-type@1.3.0: {} express-rate-limit@8.3.2(express@5.2.1): @@ -2053,6 +2234,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-uri-to-path@1.0.0: {} + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -2085,6 +2268,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fsevents@2.3.3: optional: true @@ -2112,6 +2297,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} + glob@11.1.0: dependencies: foreground-child: 3.3.1 @@ -2143,8 +2330,12 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + inherits@2.0.4: {} + ini@1.3.8: {} + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -2207,12 +2398,18 @@ snapshots: mime@3.0.0: {} + mimic-response@3.1.0: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 + minimist@1.2.8: {} + minipass@7.1.3: {} + mkdirp-classic@0.5.3: {} + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -2232,8 +2429,14 @@ snapshots: nanoid@5.1.7: {} + napi-build-utils@2.0.0: {} + negotiator@1.0.0: {} + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -2357,6 +2560,21 @@ snapshots: dependencies: xtend: 4.0.2 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.89.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + process-warning@4.0.1: {} process-warning@5.0.0: {} @@ -2366,6 +2584,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + qs@6.15.1: dependencies: side-channel: 1.1.0 @@ -2381,6 +2604,19 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} real-require@0.2.0: {} @@ -2519,6 +2755,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -2535,6 +2779,12 @@ snapshots: std-env@3.10.0: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -2549,6 +2799,21 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -2619,6 +2884,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -2633,6 +2902,8 @@ snapshots: unpipe@1.0.0: {} + util-deprecate@1.0.2: {} + vary@1.1.2: {} vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0):