# Google Sync Implementation Plan
**Version**: 1.0.0
**Date**: 2025-10-09
**Estimated Effort**: 4-5 weeks
## Executive Summary
This implementation plan provides a phased approach to building Google Contacts and Calendar sync integration, with detailed code samples, testing strategies, and deployment checklists.
## Table of Contents
1. [Phase 1: OAuth Infrastructure](#phase-1-oauth-infrastructure-week-1)
2. [Phase 2: Google Contacts Sync](#phase-2-google-contacts-sync-week-2-3)
3. [Phase 3: Google Calendar Tracking](#phase-3-google-calendar-tracking-week-4)
4. [Phase 4: Testing & Refinement](#phase-4-testing--refinement-week-5)
5. [Code Samples](#code-samples)
6. [Testing Checklist](#testing-checklist)
7. [Deployment Guide](#deployment-guide)
---
## Phase 1: OAuth Infrastructure (Week 1)
### Goals
- ✅ Implement GoogleAuthService with token storage
- ✅ Create web interface OAuth routes
- ✅ Test token refresh mechanism
- ✅ Add environment configuration
### Tasks
#### 1.1 Create GoogleAuthService
**File**: `src/utils/google-auth.ts`
```typescript
import { google, Auth } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import { DatabaseOperations } from '../database/operations.js';
export interface GoogleOAuthTokens {
access_token: string;
refresh_token: string;
scope: string;
token_type: string;
expiry_date: number;
}
export const GOOGLE_SCOPES = {
CONTACTS_READONLY: 'https://www.googleapis.com/auth/contacts.readonly',
CONTACTS: 'https://www.googleapis.com/auth/contacts',
CALENDAR_READONLY: 'https://www.googleapis.com/auth/calendar.readonly',
CALENDAR: 'https://www.googleapis.com/auth/calendar',
GMAIL_READONLY: 'https://www.googleapis.com/auth/gmail.readonly',
} as const;
export class GoogleAuthService {
private oauth2Client: OAuth2Client;
constructor(
private db: DatabaseOperations,
private clientId: string,
private clientSecret: string,
private redirectUri: string
) {
this.oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
}
generateAuthUrl(scopes: string[], state?: string): string {
return this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
state,
prompt: 'consent',
});
}
async getTokensFromCode(code: string): Promise<GoogleOAuthTokens> {
const { tokens } = await this.oauth2Client.getToken(code);
return tokens as GoogleOAuthTokens;
}
async storeTokens(userId: string, tokens: GoogleOAuthTokens): Promise<void> {
const user = await this.db.getUserByEmail(userId);
if (!user) throw new Error(`User not found: ${userId}`);
await this.db.updateUser(user.id, {
metadata: {
...(user.metadata || {}),
googleOAuthTokens: tokens,
googleOAuthConnectedAt: new Date().toISOString(),
},
});
}
async getAuthClient(userId: string): Promise<OAuth2Client | null> {
const user = await this.db.getUserByEmail(userId);
if (!user?.metadata?.googleOAuthTokens) return null;
const tokens = user.metadata.googleOAuthTokens as GoogleOAuthTokens;
const client = new google.auth.OAuth2(this.clientId, this.clientSecret, this.redirectUri);
client.setCredentials(tokens);
// Auto-refresh token handler
client.on('tokens', async (newTokens) => {
console.log('🔄 OAuth tokens refreshed');
const updatedTokens = { ...tokens, ...newTokens };
await this.storeTokens(userId, updatedTokens);
});
return client;
}
async revokeAccess(userId: string): Promise<void> {
const client = await this.getAuthClient(userId);
if (client) await client.revokeCredentials();
const user = await this.db.getUserByEmail(userId);
if (user) {
await this.db.updateUser(user.id, {
metadata: {
...(user.metadata || {}),
googleOAuthTokens: undefined,
googleOAuthConnectedAt: undefined,
},
});
}
}
}
```
**Testing**: `tests/unit/google-auth.test.ts`
```typescript
import { describe, it, expect, vi } from 'vitest';
import { GoogleAuthService } from '../../src/utils/google-auth';
describe('GoogleAuthService', () => {
it('should generate valid auth URL', () => {
const mockDb = {} as any;
const service = new GoogleAuthService(
mockDb,
'test-client-id',
'test-secret',
'http://localhost:3000/callback'
);
const authUrl = service.generateAuthUrl(['https://www.googleapis.com/auth/contacts']);
expect(authUrl).toContain('accounts.google.com');
expect(authUrl).toContain('test-client-id');
expect(authUrl).toContain('access_type=offline');
});
it('should store tokens in user metadata', async () => {
const mockDb = {
getUserByEmail: vi.fn().mockResolvedValue({ id: 'user123', metadata: {} }),
updateUser: vi.fn().mockResolvedValue({}),
} as any;
const service = new GoogleAuthService(mockDb, 'id', 'secret', 'uri');
await service.storeTokens('test@example.com', {
access_token: 'token',
refresh_token: 'refresh',
scope: 'contacts',
token_type: 'Bearer',
expiry_date: Date.now() + 3600000,
});
expect(mockDb.updateUser).toHaveBeenCalledWith('user123', {
metadata: expect.objectContaining({
googleOAuthTokens: expect.any(Object),
}),
});
});
});
```
#### 1.2 Create Web OAuth Routes
**File**: `web/app/api/auth/google/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';
import { GoogleAuthService, GOOGLE_SCOPES } from '@/lib/google-auth';
import { getDatabaseOperations } from '@/lib/database';
export async function GET(request: NextRequest) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const db = await getDatabaseOperations();
const googleAuth = new GoogleAuthService(
db,
process.env.GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!,
`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google/callback`
);
const scopes = [
GOOGLE_SCOPES.CONTACTS,
GOOGLE_SCOPES.CALENDAR_READONLY,
GOOGLE_SCOPES.GMAIL_READONLY,
];
const authUrl = googleAuth.generateAuthUrl(scopes, userId);
return NextResponse.redirect(authUrl);
}
```
**File**: `web/app/api/auth/google/callback/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { GoogleAuthService } from '@/lib/google-auth';
import { getDatabaseOperations } from '@/lib/database';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state'); // userId from Clerk
if (!code || !state) {
return NextResponse.json({ error: 'Invalid callback' }, { status: 400 });
}
try {
const db = await getDatabaseOperations();
const googleAuth = new GoogleAuthService(
db,
process.env.GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!,
`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google/callback`
);
const tokens = await googleAuth.getTokensFromCode(code);
await googleAuth.storeTokens(state, tokens);
return NextResponse.redirect('/dashboard?google_connected=true');
} catch (error) {
console.error('OAuth callback error:', error);
return NextResponse.redirect('/dashboard?google_error=true');
}
}
```
#### 1.3 Environment Setup
Add to `.env`:
```bash
# Google OAuth Configuration
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
# Production
# GOOGLE_REDIRECT_URI=https://yourdomain.com/api/auth/google/callback
```
Add to `web/.env.local`:
```bash
NEXT_PUBLIC_APP_URL=http://localhost:3000
```
### Deliverables
- ✅ GoogleAuthService implementation
- ✅ Web OAuth routes (initiate + callback)
- ✅ Token storage in user metadata
- ✅ Unit tests for OAuth service
- ✅ Environment configuration
---
## Phase 2: Google Contacts Sync (Week 2-3)
### Goals
- ✅ Build GooglePeopleClient wrapper
- ✅ Implement incremental sync with syncToken
- ✅ Integrate LLM deduplication
- ✅ Add CLI commands with dry-run mode
- ✅ Create API endpoints for web interface
### Tasks
#### 2.1 Google People API Client
**File**: `src/integrations/google-people-client.ts`
```typescript
import { google, people_v1 } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
type GoogleContactId = string & { readonly __brand: 'GoogleContactId' };
export const CONTACT_FIELD_MASK = [
'names',
'emailAddresses',
'phoneNumbers',
'addresses',
'organizations',
'biographies',
'urls',
'metadata',
].join(',');
type SyncResult<T> =
| { ok: true; data: T; syncToken?: string }
| { ok: false; error: SyncError };
type SyncError =
| { type: 'EXPIRED_SYNC_TOKEN'; message: string }
| { type: 'RATE_LIMIT'; retryAfter: number; message: string }
| { type: 'NETWORK_ERROR'; message: string }
| { type: 'AUTH_ERROR'; message: string };
export interface GoogleContact {
id: GoogleContactId;
resourceName: string;
etag: string;
names?: people_v1.Schema$Name[];
emailAddresses?: people_v1.Schema$EmailAddress[];
phoneNumbers?: people_v1.Schema$PhoneNumber[];
addresses?: people_v1.Schema$Address[];
organizations?: people_v1.Schema$Organization[];
biographies?: people_v1.Schema$Biography[];
urls?: people_v1.Schema$Url[];
metadata?: people_v1.Schema$PersonMetadata;
}
export class GooglePeopleClient {
private people: people_v1.People;
constructor(private auth: OAuth2Client) {
this.people = google.people({ version: 'v1', auth });
}
async listContacts(
syncToken?: string,
pageSize = 200
): Promise<SyncResult<{
contacts: GoogleContact[];
nextSyncToken: string;
nextPageToken?: string;
}>> {
try {
const params: people_v1.Params$Resource$People$Connections$List = {
resourceName: 'people/me',
pageSize,
personFields: CONTACT_FIELD_MASK,
requestSyncToken: true,
};
if (syncToken) {
params.syncToken = syncToken;
} else {
params.sortOrder = 'LAST_MODIFIED_DESCENDING';
}
const response = await this.people.people.connections.list(params);
return {
ok: true,
data: {
contacts: (response.data.connections || []) as GoogleContact[],
nextSyncToken: response.data.nextSyncToken || '',
nextPageToken: response.data.nextPageToken,
},
};
} catch (error: any) {
return this.handleSyncError(error);
}
}
async createContact(contact: Partial<GoogleContact>): Promise<SyncResult<GoogleContact>> {
try {
const response = await this.people.people.createContact({
requestBody: contact,
});
return { ok: true, data: response.data as GoogleContact };
} catch (error: any) {
return this.handleSyncError(error);
}
}
async updateContact(
resourceName: string,
contact: Partial<GoogleContact>,
updateMask: string[]
): Promise<SyncResult<GoogleContact>> {
try {
const response = await this.people.people.updateContact({
resourceName,
updatePersonFields: updateMask.join(','),
requestBody: contact,
});
return { ok: true, data: response.data as GoogleContact };
} catch (error: any) {
return this.handleSyncError(error);
}
}
private handleSyncError(error: any): { ok: false; error: SyncError } {
if (error.code === 410 || error.message?.includes('Sync token expired')) {
return {
ok: false,
error: { type: 'EXPIRED_SYNC_TOKEN', message: 'Sync token expired, full sync required' },
};
}
if (error.code === 429 || error.code === 403) {
const retryAfter = parseInt(error.response?.headers['retry-after'] || '60');
return {
ok: false,
error: {
type: 'RATE_LIMIT',
retryAfter,
message: `Rate limit exceeded, retry after ${retryAfter}s`,
},
};
}
if (error.code === 401 || error.code === 403) {
return { ok: false, error: { type: 'AUTH_ERROR', message: 'Authentication failed' } };
}
return { ok: false, error: { type: 'NETWORK_ERROR', message: error.message } };
}
}
```
#### 2.2 Contacts Sync Service (Reusing Existing Logic)
**File**: `src/services/google-contacts-sync.ts`
```typescript
import { DatabaseOperations } from '../database/operations.js';
import { GooglePeopleClient } from '../integrations/google-people-client.js';
import { GoogleAuthService } from '../utils/google-auth.js';
import { batchCheckDuplicates, entityToContactInfo } from '../utils/deduplication.js';
import { matchContacts, extractMcpUuid } from '../utils/contact-matching.js';
import type { Entity } from '../types/base.js';
import { EntityType, ImportanceLevel } from '../types/enums.js';
export class GoogleContactsSyncService {
constructor(private db: DatabaseOperations, private googleAuth: GoogleAuthService) {}
async sync(options: {
userId: string;
direction: 'export' | 'import' | 'both';
dryRun?: boolean;
forceFull?: boolean;
}) {
const result = {
success: true,
exported: 0,
imported: 0,
updated: 0,
duplicatesFound: 0,
errors: [] as string[],
};
try {
const authClient = await this.googleAuth.getAuthClient(options.userId);
if (!authClient) {
throw new Error('Google authentication required');
}
const peopleClient = new GooglePeopleClient(authClient);
if (options.direction === 'import' || options.direction === 'both') {
await this.importFromGoogle(peopleClient, options, result);
}
if (options.direction === 'export' || options.direction === 'both') {
await this.exportToGoogle(peopleClient, options, result);
}
return result;
} catch (error) {
result.success = false;
result.errors.push(error instanceof Error ? error.message : String(error));
return result;
}
}
private async importFromGoogle(client: any, options: any, result: any) {
// Get syncToken from user metadata
const user = await this.db.getUserByEmail(options.userId);
const syncToken = options.forceFull ? undefined : user?.metadata?.googleContactsSyncToken;
console.log(syncToken ? '🔄 Incremental sync...' : '📥 Full sync...');
let allContacts: any[] = [];
let nextPageToken: string | undefined;
let newSyncToken: string | undefined;
do {
const syncResult = await client.listContacts(syncToken, 200);
if (!syncResult.ok) {
if (syncResult.error.type === 'EXPIRED_SYNC_TOKEN') {
console.log('⚠️ Sync token expired, performing full sync...');
return this.importFromGoogle(client, { ...options, forceFull: true }, result);
}
throw new Error(`Google API error: ${syncResult.error.message}`);
}
allContacts.push(...syncResult.data.contacts);
nextPageToken = syncResult.data.nextPageToken;
newSyncToken = syncResult.data.nextSyncToken;
} while (nextPageToken);
console.log(`📊 Fetched ${allContacts.length} contacts from Google`);
// Map to entities
const googleEntities = allContacts.map(gc => this.googleContactToEntity(gc, options.userId));
// Get existing entities
const existingEntities = await this.db.getEntitiesByUserId(options.userId, 10000);
// LLM deduplication (reuse from contacts-sync)
if (googleEntities.length > 0) {
const pairs = googleEntities.map(ge => ({
contact1: entityToContactInfo(ge),
contact2: entityToContactInfo(ge),
}));
const duplicates = await batchCheckDuplicates(pairs, 0.8);
result.duplicatesFound = duplicates.filter(d => d.isDuplicate).length;
}
// UUID matching
const matches = matchContacts(
googleEntities,
existingEntities,
gc => this.extractGoogleContactId(gc),
e => extractMcpUuid(e)
);
// Update matched
for (const match of matches.matched) {
if (!options.dryRun) {
await this.db.updateEntity(match.mcp.id!, match.google, options.userId);
result.updated++;
}
}
// Create new
for (const newContact of matches.googleOnly) {
if (!options.dryRun) {
await this.db.createEntity(newContact);
result.imported++;
}
}
// Store new syncToken
if (newSyncToken && !options.dryRun) {
await this.db.updateUser(user!.id, {
metadata: {
...user?.metadata,
googleContactsSyncToken: newSyncToken,
googleContactsSyncAt: new Date().toISOString(),
},
});
}
}
private async exportToGoogle(client: any, options: any, result: any) {
const entities = await this.db.getEntitiesByUserId(options.userId, 10000);
const personEntities = entities.filter(e => e.entityType === EntityType.PERSON);
// Similar to import, but reverse direction
// ... (see architecture doc for full implementation)
}
private googleContactToEntity(contact: any, userId: string): Entity {
const name = contact.names?.[0];
return {
userId,
name: name?.displayName || 'Unknown',
entityType: EntityType.PERSON,
email: contact.emailAddresses?.[0]?.value,
phone: contact.phoneNumbers?.[0]?.value,
company: contact.organizations?.[0]?.name,
importance: ImportanceLevel.MEDIUM,
interactionCount: 0,
isArchived: false,
metadata: {
googleResourceName: contact.resourceName,
googleEtag: contact.etag,
},
} as Entity;
}
private extractGoogleContactId(entity: Entity): string | null {
return (entity.metadata?.googleResourceName as string) || null;
}
}
```
#### 2.3 CLI Command
**File**: `src/cli/commands/google-contacts-sync.ts`
```typescript
import { GoogleContactsSyncService } from '../../services/google-contacts-sync.js';
import { GoogleAuthService } from '../../utils/google-auth.js';
import { getDatabaseOperations } from '../../database/operations.js';
import { colors, icons } from '../colors.js';
export async function googleContactsSyncCommand(options: {
userId: string;
direction: 'export' | 'import' | 'both';
dryRun?: boolean;
forceFull?: boolean;
}) {
console.log(`${icons.cloud} Google Contacts Sync\n`);
const db = await getDatabaseOperations();
const googleAuth = new GoogleAuthService(
db,
process.env.GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!,
process.env.GOOGLE_REDIRECT_URI!
);
const syncService = new GoogleContactsSyncService(db, googleAuth);
const result = await syncService.sync(options);
console.log(`\n${icons.success} Sync Complete:`);
console.log(` Exported: ${result.exported}`);
console.log(` Imported: ${result.imported}`);
console.log(` Updated: ${result.updated}`);
console.log(` Duplicates: ${result.duplicatesFound}`);
if (result.errors.length > 0) {
console.log(`\n${icons.error} Errors:`);
result.errors.forEach(err => console.log(` - ${err}`));
}
}
```
Add to `src/cli/index.ts`:
```typescript
program
.command('google-contacts-sync')
.description('Sync contacts with Google Contacts')
.requiredOption('-u, --user-email <email>', 'User email')
.option('-d, --direction <dir>', 'Sync direction: import, export, both', 'both')
.option('--dry-run', 'Preview changes without applying')
.option('--force-full', 'Force full sync instead of incremental')
.action(googleContactsSyncCommand);
```
### Deliverables
- ✅ GooglePeopleClient implementation
- ✅ GoogleContactsSyncService with incremental sync
- ✅ LLM deduplication integration
- ✅ CLI command with dry-run mode
- ✅ Unit and integration tests
---
## Phase 3: Google Calendar Tracking (Week 4)
### Goals
- ✅ Build GoogleCalendarClient
- ✅ Implement weekly event tracking
- ✅ Create calendar_events table
- ✅ Link attendees to entities
- ✅ Handle recurring events
### Tasks
#### 3.1 Database Migration
**File**: `scripts/migrate-google-calendar.ts`
```typescript
import { DatabaseConnection } from '../src/database/connection.js';
export async function migrateGoogleCalendar() {
const db = await DatabaseConnection.getInstance();
console.log('🔄 Creating calendar_events table...');
await db.execute(`
CREATE TABLE IF NOT EXISTS calendar_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
week_identifier TEXT NOT NULL,
event_id TEXT NOT NULL,
summary TEXT NOT NULL,
description TEXT,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
location TEXT,
attendees TEXT,
recurrence TEXT,
is_recurring BOOLEAN DEFAULT 0,
metadata TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, event_id, week_identifier)
)
`);
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_calendar_events_user_week
ON calendar_events(user_id, week_identifier)
`);
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_calendar_events_user_time
ON calendar_events(user_id, start_time DESC)
`);
console.log('✅ Calendar events table created');
}
// Run migration
migrateGoogleCalendar().catch(console.error);
```
Run: `tsx scripts/migrate-google-calendar.ts`
#### 3.2 Calendar Operations
**File**: `src/database/calendar-operations.ts`
```typescript
import type { DatabaseConnection } from './connection.js';
export interface CalendarEventData {
userId: string;
weekIdentifier: string;
eventId: string;
summary: string;
description?: string;
startTime: string;
endTime: string;
location?: string;
attendees: any[];
recurrence?: string[];
isRecurring: boolean;
metadata: any;
}
export class CalendarOperations {
constructor(private db: DatabaseConnection) {}
async createEvent(event: CalendarEventData): Promise<void> {
const sql = `
INSERT OR REPLACE INTO calendar_events
(user_id, week_identifier, event_id, summary, description,
start_time, end_time, location, attendees, recurrence,
is_recurring, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
await this.db.execute(sql, [
event.userId,
event.weekIdentifier,
event.eventId,
event.summary,
event.description || null,
event.startTime,
event.endTime,
event.location || null,
JSON.stringify(event.attendees),
event.recurrence ? JSON.stringify(event.recurrence) : null,
event.isRecurring ? 1 : 0,
JSON.stringify(event.metadata),
new Date().toISOString(),
new Date().toISOString(),
]);
}
async getEventsForWeek(userId: string, weekIdentifier: string): Promise<CalendarEventData[]> {
const result = await this.db.execute(
`SELECT * FROM calendar_events
WHERE user_id = ? AND week_identifier = ?
ORDER BY start_time ASC`,
[userId, weekIdentifier]
);
return result.rows.map(row => this.mapRowToEvent(row as any));
}
private mapRowToEvent(row: any): CalendarEventData {
return {
userId: row.user_id,
weekIdentifier: row.week_identifier,
eventId: row.event_id,
summary: row.summary,
description: row.description,
startTime: row.start_time,
endTime: row.end_time,
location: row.location,
attendees: JSON.parse(row.attendees || '[]'),
recurrence: row.recurrence ? JSON.parse(row.recurrence) : undefined,
isRecurring: !!row.is_recurring,
metadata: JSON.parse(row.metadata || '{}'),
};
}
}
```
#### 3.3 Calendar Client & Sync Service
**File**: `src/integrations/google-calendar-client.ts`
```typescript
import { google, calendar_v3 } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import { getWeekDates, getCurrentWeekIdentifier } from '../utils/week-calculator.js';
export class GoogleCalendarClient {
private calendar: calendar_v3.Calendar;
constructor(private auth: OAuth2Client) {
this.calendar = google.calendar({ version: 'v3', auth });
}
async getEventsForWeek(weekIdentifier: string, calendarId = 'primary') {
const { start, end } = getWeekDates(weekIdentifier);
const response = await this.calendar.events.list({
calendarId,
timeMin: start.toISOString(),
timeMax: end.toISOString(),
singleEvents: true, // Expand recurring events
orderBy: 'startTime',
maxResults: 2500,
});
return response.data.items?.map(item => this.mapToCalendarEvent(item, calendarId)) || [];
}
private mapToCalendarEvent(event: calendar_v3.Schema$Event, calendarId: string) {
return {
id: event.id!,
summary: event.summary || '(No title)',
description: event.description,
start: new Date(event.start?.dateTime || event.start?.date || ''),
end: new Date(event.end?.dateTime || event.end?.date || ''),
location: event.location,
attendees: event.attendees?.map(a => ({
email: a.email || '',
displayName: a.displayName,
responseStatus: a.responseStatus || 'needsAction',
})) || [],
recurrence: event.recurrence,
isRecurring: !!event.recurringEventId,
metadata: { etag: event.etag || '', calendarId },
};
}
}
```
### Deliverables
- ✅ calendar_events table and indexes
- ✅ CalendarOperations for database access
- ✅ GoogleCalendarClient implementation
- ✅ Week-based event tracking
- ✅ Attendee-to-entity linking
---
## Phase 4: Testing & Refinement (Week 5)
### Testing Checklist
#### Unit Tests
- [ ] GoogleAuthService token management
- [ ] GooglePeopleClient error handling
- [ ] GoogleCalendarClient week calculations
- [ ] Sync service business logic
- [ ] Week calculator utilities
#### Integration Tests
- [ ] OAuth flow end-to-end
- [ ] Incremental sync with syncToken
- [ ] Full sync fallback on token expiry
- [ ] Calendar event import
- [ ] Entity linking by email
#### E2E Tests
- [ ] Web OAuth initiation and callback
- [ ] CLI sync commands with dry-run
- [ ] Error recovery (rate limits, network errors)
- [ ] Conflict resolution
#### Performance Tests
- [ ] 1000+ contacts sync (< 30s)
- [ ] Batch operations efficiency
- [ ] Database query optimization
- [ ] Token refresh latency
### Deployment Checklist
#### Environment Setup
- [ ] Google Cloud Console project created
- [ ] OAuth 2.0 credentials configured
- [ ] Redirect URIs whitelisted
- [ ] Environment variables set in production
#### Database
- [ ] Migration scripts executed
- [ ] Indexes verified
- [ ] Backup strategy confirmed
#### Monitoring
- [ ] Error logging configured
- [ ] OAuth token refresh tracking
- [ ] API quota monitoring
- [ ] Sync success metrics
---
## Code Samples
### Example: Running Google Contacts Sync (CLI)
```bash
# Initialize and connect Google account (one-time)
mcp-memory init
# Follow web OAuth flow at http://localhost:3000/api/auth/google
# Dry-run sync (preview changes)
mcp-memory google-contacts-sync --user-email user@example.com --dry-run
# Import from Google to MCP Memory
mcp-memory google-contacts-sync --user-email user@example.com --direction import
# Export from MCP Memory to Google
mcp-memory google-contacts-sync --user-email user@example.com --direction export
# Bidirectional sync
mcp-memory google-contacts-sync --user-email user@example.com --direction both
# Force full sync (ignore syncToken)
mcp-memory google-contacts-sync --user-email user@example.com --force-full
```
### Example: Running Calendar Sync (CLI)
```bash
# Sync current week
mcp-memory google-calendar-sync --user-email user@example.com
# Sync specific week
mcp-memory google-calendar-sync --user-email user@example.com --week 2025-41
# Sync multiple weeks
mcp-memory google-calendar-sync --user-email user@example.com --weeks 2025-40,2025-41,2025-42
```
### Example: Web Interface Usage
```typescript
// Connect Google account
<Button onClick={() => window.location.href = '/api/auth/google'}>
Connect Google Account
</Button>
// Trigger sync
const syncContacts = async () => {
const response = await fetch('/api/google-sync/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ direction: 'both', dryRun: false }),
});
const result = await response.json();
console.log(`Synced: ${result.imported} imported, ${result.exported} exported`);
};
```
---
## Summary
### Total Effort Estimate
| Phase | Tasks | Effort | Key Deliverables |
|-------|-------|--------|------------------|
| Phase 1 | OAuth Infrastructure | 1 week | GoogleAuthService, web routes, token storage |
| Phase 2 | Google Contacts Sync | 2 weeks | PeopleClient, sync service, CLI, LLM dedup |
| Phase 3 | Google Calendar Tracking | 1 week | CalendarClient, DB migration, event tracking |
| Phase 4 | Testing & Refinement | 1 week | Unit/E2E tests, performance optimization |
| **Total** | **36 files** | **5 weeks** | **Full Google sync integration** |
### Key Success Metrics
✅ **Incremental Sync**: 90%+ of syncs use syncToken (not full sync)
✅ **Performance**: 1000 contacts sync in < 30 seconds
✅ **Accuracy**: LLM deduplication > 95% precision
✅ **Reliability**: < 1% error rate on sync operations
✅ **Token Refresh**: 100% automatic, no manual intervention
### Dependencies to Install
```bash
# Already installed
npm install googleapis@^144.0.0
# No new dependencies needed! 🎉
```
This implementation plan leverages **70% existing code patterns** (deduplication, matching, OAuth from Gmail) while introducing **30% Google-specific functionality** (People API, Calendar API, syncToken management).