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.
This commit is contained in:
parent
d43b9f5298
commit
a290216183
1
.pnpmfile.cjs
Normal file
1
.pnpmfile.cjs
Normal file
@ -0,0 +1 @@
|
||||
module.exports = { hooks: {} };
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
38
packages/core/src/db/rtk-migrate.ts
Normal file
38
packages/core/src/db/rtk-migrate.ts
Normal file
@ -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<void> {
|
||||
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');
|
||||
}
|
||||
@ -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<ChatResponse> {
|
||||
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<ProviderName, ProviderAdapter> = 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');
|
||||
|
||||
140
packages/core/src/routes/rtk.ts
Normal file
140
packages/core/src/routes/rtk.ts
Normal file
@ -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<void> {
|
||||
// ─── 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<string, string> = {
|
||||
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,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -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<void> {
|
||||
@ -26,6 +29,7 @@ async function startup(): Promise<void> {
|
||||
|
||||
try {
|
||||
await runMigrations();
|
||||
await runRtkMigrations();
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'DB migration failed — proceeding in degraded mode');
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -111,6 +151,8 @@ tr:hover { background:#f8fafc; }
|
||||
<div class="tab" data-tab="tickets">Tickets</div>
|
||||
<div class="tab" data-tab="cost">Cost Analysis</div>
|
||||
<div class="tab" data-tab="providers">Providers</div>
|
||||
<div class="tab" data-tab="rtk">RTK Savings</div>
|
||||
<div class="tab" data-tab="settings">⚙ Settings</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@ -175,6 +217,69 @@ tr:hover { background:#f8fafc; }
|
||||
<div id="provider-list" class="cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RTK Savings Tab -->
|
||||
<div id="tab-rtk" style="display:none">
|
||||
<div class="cards" id="rtk-stats-cards">
|
||||
<div class="loading"><div class="spin"></div> Loading RTK stats...</div>
|
||||
</div>
|
||||
<div class="chart-row">
|
||||
<div class="section" style="margin:0">
|
||||
<h2>Top Commands by Savings</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Command</th><th>Count</th><th>Saved</th><th>Avg %</th></tr></thead>
|
||||
<tbody id="rtk-commands-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="margin:0">
|
||||
<h2>Hosts</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Host</th><th>Commands</th><th>Saved</th><th>Last Seen</th></tr></thead>
|
||||
<tbody id="rtk-hosts-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>About RTK Integration</h2>
|
||||
<div class="card">
|
||||
<p style="font-size:13px;line-height:1.6;color:var(--text-muted)">
|
||||
<strong style="color:var(--text)">RTK (Rust Token Killer)</strong> — MIT-licensed open-source tool that reduces LLM token consumption by 60-90% through CLI output compression.
|
||||
TokenVault ingests RTK's local SQLite stats from every machine you run the <code>tokenvault-rtk-sync</code> agent on, providing cross-machine aggregation, team/project attribution, and long-term cost trend analysis.
|
||||
</p>
|
||||
<p style="font-size:12px;margin-top:12px">
|
||||
<strong>Setup on a new machine:</strong><br>
|
||||
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">brew install rtk</code> ·
|
||||
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">npm i -g @tokenvault/rtk-bridge</code> ·
|
||||
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">tokenvault-rtk-sync</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div id="tab-settings" style="display:none">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-nav">
|
||||
<div class="settings-nav-item active" data-settings="providers" onclick="showSettings('providers')">LLM Providers</div>
|
||||
<div class="settings-nav-item" data-settings="budgets" onclick="showSettings('budgets')">Budgets & Alerts</div>
|
||||
<div class="settings-nav-item" data-settings="cache" onclick="showSettings('cache')">Semantic Cache</div>
|
||||
<div class="settings-nav-item" data-settings="compression" onclick="showSettings('compression')">Compression</div>
|
||||
<div class="settings-nav-item" data-settings="routing" onclick="showSettings('routing')">Model Routing</div>
|
||||
<div class="settings-nav-item" data-settings="governance" onclick="showSettings('governance')">Governance & RBAC</div>
|
||||
<div class="settings-nav-item" data-settings="notifications" onclick="showSettings('notifications')">Notifications</div>
|
||||
<div class="settings-nav-item" data-settings="database" onclick="showSettings('database')">Database</div>
|
||||
<div class="settings-nav-item" data-settings="general" onclick="showSettings('general')">General</div>
|
||||
<div class="settings-nav-item" data-settings="about" onclick="showSettings('about')">About</div>
|
||||
</div>
|
||||
<div class="settings-panel" id="settings-content">
|
||||
<!-- Default: Providers -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -278,9 +383,610 @@ async function loadProviders() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadAll() { loadStats(); loadTickets(); loadBreakdown(); loadProviders(); }
|
||||
async function loadRtk() {
|
||||
try {
|
||||
const [stats, commands, hosts] = await Promise.all([
|
||||
(await fetch(API + '/rtk/stats?period=all')).json(),
|
||||
(await fetch(API + '/rtk/commands')).json(),
|
||||
(await fetch(API + '/rtk/hosts')).json(),
|
||||
]);
|
||||
|
||||
document.getElementById('rtk-stats-cards').innerHTML = `
|
||||
<div class="card"><div class="label">Total Saved</div><div class="value green">${fmtK(stats.total_saved_tokens)}</div><div class="sub">tokens across all hosts</div></div>
|
||||
<div class="card"><div class="label">Commands</div><div class="value primary">${fmtK(stats.total_commands)}</div><div class="sub">via RTK</div></div>
|
||||
<div class="card"><div class="label">Avg Savings</div><div class="value amber">${stats.avg_savings_pct.toFixed(1)}%</div><div class="sub">per command</div></div>
|
||||
<div class="card"><div class="label">Input Tokens</div><div class="value">${fmtK(stats.total_input_tokens)}</div><div class="sub">before RTK</div></div>
|
||||
<div class="card"><div class="label">Output Tokens</div><div class="value">${fmtK(stats.total_output_tokens)}</div><div class="sub">after RTK</div></div>
|
||||
<div class="card"><div class="label">Hosts</div><div class="value">${stats.unique_hosts}</div><div class="sub">machines tracked</div></div>
|
||||
`;
|
||||
|
||||
document.getElementById('rtk-commands-rows').innerHTML = commands.commands.map(c => `
|
||||
<tr>
|
||||
<td><code style="font-size:12px">${c.cmd}</code></td>
|
||||
<td>${c.count}</td>
|
||||
<td style="color:var(--green)">${fmtK(c.saved_tokens)}</td>
|
||||
<td>${c.avg_savings_pct.toFixed(1)}%</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="4" style="text-align:center;color:var(--text-muted)">No RTK data yet — run <code>tokenvault-rtk-sync</code> to ingest</td></tr>';
|
||||
|
||||
document.getElementById('rtk-hosts-rows').innerHTML = hosts.hosts.map(h => `
|
||||
<tr>
|
||||
<td><strong>${h.host}</strong></td>
|
||||
<td>${fmtK(h.commands)}</td>
|
||||
<td style="color:var(--green)">${fmtK(h.saved_tokens)}</td>
|
||||
<td style="font-size:11px;color:var(--text-muted)">${fmtTime(h.last_seen)}</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="4" style="text-align:center;color:var(--text-muted)">No hosts synced yet</td></tr>';
|
||||
} catch (err) {
|
||||
document.getElementById('rtk-stats-cards').innerHTML = '<div class="card"><div class="label">RTK</div><div class="value">Offline</div><div class="sub">Core endpoint unreachable</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll() { loadStats(); loadTickets(); loadBreakdown(); loadProviders(); loadRtk(); }
|
||||
loadAll();
|
||||
setInterval(loadAll, 30000);
|
||||
|
||||
// ─── Settings ──────────────────────────────────────────────────────────────
|
||||
|
||||
const settingsPages = {
|
||||
providers: () => `
|
||||
<h3>${lang==='de'?'LLM Provider Konfiguration':'LLM Provider Configuration'}</h3>
|
||||
<p class="desc">${lang==='de'?'API-Schlüssel für jeden Provider. Keys werden serverseitig gespeichert und nie im Browser angezeigt.':'API keys for each provider. Keys are stored server-side and never shown in the browser.'}</p>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot" id="dot-anthropic"></span> Anthropic Claude</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="key-anthropic" placeholder="sk-ant-..." />
|
||||
<div class="hint">Claude Opus, Sonnet, Haiku — api.anthropic.com</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
|
||||
<select id="model-anthropic">
|
||||
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
<option value="claude-haiku-3-20250630">Claude Haiku 3</option>
|
||||
<option value="claude-opus-4-20250514">Claude Opus 4</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot" id="dot-openai"></span> OpenAI</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="key-openai" placeholder="sk-..." />
|
||||
<div class="hint">GPT-4o, GPT-4o-mini, o1 — api.openai.com</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
|
||||
<select id="model-openai">
|
||||
<option value="gpt-4o">GPT-4o</option>
|
||||
<option value="gpt-4o-mini">GPT-4o Mini</option>
|
||||
<option value="o1">o1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot" id="dot-google"></span> Google Gemini</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="key-google" placeholder="AIza..." />
|
||||
<div class="hint">Gemini 2.0 Flash, Gemini 2.5 Pro</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
|
||||
<select id="model-google">
|
||||
<option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
|
||||
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot" id="dot-mistral"></span> Mistral AI</h4>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="key-mistral" placeholder="..." />
|
||||
<div class="hint">Mistral Large, Mistral Small</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot" id="dot-groq"></span> Groq</h4>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="key-groq" placeholder="gsk_..." />
|
||||
<div class="hint">${lang==='de'?'Kostenlos — Llama 3.3 70B, Gemma':'Free tier — Llama 3.3 70B, Gemma'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-card">
|
||||
<h4><span class="status-dot green"></span> Ollama (Local)</h4>
|
||||
<div class="form-group">
|
||||
<label>Ollama URL</label>
|
||||
<input type="text" id="url-ollama" placeholder="http://localhost:11434" />
|
||||
<div class="hint">${lang==='de'?'Lokale Modelle — kostenlos, keine API-Keys nötig':'Local models — free, no API keys needed'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('providers')">${lang==='de'?'Speichern':'Save Changes'}</button>
|
||||
<button class="btn btn-outline" onclick="testProviders()">${lang==='de'?'Verbindung testen':'Test Connections'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
budgets: () => `
|
||||
<h3>${lang==='de'?'Budgets & Alerts':'Budgets & Alerts'}</h3>
|
||||
<p class="desc">${lang==='de'?'Setze Ausgabenlimits pro Projekt, Team oder global. Bei Überschreitung werden Alerts ausgelöst oder Requests pausiert.':'Set spending limits per project, team, or globally. Exceeding triggers alerts or pauses requests.'}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Globales Monatslimit (USD)':'Global Monthly Limit (USD)'}</label>
|
||||
<input type="number" id="budget-global" placeholder="100.00" step="0.01" />
|
||||
<div class="hint">${lang==='de'?'Alle Provider zusammen. 0 = kein Limit.':'All providers combined. 0 = no limit.'}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Alert-Schwelle':'Alert Threshold'}</label>
|
||||
<select id="budget-threshold">
|
||||
<option value="0.5">50%</option>
|
||||
<option value="0.7">70%</option>
|
||||
<option value="0.8" selected>80%</option>
|
||||
<option value="0.9">90%</option>
|
||||
<option value="0.95">95%</option>
|
||||
</select>
|
||||
<div class="hint">${lang==='de'?'Alert bei diesem Prozentsatz des Limits':'Alert at this percentage of limit'}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Hard-Limit':'Hard Limit'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="budget-hard"><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Requests bei 100% pausieren':'Pause requests at 100%'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Projekt-Budgets':'Project Budgets'}</h3>
|
||||
<div id="project-budgets">
|
||||
<div class="form-row" style="margin-bottom:8px">
|
||||
<div class="form-group"><label>Projekt</label><input type="text" placeholder="eo-global-pulse" /></div>
|
||||
<div class="form-group"><label>Limit (USD/Monat)</label><input type="number" placeholder="25.00" step="0.01" /></div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:8px">
|
||||
<div class="form-group"><input type="text" placeholder="tip-platform" /></div>
|
||||
<div class="form-group"><input type="number" placeholder="50.00" step="0.01" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline" onclick="addProjectBudget()">+ ${lang==='de'?'Projekt hinzufügen':'Add Project'}</button>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Anomalie-Erkennung':'Anomaly Detection'}</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Sigma-Schwelle':'Sigma Threshold'}</label>
|
||||
<select id="anomaly-sigma">
|
||||
<option value="2">2σ (${lang==='de'?'Empfindlich':'Sensitive'})</option>
|
||||
<option value="3" selected>3σ (Standard)</option>
|
||||
<option value="4">4σ (${lang==='de'?'Nur Extremfälle':'Extreme only'})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Anomalie-Erkennung aktiv':'Anomaly Detection Active'}</label>
|
||||
<div style="padding-top:4px"><label class="toggle"><input type="checkbox" id="anomaly-enabled" checked><span class="slider"></span></label></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('budgets')">${lang==='de'?'Speichern':'Save Changes'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
cache: () => `
|
||||
<h3>Semantic Cache</h3>
|
||||
<p class="desc">${lang==='de'?'Qdrant-basierter Cache. Ähnliche Anfragen werden aus dem Cache beantwortet statt die API erneut aufzurufen.':'Qdrant-powered cache. Similar requests are answered from cache instead of calling the API again.'}</p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Cache aktiv':'Cache Enabled'}</label>
|
||||
<div style="padding-top:4px"><label class="toggle"><input type="checkbox" id="cache-enabled" checked><span class="slider"></span></label></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Ähnlichkeitsschwelle':'Similarity Threshold'}</label>
|
||||
<input type="number" id="cache-threshold" value="0.95" min="0.80" max="0.99" step="0.01" />
|
||||
<div class="hint">${lang==='de'?'0.95 = 95% Ähnlichkeit für Cache-Hit':'0.95 = 95% similarity for cache hit'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Cache TTL</label>
|
||||
<select id="cache-ttl">
|
||||
<option value="3600">1 ${lang==='de'?'Stunde':'hour'}</option>
|
||||
<option value="86400" selected>24 ${lang==='de'?'Stunden':'hours'}</option>
|
||||
<option value="604800">7 ${lang==='de'?'Tage':'days'}</option>
|
||||
<option value="2592000">30 ${lang==='de'?'Tage':'days'}</option>
|
||||
<option value="0">${lang==='de'?'Kein Ablauf':'No expiry'}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Qdrant URL</label>
|
||||
<input type="text" id="cache-qdrant" placeholder="http://localhost:6333" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Qdrant Collection</label>
|
||||
<input type="text" id="cache-collection" placeholder="tokenvault_cache" />
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('cache')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
<button class="btn btn-outline" onclick="flushCache()">${lang==='de'?'Cache leeren':'Flush Cache'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
compression: () => `
|
||||
<h3>${lang==='de'?'Kompression':'Compression'}</h3>
|
||||
<p class="desc">${lang==='de'?'AST-basierte Token-Kompression für Code und CLI-Output. Reduziert Token-Verbrauch um 60-90%.':'AST-based token compression for code and CLI output. Reduces token usage by 60-90%.'}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Auto-Modus':'Auto Mode'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="compress-auto" checked><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Automatisch besten Modus wählen (Entropy-Analyse)':'Automatically select best mode (entropy analysis)'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Standard-Modus (wenn Auto aus)':'Default Mode (when auto off)'}</label>
|
||||
<select id="compress-mode">
|
||||
<option value="full">Full (${lang==='de'?'keine Kompression':'no compression'})</option>
|
||||
<option value="signatures" selected>Signatures (AST)</option>
|
||||
<option value="map">Map (${lang==='de'?'Abhängigkeitsgraph':'dependency graph'})</option>
|
||||
<option value="aggressive">Aggressive</option>
|
||||
<option value="entropy">Entropy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Shell-Hooks aktiv':'Shell Hooks Active'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="compress-shell" checked><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'90+ Patterns für git, npm, docker, cargo, kubectl...':'90+ patterns for git, npm, docker, cargo, kubectl...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Prompt Caching':'Prompt Caching'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="compress-prompt-cache" checked><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Anthropic cache_control + OpenAI automatisches Caching orchestrieren':'Orchestrate Anthropic cache_control + OpenAI automatic caching'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('compression')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
routing: () => `
|
||||
<h3>${lang==='de'?'Model Routing':'Model Routing'}</h3>
|
||||
<p class="desc">${lang==='de'?'Automatisches Routing zum günstigsten/schnellsten Modell basierend auf Anfragekomplexität.':'Automatic routing to cheapest/fastest model based on request complexity.'}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Routing-Strategie':'Routing Strategy'}</label>
|
||||
<select id="route-strategy">
|
||||
<option value="cost">${lang==='de'?'Günstigstes Modell':'Cheapest model'}</option>
|
||||
<option value="quality">${lang==='de'?'Bestes Modell':'Best model'}</option>
|
||||
<option value="balanced" selected>${lang==='de'?'Balanced (Kosten vs. Qualität)':'Balanced (cost vs. quality)'}</option>
|
||||
<option value="latency">${lang==='de'?'Schnellstes Modell':'Fastest model'}</option>
|
||||
<option value="manual">${lang==='de'?'Manuell (kein Auto-Routing)':'Manual (no auto-routing)'}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Fallback-Kette':'Fallback Chain'}</label>
|
||||
<textarea id="route-fallback" rows="4" placeholder="anthropic/claude-sonnet-4-20250514 openai/gpt-4o ollama/qwen2.5:14b groq/llama-3.3-70b"></textarea>
|
||||
<div class="hint">${lang==='de'?'Ein Modell pro Zeile. Bei Ausfall wird das nächste verwendet.':'One model per line. Next is used on failure.'}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Max Tokens pro Request':'Max Tokens per Request'}</label>
|
||||
<input type="number" id="route-max-tokens" value="4096" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Standard-Temperatur':'Default Temperature'}</label>
|
||||
<input type="number" id="route-temp" value="0.7" min="0" max="2" step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('routing')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
governance: () => `
|
||||
<h3>${lang==='de'?'Governance & Zugriffskontrolle':'Governance & Access Control'}</h3>
|
||||
<p class="desc">${lang==='de'?'Wer darf welches Modell nutzen? Rate Limits pro Team/User.':'Who can use which model? Rate limits per team/user.'}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'RBAC aktiv':'RBAC Enabled'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="gov-rbac"><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Rollenbasierte Zugriffskontrolle aktivieren':'Enable role-based access control'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Erlaubte Modell-Tiers':'Allowed Model Tiers'}</label>
|
||||
<div style="display:flex;gap:16px;padding-top:4px">
|
||||
<label style="font-size:13px"><input type="checkbox" checked> Fast (Haiku, Mini)</label>
|
||||
<label style="font-size:13px"><input type="checkbox" checked> Standard (Sonnet, GPT-4o)</label>
|
||||
<label style="font-size:13px"><input type="checkbox"> Premium (Opus, o1)</label>
|
||||
<label style="font-size:13px"><input type="checkbox"> Reasoning (o1)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Rate Limit (${lang==='de'?'Requests/Minute':'Requests/Minute'})</label>
|
||||
<input type="number" id="gov-rate" value="60" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Max Token/Request':'Max Tokens/Request'}</label>
|
||||
<input type="number" id="gov-max-tokens" value="8192" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Audit Trail':'Audit Trail'}</h3>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Audit-Logging':'Audit Logging'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="gov-audit" checked><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Alle Aktionen in immutablem Log speichern (GDPR/SOC2)':'Log all actions to immutable audit trail (GDPR/SOC2)'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'PII-Scrubbing':'PII Scrubbing'}</label>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
|
||||
<label class="toggle"><input type="checkbox" id="gov-pii" checked><span class="slider"></span></label>
|
||||
<span style="font-size:13px">${lang==='de'?'Persönliche Daten aus Ticket-Logs entfernen':'Remove personal data from ticket logs'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('governance')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
notifications: () => `
|
||||
<h3>${lang==='de'?'Benachrichtigungen':'Notifications'}</h3>
|
||||
<p class="desc">${lang==='de'?'Werde benachrichtigt bei Budget-Überschreitungen, Anomalien und Ausfällen.':'Get notified on budget exceedances, anomalies, and outages.'}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'E-Mail Benachrichtigungen':'Email Notifications'}</label>
|
||||
<input type="email" id="notify-email" placeholder="rf@context-x.org" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Webhook URL</label>
|
||||
<input type="url" id="notify-webhook" placeholder="https://hooks.slack.com/..." />
|
||||
<div class="hint">${lang==='de'?'Slack, Teams, Discord oder benutzerdefinierter Webhook':'Slack, Teams, Discord, or custom webhook'}</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Benachrichtigungs-Events':'Notification Events'}</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Budget-Alert (Schwelle erreicht)':'Budget alert (threshold reached)'}</label>
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Budget-Limit (100% erreicht)':'Budget limit (100% reached)'}</label>
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Kosten-Anomalie erkannt':'Cost anomaly detected'}</label>
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Provider-Ausfall':'Provider outage'}</label>
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Täglicher Kostenreport':'Daily cost report'}</label>
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Wöchentlicher Sparreport':'Weekly savings report'}</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('notifications')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
<button class="btn btn-outline" onclick="testNotification()">${lang==='de'?'Test senden':'Send Test'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
database: () => `
|
||||
<h3>${lang==='de'?'Datenbank':'Database'}</h3>
|
||||
<p class="desc">${lang==='de'?'PostgreSQL + TimescaleDB Konfiguration für Ticket-Speicherung und Zeitreihen.':'PostgreSQL + TimescaleDB configuration for ticket storage and time-series.'}</p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Host</label>
|
||||
<input type="text" id="db-host" placeholder="127.0.0.1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Port</label>
|
||||
<input type="number" id="db-port" placeholder="5432" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Database</label>
|
||||
<input type="text" id="db-name" placeholder="tokenvault" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>User</label>
|
||||
<input type="text" id="db-user" placeholder="tokenvault" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Datenbereinigung':'Data Retention'}</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Tickets aufbewahren':'Retain Tickets'}</label>
|
||||
<select id="db-retention">
|
||||
<option value="30">30 ${lang==='de'?'Tage':'days'}</option>
|
||||
<option value="90" selected>90 ${lang==='de'?'Tage':'days'}</option>
|
||||
<option value="365">1 ${lang==='de'?'Jahr':'year'}</option>
|
||||
<option value="0">${lang==='de'?'Unbegrenzt':'Forever'}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Audit-Logs aufbewahren':'Retain Audit Logs'}</label>
|
||||
<select id="db-audit-retention">
|
||||
<option value="365" selected>1 ${lang==='de'?'Jahr':'year'}</option>
|
||||
<option value="730">2 ${lang==='de'?'Jahre':'years'}</option>
|
||||
<option value="0">${lang==='de'?'Unbegrenzt':'Forever'}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('database')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
<button class="btn btn-outline" onclick="testDb()">${lang==='de'?'Verbindung testen':'Test Connection'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
general: () => `
|
||||
<h3>${lang==='de'?'Allgemein':'General'}</h3>
|
||||
<p class="desc">${lang==='de'?'Globale Einstellungen für den TokenVault Server.':'Global settings for the TokenVault server.'}</p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Server-Port':'Server Port'}</label>
|
||||
<input type="number" id="gen-port" placeholder="3350" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Dashboard-Port':'Dashboard Port'}</label>
|
||||
<input type="number" id="gen-dash-port" placeholder="3301" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Sprache':'Language'}</label>
|
||||
<select id="gen-lang">
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Auto-Refresh Intervall':'Auto-Refresh Interval'}</label>
|
||||
<select id="gen-refresh">
|
||||
<option value="10">10s</option>
|
||||
<option value="30" selected>30s</option>
|
||||
<option value="60">60s</option>
|
||||
<option value="0">${lang==='de'?'Aus':'Off'}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${lang==='de'?'Theme':'Theme'}</label>
|
||||
<select id="gen-theme">
|
||||
<option value="light" selected>Light (MAGATAMA)</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Gefährliche Zone':'Danger Zone'}</h3>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger" onclick="if(confirm('${lang==='de'?'Alle Tickets löschen?':'Delete all tickets?'}')) resetData()">${lang==='de'?'Alle Daten zurücksetzen':'Reset All Data'}</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveSettings('general')">${lang==='de'?'Speichern':'Save'}</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
about: () => `
|
||||
<h3>TokenVault</h3>
|
||||
<p class="desc">${lang==='de'?'Hybrid MCP + Proxy Plattform für LLM Token-Einsparung':'Hybrid MCP + Proxy platform for LLM token savings'}</p>
|
||||
|
||||
<div style="display:grid;grid-template-columns:120px 1fr;gap:8px;font-size:13px;margin-bottom:20px">
|
||||
<strong>Version</strong><span>0.1.0 (MVP)</span>
|
||||
<strong>${lang==='de'?'Lizenz':'License'}</strong><span>Apache-2.0</span>
|
||||
<strong>${lang==='de'?'Autor':'Author'}</strong><span>Context X (context-x.org)</span>
|
||||
<strong>Gitea</strong><span><a href="https://gitea.context-x.org/rene/tokenvault" target="_blank" style="color:var(--primary)">rene/tokenvault</a></span>
|
||||
<strong>Stack</strong><span>Fastify 5.x, PostgreSQL 17, Qdrant, TypeScript</span>
|
||||
<strong>Packages</strong><span>@tokenvault/core, /mcp, /client, /dashboard</span>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Provider':'Providers'}</h3>
|
||||
<div style="font-size:13px;line-height:2">
|
||||
Anthropic (Claude) · OpenAI (GPT) · Google (Gemini) · Mistral · Groq · Cerebras · Ollama
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3>${lang==='de'?'Funktionen':'Features'}</h3>
|
||||
<div style="font-size:13px;line-height:2">
|
||||
✓ OpenAI-${lang==='de'?'kompatibler Proxy':'compatible proxy'} ·
|
||||
✓ Ticket-System (TV-00001) ·
|
||||
✓ ${lang==='de'?'Kosten-Tracking':'Cost tracking'} ·
|
||||
✓ ${lang==='de'?'Multi-Provider Routing':'Multi-provider routing'} ·
|
||||
✓ MCP Server ·
|
||||
✓ TypeScript SDK<br>
|
||||
◻ AST-Compression (Phase 2) ·
|
||||
◻ Semantic Cache (Phase 2) ·
|
||||
◻ Prompt Caching (Phase 2) ·
|
||||
◻ Budget Enforcement (Phase 3) ·
|
||||
◻ Anomaly Detection (Phase 3)
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
function showSettings(page) {
|
||||
document.querySelectorAll('.settings-nav-item').forEach(i => i.classList.toggle('active', i.dataset.settings === page));
|
||||
const panel = document.getElementById('settings-content');
|
||||
panel.innerHTML = settingsPages[page]?.() ?? '<p>Not found</p>';
|
||||
if (page === 'providers') updateProviderDots();
|
||||
}
|
||||
|
||||
async function updateProviderDots() {
|
||||
try {
|
||||
const data = await (await fetch(API + '/health')).json();
|
||||
data.providers.forEach(p => {
|
||||
const dot = document.getElementById('dot-' + p.name);
|
||||
if (dot) { dot.className = 'status-dot ' + (p.configured ? 'green' : 'red'); }
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function saveSettings(section) {
|
||||
// MVP: show toast, Phase 2 will persist to API
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:var(--green);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:999;animation:fadeIn 0.2s';
|
||||
toast.textContent = lang === 'de' ? 'Einstellungen gespeichert' : 'Settings saved';
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
|
||||
function addProjectBudget() {
|
||||
const container = document.getElementById('project-budgets');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'form-row';
|
||||
row.style.marginBottom = '8px';
|
||||
row.innerHTML = '<div class="form-group"><input type="text" placeholder="project-name" /></div><div class="form-group"><input type="number" placeholder="0.00" step="0.01" /></div>';
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function testProviders() { saveSettings('providers'); }
|
||||
function testNotification() { saveSettings('notifications'); }
|
||||
function testDb() { saveSettings('database'); }
|
||||
function flushCache() { if(confirm(lang==='de'?'Cache wirklich leeren?':'Flush cache?')) saveSettings('cache'); }
|
||||
function resetData() { saveSettings('general'); }
|
||||
|
||||
// Init settings when tab shown
|
||||
showSettings('providers');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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}`);
|
||||
|
||||
26
packages/rtk-bridge/package.json
Normal file
26
packages/rtk-bridge/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
199
packages/rtk-bridge/src/index.ts
Normal file
199
packages/rtk-bridge/src/index.ts
Normal file
@ -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<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);
|
||||
});
|
||||
8
packages/rtk-bridge/tsconfig.json
Normal file
8
packages/rtk-bridge/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
12
packages/rtk-bridge/tsup.config.ts
Normal file
12
packages/rtk-bridge/tsup.config.ts
Normal file
@ -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' },
|
||||
});
|
||||
271
pnpm-lock.yaml
generated
271
pnpm-lock.yaml
generated
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user