@kamel-ahmed/proxy-claude 1.0.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Data Store
3
+ * Holds Accounts, Models, and Computed Quota Rows
4
+ * Shared between Dashboard and AccountManager
5
+ */
6
+
7
+ // utils is loaded globally as window.utils in utils.js
8
+
9
+ document.addEventListener('alpine:init', () => {
10
+ Alpine.store('data', {
11
+ accounts: [],
12
+ models: [], // Source of truth
13
+ modelConfig: {}, // Model metadata (hidden, pinned, alias)
14
+ quotaRows: [], // Filtered view
15
+ usageHistory: {}, // Usage statistics history (from /account-limits?includeHistory=true)
16
+ loading: false,
17
+ initialLoad: true, // Track first load for skeleton screen
18
+ connectionStatus: 'connecting',
19
+ lastUpdated: '-',
20
+ healthCheckTimer: null,
21
+
22
+ // Filters state
23
+ filters: {
24
+ account: 'all',
25
+ family: 'all',
26
+ search: '',
27
+ sortCol: 'avgQuota',
28
+ sortAsc: true
29
+ },
30
+
31
+ // Settings for calculation
32
+ // We need to access global settings? Or duplicate?
33
+ // Let's assume settings are passed or in another store.
34
+ // For simplicity, let's keep relevant filters here.
35
+
36
+ init() {
37
+ // Restore from cache first for instant render
38
+ this.loadFromCache();
39
+
40
+ // Watch filters to recompute
41
+ // Alpine stores don't have $watch automatically unless inside a component?
42
+ // We can manually call compute when filters change.
43
+
44
+ // Start health check monitoring
45
+ this.startHealthCheck();
46
+ },
47
+
48
+ loadFromCache() {
49
+ try {
50
+ const cached = localStorage.getItem('ag_data_cache');
51
+ if (cached) {
52
+ const data = JSON.parse(cached);
53
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
54
+
55
+ // Check TTL
56
+ if (data.timestamp && (Date.now() - data.timestamp > CACHE_TTL)) {
57
+ console.log('Cache expired, skipping restoration');
58
+ localStorage.removeItem('ag_data_cache');
59
+ return;
60
+ }
61
+
62
+ // Basic validity check
63
+ if (data.accounts && data.models) {
64
+ this.accounts = data.accounts;
65
+ this.models = data.models;
66
+ this.modelConfig = data.modelConfig || {};
67
+ this.usageHistory = data.usageHistory || {};
68
+
69
+ // Don't show loading on initial load if we have cache
70
+ this.initialLoad = false;
71
+ this.computeQuotaRows();
72
+ console.log('Restored data from cache');
73
+ }
74
+ }
75
+ } catch (e) {
76
+ console.warn('Failed to load cache', e);
77
+ }
78
+ },
79
+
80
+ saveToCache() {
81
+ try {
82
+ const cacheData = {
83
+ accounts: this.accounts,
84
+ models: this.models,
85
+ modelConfig: this.modelConfig,
86
+ usageHistory: this.usageHistory,
87
+ timestamp: Date.now()
88
+ };
89
+ localStorage.setItem('ag_data_cache', JSON.stringify(cacheData));
90
+ } catch (e) {
91
+ console.warn('Failed to save cache', e);
92
+ }
93
+ },
94
+
95
+ async fetchData() {
96
+ // Only show skeleton on initial load if we didn't restore from cache
97
+ if (this.initialLoad) {
98
+ this.loading = true;
99
+ }
100
+ try {
101
+ // Get password from global store
102
+ const password = Alpine.store('global').webuiPassword;
103
+
104
+ // Include history for dashboard (single API call optimization)
105
+ const url = '/account-limits?includeHistory=true';
106
+ const { response, newPassword } = await window.utils.request(url, {}, password);
107
+
108
+ if (newPassword) Alpine.store('global').webuiPassword = newPassword;
109
+
110
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
111
+
112
+ const data = await response.json();
113
+ this.accounts = data.accounts || [];
114
+ if (data.models && data.models.length > 0) {
115
+ this.models = data.models;
116
+ }
117
+ this.modelConfig = data.modelConfig || {};
118
+
119
+ // Store usage history if included (for dashboard)
120
+ if (data.history) {
121
+ this.usageHistory = data.history;
122
+ }
123
+
124
+ this.saveToCache(); // Save fresh data
125
+ this.computeQuotaRows();
126
+
127
+ this.lastUpdated = new Date().toLocaleTimeString();
128
+
129
+ // Fetch version from config endpoint if not already loaded
130
+ if (this.initialLoad) {
131
+ this.fetchVersion(password);
132
+ }
133
+ } catch (error) {
134
+ console.error('Fetch error:', error);
135
+ const store = Alpine.store('global');
136
+ store.showToast(store.t('connectionLost'), 'error');
137
+ } finally {
138
+ this.loading = false;
139
+ this.initialLoad = false; // Mark initial load as complete
140
+ }
141
+ },
142
+
143
+ async fetchVersion(password) {
144
+ try {
145
+ const { response } = await window.utils.request('/api/config', {}, password);
146
+ if (response.ok) {
147
+ const data = await response.json();
148
+ if (data.version) {
149
+ Alpine.store('global').version = data.version;
150
+ }
151
+ }
152
+ } catch (error) {
153
+ console.warn('Failed to fetch version:', error);
154
+ }
155
+ },
156
+
157
+ async performHealthCheck() {
158
+ try {
159
+ // Get password from global store
160
+ const password = Alpine.store('global').webuiPassword;
161
+
162
+ // Use lightweight endpoint (no quota fetching)
163
+ const { response, newPassword } = await window.utils.request('/api/config', {}, password);
164
+
165
+ if (newPassword) Alpine.store('global').webuiPassword = newPassword;
166
+
167
+ if (response.ok) {
168
+ this.connectionStatus = 'connected';
169
+ } else {
170
+ this.connectionStatus = 'disconnected';
171
+ }
172
+ } catch (error) {
173
+ console.error('Health check error:', error);
174
+ this.connectionStatus = 'disconnected';
175
+ }
176
+ },
177
+
178
+ startHealthCheck() {
179
+ // Clear existing timer
180
+ if (this.healthCheckTimer) {
181
+ clearInterval(this.healthCheckTimer);
182
+ }
183
+
184
+ // Setup visibility change listener (only once)
185
+ if (!this._healthVisibilitySetup) {
186
+ this._healthVisibilitySetup = true;
187
+ this._visibilityHandler = () => {
188
+ if (document.hidden) {
189
+ // Tab hidden - stop health checks
190
+ this.stopHealthCheck();
191
+ } else {
192
+ // Tab visible - restart health checks
193
+ this.startHealthCheck();
194
+ }
195
+ };
196
+ document.addEventListener('visibilitychange', this._visibilityHandler);
197
+ }
198
+
199
+ // Perform immediate health check
200
+ this.performHealthCheck();
201
+
202
+ // Schedule regular health checks every 15 seconds
203
+ this.healthCheckTimer = setInterval(() => {
204
+ // Only perform health check if tab is visible
205
+ if (!document.hidden) {
206
+ this.performHealthCheck();
207
+ }
208
+ }, 15000);
209
+ },
210
+
211
+ stopHealthCheck() {
212
+ if (this.healthCheckTimer) {
213
+ clearInterval(this.healthCheckTimer);
214
+ this.healthCheckTimer = null;
215
+ }
216
+ },
217
+
218
+ computeQuotaRows() {
219
+ const models = this.models || [];
220
+ const rows = [];
221
+ const showExhausted = Alpine.store('settings')?.showExhausted ?? true;
222
+
223
+ models.forEach(modelId => {
224
+ // Config
225
+ const config = this.modelConfig[modelId] || {};
226
+ const family = this.getModelFamily(modelId);
227
+
228
+ // Visibility Logic for Models Page (quotaRows):
229
+ // 1. If explicitly hidden via config, ALWAYS hide (clean interface)
230
+ // 2. If no config, default 'unknown' families to HIDDEN
231
+ // 3. Known families (Claude/Gemini) default to VISIBLE
232
+ // Note: To manage hidden models, use Settings → Models tab
233
+ let isHidden = config.hidden;
234
+ if (isHidden === undefined) {
235
+ isHidden = (family === 'other' || family === 'unknown');
236
+ }
237
+
238
+ // Models Page: Check settings for visibility
239
+ const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
240
+ if (isHidden && !showHidden) return;
241
+
242
+ // Filters
243
+ if (this.filters.family !== 'all' && this.filters.family !== family) return;
244
+ if (this.filters.search) {
245
+ const searchLower = this.filters.search.toLowerCase();
246
+ const idMatch = modelId.toLowerCase().includes(searchLower);
247
+ if (!idMatch) return;
248
+ }
249
+
250
+ // Data Collection
251
+ const quotaInfo = [];
252
+ let minQuota = 100;
253
+ let totalQuotaSum = 0;
254
+ let validAccountCount = 0;
255
+ let minResetTime = null;
256
+
257
+ this.accounts.forEach(acc => {
258
+ if (this.filters.account !== 'all' && acc.email !== this.filters.account) return;
259
+
260
+ const limit = acc.limits?.[modelId];
261
+ if (!limit) return;
262
+
263
+ const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
264
+ minQuota = Math.min(minQuota, pct);
265
+
266
+ // Accumulate for average
267
+ totalQuotaSum += pct;
268
+ validAccountCount++;
269
+
270
+ if (limit.resetTime && (!minResetTime || new Date(limit.resetTime) < new Date(minResetTime))) {
271
+ minResetTime = limit.resetTime;
272
+ }
273
+
274
+ quotaInfo.push({
275
+ email: acc.email.split('@')[0],
276
+ fullEmail: acc.email,
277
+ pct: pct,
278
+ resetTime: limit.resetTime
279
+ });
280
+ });
281
+
282
+ if (quotaInfo.length === 0) return;
283
+ const avgQuota = validAccountCount > 0 ? Math.round(totalQuotaSum / validAccountCount) : 0;
284
+
285
+ if (!showExhausted && minQuota === 0) return;
286
+
287
+ rows.push({
288
+ modelId,
289
+ displayName: modelId, // Simplified: no longer using alias
290
+ family,
291
+ minQuota,
292
+ avgQuota, // Added Average Quota
293
+ minResetTime,
294
+ resetIn: minResetTime ? window.utils.formatTimeUntil(minResetTime) : '-',
295
+ quotaInfo,
296
+ pinned: !!config.pinned,
297
+ hidden: !!isHidden, // Use computed visibility
298
+ activeCount: quotaInfo.filter(q => q.pct > 0).length
299
+ });
300
+ });
301
+
302
+ // Sort: Pinned first, then by selected column
303
+ const sortCol = this.filters.sortCol;
304
+ const sortAsc = this.filters.sortAsc;
305
+
306
+ this.quotaRows = rows.sort((a, b) => {
307
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
308
+
309
+ let valA = a[sortCol];
310
+ let valB = b[sortCol];
311
+
312
+ // Handle nulls (always push to bottom)
313
+ if (valA === valB) return 0;
314
+ if (valA === null || valA === undefined) return 1;
315
+ if (valB === null || valB === undefined) return -1;
316
+
317
+ if (typeof valA === 'string' && typeof valB === 'string') {
318
+ return sortAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
319
+ }
320
+
321
+ return sortAsc ? valA - valB : valB - valA;
322
+ });
323
+
324
+ // Trigger Dashboard Update if active
325
+ // Ideally dashboard watches this store.
326
+ },
327
+
328
+ setSort(col) {
329
+ if (this.filters.sortCol === col) {
330
+ this.filters.sortAsc = !this.filters.sortAsc;
331
+ } else {
332
+ this.filters.sortCol = col;
333
+ // Default sort direction: Descending for numbers/stats, Ascending for text/time
334
+ if (['avgQuota', 'activeCount'].includes(col)) {
335
+ this.filters.sortAsc = false;
336
+ } else {
337
+ this.filters.sortAsc = true;
338
+ }
339
+ }
340
+ this.computeQuotaRows();
341
+ },
342
+
343
+ getModelFamily(modelId) {
344
+ const lower = modelId.toLowerCase();
345
+ if (lower.includes('claude')) return 'claude';
346
+ if (lower.includes('gemini')) return 'gemini';
347
+ return 'other';
348
+ },
349
+
350
+ /**
351
+ * Get quota data without filters applied (for Dashboard global charts)
352
+ * Returns array of { modelId, family, quotaInfo: [{pct}] }
353
+ */
354
+ getUnfilteredQuotaData() {
355
+ const models = this.models || [];
356
+ const rows = [];
357
+ const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
358
+
359
+ models.forEach(modelId => {
360
+ const config = this.modelConfig[modelId] || {};
361
+ const family = this.getModelFamily(modelId);
362
+
363
+ // Smart visibility (same logic as computeQuotaRows)
364
+ let isHidden = config.hidden;
365
+ if (isHidden === undefined) {
366
+ isHidden = (family === 'other' || family === 'unknown');
367
+ }
368
+ if (isHidden && !showHidden) return;
369
+
370
+ const quotaInfo = [];
371
+ // Use ALL accounts (no account filter)
372
+ this.accounts.forEach(acc => {
373
+ const limit = acc.limits?.[modelId];
374
+ if (!limit) return;
375
+ const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
376
+ quotaInfo.push({ pct });
377
+ });
378
+
379
+ // treat missing quotaInfo as 0%/unknown; still include row
380
+ rows.push({ modelId, family, quotaInfo });
381
+ });
382
+
383
+ return rows;
384
+ }
385
+ });
386
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Settings Store
3
+ */
4
+ document.addEventListener('alpine:init', () => {
5
+ Alpine.store('settings', {
6
+ refreshInterval: 60,
7
+ logLimit: 2000,
8
+ showExhausted: true,
9
+ showHiddenModels: false, // New field
10
+ compact: false,
11
+ port: 8080, // Display only
12
+
13
+ init() {
14
+ this.loadSettings();
15
+ },
16
+
17
+ // Call this method when toggling settings in the UI
18
+ toggle(key) {
19
+ if (this.hasOwnProperty(key) && typeof this[key] === 'boolean') {
20
+ this[key] = !this[key];
21
+ this.saveSettings(true);
22
+ }
23
+ },
24
+
25
+ loadSettings() {
26
+ const saved = localStorage.getItem('antigravity_settings');
27
+ if (saved) {
28
+ const parsed = JSON.parse(saved);
29
+ Object.keys(parsed).forEach(k => {
30
+ // Only load keys that exist in our default state (safety)
31
+ if (this.hasOwnProperty(k)) this[k] = parsed[k];
32
+ });
33
+ }
34
+ },
35
+
36
+ saveSettings(silent = false) {
37
+ const toSave = {
38
+ refreshInterval: this.refreshInterval,
39
+ logLimit: this.logLimit,
40
+ showExhausted: this.showExhausted,
41
+ showHiddenModels: this.showHiddenModels,
42
+ compact: this.compact
43
+ };
44
+ localStorage.setItem('antigravity_settings', JSON.stringify(toSave));
45
+
46
+ if (!silent) {
47
+ const store = Alpine.store('global');
48
+ store.showToast(store.t('configSaved'), 'success');
49
+ }
50
+
51
+ // Trigger updates
52
+ document.dispatchEvent(new CustomEvent('refresh-interval-changed'));
53
+ if (Alpine.store('data')) {
54
+ Alpine.store('data').computeQuotaRows();
55
+ }
56
+ }
57
+ });
58
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Global Store for Antigravity Console
3
+ * Handles Translations, Toasts, and Shared Config
4
+ */
5
+
6
+ document.addEventListener('alpine:init', () => {
7
+ Alpine.store('global', {
8
+ init() {
9
+ // Hash-based routing
10
+ const validTabs = ['dashboard', 'models', 'accounts', 'logs', 'settings'];
11
+ const getHash = () => window.location.hash.substring(1);
12
+
13
+ // 1. Initial load from hash
14
+ const initialHash = getHash();
15
+ if (validTabs.includes(initialHash)) {
16
+ this.activeTab = initialHash;
17
+ }
18
+
19
+ // 2. Sync State -> URL
20
+ Alpine.effect(() => {
21
+ if (validTabs.includes(this.activeTab) && getHash() !== this.activeTab) {
22
+ window.location.hash = this.activeTab;
23
+ }
24
+ });
25
+
26
+ // 3. Sync URL -> State (Back/Forward buttons)
27
+ window.addEventListener('hashchange', () => {
28
+ const hash = getHash();
29
+ if (validTabs.includes(hash) && this.activeTab !== hash) {
30
+ this.activeTab = hash;
31
+ }
32
+ });
33
+ },
34
+
35
+ // App State
36
+ version: '1.0.0',
37
+ activeTab: 'dashboard',
38
+ webuiPassword: localStorage.getItem('antigravity_webui_password') || '',
39
+
40
+ // i18n
41
+ lang: localStorage.getItem('app_lang') || 'en',
42
+ translations: window.translations || {},
43
+
44
+ // Toast Messages
45
+ toast: null,
46
+
47
+ // OAuth Progress
48
+ oauthProgress: {
49
+ active: false,
50
+ current: 0,
51
+ max: 60,
52
+ cancel: null
53
+ },
54
+
55
+ t(key, params = {}) {
56
+ let str = this.translations[this.lang][key] || key;
57
+ if (typeof str === 'string') {
58
+ Object.keys(params).forEach(p => {
59
+ str = str.replace(`{${p}}`, params[p]);
60
+ });
61
+ }
62
+ return str;
63
+ },
64
+
65
+ setLang(l) {
66
+ this.lang = l;
67
+ localStorage.setItem('app_lang', l);
68
+ },
69
+
70
+ showToast(message, type = 'info') {
71
+ const id = Date.now();
72
+ this.toast = { message, type, id };
73
+ setTimeout(() => {
74
+ if (this.toast && this.toast.id === id) this.toast = null;
75
+ }, 3000);
76
+ }
77
+ });
78
+ });