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',
|
cwd: '/opt/tokenvault',
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
TOKENVAULT_PORT: '3300',
|
TOKENVAULT_PORT: '3350',
|
||||||
TOKENVAULT_HOST: '0.0.0.0',
|
TOKENVAULT_HOST: '0.0.0.0',
|
||||||
DB_HOST: '127.0.0.1',
|
DB_HOST: '127.0.0.1',
|
||||||
DB_PORT: '5432',
|
DB_PORT: '5432',
|
||||||
@ -34,7 +34,7 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: '3301',
|
PORT: '3301',
|
||||||
TOKENVAULT_CORE_URL: 'http://localhost:3300',
|
TOKENVAULT_CORE_URL: 'http://localhost:3350',
|
||||||
},
|
},
|
||||||
instances: 1,
|
instances: 1,
|
||||||
exec_mode: 'fork',
|
exec_mode: 'fork',
|
||||||
|
|||||||
@ -24,5 +24,6 @@ export const config = {
|
|||||||
groq: { apiKey: process.env['GROQ_API_KEY'] ?? '' },
|
groq: { apiKey: process.env['GROQ_API_KEY'] ?? '' },
|
||||||
cerebras: { apiKey: process.env['CEREBRAS_API_KEY'] ?? '' },
|
cerebras: { apiKey: process.env['CEREBRAS_API_KEY'] ?? '' },
|
||||||
ollama: { url: process.env['OLLAMA_URL'] ?? 'http://localhost:11434' },
|
ollama: { url: process.env['OLLAMA_URL'] ?? 'http://localhost:11434' },
|
||||||
|
aiBridge: { url: process.env['AI_BRIDGE_URL'] ?? 'http://localhost:3250' },
|
||||||
},
|
},
|
||||||
} as const;
|
} 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 ──────────────────────────────────────────────────────
|
// ─── Provider Registry ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const adapters: Map<ProviderName, ProviderAdapter> = new Map();
|
const adapters: Map<ProviderName, ProviderAdapter> = new Map();
|
||||||
@ -247,10 +318,12 @@ export function initProviders(): void {
|
|||||||
const anthropic = createAnthropicAdapter();
|
const anthropic = createAnthropicAdapter();
|
||||||
const openai = createOpenAIAdapter();
|
const openai = createOpenAIAdapter();
|
||||||
const ollama = createOllamaAdapter();
|
const ollama = createOllamaAdapter();
|
||||||
|
const aiBridge = createAiBridgeAdapter();
|
||||||
|
|
||||||
adapters.set('anthropic', anthropic);
|
adapters.set('anthropic', anthropic);
|
||||||
adapters.set('openai', openai);
|
adapters.set('openai', openai);
|
||||||
adapters.set('ollama', ollama);
|
adapters.set('ollama', ollama);
|
||||||
|
adapters.set('ai-bridge', aiBridge);
|
||||||
|
|
||||||
const configured = [...adapters.values()].filter(a => a.isConfigured()).map(a => a.name);
|
const configured = [...adapters.values()].filter(a => a.isConfigured()).map(a => a.name);
|
||||||
logger.info({ configured }, 'Provider registry initialized');
|
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 { config } from './config.js';
|
||||||
import { logger } from './observability/logger.js';
|
import { logger } from './observability/logger.js';
|
||||||
import { runMigrations } from './db/migrate.js';
|
import { runMigrations } from './db/migrate.js';
|
||||||
|
import { runRtkMigrations } from './db/rtk-migrate.js';
|
||||||
import { closePool } from './db/client.js';
|
import { closePool } from './db/client.js';
|
||||||
import { initProviders } from './providers/index.js';
|
import { initProviders } from './providers/index.js';
|
||||||
import { healthRoutes } from './routes/health.js';
|
import { healthRoutes } from './routes/health.js';
|
||||||
import { proxyRoutes } from './routes/proxy.js';
|
import { proxyRoutes } from './routes/proxy.js';
|
||||||
import { ticketRoutes } from './routes/tickets.js';
|
import { ticketRoutes } from './routes/tickets.js';
|
||||||
|
import { rtkRoutes } from './routes/rtk.js';
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: false,
|
logger: false,
|
||||||
@ -19,6 +21,7 @@ await app.register(cors, { origin: true });
|
|||||||
await app.register(healthRoutes);
|
await app.register(healthRoutes);
|
||||||
await app.register(proxyRoutes);
|
await app.register(proxyRoutes);
|
||||||
await app.register(ticketRoutes);
|
await app.register(ticketRoutes);
|
||||||
|
await app.register(rtkRoutes);
|
||||||
|
|
||||||
// ─── Startup ─────────────────────────────────────────────────────────────────
|
// ─── Startup ─────────────────────────────────────────────────────────────────
|
||||||
async function startup(): Promise<void> {
|
async function startup(): Promise<void> {
|
||||||
@ -26,6 +29,7 @@ async function startup(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
await runRtkMigrations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'DB migration failed — proceeding in degraded mode');
|
logger.error({ err }, 'DB migration failed — proceeding in degraded mode');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// ─── Provider Types ────────────────────────────────────────────────────────
|
// ─── 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';
|
export type TicketStatus = 'completed' | 'cached' | 'failed' | 'pending_review';
|
||||||
|
|
||||||
|
|||||||
@ -91,6 +91,46 @@ tr:hover { background:#f8fafc; }
|
|||||||
/* Refresh button */
|
/* 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 { 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); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -111,6 +151,8 @@ tr:hover { background:#f8fafc; }
|
|||||||
<div class="tab" data-tab="tickets">Tickets</div>
|
<div class="tab" data-tab="tickets">Tickets</div>
|
||||||
<div class="tab" data-tab="cost">Cost Analysis</div>
|
<div class="tab" data-tab="cost">Cost Analysis</div>
|
||||||
<div class="tab" data-tab="providers">Providers</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>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -175,6 +217,69 @@ tr:hover { background:#f8fafc; }
|
|||||||
<div id="provider-list" class="cards"></div>
|
<div id="provider-list" class="cards"></div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -278,9 +383,610 @@ async function loadProviders() {
|
|||||||
} catch {}
|
} 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();
|
loadAll();
|
||||||
setInterval(loadAll, 30000);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -47,5 +47,21 @@ app.get('/api/cost/breakdown', async (req) => {
|
|||||||
return res.json();
|
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' });
|
await app.listen({ port: PORT, host: '0.0.0.0' });
|
||||||
console.log(`TokenVault Dashboard running on http://localhost:${PORT}`);
|
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
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
pnpmfileChecksum: sha256-dS522kUCN9FHUHk8JODaJjlMeNKVaNv8hwk1JhcGjEY=
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.: {}
|
||||||
@ -120,6 +122,28 @@ importers:
|
|||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.2.4(@types/node@22.19.17)(tsx@4.21.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:
|
packages:
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
@ -486,6 +510,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/better-sqlite3@7.6.13':
|
||||||
|
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
@ -571,6 +598,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
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:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -579,6 +618,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
bundle-require@5.1.0:
|
bundle-require@5.1.0:
|
||||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@ -613,6 +655,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
|
chownr@1.1.4:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -665,10 +710,18 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
deep-eql@5.0.2:
|
deep-eql@5.0.2:
|
||||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
deep-extend@0.6.0:
|
||||||
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -677,6 +730,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -688,6 +745,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
es-define-property@1.0.1:
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -726,6 +786,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
||||||
engines: {node: '>=18.0.0'}
|
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:
|
expect-type@1.3.0:
|
||||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -773,6 +837,9 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||||
engines: {node: '>= 18.0.0'}
|
engines: {node: '>= 18.0.0'}
|
||||||
@ -796,6 +863,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fs-constants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@ -815,6 +885,9 @@ packages:
|
|||||||
get-tsconfig@4.13.7:
|
get-tsconfig@4.13.7:
|
||||||
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
||||||
|
|
||||||
|
github-from-package@0.0.0:
|
||||||
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
glob@11.1.0:
|
glob@11.1.0:
|
||||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@ -845,9 +918,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ini@1.3.8:
|
||||||
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
ip-address@10.1.0:
|
ip-address@10.1.0:
|
||||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
@ -938,14 +1017,24 @@ packages:
|
|||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mimic-response@3.1.0:
|
||||||
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
minimist@1.2.8:
|
||||||
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
minipass@7.1.3:
|
minipass@7.1.3:
|
||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
@ -965,10 +1054,17 @@ packages:
|
|||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0:
|
||||||
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
negotiator@1.0.0:
|
negotiator@1.0.0:
|
||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
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:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -1113,6 +1209,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
process-warning@4.0.1:
|
||||||
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
||||||
|
|
||||||
@ -1123,6 +1225,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
pump@3.0.4:
|
||||||
|
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||||
|
|
||||||
qs@6.15.1:
|
qs@6.15.1:
|
||||||
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
|
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@ -1138,6 +1243,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||||
engines: {node: '>= 0.10'}
|
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:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
@ -1244,6 +1357,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
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:
|
sonic-boom@4.2.1:
|
||||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||||
|
|
||||||
@ -1269,6 +1388,13 @@ packages:
|
|||||||
std-env@3.10.0:
|
std-env@3.10.0:
|
||||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
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:
|
strip-literal@3.1.0:
|
||||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||||
|
|
||||||
@ -1277,6 +1403,13 @@ packages:
|
|||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
hasBin: true
|
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:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@ -1348,6 +1481,9 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tunnel-agent@0.6.0:
|
||||||
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
type-is@2.0.1:
|
type-is@2.0.1:
|
||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -1367,6 +1503,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -1717,6 +1856,10 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/better-sqlite3@7.6.13':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.17
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/deep-eql': 4.0.2
|
'@types/deep-eql': 4.0.2
|
||||||
@ -1811,6 +1954,23 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.4: {}
|
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:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@ -1829,6 +1989,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
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):
|
bundle-require@5.1.0(esbuild@0.27.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
@ -1862,6 +2027,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
|
chownr@1.1.4: {}
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
confbox@0.1.8: {}
|
confbox@0.1.8: {}
|
||||||
@ -1897,12 +2064,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@ -1913,6 +2088,10 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
@ -1966,6 +2145,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eventsource-parser: 3.0.6
|
eventsource-parser: 3.0.6
|
||||||
|
|
||||||
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
expect-type@1.3.0: {}
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
express-rate-limit@8.3.2(express@5.2.1):
|
express-rate-limit@8.3.2(express@5.2.1):
|
||||||
@ -2053,6 +2234,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@ -2085,6 +2268,8 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -2112,6 +2297,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
glob@11.1.0:
|
glob@11.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
@ -2143,8 +2330,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ini@1.3.8: {}
|
||||||
|
|
||||||
ip-address@10.1.0: {}
|
ip-address@10.1.0: {}
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
@ -2207,12 +2398,18 @@ snapshots:
|
|||||||
|
|
||||||
mime@3.0.0: {}
|
mime@3.0.0: {}
|
||||||
|
|
||||||
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.5
|
brace-expansion: 5.0.5
|
||||||
|
|
||||||
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
minipass@7.1.3: {}
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
@ -2232,8 +2429,14 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@5.1.7: {}
|
nanoid@5.1.7: {}
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
node-abi@3.89.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.4
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
@ -2357,6 +2560,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
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@4.0.1: {}
|
||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
@ -2366,6 +2584,11 @@ snapshots:
|
|||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
ipaddr.js: 1.9.1
|
ipaddr.js: 1.9.1
|
||||||
|
|
||||||
|
pump@3.0.4:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
qs@6.15.1:
|
qs@6.15.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
@ -2381,6 +2604,19 @@ snapshots:
|
|||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
unpipe: 1.0.0
|
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: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
real-require@0.2.0: {}
|
||||||
@ -2519,6 +2755,14 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
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:
|
sonic-boom@4.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep: 1.0.0
|
atomic-sleep: 1.0.0
|
||||||
@ -2535,6 +2779,12 @@ snapshots:
|
|||||||
|
|
||||||
std-env@3.10.0: {}
|
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:
|
strip-literal@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 9.0.1
|
js-tokens: 9.0.1
|
||||||
@ -2549,6 +2799,21 @@ snapshots:
|
|||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
ts-interface-checker: 0.1.13
|
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:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@ -2619,6 +2884,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
tunnel-agent@0.6.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
type-is@2.0.1:
|
type-is@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
@ -2633,6 +2902,8 @@ snapshots:
|
|||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0):
|
vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user