get-claudia 1.51.1 → 1.51.3

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/bin/index.js CHANGED
@@ -811,8 +811,7 @@ async function main() {
811
811
 
812
812
  console.log('');
813
813
  console.log(` ${colors.dim}Optional: connect Google services${colors.reset}`);
814
- console.log(` ${colors.cyan}claudia gmail login${colors.reset} ${colors.dim}Read & send email${colors.reset}`);
815
- console.log(` ${colors.cyan}claudia calendar login${colors.reset} ${colors.dim}View & create events${colors.reset}`);
814
+ console.log(` ${colors.cyan}claudia google login${colors.reset} ${colors.dim}Gmail + Calendar in one step${colors.reset}`);
816
815
  console.log('');
817
816
  }
818
817
  }
@@ -2,15 +2,20 @@
2
2
  * Google integration CLI commands.
3
3
  *
4
4
  * Provides:
5
- * claudia gmail login - Sign in with Google (Gmail)
6
- * claudia gmail status - Check connection status
5
+ * claudia google login - Sign in once for Gmail + Calendar
6
+ * claudia google status - Check connection status for all services
7
+ * claudia google logout - Sign out of all Google services
8
+ * claudia gmail login - Sign in with Google (Gmail only)
9
+ * claudia gmail status - Check Gmail connection status
7
10
  * claudia gmail search - Search emails
8
11
  * claudia gmail read - Read a specific email
9
- * claudia gmail logout - Sign out (remove stored tokens)
10
- * claudia calendar login - Sign in with Google (Calendar)
11
- * claudia calendar status - Check connection status
12
+ * claudia gmail logout - Sign out of Gmail
13
+ * claudia calendar login - Sign in with Google (Calendar only)
14
+ * claudia calendar status - Check Calendar connection status
12
15
  * claudia calendar list - List upcoming events
13
- * claudia calendar logout - Sign out (remove stored tokens)
16
+ * claudia calendar search - Search events by text
17
+ * claudia calendar read - Read a specific event by ID
18
+ * claudia calendar logout - Sign out of Calendar
14
19
  */
15
20
 
16
21
  import { authenticate, getAccessToken, isAuthenticated, revokeTokens, authStatus } from '../core/google-oauth.js';
@@ -218,6 +223,103 @@ export async function calendarListCommand(opts) {
218
223
  output({ events, timeRange: { from: now.toISOString(), to: timeMax.toISOString() }, total: events.length });
219
224
  }
220
225
 
