mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +02:00
Integrate Google Calendar sync functionality with Composio, enhancing the connection handling in composio-handler and oauth-handler. Update onboarding modal and connectors-popover to manage connection states and provide user feedback during the sync process. Implement Composio-based event syncing in sync_calendar.ts.
This commit is contained in:
parent
71e2e43ed1
commit
bc54ef2177
7 changed files with 267 additions and 72 deletions
|
|
@ -5,6 +5,7 @@ import { composioAccountsRepo } from '@x/core/dist/composio/repo.js';
|
||||||
import type { LocalConnectedAccount, ZExecuteActionResponse } from '@x/core/dist/composio/types.js';
|
import type { LocalConnectedAccount, ZExecuteActionResponse } from '@x/core/dist/composio/types.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
|
import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
|
||||||
|
import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js';
|
||||||
|
|
||||||
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
|
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
|
||||||
|
|
||||||
|
|
@ -145,7 +146,11 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
|
||||||
|
|
||||||
// Set up callback server
|
// Set up callback server
|
||||||
let cleanupTimeout: NodeJS.Timeout;
|
let cleanupTimeout: NodeJS.Timeout;
|
||||||
|
let callbackHandled = false;
|
||||||
const { server } = await createAuthServer(8081, async (_code, _state) => {
|
const { server } = await createAuthServer(8081, async (_code, _state) => {
|
||||||
|
// Guard against duplicate callbacks (browser may send multiple requests)
|
||||||
|
if (callbackHandled) return;
|
||||||
|
callbackHandled = true;
|
||||||
// OAuth callback received - sync the account status
|
// OAuth callback received - sync the account status
|
||||||
try {
|
try {
|
||||||
const accountStatus = await composioClient.getConnectedAccount(connectedAccountId);
|
const accountStatus = await composioClient.getConnectedAccount(connectedAccountId);
|
||||||
|
|
@ -156,6 +161,9 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
|
||||||
if (toolkitSlug === 'gmail') {
|
if (toolkitSlug === 'gmail') {
|
||||||
triggerGmailSync();
|
triggerGmailSync();
|
||||||
}
|
}
|
||||||
|
if (toolkitSlug === 'googlecalendar') {
|
||||||
|
triggerCalendarSync();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emitComposioEvent({
|
emitComposioEvent({
|
||||||
toolkitSlug,
|
toolkitSlug,
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,11 @@ export async function connectProvider(provider: string, clientId?: string): Prom
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create callback server
|
// Create callback server
|
||||||
|
let callbackHandled = false;
|
||||||
const { server } = await createAuthServer(8080, async (code, receivedState) => {
|
const { server } = await createAuthServer(8080, async (code, receivedState) => {
|
||||||
|
// Guard against duplicate callbacks (browser may send multiple requests)
|
||||||
|
if (callbackHandled) return;
|
||||||
|
callbackHandled = true;
|
||||||
// Validate state
|
// Validate state
|
||||||
if (receivedState !== state) {
|
if (receivedState !== state) {
|
||||||
throw new Error('Invalid state parameter - possible CSRF attack');
|
throw new Error('Invalid state parameter - possible CSRF attack');
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,10 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
|
||||||
setGoogleCalendarConnecting(false)
|
setGoogleCalendarConnecting(false)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Connected to Google Calendar')
|
toast.success('Connected to Google Calendar', {
|
||||||
|
description: 'Syncing your calendar in the background. This may take a few minutes before changes appear.',
|
||||||
|
duration: 8000,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
toast.error(error || 'Failed to connect to Google Calendar')
|
toast.error(error || 'Failed to connect to Google Calendar')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -538,10 +538,10 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
}
|
}
|
||||||
}, [open, providers, refreshAllStatuses])
|
}, [open, providers, refreshAllStatuses])
|
||||||
|
|
||||||
// Listen for OAuth completion events
|
// Listen for OAuth completion events (state updates only — toasts handled by ConnectorsPopover)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
|
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
|
||||||
const { provider, success, error } = event
|
const { provider, success } = event
|
||||||
|
|
||||||
setProviderStates(prev => ({
|
setProviderStates(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -551,13 +551,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (success) {
|
|
||||||
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
|
|
||||||
toast.success(`Connected to ${displayName}`)
|
|
||||||
} else {
|
|
||||||
toast.error(error || `Failed to connect to ${provider}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return cleanup
|
return cleanup
|
||||||
|
|
@ -576,34 +569,19 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [onboardingPath, currentStep])
|
}, [onboardingPath, currentStep])
|
||||||
|
|
||||||
// Listen for Composio connection events (Gmail, Google Calendar)
|
// Listen for Composio connection events (state updates only — toasts handled by ConnectorsPopover)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
||||||
const { toolkitSlug, success, error } = event
|
const { toolkitSlug, success } = event
|
||||||
|
|
||||||
if (toolkitSlug === 'gmail') {
|
if (toolkitSlug === 'gmail') {
|
||||||
setGmailConnected(success)
|
setGmailConnected(success)
|
||||||
setGmailConnecting(false)
|
setGmailConnecting(false)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success('Connected to Gmail', {
|
|
||||||
description: 'Syncing your emails in the background. This may take a few minutes before changes appear.',
|
|
||||||
duration: 8000,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast.error(error || 'Failed to connect to Gmail')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolkitSlug === 'googlecalendar') {
|
if (toolkitSlug === 'googlecalendar') {
|
||||||
setGoogleCalendarConnected(success)
|
setGoogleCalendarConnected(success)
|
||||||
setGoogleCalendarConnecting(false)
|
setGoogleCalendarConnecting(false)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success('Connected to Google Calendar')
|
|
||||||
} else {
|
|
||||||
toast.error(error || 'Failed to connect to Google Calendar')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -510,10 +510,10 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
}
|
}
|
||||||
}, [open, providers, refreshAllStatuses])
|
}, [open, providers, refreshAllStatuses])
|
||||||
|
|
||||||
// Listen for OAuth completion events
|
// Listen for OAuth completion events (state updates only — toasts handled by ConnectorsPopover)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
|
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
|
||||||
const { provider, success, error } = event
|
const { provider, success } = event
|
||||||
|
|
||||||
setProviderStates(prev => ({
|
setProviderStates(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -523,13 +523,6 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (success) {
|
|
||||||
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
|
|
||||||
toast.success(`Connected to ${displayName}`)
|
|
||||||
} else {
|
|
||||||
toast.error(error || `Failed to connect to ${provider}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return cleanup
|
return cleanup
|
||||||
|
|
@ -559,41 +552,23 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [onboardingPath, currentStep])
|
}, [onboardingPath, currentStep])
|
||||||
|
|
||||||
// Listen for Composio connection events
|
// Listen for Composio connection events (state updates only — toasts handled by ConnectorsPopover)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
||||||
const { toolkitSlug, success, error } = event
|
const { toolkitSlug, success } = event
|
||||||
|
|
||||||
if (toolkitSlug === 'slack') {
|
if (toolkitSlug === 'slack') {
|
||||||
setSlackEnabled(success)
|
setSlackEnabled(success)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success('Connected to Slack')
|
|
||||||
} else {
|
|
||||||
toast.error(error || 'Failed to connect to Slack')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolkitSlug === 'gmail') {
|
if (toolkitSlug === 'gmail') {
|
||||||
setGmailConnected(success)
|
setGmailConnected(success)
|
||||||
setGmailConnecting(false)
|
setGmailConnecting(false)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success('Connected to Gmail')
|
|
||||||
} else {
|
|
||||||
toast.error(error || 'Failed to connect to Gmail')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolkitSlug === 'googlecalendar') {
|
if (toolkitSlug === 'googlecalendar') {
|
||||||
setGoogleCalendarConnected(success)
|
setGoogleCalendarConnected(success)
|
||||||
setGoogleCalendarConnecting(false)
|
setGoogleCalendarConnecting(false)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success('Connected to Google Calendar')
|
|
||||||
} else {
|
|
||||||
toast.error(error || 'Failed to connect to Google Calendar')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@ import { OAuth2Client } from 'google-auth-library';
|
||||||
import { NodeHtmlMarkdown } from 'node-html-markdown'
|
import { NodeHtmlMarkdown } from 'node-html-markdown'
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { GoogleClientFactory } from './google-client-factory.js';
|
import { GoogleClientFactory } from './google-client-factory.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger, type ServiceRunContext } from '../services/service_logger.js';
|
||||||
import { limitEventItems } from './limit_event_items.js';
|
import { limitEventItems } from './limit_event_items.js';
|
||||||
|
import { executeAction, useComposioForGoogleCalendar } from '../composio/client.js';
|
||||||
|
import { composioAccountsRepo } from '../composio/repo.js';
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
||||||
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
||||||
const LOOKBACK_DAYS = 14;
|
const LOOKBACK_DAYS = 14;
|
||||||
|
const COMPOSIO_LOOKBACK_DAYS = 14;
|
||||||
const REQUIRED_SCOPES = [
|
const REQUIRED_SCOPES = [
|
||||||
'https://www.googleapis.com/auth/calendar.events.readonly',
|
'https://www.googleapis.com/auth/calendar.events.readonly',
|
||||||
'https://www.googleapis.com/auth/drive.readonly'
|
'https://www.googleapis.com/auth/drive.readonly'
|
||||||
|
|
@ -56,7 +59,7 @@ function cleanUpOldFiles(currentEventIds: Set<string>, syncDir: string): string[
|
||||||
const files = fs.readdirSync(syncDir);
|
const files = fs.readdirSync(syncDir);
|
||||||
const deleted: string[] = [];
|
const deleted: string[] = [];
|
||||||
for (const filename of files) {
|
for (const filename of files) {
|
||||||
if (filename === 'sync_state.json') continue;
|
if (filename === 'sync_state.json' || filename === 'composio_state.json') continue;
|
||||||
|
|
||||||
// We expect files like:
|
// We expect files like:
|
||||||
// {eventId}.json
|
// {eventId}.json
|
||||||
|
|
@ -133,10 +136,10 @@ async function processAttachments(drive: drive.Drive, event: cal.Schema$Event, s
|
||||||
const filename = `${eventId}_doc_${safeTitle}.md`;
|
const filename = `${eventId}_doc_${safeTitle}.md`;
|
||||||
const filePath = path.join(syncDir, filename);
|
const filePath = path.join(syncDir, filename);
|
||||||
|
|
||||||
// Simple cache check: if file exists, skip.
|
// Simple cache check: if file exists, skip.
|
||||||
// Ideally we check modifiedTime, but that requires an extra API call per file.
|
// Ideally we check modifiedTime, but that requires an extra API call per file.
|
||||||
// Given the loop interval, we can just check existence to save quota.
|
// Given the loop interval, we can just check existence to save quota.
|
||||||
// If user updates notes, they might want them re-synced.
|
// If user updates notes, they might want them re-synced.
|
||||||
// For now, let's just check existence. To be smarter, we'd need a state file or check API.
|
// For now, let's just check existence. To be smarter, we'd need a state file or check API.
|
||||||
if (fs.existsSync(filePath)) continue;
|
if (fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
|
@ -343,20 +346,248 @@ async function performSync(syncDir: string, lookbackDays: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Composio-based Sync ---
|
||||||
|
|
||||||
|
interface ComposioCalendarState {
|
||||||
|
last_sync: string; // ISO string
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadComposioState(stateFile: string): ComposioCalendarState | null {
|
||||||
|
if (fs.existsSync(stateFile)) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
||||||
|
if (data.last_sync) {
|
||||||
|
return { last_sync: data.last_sync };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Calendar] Failed to load composio state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveComposioState(stateFile: string, lastSync: string): void {
|
||||||
|
fs.writeFileSync(stateFile, JSON.stringify({ last_sync: lastSync }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a Composio calendar event as JSON (same format used by Google OAuth path).
|
||||||
|
* The event data from Composio is already structured similarly to Google Calendar API.
|
||||||
|
*/
|
||||||
|
function saveComposioEvent(eventData: Record<string, unknown>, syncDir: string): { changed: boolean; isNew: boolean; title: string } {
|
||||||
|
const eventId = eventData.id as string | undefined;
|
||||||
|
if (!eventId) return { changed: false, isNew: false, title: 'Unknown' };
|
||||||
|
|
||||||
|
const filePath = path.join(syncDir, `${eventId}.json`);
|
||||||
|
const content = JSON.stringify(eventData, null, 2);
|
||||||
|
const exists = fs.existsSync(filePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (exists) {
|
||||||
|
const existing = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
if (existing === content) {
|
||||||
|
return { changed: false, isNew: false, title: (eventData.summary as string) || eventId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return { changed: true, isNew: !exists, title: (eventData.summary as string) || eventId };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Calendar] Error saving event ${eventId}:`, e);
|
||||||
|
return { changed: false, isNew: false, title: (eventData.summary as string) || eventId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performSyncComposio() {
|
||||||
|
const STATE_FILE = path.join(SYNC_DIR, 'composio_state.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(SYNC_DIR)) fs.mkdirSync(SYNC_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const account = composioAccountsRepo.getAccount('googlecalendar');
|
||||||
|
if (!account || account.status !== 'ACTIVE') {
|
||||||
|
console.log('[Calendar] Google Calendar not connected via Composio. Skipping sync.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedAccountId = account.id;
|
||||||
|
|
||||||
|
// Calculate time window: lookback + 14 days forward
|
||||||
|
const now = new Date();
|
||||||
|
const lookbackMs = COMPOSIO_LOOKBACK_DAYS * 24 * 60 * 60 * 1000;
|
||||||
|
const twoWeeksForwardMs = 14 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const timeMin = new Date(now.getTime() - lookbackMs).toISOString();
|
||||||
|
const timeMax = new Date(now.getTime() + twoWeeksForwardMs).toISOString();
|
||||||
|
|
||||||
|
console.log(`[Calendar] Syncing via Composio from ${timeMin} to ${timeMax} (lookback: ${COMPOSIO_LOOKBACK_DAYS} days)...`);
|
||||||
|
|
||||||
|
let run: ServiceRunContext | null = null;
|
||||||
|
const ensureRun = async (): Promise<ServiceRunContext> => {
|
||||||
|
if (!run) {
|
||||||
|
run = await serviceLogger.startRun({
|
||||||
|
service: 'calendar',
|
||||||
|
message: 'Syncing calendar (Composio)',
|
||||||
|
trigger: 'timer',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return run;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeAction(
|
||||||
|
'GOOGLECALENDAR_FIND_EVENT',
|
||||||
|
{
|
||||||
|
connected_account_id: connectedAccountId,
|
||||||
|
user_id: 'rowboat-user',
|
||||||
|
version: 'latest',
|
||||||
|
arguments: {
|
||||||
|
calendar_id: 'primary',
|
||||||
|
time_min: timeMin,
|
||||||
|
time_max: timeMax,
|
||||||
|
single_events: true,
|
||||||
|
order_by: 'startTime',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.successful || !result.data) {
|
||||||
|
console.error('[Calendar] Failed to list events via Composio:', result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.data as Record<string, unknown>;
|
||||||
|
// Composio may return events in different structures
|
||||||
|
let events: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
|
if (Array.isArray(data.items)) {
|
||||||
|
events = data.items as Array<Record<string, unknown>>;
|
||||||
|
} else if (Array.isArray(data.events)) {
|
||||||
|
events = data.events as Array<Record<string, unknown>>;
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
events = data as unknown as Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEventIds = new Set<string>();
|
||||||
|
let newCount = 0;
|
||||||
|
let updatedCount = 0;
|
||||||
|
const changedTitles: string[] = [];
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
console.log('[Calendar] No events found in this window.');
|
||||||
|
} else {
|
||||||
|
console.log(`[Calendar] Found ${events.length} events.`);
|
||||||
|
for (const event of events) {
|
||||||
|
const eventId = event.id as string | undefined;
|
||||||
|
if (eventId) {
|
||||||
|
const saveResult = saveComposioEvent(event, SYNC_DIR);
|
||||||
|
currentEventIds.add(eventId);
|
||||||
|
|
||||||
|
if (saveResult.changed) {
|
||||||
|
await ensureRun();
|
||||||
|
changedTitles.push(saveResult.title);
|
||||||
|
if (saveResult.isNew) {
|
||||||
|
newCount++;
|
||||||
|
} else {
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up events no longer in the window
|
||||||
|
const deletedFiles = cleanUpOldFiles(currentEventIds, SYNC_DIR);
|
||||||
|
let deletedCount = 0;
|
||||||
|
if (deletedFiles.length > 0) {
|
||||||
|
await ensureRun();
|
||||||
|
deletedCount = deletedFiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log results if any changes were detected (run was started by ensureRun)
|
||||||
|
if (run) {
|
||||||
|
const r = run as ServiceRunContext;
|
||||||
|
const totalChanges = newCount + updatedCount + deletedCount;
|
||||||
|
const limitedTitles = limitEventItems(changedTitles);
|
||||||
|
await serviceLogger.log({
|
||||||
|
type: 'changes_identified',
|
||||||
|
service: r.service,
|
||||||
|
runId: r.runId,
|
||||||
|
level: 'info',
|
||||||
|
message: `Calendar updates: ${totalChanges} change${totalChanges === 1 ? '' : 's'}`,
|
||||||
|
counts: {
|
||||||
|
newEvents: newCount,
|
||||||
|
updatedEvents: updatedCount,
|
||||||
|
deletedFiles: deletedCount,
|
||||||
|
},
|
||||||
|
items: limitedTitles.items,
|
||||||
|
truncated: limitedTitles.truncated,
|
||||||
|
});
|
||||||
|
await serviceLogger.log({
|
||||||
|
type: 'run_complete',
|
||||||
|
service: r.service,
|
||||||
|
runId: r.runId,
|
||||||
|
level: 'info',
|
||||||
|
message: `Calendar sync complete: ${totalChanges} change${totalChanges === 1 ? '' : 's'}`,
|
||||||
|
durationMs: Date.now() - r.startedAt,
|
||||||
|
outcome: 'ok',
|
||||||
|
summary: {
|
||||||
|
newEvents: newCount,
|
||||||
|
updatedEvents: updatedCount,
|
||||||
|
deletedFiles: deletedCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save state
|
||||||
|
saveComposioState(STATE_FILE, new Date().toISOString());
|
||||||
|
console.log(`[Calendar] Composio sync completed. ${newCount} new, ${updatedCount} updated, ${deletedCount} deleted.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Calendar] Error during Composio sync:', error);
|
||||||
|
const errRun = await ensureRun();
|
||||||
|
await serviceLogger.log({
|
||||||
|
type: 'error',
|
||||||
|
service: errRun.service,
|
||||||
|
runId: errRun.runId,
|
||||||
|
level: 'error',
|
||||||
|
message: 'Calendar sync error',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
await serviceLogger.log({
|
||||||
|
type: 'run_complete',
|
||||||
|
service: errRun.service,
|
||||||
|
runId: errRun.runId,
|
||||||
|
level: 'error',
|
||||||
|
message: 'Calendar sync failed',
|
||||||
|
durationMs: Date.now() - errRun.startedAt,
|
||||||
|
outcome: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
console.log("Starting Google Calendar & Notes Sync (TS)...");
|
console.log("Starting Google Calendar & Notes Sync (TS)...");
|
||||||
console.log(`Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`);
|
console.log(`Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
// Check if credentials are available with required scopes
|
const composioMode = await useComposioForGoogleCalendar();
|
||||||
const hasCredentials = await GoogleClientFactory.hasValidCredentials(REQUIRED_SCOPES);
|
if (composioMode) {
|
||||||
|
const isConnected = composioAccountsRepo.isConnected('googlecalendar');
|
||||||
if (!hasCredentials) {
|
if (!isConnected) {
|
||||||
console.log("Google OAuth credentials not available or missing required Calendar/Drive scopes. Sleeping...");
|
console.log('[Calendar] Google Calendar not connected via Composio. Sleeping...');
|
||||||
|
} else {
|
||||||
|
await performSyncComposio();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Perform one sync
|
// Check if credentials are available with required scopes
|
||||||
await performSync(SYNC_DIR, LOOKBACK_DAYS);
|
const hasCredentials = await GoogleClientFactory.hasValidCredentials(REQUIRED_SCOPES);
|
||||||
|
|
||||||
|
if (!hasCredentials) {
|
||||||
|
console.log("Google OAuth credentials not available or missing required Calendar/Drive scopes. Sleeping...");
|
||||||
|
} else {
|
||||||
|
// Perform one sync
|
||||||
|
await performSync(SYNC_DIR, LOOKBACK_DAYS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in main loop:", error);
|
console.error("Error in main loop:", error);
|
||||||
|
|
|
||||||
|
|
@ -786,13 +786,9 @@ export async function init() {
|
||||||
console.log("Starting Gmail Sync (TS)...");
|
console.log("Starting Gmail Sync (TS)...");
|
||||||
console.log(`Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`);
|
console.log(`Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`);
|
||||||
|
|
||||||
const composioMode = await useComposioForGoogle();
|
|
||||||
if (composioMode) {
|
|
||||||
console.log('[Gmail] Using Composio backend for Gmail sync.');
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
|
const composioMode = await useComposioForGoogle();
|
||||||
if (composioMode) {
|
if (composioMode) {
|
||||||
const isConnected = composioAccountsRepo.isConnected('gmail');
|
const isConnected = composioAccountsRepo.isConnected('gmail');
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue