@naarang/glancebar 1.0.8 → 1.0.10
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/package.json +1 -1
- package/src/cli.ts +2224 -1808
package/src/cli.ts
CHANGED
|
@@ -1,1808 +1,2224 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { google } from "googleapis";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
4
|
-
import { join, dirname } from "path";
|
|
5
|
-
import { createServer, Server } from "http";
|
|
6
|
-
import { createInterface } from "readline";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
|
|
9
|
-
// Get package version
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = dirname(__filename);
|
|
12
|
-
const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
13
|
-
const VERSION = packageJson.version;
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Configuration
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
interface Config {
|
|
20
|
-
accounts: string[]; // Legacy - for backwards compatibility
|
|
21
|
-
gmailAccounts: string[]; // Google accounts
|
|
22
|
-
zohoAccounts: ZohoAccount[]; // Zoho accounts
|
|
23
|
-
lookaheadHours: number;
|
|
24
|
-
showCalendarName: boolean;
|
|
25
|
-
countdownThresholdMinutes: number;
|
|
26
|
-
maxTitleLength: number;
|
|
27
|
-
waterReminderEnabled: boolean;
|
|
28
|
-
stretchReminderEnabled: boolean;
|
|
29
|
-
eyeReminderEnabled: boolean;
|
|
30
|
-
showCpuUsage: boolean;
|
|
31
|
-
showMemoryUsage: boolean;
|
|
32
|
-
showZohoTasks: boolean;
|
|
33
|
-
maxTasksToShow: number;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
return
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
console.log(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
config.
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
//
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { google } from "googleapis";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { createServer, Server } from "http";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// Get package version
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
13
|
+
const VERSION = packageJson.version;
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Configuration
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
interface Config {
|
|
20
|
+
accounts: string[]; // Legacy - for backwards compatibility
|
|
21
|
+
gmailAccounts: string[]; // Google accounts
|
|
22
|
+
zohoAccounts: ZohoAccount[]; // Zoho accounts
|
|
23
|
+
lookaheadHours: number;
|
|
24
|
+
showCalendarName: boolean;
|
|
25
|
+
countdownThresholdMinutes: number;
|
|
26
|
+
maxTitleLength: number;
|
|
27
|
+
waterReminderEnabled: boolean;
|
|
28
|
+
stretchReminderEnabled: boolean;
|
|
29
|
+
eyeReminderEnabled: boolean;
|
|
30
|
+
showCpuUsage: boolean;
|
|
31
|
+
showMemoryUsage: boolean;
|
|
32
|
+
showZohoTasks: boolean;
|
|
33
|
+
maxTasksToShow: number;
|
|
34
|
+
showUsageLimits: boolean;
|
|
35
|
+
show5HourLimit: boolean;
|
|
36
|
+
show7DayLimit: boolean;
|
|
37
|
+
show7DaySonnetLimit: boolean;
|
|
38
|
+
show5HourResets: boolean;
|
|
39
|
+
show7DayResets: boolean;
|
|
40
|
+
show7DaySonnetResets: boolean;
|
|
41
|
+
resetsTimeFormat: "relative" | "absolute";
|
|
42
|
+
usageLimitsCacheTTL: number; // In seconds, default 300 (5 min)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ZohoAccount {
|
|
46
|
+
email: string;
|
|
47
|
+
datacenter: string; // com, eu, in, com.au, com.cn, jp, zohocloud.ca
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ClaudeCredentials {
|
|
51
|
+
claudeAiOauth: {
|
|
52
|
+
accessToken: string;
|
|
53
|
+
refreshToken: string;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
scopes: string[];
|
|
56
|
+
subscriptionType: string;
|
|
57
|
+
rateLimitTier: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface UsageLimits {
|
|
62
|
+
five_hour: {
|
|
63
|
+
utilization: number; // Percentage (0-100)
|
|
64
|
+
resets_at: string; // ISO timestamp
|
|
65
|
+
};
|
|
66
|
+
seven_day: {
|
|
67
|
+
utilization: number; // Percentage (0-100)
|
|
68
|
+
resets_at: string; // ISO timestamp
|
|
69
|
+
};
|
|
70
|
+
seven_day_sonnet: {
|
|
71
|
+
utilization: number; // Percentage (0-100)
|
|
72
|
+
resets_at: string; // ISO timestamp
|
|
73
|
+
} | null; // null if not available in API response
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface UsageLimitsCache {
|
|
77
|
+
data: UsageLimits | null;
|
|
78
|
+
fetchedAt: number; // Unix timestamp
|
|
79
|
+
ttl: number; // Cache TTL in milliseconds (default: 5 minutes)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const COLORS: Record<string, string> = {
|
|
83
|
+
reset: "\x1b[0m",
|
|
84
|
+
red: "\x1b[31m",
|
|
85
|
+
green: "\x1b[32m",
|
|
86
|
+
yellow: "\x1b[33m",
|
|
87
|
+
blue: "\x1b[34m",
|
|
88
|
+
magenta: "\x1b[35m",
|
|
89
|
+
cyan: "\x1b[36m",
|
|
90
|
+
white: "\x1b[37m",
|
|
91
|
+
brightRed: "\x1b[91m",
|
|
92
|
+
brightGreen: "\x1b[92m",
|
|
93
|
+
brightYellow: "\x1b[93m",
|
|
94
|
+
brightBlue: "\x1b[94m",
|
|
95
|
+
brightMagenta: "\x1b[95m",
|
|
96
|
+
brightCyan: "\x1b[96m",
|
|
97
|
+
orange: "\x1b[38;5;208m",
|
|
98
|
+
pink: "\x1b[38;5;213m",
|
|
99
|
+
purple: "\x1b[38;5;141m",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const ACCOUNT_COLORS = ["cyan", "magenta", "brightGreen", "orange", "brightBlue", "pink", "yellow", "purple"];
|
|
103
|
+
|
|
104
|
+
const DEFAULT_CONFIG: Config = {
|
|
105
|
+
accounts: [], // Legacy
|
|
106
|
+
gmailAccounts: [],
|
|
107
|
+
zohoAccounts: [],
|
|
108
|
+
lookaheadHours: 8,
|
|
109
|
+
showCalendarName: true,
|
|
110
|
+
countdownThresholdMinutes: 60,
|
|
111
|
+
maxTitleLength: 120,
|
|
112
|
+
waterReminderEnabled: true,
|
|
113
|
+
stretchReminderEnabled: true,
|
|
114
|
+
eyeReminderEnabled: true,
|
|
115
|
+
showCpuUsage: false,
|
|
116
|
+
showMemoryUsage: false,
|
|
117
|
+
showZohoTasks: true,
|
|
118
|
+
maxTasksToShow: 3,
|
|
119
|
+
showUsageLimits: true,
|
|
120
|
+
show5HourLimit: true,
|
|
121
|
+
show7DayLimit: true,
|
|
122
|
+
show7DaySonnetLimit: true,
|
|
123
|
+
show5HourResets: false,
|
|
124
|
+
show7DayResets: false,
|
|
125
|
+
show7DaySonnetResets: false,
|
|
126
|
+
resetsTimeFormat: "relative",
|
|
127
|
+
usageLimitsCacheTTL: 120, // 2 minutes
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const WATER_REMINDERS = [
|
|
131
|
+
"Stay hydrated! Drink some water",
|
|
132
|
+
"Time for a water break!",
|
|
133
|
+
"Hydration check! Grab some water",
|
|
134
|
+
"Your body needs water. Drink up!",
|
|
135
|
+
"Water break! Stay refreshed",
|
|
136
|
+
"Don't forget to drink water!",
|
|
137
|
+
"Hydrate yourself! Take a sip",
|
|
138
|
+
"Quick reminder: Drink water!",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const STRETCH_REMINDERS = [
|
|
142
|
+
"Time to stretch! Stand up and move",
|
|
143
|
+
"Stretch break! Roll your shoulders",
|
|
144
|
+
"Stand up and stretch your legs",
|
|
145
|
+
"Posture check! Sit up straight",
|
|
146
|
+
"Take a quick stretch break",
|
|
147
|
+
"Move your body! Quick stretch",
|
|
148
|
+
"Stretch your neck and shoulders",
|
|
149
|
+
"Stand up! Your body will thank you",
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const EYE_REMINDERS = [
|
|
153
|
+
"Eye break! Look 20ft away for 20s",
|
|
154
|
+
"Rest your eyes - look at something distant",
|
|
155
|
+
"20-20-20: Look away from screen",
|
|
156
|
+
"Give your eyes a break!",
|
|
157
|
+
"Look away from the screen for a moment",
|
|
158
|
+
"Eye rest time! Focus on something far",
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
function getConfigDir(): string {
|
|
162
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
163
|
+
return join(home, ".glancebar");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getConfigPath(): string {
|
|
167
|
+
return join(getConfigDir(), "config.json");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getTokensDir(): string {
|
|
171
|
+
return join(getConfigDir(), "tokens");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Claude Usage Limits
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
function getClaudeCredentialsPath(): string {
|
|
179
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
180
|
+
return join(home, ".claude", ".credentials.json");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function loadClaudeCredentials(): ClaudeCredentials | null {
|
|
184
|
+
try {
|
|
185
|
+
const credPath = getClaudeCredentialsPath();
|
|
186
|
+
if (!existsSync(credPath)) return null;
|
|
187
|
+
const content = readFileSync(credPath, "utf-8");
|
|
188
|
+
return JSON.parse(content);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getUsageLimitsCachePath(): string {
|
|
195
|
+
return join(getConfigDir(), "usage_limits_cache.json");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadUsageLimitsCache(): UsageLimitsCache {
|
|
199
|
+
try {
|
|
200
|
+
const cachePath = getUsageLimitsCachePath();
|
|
201
|
+
if (!existsSync(cachePath)) {
|
|
202
|
+
return { data: null, fetchedAt: 0, ttl: 120000 };
|
|
203
|
+
}
|
|
204
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
205
|
+
return JSON.parse(content);
|
|
206
|
+
} catch {
|
|
207
|
+
return { data: null, fetchedAt: 0, ttl: 120000 };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function saveUsageLimitsCache(cache: UsageLimitsCache): void {
|
|
212
|
+
try {
|
|
213
|
+
ensureConfigDir();
|
|
214
|
+
const cachePath = getUsageLimitsCachePath();
|
|
215
|
+
writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
216
|
+
} catch {
|
|
217
|
+
// Silently fail if we can't save cache
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function fetchUsageLimitsFromAPI(): Promise<UsageLimits | null> {
|
|
222
|
+
try {
|
|
223
|
+
const creds = loadClaudeCredentials();
|
|
224
|
+
if (!creds?.claudeAiOauth?.accessToken) return null;
|
|
225
|
+
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
228
|
+
|
|
229
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
230
|
+
method: "GET",
|
|
231
|
+
headers: {
|
|
232
|
+
"Authorization": `Bearer ${creds.claudeAiOauth.accessToken}`,
|
|
233
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
234
|
+
},
|
|
235
|
+
signal: controller.signal,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
clearTimeout(timeoutId);
|
|
239
|
+
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const data = await response.json();
|
|
245
|
+
|
|
246
|
+
// Validate at least one limit exists
|
|
247
|
+
if (!data.five_hour && !data.seven_day && !data.seven_day_sonnet) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
five_hour: data.five_hour ? {
|
|
253
|
+
utilization: data.five_hour.utilization || 0,
|
|
254
|
+
resets_at: data.five_hour.resets_at || "",
|
|
255
|
+
} : { utilization: 0, resets_at: "" },
|
|
256
|
+
seven_day: data.seven_day ? {
|
|
257
|
+
utilization: data.seven_day.utilization || 0,
|
|
258
|
+
resets_at: data.seven_day.resets_at || "",
|
|
259
|
+
} : { utilization: 0, resets_at: "" },
|
|
260
|
+
seven_day_sonnet: data.seven_day_sonnet ? {
|
|
261
|
+
utilization: data.seven_day_sonnet.utilization || 0,
|
|
262
|
+
resets_at: data.seven_day_sonnet.resets_at || "",
|
|
263
|
+
} : null,
|
|
264
|
+
};
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function getUsageLimits(config: Config): Promise<UsageLimits | null> {
|
|
271
|
+
const cache = loadUsageLimitsCache();
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const ttlMs = config.usageLimitsCacheTTL * 1000;
|
|
274
|
+
|
|
275
|
+
// Return cached data if fresh
|
|
276
|
+
if (cache.data && (now - cache.fetchedAt) < ttlMs) {
|
|
277
|
+
return cache.data;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Cache is stale or missing, fetch from API
|
|
281
|
+
const freshData = await fetchUsageLimitsFromAPI();
|
|
282
|
+
|
|
283
|
+
// Update cache if fetch succeeded
|
|
284
|
+
if (freshData) {
|
|
285
|
+
saveUsageLimitsCache({
|
|
286
|
+
data: freshData,
|
|
287
|
+
fetchedAt: now,
|
|
288
|
+
ttl: ttlMs,
|
|
289
|
+
});
|
|
290
|
+
return freshData;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fetch failed - return stale cache if available
|
|
294
|
+
return cache.data || null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Helper function to format reset time
|
|
298
|
+
function formatResetTime(isoTimestamp: string, format: "relative" | "absolute"): string {
|
|
299
|
+
const resetDate = new Date(isoTimestamp);
|
|
300
|
+
const now = new Date();
|
|
301
|
+
|
|
302
|
+
if (format === "absolute") {
|
|
303
|
+
// Format as "Jan 30, 2:59 PM"
|
|
304
|
+
const dateStr = resetDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
305
|
+
const timeStr = resetDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
306
|
+
return `${dateStr}, ${timeStr}`;
|
|
307
|
+
} else {
|
|
308
|
+
// Relative: "in 2h 15m" or "in 4d 10h"
|
|
309
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
310
|
+
if (diffMs <= 0) return "now";
|
|
311
|
+
|
|
312
|
+
const diffMinutes = Math.floor(diffMs / 60000);
|
|
313
|
+
const hours = Math.floor(diffMinutes / 60);
|
|
314
|
+
const minutes = diffMinutes % 60;
|
|
315
|
+
|
|
316
|
+
// If over 24 hours, show days
|
|
317
|
+
if (hours >= 24) {
|
|
318
|
+
const days = Math.floor(hours / 24);
|
|
319
|
+
const remainingHours = hours % 24;
|
|
320
|
+
if (remainingHours === 0) return `in ${days}d`;
|
|
321
|
+
return `in ${days}d ${remainingHours}h`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (hours === 0) return `in ${minutes}m`;
|
|
325
|
+
if (minutes === 0) return `in ${hours}h`;
|
|
326
|
+
return `in ${hours}h${minutes}m`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatUsageLimits(usageLimits: UsageLimits, config: Config): string | null {
|
|
331
|
+
const parts: string[] = [];
|
|
332
|
+
|
|
333
|
+
// Helper to format a single limit
|
|
334
|
+
const formatLimit = (label: string, utilization: number, resetsAt: string, showResets: boolean) => {
|
|
335
|
+
let color = COLORS.green;
|
|
336
|
+
if (utilization >= 80) color = COLORS.red;
|
|
337
|
+
else if (utilization >= 50) color = COLORS.yellow;
|
|
338
|
+
|
|
339
|
+
let text = `${label}: ${utilization.toFixed(0)}%`;
|
|
340
|
+
if (showResets && resetsAt) {
|
|
341
|
+
text += ` (${formatResetTime(resetsAt, config.resetsTimeFormat)})`;
|
|
342
|
+
}
|
|
343
|
+
return `${color}${text}${COLORS.reset}`;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// 5-hour limit
|
|
347
|
+
if (config.show5HourLimit) {
|
|
348
|
+
parts.push(formatLimit("5h", usageLimits.five_hour.utilization,
|
|
349
|
+
usageLimits.five_hour.resets_at, config.show5HourResets));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 7-day limit
|
|
353
|
+
if (config.show7DayLimit) {
|
|
354
|
+
parts.push(formatLimit("7d", usageLimits.seven_day.utilization,
|
|
355
|
+
usageLimits.seven_day.resets_at, config.show7DayResets));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 7-day sonnet limit
|
|
359
|
+
if (config.show7DaySonnetLimit && usageLimits.seven_day_sonnet) {
|
|
360
|
+
parts.push(formatLimit("7d-sonnet", usageLimits.seven_day_sonnet.utilization,
|
|
361
|
+
usageLimits.seven_day_sonnet.resets_at, config.show7DaySonnetResets));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return parts.length > 0 ? parts.join(" | ") : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ensureConfigDir(): void {
|
|
368
|
+
const dir = getConfigDir();
|
|
369
|
+
if (!existsSync(dir)) {
|
|
370
|
+
mkdirSync(dir, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function loadConfig(): Config {
|
|
375
|
+
const configPath = getConfigPath();
|
|
376
|
+
if (!existsSync(configPath)) {
|
|
377
|
+
return { ...DEFAULT_CONFIG };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const content = readFileSync(configPath, "utf-8");
|
|
382
|
+
const userConfig = JSON.parse(content);
|
|
383
|
+
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
384
|
+
|
|
385
|
+
// Migrate legacy accounts to gmailAccounts
|
|
386
|
+
if (config.accounts && config.accounts.length > 0 && (!config.gmailAccounts || config.gmailAccounts.length === 0)) {
|
|
387
|
+
config.gmailAccounts = [...config.accounts];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return config;
|
|
391
|
+
} catch {
|
|
392
|
+
return { ...DEFAULT_CONFIG };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function saveConfig(config: Config): void {
|
|
397
|
+
ensureConfigDir();
|
|
398
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ============================================================================
|
|
402
|
+
// Google OAuth Authentication
|
|
403
|
+
// ============================================================================
|
|
404
|
+
|
|
405
|
+
const GOOGLE_SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
406
|
+
const REDIRECT_URI = "http://localhost:3000/callback";
|
|
407
|
+
|
|
408
|
+
interface GoogleCredentials {
|
|
409
|
+
installed?: { client_id: string; client_secret: string };
|
|
410
|
+
web?: { client_id: string; client_secret: string };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// Zoho OAuth Authentication
|
|
415
|
+
// ============================================================================
|
|
416
|
+
|
|
417
|
+
const ZOHO_SCOPES = [
|
|
418
|
+
"ZohoCalendar.calendar.READ",
|
|
419
|
+
"ZohoCalendar.event.READ",
|
|
420
|
+
"ZohoMail.tasks.READ",
|
|
421
|
+
];
|
|
422
|
+
const ZOHO_REDIRECT_URI = "http://localhost:3000/callback";
|
|
423
|
+
|
|
424
|
+
// Zoho datacenter mappings
|
|
425
|
+
const ZOHO_DATACENTERS: Record<string, { accounts: string; calendar: string; mail: string }> = {
|
|
426
|
+
"com": { accounts: "https://accounts.zoho.com", calendar: "https://calendar.zoho.com", mail: "https://mail.zoho.com" },
|
|
427
|
+
"eu": { accounts: "https://accounts.zoho.eu", calendar: "https://calendar.zoho.eu", mail: "https://mail.zoho.eu" },
|
|
428
|
+
"in": { accounts: "https://accounts.zoho.in", calendar: "https://calendar.zoho.in", mail: "https://mail.zoho.in" },
|
|
429
|
+
"com.au": { accounts: "https://accounts.zoho.com.au", calendar: "https://calendar.zoho.com.au", mail: "https://mail.zoho.com.au" },
|
|
430
|
+
"com.cn": { accounts: "https://accounts.zoho.com.cn", calendar: "https://calendar.zoho.com.cn", mail: "https://mail.zoho.com.cn" },
|
|
431
|
+
"jp": { accounts: "https://accounts.zoho.jp", calendar: "https://calendar.zoho.jp", mail: "https://mail.zoho.jp" },
|
|
432
|
+
"zohocloud.ca": { accounts: "https://accounts.zohocloud.ca", calendar: "https://calendar.zohocloud.ca", mail: "https://mail.zohocloud.ca" },
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
interface ZohoCredentials {
|
|
436
|
+
client_id: string;
|
|
437
|
+
client_secret: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
interface ZohoToken {
|
|
441
|
+
access_token: string;
|
|
442
|
+
refresh_token: string;
|
|
443
|
+
expires_at: number;
|
|
444
|
+
api_domain?: string;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Google credentials
|
|
448
|
+
function getGoogleCredentialsPath(): string {
|
|
449
|
+
return join(getConfigDir(), "credentials.json");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function loadGoogleCredentials(): GoogleCredentials {
|
|
453
|
+
const credPath = getGoogleCredentialsPath();
|
|
454
|
+
if (!existsSync(credPath)) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`credentials.json not found at ${credPath}\n\nPlease download OAuth credentials from Google Cloud Console and save to:\n${credPath}\n\nRun 'glancebar setup' for detailed instructions.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return JSON.parse(readFileSync(credPath, "utf-8"));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getGoogleTokenPath(account: string): string {
|
|
463
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
464
|
+
return join(getTokensDir(), `google_${safeAccount}.json`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Legacy token path (for migration)
|
|
468
|
+
function getLegacyTokenPath(account: string): string {
|
|
469
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
470
|
+
return join(getTokensDir(), `${safeAccount}.json`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Zoho credentials
|
|
474
|
+
function getZohoCredentialsPath(): string {
|
|
475
|
+
return join(getConfigDir(), "zoho_credentials.json");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function loadZohoCredentials(): ZohoCredentials {
|
|
479
|
+
const credPath = getZohoCredentialsPath();
|
|
480
|
+
if (!existsSync(credPath)) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`zoho_credentials.json not found at ${credPath}\n\nPlease create OAuth credentials in Zoho API Console and save to:\n${credPath}\n\nRun 'glancebar setup' for detailed instructions.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return JSON.parse(readFileSync(credPath, "utf-8"));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getZohoTokenPath(account: string): string {
|
|
489
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
490
|
+
return join(getTokensDir(), `zoho_${safeAccount}.json`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function createGoogleOAuth2Client(credentials: GoogleCredentials) {
|
|
494
|
+
const { client_id, client_secret } = credentials.installed || credentials.web!;
|
|
495
|
+
return new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getGoogleAuthenticatedClient(account: string) {
|
|
499
|
+
const credentials = loadGoogleCredentials();
|
|
500
|
+
const oauth2Client = createGoogleOAuth2Client(credentials);
|
|
501
|
+
|
|
502
|
+
// Try new path first, then legacy path
|
|
503
|
+
let tokenPath = getGoogleTokenPath(account);
|
|
504
|
+
if (!existsSync(tokenPath)) {
|
|
505
|
+
const legacyPath = getLegacyTokenPath(account);
|
|
506
|
+
if (existsSync(legacyPath)) {
|
|
507
|
+
tokenPath = legacyPath;
|
|
508
|
+
} else {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const token = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
514
|
+
oauth2Client.setCredentials(token);
|
|
515
|
+
|
|
516
|
+
oauth2Client.on("tokens", (tokens) => {
|
|
517
|
+
const currentToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
518
|
+
const updatedToken = { ...currentToken, ...tokens };
|
|
519
|
+
writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return oauth2Client;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function authenticateGoogleAccount(account: string): Promise<void> {
|
|
526
|
+
const credentials = loadGoogleCredentials();
|
|
527
|
+
const oauth2Client = createGoogleOAuth2Client(credentials);
|
|
528
|
+
|
|
529
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
530
|
+
access_type: "offline",
|
|
531
|
+
scope: GOOGLE_SCOPES,
|
|
532
|
+
prompt: "consent",
|
|
533
|
+
login_hint: account,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
console.log(`\nAuthenticating: ${account}`);
|
|
537
|
+
console.log(`Opening browser...`);
|
|
538
|
+
|
|
539
|
+
const code = await startServerAndGetCode(authUrl);
|
|
540
|
+
|
|
541
|
+
console.log(`Exchanging code for tokens...`);
|
|
542
|
+
|
|
543
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
544
|
+
oauth2Client.setCredentials(tokens);
|
|
545
|
+
|
|
546
|
+
const tokensDir = getTokensDir();
|
|
547
|
+
if (!existsSync(tokensDir)) {
|
|
548
|
+
mkdirSync(tokensDir, { recursive: true });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const tokenPath = getGoogleTokenPath(account);
|
|
552
|
+
writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
|
|
553
|
+
console.log(`Token saved for ${account}`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ============================================================================
|
|
557
|
+
// Zoho OAuth Flow
|
|
558
|
+
// ============================================================================
|
|
559
|
+
|
|
560
|
+
async function authenticateZohoAccount(account: ZohoAccount): Promise<void> {
|
|
561
|
+
const credentials = loadZohoCredentials();
|
|
562
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
563
|
+
|
|
564
|
+
if (!dc) {
|
|
565
|
+
throw new Error(`Invalid datacenter: ${account.datacenter}. Valid options: ${Object.keys(ZOHO_DATACENTERS).join(", ")}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const params = new URLSearchParams({
|
|
569
|
+
response_type: "code",
|
|
570
|
+
client_id: credentials.client_id,
|
|
571
|
+
scope: ZOHO_SCOPES.join(","),
|
|
572
|
+
redirect_uri: ZOHO_REDIRECT_URI,
|
|
573
|
+
access_type: "offline",
|
|
574
|
+
prompt: "consent",
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const authUrl = `${dc.accounts}/oauth/v2/auth?${params.toString()}`;
|
|
578
|
+
|
|
579
|
+
console.log(`\nAuthenticating Zoho: ${account.email}`);
|
|
580
|
+
console.log(`Datacenter: ${account.datacenter}`);
|
|
581
|
+
console.log(`Opening browser...`);
|
|
582
|
+
|
|
583
|
+
const code = await startServerAndGetCode(authUrl);
|
|
584
|
+
|
|
585
|
+
console.log(`Exchanging code for tokens...`);
|
|
586
|
+
|
|
587
|
+
// Exchange code for tokens
|
|
588
|
+
const tokenParams = new URLSearchParams({
|
|
589
|
+
grant_type: "authorization_code",
|
|
590
|
+
client_id: credentials.client_id,
|
|
591
|
+
client_secret: credentials.client_secret,
|
|
592
|
+
redirect_uri: ZOHO_REDIRECT_URI,
|
|
593
|
+
code: code,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const tokenResponse = await fetch(`${dc.accounts}/oauth/v2/token`, {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
599
|
+
body: tokenParams.toString(),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (!tokenResponse.ok) {
|
|
603
|
+
const errorText = await tokenResponse.text();
|
|
604
|
+
throw new Error(`Failed to get Zoho tokens: ${errorText}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const tokenData = await tokenResponse.json();
|
|
608
|
+
|
|
609
|
+
const token: ZohoToken = {
|
|
610
|
+
access_token: tokenData.access_token,
|
|
611
|
+
refresh_token: tokenData.refresh_token,
|
|
612
|
+
expires_at: Date.now() + (tokenData.expires_in * 1000),
|
|
613
|
+
api_domain: tokenData.api_domain || dc.calendar,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const tokensDir = getTokensDir();
|
|
617
|
+
if (!existsSync(tokensDir)) {
|
|
618
|
+
mkdirSync(tokensDir, { recursive: true });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
622
|
+
writeFileSync(tokenPath, JSON.stringify(token, null, 2));
|
|
623
|
+
console.log(`Token saved for ${account.email}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function refreshZohoToken(account: ZohoAccount): Promise<ZohoToken | null> {
|
|
627
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
628
|
+
if (!existsSync(tokenPath)) return null;
|
|
629
|
+
|
|
630
|
+
const token: ZohoToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
631
|
+
|
|
632
|
+
// Check if token is still valid (with 5 minute buffer)
|
|
633
|
+
if (token.expires_at > Date.now() + 300000) {
|
|
634
|
+
return token;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Refresh the token
|
|
638
|
+
try {
|
|
639
|
+
const credentials = loadZohoCredentials();
|
|
640
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
641
|
+
|
|
642
|
+
const params = new URLSearchParams({
|
|
643
|
+
grant_type: "refresh_token",
|
|
644
|
+
client_id: credentials.client_id,
|
|
645
|
+
client_secret: credentials.client_secret,
|
|
646
|
+
refresh_token: token.refresh_token,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const response = await fetch(`${dc.accounts}/oauth/v2/token`, {
|
|
650
|
+
method: "POST",
|
|
651
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
652
|
+
body: params.toString(),
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
if (!response.ok) return null;
|
|
656
|
+
|
|
657
|
+
const data = await response.json();
|
|
658
|
+
const updatedToken: ZohoToken = {
|
|
659
|
+
...token,
|
|
660
|
+
access_token: data.access_token,
|
|
661
|
+
expires_at: Date.now() + (data.expires_in * 1000),
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
|
|
665
|
+
return updatedToken;
|
|
666
|
+
} catch {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function getZohoAuthenticatedToken(account: ZohoAccount): ZohoToken | null {
|
|
672
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
673
|
+
if (!existsSync(tokenPath)) return null;
|
|
674
|
+
return JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function startServerAndGetCode(authUrl: string): Promise<string> {
|
|
678
|
+
return new Promise((resolve, reject) => {
|
|
679
|
+
let server: Server;
|
|
680
|
+
|
|
681
|
+
server = createServer(async (req, res) => {
|
|
682
|
+
const url = new URL(req.url!, `http://localhost:3000`);
|
|
683
|
+
|
|
684
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
685
|
+
res.writeHead(404);
|
|
686
|
+
res.end("Not found");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const code = url.searchParams.get("code");
|
|
691
|
+
const error = url.searchParams.get("error");
|
|
692
|
+
|
|
693
|
+
if (error) {
|
|
694
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
695
|
+
res.end(`<html><body><h1>Authentication failed</h1><p>Error: ${error}</p></body></html>`);
|
|
696
|
+
server.close();
|
|
697
|
+
reject(new Error(error));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (code) {
|
|
702
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
703
|
+
res.end(`
|
|
704
|
+
<html>
|
|
705
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
|
|
706
|
+
<div style="text-align: center;">
|
|
707
|
+
<h1 style="color: #4ade80;">Authentication Successful!</h1>
|
|
708
|
+
<p>You can close this window and return to the terminal.</p>
|
|
709
|
+
</div>
|
|
710
|
+
</body>
|
|
711
|
+
</html>
|
|
712
|
+
`);
|
|
713
|
+
|
|
714
|
+
setTimeout(() => {
|
|
715
|
+
server.close(() => resolve(code));
|
|
716
|
+
}, 500);
|
|
717
|
+
} else {
|
|
718
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
719
|
+
res.end("<html><body><h1>Authentication failed</h1><p>No code received.</p></body></html>");
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
server.listen(3000, () => openBrowser(authUrl));
|
|
724
|
+
|
|
725
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
726
|
+
if (err.code === "EADDRINUSE") {
|
|
727
|
+
reject(new Error("Port 3000 is already in use. Please close any application using it and try again."));
|
|
728
|
+
} else {
|
|
729
|
+
reject(err);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
setTimeout(() => {
|
|
734
|
+
server.close();
|
|
735
|
+
reject(new Error("Authentication timeout (5 minutes)"));
|
|
736
|
+
}, 300000);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function openBrowser(url: string) {
|
|
741
|
+
const { exec } = require("child_process");
|
|
742
|
+
const platform = process.platform;
|
|
743
|
+
|
|
744
|
+
let command: string;
|
|
745
|
+
if (platform === "win32") {
|
|
746
|
+
command = `start "" "${url}"`;
|
|
747
|
+
} else if (platform === "darwin") {
|
|
748
|
+
command = `open "${url}"`;
|
|
749
|
+
} else {
|
|
750
|
+
command = `xdg-open "${url}"`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
exec(command, (err: Error | null) => {
|
|
754
|
+
if (err) {
|
|
755
|
+
console.log(`\nCould not open browser automatically.`);
|
|
756
|
+
console.log(`Please open this URL manually:\n${url}\n`);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ============================================================================
|
|
762
|
+
// Calendar
|
|
763
|
+
// ============================================================================
|
|
764
|
+
|
|
765
|
+
interface CalendarEvent {
|
|
766
|
+
id: string;
|
|
767
|
+
title: string;
|
|
768
|
+
start: Date;
|
|
769
|
+
end: Date;
|
|
770
|
+
isAllDay: boolean;
|
|
771
|
+
account: string;
|
|
772
|
+
accountEmail: string;
|
|
773
|
+
accountIndex: number;
|
|
774
|
+
provider: "google" | "zoho";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Get all accounts combined for indexing
|
|
778
|
+
function getAllAccounts(config: Config): string[] {
|
|
779
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
780
|
+
const zohoEmails = config.zohoAccounts.map(z => z.email);
|
|
781
|
+
return [...gmailAccounts, ...zohoEmails];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function getGoogleEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
|
|
785
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
786
|
+
const allAccounts = getAllAccounts(config);
|
|
787
|
+
|
|
788
|
+
const eventPromises = gmailAccounts.map(async (account) => {
|
|
789
|
+
const accountIndex = allAccounts.indexOf(account);
|
|
790
|
+
try {
|
|
791
|
+
const auth = getGoogleAuthenticatedClient(account);
|
|
792
|
+
if (!auth) return [];
|
|
793
|
+
|
|
794
|
+
const calendar = google.calendar({ version: "v3", auth });
|
|
795
|
+
|
|
796
|
+
const response = await calendar.events.list({
|
|
797
|
+
calendarId: "primary",
|
|
798
|
+
timeMin: now.toISOString(),
|
|
799
|
+
timeMax: timeMax.toISOString(),
|
|
800
|
+
maxResults: 10,
|
|
801
|
+
singleEvents: true,
|
|
802
|
+
orderBy: "startTime",
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const events = response.data.items || [];
|
|
806
|
+
return events.map((event) => {
|
|
807
|
+
const isAllDay = !event.start?.dateTime;
|
|
808
|
+
let start: Date, end: Date;
|
|
809
|
+
|
|
810
|
+
if (isAllDay) {
|
|
811
|
+
start = new Date(event.start?.date + "T00:00:00");
|
|
812
|
+
end = new Date(event.end?.date + "T00:00:00");
|
|
813
|
+
} else {
|
|
814
|
+
start = new Date(event.start?.dateTime!);
|
|
815
|
+
end = new Date(event.end?.dateTime!);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
id: event.id || "",
|
|
820
|
+
title: event.summary || "(No title)",
|
|
821
|
+
start,
|
|
822
|
+
end,
|
|
823
|
+
isAllDay,
|
|
824
|
+
account: extractAccountName(account),
|
|
825
|
+
accountEmail: account,
|
|
826
|
+
accountIndex,
|
|
827
|
+
provider: "google" as const,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
} catch {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const results = await Promise.all(eventPromises);
|
|
836
|
+
return results.flat();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function getZohoEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
|
|
840
|
+
if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
|
|
841
|
+
|
|
842
|
+
const allAccounts = getAllAccounts(config);
|
|
843
|
+
const gmailCount = (config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts).length;
|
|
844
|
+
|
|
845
|
+
const eventPromises = config.zohoAccounts.map(async (account, idx) => {
|
|
846
|
+
const accountIndex = gmailCount + idx;
|
|
847
|
+
try {
|
|
848
|
+
const token = await refreshZohoToken(account);
|
|
849
|
+
if (!token) return [];
|
|
850
|
+
|
|
851
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
852
|
+
// Always use the calendar-specific API domain, not the generic api_domain
|
|
853
|
+
const apiBase = dc.calendar;
|
|
854
|
+
|
|
855
|
+
// First get list of calendars
|
|
856
|
+
const calendarsResponse = await fetch(`${apiBase}/api/v1/calendars?category=own`, {
|
|
857
|
+
headers: {
|
|
858
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
if (!calendarsResponse.ok) return [];
|
|
863
|
+
|
|
864
|
+
const calendarsData = await calendarsResponse.json();
|
|
865
|
+
const calendars = calendarsData.calendars || [];
|
|
866
|
+
|
|
867
|
+
if (calendars.length === 0) return [];
|
|
868
|
+
|
|
869
|
+
// Use the first (primary) calendar
|
|
870
|
+
const primaryCalendar = calendars.find((c: any) => c.isdefault) || calendars[0];
|
|
871
|
+
const calendarUid = primaryCalendar.uid;
|
|
872
|
+
|
|
873
|
+
// Format dates for Zoho API (yyyyMMdd'T'HHmmss'Z')
|
|
874
|
+
const formatZohoDate = (date: Date): string => {
|
|
875
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const range = JSON.stringify({
|
|
879
|
+
start: formatZohoDate(now),
|
|
880
|
+
end: formatZohoDate(timeMax),
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const eventsResponse = await fetch(
|
|
884
|
+
`${apiBase}/api/v1/calendars/${encodeURIComponent(calendarUid)}/events?range=${encodeURIComponent(range)}`,
|
|
885
|
+
{
|
|
886
|
+
headers: {
|
|
887
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
888
|
+
},
|
|
889
|
+
}
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
if (!eventsResponse.ok) return [];
|
|
893
|
+
|
|
894
|
+
const eventsData = await eventsResponse.json();
|
|
895
|
+
const events = eventsData.events || [];
|
|
896
|
+
|
|
897
|
+
return events.map((event: any) => {
|
|
898
|
+
const isAllDay = event.isallday === true;
|
|
899
|
+
let start: Date, end: Date;
|
|
900
|
+
|
|
901
|
+
// Parse Zoho date format: "20260109T163000+0530" or "20260109T163000Z"
|
|
902
|
+
const parseZohoDate = (dateStr: string): Date => {
|
|
903
|
+
// Format: YYYYMMDDTHHmmss+HHMM or YYYYMMDDTHHmmssZ
|
|
904
|
+
const match = dateStr.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})([Z]|([+-])(\d{2})(\d{2}))?$/);
|
|
905
|
+
if (match) {
|
|
906
|
+
const [, year, month, day, hour, min, sec, tz, sign, tzHour, tzMin] = match;
|
|
907
|
+
if (tz === "Z") {
|
|
908
|
+
return new Date(Date.UTC(+year, +month - 1, +day, +hour, +min, +sec));
|
|
909
|
+
} else if (sign && tzHour && tzMin) {
|
|
910
|
+
const offsetMinutes = (+tzHour * 60 + +tzMin) * (sign === "+" ? -1 : 1);
|
|
911
|
+
const utc = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec);
|
|
912
|
+
return new Date(utc + offsetMinutes * 60000);
|
|
913
|
+
}
|
|
914
|
+
// No timezone, assume local
|
|
915
|
+
return new Date(+year, +month - 1, +day, +hour, +min, +sec);
|
|
916
|
+
}
|
|
917
|
+
// Fallback to standard parsing
|
|
918
|
+
return new Date(dateStr);
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
if (event.dateandtime) {
|
|
922
|
+
start = parseZohoDate(event.dateandtime.start);
|
|
923
|
+
end = parseZohoDate(event.dateandtime.end);
|
|
924
|
+
} else {
|
|
925
|
+
start = parseZohoDate(event.start);
|
|
926
|
+
end = parseZohoDate(event.end);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
id: event.uid || "",
|
|
931
|
+
title: event.title || "(No title)",
|
|
932
|
+
start,
|
|
933
|
+
end,
|
|
934
|
+
isAllDay,
|
|
935
|
+
account: extractAccountName(account.email),
|
|
936
|
+
accountEmail: account.email,
|
|
937
|
+
accountIndex,
|
|
938
|
+
provider: "zoho" as const,
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
} catch {
|
|
942
|
+
return [];
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
const results = await Promise.all(eventPromises);
|
|
947
|
+
return results.flat();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
|
|
951
|
+
const now = new Date();
|
|
952
|
+
const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
|
|
953
|
+
|
|
954
|
+
// Fetch from both providers in parallel
|
|
955
|
+
const [googleEvents, zohoEvents] = await Promise.all([
|
|
956
|
+
getGoogleEvents(config, now, timeMax),
|
|
957
|
+
getZohoEvents(config, now, timeMax),
|
|
958
|
+
]);
|
|
959
|
+
|
|
960
|
+
const allEvents = [...googleEvents, ...zohoEvents];
|
|
961
|
+
allEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
962
|
+
return allEvents;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ============================================================================
|
|
966
|
+
// Zoho Tasks
|
|
967
|
+
// ============================================================================
|
|
968
|
+
|
|
969
|
+
interface ZohoTask {
|
|
970
|
+
id: string;
|
|
971
|
+
title: string;
|
|
972
|
+
description: string;
|
|
973
|
+
dueDate: Date | null;
|
|
974
|
+
priority: "High" | "Normal" | "Low";
|
|
975
|
+
status: string;
|
|
976
|
+
isOverdue: boolean;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function getZohoTasks(config: Config): Promise<ZohoTask[]> {
|
|
980
|
+
if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
|
|
981
|
+
if (!config.showZohoTasks) return [];
|
|
982
|
+
|
|
983
|
+
const allTasks: ZohoTask[] = [];
|
|
984
|
+
|
|
985
|
+
for (const account of config.zohoAccounts) {
|
|
986
|
+
try {
|
|
987
|
+
const token = await refreshZohoToken(account);
|
|
988
|
+
if (!token) continue;
|
|
989
|
+
|
|
990
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
991
|
+
const mailBase = dc.mail;
|
|
992
|
+
|
|
993
|
+
// Fetch tasks assigned to user
|
|
994
|
+
const response = await fetch(
|
|
995
|
+
`${mailBase}/api/tasks/?view=assignedtome&action=view&limit=10&from=0`,
|
|
996
|
+
{
|
|
997
|
+
headers: {
|
|
998
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
999
|
+
Accept: "application/json",
|
|
1000
|
+
},
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
if (!response.ok) continue;
|
|
1005
|
+
|
|
1006
|
+
const data = await response.json();
|
|
1007
|
+
const tasks = data.data?.tasks || [];
|
|
1008
|
+
|
|
1009
|
+
const now = new Date();
|
|
1010
|
+
|
|
1011
|
+
for (const task of tasks) {
|
|
1012
|
+
// Skip completed tasks
|
|
1013
|
+
if (task.status === "Completed" || task.status === "completed") continue;
|
|
1014
|
+
|
|
1015
|
+
let dueDate: Date | null = null;
|
|
1016
|
+
let isOverdue = false;
|
|
1017
|
+
|
|
1018
|
+
if (task.dueDate) {
|
|
1019
|
+
// Parse DD/MM/YYYY format
|
|
1020
|
+
const parts = task.dueDate.split("/");
|
|
1021
|
+
if (parts.length === 3) {
|
|
1022
|
+
dueDate = new Date(+parts[2], +parts[1] - 1, +parts[0]);
|
|
1023
|
+
isOverdue = dueDate < now;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
allTasks.push({
|
|
1028
|
+
id: task.id || "",
|
|
1029
|
+
title: task.title || "(No title)",
|
|
1030
|
+
description: task.description || "",
|
|
1031
|
+
dueDate,
|
|
1032
|
+
priority: task.priority || "Normal",
|
|
1033
|
+
status: task.status || "Open",
|
|
1034
|
+
isOverdue,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
} catch {
|
|
1038
|
+
// Silently continue on error
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Sort: overdue first, then by due date (soonest first), then by priority
|
|
1043
|
+
allTasks.sort((a, b) => {
|
|
1044
|
+
// Overdue tasks first
|
|
1045
|
+
if (a.isOverdue && !b.isOverdue) return -1;
|
|
1046
|
+
if (!a.isOverdue && b.isOverdue) return 1;
|
|
1047
|
+
|
|
1048
|
+
// Then by due date (null dates go last)
|
|
1049
|
+
if (a.dueDate && b.dueDate) {
|
|
1050
|
+
return a.dueDate.getTime() - b.dueDate.getTime();
|
|
1051
|
+
}
|
|
1052
|
+
if (a.dueDate && !b.dueDate) return -1;
|
|
1053
|
+
if (!a.dueDate && b.dueDate) return 1;
|
|
1054
|
+
|
|
1055
|
+
// Then by priority
|
|
1056
|
+
const priorityOrder = { High: 0, Normal: 1, Low: 2 };
|
|
1057
|
+
return (priorityOrder[a.priority] || 1) - (priorityOrder[b.priority] || 1);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
return allTasks.slice(0, config.maxTasksToShow);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function formatTasks(tasks: ZohoTask[]): string | null {
|
|
1064
|
+
if (tasks.length === 0) return null;
|
|
1065
|
+
|
|
1066
|
+
const formatted = tasks.map((task) => {
|
|
1067
|
+
const title = task.title.length > 25 ? task.title.slice(0, 24) + "…" : task.title;
|
|
1068
|
+
|
|
1069
|
+
if (task.isOverdue) {
|
|
1070
|
+
return `${COLORS.red}${title}${COLORS.reset}`;
|
|
1071
|
+
} else if (task.priority === "High") {
|
|
1072
|
+
return `${COLORS.yellow}${title}${COLORS.reset}`;
|
|
1073
|
+
} else {
|
|
1074
|
+
return `${COLORS.white}${title}${COLORS.reset}`;
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
return `${COLORS.cyan}Tasks:${COLORS.reset} ${formatted.join(", ")}`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function extractAccountName(email: string): string {
|
|
1082
|
+
const atIndex = email.indexOf("@");
|
|
1083
|
+
if (atIndex === -1) return email;
|
|
1084
|
+
|
|
1085
|
+
const domain = email.slice(atIndex + 1);
|
|
1086
|
+
if (domain === "gmail.com") {
|
|
1087
|
+
return email.slice(0, atIndex);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return domain.split(".")[0];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function getCurrentOrNextEvent(events: CalendarEvent[]): CalendarEvent | null {
|
|
1094
|
+
const now = new Date();
|
|
1095
|
+
|
|
1096
|
+
for (const event of events) {
|
|
1097
|
+
if (event.start <= now && event.end > now) return event;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
for (const event of events) {
|
|
1101
|
+
if (event.start > now) return event;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ============================================================================
|
|
1108
|
+
// Formatter
|
|
1109
|
+
// ============================================================================
|
|
1110
|
+
|
|
1111
|
+
function formatEvent(event: CalendarEvent, config: Config): string {
|
|
1112
|
+
const now = new Date();
|
|
1113
|
+
const isHappening = event.start <= now && event.end > now;
|
|
1114
|
+
const minutesUntil = Math.round((event.start.getTime() - now.getTime()) / 60000);
|
|
1115
|
+
|
|
1116
|
+
let timeStr: string;
|
|
1117
|
+
if (isHappening) {
|
|
1118
|
+
timeStr = "Now";
|
|
1119
|
+
} else if (minutesUntil <= config.countdownThresholdMinutes && minutesUntil > 0) {
|
|
1120
|
+
timeStr = formatCountdown(minutesUntil);
|
|
1121
|
+
} else {
|
|
1122
|
+
timeStr = formatTime(event.start);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const title = event.title.length > config.maxTitleLength
|
|
1126
|
+
? event.title.slice(0, config.maxTitleLength - 1) + "…"
|
|
1127
|
+
: event.title;
|
|
1128
|
+
|
|
1129
|
+
const colorName = ACCOUNT_COLORS[event.accountIndex % ACCOUNT_COLORS.length];
|
|
1130
|
+
const color = COLORS[colorName] || COLORS.white;
|
|
1131
|
+
|
|
1132
|
+
if (config.showCalendarName) {
|
|
1133
|
+
return `${color}${timeStr}: ${title} (${event.account})${COLORS.reset}`;
|
|
1134
|
+
}
|
|
1135
|
+
return `${color}${timeStr}: ${title}${COLORS.reset}`;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function formatCountdown(minutes: number): string {
|
|
1139
|
+
if (minutes < 60) return `In ${minutes}m`;
|
|
1140
|
+
const hours = Math.floor(minutes / 60);
|
|
1141
|
+
const mins = minutes % 60;
|
|
1142
|
+
return mins === 0 ? `In ${hours}h` : `In ${hours}h${mins}m`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function getMeetingWarning(event: CalendarEvent | null): string | null {
|
|
1146
|
+
if (!event) return null;
|
|
1147
|
+
|
|
1148
|
+
const now = new Date();
|
|
1149
|
+
const minutesUntil = Math.round((event.start.getTime() - now.getTime()) / 60000);
|
|
1150
|
+
|
|
1151
|
+
// Warning when meeting is 5 minutes or less away
|
|
1152
|
+
if (minutesUntil > 0 && minutesUntil <= 5) {
|
|
1153
|
+
return `${COLORS.brightRed}Meeting in ${minutesUntil}m - wrap up!${COLORS.reset}`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// ============================================================================
|
|
1160
|
+
// System Stats
|
|
1161
|
+
// ============================================================================
|
|
1162
|
+
|
|
1163
|
+
function getCpuUsage(): string | null {
|
|
1164
|
+
try {
|
|
1165
|
+
const os = require("os");
|
|
1166
|
+
const cpus = os.cpus();
|
|
1167
|
+
|
|
1168
|
+
let totalIdle = 0;
|
|
1169
|
+
let totalTick = 0;
|
|
1170
|
+
|
|
1171
|
+
for (const cpu of cpus) {
|
|
1172
|
+
for (const type in cpu.times) {
|
|
1173
|
+
totalTick += cpu.times[type as keyof typeof cpu.times];
|
|
1174
|
+
}
|
|
1175
|
+
totalIdle += cpu.times.idle;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const usage = Math.round(100 - (totalIdle / totalTick) * 100);
|
|
1179
|
+
|
|
1180
|
+
// Color based on usage
|
|
1181
|
+
let color = COLORS.green;
|
|
1182
|
+
if (usage >= 80) color = COLORS.red;
|
|
1183
|
+
else if (usage >= 50) color = COLORS.yellow;
|
|
1184
|
+
|
|
1185
|
+
return `${color}CPU ${usage}%${COLORS.reset}`;
|
|
1186
|
+
} catch {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function getMemoryUsage(): string | null {
|
|
1192
|
+
try {
|
|
1193
|
+
const os = require("os");
|
|
1194
|
+
const totalMem = os.totalmem();
|
|
1195
|
+
|
|
1196
|
+
// Read MemAvailable from /proc/meminfo (Linux only)
|
|
1197
|
+
// MemAvailable accounts for reclaimable cache/buffers
|
|
1198
|
+
let availableMem = os.freemem(); // fallback for non-Linux
|
|
1199
|
+
|
|
1200
|
+
if (process.platform === "linux") {
|
|
1201
|
+
try {
|
|
1202
|
+
const meminfo = readFileSync("/proc/meminfo", "utf-8");
|
|
1203
|
+
const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
1204
|
+
if (match) {
|
|
1205
|
+
availableMem = parseInt(match[1], 10) * 1024; // Convert kB to bytes
|
|
1206
|
+
}
|
|
1207
|
+
} catch {
|
|
1208
|
+
// Fall back to freemem if /proc/meminfo is unavailable
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const usedMem = totalMem - availableMem;
|
|
1213
|
+
const usagePercent = Math.round((usedMem / totalMem) * 100);
|
|
1214
|
+
|
|
1215
|
+
// Format used memory
|
|
1216
|
+
const usedGB = (usedMem / (1024 * 1024 * 1024)).toFixed(1);
|
|
1217
|
+
const totalGB = (totalMem / (1024 * 1024 * 1024)).toFixed(1);
|
|
1218
|
+
|
|
1219
|
+
// Color based on usage
|
|
1220
|
+
let color = COLORS.green;
|
|
1221
|
+
if (usagePercent >= 80) color = COLORS.red;
|
|
1222
|
+
else if (usagePercent >= 50) color = COLORS.yellow;
|
|
1223
|
+
|
|
1224
|
+
return `${color}Mem ${usedGB}/${totalGB}GB${COLORS.reset}`;
|
|
1225
|
+
} catch {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function formatTime(date: Date): string {
|
|
1231
|
+
const hours = date.getHours();
|
|
1232
|
+
const minutes = date.getMinutes();
|
|
1233
|
+
const isPM = hours >= 12;
|
|
1234
|
+
const hour12 = hours % 12 || 12;
|
|
1235
|
+
return `${hour12}:${minutes.toString().padStart(2, "0")} ${isPM ? "PM" : "AM"}`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
// CLI Commands
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
|
|
1242
|
+
function printHelp() {
|
|
1243
|
+
console.log(`
|
|
1244
|
+
glancebar - A customizable statusline for Claude Code
|
|
1245
|
+
|
|
1246
|
+
Display calendar events, tasks, and more at a glance.
|
|
1247
|
+
|
|
1248
|
+
Usage:
|
|
1249
|
+
glancebar Output statusline (for Claude Code)
|
|
1250
|
+
glancebar auth Authenticate all configured accounts
|
|
1251
|
+
glancebar auth --add <email> Add and authenticate a new account (prompts for provider)
|
|
1252
|
+
glancebar auth --remove <email> Remove an account
|
|
1253
|
+
glancebar auth --list List all configured accounts
|
|
1254
|
+
glancebar config Show current configuration
|
|
1255
|
+
glancebar config --lookahead <hours> Set lookahead hours (default: 8)
|
|
1256
|
+
glancebar config --countdown-threshold <mins> Set countdown threshold in minutes (default: 60)
|
|
1257
|
+
glancebar config --max-title <length> Set max title length (default: 120)
|
|
1258
|
+
glancebar config --show-calendar <true|false> Show calendar name (default: true)
|
|
1259
|
+
glancebar config --water-reminder <true|false> Enable/disable water reminders (default: true)
|
|
1260
|
+
glancebar config --stretch-reminder <true|false> Enable/disable stretch reminders (default: true)
|
|
1261
|
+
glancebar config --eye-reminder <true|false> Enable/disable eye break reminders (default: true)
|
|
1262
|
+
glancebar config --cpu-usage <true|false> Show CPU usage (default: false)
|
|
1263
|
+
glancebar config --memory-usage <true|false> Show memory usage (default: false)
|
|
1264
|
+
glancebar config --zoho-tasks <true|false> Show Zoho tasks (default: true)
|
|
1265
|
+
glancebar config --max-tasks <number> Max tasks to show (default: 3)
|
|
1266
|
+
glancebar config --show-usage-limits <true|false> Show usage limits from API (default: true)
|
|
1267
|
+
glancebar config --show-5hour-limit <true|false> Show 5-hour window utilization (default: true)
|
|
1268
|
+
glancebar config --show-7day-limit <true|false> Show 7-day window utilization (default: true)
|
|
1269
|
+
glancebar config --show-7day-sonnet-limit <true|false> Show 7-day Sonnet limit (default: true)
|
|
1270
|
+
glancebar config --show-5hour-resets <true|false> Show 5-hour reset time (default: false)
|
|
1271
|
+
glancebar config --show-7day-resets <true|false> Show 7-day reset time (default: false)
|
|
1272
|
+
glancebar config --show-7day-sonnet-resets <true|false> Show 7-day Sonnet reset time (default: false)
|
|
1273
|
+
glancebar config --resets-time-format <relative|absolute> Reset time display format (default: relative)
|
|
1274
|
+
glancebar config --usage-cache-ttl <seconds> API cache TTL in seconds (default: 120)
|
|
1275
|
+
glancebar config --reset Reset to default configuration
|
|
1276
|
+
glancebar setup Show setup instructions
|
|
1277
|
+
glancebar --version Show version
|
|
1278
|
+
|
|
1279
|
+
Examples:
|
|
1280
|
+
glancebar auth --add user@gmail.com # Will prompt for Google or Zoho
|
|
1281
|
+
glancebar auth --add user@zoho.com # Will prompt for Google or Zoho
|
|
1282
|
+
glancebar config --lookahead 12
|
|
1283
|
+
glancebar config --stretch-reminder false
|
|
1284
|
+
|
|
1285
|
+
Config location: ${getConfigDir()}
|
|
1286
|
+
`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function printSetup() {
|
|
1290
|
+
console.log(`
|
|
1291
|
+
Glancebar - Setup Instructions
|
|
1292
|
+
==============================
|
|
1293
|
+
|
|
1294
|
+
GOOGLE CALENDAR SETUP
|
|
1295
|
+
---------------------
|
|
1296
|
+
|
|
1297
|
+
Step 1: Create Google Cloud Project
|
|
1298
|
+
- Go to https://console.cloud.google.com/
|
|
1299
|
+
- Create a new project or select existing one
|
|
1300
|
+
|
|
1301
|
+
Step 2: Enable Google Calendar API
|
|
1302
|
+
- Go to "APIs & Services" > "Library"
|
|
1303
|
+
- Search for "Google Calendar API" and enable it
|
|
1304
|
+
|
|
1305
|
+
Step 3: Create OAuth Credentials
|
|
1306
|
+
- Go to "APIs & Services" > "Credentials"
|
|
1307
|
+
- Click "Create Credentials" > "OAuth client ID"
|
|
1308
|
+
- Select "Desktop app" as application type
|
|
1309
|
+
- Download the JSON file
|
|
1310
|
+
|
|
1311
|
+
Step 4: Save credentials
|
|
1312
|
+
- Rename downloaded file to "credentials.json"
|
|
1313
|
+
- Save it to: ${getGoogleCredentialsPath()}
|
|
1314
|
+
|
|
1315
|
+
Step 5: Add redirect URI
|
|
1316
|
+
- Edit credentials.json and ensure redirect_uris contains:
|
|
1317
|
+
"redirect_uris": ["http://localhost:3000/callback"]
|
|
1318
|
+
|
|
1319
|
+
ZOHO CALENDAR SETUP
|
|
1320
|
+
-------------------
|
|
1321
|
+
|
|
1322
|
+
Step 1: Register Application
|
|
1323
|
+
- Go to https://api-console.zoho.com/
|
|
1324
|
+
- Click "Add Client" > "Server-based Applications"
|
|
1325
|
+
|
|
1326
|
+
Step 2: Configure Client
|
|
1327
|
+
- Set Authorized Redirect URI: http://localhost:3000/callback
|
|
1328
|
+
- Note your Client ID and Client Secret
|
|
1329
|
+
|
|
1330
|
+
Step 3: Save credentials
|
|
1331
|
+
- Create file: ${getZohoCredentialsPath()}
|
|
1332
|
+
- Add content:
|
|
1333
|
+
{
|
|
1334
|
+
"client_id": "YOUR_CLIENT_ID",
|
|
1335
|
+
"client_secret": "YOUR_CLIENT_SECRET"
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
ADDING ACCOUNTS
|
|
1339
|
+
---------------
|
|
1340
|
+
|
|
1341
|
+
glancebar auth --add your-email@gmail.com
|
|
1342
|
+
# Select "Google" or "Zoho" when prompted
|
|
1343
|
+
# For Zoho, select your datacenter region
|
|
1344
|
+
|
|
1345
|
+
CONFIGURE CLAUDE CODE
|
|
1346
|
+
---------------------
|
|
1347
|
+
|
|
1348
|
+
Update ~/.claude/settings.json:
|
|
1349
|
+
{
|
|
1350
|
+
"statusLine": {
|
|
1351
|
+
"type": "command",
|
|
1352
|
+
"command": "bunx @naarang/glancebar",
|
|
1353
|
+
"padding": 0
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
For more info: https://github.com/vishal-android-freak/glancebar
|
|
1358
|
+
`);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
async function handleAuth(args: string[]) {
|
|
1362
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1363
|
+
const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
|
|
1364
|
+
|
|
1365
|
+
// Handle --list
|
|
1366
|
+
if (args.includes("--list")) {
|
|
1367
|
+
const config = loadConfig();
|
|
1368
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1369
|
+
const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
1370
|
+
|
|
1371
|
+
if (!hasAny) {
|
|
1372
|
+
console.log("No accounts configured.");
|
|
1373
|
+
} else {
|
|
1374
|
+
if (gmailAccounts.length > 0) {
|
|
1375
|
+
console.log("\nGoogle Calendar accounts:");
|
|
1376
|
+
gmailAccounts.forEach((acc, i) => {
|
|
1377
|
+
let tokenPath = getGoogleTokenPath(acc);
|
|
1378
|
+
if (!existsSync(tokenPath)) {
|
|
1379
|
+
tokenPath = getLegacyTokenPath(acc);
|
|
1380
|
+
}
|
|
1381
|
+
const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
|
|
1382
|
+
console.log(` ${i + 1}. ${acc} (${status})`);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (config.zohoAccounts.length > 0) {
|
|
1387
|
+
console.log("\nZoho Calendar accounts:");
|
|
1388
|
+
config.zohoAccounts.forEach((acc, i) => {
|
|
1389
|
+
const tokenPath = getZohoTokenPath(acc.email);
|
|
1390
|
+
const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
|
|
1391
|
+
console.log(` ${i + 1}. ${acc.email} [${acc.datacenter}] (${status})`);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
rl.close();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Handle --add
|
|
1400
|
+
const addIndex = args.indexOf("--add");
|
|
1401
|
+
if (addIndex !== -1) {
|
|
1402
|
+
const email = args[addIndex + 1];
|
|
1403
|
+
if (!email || email.startsWith("--")) {
|
|
1404
|
+
console.error("Error: Please provide an email address after --add");
|
|
1405
|
+
rl.close();
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (!email.includes("@")) {
|
|
1410
|
+
console.error("Error: Invalid email address");
|
|
1411
|
+
rl.close();
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Prompt for provider
|
|
1416
|
+
console.log("\nSelect calendar provider:");
|
|
1417
|
+
console.log(" 1. Google Calendar");
|
|
1418
|
+
console.log(" 2. Zoho Calendar");
|
|
1419
|
+
const providerChoice = await prompt("\nEnter choice (1 or 2): ");
|
|
1420
|
+
|
|
1421
|
+
const config = loadConfig();
|
|
1422
|
+
|
|
1423
|
+
if (providerChoice === "1") {
|
|
1424
|
+
// Google Calendar
|
|
1425
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1426
|
+
if (gmailAccounts.includes(email)) {
|
|
1427
|
+
console.log(`\nGoogle account ${email} already exists. Re-authenticating...`);
|
|
1428
|
+
} else {
|
|
1429
|
+
if (config.gmailAccounts.length === 0 && config.accounts.length > 0) {
|
|
1430
|
+
config.gmailAccounts = [...config.accounts];
|
|
1431
|
+
}
|
|
1432
|
+
config.gmailAccounts.push(email);
|
|
1433
|
+
saveConfig(config);
|
|
1434
|
+
console.log(`\nAdded ${email} to Google accounts.`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
await authenticateGoogleAccount(email);
|
|
1438
|
+
console.log("\nDone!");
|
|
1439
|
+
} else if (providerChoice === "2") {
|
|
1440
|
+
// Zoho Calendar
|
|
1441
|
+
console.log("\nSelect Zoho datacenter:");
|
|
1442
|
+
console.log(" 1. com - United States");
|
|
1443
|
+
console.log(" 2. eu - Europe");
|
|
1444
|
+
console.log(" 3. in - India");
|
|
1445
|
+
console.log(" 4. com.au - Australia");
|
|
1446
|
+
console.log(" 5. com.cn - China");
|
|
1447
|
+
console.log(" 6. jp - Japan");
|
|
1448
|
+
console.log(" 7. zohocloud.ca - Canada");
|
|
1449
|
+
|
|
1450
|
+
const dcChoice = await prompt("\nEnter choice (1-7): ");
|
|
1451
|
+
const dcMap: Record<string, string> = {
|
|
1452
|
+
"1": "com",
|
|
1453
|
+
"2": "eu",
|
|
1454
|
+
"3": "in",
|
|
1455
|
+
"4": "com.au",
|
|
1456
|
+
"5": "com.cn",
|
|
1457
|
+
"6": "jp",
|
|
1458
|
+
"7": "zohocloud.ca",
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
const datacenter = dcMap[dcChoice];
|
|
1462
|
+
if (!datacenter) {
|
|
1463
|
+
console.error("Error: Invalid datacenter choice");
|
|
1464
|
+
rl.close();
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const existingZoho = config.zohoAccounts.find((z) => z.email === email);
|
|
1469
|
+
if (existingZoho) {
|
|
1470
|
+
console.log(`\nZoho account ${email} already exists. Re-authenticating...`);
|
|
1471
|
+
existingZoho.datacenter = datacenter;
|
|
1472
|
+
saveConfig(config);
|
|
1473
|
+
} else {
|
|
1474
|
+
config.zohoAccounts.push({ email, datacenter });
|
|
1475
|
+
saveConfig(config);
|
|
1476
|
+
console.log(`\nAdded ${email} to Zoho accounts.`);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
await authenticateZohoAccount({ email, datacenter });
|
|
1480
|
+
console.log("\nDone!");
|
|
1481
|
+
} else {
|
|
1482
|
+
console.error("Error: Invalid choice. Please enter 1 or 2.");
|
|
1483
|
+
rl.close();
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
rl.close();
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Handle --remove
|
|
1492
|
+
const removeIndex = args.indexOf("--remove");
|
|
1493
|
+
if (removeIndex !== -1) {
|
|
1494
|
+
const email = args[removeIndex + 1];
|
|
1495
|
+
if (!email || email.startsWith("--")) {
|
|
1496
|
+
console.error("Error: Please provide an email address after --remove");
|
|
1497
|
+
rl.close();
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const config = loadConfig();
|
|
1502
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1503
|
+
|
|
1504
|
+
// Check Google accounts
|
|
1505
|
+
const googleIdx = gmailAccounts.indexOf(email);
|
|
1506
|
+
if (googleIdx !== -1) {
|
|
1507
|
+
if (config.gmailAccounts.length > 0) {
|
|
1508
|
+
config.gmailAccounts.splice(googleIdx, 1);
|
|
1509
|
+
} else {
|
|
1510
|
+
config.accounts.splice(googleIdx, 1);
|
|
1511
|
+
}
|
|
1512
|
+
saveConfig(config);
|
|
1513
|
+
|
|
1514
|
+
// Remove token files
|
|
1515
|
+
const tokenPath = getGoogleTokenPath(email);
|
|
1516
|
+
const legacyPath = getLegacyTokenPath(email);
|
|
1517
|
+
if (existsSync(tokenPath)) unlinkSync(tokenPath);
|
|
1518
|
+
if (existsSync(legacyPath)) unlinkSync(legacyPath);
|
|
1519
|
+
|
|
1520
|
+
console.log(`Removed Google account ${email}.`);
|
|
1521
|
+
rl.close();
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Check Zoho accounts
|
|
1526
|
+
const zohoIdx = config.zohoAccounts.findIndex((z) => z.email === email);
|
|
1527
|
+
if (zohoIdx !== -1) {
|
|
1528
|
+
config.zohoAccounts.splice(zohoIdx, 1);
|
|
1529
|
+
saveConfig(config);
|
|
1530
|
+
|
|
1531
|
+
const tokenPath = getZohoTokenPath(email);
|
|
1532
|
+
if (existsSync(tokenPath)) unlinkSync(tokenPath);
|
|
1533
|
+
|
|
1534
|
+
console.log(`Removed Zoho account ${email}.`);
|
|
1535
|
+
rl.close();
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
console.error(`Error: Account ${email} not found.`);
|
|
1540
|
+
rl.close();
|
|
1541
|
+
process.exit(1);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Default: authenticate all accounts
|
|
1545
|
+
const config = loadConfig();
|
|
1546
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1547
|
+
const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
1548
|
+
|
|
1549
|
+
if (!hasAny) {
|
|
1550
|
+
console.log("No accounts configured.\n");
|
|
1551
|
+
console.log("Add an account using:");
|
|
1552
|
+
console.log(" glancebar auth --add your-email@gmail.com\n");
|
|
1553
|
+
rl.close();
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
console.log("Glancebar - Calendar Authentication");
|
|
1558
|
+
console.log("====================================\n");
|
|
1559
|
+
|
|
1560
|
+
// Authenticate Google accounts
|
|
1561
|
+
for (const account of gmailAccounts) {
|
|
1562
|
+
let tokenPath = getGoogleTokenPath(account);
|
|
1563
|
+
if (!existsSync(tokenPath)) {
|
|
1564
|
+
tokenPath = getLegacyTokenPath(account);
|
|
1565
|
+
}
|
|
1566
|
+
if (existsSync(tokenPath)) {
|
|
1567
|
+
console.log(`Google: ${account} - Already authenticated`);
|
|
1568
|
+
const answer = await prompt(`Re-authenticate? (y/N): `);
|
|
1569
|
+
if (answer.toLowerCase() !== "y") continue;
|
|
1570
|
+
}
|
|
1571
|
+
await authenticateGoogleAccount(account);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Authenticate Zoho accounts
|
|
1575
|
+
for (const account of config.zohoAccounts) {
|
|
1576
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
1577
|
+
if (existsSync(tokenPath)) {
|
|
1578
|
+
console.log(`Zoho: ${account.email} [${account.datacenter}] - Already authenticated`);
|
|
1579
|
+
const answer = await prompt(`Re-authenticate? (y/N): `);
|
|
1580
|
+
if (answer.toLowerCase() !== "y") continue;
|
|
1581
|
+
}
|
|
1582
|
+
await authenticateZohoAccount(account);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
rl.close();
|
|
1586
|
+
console.log("\nAll accounts authenticated!");
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function handleConfig(args: string[]) {
|
|
1590
|
+
const config = loadConfig();
|
|
1591
|
+
|
|
1592
|
+
// Handle --reset
|
|
1593
|
+
if (args.includes("--reset")) {
|
|
1594
|
+
// Preserve all account types
|
|
1595
|
+
const { accounts, gmailAccounts, zohoAccounts } = config;
|
|
1596
|
+
saveConfig({ ...DEFAULT_CONFIG, accounts, gmailAccounts, zohoAccounts });
|
|
1597
|
+
console.log("Configuration reset to defaults (accounts preserved).");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Handle --lookahead
|
|
1602
|
+
const lookaheadIndex = args.indexOf("--lookahead");
|
|
1603
|
+
if (lookaheadIndex !== -1) {
|
|
1604
|
+
const value = parseInt(args[lookaheadIndex + 1], 10);
|
|
1605
|
+
if (isNaN(value) || value < 1 || value > 168) {
|
|
1606
|
+
console.error("Error: lookahead must be between 1 and 168 hours");
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
config.lookaheadHours = value;
|
|
1610
|
+
saveConfig(config);
|
|
1611
|
+
console.log(`Lookahead hours set to ${value}`);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Handle --countdown-threshold
|
|
1616
|
+
const countdownIndex = args.indexOf("--countdown-threshold");
|
|
1617
|
+
if (countdownIndex !== -1) {
|
|
1618
|
+
const value = parseInt(args[countdownIndex + 1], 10);
|
|
1619
|
+
if (isNaN(value) || value < 0 || value > 1440) {
|
|
1620
|
+
console.error("Error: countdown-threshold must be between 0 and 1440 minutes");
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
config.countdownThresholdMinutes = value;
|
|
1624
|
+
saveConfig(config);
|
|
1625
|
+
console.log(`Countdown threshold set to ${value} minutes`);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Handle --max-title
|
|
1630
|
+
const maxTitleIndex = args.indexOf("--max-title");
|
|
1631
|
+
if (maxTitleIndex !== -1) {
|
|
1632
|
+
const value = parseInt(args[maxTitleIndex + 1], 10);
|
|
1633
|
+
if (isNaN(value) || value < 10 || value > 500) {
|
|
1634
|
+
console.error("Error: max-title must be between 10 and 500");
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
1637
|
+
config.maxTitleLength = value;
|
|
1638
|
+
saveConfig(config);
|
|
1639
|
+
console.log(`Max title length set to ${value}`);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Handle --show-calendar
|
|
1644
|
+
const showCalIndex = args.indexOf("--show-calendar");
|
|
1645
|
+
if (showCalIndex !== -1) {
|
|
1646
|
+
const value = args[showCalIndex + 1]?.toLowerCase();
|
|
1647
|
+
if (value !== "true" && value !== "false") {
|
|
1648
|
+
console.error("Error: --show-calendar must be 'true' or 'false'");
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
config.showCalendarName = value === "true";
|
|
1652
|
+
saveConfig(config);
|
|
1653
|
+
console.log(`Show calendar name set to ${value}`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Handle --water-reminder
|
|
1658
|
+
const waterReminderIndex = args.indexOf("--water-reminder");
|
|
1659
|
+
if (waterReminderIndex !== -1) {
|
|
1660
|
+
const value = args[waterReminderIndex + 1]?.toLowerCase();
|
|
1661
|
+
if (value !== "true" && value !== "false") {
|
|
1662
|
+
console.error("Error: --water-reminder must be 'true' or 'false'");
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
config.waterReminderEnabled = value === "true";
|
|
1666
|
+
saveConfig(config);
|
|
1667
|
+
console.log(`Water reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Handle --stretch-reminder
|
|
1672
|
+
const stretchReminderIndex = args.indexOf("--stretch-reminder");
|
|
1673
|
+
if (stretchReminderIndex !== -1) {
|
|
1674
|
+
const value = args[stretchReminderIndex + 1]?.toLowerCase();
|
|
1675
|
+
if (value !== "true" && value !== "false") {
|
|
1676
|
+
console.error("Error: --stretch-reminder must be 'true' or 'false'");
|
|
1677
|
+
process.exit(1);
|
|
1678
|
+
}
|
|
1679
|
+
config.stretchReminderEnabled = value === "true";
|
|
1680
|
+
saveConfig(config);
|
|
1681
|
+
console.log(`Stretch reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Handle --eye-reminder
|
|
1686
|
+
const eyeReminderIndex = args.indexOf("--eye-reminder");
|
|
1687
|
+
if (eyeReminderIndex !== -1) {
|
|
1688
|
+
const value = args[eyeReminderIndex + 1]?.toLowerCase();
|
|
1689
|
+
if (value !== "true" && value !== "false") {
|
|
1690
|
+
console.error("Error: --eye-reminder must be 'true' or 'false'");
|
|
1691
|
+
process.exit(1);
|
|
1692
|
+
}
|
|
1693
|
+
config.eyeReminderEnabled = value === "true";
|
|
1694
|
+
saveConfig(config);
|
|
1695
|
+
console.log(`Eye break reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Handle --cpu-usage
|
|
1700
|
+
const cpuUsageIndex = args.indexOf("--cpu-usage");
|
|
1701
|
+
if (cpuUsageIndex !== -1) {
|
|
1702
|
+
const value = args[cpuUsageIndex + 1]?.toLowerCase();
|
|
1703
|
+
if (value !== "true" && value !== "false") {
|
|
1704
|
+
console.error("Error: --cpu-usage must be 'true' or 'false'");
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
config.showCpuUsage = value === "true";
|
|
1708
|
+
saveConfig(config);
|
|
1709
|
+
console.log(`CPU usage display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Handle --memory-usage
|
|
1714
|
+
const memoryUsageIndex = args.indexOf("--memory-usage");
|
|
1715
|
+
if (memoryUsageIndex !== -1) {
|
|
1716
|
+
const value = args[memoryUsageIndex + 1]?.toLowerCase();
|
|
1717
|
+
if (value !== "true" && value !== "false") {
|
|
1718
|
+
console.error("Error: --memory-usage must be 'true' or 'false'");
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
}
|
|
1721
|
+
config.showMemoryUsage = value === "true";
|
|
1722
|
+
saveConfig(config);
|
|
1723
|
+
console.log(`Memory usage display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Handle --zoho-tasks
|
|
1728
|
+
const zohoTasksIndex = args.indexOf("--zoho-tasks");
|
|
1729
|
+
if (zohoTasksIndex !== -1) {
|
|
1730
|
+
const value = args[zohoTasksIndex + 1]?.toLowerCase();
|
|
1731
|
+
if (value !== "true" && value !== "false") {
|
|
1732
|
+
console.error("Error: --zoho-tasks must be 'true' or 'false'");
|
|
1733
|
+
process.exit(1);
|
|
1734
|
+
}
|
|
1735
|
+
config.showZohoTasks = value === "true";
|
|
1736
|
+
saveConfig(config);
|
|
1737
|
+
console.log(`Zoho tasks display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Handle --max-tasks
|
|
1742
|
+
const maxTasksIndex = args.indexOf("--max-tasks");
|
|
1743
|
+
if (maxTasksIndex !== -1) {
|
|
1744
|
+
const value = parseInt(args[maxTasksIndex + 1], 10);
|
|
1745
|
+
if (isNaN(value) || value < 1 || value > 10) {
|
|
1746
|
+
console.error("Error: --max-tasks must be between 1 and 10");
|
|
1747
|
+
process.exit(1);
|
|
1748
|
+
}
|
|
1749
|
+
config.maxTasksToShow = value;
|
|
1750
|
+
saveConfig(config);
|
|
1751
|
+
console.log(`Max tasks to show set to ${value}`);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Handle --show-usage-limits
|
|
1756
|
+
const showUsageLimitsIndex = args.indexOf("--show-usage-limits");
|
|
1757
|
+
if (showUsageLimitsIndex !== -1) {
|
|
1758
|
+
const value = args[showUsageLimitsIndex + 1]?.toLowerCase();
|
|
1759
|
+
if (value !== "true" && value !== "false") {
|
|
1760
|
+
console.error("Error: --show-usage-limits must be 'true' or 'false'");
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
config.showUsageLimits = value === "true";
|
|
1764
|
+
saveConfig(config);
|
|
1765
|
+
console.log(`Usage limits display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Handle --show-5hour-limit
|
|
1770
|
+
const show5HourIndex = args.indexOf("--show-5hour-limit");
|
|
1771
|
+
if (show5HourIndex !== -1) {
|
|
1772
|
+
const value = args[show5HourIndex + 1]?.toLowerCase();
|
|
1773
|
+
if (value !== "true" && value !== "false") {
|
|
1774
|
+
console.error("Error: --show-5hour-limit must be 'true' or 'false'");
|
|
1775
|
+
process.exit(1);
|
|
1776
|
+
}
|
|
1777
|
+
config.show5HourLimit = value === "true";
|
|
1778
|
+
saveConfig(config);
|
|
1779
|
+
console.log(`5-hour limit display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Handle --show-7day-limit
|
|
1784
|
+
const show7DayIndex = args.indexOf("--show-7day-limit");
|
|
1785
|
+
if (show7DayIndex !== -1) {
|
|
1786
|
+
const value = args[show7DayIndex + 1]?.toLowerCase();
|
|
1787
|
+
if (value !== "true" && value !== "false") {
|
|
1788
|
+
console.error("Error: --show-7day-limit must be 'true' or 'false'");
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
config.show7DayLimit = value === "true";
|
|
1792
|
+
saveConfig(config);
|
|
1793
|
+
console.log(`7-day limit display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Handle --show-7day-sonnet-limit
|
|
1798
|
+
const show7DaySonnetIndex = args.indexOf("--show-7day-sonnet-limit");
|
|
1799
|
+
if (show7DaySonnetIndex !== -1) {
|
|
1800
|
+
const value = args[show7DaySonnetIndex + 1]?.toLowerCase();
|
|
1801
|
+
if (value !== "true" && value !== "false") {
|
|
1802
|
+
console.error("Error: --show-7day-sonnet-limit must be 'true' or 'false'");
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
config.show7DaySonnetLimit = value === "true";
|
|
1806
|
+
saveConfig(config);
|
|
1807
|
+
console.log(`7-day Sonnet limit display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Handle --show-5hour-resets
|
|
1812
|
+
const show5HourResetsIndex = args.indexOf("--show-5hour-resets");
|
|
1813
|
+
if (show5HourResetsIndex !== -1) {
|
|
1814
|
+
const value = args[show5HourResetsIndex + 1]?.toLowerCase();
|
|
1815
|
+
if (value !== "true" && value !== "false") {
|
|
1816
|
+
console.error("Error: --show-5hour-resets must be 'true' or 'false'");
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
config.show5HourResets = value === "true";
|
|
1820
|
+
saveConfig(config);
|
|
1821
|
+
console.log(`5-hour reset time display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Handle --show-7day-resets
|
|
1826
|
+
const show7DayResetsIndex = args.indexOf("--show-7day-resets");
|
|
1827
|
+
if (show7DayResetsIndex !== -1) {
|
|
1828
|
+
const value = args[show7DayResetsIndex + 1]?.toLowerCase();
|
|
1829
|
+
if (value !== "true" && value !== "false") {
|
|
1830
|
+
console.error("Error: --show-7day-resets must be 'true' or 'false'");
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
config.show7DayResets = value === "true";
|
|
1834
|
+
saveConfig(config);
|
|
1835
|
+
console.log(`7-day reset time display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Handle --show-7day-sonnet-resets
|
|
1840
|
+
const show7DaySonnetResetsIndex = args.indexOf("--show-7day-sonnet-resets");
|
|
1841
|
+
if (show7DaySonnetResetsIndex !== -1) {
|
|
1842
|
+
const value = args[show7DaySonnetResetsIndex + 1]?.toLowerCase();
|
|
1843
|
+
if (value !== "true" && value !== "false") {
|
|
1844
|
+
console.error("Error: --show-7day-sonnet-resets must be 'true' or 'false'");
|
|
1845
|
+
process.exit(1);
|
|
1846
|
+
}
|
|
1847
|
+
config.show7DaySonnetResets = value === "true";
|
|
1848
|
+
saveConfig(config);
|
|
1849
|
+
console.log(`7-day Sonnet reset time display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Handle --resets-time-format
|
|
1854
|
+
const resetsTimeFormatIndex = args.indexOf("--resets-time-format");
|
|
1855
|
+
if (resetsTimeFormatIndex !== -1) {
|
|
1856
|
+
const value = args[resetsTimeFormatIndex + 1]?.toLowerCase();
|
|
1857
|
+
if (value !== "relative" && value !== "absolute") {
|
|
1858
|
+
console.error("Error: --resets-time-format must be 'relative' or 'absolute'");
|
|
1859
|
+
process.exit(1);
|
|
1860
|
+
}
|
|
1861
|
+
config.resetsTimeFormat = value as "relative" | "absolute";
|
|
1862
|
+
saveConfig(config);
|
|
1863
|
+
console.log(`Resets time format set to ${value}`);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Handle --usage-cache-ttl
|
|
1868
|
+
const cacheTTLIndex = args.indexOf("--usage-cache-ttl");
|
|
1869
|
+
if (cacheTTLIndex !== -1) {
|
|
1870
|
+
const value = parseInt(args[cacheTTLIndex + 1], 10);
|
|
1871
|
+
if (isNaN(value) || value < 60 || value > 3600) {
|
|
1872
|
+
console.error("Error: --usage-cache-ttl must be between 60 and 3600 seconds");
|
|
1873
|
+
process.exit(1);
|
|
1874
|
+
}
|
|
1875
|
+
config.usageLimitsCacheTTL = value;
|
|
1876
|
+
saveConfig(config);
|
|
1877
|
+
console.log(`API cache TTL set to ${value} seconds`);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Show current config
|
|
1882
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1883
|
+
const googleStr = gmailAccounts.length > 0 ? gmailAccounts.join(", ") : "(none)";
|
|
1884
|
+
const zohoStr = config.zohoAccounts.length > 0
|
|
1885
|
+
? config.zohoAccounts.map((z) => `${z.email} [${z.datacenter}]`).join(", ")
|
|
1886
|
+
: "(none)";
|
|
1887
|
+
|
|
1888
|
+
console.log(`
|
|
1889
|
+
Glancebar Configuration
|
|
1890
|
+
=======================
|
|
1891
|
+
Config directory: ${getConfigDir()}
|
|
1892
|
+
|
|
1893
|
+
Accounts:
|
|
1894
|
+
Google Calendar: ${googleStr}
|
|
1895
|
+
Zoho Calendar: ${zohoStr}
|
|
1896
|
+
|
|
1897
|
+
Calendar Settings:
|
|
1898
|
+
Lookahead hours: ${config.lookaheadHours}
|
|
1899
|
+
Countdown threshold: ${config.countdownThresholdMinutes} minutes
|
|
1900
|
+
Max title length: ${config.maxTitleLength}
|
|
1901
|
+
Show calendar name: ${config.showCalendarName}
|
|
1902
|
+
|
|
1903
|
+
Reminders:
|
|
1904
|
+
Water reminder: ${config.waterReminderEnabled ? "enabled" : "disabled"}
|
|
1905
|
+
Stretch reminder: ${config.stretchReminderEnabled ? "enabled" : "disabled"}
|
|
1906
|
+
Eye break reminder: ${config.eyeReminderEnabled ? "enabled" : "disabled"}
|
|
1907
|
+
|
|
1908
|
+
System Stats:
|
|
1909
|
+
CPU usage: ${config.showCpuUsage ? "enabled" : "disabled"}
|
|
1910
|
+
Memory usage: ${config.showMemoryUsage ? "enabled" : "disabled"}
|
|
1911
|
+
|
|
1912
|
+
Zoho Tasks:
|
|
1913
|
+
Show tasks: ${config.showZohoTasks ? "enabled" : "disabled"}
|
|
1914
|
+
Max tasks to show: ${config.maxTasksToShow}
|
|
1915
|
+
|
|
1916
|
+
Usage Limits:
|
|
1917
|
+
Show usage limits: ${config.showUsageLimits ? "enabled" : "disabled"}
|
|
1918
|
+
Show 5-hour limit: ${config.show5HourLimit ? "enabled" : "disabled"}
|
|
1919
|
+
Show 7-day limit: ${config.show7DayLimit ? "enabled" : "disabled"}
|
|
1920
|
+
Show 7-day Sonnet limit: ${config.show7DaySonnetLimit ? "enabled" : "disabled"}
|
|
1921
|
+
Show 5-hour resets: ${config.show5HourResets ? "enabled" : "disabled"}
|
|
1922
|
+
Show 7-day resets: ${config.show7DayResets ? "enabled" : "disabled"}
|
|
1923
|
+
Show 7-day Sonnet resets: ${config.show7DaySonnetResets ? "enabled" : "disabled"}
|
|
1924
|
+
Resets time format: ${config.resetsTimeFormat}
|
|
1925
|
+
Cache TTL: ${config.usageLimitsCacheTTL} seconds
|
|
1926
|
+
`);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function getRandomReminder(config: Config): string | null {
|
|
1930
|
+
const enabledReminders: Array<() => string> = [];
|
|
1931
|
+
|
|
1932
|
+
if (config.waterReminderEnabled) {
|
|
1933
|
+
enabledReminders.push(() => {
|
|
1934
|
+
const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
|
|
1935
|
+
return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (config.stretchReminderEnabled) {
|
|
1940
|
+
enabledReminders.push(() => {
|
|
1941
|
+
const reminder = STRETCH_REMINDERS[Math.floor(Math.random() * STRETCH_REMINDERS.length)];
|
|
1942
|
+
return `${COLORS.brightGreen}${reminder}${COLORS.reset}`;
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
if (config.eyeReminderEnabled) {
|
|
1947
|
+
enabledReminders.push(() => {
|
|
1948
|
+
const reminder = EYE_REMINDERS[Math.floor(Math.random() * EYE_REMINDERS.length)];
|
|
1949
|
+
return `${COLORS.brightMagenta}${reminder}${COLORS.reset}`;
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
if (enabledReminders.length === 0) return null;
|
|
1954
|
+
|
|
1955
|
+
// ~5% chance to show any reminder (reduced from 30% to be less intrusive)
|
|
1956
|
+
if (Math.random() >= 0.05) return null;
|
|
1957
|
+
|
|
1958
|
+
// Pick a random reminder type from enabled ones
|
|
1959
|
+
const randomPicker = enabledReminders[Math.floor(Math.random() * enabledReminders.length)];
|
|
1960
|
+
return randomPicker();
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
interface ClaudeCodeStatus {
|
|
1964
|
+
model?: { display_name?: string };
|
|
1965
|
+
cost?: {
|
|
1966
|
+
total_cost_usd?: number;
|
|
1967
|
+
total_lines_added?: number;
|
|
1968
|
+
total_lines_removed?: number;
|
|
1969
|
+
};
|
|
1970
|
+
cwd?: string;
|
|
1971
|
+
workspace?: {
|
|
1972
|
+
project_dir?: string;
|
|
1973
|
+
};
|
|
1974
|
+
context_window?: {
|
|
1975
|
+
context_window_size?: number;
|
|
1976
|
+
current_usage?: {
|
|
1977
|
+
input_tokens?: number;
|
|
1978
|
+
output_tokens?: number;
|
|
1979
|
+
cache_creation_input_tokens?: number;
|
|
1980
|
+
cache_read_input_tokens?: number;
|
|
1981
|
+
};
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function getGitBranch(cwd?: string): string | null {
|
|
1986
|
+
try {
|
|
1987
|
+
const { execSync } = require("child_process");
|
|
1988
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1989
|
+
cwd: cwd || process.cwd(),
|
|
1990
|
+
encoding: "utf-8",
|
|
1991
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1992
|
+
}).trim();
|
|
1993
|
+
|
|
1994
|
+
// Check if there are uncommitted changes
|
|
1995
|
+
let isDirty = false;
|
|
1996
|
+
try {
|
|
1997
|
+
const status = execSync("git status --porcelain", {
|
|
1998
|
+
cwd: cwd || process.cwd(),
|
|
1999
|
+
encoding: "utf-8",
|
|
2000
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2001
|
+
}).trim();
|
|
2002
|
+
isDirty = status.length > 0;
|
|
2003
|
+
} catch {}
|
|
2004
|
+
|
|
2005
|
+
return isDirty ? `${branch}*` : branch;
|
|
2006
|
+
} catch {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function getProjectName(projectDir?: string): string | null {
|
|
2012
|
+
if (!projectDir) return null;
|
|
2013
|
+
// Extract the last part of the path as project name
|
|
2014
|
+
const parts = projectDir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
2015
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
async function readStdinJson(): Promise<ClaudeCodeStatus | null> {
|
|
2019
|
+
try {
|
|
2020
|
+
const chunks: Uint8Array[] = [];
|
|
2021
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
2022
|
+
chunks.push(chunk);
|
|
2023
|
+
}
|
|
2024
|
+
if (chunks.length === 0) return null;
|
|
2025
|
+
const text = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2026
|
+
if (!text) return null;
|
|
2027
|
+
return JSON.parse(text);
|
|
2028
|
+
} catch {
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function formatSessionInfo(status: ClaudeCodeStatus): string {
|
|
2034
|
+
const parts: string[] = [];
|
|
2035
|
+
|
|
2036
|
+
// Project name
|
|
2037
|
+
const projectName = getProjectName(status.workspace?.project_dir);
|
|
2038
|
+
if (projectName) {
|
|
2039
|
+
parts.push(`${COLORS.brightBlue}${projectName}${COLORS.reset}`);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// Git branch
|
|
2043
|
+
const gitBranch = getGitBranch(status.cwd || status.workspace?.project_dir);
|
|
2044
|
+
if (gitBranch) {
|
|
2045
|
+
parts.push(`${COLORS.magenta}${gitBranch}${COLORS.reset}`);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Model name
|
|
2049
|
+
if (status.model?.display_name) {
|
|
2050
|
+
parts.push(`${COLORS.brightYellow}${status.model.display_name}${COLORS.reset}`);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Cost
|
|
2054
|
+
if (status.cost?.total_cost_usd !== undefined) {
|
|
2055
|
+
const cost = status.cost.total_cost_usd;
|
|
2056
|
+
const costStr = cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
|
|
2057
|
+
parts.push(`${COLORS.green}${costStr}${COLORS.reset}`);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Lines changed
|
|
2061
|
+
const linesAdded = status.cost?.total_lines_added || 0;
|
|
2062
|
+
const linesRemoved = status.cost?.total_lines_removed || 0;
|
|
2063
|
+
if (linesAdded > 0 || linesRemoved > 0) {
|
|
2064
|
+
const linesStr = `${COLORS.green}+${linesAdded}${COLORS.reset} ${COLORS.red}-${linesRemoved}${COLORS.reset}`;
|
|
2065
|
+
parts.push(linesStr);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// Context usage (using current_usage for accurate context window state)
|
|
2069
|
+
if (status.context_window?.current_usage && status.context_window?.context_window_size) {
|
|
2070
|
+
const usage = status.context_window.current_usage;
|
|
2071
|
+
// Sum all token types for total context usage
|
|
2072
|
+
const totalUsed =
|
|
2073
|
+
(usage.input_tokens || 0) +
|
|
2074
|
+
(usage.output_tokens || 0) +
|
|
2075
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
2076
|
+
(usage.cache_read_input_tokens || 0);
|
|
2077
|
+
const windowSize = status.context_window.context_window_size;
|
|
2078
|
+
const percentage = Math.round((totalUsed / windowSize) * 100);
|
|
2079
|
+
const usedK = (totalUsed / 1000).toFixed(1);
|
|
2080
|
+
const windowK = Math.round(windowSize / 1000);
|
|
2081
|
+
|
|
2082
|
+
// Color based on usage: green < 50%, yellow 50-80%, red > 80%
|
|
2083
|
+
let color = COLORS.green;
|
|
2084
|
+
if (percentage >= 80) color = COLORS.red;
|
|
2085
|
+
else if (percentage >= 50) color = COLORS.yellow;
|
|
2086
|
+
|
|
2087
|
+
parts.push(`${color}${usedK}k/${windowK}k (${percentage}%)${COLORS.reset}`);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
return parts.join(" | ");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
async function outputStatusline() {
|
|
2094
|
+
try {
|
|
2095
|
+
const config = loadConfig();
|
|
2096
|
+
|
|
2097
|
+
// Read and parse stdin from Claude Code
|
|
2098
|
+
const status = await readStdinJson();
|
|
2099
|
+
const parts: string[] = [];
|
|
2100
|
+
|
|
2101
|
+
// Add session info from Claude Code
|
|
2102
|
+
if (status) {
|
|
2103
|
+
const sessionInfo = formatSessionInfo(status);
|
|
2104
|
+
if (sessionInfo) {
|
|
2105
|
+
parts.push(sessionInfo);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Fetch and display usage limits
|
|
2110
|
+
if (config.showUsageLimits) {
|
|
2111
|
+
const usageLimits = await getUsageLimits(config);
|
|
2112
|
+
if (usageLimits) {
|
|
2113
|
+
const limitsStr = formatUsageLimits(usageLimits, config);
|
|
2114
|
+
if (limitsStr) {
|
|
2115
|
+
parts.push(limitsStr);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// Add system stats if enabled
|
|
2121
|
+
if (config.showCpuUsage) {
|
|
2122
|
+
const cpu = getCpuUsage();
|
|
2123
|
+
if (cpu) parts.push(cpu);
|
|
2124
|
+
}
|
|
2125
|
+
if (config.showMemoryUsage) {
|
|
2126
|
+
const mem = getMemoryUsage();
|
|
2127
|
+
if (mem) parts.push(mem);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Check for health reminder (water, stretch, eye break)
|
|
2131
|
+
const reminder = getRandomReminder(config);
|
|
2132
|
+
if (reminder) {
|
|
2133
|
+
parts.push(reminder);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Get calendar events and tasks in parallel
|
|
2137
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
2138
|
+
const hasAccounts = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
2139
|
+
|
|
2140
|
+
if (hasAccounts) {
|
|
2141
|
+
const [events, tasks] = await Promise.all([
|
|
2142
|
+
getUpcomingEvents(config),
|
|
2143
|
+
getZohoTasks(config),
|
|
2144
|
+
]);
|
|
2145
|
+
|
|
2146
|
+
const event = getCurrentOrNextEvent(events);
|
|
2147
|
+
|
|
2148
|
+
// Check for meeting warning (within 5 minutes)
|
|
2149
|
+
const meetingWarning = getMeetingWarning(event);
|
|
2150
|
+
if (meetingWarning) {
|
|
2151
|
+
parts.push(meetingWarning);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Add tasks if available
|
|
2155
|
+
const tasksStr = formatTasks(tasks);
|
|
2156
|
+
if (tasksStr) {
|
|
2157
|
+
parts.push(tasksStr);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
if (event) {
|
|
2161
|
+
parts.push(formatEvent(event, config));
|
|
2162
|
+
} else if (parts.length === 0) {
|
|
2163
|
+
parts.push("No upcoming events");
|
|
2164
|
+
}
|
|
2165
|
+
} else if (parts.length === 0) {
|
|
2166
|
+
parts.push("No accounts configured");
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
console.log(parts.join(" | "));
|
|
2170
|
+
} catch {
|
|
2171
|
+
console.log("Calendar unavailable");
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// ============================================================================
|
|
2176
|
+
// Main
|
|
2177
|
+
// ============================================================================
|
|
2178
|
+
|
|
2179
|
+
async function main() {
|
|
2180
|
+
const args = process.argv.slice(2);
|
|
2181
|
+
const command = args[0];
|
|
2182
|
+
|
|
2183
|
+
if (!command) {
|
|
2184
|
+
// Default: output statusline
|
|
2185
|
+
await outputStatusline();
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
switch (command) {
|
|
2190
|
+
case "version":
|
|
2191
|
+
case "--version":
|
|
2192
|
+
case "-v":
|
|
2193
|
+
console.log(`@naarang/glancebar v${VERSION}`);
|
|
2194
|
+
break;
|
|
2195
|
+
|
|
2196
|
+
case "help":
|
|
2197
|
+
case "--help":
|
|
2198
|
+
case "-h":
|
|
2199
|
+
printHelp();
|
|
2200
|
+
break;
|
|
2201
|
+
|
|
2202
|
+
case "setup":
|
|
2203
|
+
printSetup();
|
|
2204
|
+
break;
|
|
2205
|
+
|
|
2206
|
+
case "auth":
|
|
2207
|
+
await handleAuth(args.slice(1));
|
|
2208
|
+
break;
|
|
2209
|
+
|
|
2210
|
+
case "config":
|
|
2211
|
+
handleConfig(args.slice(1));
|
|
2212
|
+
break;
|
|
2213
|
+
|
|
2214
|
+
default:
|
|
2215
|
+
console.error(`Unknown command: ${command}`);
|
|
2216
|
+
console.error("Run 'glancebar --help' for usage.");
|
|
2217
|
+
process.exit(1);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
main().catch((err) => {
|
|
2222
|
+
console.error("Error:", err.message);
|
|
2223
|
+
process.exit(1);
|
|
2224
|
+
});
|