agentgate 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 (40) hide show
  1. package/README.md +216 -0
  2. package/package.json +63 -0
  3. package/public/favicon.svg +48 -0
  4. package/public/icons/bluesky.svg +1 -0
  5. package/public/icons/fitbit.svg +16 -0
  6. package/public/icons/github.svg +1 -0
  7. package/public/icons/google-calendar.svg +1 -0
  8. package/public/icons/jira.svg +1 -0
  9. package/public/icons/linkedin.svg +1 -0
  10. package/public/icons/mastodon.svg +1 -0
  11. package/public/icons/reddit.svg +1 -0
  12. package/public/icons/youtube.svg +1 -0
  13. package/public/logo.svg +52 -0
  14. package/public/style.css +584 -0
  15. package/src/cli.js +77 -0
  16. package/src/index.js +344 -0
  17. package/src/lib/db.js +325 -0
  18. package/src/lib/hsyncManager.js +57 -0
  19. package/src/lib/queueExecutor.js +362 -0
  20. package/src/routes/bluesky.js +130 -0
  21. package/src/routes/calendar.js +120 -0
  22. package/src/routes/fitbit.js +127 -0
  23. package/src/routes/github.js +72 -0
  24. package/src/routes/jira.js +77 -0
  25. package/src/routes/linkedin.js +137 -0
  26. package/src/routes/mastodon.js +91 -0
  27. package/src/routes/queue.js +186 -0
  28. package/src/routes/reddit.js +138 -0
  29. package/src/routes/ui/bluesky.js +66 -0
  30. package/src/routes/ui/calendar.js +120 -0
  31. package/src/routes/ui/fitbit.js +122 -0
  32. package/src/routes/ui/github.js +60 -0
  33. package/src/routes/ui/index.js +35 -0
  34. package/src/routes/ui/jira.js +72 -0
  35. package/src/routes/ui/linkedin.js +120 -0
  36. package/src/routes/ui/mastodon.js +140 -0
  37. package/src/routes/ui/reddit.js +120 -0
  38. package/src/routes/ui/youtube.js +120 -0
  39. package/src/routes/ui.js +1077 -0
  40. package/src/routes/youtube.js +119 -0
