@naarang/glancebar 1.0.5 → 1.0.7
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/README.md +93 -18
- package/package.json +1 -1
- package/src/cli.ts +744 -80
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ 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
|
|
12
|
+
- **Calendar events** - Upcoming events from Google Calendar and Zoho Calendar
|
|
13
|
+
- **Zoho Tasks** - Display top pending tasks from Zoho Mail
|
|
13
14
|
- **Meeting warnings** - Red alert when a meeting is 5 minutes away
|
|
14
15
|
- **Health reminders** - Water, stretch, and eye break reminders
|
|
15
16
|
- **Color-coded** - Everything has distinct colors for quick scanning
|
|
@@ -19,7 +20,8 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
|
|
|
19
20
|
## Requirements
|
|
20
21
|
|
|
21
22
|
- [Bun](https://bun.sh/) >= 1.0.0
|
|
22
|
-
- Google Cloud project with Calendar API enabled
|
|
23
|
+
- Google Cloud project with Calendar API enabled (for Google Calendar)
|
|
24
|
+
- Zoho API Console credentials (for Zoho Calendar)
|
|
23
25
|
|
|
24
26
|
## Installation
|
|
25
27
|
|
|
@@ -49,8 +51,9 @@ npm install -g @naarang/glancebar
|
|
|
49
51
|
# 1. Run setup guide
|
|
50
52
|
glancebar setup
|
|
51
53
|
|
|
52
|
-
# 2. Add your
|
|
54
|
+
# 2. Add your account (after setting up credentials)
|
|
53
55
|
glancebar auth --add your-email@gmail.com
|
|
56
|
+
# Select Google (1) or Zoho (2) when prompted
|
|
54
57
|
|
|
55
58
|
# 3. Test it
|
|
56
59
|
glancebar
|
|
@@ -58,7 +61,9 @@ glancebar
|
|
|
58
61
|
|
|
59
62
|
## Setup
|
|
60
63
|
|
|
61
|
-
###
|
|
64
|
+
### Google Calendar Setup
|
|
65
|
+
|
|
66
|
+
#### 1. Create Google Cloud Project
|
|
62
67
|
|
|
63
68
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
64
69
|
2. Create a new project or select an existing one
|
|
@@ -66,30 +71,82 @@ glancebar
|
|
|
66
71
|
- Go to "APIs & Services" > "Library"
|
|
67
72
|
- Search for "Google Calendar API" and enable it
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
#### 2. Create OAuth Credentials
|
|
70
75
|
|
|
71
76
|
1. Go to "APIs & Services" > "Credentials"
|
|
72
77
|
2. Click "Create Credentials" > "OAuth client ID"
|
|
73
78
|
3. Select "Desktop app" as application type
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
79
|
+
4. Give it a name and click "Create"
|
|
80
|
+
5. Download the JSON file
|
|
81
|
+
6. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
#### 3. Add Redirect URI
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
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
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
1. Open `~/.glancebar/credentials.json` in a text editor
|
|
88
|
+
2. Find the `"redirect_uris"` array and ensure it contains:
|
|
89
|
+
```json
|
|
90
|
+
"redirect_uris": ["http://localhost:3000/callback"]
|
|
91
|
+
```
|
|
92
|
+
3. Save the file
|
|
93
|
+
|
|
94
|
+
Alternatively, you can add it via Google Cloud Console:
|
|
95
|
+
1. Go to "APIs & Services" > "Credentials"
|
|
96
|
+
2. Click on your OAuth client to edit it
|
|
97
|
+
3. Under "Authorized redirect URIs", click "Add URI"
|
|
98
|
+
4. Enter `http://localhost:3000/callback`
|
|
99
|
+
5. Save and re-download the JSON file
|
|
100
|
+
|
|
101
|
+
### Zoho Calendar Setup
|
|
102
|
+
|
|
103
|
+
#### 1. Register Application
|
|
104
|
+
|
|
105
|
+
1. Go to [Zoho API Console](https://api-console.zoho.com/)
|
|
106
|
+
2. Click "Add Client" > "Server-based Applications"
|
|
107
|
+
3. Enter application details
|
|
108
|
+
|
|
109
|
+
#### 2. Configure Client
|
|
110
|
+
|
|
111
|
+
1. Set Authorized Redirect URI: `http://localhost:3000/callback`
|
|
112
|
+
2. Note your Client ID and Client Secret
|
|
113
|
+
|
|
114
|
+
#### 3. Save Credentials
|
|
115
|
+
|
|
116
|
+
Create `~/.glancebar/zoho_credentials.json`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"client_id": "YOUR_CLIENT_ID",
|
|
121
|
+
"client_secret": "YOUR_CLIENT_SECRET"
|
|
122
|
+
}
|
|
83
123
|
```
|
|
84
124
|
|
|
85
|
-
###
|
|
125
|
+
### Add Accounts
|
|
86
126
|
|
|
87
127
|
```bash
|
|
128
|
+
# Add Google Calendar account
|
|
88
129
|
glancebar auth --add your-email@gmail.com
|
|
89
|
-
|
|
130
|
+
# Select "1" for Google Calendar
|
|
131
|
+
|
|
132
|
+
# Add Zoho Calendar account
|
|
133
|
+
glancebar auth --add your-email@zoho.com
|
|
134
|
+
# Select "2" for Zoho Calendar
|
|
135
|
+
# Then select your datacenter region (1-7)
|
|
90
136
|
```
|
|
91
137
|
|
|
92
|
-
|
|
138
|
+
**Zoho Datacenters:**
|
|
139
|
+
| Choice | Region | Domain |
|
|
140
|
+
|--------|--------|--------|
|
|
141
|
+
| 1 | United States | zoho.com |
|
|
142
|
+
| 2 | Europe | zoho.eu |
|
|
143
|
+
| 3 | India | zoho.in |
|
|
144
|
+
| 4 | Australia | zoho.com.au |
|
|
145
|
+
| 5 | China | zoho.com.cn |
|
|
146
|
+
| 6 | Japan | zoho.jp |
|
|
147
|
+
| 7 | Canada | zohocloud.ca |
|
|
148
|
+
|
|
149
|
+
### Configure Claude Code
|
|
93
150
|
|
|
94
151
|
Update `~/.claude/settings.json`:
|
|
95
152
|
|
|
@@ -149,6 +206,10 @@ glancebar config --eye-reminder true
|
|
|
149
206
|
glancebar config --cpu-usage true
|
|
150
207
|
glancebar config --memory-usage true
|
|
151
208
|
|
|
209
|
+
# Enable/disable Zoho tasks display
|
|
210
|
+
glancebar config --zoho-tasks true
|
|
211
|
+
glancebar config --max-tasks 3
|
|
212
|
+
|
|
152
213
|
# Reset to defaults
|
|
153
214
|
glancebar config --reset
|
|
154
215
|
```
|
|
@@ -188,6 +249,16 @@ Context usage color changes based on percentage:
|
|
|
188
249
|
| Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
|
|
189
250
|
| No events | `No upcoming events` | |
|
|
190
251
|
|
|
252
|
+
### Zoho Tasks
|
|
253
|
+
|
|
254
|
+
| State | Color | Example |
|
|
255
|
+
|-------|-------|---------|
|
|
256
|
+
| Overdue task | Red | `Overdue task title` |
|
|
257
|
+
| High priority | Yellow | `High priority task` |
|
|
258
|
+
| Normal task | White | `Normal task title` |
|
|
259
|
+
|
|
260
|
+
Tasks are displayed as: `Tasks: Task 1, Task 2, Task 3`
|
|
261
|
+
|
|
191
262
|
### Health Reminders (~5% chance)
|
|
192
263
|
|
|
193
264
|
| Type | Color | Example |
|
|
@@ -202,10 +273,12 @@ All configuration is stored in `~/.glancebar/`:
|
|
|
202
273
|
|
|
203
274
|
```
|
|
204
275
|
~/.glancebar/
|
|
205
|
-
├── config.json
|
|
206
|
-
├── credentials.json
|
|
207
|
-
|
|
208
|
-
|
|
276
|
+
├── config.json # User settings
|
|
277
|
+
├── credentials.json # Google OAuth credentials (you provide)
|
|
278
|
+
├── zoho_credentials.json # Zoho OAuth credentials (you provide)
|
|
279
|
+
└── tokens/ # OAuth tokens per account
|
|
280
|
+
├── google_<email>.json # Google tokens
|
|
281
|
+
└── zoho_<email>.json # Zoho tokens
|
|
209
282
|
```
|
|
210
283
|
|
|
211
284
|
### Default Settings
|
|
@@ -221,6 +294,8 @@ All configuration is stored in `~/.glancebar/`:
|
|
|
221
294
|
| `eyeReminderEnabled` | true | Enable random eye break reminders (20-20-20 rule) |
|
|
222
295
|
| `showCpuUsage` | false | Show CPU usage percentage |
|
|
223
296
|
| `showMemoryUsage` | false | Show memory usage |
|
|
297
|
+
| `showZohoTasks` | true | Show pending Zoho tasks |
|
|
298
|
+
| `maxTasksToShow` | 3 | Maximum number of tasks to display |
|
|
224
299
|
|
|
225
300
|
## Building from Source
|
|
226
301
|
|
package/package.json
CHANGED
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;
|
|
@@ -20,6 +22,13 @@ interface Config {
|
|
|
20
22
|
eyeReminderEnabled: boolean;
|
|
21
23
|
showCpuUsage: boolean;
|
|
22
24
|
showMemoryUsage: boolean;
|
|
25
|
+
showZohoTasks: boolean;
|
|
26
|
+
maxTasksToShow: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ZohoAccount {
|
|
30
|
+
email: string;
|
|
31
|
+
datacenter: string; // com, eu, in, com.au, com.cn, jp, zohocloud.ca
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
const COLORS: Record<string, string> = {
|
|
@@ -45,7 +54,9 @@ const COLORS: Record<string, string> = {
|
|
|
45
54
|
const ACCOUNT_COLORS = ["cyan", "magenta", "brightGreen", "orange", "brightBlue", "pink", "yellow", "purple"];
|
|
46
55
|
|
|
47
56
|
const DEFAULT_CONFIG: Config = {
|
|
48
|
-
accounts: [],
|
|
57
|
+
accounts: [], // Legacy
|
|
58
|
+
gmailAccounts: [],
|
|
59
|
+
zohoAccounts: [],
|
|
49
60
|
lookaheadHours: 8,
|
|
50
61
|
showCalendarName: true,
|
|
51
62
|
countdownThresholdMinutes: 60,
|
|
@@ -55,6 +66,8 @@ const DEFAULT_CONFIG: Config = {
|
|
|
55
66
|
eyeReminderEnabled: true,
|
|
56
67
|
showCpuUsage: false,
|
|
57
68
|
showMemoryUsage: false,
|
|
69
|
+
showZohoTasks: true,
|
|
70
|
+
maxTasksToShow: 3,
|
|
58
71
|
};
|
|
59
72
|
|
|
60
73
|
const WATER_REMINDERS = [
|
|
@@ -117,7 +130,14 @@ function loadConfig(): Config {
|
|
|
117
130
|
try {
|
|
118
131
|
const content = readFileSync(configPath, "utf-8");
|
|
119
132
|
const userConfig = JSON.parse(content);
|
|
120
|
-
|
|
133
|
+
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
134
|
+
|
|
135
|
+
// Migrate legacy accounts to gmailAccounts
|
|
136
|
+
if (config.accounts && config.accounts.length > 0 && (!config.gmailAccounts || config.gmailAccounts.length === 0)) {
|
|
137
|
+
config.gmailAccounts = [...config.accounts];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return config;
|
|
121
141
|
} catch {
|
|
122
142
|
return { ...DEFAULT_CONFIG };
|
|
123
143
|
}
|
|
@@ -129,23 +149,58 @@ function saveConfig(config: Config): void {
|
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
// ============================================================================
|
|
132
|
-
// OAuth Authentication
|
|
152
|
+
// Google OAuth Authentication
|
|
133
153
|
// ============================================================================
|
|
134
154
|
|
|
135
|
-
const
|
|
155
|
+
const GOOGLE_SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
136
156
|
const REDIRECT_URI = "http://localhost:3000/callback";
|
|
137
157
|
|
|
138
|
-
interface
|
|
158
|
+
interface GoogleCredentials {
|
|
139
159
|
installed?: { client_id: string; client_secret: string };
|
|
140
160
|
web?: { client_id: string; client_secret: string };
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Zoho OAuth Authentication
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
const ZOHO_SCOPES = [
|
|
168
|
+
"ZohoCalendar.calendar.READ",
|
|
169
|
+
"ZohoCalendar.event.READ",
|
|
170
|
+
"ZohoMail.tasks.READ",
|
|
171
|
+
];
|
|
172
|
+
const ZOHO_REDIRECT_URI = "http://localhost:3000/callback";
|
|
173
|
+
|
|
174
|
+
// Zoho datacenter mappings
|
|
175
|
+
const ZOHO_DATACENTERS: Record<string, { accounts: string; calendar: string; mail: string }> = {
|
|
176
|
+
"com": { accounts: "https://accounts.zoho.com", calendar: "https://calendar.zoho.com", mail: "https://mail.zoho.com" },
|
|
177
|
+
"eu": { accounts: "https://accounts.zoho.eu", calendar: "https://calendar.zoho.eu", mail: "https://mail.zoho.eu" },
|
|
178
|
+
"in": { accounts: "https://accounts.zoho.in", calendar: "https://calendar.zoho.in", mail: "https://mail.zoho.in" },
|
|
179
|
+
"com.au": { accounts: "https://accounts.zoho.com.au", calendar: "https://calendar.zoho.com.au", mail: "https://mail.zoho.com.au" },
|
|
180
|
+
"com.cn": { accounts: "https://accounts.zoho.com.cn", calendar: "https://calendar.zoho.com.cn", mail: "https://mail.zoho.com.cn" },
|
|
181
|
+
"jp": { accounts: "https://accounts.zoho.jp", calendar: "https://calendar.zoho.jp", mail: "https://mail.zoho.jp" },
|
|
182
|
+
"zohocloud.ca": { accounts: "https://accounts.zohocloud.ca", calendar: "https://calendar.zohocloud.ca", mail: "https://mail.zohocloud.ca" },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
interface ZohoCredentials {
|
|
186
|
+
client_id: string;
|
|
187
|
+
client_secret: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface ZohoToken {
|
|
191
|
+
access_token: string;
|
|
192
|
+
refresh_token: string;
|
|
193
|
+
expires_at: number;
|
|
194
|
+
api_domain?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Google credentials
|
|
198
|
+
function getGoogleCredentialsPath(): string {
|
|
144
199
|
return join(getConfigDir(), "credentials.json");
|
|
145
200
|
}
|
|
146
201
|
|
|
147
|
-
function
|
|
148
|
-
const credPath =
|
|
202
|
+
function loadGoogleCredentials(): GoogleCredentials {
|
|
203
|
+
const credPath = getGoogleCredentialsPath();
|
|
149
204
|
if (!existsSync(credPath)) {
|
|
150
205
|
throw new Error(
|
|
151
206
|
`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 +209,55 @@ function loadCredentials(): Credentials {
|
|
|
154
209
|
return JSON.parse(readFileSync(credPath, "utf-8"));
|
|
155
210
|
}
|
|
156
211
|
|
|
157
|
-
function
|
|
212
|
+
function getGoogleTokenPath(account: string): string {
|
|
213
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
214
|
+
return join(getTokensDir(), `google_${safeAccount}.json`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Legacy token path (for migration)
|
|
218
|
+
function getLegacyTokenPath(account: string): string {
|
|
158
219
|
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
159
220
|
return join(getTokensDir(), `${safeAccount}.json`);
|
|
160
221
|
}
|
|
161
222
|
|
|
162
|
-
|
|
223
|
+
// Zoho credentials
|
|
224
|
+
function getZohoCredentialsPath(): string {
|
|
225
|
+
return join(getConfigDir(), "zoho_credentials.json");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function loadZohoCredentials(): ZohoCredentials {
|
|
229
|
+
const credPath = getZohoCredentialsPath();
|
|
230
|
+
if (!existsSync(credPath)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`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.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return JSON.parse(readFileSync(credPath, "utf-8"));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getZohoTokenPath(account: string): string {
|
|
239
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
240
|
+
return join(getTokensDir(), `zoho_${safeAccount}.json`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createGoogleOAuth2Client(credentials: GoogleCredentials) {
|
|
163
244
|
const { client_id, client_secret } = credentials.installed || credentials.web!;
|
|
164
245
|
return new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
|
|
165
246
|
}
|
|
166
247
|
|
|
167
|
-
function
|
|
168
|
-
const credentials =
|
|
169
|
-
const oauth2Client =
|
|
170
|
-
const tokenPath = getTokenPath(account);
|
|
248
|
+
function getGoogleAuthenticatedClient(account: string) {
|
|
249
|
+
const credentials = loadGoogleCredentials();
|
|
250
|
+
const oauth2Client = createGoogleOAuth2Client(credentials);
|
|
171
251
|
|
|
252
|
+
// Try new path first, then legacy path
|
|
253
|
+
let tokenPath = getGoogleTokenPath(account);
|
|
172
254
|
if (!existsSync(tokenPath)) {
|
|
173
|
-
|
|
255
|
+
const legacyPath = getLegacyTokenPath(account);
|
|
256
|
+
if (existsSync(legacyPath)) {
|
|
257
|
+
tokenPath = legacyPath;
|
|
258
|
+
} else {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
174
261
|
}
|
|
175
262
|
|
|
176
263
|
const token = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
@@ -185,13 +272,13 @@ function getAuthenticatedClient(account: string) {
|
|
|
185
272
|
return oauth2Client;
|
|
186
273
|
}
|
|
187
274
|
|
|
188
|
-
async function
|
|
189
|
-
const credentials =
|
|
190
|
-
const oauth2Client =
|
|
275
|
+
async function authenticateGoogleAccount(account: string): Promise<void> {
|
|
276
|
+
const credentials = loadGoogleCredentials();
|
|
277
|
+
const oauth2Client = createGoogleOAuth2Client(credentials);
|
|
191
278
|
|
|
192
279
|
const authUrl = oauth2Client.generateAuthUrl({
|
|
193
280
|
access_type: "offline",
|
|
194
|
-
scope:
|
|
281
|
+
scope: GOOGLE_SCOPES,
|
|
195
282
|
prompt: "consent",
|
|
196
283
|
login_hint: account,
|
|
197
284
|
});
|
|
@@ -211,11 +298,132 @@ async function authenticateAccount(account: string): Promise<void> {
|
|
|
211
298
|
mkdirSync(tokensDir, { recursive: true });
|
|
212
299
|
}
|
|
213
300
|
|
|
214
|
-
const tokenPath =
|
|
301
|
+
const tokenPath = getGoogleTokenPath(account);
|
|
215
302
|
writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
|
|
216
303
|
console.log(`Token saved for ${account}`);
|
|
217
304
|
}
|
|
218
305
|
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// Zoho OAuth Flow
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
async function authenticateZohoAccount(account: ZohoAccount): Promise<void> {
|
|
311
|
+
const credentials = loadZohoCredentials();
|
|
312
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
313
|
+
|
|
314
|
+
if (!dc) {
|
|
315
|
+
throw new Error(`Invalid datacenter: ${account.datacenter}. Valid options: ${Object.keys(ZOHO_DATACENTERS).join(", ")}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const params = new URLSearchParams({
|
|
319
|
+
response_type: "code",
|
|
320
|
+
client_id: credentials.client_id,
|
|
321
|
+
scope: ZOHO_SCOPES.join(","),
|
|
322
|
+
redirect_uri: ZOHO_REDIRECT_URI,
|
|
323
|
+
access_type: "offline",
|
|
324
|
+
prompt: "consent",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const authUrl = `${dc.accounts}/oauth/v2/auth?${params.toString()}`;
|
|
328
|
+
|
|
329
|
+
console.log(`\nAuthenticating Zoho: ${account.email}`);
|
|
330
|
+
console.log(`Datacenter: ${account.datacenter}`);
|
|
331
|
+
console.log(`Opening browser...`);
|
|
332
|
+
|
|
333
|
+
const code = await startServerAndGetCode(authUrl);
|
|
334
|
+
|
|
335
|
+
console.log(`Exchanging code for tokens...`);
|
|
336
|
+
|
|
337
|
+
// Exchange code for tokens
|
|
338
|
+
const tokenParams = new URLSearchParams({
|
|
339
|
+
grant_type: "authorization_code",
|
|
340
|
+
client_id: credentials.client_id,
|
|
341
|
+
client_secret: credentials.client_secret,
|
|
342
|
+
redirect_uri: ZOHO_REDIRECT_URI,
|
|
343
|
+
code: code,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const tokenResponse = await fetch(`${dc.accounts}/oauth/v2/token`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
349
|
+
body: tokenParams.toString(),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (!tokenResponse.ok) {
|
|
353
|
+
const errorText = await tokenResponse.text();
|
|
354
|
+
throw new Error(`Failed to get Zoho tokens: ${errorText}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const tokenData = await tokenResponse.json();
|
|
358
|
+
|
|
359
|
+
const token: ZohoToken = {
|
|
360
|
+
access_token: tokenData.access_token,
|
|
361
|
+
refresh_token: tokenData.refresh_token,
|
|
362
|
+
expires_at: Date.now() + (tokenData.expires_in * 1000),
|
|
363
|
+
api_domain: tokenData.api_domain || dc.calendar,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const tokensDir = getTokensDir();
|
|
367
|
+
if (!existsSync(tokensDir)) {
|
|
368
|
+
mkdirSync(tokensDir, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
372
|
+
writeFileSync(tokenPath, JSON.stringify(token, null, 2));
|
|
373
|
+
console.log(`Token saved for ${account.email}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function refreshZohoToken(account: ZohoAccount): Promise<ZohoToken | null> {
|
|
377
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
378
|
+
if (!existsSync(tokenPath)) return null;
|
|
379
|
+
|
|
380
|
+
const token: ZohoToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
381
|
+
|
|
382
|
+
// Check if token is still valid (with 5 minute buffer)
|
|
383
|
+
if (token.expires_at > Date.now() + 300000) {
|
|
384
|
+
return token;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Refresh the token
|
|
388
|
+
try {
|
|
389
|
+
const credentials = loadZohoCredentials();
|
|
390
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
391
|
+
|
|
392
|
+
const params = new URLSearchParams({
|
|
393
|
+
grant_type: "refresh_token",
|
|
394
|
+
client_id: credentials.client_id,
|
|
395
|
+
client_secret: credentials.client_secret,
|
|
396
|
+
refresh_token: token.refresh_token,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const response = await fetch(`${dc.accounts}/oauth/v2/token`, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
402
|
+
body: params.toString(),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) return null;
|
|
406
|
+
|
|
407
|
+
const data = await response.json();
|
|
408
|
+
const updatedToken: ZohoToken = {
|
|
409
|
+
...token,
|
|
410
|
+
access_token: data.access_token,
|
|
411
|
+
expires_at: Date.now() + (data.expires_in * 1000),
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
|
|
415
|
+
return updatedToken;
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getZohoAuthenticatedToken(account: ZohoAccount): ZohoToken | null {
|
|
422
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
423
|
+
if (!existsSync(tokenPath)) return null;
|
|
424
|
+
return JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
425
|
+
}
|
|
426
|
+
|
|
219
427
|
function startServerAndGetCode(authUrl: string): Promise<string> {
|
|
220
428
|
return new Promise((resolve, reject) => {
|
|
221
429
|
let server: Server;
|
|
@@ -313,16 +521,24 @@ interface CalendarEvent {
|
|
|
313
521
|
account: string;
|
|
314
522
|
accountEmail: string;
|
|
315
523
|
accountIndex: number;
|
|
524
|
+
provider: "google" | "zoho";
|
|
316
525
|
}
|
|
317
526
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
527
|
+
// Get all accounts combined for indexing
|
|
528
|
+
function getAllAccounts(config: Config): string[] {
|
|
529
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
530
|
+
const zohoEmails = config.zohoAccounts.map(z => z.email);
|
|
531
|
+
return [...gmailAccounts, ...zohoEmails];
|
|
532
|
+
}
|
|
322
533
|
|
|
323
|
-
|
|
534
|
+
async function getGoogleEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
|
|
535
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
536
|
+
const allAccounts = getAllAccounts(config);
|
|
537
|
+
|
|
538
|
+
const eventPromises = gmailAccounts.map(async (account) => {
|
|
539
|
+
const accountIndex = allAccounts.indexOf(account);
|
|
324
540
|
try {
|
|
325
|
-
const auth =
|
|
541
|
+
const auth = getGoogleAuthenticatedClient(account);
|
|
326
542
|
if (!auth) return [];
|
|
327
543
|
|
|
328
544
|
const calendar = google.calendar({ version: "v3", auth });
|
|
@@ -358,6 +574,7 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
|
|
|
358
574
|
account: extractAccountName(account),
|
|
359
575
|
accountEmail: account,
|
|
360
576
|
accountIndex,
|
|
577
|
+
provider: "google" as const,
|
|
361
578
|
};
|
|
362
579
|
});
|
|
363
580
|
} catch {
|
|
@@ -366,14 +583,251 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
|
|
|
366
583
|
});
|
|
367
584
|
|
|
368
585
|
const results = await Promise.all(eventPromises);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
586
|
+
return results.flat();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function getZohoEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
|
|
590
|
+
if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
|
|
591
|
+
|
|
592
|
+
const allAccounts = getAllAccounts(config);
|
|
593
|
+
const gmailCount = (config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts).length;
|
|
594
|
+
|
|
595
|
+
const eventPromises = config.zohoAccounts.map(async (account, idx) => {
|
|
596
|
+
const accountIndex = gmailCount + idx;
|
|
597
|
+
try {
|
|
598
|
+
const token = await refreshZohoToken(account);
|
|
599
|
+
if (!token) return [];
|
|
600
|
+
|
|
601
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
602
|
+
// Always use the calendar-specific API domain, not the generic api_domain
|
|
603
|
+
const apiBase = dc.calendar;
|
|
604
|
+
|
|
605
|
+
// First get list of calendars
|
|
606
|
+
const calendarsResponse = await fetch(`${apiBase}/api/v1/calendars?category=own`, {
|
|
607
|
+
headers: {
|
|
608
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
if (!calendarsResponse.ok) return [];
|
|
613
|
+
|
|
614
|
+
const calendarsData = await calendarsResponse.json();
|
|
615
|
+
const calendars = calendarsData.calendars || [];
|
|
616
|
+
|
|
617
|
+
if (calendars.length === 0) return [];
|
|
618
|
+
|
|
619
|
+
// Use the first (primary) calendar
|
|
620
|
+
const primaryCalendar = calendars.find((c: any) => c.isdefault) || calendars[0];
|
|
621
|
+
const calendarUid = primaryCalendar.uid;
|
|
622
|
+
|
|
623
|
+
// Format dates for Zoho API (yyyyMMdd'T'HHmmss'Z')
|
|
624
|
+
const formatZohoDate = (date: Date): string => {
|
|
625
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const range = JSON.stringify({
|
|
629
|
+
start: formatZohoDate(now),
|
|
630
|
+
end: formatZohoDate(timeMax),
|
|
631
|
+
});
|
|
372
632
|
|
|
633
|
+
const eventsResponse = await fetch(
|
|
634
|
+
`${apiBase}/api/v1/calendars/${encodeURIComponent(calendarUid)}/events?range=${encodeURIComponent(range)}`,
|
|
635
|
+
{
|
|
636
|
+
headers: {
|
|
637
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
638
|
+
},
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (!eventsResponse.ok) return [];
|
|
643
|
+
|
|
644
|
+
const eventsData = await eventsResponse.json();
|
|
645
|
+
const events = eventsData.events || [];
|
|
646
|
+
|
|
647
|
+
return events.map((event: any) => {
|
|
648
|
+
const isAllDay = event.isallday === true;
|
|
649
|
+
let start: Date, end: Date;
|
|
650
|
+
|
|
651
|
+
// Parse Zoho date format: "20260109T163000+0530" or "20260109T163000Z"
|
|
652
|
+
const parseZohoDate = (dateStr: string): Date => {
|
|
653
|
+
// Format: YYYYMMDDTHHmmss+HHMM or YYYYMMDDTHHmmssZ
|
|
654
|
+
const match = dateStr.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})([Z]|([+-])(\d{2})(\d{2}))?$/);
|
|
655
|
+
if (match) {
|
|
656
|
+
const [, year, month, day, hour, min, sec, tz, sign, tzHour, tzMin] = match;
|
|
657
|
+
if (tz === "Z") {
|
|
658
|
+
return new Date(Date.UTC(+year, +month - 1, +day, +hour, +min, +sec));
|
|
659
|
+
} else if (sign && tzHour && tzMin) {
|
|
660
|
+
const offsetMinutes = (+tzHour * 60 + +tzMin) * (sign === "+" ? -1 : 1);
|
|
661
|
+
const utc = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec);
|
|
662
|
+
return new Date(utc + offsetMinutes * 60000);
|
|
663
|
+
}
|
|
664
|
+
// No timezone, assume local
|
|
665
|
+
return new Date(+year, +month - 1, +day, +hour, +min, +sec);
|
|
666
|
+
}
|
|
667
|
+
// Fallback to standard parsing
|
|
668
|
+
return new Date(dateStr);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
if (event.dateandtime) {
|
|
672
|
+
start = parseZohoDate(event.dateandtime.start);
|
|
673
|
+
end = parseZohoDate(event.dateandtime.end);
|
|
674
|
+
} else {
|
|
675
|
+
start = parseZohoDate(event.start);
|
|
676
|
+
end = parseZohoDate(event.end);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
id: event.uid || "",
|
|
681
|
+
title: event.title || "(No title)",
|
|
682
|
+
start,
|
|
683
|
+
end,
|
|
684
|
+
isAllDay,
|
|
685
|
+
account: extractAccountName(account.email),
|
|
686
|
+
accountEmail: account.email,
|
|
687
|
+
accountIndex,
|
|
688
|
+
provider: "zoho" as const,
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
} catch {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const results = await Promise.all(eventPromises);
|
|
697
|
+
return results.flat();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
|
|
701
|
+
const now = new Date();
|
|
702
|
+
const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
|
|
703
|
+
|
|
704
|
+
// Fetch from both providers in parallel
|
|
705
|
+
const [googleEvents, zohoEvents] = await Promise.all([
|
|
706
|
+
getGoogleEvents(config, now, timeMax),
|
|
707
|
+
getZohoEvents(config, now, timeMax),
|
|
708
|
+
]);
|
|
709
|
+
|
|
710
|
+
const allEvents = [...googleEvents, ...zohoEvents];
|
|
373
711
|
allEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
374
712
|
return allEvents;
|
|
375
713
|
}
|
|
376
714
|
|
|
715
|
+
// ============================================================================
|
|
716
|
+
// Zoho Tasks
|
|
717
|
+
// ============================================================================
|
|
718
|
+
|
|
719
|
+
interface ZohoTask {
|
|
720
|
+
id: string;
|
|
721
|
+
title: string;
|
|
722
|
+
description: string;
|
|
723
|
+
dueDate: Date | null;
|
|
724
|
+
priority: "High" | "Normal" | "Low";
|
|
725
|
+
status: string;
|
|
726
|
+
isOverdue: boolean;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function getZohoTasks(config: Config): Promise<ZohoTask[]> {
|
|
730
|
+
if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
|
|
731
|
+
if (!config.showZohoTasks) return [];
|
|
732
|
+
|
|
733
|
+
const allTasks: ZohoTask[] = [];
|
|
734
|
+
|
|
735
|
+
for (const account of config.zohoAccounts) {
|
|
736
|
+
try {
|
|
737
|
+
const token = await refreshZohoToken(account);
|
|
738
|
+
if (!token) continue;
|
|
739
|
+
|
|
740
|
+
const dc = ZOHO_DATACENTERS[account.datacenter];
|
|
741
|
+
const mailBase = dc.mail;
|
|
742
|
+
|
|
743
|
+
// Fetch tasks assigned to user
|
|
744
|
+
const response = await fetch(
|
|
745
|
+
`${mailBase}/api/tasks/?view=assignedtome&action=view&limit=10&from=0`,
|
|
746
|
+
{
|
|
747
|
+
headers: {
|
|
748
|
+
Authorization: `Zoho-oauthtoken ${token.access_token}`,
|
|
749
|
+
Accept: "application/json",
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
if (!response.ok) continue;
|
|
755
|
+
|
|
756
|
+
const data = await response.json();
|
|
757
|
+
const tasks = data.data?.tasks || [];
|
|
758
|
+
|
|
759
|
+
const now = new Date();
|
|
760
|
+
|
|
761
|
+
for (const task of tasks) {
|
|
762
|
+
// Skip completed tasks
|
|
763
|
+
if (task.status === "Completed" || task.status === "completed") continue;
|
|
764
|
+
|
|
765
|
+
let dueDate: Date | null = null;
|
|
766
|
+
let isOverdue = false;
|
|
767
|
+
|
|
768
|
+
if (task.dueDate) {
|
|
769
|
+
// Parse DD/MM/YYYY format
|
|
770
|
+
const parts = task.dueDate.split("/");
|
|
771
|
+
if (parts.length === 3) {
|
|
772
|
+
dueDate = new Date(+parts[2], +parts[1] - 1, +parts[0]);
|
|
773
|
+
isOverdue = dueDate < now;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
allTasks.push({
|
|
778
|
+
id: task.id || "",
|
|
779
|
+
title: task.title || "(No title)",
|
|
780
|
+
description: task.description || "",
|
|
781
|
+
dueDate,
|
|
782
|
+
priority: task.priority || "Normal",
|
|
783
|
+
status: task.status || "Open",
|
|
784
|
+
isOverdue,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
} catch {
|
|
788
|
+
// Silently continue on error
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Sort: overdue first, then by due date (soonest first), then by priority
|
|
793
|
+
allTasks.sort((a, b) => {
|
|
794
|
+
// Overdue tasks first
|
|
795
|
+
if (a.isOverdue && !b.isOverdue) return -1;
|
|
796
|
+
if (!a.isOverdue && b.isOverdue) return 1;
|
|
797
|
+
|
|
798
|
+
// Then by due date (null dates go last)
|
|
799
|
+
if (a.dueDate && b.dueDate) {
|
|
800
|
+
return a.dueDate.getTime() - b.dueDate.getTime();
|
|
801
|
+
}
|
|
802
|
+
if (a.dueDate && !b.dueDate) return -1;
|
|
803
|
+
if (!a.dueDate && b.dueDate) return 1;
|
|
804
|
+
|
|
805
|
+
// Then by priority
|
|
806
|
+
const priorityOrder = { High: 0, Normal: 1, Low: 2 };
|
|
807
|
+
return (priorityOrder[a.priority] || 1) - (priorityOrder[b.priority] || 1);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
return allTasks.slice(0, config.maxTasksToShow);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function formatTasks(tasks: ZohoTask[]): string | null {
|
|
814
|
+
if (tasks.length === 0) return null;
|
|
815
|
+
|
|
816
|
+
const formatted = tasks.map((task) => {
|
|
817
|
+
const title = task.title.length > 25 ? task.title.slice(0, 24) + "…" : task.title;
|
|
818
|
+
|
|
819
|
+
if (task.isOverdue) {
|
|
820
|
+
return `${COLORS.red}${title}${COLORS.reset}`;
|
|
821
|
+
} else if (task.priority === "High") {
|
|
822
|
+
return `${COLORS.yellow}${title}${COLORS.reset}`;
|
|
823
|
+
} else {
|
|
824
|
+
return `${COLORS.white}${title}${COLORS.reset}`;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
return `${COLORS.cyan}Tasks:${COLORS.reset} ${formatted.join(", ")}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
377
831
|
function extractAccountName(email: string): string {
|
|
378
832
|
const atIndex = email.indexOf("@");
|
|
379
833
|
if (atIndex === -1) return email;
|
|
@@ -528,9 +982,9 @@ Display calendar events, tasks, and more at a glance.
|
|
|
528
982
|
Usage:
|
|
529
983
|
glancebar Output statusline (for Claude Code)
|
|
530
984
|
glancebar auth Authenticate all configured accounts
|
|
531
|
-
glancebar auth --add <email> Add and authenticate a new account
|
|
985
|
+
glancebar auth --add <email> Add and authenticate a new account (prompts for provider)
|
|
532
986
|
glancebar auth --remove <email> Remove an account
|
|
533
|
-
glancebar auth --list List configured accounts
|
|
987
|
+
glancebar auth --list List all configured accounts
|
|
534
988
|
glancebar config Show current configuration
|
|
535
989
|
glancebar config --lookahead <hours> Set lookahead hours (default: 8)
|
|
536
990
|
glancebar config --countdown-threshold <mins> Set countdown threshold in minutes (default: 60)
|
|
@@ -541,11 +995,14 @@ Usage:
|
|
|
541
995
|
glancebar config --eye-reminder <true|false> Enable/disable eye break reminders (default: true)
|
|
542
996
|
glancebar config --cpu-usage <true|false> Show CPU usage (default: false)
|
|
543
997
|
glancebar config --memory-usage <true|false> Show memory usage (default: false)
|
|
998
|
+
glancebar config --zoho-tasks <true|false> Show Zoho tasks (default: true)
|
|
999
|
+
glancebar config --max-tasks <number> Max tasks to show (default: 3)
|
|
544
1000
|
glancebar config --reset Reset to default configuration
|
|
545
1001
|
glancebar setup Show setup instructions
|
|
546
1002
|
|
|
547
1003
|
Examples:
|
|
548
|
-
glancebar auth --add user@gmail.com
|
|
1004
|
+
glancebar auth --add user@gmail.com # Will prompt for Google or Zoho
|
|
1005
|
+
glancebar auth --add user@zoho.com # Will prompt for Google or Zoho
|
|
549
1006
|
glancebar config --lookahead 12
|
|
550
1007
|
glancebar config --stretch-reminder false
|
|
551
1008
|
|
|
@@ -558,6 +1015,9 @@ function printSetup() {
|
|
|
558
1015
|
Glancebar - Setup Instructions
|
|
559
1016
|
==============================
|
|
560
1017
|
|
|
1018
|
+
GOOGLE CALENDAR SETUP
|
|
1019
|
+
---------------------
|
|
1020
|
+
|
|
561
1021
|
Step 1: Create Google Cloud Project
|
|
562
1022
|
- Go to https://console.cloud.google.com/
|
|
563
1023
|
- Create a new project or select existing one
|
|
@@ -574,17 +1034,41 @@ Step 3: Create OAuth Credentials
|
|
|
574
1034
|
|
|
575
1035
|
Step 4: Save credentials
|
|
576
1036
|
- Rename downloaded file to "credentials.json"
|
|
577
|
-
- Save it to: ${
|
|
1037
|
+
- Save it to: ${getGoogleCredentialsPath()}
|
|
578
1038
|
|
|
579
1039
|
Step 5: Add redirect URI
|
|
580
|
-
-
|
|
581
|
-
|
|
1040
|
+
- Edit credentials.json and ensure redirect_uris contains:
|
|
1041
|
+
"redirect_uris": ["http://localhost:3000/callback"]
|
|
1042
|
+
|
|
1043
|
+
ZOHO CALENDAR SETUP
|
|
1044
|
+
-------------------
|
|
1045
|
+
|
|
1046
|
+
Step 1: Register Application
|
|
1047
|
+
- Go to https://api-console.zoho.com/
|
|
1048
|
+
- Click "Add Client" > "Server-based Applications"
|
|
1049
|
+
|
|
1050
|
+
Step 2: Configure Client
|
|
1051
|
+
- Set Authorized Redirect URI: http://localhost:3000/callback
|
|
1052
|
+
- Note your Client ID and Client Secret
|
|
1053
|
+
|
|
1054
|
+
Step 3: Save credentials
|
|
1055
|
+
- Create file: ${getZohoCredentialsPath()}
|
|
1056
|
+
- Add content:
|
|
1057
|
+
{
|
|
1058
|
+
"client_id": "YOUR_CLIENT_ID",
|
|
1059
|
+
"client_secret": "YOUR_CLIENT_SECRET"
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
ADDING ACCOUNTS
|
|
1063
|
+
---------------
|
|
582
1064
|
|
|
583
|
-
Step 6: Add your Google accounts
|
|
584
1065
|
glancebar auth --add your-email@gmail.com
|
|
585
|
-
|
|
1066
|
+
# Select "Google" or "Zoho" when prompted
|
|
1067
|
+
# For Zoho, select your datacenter region
|
|
1068
|
+
|
|
1069
|
+
CONFIGURE CLAUDE CODE
|
|
1070
|
+
---------------------
|
|
586
1071
|
|
|
587
|
-
Step 7: Configure Claude Code statusline
|
|
588
1072
|
Update ~/.claude/settings.json:
|
|
589
1073
|
{
|
|
590
1074
|
"statusLine": {
|
|
@@ -599,19 +1083,40 @@ For more info: https://github.com/vishal-android-freak/glancebar
|
|
|
599
1083
|
}
|
|
600
1084
|
|
|
601
1085
|
async function handleAuth(args: string[]) {
|
|
1086
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1087
|
+
const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
|
|
1088
|
+
|
|
602
1089
|
// Handle --list
|
|
603
1090
|
if (args.includes("--list")) {
|
|
604
1091
|
const config = loadConfig();
|
|
605
|
-
|
|
1092
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1093
|
+
const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
1094
|
+
|
|
1095
|
+
if (!hasAny) {
|
|
606
1096
|
console.log("No accounts configured.");
|
|
607
1097
|
} else {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1098
|
+
if (gmailAccounts.length > 0) {
|
|
1099
|
+
console.log("\nGoogle Calendar accounts:");
|
|
1100
|
+
gmailAccounts.forEach((acc, i) => {
|
|
1101
|
+
let tokenPath = getGoogleTokenPath(acc);
|
|
1102
|
+
if (!existsSync(tokenPath)) {
|
|
1103
|
+
tokenPath = getLegacyTokenPath(acc);
|
|
1104
|
+
}
|
|
1105
|
+
const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
|
|
1106
|
+
console.log(` ${i + 1}. ${acc} (${status})`);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (config.zohoAccounts.length > 0) {
|
|
1111
|
+
console.log("\nZoho Calendar accounts:");
|
|
1112
|
+
config.zohoAccounts.forEach((acc, i) => {
|
|
1113
|
+
const tokenPath = getZohoTokenPath(acc.email);
|
|
1114
|
+
const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
|
|
1115
|
+
console.log(` ${i + 1}. ${acc.email} [${acc.datacenter}] (${status})`);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
614
1118
|
}
|
|
1119
|
+
rl.close();
|
|
615
1120
|
return;
|
|
616
1121
|
}
|
|
617
1122
|
|
|
@@ -621,25 +1126,89 @@ async function handleAuth(args: string[]) {
|
|
|
621
1126
|
const email = args[addIndex + 1];
|
|
622
1127
|
if (!email || email.startsWith("--")) {
|
|
623
1128
|
console.error("Error: Please provide an email address after --add");
|
|
1129
|
+
rl.close();
|
|
624
1130
|
process.exit(1);
|
|
625
1131
|
}
|
|
626
1132
|
|
|
627
1133
|
if (!email.includes("@")) {
|
|
628
1134
|
console.error("Error: Invalid email address");
|
|
1135
|
+
rl.close();
|
|
629
1136
|
process.exit(1);
|
|
630
1137
|
}
|
|
631
1138
|
|
|
1139
|
+
// Prompt for provider
|
|
1140
|
+
console.log("\nSelect calendar provider:");
|
|
1141
|
+
console.log(" 1. Google Calendar");
|
|
1142
|
+
console.log(" 2. Zoho Calendar");
|
|
1143
|
+
const providerChoice = await prompt("\nEnter choice (1 or 2): ");
|
|
1144
|
+
|
|
632
1145
|
const config = loadConfig();
|
|
633
|
-
|
|
634
|
-
|
|
1146
|
+
|
|
1147
|
+
if (providerChoice === "1") {
|
|
1148
|
+
// Google Calendar
|
|
1149
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1150
|
+
if (gmailAccounts.includes(email)) {
|
|
1151
|
+
console.log(`\nGoogle account ${email} already exists. Re-authenticating...`);
|
|
1152
|
+
} else {
|
|
1153
|
+
if (config.gmailAccounts.length === 0 && config.accounts.length > 0) {
|
|
1154
|
+
config.gmailAccounts = [...config.accounts];
|
|
1155
|
+
}
|
|
1156
|
+
config.gmailAccounts.push(email);
|
|
1157
|
+
saveConfig(config);
|
|
1158
|
+
console.log(`\nAdded ${email} to Google accounts.`);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
await authenticateGoogleAccount(email);
|
|
1162
|
+
console.log("\nDone!");
|
|
1163
|
+
} else if (providerChoice === "2") {
|
|
1164
|
+
// Zoho Calendar
|
|
1165
|
+
console.log("\nSelect Zoho datacenter:");
|
|
1166
|
+
console.log(" 1. com - United States");
|
|
1167
|
+
console.log(" 2. eu - Europe");
|
|
1168
|
+
console.log(" 3. in - India");
|
|
1169
|
+
console.log(" 4. com.au - Australia");
|
|
1170
|
+
console.log(" 5. com.cn - China");
|
|
1171
|
+
console.log(" 6. jp - Japan");
|
|
1172
|
+
console.log(" 7. zohocloud.ca - Canada");
|
|
1173
|
+
|
|
1174
|
+
const dcChoice = await prompt("\nEnter choice (1-7): ");
|
|
1175
|
+
const dcMap: Record<string, string> = {
|
|
1176
|
+
"1": "com",
|
|
1177
|
+
"2": "eu",
|
|
1178
|
+
"3": "in",
|
|
1179
|
+
"4": "com.au",
|
|
1180
|
+
"5": "com.cn",
|
|
1181
|
+
"6": "jp",
|
|
1182
|
+
"7": "zohocloud.ca",
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const datacenter = dcMap[dcChoice];
|
|
1186
|
+
if (!datacenter) {
|
|
1187
|
+
console.error("Error: Invalid datacenter choice");
|
|
1188
|
+
rl.close();
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const existingZoho = config.zohoAccounts.find((z) => z.email === email);
|
|
1193
|
+
if (existingZoho) {
|
|
1194
|
+
console.log(`\nZoho account ${email} already exists. Re-authenticating...`);
|
|
1195
|
+
existingZoho.datacenter = datacenter;
|
|
1196
|
+
saveConfig(config);
|
|
1197
|
+
} else {
|
|
1198
|
+
config.zohoAccounts.push({ email, datacenter });
|
|
1199
|
+
saveConfig(config);
|
|
1200
|
+
console.log(`\nAdded ${email} to Zoho accounts.`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
await authenticateZohoAccount({ email, datacenter });
|
|
1204
|
+
console.log("\nDone!");
|
|
635
1205
|
} else {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1206
|
+
console.error("Error: Invalid choice. Please enter 1 or 2.");
|
|
1207
|
+
rl.close();
|
|
1208
|
+
process.exit(1);
|
|
639
1209
|
}
|
|
640
1210
|
|
|
641
|
-
|
|
642
|
-
console.log("\nDone!");
|
|
1211
|
+
rl.close();
|
|
643
1212
|
return;
|
|
644
1213
|
}
|
|
645
1214
|
|
|
@@ -649,52 +1218,92 @@ async function handleAuth(args: string[]) {
|
|
|
649
1218
|
const email = args[removeIndex + 1];
|
|
650
1219
|
if (!email || email.startsWith("--")) {
|
|
651
1220
|
console.error("Error: Please provide an email address after --remove");
|
|
1221
|
+
rl.close();
|
|
652
1222
|
process.exit(1);
|
|
653
1223
|
}
|
|
654
1224
|
|
|
655
1225
|
const config = loadConfig();
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1226
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1227
|
+
|
|
1228
|
+
// Check Google accounts
|
|
1229
|
+
const googleIdx = gmailAccounts.indexOf(email);
|
|
1230
|
+
if (googleIdx !== -1) {
|
|
1231
|
+
if (config.gmailAccounts.length > 0) {
|
|
1232
|
+
config.gmailAccounts.splice(googleIdx, 1);
|
|
1233
|
+
} else {
|
|
1234
|
+
config.accounts.splice(googleIdx, 1);
|
|
1235
|
+
}
|
|
1236
|
+
saveConfig(config);
|
|
1237
|
+
|
|
1238
|
+
// Remove token files
|
|
1239
|
+
const tokenPath = getGoogleTokenPath(email);
|
|
1240
|
+
const legacyPath = getLegacyTokenPath(email);
|
|
1241
|
+
if (existsSync(tokenPath)) unlinkSync(tokenPath);
|
|
1242
|
+
if (existsSync(legacyPath)) unlinkSync(legacyPath);
|
|
1243
|
+
|
|
1244
|
+
console.log(`Removed Google account ${email}.`);
|
|
1245
|
+
rl.close();
|
|
1246
|
+
return;
|
|
660
1247
|
}
|
|
661
1248
|
|
|
662
|
-
|
|
663
|
-
|
|
1249
|
+
// Check Zoho accounts
|
|
1250
|
+
const zohoIdx = config.zohoAccounts.findIndex((z) => z.email === email);
|
|
1251
|
+
if (zohoIdx !== -1) {
|
|
1252
|
+
config.zohoAccounts.splice(zohoIdx, 1);
|
|
1253
|
+
saveConfig(config);
|
|
664
1254
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1255
|
+
const tokenPath = getZohoTokenPath(email);
|
|
1256
|
+
if (existsSync(tokenPath)) unlinkSync(tokenPath);
|
|
1257
|
+
|
|
1258
|
+
console.log(`Removed Zoho account ${email}.`);
|
|
1259
|
+
rl.close();
|
|
1260
|
+
return;
|
|
668
1261
|
}
|
|
669
1262
|
|
|
670
|
-
console.
|
|
671
|
-
|
|
1263
|
+
console.error(`Error: Account ${email} not found.`);
|
|
1264
|
+
rl.close();
|
|
1265
|
+
process.exit(1);
|
|
672
1266
|
}
|
|
673
1267
|
|
|
674
1268
|
// Default: authenticate all accounts
|
|
675
1269
|
const config = loadConfig();
|
|
1270
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1271
|
+
const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
676
1272
|
|
|
677
|
-
if (
|
|
1273
|
+
if (!hasAny) {
|
|
678
1274
|
console.log("No accounts configured.\n");
|
|
679
1275
|
console.log("Add an account using:");
|
|
680
1276
|
console.log(" glancebar auth --add your-email@gmail.com\n");
|
|
1277
|
+
rl.close();
|
|
681
1278
|
return;
|
|
682
1279
|
}
|
|
683
1280
|
|
|
684
|
-
console.log("Glancebar -
|
|
685
|
-
console.log("
|
|
1281
|
+
console.log("Glancebar - Calendar Authentication");
|
|
1282
|
+
console.log("====================================\n");
|
|
686
1283
|
|
|
687
|
-
|
|
688
|
-
|
|
1284
|
+
// Authenticate Google accounts
|
|
1285
|
+
for (const account of gmailAccounts) {
|
|
1286
|
+
let tokenPath = getGoogleTokenPath(account);
|
|
1287
|
+
if (!existsSync(tokenPath)) {
|
|
1288
|
+
tokenPath = getLegacyTokenPath(account);
|
|
1289
|
+
}
|
|
1290
|
+
if (existsSync(tokenPath)) {
|
|
1291
|
+
console.log(`Google: ${account} - Already authenticated`);
|
|
1292
|
+
const answer = await prompt(`Re-authenticate? (y/N): `);
|
|
1293
|
+
if (answer.toLowerCase() !== "y") continue;
|
|
1294
|
+
}
|
|
1295
|
+
await authenticateGoogleAccount(account);
|
|
1296
|
+
}
|
|
689
1297
|
|
|
690
|
-
|
|
691
|
-
|
|
1298
|
+
// Authenticate Zoho accounts
|
|
1299
|
+
for (const account of config.zohoAccounts) {
|
|
1300
|
+
const tokenPath = getZohoTokenPath(account.email);
|
|
692
1301
|
if (existsSync(tokenPath)) {
|
|
693
|
-
console.log(
|
|
694
|
-
const answer = await prompt(`Re-authenticate
|
|
1302
|
+
console.log(`Zoho: ${account.email} [${account.datacenter}] - Already authenticated`);
|
|
1303
|
+
const answer = await prompt(`Re-authenticate? (y/N): `);
|
|
695
1304
|
if (answer.toLowerCase() !== "y") continue;
|
|
696
1305
|
}
|
|
697
|
-
await
|
|
1306
|
+
await authenticateZohoAccount(account);
|
|
698
1307
|
}
|
|
699
1308
|
|
|
700
1309
|
rl.close();
|
|
@@ -706,8 +1315,9 @@ function handleConfig(args: string[]) {
|
|
|
706
1315
|
|
|
707
1316
|
// Handle --reset
|
|
708
1317
|
if (args.includes("--reset")) {
|
|
709
|
-
|
|
710
|
-
|
|
1318
|
+
// Preserve all account types
|
|
1319
|
+
const { accounts, gmailAccounts, zohoAccounts } = config;
|
|
1320
|
+
saveConfig({ ...DEFAULT_CONFIG, accounts, gmailAccounts, zohoAccounts });
|
|
711
1321
|
console.log("Configuration reset to defaults (accounts preserved).");
|
|
712
1322
|
return;
|
|
713
1323
|
}
|
|
@@ -838,12 +1448,49 @@ function handleConfig(args: string[]) {
|
|
|
838
1448
|
return;
|
|
839
1449
|
}
|
|
840
1450
|
|
|
1451
|
+
// Handle --zoho-tasks
|
|
1452
|
+
const zohoTasksIndex = args.indexOf("--zoho-tasks");
|
|
1453
|
+
if (zohoTasksIndex !== -1) {
|
|
1454
|
+
const value = args[zohoTasksIndex + 1]?.toLowerCase();
|
|
1455
|
+
if (value !== "true" && value !== "false") {
|
|
1456
|
+
console.error("Error: --zoho-tasks must be 'true' or 'false'");
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
config.showZohoTasks = value === "true";
|
|
1460
|
+
saveConfig(config);
|
|
1461
|
+
console.log(`Zoho tasks display ${value === "true" ? "enabled" : "disabled"}`);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Handle --max-tasks
|
|
1466
|
+
const maxTasksIndex = args.indexOf("--max-tasks");
|
|
1467
|
+
if (maxTasksIndex !== -1) {
|
|
1468
|
+
const value = parseInt(args[maxTasksIndex + 1], 10);
|
|
1469
|
+
if (isNaN(value) || value < 1 || value > 10) {
|
|
1470
|
+
console.error("Error: --max-tasks must be between 1 and 10");
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
config.maxTasksToShow = value;
|
|
1474
|
+
saveConfig(config);
|
|
1475
|
+
console.log(`Max tasks to show set to ${value}`);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
841
1479
|
// Show current config
|
|
1480
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1481
|
+
const googleStr = gmailAccounts.length > 0 ? gmailAccounts.join(", ") : "(none)";
|
|
1482
|
+
const zohoStr = config.zohoAccounts.length > 0
|
|
1483
|
+
? config.zohoAccounts.map((z) => `${z.email} [${z.datacenter}]`).join(", ")
|
|
1484
|
+
: "(none)";
|
|
1485
|
+
|
|
842
1486
|
console.log(`
|
|
843
1487
|
Glancebar Configuration
|
|
844
1488
|
=======================
|
|
845
1489
|
Config directory: ${getConfigDir()}
|
|
846
|
-
|
|
1490
|
+
|
|
1491
|
+
Accounts:
|
|
1492
|
+
Google Calendar: ${googleStr}
|
|
1493
|
+
Zoho Calendar: ${zohoStr}
|
|
847
1494
|
|
|
848
1495
|
Calendar Settings:
|
|
849
1496
|
Lookahead hours: ${config.lookaheadHours}
|
|
@@ -859,6 +1506,10 @@ Reminders:
|
|
|
859
1506
|
System Stats:
|
|
860
1507
|
CPU usage: ${config.showCpuUsage ? "enabled" : "disabled"}
|
|
861
1508
|
Memory usage: ${config.showMemoryUsage ? "enabled" : "disabled"}
|
|
1509
|
+
|
|
1510
|
+
Zoho Tasks:
|
|
1511
|
+
Show tasks: ${config.showZohoTasks ? "enabled" : "disabled"}
|
|
1512
|
+
Max tasks to show: ${config.maxTasksToShow}
|
|
862
1513
|
`);
|
|
863
1514
|
}
|
|
864
1515
|
|
|
@@ -1058,9 +1709,16 @@ async function outputStatusline() {
|
|
|
1058
1709
|
parts.push(reminder);
|
|
1059
1710
|
}
|
|
1060
1711
|
|
|
1061
|
-
// Get calendar events
|
|
1062
|
-
|
|
1063
|
-
|
|
1712
|
+
// Get calendar events and tasks in parallel
|
|
1713
|
+
const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
|
|
1714
|
+
const hasAccounts = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
|
|
1715
|
+
|
|
1716
|
+
if (hasAccounts) {
|
|
1717
|
+
const [events, tasks] = await Promise.all([
|
|
1718
|
+
getUpcomingEvents(config),
|
|
1719
|
+
getZohoTasks(config),
|
|
1720
|
+
]);
|
|
1721
|
+
|
|
1064
1722
|
const event = getCurrentOrNextEvent(events);
|
|
1065
1723
|
|
|
1066
1724
|
// Check for meeting warning (within 5 minutes)
|
|
@@ -1069,6 +1727,12 @@ async function outputStatusline() {
|
|
|
1069
1727
|
parts.push(meetingWarning);
|
|
1070
1728
|
}
|
|
1071
1729
|
|
|
1730
|
+
// Add tasks if available
|
|
1731
|
+
const tasksStr = formatTasks(tasks);
|
|
1732
|
+
if (tasksStr) {
|
|
1733
|
+
parts.push(tasksStr);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1072
1736
|
if (event) {
|
|
1073
1737
|
parts.push(formatEvent(event, config));
|
|
1074
1738
|
} else if (parts.length === 0) {
|