get-claudia 1.51.2 → 1.51.4

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
@@ -413,6 +413,9 @@ async function main() {
413
413
  console.log(` ${colors.green}✓${colors.reset} Framework updated (data preserved)`);
414
414
  }
415
415
 
416
+ // Disable Gmail/Calendar MCP servers (Claudia has native CLI commands now)
417
+ disableGoogleMcpServers(targetPath);
418
+
416
419
  // Write context/whats-new.md for Claudia's self-awareness (silent)
417
420
  writeWhatsNewFile(targetPath, version);
418
421
 
@@ -811,12 +814,45 @@ async function main() {
811
814
 
812
815
  console.log('');
813
816
  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}`);
817
+ console.log(` ${colors.cyan}claudia google login${colors.reset} ${colors.dim}Gmail + Calendar in one step${colors.reset}`);
816
818
  console.log('');
817
819
  }
818
820
  }
819
821
 
822
+ /**
823
+ * Disable Gmail and Google Calendar MCP servers in .mcp.json if present.
824
+ * Claudia now has native CLI commands for both services, so external MCP
825
+ * servers are no longer needed. This runs on both fresh installs and upgrades.
826
+ */
827
+ function disableGoogleMcpServers(targetPath) {
828
+ const mcpPath = join(targetPath, '.mcp.json');
829
+ if (!existsSync(mcpPath)) return;
830
+
831
+ try {
832
+ const raw = readFileSync(mcpPath, 'utf-8');
833
+ const config = JSON.parse(raw);
834
+ if (!config.mcpServers) return;
835
+
836
+ const googleKeys = ['gmail', 'google-calendar', 'google_calendar', 'googleCalendar'];
837
+ let changed = false;
838
+
839
+ for (const key of googleKeys) {
840
+ if (config.mcpServers[key] && !config.mcpServers[key]._disabled) {
841
+ config.mcpServers[key]._disabled = true;
842
+ config.mcpServers[key]._replaced_by = 'claudia google login';
843
+ changed = true;
844
+ }
845
+ }
846
+
847
+ if (changed) {
848
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
849
+ console.log(` ${colors.yellow}→${colors.reset} Disabled Gmail/Calendar MCP servers (replaced by ${colors.cyan}claudia google login${colors.reset})`);
850
+ }
851
+ } catch {
852
+ // Not valid JSON or can't read -- skip silently
853
+ }
854
+ }
855
+
820
856
  function installVisualizer() {
821
857
  const vizSrc = join(__dirname, '..', 'visualizer');
822
858
  if (!existsSync(vizSrc)) return;
@@ -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.2",
3
+ "version": "1.51.4",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",
@@ -1,36 +1,16 @@
1
1
  {
2
2
  "mcpServers": {
3
-
4
- "gmail": {
5
- "_disabled": true,
6
- "command": "npx",
7
- "args": ["-y", "@gongrzhe/server-gmail-autoauth-mcp"],
8
- "_setup": "Remove _disabled, restart Claude Code, authenticate when browser opens"
9
- },
10
-
11
- "google-calendar": {
12
- "_disabled": true,
13
- "command": "npx",
14
- "args": ["-y", "@modelcontextprotocol/server-google-calendar"],
15
- "_setup": "Remove _disabled, restart Claude Code, authenticate when browser opens"
16
- }
17
-
18
3
  },
19
4
 
20
5
  "_notes": {
21
- "quick_start": [
22
- "1. Rename this file to .mcp.json",
23
- "2. Remove _disabled from servers you want",
24
- "3. Restart Claude Code",
25
- "4. Gmail/Calendar will open a browser for you to authenticate with YOUR Google account"
26
- ],
27
- "memory": "Claudia's memory system now uses the `claudia` CLI (via Bash tool), not MCP. No MCP server needed for memory.",
6
+ "gmail_and_calendar": "Claudia has built-in Gmail and Calendar support. Run 'claudia google login' to connect. No MCP server needed.",
7
+ "memory": "Claudia's memory system uses the 'claudia' CLI (via Bash tool), not MCP. No MCP server needed for memory.",
28
8
  "security": "Each user authenticates with their own accounts. OAuth tokens are stored locally on your machine, never shared.",
29
9
  "not_included": {
30
10
  "filesystem": "Claude Code has native Read/Write/Edit tools",
31
11
  "web-search": "Claude Code has native WebSearch",
32
12
  "fetch": "Claude Code has native WebFetch"
33
13
  },
34
- "more_servers": "Find more at mcp.so or github.com/modelcontextprotocol/servers"
14
+ "adding_servers": "Add MCP servers here if needed. Find more at mcp.so or github.com/modelcontextprotocol/servers"
35
15
  }
36
16
  }
@@ -321,16 +321,20 @@ I adapt to whatever tools are available. When you ask me to do something that ne
321
321
 
322
322
  | Command | What it does |
323
323
  |---------|-------------|
324
- | `claudia gmail login` | Opens browser for Google OAuth sign-in |
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 |
325
328
  | `claudia gmail status` | Check if Gmail is connected |
326
329
  | `claudia gmail search "<query>"` | Search emails (Gmail search syntax) |
327
330
  | `claudia gmail read <messageId>` | Read a specific email |
328
331
  | `claudia gmail logout` | Disconnect Gmail, remove tokens |
329
- | `claudia calendar login` | Opens browser for Google Calendar OAuth |
332
+ | `claudia calendar login` | Opens browser for Calendar-only OAuth |
330
333
  | `claudia calendar status` | Check if Calendar is connected |
331
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 |
332
337
  | `claudia calendar logout` | Disconnect Calendar, remove tokens |
333
- | `claudia google-status` | Show status of all Google connections |
334
338
 
335
339
  These are **real CLI commands**, not questions. Always run them via the Bash tool. Tokens are stored locally at `~/.claudia/tokens/`.
336
340