226
+ export async function calendarSearchCommand(query, opts) {
227
+ const token = await getAccessToken('calendar');
228
+ if (!token) {
229
+ console.error('Not authenticated. Run: claudia calendar login');
230
+ process.exitCode = 1;
231
+ return;
232
+ }
233
+
234
+ const now = new Date();
235
+ const maxDays = opts.days || 90;
236
+ const timeMin = opts.past
237
+ ? new Date(now.getTime() - maxDays * 24 * 60 * 60 * 1000)
238
+ : now;
239
+ const timeMax = new Date(now.getTime() + maxDays * 24 * 60 * 60 * 1000);
240
+ const maxResults = opts.limit || 25;
241
+
242
+ const url = new URL('https://www.googleapis.com/calendar/v3/calendars/primary/events');
243
+ url.searchParams.set('q', query);
244
+ url.searchParams.set('timeMin', timeMin.toISOString());
245
+ url.searchParams.set('timeMax', timeMax.toISOString());
246
+ url.searchParams.set('maxResults', String(maxResults));
247
+ url.searchParams.set('singleEvents', 'true');
248
+ url.searchParams.set('orderBy', 'startTime');
249
+
250
+ const resp = await fetch(url, {
251
+ headers: { Authorization: `Bearer ${token}` },
252
+ });
253
+
254
+ if (!resp.ok) {
255
+ const err = await resp.text();
256
+ console.error(`Calendar API error (${resp.status}): ${err}`);
257
+ process.exitCode = 1;
258
+ return;
259
+ }
260
+
261
+ const data = await resp.json();
262
+ const events = (data.items || []).map(e => ({
263
+ id: e.id,
264
+ summary: e.summary || '(no title)',
265
+ start: e.start?.dateTime || e.start?.date || '',
266
+ end: e.end?.dateTime || e.end?.date || '',
267
+ location: e.location || '',
268
+ attendees: (e.attendees || []).map(a => a.email),
269
+ status: e.status,
270
+ htmlLink: e.htmlLink,
271
+ }));
272
+
273
+ output({ events, query, total: events.length });
274
+ }
275
+
276
+ export async function calendarReadCommand(eventId) {
277
+ const token = await getAccessToken('calendar');
278
+ if (!token) {
279
+ console.error('Not authenticated. Run: claudia calendar login');
280
+ process.exitCode = 1;
281
+ return;
282
+ }
283
+
284
+ const resp = await fetch(
285
+ `https://www.googleapis.com/calendar/v3/calendars/primary/events/${encodeURIComponent(eventId)}`,
286
+ { headers: { Authorization: `Bearer ${token}` } }
287
+ );
288
+
289
+ if (!resp.ok) {
290
+ const err = await resp.text();
291
+ console.error(`Calendar API error (${resp.status}): ${err}`);
292
+ process.exitCode = 1;
293
+ return;
294
+ }
295
+
296
+ const e = await resp.json();
297
+ output({
298
+ id: e.id,
299
+ summary: e.summary || '(no title)',
300
+ description: e.description || '',
301
+ start: e.start?.dateTime || e.start?.date || '',
302
+ end: e.end?.dateTime || e.end?.date || '',
303
+ location: e.location || '',
304
+ attendees: (e.attendees || []).map(a => ({
305
+ email: a.email,
306
+ displayName: a.displayName || '',
307
+ responseStatus: a.responseStatus || '',
308
+ organizer: a.organizer || false,
309
+ })),
310
+ organizer: e.organizer?.email || '',
311
+ status: e.status,
312
+ htmlLink: e.htmlLink,
313
+ created: e.created,
314
+ updated: e.updated,
315
+ recurringEventId: e.recurringEventId || null,
316
+ conferenceData: e.conferenceData?.entryPoints?.map(ep => ({
317
+ type: ep.entryPointType,
318
+ uri: ep.uri,
319
+ })) || [],
320
+ });
321
+ }
322
+
221
323
  export async function calendarLogoutCommand() {
222
324
  const removed = revokeTokens('calendar');
223
325
  if (removed) {
@@ -227,6 +329,31 @@ export async function calendarLogoutCommand() {
227
329
  }
228
330
  }
229
331
 
332
+ // ── Unified Google Commands ──
333
+
334
+ export async function googleLoginCommand() {
335
+ try {
336
+ await authenticate('google');
337
+ console.log('\n\u2713 Google connected! Claudia can now access Gmail and Calendar.');
338
+ console.log(' Try: claudia gmail search "is:unread"');
339
+ console.log(' Try: claudia calendar list');
340
+ } catch (err) {
341
+ console.error(`\n\u2717 ${err.message}`);
342
+ process.exitCode = 1;
343
+ }
344
+ }
345
+
346
+ export async function googleLogoutCommand() {
347
+ const gmailRemoved = revokeTokens('gmail');
348
+ const calendarRemoved = revokeTokens('calendar');
349
+ if (gmailRemoved || calendarRemoved) {
350
+ const services = [gmailRemoved && 'Gmail', calendarRemoved && 'Calendar'].filter(Boolean).join(' and ');
351
+ console.log(`\u2713 Signed out of ${services}. Run "claudia google login" to reconnect.`);
352
+ } else {
353
+ console.log('Not signed in to any Google services.');
354
+ }
355
+ }
356
+
230
357
  // ── Shared status command ──
231
358
 
232
359
  export async function googleStatusCommand() {
@@ -234,6 +361,7 @@ export async function googleStatusCommand() {
234
361
  output({
235
362
  gmail: { connected: status.gmail, login_command: 'claudia gmail login' },
236
363
  calendar: { connected: status.calendar, login_command: 'claudia calendar login' },
364
+ unified_login: 'claudia google login',
237
365
  tokens_dir: '~/.claudia/tokens/',
238
366
  });
239
367
  }
@@ -45,6 +45,13 @@ const SCOPES = {
45
45
  'https://www.googleapis.com/auth/calendar.readonly',
46
46
  'https://www.googleapis.com/auth/calendar.events',
47
47
  ],
48
+ google: [
49
+ 'https://www.googleapis.com/auth/gmail.readonly',
50
+ 'https://www.googleapis.com/auth/gmail.send',
51
+ 'https://www.googleapis.com/auth/gmail.modify',
52
+ 'https://www.googleapis.com/auth/calendar.readonly',
53
+ 'https://www.googleapis.com/auth/calendar.events',
54
+ ],
48
55
  };
49
56
 
50
57
  // ── Credential resolution ──
@@ -75,12 +82,12 @@ function getCredentials() {
75
82
  /**
76
83
  * Run the full OAuth browser flow for a service.
77
84
  * Opens the user's browser, waits for consent, stores tokens locally.
78
- * @param {'gmail'|'calendar'} service
85
+ * @param {'gmail'|'calendar'|'google'} service
79
86
  * @returns {Promise<{access_token: string, refresh_token: string}>}
80
87
  */
81
88
  export async function authenticate(service) {
82
89
  const scopes = SCOPES[service];
83
- if (!scopes) throw new Error(`Unknown service: ${service}. Use "gmail" or "calendar".`);
90
+ if (!scopes) throw new Error(`Unknown service: ${service}. Use "gmail", "calendar", or "google".`);
84
91
 
85
92
  const { clientId, clientSecret } = getCredentials();
86
93
 
@@ -101,6 +108,7 @@ export async function authenticate(service) {
101
108
 
102
109
  console.log(`\nOpening your browser to sign in with Google...`);
103
110
  console.log(`If it doesn't open, visit:\n ${authUrl.toString()}\n`);
111
+ console.log(`Waiting for browser authorization...`);
104
112
 
105
113
  // 3. Start local server, open browser, wait for callback
106
114
  const code = await new Promise((resolve, reject) => {
@@ -134,8 +142,7 @@ export async function authenticate(service) {
134
142
  }
135
143
 
136
144
  res.writeHead(200, { 'Content-Type': 'text/html' });
137
- const serviceName = service === 'calendar' ? 'Google Calendar' : 'Gmail';
138
- res.end(htmlPage('Connected!', `Claudia now has access to your ${serviceName}. Here's what she can do:`, service));
145
+ res.end(htmlPage('Connected', `You're all set. Claudia is now connected.`, service));
139
146
  server.close();
140
147
  resolve(authCode);
141
148
  });
@@ -154,17 +161,28 @@ export async function authenticate(service) {
154
161
  });
155
162
 
156
163
  // 4. Exchange authorization code for tokens
157
- const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
158
- method: 'POST',
159
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
160
- body: new URLSearchParams({
161
- code,
162
- client_id: clientId,
163
- client_secret: clientSecret,
164
- redirect_uri: redirectUri,
165
- grant_type: 'authorization_code',
166
- }),
167
- });
164
+ console.log(`Token received, finishing setup...`);
165
+
166
+ const controller = new AbortController();
167
+ const fetchTimeout = setTimeout(() => controller.abort(), 10_000);
168
+
169
+ let tokenResp;
170
+ try {
171
+ tokenResp = await fetch('https://oauth2.googleapis.com/token', {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
174
+ body: new URLSearchParams({
175
+ code,
176
+ client_id: clientId,
177
+ client_secret: clientSecret,
178
+ redirect_uri: redirectUri,
179
+ grant_type: 'authorization_code',
180
+ }),
181
+ signal: controller.signal,
182
+ });
183
+ } finally {
184
+ clearTimeout(fetchTimeout);
185
+ }
168
186
 
169
187
  if (!tokenResp.ok) {
170
188
  const errBody = await tokenResp.text();
@@ -182,7 +200,14 @@ export async function authenticate(service) {
182
200
  scopes,
183
201
  created: new Date().toISOString(),
184
202
  };
185
- writeFileSync(join(TOKENS_DIR, `${service}.json`), JSON.stringify(tokenData, null, 2));
203
+
204
+ if (service === 'google') {
205
+ // Unified login: save tokens for both gmail and calendar
206
+ writeFileSync(join(TOKENS_DIR, 'gmail.json'), JSON.stringify(tokenData, null, 2));
207
+ writeFileSync(join(TOKENS_DIR, 'calendar.json'), JSON.stringify(tokenData, null, 2));
208
+ } else {
209
+ writeFileSync(join(TOKENS_DIR, `${service}.json`), JSON.stringify(tokenData, null, 2));
210
+ }
186
211
 
187
212
  return tokens;
188
213
  }
@@ -291,23 +316,45 @@ function openBrowser(url) {
291
316
  }
292
317
 
293
318
  function htmlPage(title, message, service) {
294
- const isSuccess = title === 'Connected!';
295
- const LOGO_BASE64 = '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';
319
+ const isSuccess = title === 'Connected';
296
320
 
297
321
  const features = {
298
322
  gmail: [
299
- ['Search & read emails', 'Ask Claudia to find emails by sender, subject, or content'],
300
- ['Draft & send replies', 'Claudia can compose emails with your tone and context'],
301
- ['Inbox triage', 'Get a morning brief of what needs attention'],
323
+ ['Search & read emails', 'Find emails by sender, subject, or content'],
324
+ ['Draft & send replies', 'Compose emails with your tone and context'],
325
+ ['Inbox triage', 'Morning brief of what needs attention'],
302
326
  ],
303
327
  calendar: [
304
- ['View your schedule', 'Claudia sees upcoming events for meeting prep'],
305
- ['Create events', 'Schedule meetings through natural conversation'],
306
- ['Time awareness', 'Claudia knows when you\'re busy or free'],
328
+ ['View your schedule', 'See upcoming events for meeting prep'],
329
+ ['Create events', 'Schedule meetings through conversation'],
330
+ ['Time awareness', 'Know when you\'re busy or free'],
307
331
  ],
308
332
  };
309
333
 
310
- const serviceFeatures = features[service] || features.gmail;
334
+ // For unified 'google' login, show both sets of features
335
+ const showGmail = service === 'gmail' || service === 'google';
336
+ const showCalendar = service === 'calendar' || service === 'google';
337
+
338
+ const featureCards = [];
339
+ if (showGmail) {
340
+ for (const [name, desc] of features.gmail) {
341
+ featureCards.push({ icon: '&#128233;', name, desc });
342
+ }
343
+ }
344
+ if (showCalendar) {
345
+ for (const [name, desc] of features.calendar) {
346
+ featureCards.push({ icon: '&#128197;', name, desc });
347
+ }
348
+ }
349
+
350
+ const serviceName = service === 'google' ? 'Gmail & Google Calendar'
351
+ : service === 'calendar' ? 'Google Calendar' : 'Gmail';
352
+
353
+ const featuresHtml = featureCards.map((f, i) => `
354
+ <div class="feature" style="animation-delay:${0.1 + i * 0.08}s">
355
+ <div class="feature-icon">${f.icon}</div>
356
+ <div class="feature-text"><h3>${f.name}</h3><p>${f.desc}</p></div>
357
+ </div>`).join('');
311
358
 
312
359
  return `<!DOCTYPE html>
313
360
  <html>
@@ -317,95 +364,70 @@ function htmlPage(title, message, service) {
317
364
  body {
318
365
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
319
366
  display: flex; justify-content: center; align-items: center; min-height: 100vh;
320
- background: #09090b; color: #e0e0e0;
321
- background-image: radial-gradient(ellipse at 50% 0%, rgba(124,111,239,0.12) 0%, transparent 50%);
367
+ background: #F0F0F0; color: #1d1d1f;
322
368
  }
323
369
  .card {
324
- text-align: center; padding: 2.5rem 3rem 2rem; border-radius: 20px;
325
- background: #131316; border: 1px solid #27272a; max-width: 480px; width: 100%;
326
- box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(124,111,239,0.06);
327
- }
328
- .logo { margin-bottom: 1.25rem; }
329
- .logo img {
330
- width: 64px; height: auto; image-rendering: pixelated;
331
- ${isSuccess ? 'animation: float 3s ease-in-out infinite;' : ''}
370
+ text-align: center; padding: 2.5rem 2.5rem 2rem; border-radius: 16px;
371
+ background: #fff; max-width: 440px; width: 100%;
372
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.04);
332
373
  }
333
- @keyframes float {
334
- 0%, 100% { transform: translateY(0); }
335
- 50% { transform: translateY(-6px); }
336
- }
337
- .badge {
338
- display: inline-flex; align-items: center; gap: 0.4rem;
339
- padding: 0.35rem 0.9rem; border-radius: 99px; font-size: 0.78rem; font-weight: 500;
340
- margin-bottom: 1.25rem;
374
+ .checkmark {
375
+ width: 48px; height: 48px; border-radius: 50%; margin: 0 auto 1.25rem;
376
+ display: flex; align-items: center; justify-content: center;
341
377
  ${isSuccess
342
- ? 'background: rgba(124,111,239,0.12); color: #a99ef5; border: 1px solid rgba(124,111,239,0.2);'
343
- : 'background: rgba(239,68,68,0.12); color: #fca5a5; border: 1px solid rgba(239,68,68,0.2);'}
344
- ${isSuccess ? 'animation: fadeIn 0.5s ease;' : ''}
378
+ ? 'background: #00CED1; color: #fff;'
379
+ : 'background: #ef4444; color: #fff;'}
380
+ font-size: 1.4rem; font-weight: bold;
381
+ animation: popIn 0.4s cubic-bezier(0.175,0.885,0.32,1.275);
345
382
  }
346
- @keyframes fadeIn { 0% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: translateY(0); } }
383
+ @keyframes popIn { 0% { transform: scale(0); } 100% { transform: scale(1); } }
347
384
  h1 {
348
- color: ${isSuccess ? '#e4e2ff' : '#fca5a5'}; font-size: 1.35rem;
349
- font-weight: 600; margin-bottom: 0.5rem;
385
+ color: ${isSuccess ? '#00CED1' : '#ef4444'}; font-size: 1.4rem;
386
+ font-weight: 600; margin-bottom: 0.4rem;
387
+ }
388
+ .subtitle { color: #6e6e73; line-height: 1.5; font-size: 0.9rem; margin-bottom: 1.5rem; }
389
+ .service-label {
390
+ display: inline-block; padding: 0.2rem 0.7rem; border-radius: 99px;
391
+ font-size: 0.75rem; font-weight: 500; margin-bottom: 1.25rem;
392
+ background: rgba(0,206,209,0.08); color: #00CED1;
350
393
  }
351
- .subtitle { color: #71717a; line-height: 1.6; font-size: 0.88rem; margin-bottom: 1.5rem; }
352
394
  .features {
353
395
  text-align: left; margin: 0 auto 1.5rem; padding: 0;
354
- display: flex; flex-direction: column; gap: 0.75rem;
396
+ display: flex; flex-direction: column; gap: 0.5rem;
355
397
  }
356
398
  .feature {
357
- display: flex; gap: 0.75rem; align-items: flex-start;
358
- padding: 0.65rem 0.85rem; border-radius: 10px;
359
- background: #18181b; border: 1px solid #27272a;
360
- animation: slideIn 0.4s ease backwards;
399
+ display: flex; gap: 0.65rem; align-items: flex-start;
400
+ padding: 0.55rem 0.75rem; border-radius: 10px;
401
+ background: #f9f9fb; border: 1px solid #e8e8ed;
402
+ animation: slideIn 0.35s ease backwards;
361
403
  }
362
- .feature:nth-child(1) { animation-delay: 0.1s; }
363
- .feature:nth-child(2) { animation-delay: 0.2s; }
364
- .feature:nth-child(3) { animation-delay: 0.3s; }
365
- @keyframes slideIn { 0% { opacity: 0; transform: translateX(-12px); } 100% { opacity: 1; transform: translateX(0); } }
404
+ @keyframes slideIn { 0% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: translateY(0); } }
366
405
  .feature-icon {
367
- flex-shrink: 0; width: 28px; height: 28px; border-radius: 6px;
368
- background: rgba(124,111,239,0.1); display: flex; align-items: center;
369
- justify-content: center; font-size: 0.85rem; margin-top: 1px;
406
+ flex-shrink: 0; width: 26px; height: 26px; border-radius: 6px;
407
+ background: rgba(0,206,209,0.08); display: flex; align-items: center;
408
+ justify-content: center; font-size: 0.8rem; margin-top: 1px;
370
409
  }
371
- .feature-text h3 { font-size: 0.82rem; font-weight: 500; color: #d4d4d8; margin-bottom: 2px; }
372
- .feature-text p { font-size: 0.75rem; color: #52525b; line-height: 1.4; }
373
- .divider { border: none; border-top: 1px solid #27272a; margin: 0 0 1rem; }
374
- .footer { color: #3f3f46; font-size: 0.75rem; line-height: 1.5; }
375
- .footer .privacy { color: #52525b; margin-bottom: 0.35rem; }
376
- .footer .close { color: #3f3f46; }
410
+ .feature-text h3 { font-size: 0.8rem; font-weight: 500; color: #1d1d1f; margin-bottom: 1px; }
411
+ .feature-text p { font-size: 0.72rem; color: #86868b; line-height: 1.4; }
412
+ hr { border: none; border-top: 1px solid #e8e8ed; margin: 0 0 0.85rem; }
413
+ .footer { color: #86868b; font-size: 0.72rem; line-height: 1.6; }
414
+ .footer .close { color: #aeaeb2; margin-top: 0.25rem; }
377
415
  </style></head>
378
416
  <body>
379
417
  <div class="card">
380
- <div class="logo">
381
- <img src="data:image/png;base64,${LOGO_BASE64}" alt="Claudia" />
382
- </div>
383
- <div class="badge">${isSuccess ? '&#10003; Connected' : '&#10007; Failed'}</div>
384
- <h1>${title}</h1>
385
- <p class="subtitle">${message}</p>
418
+ <div class="checkmark">${isSuccess ? '&#10003;' : '&#10007;'}</div>
419
+ <h1>${isSuccess ? serviceName + ' Connected' : title}</h1>
420
+ <p class="subtitle">${isSuccess ? 'You can close this tab and return to your terminal.' : message}</p>
386
421
  ${isSuccess ? `
387
- <div class="features">
388
- <div class="feature">
389
- <div class="feature-icon">${service === 'calendar' ? '&#128197;' : '&#128233;'}</div>
390
- <div class="feature-text"><h3>${serviceFeatures[0][0]}</h3><p>${serviceFeatures[0][1]}</p></div>
391
- </div>
392
- <div class="feature">
393
- <div class="feature-icon">${service === 'calendar' ? '&#9201;' : '&#9997;'}</div>
394
- <div class="feature-text"><h3>${serviceFeatures[1][0]}</h3><p>${serviceFeatures[1][1]}</p></div>
395
- </div>
396
- <div class="feature">
397
- <div class="feature-icon">${service === 'calendar' ? '&#128276;' : '&#128203;'}</div>
398
- <div class="feature-text"><h3>${serviceFeatures[2][0]}</h3><p>${serviceFeatures[2][1]}</p></div>
399
- </div>
400
- </div>
401
- <hr class="divider" />
422
+ <div class="service-label">What Claudia can do</div>
423
+ <div class="features">${featuresHtml}</div>
424
+ <hr />
402
425
  <div class="footer">
403
- <div class="privacy">&#128274; Your tokens are stored locally and never leave your machine.</div>
404
- <div class="close">You can close this tab and return to your terminal.</div>
426
+ <div>&#128274; Tokens stored locally. They never leave your machine.</div>
405
427
  </div>
406
428
  ` : `
407
- <div class="footer" style="margin-top:1rem;">
408
- <div class="close">Check your terminal for details, then try again.</div>
429
+ <div class="footer" style="margin-top:0.5rem;">
430
+ <div>Check your terminal for details, then try again.</div>
409
431
  </div>
410
432
  `}
411
433
  </div>
package/cli/index.js CHANGED
@@ -570,7 +570,7 @@ gmail
570
570
  // ── Calendar subcommand group ──
571
571
  const calendar = program
572
572
  .command('calendar')
573
- .description('Google Calendar integration (login, list events)');
573
+ .description('Google Calendar integration (login, list, search, read)');
574
574
 
575
575
  calendar
576
576
  .command('login')
@@ -598,6 +598,27 @@ calendar
598
598
  await calendarListCommand(opts);
599
599
  });
600
600
 
601
+ calendar
602
+ .command('search')
603
+ .description('Search calendar events by text')
604
+ .argument('<query>', 'Search query, e.g. "meeting" or "lunch"')
605
+ .option('--days <n>', 'Days ahead to search', parseInt, 90)
606
+ .option('--limit <n>', 'Max results', parseInt, 25)
607
+ .option('--past', 'Also search past events within range')
608
+ .action(async (query, opts) => {
609
+ const { calendarSearchCommand } = await import('./commands/google-auth.js');
610
+ await calendarSearchCommand(query, opts);
611
+ });
612
+
613
+ calendar
614
+ .command('read')
615
+ .description('Read a specific calendar event by ID')
616
+ .argument('<eventId>', 'Calendar event ID')
617
+ .action(async (eventId) => {
618
+ const { calendarReadCommand } = await import('./commands/google-auth.js');
619
+ await calendarReadCommand(eventId);
620
+ });
621
+
601
622
  calendar
602
623
  .command('logout')
603
624
  .description('Sign out of Calendar (remove stored tokens)')
@@ -606,14 +627,34 @@ calendar
606
627
  await calendarLogoutCommand();
607
628
  });
608
629
 
609
- // ── Google status (combined) ──
610
- program
611
- .command('google-status')
630
+ // ── Unified Google command group ──
631
+ const google = program
632
+ .command('google')
633
+ .description('Google services (login, status, logout for Gmail + Calendar)');
634
+
635
+ google
636
+ .command('login')
637
+ .description('Sign in once for both Gmail and Calendar')
638
+ .action(async () => {
639
+ const { googleLoginCommand } = await import('./commands/google-auth.js');
640
+ await googleLoginCommand();
641
+ });
642
+
643
+ google
644
+ .command('status')
612
645
  .description('Show connection status for all Google services')
613
646
  .action(async () => {
614
647
  const { googleStatusCommand } = await import('./commands/google-auth.js');
615
648
  await googleStatusCommand();
616
649
  });
617
650
 
651
+ google
652
+ .command('logout')
653
+ .description('Sign out of all Google services')
654
+ .action(async () => {
655
+ const { googleLogoutCommand } = await import('./commands/google-auth.js');
656
+ await googleLogoutCommand();
657
+ });
658
+
618
659
  // Parse and execute
619
660
  program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-claudia",
3
- "version": "1.51.1",
3
+ "version": "1.51.3",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",
@@ -317,6 +317,27 @@ I adapt to whatever tools are available. When you ask me to do something that ne
317
317
 
318
318
  **Obsidian vault:** My memory syncs to an Obsidian vault at `~/.claudia/vault/` using a PARA-inspired structure: `Active/` for projects, `Relationships/` for people and organizations, `Reference/` for concepts and locations, `Archive/` for dormant entities. Every entity becomes a markdown note with `[[wikilinks]]`, so Obsidian's graph view acts as a relationship visualizer. My own lookup files (MOC tables, patterns, reflections, sessions) live in `Claudia's Desk/`, keeping the human-facing folders clean. The vault syncs on-demand via `claudia vault sync`. SQLite remains the source of truth; the vault is a read projection.
319
319
 
320
+ **Gmail and Calendar CLI:** These are shell commands run via the Bash tool, not prompts to interpret. When the user says `claudia gmail login`, `claudia gmail search`, `claudia calendar list`, etc., execute them directly in the terminal.
321
+
322
+ | Command | What it does |
323
+ |---------|-------------|
324
+ | `claudia google login` | Sign in once for both Gmail + Calendar |
325
+ | `claudia google status` | Show connection status for all services |
326
+ | `claudia google logout` | Disconnect all Google services |
327
+ | `claudia gmail login` | Opens browser for Gmail-only OAuth |
328
+ | `claudia gmail status` | Check if Gmail is connected |
329
+ | `claudia gmail search "<query>"` | Search emails (Gmail search syntax) |
330
+ | `claudia gmail read <messageId>` | Read a specific email |
331
+ | `claudia gmail logout` | Disconnect Gmail, remove tokens |
332
+ | `claudia calendar login` | Opens browser for Calendar-only OAuth |
333
+ | `claudia calendar status` | Check if Calendar is connected |
334
+ | `claudia calendar list` | Show upcoming events |
335
+ | `claudia calendar search "<query>"` | Search events by text |
336
+ | `claudia calendar read <eventId>` | Read a specific event by ID |
337
+ | `claudia calendar logout` | Disconnect Calendar, remove tokens |
338
+
339
+ These are **real CLI commands**, not questions. Always run them via the Bash tool. Tokens are stored locally at `~/.claudia/tokens/`.
340
+
320
341
  **External integrations** (Gmail, Google Calendar, Brave Search) are optional add-ons that extend what I can see and do. I work fully without them. The core value is relationships and context.
321
342
 
322
343
  ---