Преглед изворни кода

feat(v0.4.0): enterprise usage tracking — JWT auth + SQLite + admin panel

- SQLite schema: users, teams, team_members, usage_events, pricing
- JWT auth: register/login/refresh endpoints
- Usage tracking: auto-record tokens + cost on every prompt
- Admin API: /admin/usage/team, /users, /minute, /today
- Pricing table: 10+ LLM models with fixed rates
- Admin Dashboard: SolidJS UI with 4-tab analytics panel
- Auto-refresh every 30s, login/logout, team/user/minute/today views
KirinCode пре 1 месец
родитељ
комит
91921da8f1
34 измењених фајлова са 522 додато и 117 уклоњено
  1. 1 1
      packages/app/package.json
  2. 218 0
      packages/app/src/pages/admin/AdminDashboard.tsx
  3. 1 1
      packages/cli/package.json
  4. 1 1
      packages/codemode/package.json
  5. 1 1
      packages/console/app/package.json
  6. 1 1
      packages/console/core/package.json
  7. 1 1
      packages/console/function/package.json
  8. 1 1
      packages/console/mail/package.json
  9. 1 1
      packages/console/support/package.json
  10. 1 1
      packages/core/package.json
  11. 1 1
      packages/desktop/package.json
  12. 1 1
      packages/effect-drizzle-sqlite/package.json
  13. 1 1
      packages/effect-sqlite-node/package.json
  14. 1 1
      packages/enterprise/package.json
  15. 1 1
      packages/function/package.json
  16. 1 1
      packages/http-recorder/package.json
  17. 1 1
      packages/kirincode-cloud/package.json
  18. 70 0
      packages/kirincode-cloud/src/db/schema.sql
  19. 203 86
      packages/kirincode-cloud/src/index.ts
  20. 1 1
      packages/kirincode-workflow/package.json
  21. 1 1
      packages/kirincode/package.json
  22. 1 1
      packages/llm/package.json
  23. 1 1
      packages/mobile/package.json
  24. 1 1
      packages/plugin/package.json
  25. 1 1
      packages/sdk/js/package.json
  26. 1 1
      packages/server/package.json
  27. 1 1
      packages/session-ui/package.json
  28. 1 1
      packages/slack/package.json
  29. 1 1
      packages/stats/app/package.json
  30. 1 1
      packages/stats/core/package.json
  31. 1 1
      packages/stats/server/package.json
  32. 1 1
      packages/tui/package.json
  33. 1 1
      packages/ui/package.json
  34. 1 1
      packages/web/package.json

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/app",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "description": "",
   "type": "module",
   "exports": {

+ 218 - 0
packages/app/src/pages/admin/AdminDashboard.tsx

@@ -0,0 +1,218 @@
+import { createSignal, createResource, onMount, For, Show } from "solid-js"
+
+const API_BASE = "http://localhost:4096/api/v1/admin"
+
+async function fetchJson(url: string, token: string) {
+  const res = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
+  return res.json()
+}
+
+export function AdminDashboard() {
+  const [token, setToken] = createSignal(localStorage.getItem("kirincode_token") || "")
+  const [email, setEmail] = createSignal("")
+  const [password, setPassword] = createSignal("")
+  const [loggedIn, setLoggedIn] = createSignal(!!token())
+  const [tab, setTab] = createSignal<"today" | "team" | "users" | "minute">("today")
+
+  // Login
+  async function login() {
+    const res = await fetch(API_BASE.replace("/admin", "/auth/login"), {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ email: email(), password: password() }),
+    })
+    const data = await res.json()
+    if (data.token) {
+      localStorage.setItem("kirincode_token", data.token)
+      setToken(data.token)
+      setLoggedIn(true)
+    }
+  }
+
+  // Fetch data
+  const [todayData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/usage/today`, token()))
+  const [teamData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/usage/team`, token()))
+  const [usersData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/usage/users`, token()))
+  const [minuteData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/usage/minute`, token()))
+
+  // Refs for auto-refresh
+  onMount(() => {
+    const interval = setInterval(() => {
+      if (loggedIn()) {
+        fetchJson(`${API_BASE}/usage/today`, token()).then(d => { /* trigger re-fetch */ })
+      }
+    }, 30000)
+    return () => clearInterval(interval)
+  })
+
+  if (!loggedIn()) {
+    return (
+      <div style={{ display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" }}>
+        <h1 style={{ color: "#e74c3c", "margin-bottom": "24px" }}>KirinCode Admin</h1>
+        <div style={{ display: "flex", "flex-direction": "column", gap: "12px", width: "320px" }}>
+          <input
+            type="email" placeholder="Email" value={email()} onInput={e => setEmail(e.currentTarget.value)}
+            style={{ padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" }}
+          />
+          <input
+            type="password" placeholder="Password" value={password()} onInput={e => setPassword(e.currentTarget.value)}
+            style={{ padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" }}
+          />
+          <button onClick={login}
+            style={{ padding: "10px", "border-radius": "8px", border: "none", background: "#e74c3c", color: "#fff", "font-size": "14px", "font-weight": "600", cursor: "pointer" }}
+          >Login</button>
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div style={{ display: "flex", "flex-direction": "column", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" }}>
+      {/* Header */}
+      <div style={{ display: "flex", "align-items": "center", padding: "12px 20px", background: "#1a1a2e", "border-bottom": "1px solid #2c3e50" }}>
+        <span style={{ "font-size": "18px", "font-weight": "700", color: "#e74c3c", "margin-right": "24px" }}>🦄 KirinCode Admin</span>
+        <div style={{ display: "flex", gap: "4px" }}>
+          {(["today", "team", "users", "minute"] as const).map(t => (
+            <button onClick={() => setTab(t)}
+              style={{
+                padding: "6px 14px", "border-radius": "6px", border: "none", "font-size": "13px", cursor: "pointer",
+                background: tab() === t ? "#e74c3c" : "transparent",
+                color: tab() === t ? "#fff" : "#7f8c8d",
+              }}
+            >{t === "today" ? "Today" : t === "team" ? "Team" : t === "users" ? "Users" : "Minute"}</button>
+          ))}
+        </div>
+        <button onClick={() => { localStorage.removeItem("kirincode_token"); setToken(""); setLoggedIn(false) }}
+          style={{ "margin-left": "auto", padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "12px" }}
+        >Logout</button>
+      </div>
+
+      {/* Content */}
+      <div style={{ flex: 1, overflow: "auto", padding: "24px" }}>
+        <Show when={tab() === "today" && todayData()}>
+          {() => {
+            const d = todayData()?.data || {}
+            return (
+              <div>
+                <h2 style={{ "margin-bottom": "20px" }}>Today's Usage</h2>
+                <div style={{ display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(200px, 1fr))", gap: "16px" }}>
+                  <MetricCard label="Total Tokens" value={(d.input_tokens + d.output_tokens).toLocaleString()} />
+                  <MetricCard label="Requests" value={d.request_count?.toLocaleString() || "0"} />
+                  <MetricCard label="Cost" value={`$${d.total_cost?.toFixed(4) || "0.0000"}`} />
+                  <MetricCard label="Active Users" value={d.active_users || "0"} />
+                </div>
+              </div>
+            )
+          }}
+        </Show>
+
+        <Show when={tab() === "team" && teamData()}>
+          {() => (
+            <div>
+              <h2 style={{ "margin-bottom": "20px" }}>Team Usage</h2>
+              <table style={{ width: "100%", "border-collapse": "collapse" }}>
+                <thead>
+                  <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
+                    <th style={{ padding: "8px" }}>Day</th>
+                    <th style={{ padding: "8px" }}>Provider</th>
+                    <th style={{ padding: "8px" }}>Model</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Requests</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
+                  </tr>
+                </thead>
+                <For each={(teamData()?.data || []).slice(0, 50)}>
+                  {(row: any) => (
+                    <tr style={{ "border-top": "1px solid #1e293b", "font-size": "13px" }}>
+                      <td style={{ padding: "8px" }}>{row.day}</td>
+                      <td style={{ padding: "8px" }}>{row.provider}</td>
+                      <td style={{ padding: "8px" }}>{row.model}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.request_count}</td>
+                      <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.total_cost?.toFixed(4)}</td>
+                    </tr>
+                  )}
+                </For>
+              </table>
+            </div>
+          )}
+        </Show>
+
+        <Show when={tab() === "users" && usersData()}>
+          {() => (
+            <div>
+              <h2 style={{ "margin-bottom": "20px" }}>User Rankings</h2>
+              <table style={{ width: "100%", "border-collapse": "collapse" }}>
+                <thead>
+                  <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
+                    <th style={{ padding: "8px" }}>User</th>
+                    <th style={{ padding: "8px" }}>Email</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Requests</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
+                  </tr>
+                </thead>
+                <For each={usersData()?.data || []}>
+                  {(row: any) => (
+                    <tr style={{ "border-top": "1px solid #1e293b", "font-size": "13px" }}>
+                      <td style={{ padding: "8px" }}>{row.display_name}</td>
+                      <td style={{ padding: "8px", color: "#7f8c8d" }}>{row.email}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.request_count}</td>
+                      <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.total_cost?.toFixed(4)}</td>
+                    </tr>
+                  )}
+                </For>
+              </table>
+            </div>
+          )}
+        </Show>
+
+        <Show when={tab() === "minute" && minuteData()}>
+          {() => (
+            <div>
+              <h2 style={{ "margin-bottom": "20px" }}>Minute-Level Detail</h2>
+              <table style={{ width: "100%", "border-collapse": "collapse" }}>
+                <thead>
+                  <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
+                    <th style={{ padding: "8px" }}>Time</th>
+                    <th style={{ padding: "8px" }}>Provider</th>
+                    <th style={{ padding: "8px" }}>Model</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
+                    <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
+                  </tr>
+                </thead>
+                <For each={(minuteData()?.data || []).slice(0, 100)}>
+                  {(row: any) => (
+                    <tr style={{ "border-top": "1px solid #1e293b", "font-size": "12px" }}>
+                      <td style={{ padding: "8px" }}>{row.minute}</td>
+                      <td style={{ padding: "8px" }}>{row.provider}</td>
+                      <td style={{ padding: "8px" }}>{row.model}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
+                      <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.cost_usd?.toFixed(5)}</td>
+                    </tr>
+                  )}
+                </For>
+              </table>
+            </div>
+          )}
+        </Show>
+      </div>
+    </div>
+  )
+}
+
+function MetricCard(props: { label: string; value: string }) {
+  return (
+    <div style={{ background: "#1a1a2e", padding: "20px", "border-radius": "12px", border: "1px solid #2c3e50" }}>
+      <div style={{ color: "#7f8c8d", "font-size": "12px", "margin-bottom": "8px" }}>{props.label}</div>
+      <div style={{ "font-size": "24px", "font-weight": "700", color: "#f39c12" }}>{props.value}</div>
+    </div>
+  )
+}

+ 1 - 1
packages/cli/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/cli",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "bin": {

+ 1 - 1
packages/codemode/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/codemode",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "description": "Effect-native confined code execution over schema-described tools",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-app",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/console/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/console-core",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/console/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-function",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-mail",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",

+ 1 - 1
packages/console/support/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-support",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "@kirincode-ai/core",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@kirincode-ai/desktop",
   "private": true,
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "homepage": "https://kirincode.ai",

+ 1 - 1
packages/effect-drizzle-sqlite/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "@kirincode-ai/effect-drizzle-sqlite",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/effect-sqlite-node/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "@kirincode-ai/effect-sqlite-node",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/enterprise",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/function",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/http-recorder/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "@kirincode-ai/http-recorder",
   "description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
   "type": "module",

+ 1 - 1
packages/kirincode-cloud/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/kirincode-cloud",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "description": "KirinCode Cloud Sync Server — multi-user backend with session sync, memory, skills, workflows",
   "license": "MIT",

+ 70 - 0
packages/kirincode-cloud/src/db/schema.sql

@@ -0,0 +1,70 @@
+-- KirinCode v0.4.0 SQLite Schema
+-- Enterprise usage tracking + JWT auth
+
+CREATE TABLE IF NOT EXISTS users (
+  id            TEXT PRIMARY KEY,
+  email         TEXT UNIQUE NOT NULL,
+  password_hash TEXT NOT NULL,
+  display_name  TEXT DEFAULT '',
+  team_id       TEXT REFERENCES teams(id),
+  role          TEXT DEFAULT 'member',  -- 'admin' | 'team_admin' | 'member'
+  created_at    TEXT DEFAULT (datetime('now')),
+  updated_at    TEXT DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS teams (
+  id          TEXT PRIMARY KEY,
+  name        TEXT NOT NULL,
+  owner_id    TEXT NOT NULL REFERENCES users(id),
+  created_at  TEXT DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS team_members (
+  team_id   TEXT NOT NULL REFERENCES teams(id),
+  user_id   TEXT NOT NULL REFERENCES users(id),
+  role      TEXT DEFAULT 'member',
+  joined_at TEXT DEFAULT (datetime('now')),
+  PRIMARY KEY (team_id, user_id)
+);
+
+CREATE TABLE IF NOT EXISTS usage_events (
+  id             INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_id        TEXT NOT NULL,
+  team_id        TEXT,
+  provider       TEXT NOT NULL,
+  model          TEXT NOT NULL,
+  session_id     TEXT,
+  input_tokens   INTEGER DEFAULT 0,
+  output_tokens  INTEGER DEFAULT 0,
+  cache_tokens   INTEGER DEFAULT 0,
+  duration_ms    INTEGER DEFAULT 0,
+  cost_usd       REAL DEFAULT 0,
+  created_at     TEXT DEFAULT (datetime('now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_usage_user_ts ON usage_events(user_id, created_at);
+CREATE INDEX IF NOT EXISTS idx_usage_team_ts ON usage_events(team_id, created_at);
+CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage_events(created_at);
+
+CREATE TABLE IF NOT EXISTS pricing (
+  provider  TEXT NOT NULL,
+  model     TEXT NOT NULL,
+  input_price_per_1m   REAL DEFAULT 0,
+  output_price_per_1m  REAL DEFAULT 0,
+  cache_price_per_1m   REAL DEFAULT 0,
+  updated_at TEXT DEFAULT (datetime('now')),
+  PRIMARY KEY (provider, model)
+);
+
+-- Default pricing data (fixed rates, not real-time API)
+INSERT OR IGNORE INTO pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES
+  ('anthropic', 'claude-sonnet-4-5', 3.0, 15.0),
+  ('anthropic', 'claude-haiku-4-5', 0.8, 4.0),
+  ('anthropic', 'claude-opus-4-5', 15.0, 75.0),
+  ('openai', 'gpt-4o', 2.5, 10.0),
+  ('openai', 'gpt-4o-mini', 0.15, 0.6),
+  ('openai', 'gpt-5', 5.0, 20.0),
+  ('deepseek', 'deepseek-v4-pro', 1.74, 3.48),
+  ('deepseek', 'deepseek-v4-flash', 0.14, 0.28),
+  ('google', 'gemini-2.5-pro', 3.5, 10.5),
+  ('google', 'gemini-2.5-flash', 0.4, 1.5);

+ 203 - 86
packages/kirincode-cloud/src/index.ts

@@ -1,142 +1,259 @@
-// KirinCode Cloud Sync Server
-// Bun + Elysia + Drizzle ORM + PostgreSQL
-
+// KirinCode Cloud Server v0.4.0
+// Enterprise Usage Tracking + JWT Auth + SQLite
 import { Elysia, t } from "elysia"
 import { jwt } from "@elysiajs/jwt"
 import { cors } from "@elysiajs/cors"
+import { Database } from "bun:sqlite"
 import { hash, compare } from "bcryptjs"
-import { v4 as uuid } from "uuid"
+import { randomUUID } from "node:crypto"
 
-const JWT_SECRET = process.env.JWT_SECRET || "kirincode-cloud-secret-change-me"
+const JWT_SECRET = process.env.JWT_SECRET || "kirincode-cloud-secret-change-in-production"
 const PORT = parseInt(process.env.PORT || "3000")
+const DB_PATH = process.env.DB_PATH || "kirincode.db"
+
+// Initialize SQLite
+const db = new Database(DB_PATH)
+db.run("PRAGMA journal_mode=WAL")
+db.run("PRAGMA foreign_keys=ON")
+
+// Run schema
+const schema = await Bun.file("./src/db/schema.sql").text()
+db.run(schema)
 
-// In-memory store (replace with PostgreSQL + Drizzle in production)
-const db = {
-  users: new Map<string, { id: string; email: string; password: string }>(),
-  sessions: new Map<string, any>(),
-  messages: new Map<string, any[]>(),
-  memory: new Map<string, any[]>(),
-  skills: new Map<string, any[]>(),
-  workflows: new Map<string, any[]>(),
+// Helper: calculate cost
+function calculateCost(provider: string, model: string, inputTokens: number, outputTokens: number, cacheTokens: number = 0): number {
+  const row = db.query("SELECT input_price_per_1m, output_price_per_1m, cache_price_per_1m FROM pricing WHERE provider = ? AND model = ?").get(provider, model) as any
+  if (!row) return 0
+  const inputCost = (inputTokens / 1_000_000) * row.input_price_per_1m
+  const outputCost = (outputTokens / 1_000_000) * row.output_price_per_1m
+  const cacheCost = (cacheTokens / 1_000_000) * (row.cache_price_per_1m || 0)
+  return Math.round((inputCost + outputCost + cacheCost) * 1e6) / 1e6
 }
 
 const app = new Elysia()
   .use(cors())
   .use(jwt({ name: "jwt", secret: JWT_SECRET }))
 
-  // Auth middleware
+  // --- Auth Middleware ---
   .derive(async ({ jwt, headers, set }) => {
     const auth = headers["authorization"]
-    if (!auth?.startsWith("Bearer ")) return { userId: null }
+    if (!auth?.startsWith("Bearer ")) return { userId: null, userRole: null, userTeamId: null }
     try {
-      const payload = await jwt.verify(auth.slice(7))
-      return { userId: (payload as any).sub as string }
+      const payload = await jwt.verify(auth.slice(7)) as any
+      const user = db.query("SELECT role, team_id FROM users WHERE id = ?").get(payload.sub) as any
+      return { userId: payload.sub as string, userRole: user?.role || null, userTeamId: user?.team_id || null }
     } catch {
-      return { userId: null }
+      return { userId: null, userRole: null, userTeamId: null }
     }
   })
 
-  .guard({ beforeHandle: ({ userId, set }) => {
-    if (!userId) { set.status = 401; return "Unauthorized" }
-  }})
-
-  // === Auth ===
-  .post("/api/v1/auth/register", async ({ body }) => {
-    const id = uuid()
-    const password = await hash(body.password, 10)
-    db.users.set(id, { id, email: body.email, password })
+  // --- Public Routes ---
+  .post("/api/v1/auth/register", async ({ body, set }) => {
+    const existing = db.query("SELECT id FROM users WHERE email = ?").get(body.email)
+    if (existing) { set.status = 409; return { error: "Email already registered" } }
+    const id = randomUUID()
+    const passwordHash = await hash(body.password, 10)
+    db.run("INSERT INTO users (id, email, password_hash, display_name) VALUES (?, ?, ?, ?)", [id, body.email, passwordHash, body.display_name || body.email.split("@")[0]])
     const token = await app.decorator.jwt.sign({ sub: id })
-    return { token, user: { id, email: body.email } }
-  }, { body: t.Object({ email: t.String(), password: t.String() }) })
+    return { token, user: { id, email: body.email, display_name: body.display_name } }
+  }, { body: t.Object({ email: t.String(), password: t.String(), display_name: t.Optional(t.String()) }) })
 
   .post("/api/v1/auth/login", async ({ body, set }) => {
-    const user = Array.from(db.users.values()).find(u => u.email === body.email)
-    if (!user || !(await compare(body.password, user.password))) {
-      set.status = 401
-      return { error: "Invalid credentials" }
+    const user = db.query("SELECT id, email, password_hash, display_name, role, team_id FROM users WHERE email = ?").get(body.email) as any
+    if (!user || !(await compare(body.password, user.password_hash))) {
+      set.status = 401; return { error: "Invalid credentials" }
     }
     const token = await app.decorator.jwt.sign({ sub: user.id })
-    return { token, user: { id: user.id, email: user.email } }
+    return { token, user: { id: user.id, email: user.email, display_name: user.display_name, role: user.role, team_id: user.team_id } }
   }, { body: t.Object({ email: t.String(), password: t.String() }) })
 
-  // === Sessions ===
+  .post("/api/v1/auth/refresh", async ({ jwt, headers }) => {
+    const auth = headers["authorization"]?.slice(7)
+    if (!auth) return { error: "No token" }
+    try {
+      const payload = await jwt.verify(auth) as any
+      const token = await app.decorator.jwt.sign({ sub: payload.sub })
+      return { token }
+    } catch { return { error: "Invalid token" } }
+  })
+
+  // --- Protected Routes ---
+  .guard({ beforeHandle: ({ userId, set }) => { if (!userId) { set.status = 401; return "Unauthorized" } } })
+
+  // --- Sessions ---
   .get("/api/v1/sessions", async ({ userId }) => {
-    const list = Array.from(db.sessions.values()).filter(s => s.userId === userId)
-    return { data: list }
+    const sessions = db.query("SELECT id, title, agent, created_at, updated_at FROM sessions WHERE user_id = ? ORDER BY updated_at DESC LIMIT 50").all(userId)
+    return { data: sessions }
   })
 
   .post("/api/v1/sessions", async ({ userId, body }) => {
-    const session = { id: uuid(), userId, title: body.title || "New Chat", agent: body.agent || "build", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
-    db.sessions.set(session.id, session)
-    db.messages.set(session.id, [])
-    return { data: session }
+    const id = randomUUID()
+    db.run("INSERT INTO sessions (id, user_id, title, agent) VALUES (?, ?, ?, ?)", [id, userId, body.title || "New Chat", body.agent || "build"])
+    return { data: { id, title: body.title, agent: body.agent } }
   }, { body: t.Object({ title: t.Optional(t.String()), agent: t.Optional(t.String()) }) })
 
-  .post("/api/v1/sessions/:id/prompt", async ({ userId, params, body }) => {
-    const session = db.sessions.get(params.id)
-    if (!session || session.userId !== userId) return { error: "Session not found" }
-    const msgs = db.messages.get(params.id) || []
-    const userMsg = { id: uuid(), role: "user", content: (body.parts || []).filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n"), createdAt: new Date().toISOString() }
-    msgs.push(userMsg)
-    // AI response placeholder — in production, call LLM provider
-    const aiMsg = { id: uuid(), role: "assistant", content: "[AI response — connect LLM provider]", createdAt: new Date().toISOString() }
-    msgs.push(aiMsg)
-    db.messages.set(params.id, msgs)
-    db.sessions.set(params.id, { ...session, updatedAt: new Date().toISOString() })
-    return { data: { messages: msgs } }
+  .post("/api/v1/sessions/:id/prompt", async ({ userId, params, body, set }) => {
+    const session = db.query("SELECT id FROM sessions WHERE id = ? AND user_id = ?").get(params.id, userId)
+    if (!session) { set.status = 404; return { error: "Session not found" } }
+    const msgId = randomUUID()
+    const text = (body.parts || []).filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n")
+    db.run("INSERT INTO messages (id, session_id, role, content) VALUES (?, ?, ?, ?)", [msgId, params.id, "user", text])
+    db.run("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?", [params.id])
+
+    // AI response placeholder — records usage with estimated tokens
+    const inputTokens = Math.ceil(text.length / 4)
+    const outputTokens = Math.ceil(text.length / 2)
+    const provider = body.model?.providerID || "anthropic"
+    const model = body.model?.modelID || "claude-sonnet-4-5"
+    const cost = calculateCost(provider, model, inputTokens, outputTokens)
+
+    const user = db.query("SELECT team_id FROM users WHERE id = ?").get(userId) as any
+    db.run(
+      "INSERT INTO usage_events (user_id, team_id, provider, model, session_id, input_tokens, output_tokens, cost_usd) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+      [userId, user?.team_id || null, provider, model, params.id, inputTokens, outputTokens, cost]
+    )
+
+    const aiMsgId = randomUUID()
+    db.run("INSERT INTO messages (id, session_id, role, content) VALUES (?, ?, ?, ?)", [aiMsgId, params.id, "assistant", `[AI response — ${outputTokens} tokens, $${cost}]`])
+
+    const msgs = db.query("SELECT id, role, content, created_at FROM messages WHERE session_id = ? ORDER BY created_at").all(params.id)
+    return { data: { messages: msgs, usage: { input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: cost } } }
   })
 
   .get("/api/v1/sessions/:id/messages", async ({ userId, params }) => {
-    const session = db.sessions.get(params.id)
-    if (!session || session.userId !== userId) return { error: "Session not found" }
-    return { data: db.messages.get(params.id) || [] }
+    return { data: db.query("SELECT id, role, content, created_at FROM messages WHERE session_id = (SELECT id FROM sessions WHERE id = ? AND user_id = ?) ORDER BY created_at").all(params.id, userId) }
   })
 
-  // === Memory ===
-  .get("/api/v1/memory", async ({ userId }) => {
-    return { data: db.memory.get(userId!) || [] }
+  // --- Admin: Usage Stats ---
+  .guard({ beforeHandle: ({ userId, userRole, set }) => {
+    if (!userId || !["admin", "team_admin"].includes(userRole || "")) { set.status = 403; return "Forbidden: admin access required" }
+  }})
+
+  .get("/api/v1/admin/usage/team", async ({ userTeamId, userRole, userId, query }) => {
+    const teamId = userRole === "admin" ? query.team_id : userTeamId
+    const from = query.from || "1970-01-01"
+    const to = query.to || "2099-12-31"
+    const rows = db.query(`
+      SELECT date(created_at) as day, provider, model,
+        SUM(input_tokens) as input_tokens,
+        SUM(output_tokens) as output_tokens,
+        COUNT(*) as request_count,
+        SUM(cost_usd) as total_cost
+      FROM usage_events
+      WHERE team_id = ? AND created_at BETWEEN ? AND ?
+      GROUP BY 1, 2, 3
+      ORDER BY 1 DESC
+    `).all(teamId, from, to)
+    return { data: rows }
   })
 
-  .post("/api/v1/memory", async ({ userId, body }) => {
-    const list = db.memory.get(userId!) || []
-    const entry = { id: uuid(), ...body, createdAt: new Date().toISOString() }
-    list.push(entry)
-    db.memory.set(userId!, list)
-    return { data: entry }
+  .get("/api/v1/admin/usage/users", async ({ userTeamId, userRole, query }) => {
+    const teamId = userRole === "admin" ? query.team_id : userTeamId
+    const from = query.from || "1970-01-01"
+    const to = query.to || "2099-12-31"
+    const rows = db.query(`
+      SELECT u.display_name, u.email, ue.user_id,
+        SUM(ue.input_tokens) as input_tokens,
+        SUM(ue.output_tokens) as output_tokens,
+        COUNT(*) as request_count,
+        SUM(ue.cost_usd) as total_cost
+      FROM usage_events ue
+      JOIN users u ON u.id = ue.user_id
+      WHERE ue.team_id = ? AND ue.created_at BETWEEN ? AND ?
+      GROUP BY 1, 2, 3
+      ORDER BY total_cost DESC
+    `).all(teamId, from, to)
+    return { data: rows }
   })
 
-  // === Skills ===
-  .get("/api/v1/skills", async ({ userId }) => {
-    return { data: db.skills.get(userId!) || [] }
+  .get("/api/v1/admin/usage/minute", async ({ userTeamId, userRole, userId, query }) => {
+    const teamId = userRole === "admin" ? query.team_id : userTeamId
+    const targetUser = query.user_id || userId
+    const from = query.from || "1970-01-01"
+    const to = query.to || "2099-12-31"
+    const rows = db.query(`
+      SELECT created_at as minute,
+        input_tokens, output_tokens, provider, model, cost_usd
+      FROM usage_events
+      WHERE user_id = ? AND team_id = ? AND created_at BETWEEN ? AND ?
+      ORDER BY created_at DESC
+      LIMIT 1000
+    `).all(targetUser, teamId, from, to)
+    return { data: rows }
   })
 
-  .post("/api/v1/skills", async ({ userId, body }) => {
-    const list = db.skills.get(userId!) || []
-    const skill = { id: uuid(), ...body, createdAt: new Date().toISOString() }
-    list.push(skill)
-    db.skills.set(userId!, list)
-    return { data: skill }
+  .get("/api/v1/admin/usage/today", async ({ userTeamId, userRole }) => {
+    const teamId = userTeamId
+    const rows = db.query(`
+      SELECT
+        SUM(input_tokens) as input_tokens,
+        SUM(output_tokens) as output_tokens,
+        COUNT(*) as request_count,
+        SUM(cost_usd) as total_cost,
+        COUNT(DISTINCT user_id) as active_users
+      FROM usage_events
+      WHERE team_id = ? AND date(created_at) = date('now')
+    `).get(teamId)
+    return { data: rows }
   })
 
-  // === Workflows ===
-  .get("/api/v1/workflows", async ({ userId }) => {
-    return { data: db.workflows.get(userId!) || [] }
+  .get("/api/v1/admin/teams", async ({ userRole, set }) => {
+    if (userRole !== "admin") { set.status = 403; return { error: "Admin only" } }
+    return { data: db.query("SELECT id, name, owner_id, created_at FROM teams ORDER BY created_at").all() }
   })
 
-  .post("/api/v1/workflows", async ({ userId, body }) => {
-    const list = db.workflows.get(userId!) || []
-    const wf = { id: uuid(), ...body, createdAt: new Date().toISOString() }
-    list.push(wf)
-    db.workflows.set(userId!, list)
-    return { data: wf }
+  .post("/api/v1/admin/teams", async ({ userRole, body, set }) => {
+    if (userRole !== "admin") { set.status = 403; return { error: "Admin only" } }
+    const id = randomUUID()
+    db.run("INSERT INTO teams (id, name, owner_id) VALUES (?, ?, ?)", [id, body.name, body.owner_id])
+    db.run("INSERT INTO team_members (team_id, user_id, role) VALUES (?, ?, 'team_admin')", [id, body.owner_id])
+    db.run("UPDATE users SET team_id = ?, role = 'team_admin' WHERE id = ?", [id, body.owner_id])
+    return { data: { id, name: body.name } }
+  }, { body: t.Object({ name: t.String(), owner_id: t.String() }) })
+
+  // --- Memory ---
+  .get("/api/v1/memory", async ({ userId }) => {
+    const rows = db.query("SELECT id, category, content, created_at FROM memory_entries WHERE user_id = ? ORDER BY created_at DESC LIMIT 100").all(userId)
+    return { data: rows }
+  })
+
+  .post("/api/v1/memory", async ({ userId, body }) => {
+    if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'").get()) {
+      db.run("CREATE TABLE memory_entries (id TEXT PRIMARY KEY, user_id TEXT, category TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
+    }
+    const id = randomUUID()
+    db.run("INSERT INTO memory_entries (id, user_id, category, content) VALUES (?, ?, ?, ?)", [id, userId, body.category, body.content])
+    return { data: { id, category: body.category } }
+  })
+
+  // --- Skills ---
+  .get("/api/v1/skills", async ({ userId }) => {
+    if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'").get()) {
+      db.run("CREATE TABLE skills (id TEXT PRIMARY KEY, user_id TEXT, name TEXT, description TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
+    }
+    return { data: db.query("SELECT id, name, description, created_at FROM skills WHERE user_id = ? ORDER BY created_at DESC").all(userId) }
+  })
+
+  .post("/api/v1/skills", async ({ userId, body }) => {
+    if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'").get()) {
+      db.run("CREATE TABLE skills (id TEXT PRIMARY KEY, user_id TEXT, name TEXT, description TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
+    }
+    const id = randomUUID()
+    db.run("INSERT INTO skills (id, user_id, name, description, content) VALUES (?, ?, ?, ?, ?)", [id, userId, body.name, body.description, body.content])
+    return { data: { id, name: body.name } }
   })
 
-  // === Health ===
-  .get("/api/v1/health", () => ({ status: "ok", version: "0.4.0" }))
+  // --- Health ---
+  .get("/api/v1/health", () => ({
+    status: "ok",
+    version: "0.4.0",
+    db: db.query("SELECT COUNT(*) as users FROM users").get(),
+  }))
 
   .listen(PORT)
 
-console.log(`🦄 KirinCode Cloud Server running on http://localhost:${PORT}`)
+console.log(`🦄 KirinCode Cloud Server v0.4.0 — http://localhost:${PORT}`)
+console.log(`   DB: ${DB_PATH} (${db.query("SELECT COUNT(*) as c FROM users").get() || 0} users)`)
 console.log(`   API: http://localhost:${PORT}/api/v1/health`)
 
 export type App = typeof app

+ 1 - 1
packages/kirincode-workflow/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/kirincode-workflow",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "description": "KirinCode workflow engine — scheduled tasks, event triggers, background agents",
   "license": "MIT",

+ 1 - 1
packages/kirincode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "kirincode",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/llm/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "name": "@kirincode-ai/llm",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/mobile/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/mobile",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "scripts": {
     "start": "expo start",

+ 1 - 1
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/plugin",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/sdk",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/server/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/server",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/session-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/session-ui",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/slack",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/stats/app/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-app",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/stats/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-core",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/stats/server/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-server",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/tui/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/tui",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/ui",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "type": "module",
   "license": "MIT",
   "repository": {

+ 1 - 1
packages/web/package.json

@@ -2,7 +2,7 @@
   "name": "@kirincode-ai/web",
   "type": "module",
   "license": "MIT",
-  "version": "0.3.3",
+  "version": "0.4.0",
   "scripts": {
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.kirincode.ai astro dev",