@naarang/glancebar 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +76 -18
  2. package/package.json +1 -1
  3. package/src/cli.ts +574 -78
package/README.md CHANGED
@@ -9,7 +9,7 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
9
9
 
10
10
  - **Session info** - Project name, git branch, model, cost, lines changed, and context usage
11
11
  - **System stats** - CPU and memory usage (optional)
12
- - **Calendar events** - Upcoming events from multiple Google accounts
12
+ - **Calendar events** - Upcoming events from Google Calendar and Zoho Calendar
13
13
  - **Meeting warnings** - Red alert when a meeting is 5 minutes away
14
14
  - **Health reminders** - Water, stretch, and eye break reminders
15
15
  - **Color-coded** - Everything has distinct colors for quick scanning
@@ -19,7 +19,8 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
19
19
  ## Requirements
20
20
 
21
21
  - [Bun](https://bun.sh/) >= 1.0.0
22
- - Google Cloud project with Calendar API enabled
22
+ - Google Cloud project with Calendar API enabled (for Google Calendar)
23
+ - Zoho API Console credentials (for Zoho Calendar)
23
24
 
24
25
  ## Installation
25
26
 
@@ -49,8 +50,9 @@ npm install -g @naarang/glancebar
49
50
  # 1. Run setup guide
50
51
  glancebar setup
51
52
 
52
- # 2. Add your Google account (after setting up credentials)
53
+ # 2. Add your account (after setting up credentials)
53
54
  glancebar auth --add your-email@gmail.com
55
+ # Select Google (1) or Zoho (2) when prompted
54
56
 
55
57
  # 3. Test it
56
58
  glancebar
@@ -58,7 +60,9 @@ glancebar
58
60
 
59
61
  ## Setup
60
62
 
61
- ### 1. Create Google Cloud Project
63
+ ### Google Calendar Setup
64
+
65
+ #### 1. Create Google Cloud Project
62
66
 
63
67
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
64
68
  2. Create a new project or select an existing one
@@ -66,30 +70,82 @@ glancebar
66
70
  - Go to "APIs & Services" > "Library"
67
71
  - Search for "Google Calendar API" and enable it
68
72
 
69
- ### 2. Create OAuth Credentials
73
+ #### 2. Create OAuth Credentials
70
74
 
71
75
  1. Go to "APIs & Services" > "Credentials"
72
76
  2. Click "Create Credentials" > "OAuth client ID"
73
77
  3. Select "Desktop app" as application type
74
- 4. Download the JSON file
75
- 5. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
78
+ 4. Give it a name and click "Create"
79
+ 5. Download the JSON file
80
+ 6. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
76
81
 
77
- ### 3. Add Redirect URI
82
+ #### 3. Add Redirect URI
78
83
 
79
- In Google Cloud Console, edit your OAuth client and add:
84
+ Desktop app credentials don't show a redirect URI field in the console UI. You need to manually edit the downloaded `credentials.json` file:
80
85
 
81
- ```
82
- http://localhost:3000/callback
86
+ 1. Open `~/.glancebar/credentials.json` in a text editor
87
+ 2. Find the `"redirect_uris"` array and ensure it contains:
88
+ ```json
89
+ "redirect_uris": ["http://localhost:3000/callback"]
90
+ ```
91
+ 3. Save the file
92
+
93
+ Alternatively, you can add it via Google Cloud Console:
94
+ 1. Go to "APIs & Services" > "Credentials"
95
+ 2. Click on your OAuth client to edit it
96
+ 3. Under "Authorized redirect URIs", click "Add URI"
97
+ 4. Enter `http://localhost:3000/callback`
98
+ 5. Save and re-download the JSON file
99
+
100
+ ### Zoho Calendar Setup
101
+
102
+ #### 1. Register Application
103
+
104
+ 1. Go to [Zoho API Console](https://api-console.zoho.com/)
105
+ 2. Click "Add Client" > "Server-based Applications"
106
+ 3. Enter application details
107
+
108
+ #### 2. Configure Client
109
+
110
+ 1. Set Authorized Redirect URI: `http://localhost:3000/callback`
111
+ 2. Note your Client ID and Client Secret
112
+
113
+ #### 3. Save Credentials
114
+
115
+ Create `~/.glancebar/zoho_credentials.json`:
116
+
117
+ ```json
118
+ {
119
+ "client_id": "YOUR_CLIENT_ID",
120
+ "client_secret": "YOUR_CLIENT_SECRET"
121
+ }
83
122
  ```
84
123
 
85
- ### 4. Add Accounts
124
+ ### Add Accounts
86
125
 
87
126
  ```bash
127
+ # Add Google Calendar account
88
128
  glancebar auth --add your-email@gmail.com
89
- glancebar auth --add work@company.com
129
+ # Select "1" for Google Calendar
130
+
131
+ # Add Zoho Calendar account
132
+ glancebar auth --add your-email@zoho.com
133
+ # Select "2" for Zoho Calendar
134
+ # Then select your datacenter region (1-7)
90
135
  ```
91
136
 
92
- ### 5. Configure Claude Code
137
+ **Zoho Datacenters:**
138
+ | Choice | Region | Domain |
139
+ |--------|--------|--------|
140
+ | 1 | United States | zoho.com |
141
+ | 2 | Europe | zoho.eu |
142
+ | 3 | India | zoho.in |
143
+ | 4 | Australia | zoho.com.au |
144
+ | 5 | China | zoho.com.cn |
145
+ | 6 | Japan | zoho.jp |
146
+ | 7 | Canada | zohocloud.ca |
147
+
148
+ ### Configure Claude Code
93
149
 
94
150
  Update `~/.claude/settings.json`:
95
151
 
@@ -202,10 +258,12 @@ All configuration is stored in `~/.glancebar/`:
202
258
 
203
259
  ```
204
260
  ~/.glancebar/
205
- ├── config.json # User settings
206
- ├── credentials.json # Google OAuth credentials (you provide this)
207
- └── tokens/ # OAuth tokens per account
208
- └── <email>.json
261
+ ├── config.json # User settings
262
+ ├── credentials.json # Google OAuth credentials (you provide)
263
+ ├── zoho_credentials.json # Zoho OAuth credentials (you provide)
264
+ └── tokens/ # OAuth tokens per account
265
+ ├── google_<email>.json # Google tokens
266
+ └── zoho_<email>.json # Zoho tokens
209
267
  ```
210
268
 
211
269
  ### Default Settings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naarang/glancebar",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A customizable statusline for Claude Code - display calendar events, tasks, and more at a glance",
5
5
  "author": "Vishal Dubey",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -10,7 +10,9 @@ import { createInterface } from "readline";
10
10
  // ============================================================================
11
11
 
12
12
  interface Config {
13
- accounts: string[];
13
+ accounts: string[]; // Legacy - for backwards compatibility
14
+ gmailAccounts: string[]; // Google accounts
15
+ zohoAccounts: ZohoAccount[]; // Zoho accounts
14
16
  lookaheadHours: number;
15
17
  showCalendarName: boolean;
16
18
  countdownThresholdMinutes: number;
@@ -22,6 +24,11 @@ interface Config {
22
24
  showMemoryUsage: boolean;
23
25
  }
24
26
 
27
+ interface ZohoAccount {
28
+ email: string;
29
+ datacenter: string; // com, eu, in, com.au, com.cn, jp, zohocloud.ca
30
+ }
31
+
25
32
  const COLORS: Record<string, string> = {
26
33
  reset: "\x1b[0m",
27
34
  red: "\x1b[31m",
@@ -45,7 +52,9 @@ const COLORS: Record<string, string> = {
45
52
  const ACCOUNT_COLORS = ["cyan", "magenta", "brightGreen", "orange", "brightBlue", "pink", "yellow", "purple"];
46
53
 
47
54
  const DEFAULT_CONFIG: Config = {
48
- accounts: [],
55
+ accounts: [], // Legacy
56
+ gmailAccounts: [],
57
+ zohoAccounts: [],
49
58
  lookaheadHours: 8,
50
59
  showCalendarName: true,
51
60
  countdownThresholdMinutes: 60,
@@ -117,7 +126,14 @@ function loadConfig(): Config {
117
126
  try {
118
127
  const content = readFileSync(configPath, "utf-8");
119
128
  const userConfig = JSON.parse(content);
120
- return { ...DEFAULT_CONFIG, ...userConfig };
129
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
130
+
131
+ // Migrate legacy accounts to gmailAccounts
132
+ if (config.accounts && config.accounts.length > 0 && (!config.gmailAccounts || config.gmailAccounts.length === 0)) {
133
+ config.gmailAccounts = [...config.accounts];
134
+ }
135
+
136
+ return config;
121
137
  } catch {
122
138
  return { ...DEFAULT_CONFIG };
123
139
  }
@@ -129,23 +145,54 @@ function saveConfig(config: Config): void {
129
145
  }
130
146
 
131
147
  // ============================================================================
132
- // OAuth Authentication
148
+ // Google OAuth Authentication
133
149
  // ============================================================================
134
150
 
135
- const SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
151
+ const GOOGLE_SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
136
152
  const REDIRECT_URI = "http://localhost:3000/callback";
137
153
 
138
- interface Credentials {
154
+ interface GoogleCredentials {
139
155
  installed?: { client_id: string; client_secret: string };
140
156
  web?: { client_id: string; client_secret: string };
141
157
  }
142
158
 
143
- function getCredentialsPath(): string {
159
+ // ============================================================================
160
+ // Zoho OAuth Authentication
161
+ // ============================================================================
162
+
163
+ const ZOHO_SCOPES = ["ZohoCalendar.calendar.READ", "ZohoCalendar.event.READ"];
164
+ const ZOHO_REDIRECT_URI = "http://localhost:3000/callback";
165
+
166
+ // Zoho datacenter mappings
167
+ const ZOHO_DATACENTERS: Record<string, { accounts: string; api: string }> = {
168
+ "com": { accounts: "https://accounts.zoho.com", api: "https://calendar.zoho.com" },
169
+ "eu": { accounts: "https://accounts.zoho.eu", api: "https://calendar.zoho.eu" },
170
+ "in": { accounts: "https://accounts.zoho.in", api: "https://calendar.zoho.in" },
171
+ "com.au": { accounts: "https://accounts.zoho.com.au", api: "https://calendar.zoho.com.au" },
172
+ "com.cn": { accounts: "https://accounts.zoho.com.cn", api: "https://calendar.zoho.com.cn" },
173
+ "jp": { accounts: "https://accounts.zoho.jp", api: "https://calendar.zoho.jp" },
174
+ "zohocloud.ca": { accounts: "https://accounts.zohocloud.ca", api: "https://calendar.zohocloud.ca" },
175
+ };
176
+
177
+ interface ZohoCredentials {
178
+ client_id: string;
179
+ client_secret: string;
180
+ }
181
+
182
+ interface ZohoToken {
183
+ access_token: string;
184
+ refresh_token: string;
185
+ expires_at: number;
186
+ api_domain?: string;
187
+ }
188
+
189
+ // Google credentials
190
+ function getGoogleCredentialsPath(): string {
144
191
  return join(getConfigDir(), "credentials.json");
145
192
  }
146
193
 
147
- function loadCredentials(): Credentials {
148
- const credPath = getCredentialsPath();
194
+ function loadGoogleCredentials(): GoogleCredentials {
195
+ const credPath = getGoogleCredentialsPath();
149
196
  if (!existsSync(credPath)) {
150
197
  throw new Error(
151
198
  `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.`
@@ -154,23 +201,55 @@ function loadCredentials(): Credentials {
154
201
  return JSON.parse(readFileSync(credPath, "utf-8"));
155
202
  }
156
203
 
157
- function getTokenPath(account: string): string {
204
+ function getGoogleTokenPath(account: string): string {
205
+ const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
206
+ return join(getTokensDir(), `google_${safeAccount}.json`);
207
+ }
208
+
209
+ // Legacy token path (for migration)
210
+ function getLegacyTokenPath(account: string): string {
158
211
  const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
159
212
  return join(getTokensDir(), `${safeAccount}.json`);
160
213
  }
161
214
 
162
- function createOAuth2Client(credentials: Credentials) {
215
+ // Zoho credentials
216
+ function getZohoCredentialsPath(): string {
217
+ return join(getConfigDir(), "zoho_credentials.json");
218
+ }
219
+
220
+ function loadZohoCredentials(): ZohoCredentials {
221
+ const credPath = getZohoCredentialsPath();
222
+ if (!existsSync(credPath)) {
223
+ throw new Error(
224
+ `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.`
225
+ );
226
+ }
227
+ return JSON.parse(readFileSync(credPath, "utf-8"));
228
+ }
229
+
230
+ function getZohoTokenPath(account: string): string {
231
+ const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
232
+ return join(getTokensDir(), `zoho_${safeAccount}.json`);
233
+ }
234
+
235
+ function createGoogleOAuth2Client(credentials: GoogleCredentials) {
163
236
  const { client_id, client_secret } = credentials.installed || credentials.web!;
164
237
  return new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
165
238
  }
166
239
 
167
- function getAuthenticatedClient(account: string) {
168
- const credentials = loadCredentials();
169
- const oauth2Client = createOAuth2Client(credentials);
170
- const tokenPath = getTokenPath(account);
240
+ function getGoogleAuthenticatedClient(account: string) {
241
+ const credentials = loadGoogleCredentials();
242
+ const oauth2Client = createGoogleOAuth2Client(credentials);
171
243
 
244
+ // Try new path first, then legacy path
245
+ let tokenPath = getGoogleTokenPath(account);
172
246
  if (!existsSync(tokenPath)) {
173
- return null;
247
+ const legacyPath = getLegacyTokenPath(account);
248
+ if (existsSync(legacyPath)) {
249
+ tokenPath = legacyPath;
250
+ } else {
251
+ return null;
252
+ }
174
253
  }
175
254
 
176
255
  const token = JSON.parse(readFileSync(tokenPath, "utf-8"));
@@ -185,13 +264,13 @@ function getAuthenticatedClient(account: string) {
185
264
  return oauth2Client;
186
265
  }
187
266
 
188
- async function authenticateAccount(account: string): Promise<void> {
189
- const credentials = loadCredentials();
190
- const oauth2Client = createOAuth2Client(credentials);
267
+ async function authenticateGoogleAccount(account: string): Promise<void> {
268
+ const credentials = loadGoogleCredentials();
269
+ const oauth2Client = createGoogleOAuth2Client(credentials);
191
270
 
192
271
  const authUrl = oauth2Client.generateAuthUrl({
193
272
  access_type: "offline",
194
- scope: SCOPES,
273
+ scope: GOOGLE_SCOPES,
195
274
  prompt: "consent",
196
275
  login_hint: account,
197
276
  });
@@ -211,11 +290,132 @@ async function authenticateAccount(account: string): Promise<void> {
211
290
  mkdirSync(tokensDir, { recursive: true });
212
291
  }
213
292
 
214
- const tokenPath = getTokenPath(account);
293
+ const tokenPath = getGoogleTokenPath(account);
215
294
  writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
216
295
  console.log(`Token saved for ${account}`);
217
296
  }
218
297
 
298
+ // ============================================================================
299
+ // Zoho OAuth Flow
300
+ // ============================================================================
301
+
302
+ async function authenticateZohoAccount(account: ZohoAccount): Promise<void> {
303
+ const credentials = loadZohoCredentials();
304
+ const dc = ZOHO_DATACENTERS[account.datacenter];
305
+
306
+ if (!dc) {
307
+ throw new Error(`Invalid datacenter: ${account.datacenter}. Valid options: ${Object.keys(ZOHO_DATACENTERS).join(", ")}`);
308
+ }
309
+
310
+ const params = new URLSearchParams({
311
+ response_type: "code",
312
+ client_id: credentials.client_id,
313
+ scope: ZOHO_SCOPES.join(","),
314
+ redirect_uri: ZOHO_REDIRECT_URI,
315
+ access_type: "offline",
316
+ prompt: "consent",
317
+ });
318
+
319
+ const authUrl = `${dc.accounts}/oauth/v2/auth?${params.toString()}`;
320
+
321
+ console.log(`\nAuthenticating Zoho: ${account.email}`);
322
+ console.log(`Datacenter: ${account.datacenter}`);
323
+ console.log(`Opening browser...`);
324
+
325
+ const code = await startServerAndGetCode(authUrl);
326
+
327
+ console.log(`Exchanging code for tokens...`);
328
+
329
+ // Exchange code for tokens
330
+ const tokenParams = new URLSearchParams({
331
+ grant_type: "authorization_code",
332
+ client_id: credentials.client_id,
333
+ client_secret: credentials.client_secret,
334
+ redirect_uri: ZOHO_REDIRECT_URI,
335
+ code: code,
336
+ });
337
+
338
+ const tokenResponse = await fetch(`${dc.accounts}/oauth/v2/token`, {
339
+ method: "POST",
340
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
341
+ body: tokenParams.toString(),
342
+ });
343
+
344
+ if (!tokenResponse.ok) {
345
+ const errorText = await tokenResponse.text();
346
+ throw new Error(`Failed to get Zoho tokens: ${errorText}`);
347
+ }
348
+
349
+ const tokenData = await tokenResponse.json();
350
+
351
+ const token: ZohoToken = {
352
+ access_token: tokenData.access_token,
353
+ refresh_token: tokenData.refresh_token,
354
+ expires_at: Date.now() + (tokenData.expires_in * 1000),
355
+ api_domain: tokenData.api_domain || dc.api,
356
+ };
357
+
358
+ const tokensDir = getTokensDir();
359
+ if (!existsSync(tokensDir)) {
360
+ mkdirSync(tokensDir, { recursive: true });
361
+ }
362
+
363
+ const tokenPath = getZohoTokenPath(account.email);
364
+ writeFileSync(tokenPath, JSON.stringify(token, null, 2));
365
+ console.log(`Token saved for ${account.email}`);
366
+ }
367
+
368
+ async function refreshZohoToken(account: ZohoAccount): Promise<ZohoToken | null> {
369
+ const tokenPath = getZohoTokenPath(account.email);
370
+ if (!existsSync(tokenPath)) return null;
371
+
372
+ const token: ZohoToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
373
+
374
+ // Check if token is still valid (with 5 minute buffer)
375
+ if (token.expires_at > Date.now() + 300000) {
376
+ return token;
377
+ }
378
+
379
+ // Refresh the token
380
+ try {
381
+ const credentials = loadZohoCredentials();
382
+ const dc = ZOHO_DATACENTERS[account.datacenter];
383
+
384
+ const params = new URLSearchParams({
385
+ grant_type: "refresh_token",
386
+ client_id: credentials.client_id,
387
+ client_secret: credentials.client_secret,
388
+ refresh_token: token.refresh_token,
389
+ });
390
+
391
+ const response = await fetch(`${dc.accounts}/oauth/v2/token`, {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
394
+ body: params.toString(),
395
+ });
396
+
397
+ if (!response.ok) return null;
398
+
399
+ const data = await response.json();
400
+ const updatedToken: ZohoToken = {
401
+ ...token,
402
+ access_token: data.access_token,
403
+ expires_at: Date.now() + (data.expires_in * 1000),
404
+ };
405
+
406
+ writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
407
+ return updatedToken;
408
+ } catch {
409
+ return null;
410
+ }
411
+ }
412
+
413
+ function getZohoAuthenticatedToken(account: ZohoAccount): ZohoToken | null {
414
+ const tokenPath = getZohoTokenPath(account.email);
415
+ if (!existsSync(tokenPath)) return null;
416
+ return JSON.parse(readFileSync(tokenPath, "utf-8"));
417
+ }
418
+
219
419
  function startServerAndGetCode(authUrl: string): Promise<string> {
220
420
  return new Promise((resolve, reject) => {
221
421
  let server: Server;
@@ -313,16 +513,24 @@ interface CalendarEvent {
313
513
  account: string;
314
514
  accountEmail: string;
315
515
  accountIndex: number;
516
+ provider: "google" | "zoho";
316
517
  }
317
518
 
318
- async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
319
- const allEvents: CalendarEvent[] = [];
320
- const now = new Date();
321
- const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
519
+ // Get all accounts combined for indexing
520
+ function getAllAccounts(config: Config): string[] {
521
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
522
+ const zohoEmails = config.zohoAccounts.map(z => z.email);
523
+ return [...gmailAccounts, ...zohoEmails];
524
+ }
322
525
 
323
- const eventPromises = config.accounts.map(async (account, accountIndex) => {
526
+ async function getGoogleEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
527
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
528
+ const allAccounts = getAllAccounts(config);
529
+
530
+ const eventPromises = gmailAccounts.map(async (account) => {
531
+ const accountIndex = allAccounts.indexOf(account);
324
532
  try {
325
- const auth = getAuthenticatedClient(account);
533
+ const auth = getGoogleAuthenticatedClient(account);
326
534
  if (!auth) return [];
327
535
 
328
536
  const calendar = google.calendar({ version: "v3", auth });
@@ -358,6 +566,7 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
358
566
  account: extractAccountName(account),
359
567
  accountEmail: account,
360
568
  accountIndex,
569
+ provider: "google" as const,
361
570
  };
362
571
  });
363
572
  } catch {
@@ -366,10 +575,131 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
366
575
  });
367
576
 
368
577
  const results = await Promise.all(eventPromises);
369
- for (const events of results) {
370
- allEvents.push(...events);
371
- }
578
+ return results.flat();
579
+ }
580
+
581
+ async function getZohoEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
582
+ if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
583
+
584
+ const allAccounts = getAllAccounts(config);
585
+ const gmailCount = (config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts).length;
586
+
587
+ const eventPromises = config.zohoAccounts.map(async (account, idx) => {
588
+ const accountIndex = gmailCount + idx;
589
+ try {
590
+ const token = await refreshZohoToken(account);
591
+ if (!token) return [];
592
+
593
+ const dc = ZOHO_DATACENTERS[account.datacenter];
594
+ // Always use the calendar-specific API domain, not the generic api_domain
595
+ const apiBase = dc.api;
596
+
597
+ // First get list of calendars
598
+ const calendarsResponse = await fetch(`${apiBase}/api/v1/calendars?category=own`, {
599
+ headers: {
600
+ Authorization: `Zoho-oauthtoken ${token.access_token}`,
601
+ },
602
+ });
603
+
604
+ if (!calendarsResponse.ok) return [];
605
+
606
+ const calendarsData = await calendarsResponse.json();
607
+ const calendars = calendarsData.calendars || [];
608
+
609
+ if (calendars.length === 0) return [];
610
+
611
+ // Use the first (primary) calendar
612
+ const primaryCalendar = calendars.find((c: any) => c.isdefault) || calendars[0];
613
+ const calendarUid = primaryCalendar.uid;
614
+
615
+ // Format dates for Zoho API (yyyyMMdd'T'HHmmss'Z')
616
+ const formatZohoDate = (date: Date): string => {
617
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
618
+ };
619
+
620
+ const range = JSON.stringify({
621
+ start: formatZohoDate(now),
622
+ end: formatZohoDate(timeMax),
623
+ });
624
+
625
+ const eventsResponse = await fetch(
626
+ `${apiBase}/api/v1/calendars/${encodeURIComponent(calendarUid)}/events?range=${encodeURIComponent(range)}`,
627
+ {
628
+ headers: {
629
+ Authorization: `Zoho-oauthtoken ${token.access_token}`,
630
+ },
631
+ }
632
+ );
633
+
634
+ if (!eventsResponse.ok) return [];
635
+
636
+ const eventsData = await eventsResponse.json();
637
+ const events = eventsData.events || [];
638
+
639
+ return events.map((event: any) => {
640
+ const isAllDay = event.isallday === true;
641
+ let start: Date, end: Date;
642
+
643
+ // Parse Zoho date format: "20260109T163000+0530" or "20260109T163000Z"
644
+ const parseZohoDate = (dateStr: string): Date => {
645
+ // Format: YYYYMMDDTHHmmss+HHMM or YYYYMMDDTHHmmssZ
646
+ const match = dateStr.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})([Z]|([+-])(\d{2})(\d{2}))?$/);
647
+ if (match) {
648
+ const [, year, month, day, hour, min, sec, tz, sign, tzHour, tzMin] = match;
649
+ if (tz === "Z") {
650
+ return new Date(Date.UTC(+year, +month - 1, +day, +hour, +min, +sec));
651
+ } else if (sign && tzHour && tzMin) {
652
+ const offsetMinutes = (+tzHour * 60 + +tzMin) * (sign === "+" ? -1 : 1);
653
+ const utc = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec);
654
+ return new Date(utc + offsetMinutes * 60000);
655
+ }
656
+ // No timezone, assume local
657
+ return new Date(+year, +month - 1, +day, +hour, +min, +sec);
658
+ }
659
+ // Fallback to standard parsing
660
+ return new Date(dateStr);
661
+ };
662
+
663
+ if (event.dateandtime) {
664
+ start = parseZohoDate(event.dateandtime.start);
665
+ end = parseZohoDate(event.dateandtime.end);
666
+ } else {
667
+ start = parseZohoDate(event.start);
668
+ end = parseZohoDate(event.end);
669
+ }
372
670
 
671
+ return {
672
+ id: event.uid || "",
673
+ title: event.title || "(No title)",
674
+ start,
675
+ end,
676
+ isAllDay,
677
+ account: extractAccountName(account.email),
678
+ accountEmail: account.email,
679
+ accountIndex,
680
+ provider: "zoho" as const,
681
+ };
682
+ });
683
+ } catch {
684
+ return [];
685
+ }
686
+ });
687
+
688
+ const results = await Promise.all(eventPromises);
689
+ return results.flat();
690
+ }
691
+
692
+ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
693
+ const now = new Date();
694
+ const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
695
+
696
+ // Fetch from both providers in parallel
697
+ const [googleEvents, zohoEvents] = await Promise.all([
698
+ getGoogleEvents(config, now, timeMax),
699
+ getZohoEvents(config, now, timeMax),
700
+ ]);
701
+
702
+ const allEvents = [...googleEvents, ...zohoEvents];
373
703
  allEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
374
704
  return allEvents;
375
705
  }
@@ -528,9 +858,9 @@ Display calendar events, tasks, and more at a glance.
528
858
  Usage:
529
859
  glancebar Output statusline (for Claude Code)
530
860
  glancebar auth Authenticate all configured accounts
531
- glancebar auth --add <email> Add and authenticate a new account
861
+ glancebar auth --add <email> Add and authenticate a new account (prompts for provider)
532
862
  glancebar auth --remove <email> Remove an account
533
- glancebar auth --list List configured accounts
863
+ glancebar auth --list List all configured accounts
534
864
  glancebar config Show current configuration
535
865
  glancebar config --lookahead <hours> Set lookahead hours (default: 8)
536
866
  glancebar config --countdown-threshold <mins> Set countdown threshold in minutes (default: 60)
@@ -545,7 +875,8 @@ Usage:
545
875
  glancebar setup Show setup instructions
546
876
 
547
877
  Examples:
548
- glancebar auth --add user@gmail.com
878
+ glancebar auth --add user@gmail.com # Will prompt for Google or Zoho
879
+ glancebar auth --add user@zoho.com # Will prompt for Google or Zoho
549
880
  glancebar config --lookahead 12
550
881
  glancebar config --stretch-reminder false
551
882
 
@@ -558,6 +889,9 @@ function printSetup() {
558
889
  Glancebar - Setup Instructions
559
890
  ==============================
560
891
 
892
+ GOOGLE CALENDAR SETUP
893
+ ---------------------
894
+
561
895
  Step 1: Create Google Cloud Project
562
896
  - Go to https://console.cloud.google.com/
563
897
  - Create a new project or select existing one
@@ -574,17 +908,41 @@ Step 3: Create OAuth Credentials
574
908
 
575
909
  Step 4: Save credentials
576
910
  - Rename downloaded file to "credentials.json"
577
- - Save it to: ${getCredentialsPath()}
911
+ - Save it to: ${getGoogleCredentialsPath()}
578
912
 
579
913
  Step 5: Add redirect URI
580
- - In Google Cloud Console, edit your OAuth client
581
- - Add redirect URI: http://localhost:3000/callback
914
+ - Edit credentials.json and ensure redirect_uris contains:
915
+ "redirect_uris": ["http://localhost:3000/callback"]
916
+
917
+ ZOHO CALENDAR SETUP
918
+ -------------------
919
+
920
+ Step 1: Register Application
921
+ - Go to https://api-console.zoho.com/
922
+ - Click "Add Client" > "Server-based Applications"
923
+
924
+ Step 2: Configure Client
925
+ - Set Authorized Redirect URI: http://localhost:3000/callback
926
+ - Note your Client ID and Client Secret
927
+
928
+ Step 3: Save credentials
929
+ - Create file: ${getZohoCredentialsPath()}
930
+ - Add content:
931
+ {
932
+ "client_id": "YOUR_CLIENT_ID",
933
+ "client_secret": "YOUR_CLIENT_SECRET"
934
+ }
935
+
936
+ ADDING ACCOUNTS
937
+ ---------------
582
938
 
583
- Step 6: Add your Google accounts
584
939
  glancebar auth --add your-email@gmail.com
585
- glancebar auth --add work@company.com
940
+ # Select "Google" or "Zoho" when prompted
941
+ # For Zoho, select your datacenter region
942
+
943
+ CONFIGURE CLAUDE CODE
944
+ ---------------------
586
945
 
587
- Step 7: Configure Claude Code statusline
588
946
  Update ~/.claude/settings.json:
589
947
  {
590
948
  "statusLine": {
@@ -599,19 +957,40 @@ For more info: https://github.com/vishal-android-freak/glancebar
599
957
  }
600
958
 
601
959
  async function handleAuth(args: string[]) {
960
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
961
+ const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
962
+
602
963
  // Handle --list
603
964
  if (args.includes("--list")) {
604
965
  const config = loadConfig();
605
- if (config.accounts.length === 0) {
966
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
967
+ const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
968
+
969
+ if (!hasAny) {
606
970
  console.log("No accounts configured.");
607
971
  } else {
608
- console.log("Configured accounts:");
609
- config.accounts.forEach((acc, i) => {
610
- const tokenPath = getTokenPath(acc);
611
- const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
612
- console.log(` ${i + 1}. ${acc} (${status})`);
613
- });
972
+ if (gmailAccounts.length > 0) {
973
+ console.log("\nGoogle Calendar accounts:");
974
+ gmailAccounts.forEach((acc, i) => {
975
+ let tokenPath = getGoogleTokenPath(acc);
976
+ if (!existsSync(tokenPath)) {
977
+ tokenPath = getLegacyTokenPath(acc);
978
+ }
979
+ const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
980
+ console.log(` ${i + 1}. ${acc} (${status})`);
981
+ });
982
+ }
983
+
984
+ if (config.zohoAccounts.length > 0) {
985
+ console.log("\nZoho Calendar accounts:");
986
+ config.zohoAccounts.forEach((acc, i) => {
987
+ const tokenPath = getZohoTokenPath(acc.email);
988
+ const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
989
+ console.log(` ${i + 1}. ${acc.email} [${acc.datacenter}] (${status})`);
990
+ });
991
+ }
614
992
  }
993
+ rl.close();
615
994
  return;
616
995
  }
617
996
 
@@ -621,25 +1000,89 @@ async function handleAuth(args: string[]) {
621
1000
  const email = args[addIndex + 1];
622
1001
  if (!email || email.startsWith("--")) {
623
1002
  console.error("Error: Please provide an email address after --add");
1003
+ rl.close();
624
1004
  process.exit(1);
625
1005
  }
626
1006
 
627
1007
  if (!email.includes("@")) {
628
1008
  console.error("Error: Invalid email address");
1009
+ rl.close();
629
1010
  process.exit(1);
630
1011
  }
631
1012
 
1013
+ // Prompt for provider
1014
+ console.log("\nSelect calendar provider:");
1015
+ console.log(" 1. Google Calendar");
1016
+ console.log(" 2. Zoho Calendar");
1017
+ const providerChoice = await prompt("\nEnter choice (1 or 2): ");
1018
+
632
1019
  const config = loadConfig();
633
- if (config.accounts.includes(email)) {
634
- console.log(`Account ${email} already exists. Re-authenticating...`);
1020
+
1021
+ if (providerChoice === "1") {
1022
+ // Google Calendar
1023
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1024
+ if (gmailAccounts.includes(email)) {
1025
+ console.log(`\nGoogle account ${email} already exists. Re-authenticating...`);
1026
+ } else {
1027
+ if (config.gmailAccounts.length === 0 && config.accounts.length > 0) {
1028
+ config.gmailAccounts = [...config.accounts];
1029
+ }
1030
+ config.gmailAccounts.push(email);
1031
+ saveConfig(config);
1032
+ console.log(`\nAdded ${email} to Google accounts.`);
1033
+ }
1034
+
1035
+ await authenticateGoogleAccount(email);
1036
+ console.log("\nDone!");
1037
+ } else if (providerChoice === "2") {
1038
+ // Zoho Calendar
1039
+ console.log("\nSelect Zoho datacenter:");
1040
+ console.log(" 1. com - United States");
1041
+ console.log(" 2. eu - Europe");
1042
+ console.log(" 3. in - India");
1043
+ console.log(" 4. com.au - Australia");
1044
+ console.log(" 5. com.cn - China");
1045
+ console.log(" 6. jp - Japan");
1046
+ console.log(" 7. zohocloud.ca - Canada");
1047
+
1048
+ const dcChoice = await prompt("\nEnter choice (1-7): ");
1049
+ const dcMap: Record<string, string> = {
1050
+ "1": "com",
1051
+ "2": "eu",
1052
+ "3": "in",
1053
+ "4": "com.au",
1054
+ "5": "com.cn",
1055
+ "6": "jp",
1056
+ "7": "zohocloud.ca",
1057
+ };
1058
+
1059
+ const datacenter = dcMap[dcChoice];
1060
+ if (!datacenter) {
1061
+ console.error("Error: Invalid datacenter choice");
1062
+ rl.close();
1063
+ process.exit(1);
1064
+ }
1065
+
1066
+ const existingZoho = config.zohoAccounts.find((z) => z.email === email);
1067
+ if (existingZoho) {
1068
+ console.log(`\nZoho account ${email} already exists. Re-authenticating...`);
1069
+ existingZoho.datacenter = datacenter;
1070
+ saveConfig(config);
1071
+ } else {
1072
+ config.zohoAccounts.push({ email, datacenter });
1073
+ saveConfig(config);
1074
+ console.log(`\nAdded ${email} to Zoho accounts.`);
1075
+ }
1076
+
1077
+ await authenticateZohoAccount({ email, datacenter });
1078
+ console.log("\nDone!");
635
1079
  } else {
636
- config.accounts.push(email);
637
- saveConfig(config);
638
- console.log(`Added ${email} to accounts.`);
1080
+ console.error("Error: Invalid choice. Please enter 1 or 2.");
1081
+ rl.close();
1082
+ process.exit(1);
639
1083
  }
640
1084
 
641
- await authenticateAccount(email);
642
- console.log("\nDone!");
1085
+ rl.close();
643
1086
  return;
644
1087
  }
645
1088
 
@@ -649,52 +1092,92 @@ async function handleAuth(args: string[]) {
649
1092
  const email = args[removeIndex + 1];
650
1093
  if (!email || email.startsWith("--")) {
651
1094
  console.error("Error: Please provide an email address after --remove");
1095
+ rl.close();
652
1096
  process.exit(1);
653
1097
  }
654
1098
 
655
1099
  const config = loadConfig();
656
- const idx = config.accounts.indexOf(email);
657
- if (idx === -1) {
658
- console.error(`Error: Account ${email} not found.`);
659
- process.exit(1);
1100
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1101
+
1102
+ // Check Google accounts
1103
+ const googleIdx = gmailAccounts.indexOf(email);
1104
+ if (googleIdx !== -1) {
1105
+ if (config.gmailAccounts.length > 0) {
1106
+ config.gmailAccounts.splice(googleIdx, 1);
1107
+ } else {
1108
+ config.accounts.splice(googleIdx, 1);
1109
+ }
1110
+ saveConfig(config);
1111
+
1112
+ // Remove token files
1113
+ const tokenPath = getGoogleTokenPath(email);
1114
+ const legacyPath = getLegacyTokenPath(email);
1115
+ if (existsSync(tokenPath)) unlinkSync(tokenPath);
1116
+ if (existsSync(legacyPath)) unlinkSync(legacyPath);
1117
+
1118
+ console.log(`Removed Google account ${email}.`);
1119
+ rl.close();
1120
+ return;
660
1121
  }
661
1122
 
662
- config.accounts.splice(idx, 1);
663
- saveConfig(config);
1123
+ // Check Zoho accounts
1124
+ const zohoIdx = config.zohoAccounts.findIndex((z) => z.email === email);
1125
+ if (zohoIdx !== -1) {
1126
+ config.zohoAccounts.splice(zohoIdx, 1);
1127
+ saveConfig(config);
664
1128
 
665
- const tokenPath = getTokenPath(email);
666
- if (existsSync(tokenPath)) {
667
- unlinkSync(tokenPath);
1129
+ const tokenPath = getZohoTokenPath(email);
1130
+ if (existsSync(tokenPath)) unlinkSync(tokenPath);
1131
+
1132
+ console.log(`Removed Zoho account ${email}.`);
1133
+ rl.close();
1134
+ return;
668
1135
  }
669
1136
 
670
- console.log(`Removed ${email} from accounts.`);
671
- return;
1137
+ console.error(`Error: Account ${email} not found.`);
1138
+ rl.close();
1139
+ process.exit(1);
672
1140
  }
673
1141
 
674
1142
  // Default: authenticate all accounts
675
1143
  const config = loadConfig();
1144
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1145
+ const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
676
1146
 
677
- if (config.accounts.length === 0) {
1147
+ if (!hasAny) {
678
1148
  console.log("No accounts configured.\n");
679
1149
  console.log("Add an account using:");
680
1150
  console.log(" glancebar auth --add your-email@gmail.com\n");
1151
+ rl.close();
681
1152
  return;
682
1153
  }
683
1154
 
684
- console.log("Glancebar - Google Calendar Authentication");
685
- console.log("==========================================\n");
1155
+ console.log("Glancebar - Calendar Authentication");
1156
+ console.log("====================================\n");
686
1157
 
687
- const rl = createInterface({ input: process.stdin, output: process.stdout });
688
- const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
1158
+ // Authenticate Google accounts
1159
+ for (const account of gmailAccounts) {
1160
+ let tokenPath = getGoogleTokenPath(account);
1161
+ if (!existsSync(tokenPath)) {
1162
+ tokenPath = getLegacyTokenPath(account);
1163
+ }
1164
+ if (existsSync(tokenPath)) {
1165
+ console.log(`Google: ${account} - Already authenticated`);
1166
+ const answer = await prompt(`Re-authenticate? (y/N): `);
1167
+ if (answer.toLowerCase() !== "y") continue;
1168
+ }
1169
+ await authenticateGoogleAccount(account);
1170
+ }
689
1171
 
690
- for (const account of config.accounts) {
691
- const tokenPath = getTokenPath(account);
1172
+ // Authenticate Zoho accounts
1173
+ for (const account of config.zohoAccounts) {
1174
+ const tokenPath = getZohoTokenPath(account.email);
692
1175
  if (existsSync(tokenPath)) {
693
- console.log(`${account}: Already authenticated`);
694
- const answer = await prompt(`Re-authenticate ${account}? (y/N): `);
1176
+ console.log(`Zoho: ${account.email} [${account.datacenter}] - Already authenticated`);
1177
+ const answer = await prompt(`Re-authenticate? (y/N): `);
695
1178
  if (answer.toLowerCase() !== "y") continue;
696
1179
  }
697
- await authenticateAccount(account);
1180
+ await authenticateZohoAccount(account);
698
1181
  }
699
1182
 
700
1183
  rl.close();
@@ -706,8 +1189,9 @@ function handleConfig(args: string[]) {
706
1189
 
707
1190
  // Handle --reset
708
1191
  if (args.includes("--reset")) {
709
- const accounts = config.accounts; // Preserve accounts
710
- saveConfig({ ...DEFAULT_CONFIG, accounts });
1192
+ // Preserve all account types
1193
+ const { accounts, gmailAccounts, zohoAccounts } = config;
1194
+ saveConfig({ ...DEFAULT_CONFIG, accounts, gmailAccounts, zohoAccounts });
711
1195
  console.log("Configuration reset to defaults (accounts preserved).");
712
1196
  return;
713
1197
  }
@@ -839,11 +1323,20 @@ function handleConfig(args: string[]) {
839
1323
  }
840
1324
 
841
1325
  // Show current config
1326
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1327
+ const googleStr = gmailAccounts.length > 0 ? gmailAccounts.join(", ") : "(none)";
1328
+ const zohoStr = config.zohoAccounts.length > 0
1329
+ ? config.zohoAccounts.map((z) => `${z.email} [${z.datacenter}]`).join(", ")
1330
+ : "(none)";
1331
+
842
1332
  console.log(`
843
1333
  Glancebar Configuration
844
1334
  =======================
845
1335
  Config directory: ${getConfigDir()}
846
- Accounts: ${config.accounts.length > 0 ? config.accounts.join(", ") : "(none)"}
1336
+
1337
+ Accounts:
1338
+ Google Calendar: ${googleStr}
1339
+ Zoho Calendar: ${zohoStr}
847
1340
 
848
1341
  Calendar Settings:
849
1342
  Lookahead hours: ${config.lookaheadHours}
@@ -1059,7 +1552,10 @@ async function outputStatusline() {
1059
1552
  }
1060
1553
 
1061
1554
  // Get calendar events
1062
- if (config.accounts.length > 0) {
1555
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1556
+ const hasAccounts = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
1557
+
1558
+ if (hasAccounts) {
1063
1559
  const events = await getUpcomingEvents(config);
1064
1560
  const event = getCurrentOrNextEvent(events);
1065
1561