@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.
Files changed (136) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +330 -0
  3. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/dist/assets/index-B4ru3EJb.css +32 -0
  63. package/dist/assets/index-DDFuyrpY.js +154 -0
  64. package/dist/assets/vendor-codemirror-C_VWDoZS.js +39 -0
  65. package/dist/assets/vendor-icons-CJV4dnDL.js +326 -0
  66. package/dist/assets/vendor-katex-DK8hFnhL.js +261 -0
  67. package/dist/assets/vendor-markdown-VwNYkg_0.js +35 -0
  68. package/dist/assets/vendor-react-BeVl62c0.js +59 -0
  69. package/dist/assets/vendor-syntax-CdGaPJRS.js +16 -0
  70. package/dist/assets/vendor-utils-00TdZexr.js +1 -0
  71. package/dist/assets/vendor-xterm-CvdiG4-n.js +66 -0
  72. package/dist/clear-cache.html +85 -0
  73. package/dist/convert-icons.md +53 -0
  74. package/dist/favicon.png +0 -0
  75. package/dist/favicon.svg +9 -0
  76. package/dist/generate-icons.js +49 -0
  77. package/dist/icons/claude-ai-icon.svg +1 -0
  78. package/dist/icons/codex-white.svg +3 -0
  79. package/dist/icons/codex.svg +3 -0
  80. package/dist/icons/cursor-white.svg +12 -0
  81. package/dist/icons/cursor.svg +1 -0
  82. package/dist/icons/generate-icons.md +19 -0
  83. package/dist/icons/icon-128x128.png +0 -0
  84. package/dist/icons/icon-128x128.svg +12 -0
  85. package/dist/icons/icon-144x144.png +0 -0
  86. package/dist/icons/icon-144x144.svg +12 -0
  87. package/dist/icons/icon-152x152.png +0 -0
  88. package/dist/icons/icon-152x152.svg +12 -0
  89. package/dist/icons/icon-192x192.png +0 -0
  90. package/dist/icons/icon-192x192.svg +12 -0
  91. package/dist/icons/icon-384x384.png +0 -0
  92. package/dist/icons/icon-384x384.svg +12 -0
  93. package/dist/icons/icon-512x512.png +0 -0
  94. package/dist/icons/icon-512x512.svg +12 -0
  95. package/dist/icons/icon-72x72.png +0 -0
  96. package/dist/icons/icon-72x72.svg +12 -0
  97. package/dist/icons/icon-96x96.png +0 -0
  98. package/dist/icons/icon-96x96.svg +12 -0
  99. package/dist/icons/icon-template.svg +12 -0
  100. package/dist/index.html +57 -0
  101. package/dist/logo-128.png +0 -0
  102. package/dist/logo-256.png +0 -0
  103. package/dist/logo-32.png +0 -0
  104. package/dist/logo-512.png +0 -0
  105. package/dist/logo-64.png +0 -0
  106. package/dist/logo.svg +17 -0
  107. package/dist/manifest.json +61 -0
  108. package/dist/screenshots/cli-selection.png +0 -0
  109. package/dist/screenshots/desktop-main.png +0 -0
  110. package/dist/screenshots/mobile-chat.png +0 -0
  111. package/dist/screenshots/tools-modal.png +0 -0
  112. package/dist/sw.js +49 -0
  113. package/package.json +113 -0
  114. package/server/claude-sdk.js +791 -0
  115. package/server/cli.js +330 -0
  116. package/server/database/auth.db +0 -0
  117. package/server/database/db.js +523 -0
  118. package/server/database/init.sql +23 -0
  119. package/server/index.js +1678 -0
  120. package/server/load-env.js +27 -0
  121. package/server/middleware/auth.js +118 -0
  122. package/server/projects.js +899 -0
  123. package/server/routes/admin.js +89 -0
  124. package/server/routes/auth.js +144 -0
  125. package/server/routes/commands.js +570 -0
  126. package/server/routes/mcp-utils.js +37 -0
  127. package/server/routes/mcp.js +593 -0
  128. package/server/routes/projects.js +216 -0
  129. package/server/routes/skills.js +891 -0
  130. package/server/routes/usage.js +206 -0
  131. package/server/services/pricing.js +196 -0
  132. package/server/services/usage-scanner.js +283 -0
  133. package/server/services/user-directories.js +123 -0
  134. package/server/utils/commandParser.js +303 -0
  135. package/server/utils/mcp-detector.js +73 -0
  136. 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);