@@ -0,0 +1,584 @@
1
+ * { box-sizing: border-box; }
2
+
3
+ html {
4
+ background: #0f0f1a;
5
+ }
6
+
7
+ :root {
8
+ --primary: #6366f1;
9
+ --primary-dark: #4f46e5;
10
+ --primary-light: #818cf8;
11
+ --primary-glow: rgba(99, 102, 241, 0.4);
12
+ --danger: #ef4444;
13
+ --danger-dark: #dc2626;
14
+ --success: #10b981;
15
+ --success-bg: #d1fae5;
16
+ --warning: #f59e0b;
17
+ --warning-bg: #fef3c7;
18
+ --gray-50: #f9fafb;
19
+ --gray-100: #f3f4f6;
20
+ --gray-200: #e5e7eb;
21
+ --gray-300: #d1d5db;
22
+ --gray-400: #9ca3af;
23
+ --gray-500: #6b7280;
24
+ --gray-600: #4b5563;
25
+ --gray-700: #374151;
26
+ --gray-800: #1f2937;
27
+ --gray-900: #111827;
28
+ --radius: 16px;
29
+ --radius-sm: 10px;
30
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
31
+ --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
32
+ --shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
33
+ }
34
+
35
+ body {
36
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
37
+ max-width: 960px;
38
+ margin: 0 auto;
39
+ padding: 32px;
40
+ background: linear-gradient(160deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
41
+ min-height: 100vh;
42
+ color: var(--gray-200);
43
+ line-height: 1.6;
44
+ }
45
+
46
+ /* Animated background pattern */
47
+ body::before {
48
+ content: '';
49
+ position: fixed;
50
+ top: 0;
51
+ left: 0;
52
+ right: 0;
53
+ bottom: 0;
54
+ background:
55
+ radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
56
+ radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.08) 0%, transparent 50%),
57
+ radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.05) 0%, transparent 50%);
58
+ pointer-events: none;
59
+ z-index: -1;
60
+ }
61
+
62
+ h1 {
63
+ font-size: 2.25rem;
64
+ font-weight: 800;
65
+ letter-spacing: -0.03em;
66
+ background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #c4b5fd 100%);
67
+ -webkit-background-clip: text;
68
+ -webkit-text-fill-color: transparent;
69
+ background-clip: text;
70
+ text-shadow: 0 0 60px rgba(165, 180, 252, 0.5);
71
+ }
72
+
73
+ h2 {
74
+ color: var(--gray-100);
75
+ margin-top: 48px;
76
+ margin-bottom: 20px;
77
+ font-size: 1.35rem;
78
+ font-weight: 700;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 12px;
82
+ letter-spacing: -0.02em;
83
+ }
84
+
85
+ h2::before {
86
+ content: '';
87
+ display: inline-block;
88
+ width: 4px;
89
+ height: 24px;
90
+ background: linear-gradient(180deg, var(--primary) 0%, #a855f7 100%);
91
+ border-radius: 4px;
92
+ box-shadow: 0 0 12px var(--primary-glow);
93
+ }
94
+
95
+ h3 {
96
+ margin-bottom: 14px;
97
+ color: var(--gray-100);
98
+ font-weight: 600;
99
+ font-size: 1.1rem;
100
+ }
101
+
102
+ .card {
103
+ background: linear-gradient(145deg, rgba(30, 30, 50, 0.9) 0%, rgba(25, 25, 40, 0.95) 100%);
104
+ border-radius: var(--radius);
105
+ padding: 28px;
106
+ margin: 20px 0;
107
+ box-shadow: var(--shadow), var(--shadow-glow);
108
+ border: 1px solid rgba(99, 102, 241, 0.15);
109
+ backdrop-filter: blur(10px);
110
+ transition: all 0.3s ease;
111
+ }
112
+
113
+ .card:hover {
114
+ box-shadow: var(--shadow-lg), 0 0 60px rgba(99, 102, 241, 0.2);
115
+ transform: translateY(-4px);
116
+ border-color: rgba(99, 102, 241, 0.3);
117
+ }
118
+
119
+ .status {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ padding: 5px 14px;
124
+ border-radius: 9999px;
125
+ font-size: 11px;
126
+ font-weight: 700;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.08em;
129
+ }
130
+
131
+ .status::before {
132
+ content: '';
133
+ width: 8px;
134
+ height: 8px;
135
+ border-radius: 50%;
136
+ }
137
+
138
+ .status.configured {
139
+ background: rgba(16, 185, 129, 0.15);
140
+ color: #34d399;
141
+ border: 1px solid rgba(16, 185, 129, 0.3);
142
+ }
143
+
144
+ .status.configured::before {
145
+ background: var(--success);
146
+ box-shadow: 0 0 12px var(--success);
147
+ }
148
+
149
+ .status.not-configured {
150
+ background: rgba(245, 158, 11, 0.15);
151
+ color: #fbbf24;
152
+ border: 1px solid rgba(245, 158, 11, 0.3);
153
+ }
154
+
155
+ .status.not-configured::before {
156
+ background: var(--warning);
157
+ }
158
+
159
+ input[type="text"], input[type="password"] {
160
+ width: 100%;
161
+ padding: 14px 18px;
162
+ margin: 8px 0 18px;
163
+ border: 2px solid rgba(99, 102, 241, 0.2);
164
+ border-radius: var(--radius-sm);
165
+ font-size: 14px;
166
+ transition: all 0.3s ease;
167
+ background: rgba(15, 15, 25, 0.6);
168
+ color: var(--gray-100);
169
+ }
170
+
171
+ input[type="text"]:focus, input[type="password"]:focus {
172
+ outline: none;
173
+ border-color: var(--primary);
174
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15), 0 0 20px rgba(99, 102, 241, 0.2);
175
+ background: rgba(15, 15, 25, 0.8);
176
+ }
177
+
178
+ input[type="text"]::placeholder, input[type="password"]::placeholder {
179
+ color: var(--gray-500);
180
+ }
181
+
182
+ button, input[type="submit"] {
183
+ padding: 12px 24px;
184
+ border: none;
185
+ border-radius: var(--radius-sm);
186
+ cursor: pointer;
187
+ font-size: 14px;
188
+ font-weight: 600;
189
+ transition: all 0.3s ease;
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ gap: 8px;
194
+ }
195
+
196
+ .btn-primary {
197
+ background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
198
+ color: white;
199
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
200
+ }
201
+
202
+ .btn-primary:hover {
203
+ background: linear-gradient(135deg, #7c3aed 0%, var(--primary) 100%);
204
+ transform: translateY(-2px);
205
+ box-shadow: 0 8px 25px rgba(99, 102, 241, 0.5);
206
+ }
207
+
208
+ .btn-secondary {
209
+ background: rgba(99, 102, 241, 0.1);
210
+ color: var(--primary-light);
211
+ border: 1px solid rgba(99, 102, 241, 0.3);
212
+ }
213
+
214
+ .btn-secondary:hover {
215
+ background: rgba(99, 102, 241, 0.2);
216
+ border-color: var(--primary);
217
+ transform: translateY(-2px);
218
+ }
219
+
220
+ .btn-danger {
221
+ background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%);
222
+ color: white;
223
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
224
+ }
225
+
226
+ .btn-danger:hover {
227
+ background: linear-gradient(135deg, var(--danger-dark) 0%, var(--danger) 100%);
228
+ transform: translateY(-2px);
229
+ box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
230
+ }
231
+
232
+ .btn-sm {
233
+ padding: 8px 16px;
234
+ font-size: 13px;
235
+ border-radius: 8px;
236
+ }
237
+
238
+ .btn-ghost {
239
+ background: transparent;
240
+ color: var(--gray-400);
241
+ border: 1px solid rgba(255, 255, 255, 0.1);
242
+ }
243
+
244
+ .btn-ghost:hover {
245
+ background: rgba(255, 255, 255, 0.05);
246
+ color: var(--gray-200);
247
+ border-color: rgba(255, 255, 255, 0.2);
248
+ }
249
+
250
+ /* Header nav buttons */
251
+ .nav-btn {
252
+ padding: 10px 18px;
253
+ font-size: 13px;
254
+ font-weight: 600;
255
+ border-radius: 10px;
256
+ text-decoration: none;
257
+ transition: all 0.3s ease;
258
+ display: inline-flex;
259
+ align-items: center;
260
+ gap: 8px;
261
+ }
262
+
263
+ .nav-btn-default {
264
+ background: rgba(255, 255, 255, 0.05);
265
+ color: var(--gray-300);
266
+ border: 1px solid rgba(255, 255, 255, 0.1);
267
+ }
268
+
269
+ .nav-btn-default:hover {
270
+ background: rgba(255, 255, 255, 0.1);
271
+ color: white;
272
+ border-color: rgba(255, 255, 255, 0.2);
273
+ transform: translateY(-2px);
274
+ }
275
+
276
+ .nav-btn-primary {
277
+ background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
278
+ color: white;
279
+ border: none;
280
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
281
+ }
282
+
283
+ .nav-btn-primary:hover {
284
+ box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
285
+ transform: translateY(-2px);
286
+ }
287
+
288
+ .account-item {
289
+ display: flex;
290
+ justify-content: space-between;
291
+ align-items: center;
292
+ padding: 14px 18px;
293
+ background: rgba(255, 255, 255, 0.03);
294
+ border-radius: var(--radius-sm);
295
+ margin: 12px 0;
296
+ border: 1px solid rgba(255, 255, 255, 0.06);
297
+ transition: all 0.3s ease;
298
+ }
299
+
300
+ .account-item:hover {
301
+ border-color: rgba(99, 102, 241, 0.3);
302
+ background: rgba(99, 102, 241, 0.05);
303
+ }
304
+
305
+ .account-item span {
306
+ color: var(--gray-300);
307
+ }
308
+
309
+ .account-item strong {
310
+ color: var(--gray-100);
311
+ }
312
+
313
+ label {
314
+ font-weight: 600;
315
+ color: var(--gray-300);
316
+ display: block;
317
+ font-size: 13px;
318
+ margin-bottom: 4px;
319
+ letter-spacing: 0.02em;
320
+ }
321
+
322
+ .help {
323
+ font-size: 13px;
324
+ color: var(--gray-500);
325
+ margin-bottom: 14px;
326
+ }
327
+
328
+ .help a {
329
+ color: var(--primary-light);
330
+ text-decoration: none;
331
+ font-weight: 500;
332
+ }
333
+
334
+ .help a:hover {
335
+ text-decoration: underline;
336
+ }
337
+
338
+ .copyable {
339
+ display: inline-flex;
340
+ align-items: center;
341
+ gap: 12px;
342
+ background: rgba(0, 0, 0, 0.3);
343
+ padding: 8px 14px;
344
+ border-radius: var(--radius-sm);
345
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
346
+ font-size: 13px;
347
+ border: 1px solid rgba(255, 255, 255, 0.1);
348
+ color: var(--gray-200);
349
+ }
350
+
351
+ .copy-btn {
352
+ padding: 6px 12px;
353
+ font-size: 11px;
354
+ background: rgba(99, 102, 241, 0.2);
355
+ border: 1px solid rgba(99, 102, 241, 0.3);
356
+ border-radius: 6px;
357
+ cursor: pointer;
358
+ font-weight: 600;
359
+ color: var(--primary-light);
360
+ transition: all 0.2s ease;
361
+ }
362
+
363
+ .copy-btn:hover {
364
+ background: var(--primary);
365
+ color: white;
366
+ border-color: var(--primary);
367
+ }
368
+
369
+ details {
370
+ margin-top: 18px;
371
+ }
372
+
373
+ summary {
374
+ cursor: pointer;
375
+ color: var(--primary-light);
376
+ font-weight: 600;
377
+ padding: 10px 0;
378
+ transition: color 0.2s ease;
379
+ list-style: none;
380
+ display: flex;
381
+ align-items: center;
382
+ gap: 10px;
383
+ }
384
+
385
+ summary::-webkit-details-marker {
386
+ display: none;
387
+ }
388
+
389
+ summary::before {
390
+ content: '+';
391
+ display: inline-flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ width: 24px;
395
+ height: 24px;
396
+ background: rgba(99, 102, 241, 0.15);
397
+ border-radius: 50%;
398
+ font-size: 16px;
399
+ font-weight: bold;
400
+ color: var(--primary-light);
401
+ transition: all 0.3s ease;
402
+ }
403
+
404
+ details[open] summary::before {
405
+ content: '-';
406
+ background: var(--primary);
407
+ color: white;
408
+ box-shadow: 0 0 15px var(--primary-glow);
409
+ }
410
+
411
+ summary:hover {
412
+ color: white;
413
+ }
414
+
415
+ summary:hover::before {
416
+ background: rgba(99, 102, 241, 0.3);
417
+ }
418
+
419
+ .service-header {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 14px;
423
+ }
424
+
425
+ .service-header h3 {
426
+ margin: 0;
427
+ }
428
+
429
+ .service-icon {
430
+ width: 32px;
431
+ height: 32px;
432
+ padding: 6px;
433
+ background: rgba(255, 255, 255, 0.08);
434
+ border-radius: var(--radius-sm);
435
+ border: 1px solid rgba(255, 255, 255, 0.1);
436
+ }
437
+
438
+ pre {
439
+ background: linear-gradient(145deg, rgba(10, 10, 20, 0.9) 0%, rgba(15, 15, 30, 0.95) 100%);
440
+ color: #e2e8f0;
441
+ padding: 24px;
442
+ border-radius: var(--radius-sm);
443
+ overflow-x: auto;
444
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
445
+ font-size: 13px;
446
+ line-height: 1.8;
447
+ border: 1px solid rgba(99, 102, 241, 0.2);
448
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.3);
449
+ }
450
+
451
+ code {
452
+ background: rgba(99, 102, 241, 0.1);
453
+ padding: 4px 10px;
454
+ border-radius: 6px;
455
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
456
+ font-size: 13px;
457
+ color: var(--primary-light);
458
+ border: 1px solid rgba(99, 102, 241, 0.2);
459
+ }
460
+
461
+ /* Header styles */
462
+ .header {
463
+ display: flex;
464
+ justify-content: space-between;
465
+ align-items: center;
466
+ margin-bottom: 8px;
467
+ }
468
+
469
+ /* Intro text */
470
+ body > p:first-of-type {
471
+ color: var(--gray-400);
472
+ font-size: 16px;
473
+ margin-top: 0;
474
+ }
475
+
476
+ /* Login/Setup specific styles */
477
+ .login-card {
478
+ max-width: 420px;
479
+ margin: 100px auto;
480
+ text-align: center;
481
+ background: linear-gradient(145deg, rgba(30, 30, 50, 0.95) 0%, rgba(20, 20, 35, 0.98) 100%);
482
+ box-shadow: var(--shadow-lg), 0 0 80px rgba(99, 102, 241, 0.15);
483
+ }
484
+
485
+ .login-card h1 {
486
+ margin-bottom: 8px;
487
+ }
488
+
489
+ .login-card h3 {
490
+ color: var(--gray-400);
491
+ font-weight: 500;
492
+ margin-bottom: 28px;
493
+ }
494
+
495
+ .login-card form {
496
+ text-align: left;
497
+ }
498
+
499
+ .login-card .btn-primary {
500
+ width: 100%;
501
+ margin-top: 12px;
502
+ padding: 14px 24px;
503
+ }
504
+
505
+ .error-message {
506
+ background: rgba(239, 68, 68, 0.1);
507
+ border: 1px solid rgba(239, 68, 68, 0.3);
508
+ color: #f87171;
509
+ padding: 14px 18px;
510
+ border-radius: var(--radius-sm);
511
+ margin-bottom: 18px;
512
+ font-size: 14px;
513
+ font-weight: 500;
514
+ }
515
+
516
+ /* Animations - removed fadeIn on cards to prevent flash */
517
+
518
+ /* Connected status pulse */
519
+ @keyframes pulse {
520
+ 0%, 100% { opacity: 1; box-shadow: 0 0 12px var(--success); }
521
+ 50% { opacity: 0.6; box-shadow: 0 0 20px var(--success); }
522
+ }
523
+
524
+ .status.configured::before {
525
+ animation: pulse 2s ease-in-out infinite;
526
+ }
527
+
528
+ /* Badge for queue count */
529
+ .badge {
530
+ position: absolute;
531
+ top: -8px;
532
+ right: -8px;
533
+ background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
534
+ color: white;
535
+ border-radius: 50%;
536
+ width: 22px;
537
+ height: 22px;
538
+ font-size: 11px;
539
+ font-weight: 700;
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.5);
544
+ animation: badgePulse 2s ease-in-out infinite;
545
+ }
546
+
547
+ @keyframes badgePulse {
548
+ 0%, 100% { transform: scale(1); }
549
+ 50% { transform: scale(1.1); }
550
+ }
551
+
552
+ /* Divider for nav buttons */
553
+ .nav-divider {
554
+ width: 1px;
555
+ height: 24px;
556
+ background: rgba(255, 255, 255, 0.1);
557
+ margin: 0 8px;
558
+ }
559
+
560
+ /* Scrollbar styling */
561
+ ::-webkit-scrollbar {
562
+ width: 8px;
563
+ height: 8px;
564
+ }
565
+
566
+ ::-webkit-scrollbar-track {
567
+ background: rgba(0, 0, 0, 0.2);
568
+ border-radius: 4px;
569
+ }
570
+
571
+ ::-webkit-scrollbar-thumb {
572
+ background: rgba(99, 102, 241, 0.3);
573
+ border-radius: 4px;
574
+ }
575
+
576
+ ::-webkit-scrollbar-thumb:hover {
577
+ background: rgba(99, 102, 241, 0.5);
578
+ }
579
+
580
+ /* Selection styling */
581
+ ::selection {
582
+ background: rgba(99, 102, 241, 0.3);
583
+ color: white;
584
+ }
package/src/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createApiKey, listApiKeys, deleteApiKey } from './lib/db.js';
4
+
5
+ const [,, command, ...args] = process.argv;
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Usage: node src/cli.js <command>
10
+
11
+ Commands:
12
+ list List all API keys
13
+ create <name> Create a new API key
14
+ delete <id> Delete an API key by ID
15
+ `);
16
+ }
17
+
18
+ async function main() {
19
+ switch (command) {
20
+ case 'list': {
21
+ const keys = listApiKeys();
22
+ if (keys.length === 0) {
23
+ console.log('No API keys found.');
24
+ } else {
25
+ console.log('\nAPI Keys:\n');
26
+ for (const k of keys) {
27
+ console.log(` ID: ${k.id}`);
28
+ console.log(` Name: ${k.name}`);
29
+ console.log(` Key: ${k.key_prefix} (hashed - full key shown only at creation)`);
30
+ console.log(` Created: ${k.created_at}`);
31
+ console.log('');
32
+ }
33
+ }
34
+ break;
35
+ }
36
+
37
+ case 'create': {
38
+ const name = args[0];
39
+ if (!name) {
40
+ console.error('Error: name required\n');
41
+ console.log('Usage: node src/cli.js create <name>');
42
+ process.exit(1);
43
+ }
44
+ const key = await createApiKey(name);
45
+ console.log('\nAPI key created:\n');
46
+ console.log(` Name: ${key.name}`);
47
+ console.log(` Key: ${key.key}`);
48
+ console.log('\n ⚠️ Save this key now - you won\'t be able to see it again!\n');
49
+ break;
50
+ }
51
+
52
+ case 'delete': {
53
+ const id = args[0];
54
+ if (!id) {
55
+ console.error('Error: id required\n');
56
+ console.log('Usage: node src/cli.js delete <id>');
57
+ process.exit(1);
58
+ }
59
+ const result = deleteApiKey(id);
60
+ if (result.changes > 0) {
61
+ console.log(`API key ${id} deleted.`);
62
+ } else {
63
+ console.log(`No API key found with ID: ${id}`);
64
+ }
65
+ break;
66
+ }
67
+
68
+ default:
69
+ printUsage();
70
+ break;
71
+ }
72
+ }
73
+
74
+ main().catch(err => {
75
+ console.error('Error:', err.message);
76
+ process.exit(1);
77
+ });