@ian2018cs/agenthub 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +675 -0
- package/README.md +330 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-B4ru3EJb.css +32 -0
- package/dist/assets/index-DDFuyrpY.js +154 -0
- package/dist/assets/vendor-codemirror-C_VWDoZS.js +39 -0
- package/dist/assets/vendor-icons-CJV4dnDL.js +326 -0
- package/dist/assets/vendor-katex-DK8hFnhL.js +261 -0
- package/dist/assets/vendor-markdown-VwNYkg_0.js +35 -0
- package/dist/assets/vendor-react-BeVl62c0.js +59 -0
- package/dist/assets/vendor-syntax-CdGaPJRS.js +16 -0
- package/dist/assets/vendor-utils-00TdZexr.js +1 -0
- package/dist/assets/vendor-xterm-CvdiG4-n.js +66 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +9 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-128x128.svg +12 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-144x144.svg +12 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-152x152.svg +12 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-192x192.svg +12 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-384x384.svg +12 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-512x512.svg +12 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-72x72.svg +12 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/icon-96x96.svg +12 -0
- package/dist/icons/icon-template.svg +12 -0
- package/dist/index.html +57 -0
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/dist/logo.svg +17 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/cli-selection.png +0 -0
- package/dist/screenshots/desktop-main.png +0 -0
- package/dist/screenshots/mobile-chat.png +0 -0
- package/dist/screenshots/tools-modal.png +0 -0
- package/dist/sw.js +49 -0
- package/package.json +113 -0
- package/server/claude-sdk.js +791 -0
- package/server/cli.js +330 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +523 -0
- package/server/database/init.sql +23 -0
- package/server/index.js +1678 -0
- package/server/load-env.js +27 -0
- package/server/middleware/auth.js +118 -0
- package/server/projects.js +899 -0
- package/server/routes/admin.js +89 -0
- package/server/routes/auth.js +144 -0
- package/server/routes/commands.js +570 -0
- package/server/routes/mcp-utils.js +37 -0
- package/server/routes/mcp.js +593 -0
- package/server/routes/projects.js +216 -0
- package/server/routes/skills.js +891 -0
- package/server/routes/usage.js +206 -0
- package/server/services/pricing.js +196 -0
- package/server/services/usage-scanner.js +283 -0
- package/server/services/user-directories.js +123 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/mcp-detector.js +73 -0
- package/shared/modelConstants.js +23 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// ANSI color codes for terminal output
|
|
11
|
+
const colors = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bright: '\x1b[1m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const c = {
|
|
19
|
+
info: (text) => `${colors.cyan}${text}${colors.reset}`,
|
|
20
|
+
bright: (text) => `${colors.bright}${text}${colors.reset}`,
|
|
21
|
+
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Use DATABASE_PATH environment variable if set, otherwise use default location
|
|
25
|
+
// Resolve relative paths from project root (one level up from server/)
|
|
26
|
+
const DB_PATH = process.env.DATABASE_PATH
|
|
27
|
+
? path.resolve(path.join(__dirname, '../..'), process.env.DATABASE_PATH)
|
|
28
|
+
: path.join(__dirname, 'auth.db');
|
|
29
|
+
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
|
|
30
|
+
|
|
31
|
+
// Ensure database directory exists if custom path is provided
|
|
32
|
+
if (process.env.DATABASE_PATH) {
|
|
33
|
+
const dbDir = path.dirname(DB_PATH);
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(dbDir)) {
|
|
36
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
37
|
+
console.log(`Created database directory: ${dbDir}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Failed to create database directory ${dbDir}:`, error.message);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create database connection
|
|
46
|
+
const db = new Database(DB_PATH);
|
|
47
|
+
|
|
48
|
+
// Show app installation path prominently
|
|
49
|
+
const appInstallPath = path.join(__dirname, '../..');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(c.dim('═'.repeat(60)));
|
|
52
|
+
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`);
|
|
53
|
+
console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`);
|
|
54
|
+
if (process.env.DATABASE_PATH) {
|
|
55
|
+
console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(c.dim('═'.repeat(60)));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
const runMigrations = () => {
|
|
61
|
+
try {
|
|
62
|
+
const tableInfo = db.prepare("PRAGMA table_info(users)").all();
|
|
63
|
+
const columnNames = tableInfo.map(col => col.name);
|
|
64
|
+
|
|
65
|
+
// Create usage_records table for tracking token usage
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS usage_records (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
user_uuid TEXT NOT NULL,
|
|
70
|
+
session_id TEXT,
|
|
71
|
+
model TEXT NOT NULL,
|
|
72
|
+
input_tokens INTEGER DEFAULT 0,
|
|
73
|
+
output_tokens INTEGER DEFAULT 0,
|
|
74
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
75
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
76
|
+
cost_usd REAL DEFAULT 0,
|
|
77
|
+
source TEXT DEFAULT 'sdk',
|
|
78
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_usage_records_user_uuid ON usage_records(user_uuid)');
|
|
82
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_usage_records_created_at ON usage_records(created_at)');
|
|
83
|
+
|
|
84
|
+
// Create usage_daily_summary table for aggregated stats
|
|
85
|
+
db.exec(`
|
|
86
|
+
CREATE TABLE IF NOT EXISTS usage_daily_summary (
|
|
87
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
88
|
+
user_uuid TEXT NOT NULL,
|
|
89
|
+
date TEXT NOT NULL,
|
|
90
|
+
model TEXT NOT NULL,
|
|
91
|
+
total_input_tokens INTEGER DEFAULT 0,
|
|
92
|
+
total_output_tokens INTEGER DEFAULT 0,
|
|
93
|
+
total_cost_usd REAL DEFAULT 0,
|
|
94
|
+
session_count INTEGER DEFAULT 0,
|
|
95
|
+
request_count INTEGER DEFAULT 0,
|
|
96
|
+
UNIQUE(user_uuid, date, model)
|
|
97
|
+
)
|
|
98
|
+
`);
|
|
99
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_usage_daily_summary_user_date ON usage_daily_summary(user_uuid, date)');
|
|
100
|
+
|
|
101
|
+
if (!columnNames.includes('git_name')) {
|
|
102
|
+
console.log('Running migration: Adding git_name column');
|
|
103
|
+
db.exec('ALTER TABLE users ADD COLUMN git_name TEXT');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!columnNames.includes('git_email')) {
|
|
107
|
+
console.log('Running migration: Adding git_email column');
|
|
108
|
+
db.exec('ALTER TABLE users ADD COLUMN git_email TEXT');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!columnNames.includes('has_completed_onboarding')) {
|
|
112
|
+
console.log('Running migration: Adding has_completed_onboarding column');
|
|
113
|
+
db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add uuid column if not exists (without UNIQUE constraint - use index instead)
|
|
117
|
+
if (!columnNames.includes('uuid')) {
|
|
118
|
+
console.log('Running migration: Adding uuid column');
|
|
119
|
+
db.exec('ALTER TABLE users ADD COLUMN uuid TEXT');
|
|
120
|
+
}
|
|
121
|
+
// Create unique index for uuid (safe to run even if already exists)
|
|
122
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_uuid ON users(uuid)');
|
|
123
|
+
|
|
124
|
+
// Add role column if not exists
|
|
125
|
+
if (!columnNames.includes('role')) {
|
|
126
|
+
console.log('Running migration: Adding role column');
|
|
127
|
+
db.exec("ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'");
|
|
128
|
+
}
|
|
129
|
+
// Create index for role (safe to run even if already exists)
|
|
130
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)');
|
|
131
|
+
|
|
132
|
+
// Add status column if not exists
|
|
133
|
+
if (!columnNames.includes('status')) {
|
|
134
|
+
console.log('Running migration: Adding status column');
|
|
135
|
+
db.exec("ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'");
|
|
136
|
+
}
|
|
137
|
+
// Create index for status (safe to run even if already exists)
|
|
138
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)');
|
|
139
|
+
|
|
140
|
+
console.log('Database migrations completed successfully');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error running migrations:', error.message);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Initialize database with schema
|
|
148
|
+
const initializeDatabase = async () => {
|
|
149
|
+
try {
|
|
150
|
+
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
|
|
151
|
+
db.exec(initSQL);
|
|
152
|
+
console.log('Database initialized successfully');
|
|
153
|
+
runMigrations();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Error initializing database:', error.message);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// User database operations
|
|
161
|
+
const userDb = {
|
|
162
|
+
// Check if any users exist
|
|
163
|
+
hasUsers: () => {
|
|
164
|
+
try {
|
|
165
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
166
|
+
return row.count > 0;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Create a new user
|
|
173
|
+
createUser: (username, passwordHash) => {
|
|
174
|
+
try {
|
|
175
|
+
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
|
|
176
|
+
const result = stmt.run(username, passwordHash);
|
|
177
|
+
return { id: result.lastInsertRowid, username };
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// Get user by username
|
|
184
|
+
getUserByUsername: (username) => {
|
|
185
|
+
try {
|
|
186
|
+
const row = db.prepare('SELECT * FROM users WHERE username = ? AND is_active = 1').get(username);
|
|
187
|
+
return row;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Update last login time
|
|
194
|
+
updateLastLogin: (userId) => {
|
|
195
|
+
try {
|
|
196
|
+
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Get user by ID
|
|
203
|
+
getUserById: (userId) => {
|
|
204
|
+
try {
|
|
205
|
+
const row = db.prepare('SELECT id, username, uuid, role, status, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
|
|
206
|
+
return row;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
getFirstUser: () => {
|
|
213
|
+
try {
|
|
214
|
+
const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 LIMIT 1').get();
|
|
215
|
+
return row;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
updateGitConfig: (userId, gitName, gitEmail) => {
|
|
222
|
+
try {
|
|
223
|
+
const stmt = db.prepare('UPDATE users SET git_name = ?, git_email = ? WHERE id = ?');
|
|
224
|
+
stmt.run(gitName, gitEmail, userId);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
getGitConfig: (userId) => {
|
|
231
|
+
try {
|
|
232
|
+
const row = db.prepare('SELECT git_name, git_email FROM users WHERE id = ?').get(userId);
|
|
233
|
+
return row;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
completeOnboarding: (userId) => {
|
|
240
|
+
try {
|
|
241
|
+
const stmt = db.prepare('UPDATE users SET has_completed_onboarding = 1 WHERE id = ?');
|
|
242
|
+
stmt.run(userId);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
hasCompletedOnboarding: (userId) => {
|
|
249
|
+
try {
|
|
250
|
+
const row = db.prepare('SELECT has_completed_onboarding FROM users WHERE id = ?').get(userId);
|
|
251
|
+
return row?.has_completed_onboarding === 1;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// Get user count
|
|
258
|
+
getUserCount: () => {
|
|
259
|
+
try {
|
|
260
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
261
|
+
return row.count;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// Create user with full details
|
|
268
|
+
createUserFull: (username, passwordHash, uuid, role) => {
|
|
269
|
+
try {
|
|
270
|
+
const stmt = db.prepare(
|
|
271
|
+
'INSERT INTO users (username, password_hash, uuid, role) VALUES (?, ?, ?, ?)'
|
|
272
|
+
);
|
|
273
|
+
const result = stmt.run(username, passwordHash, uuid, role);
|
|
274
|
+
return { id: result.lastInsertRowid, username, uuid, role };
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// Get all users (for admin)
|
|
281
|
+
getAllUsers: () => {
|
|
282
|
+
try {
|
|
283
|
+
return db.prepare(
|
|
284
|
+
'SELECT id, username, uuid, role, status, created_at, last_login FROM users ORDER BY created_at DESC'
|
|
285
|
+
).all();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// Update user status
|
|
292
|
+
updateUserStatus: (userId, status) => {
|
|
293
|
+
try {
|
|
294
|
+
db.prepare('UPDATE users SET status = ? WHERE id = ?').run(status, userId);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// Delete user by ID
|
|
301
|
+
deleteUserById: (userId) => {
|
|
302
|
+
try {
|
|
303
|
+
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// Get user by UUID
|
|
310
|
+
getUserByUuid: (uuid) => {
|
|
311
|
+
try {
|
|
312
|
+
return db.prepare('SELECT * FROM users WHERE uuid = ?').get(uuid);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Usage database operations
|
|
320
|
+
const usageDb = {
|
|
321
|
+
// Insert a usage record
|
|
322
|
+
insertRecord: (record) => {
|
|
323
|
+
try {
|
|
324
|
+
const stmt = db.prepare(`
|
|
325
|
+
INSERT INTO usage_records (user_uuid, session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, cost_usd, source, created_at)
|
|
326
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
+
`);
|
|
328
|
+
const result = stmt.run(
|
|
329
|
+
record.user_uuid,
|
|
330
|
+
record.session_id || null,
|
|
331
|
+
record.model,
|
|
332
|
+
record.input_tokens || 0,
|
|
333
|
+
record.output_tokens || 0,
|
|
334
|
+
record.cache_read_tokens || 0,
|
|
335
|
+
record.cache_creation_tokens || 0,
|
|
336
|
+
record.cost_usd || 0,
|
|
337
|
+
record.source || 'sdk',
|
|
338
|
+
record.created_at || new Date().toISOString()
|
|
339
|
+
);
|
|
340
|
+
return result.lastInsertRowid;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
// Upsert daily summary
|
|
347
|
+
upsertDailySummary: (summary) => {
|
|
348
|
+
try {
|
|
349
|
+
const stmt = db.prepare(`
|
|
350
|
+
INSERT INTO usage_daily_summary (user_uuid, date, model, total_input_tokens, total_output_tokens, total_cost_usd, session_count, request_count)
|
|
351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
352
|
+
ON CONFLICT(user_uuid, date, model) DO UPDATE SET
|
|
353
|
+
total_input_tokens = total_input_tokens + excluded.total_input_tokens,
|
|
354
|
+
total_output_tokens = total_output_tokens + excluded.total_output_tokens,
|
|
355
|
+
total_cost_usd = total_cost_usd + excluded.total_cost_usd,
|
|
356
|
+
session_count = session_count + excluded.session_count,
|
|
357
|
+
request_count = request_count + excluded.request_count
|
|
358
|
+
`);
|
|
359
|
+
stmt.run(
|
|
360
|
+
summary.user_uuid,
|
|
361
|
+
summary.date,
|
|
362
|
+
summary.model,
|
|
363
|
+
summary.total_input_tokens || 0,
|
|
364
|
+
summary.total_output_tokens || 0,
|
|
365
|
+
summary.total_cost_usd || 0,
|
|
366
|
+
summary.session_count || 0,
|
|
367
|
+
summary.request_count || 0
|
|
368
|
+
);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
// Get all users usage summary
|
|
375
|
+
getAllUsersSummary: () => {
|
|
376
|
+
try {
|
|
377
|
+
return db.prepare(`
|
|
378
|
+
SELECT
|
|
379
|
+
user_uuid,
|
|
380
|
+
SUM(total_cost_usd) as total_cost,
|
|
381
|
+
SUM(request_count) as total_requests,
|
|
382
|
+
SUM(session_count) as total_sessions,
|
|
383
|
+
MAX(date) as last_active
|
|
384
|
+
FROM usage_daily_summary
|
|
385
|
+
GROUP BY user_uuid
|
|
386
|
+
ORDER BY total_cost DESC
|
|
387
|
+
`).all();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
// Get user usage by period
|
|
394
|
+
getUserUsageByPeriod: (userUuid, startDate, endDate) => {
|
|
395
|
+
try {
|
|
396
|
+
return db.prepare(`
|
|
397
|
+
SELECT
|
|
398
|
+
date,
|
|
399
|
+
model,
|
|
400
|
+
total_input_tokens,
|
|
401
|
+
total_output_tokens,
|
|
402
|
+
total_cost_usd,
|
|
403
|
+
session_count,
|
|
404
|
+
request_count
|
|
405
|
+
FROM usage_daily_summary
|
|
406
|
+
WHERE user_uuid = ? AND date >= ? AND date <= ?
|
|
407
|
+
ORDER BY date DESC
|
|
408
|
+
`).all(userUuid, startDate, endDate);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// Get user total usage
|
|
415
|
+
getUserTotalUsage: (userUuid) => {
|
|
416
|
+
try {
|
|
417
|
+
return db.prepare(`
|
|
418
|
+
SELECT
|
|
419
|
+
SUM(total_cost_usd) as total_cost,
|
|
420
|
+
SUM(total_input_tokens) as total_input_tokens,
|
|
421
|
+
SUM(total_output_tokens) as total_output_tokens,
|
|
422
|
+
SUM(request_count) as total_requests,
|
|
423
|
+
SUM(session_count) as total_sessions
|
|
424
|
+
FROM usage_daily_summary
|
|
425
|
+
WHERE user_uuid = ?
|
|
426
|
+
`).get(userUuid);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
// Get model distribution for a user
|
|
433
|
+
getUserModelDistribution: (userUuid, startDate, endDate) => {
|
|
434
|
+
try {
|
|
435
|
+
return db.prepare(`
|
|
436
|
+
SELECT
|
|
437
|
+
model,
|
|
438
|
+
SUM(total_cost_usd) as cost,
|
|
439
|
+
SUM(request_count) as requests
|
|
440
|
+
FROM usage_daily_summary
|
|
441
|
+
WHERE user_uuid = ? AND date >= ? AND date <= ?
|
|
442
|
+
GROUP BY model
|
|
443
|
+
ORDER BY cost DESC
|
|
444
|
+
`).all(userUuid, startDate, endDate);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
// Get global dashboard stats
|
|
451
|
+
getDashboardStats: (startDate, endDate) => {
|
|
452
|
+
try {
|
|
453
|
+
const totals = db.prepare(`
|
|
454
|
+
SELECT
|
|
455
|
+
SUM(total_cost_usd) as total_cost,
|
|
456
|
+
SUM(request_count) as total_requests,
|
|
457
|
+
SUM(session_count) as total_sessions,
|
|
458
|
+
COUNT(DISTINCT user_uuid) as active_users
|
|
459
|
+
FROM usage_daily_summary
|
|
460
|
+
WHERE date >= ? AND date <= ?
|
|
461
|
+
`).get(startDate, endDate);
|
|
462
|
+
|
|
463
|
+
const dailyTrend = db.prepare(`
|
|
464
|
+
SELECT
|
|
465
|
+
date,
|
|
466
|
+
SUM(total_cost_usd) as cost,
|
|
467
|
+
SUM(request_count) as requests
|
|
468
|
+
FROM usage_daily_summary
|
|
469
|
+
WHERE date >= ? AND date <= ?
|
|
470
|
+
GROUP BY date
|
|
471
|
+
ORDER BY date ASC
|
|
472
|
+
`).all(startDate, endDate);
|
|
473
|
+
|
|
474
|
+
const modelDistribution = db.prepare(`
|
|
475
|
+
SELECT
|
|
476
|
+
model,
|
|
477
|
+
SUM(total_cost_usd) as cost,
|
|
478
|
+
SUM(request_count) as requests
|
|
479
|
+
FROM usage_daily_summary
|
|
480
|
+
WHERE date >= ? AND date <= ?
|
|
481
|
+
GROUP BY model
|
|
482
|
+
ORDER BY cost DESC
|
|
483
|
+
`).all(startDate, endDate);
|
|
484
|
+
|
|
485
|
+
const topUsers = db.prepare(`
|
|
486
|
+
SELECT
|
|
487
|
+
user_uuid,
|
|
488
|
+
SUM(total_cost_usd) as total_cost,
|
|
489
|
+
SUM(request_count) as total_requests
|
|
490
|
+
FROM usage_daily_summary
|
|
491
|
+
WHERE date >= ? AND date <= ?
|
|
492
|
+
GROUP BY user_uuid
|
|
493
|
+
ORDER BY total_cost DESC
|
|
494
|
+
LIMIT 10
|
|
495
|
+
`).all(startDate, endDate);
|
|
496
|
+
|
|
497
|
+
return { totals, dailyTrend, modelDistribution, topUsers };
|
|
498
|
+
} catch (err) {
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// Cleanup old records (older than specified days)
|
|
504
|
+
cleanupOldRecords: (days) => {
|
|
505
|
+
try {
|
|
506
|
+
const cutoffDate = new Date();
|
|
507
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
508
|
+
const cutoffStr = cutoffDate.toISOString();
|
|
509
|
+
|
|
510
|
+
const result = db.prepare('DELETE FROM usage_records WHERE created_at < ?').run(cutoffStr);
|
|
511
|
+
return result.changes;
|
|
512
|
+
} catch (err) {
|
|
513
|
+
throw err;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
export {
|
|
519
|
+
db,
|
|
520
|
+
initializeDatabase,
|
|
521
|
+
userDb,
|
|
522
|
+
usageDb
|
|
523
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Initialize authentication database
|
|
2
|
+
PRAGMA foreign_keys = ON;
|
|
3
|
+
|
|
4
|
+
-- Users table (multi-user system)
|
|
5
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
username TEXT UNIQUE NOT NULL,
|
|
8
|
+
password_hash TEXT NOT NULL,
|
|
9
|
+
uuid TEXT,
|
|
10
|
+
role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user')),
|
|
11
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'disabled')),
|
|
12
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
last_login DATETIME,
|
|
14
|
+
is_active BOOLEAN DEFAULT 1,
|
|
15
|
+
git_name TEXT,
|
|
16
|
+
git_email TEXT,
|
|
17
|
+
has_completed_onboarding BOOLEAN DEFAULT 0
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- Indexes for performance (base indexes only)
|
|
21
|
+
-- Note: Indexes for uuid, role, status are created in migrations to support upgrades
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
|