Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions integrations/calendly/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}
42 changes: 42 additions & 0 deletions integrations/calendly/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@core/calendly",
"version": "0.1.0",
"description": "Calendly integration for CORE",
"main": "./bin/index.js",
"type": "module",
"files": [
"calendly",
"bin"
],
"bin": {
"calendly": "./bin/index.js"
},
"scripts": {
"build": "rimraf bin && bun build src/index.ts --outfile dist/index.js --target node --minify",
"lint": "eslint --ext js,ts,tsx src/ --fix",
"prettier": "prettier --config .prettierrc --write ."
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"axios": "^1.7.9",
"commander": "^12.0.0",
"@redplanethq/sdk": "0.1.9",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.1"
},
"devDependencies": {
"@types/node": "^18.0.20",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^3.4.2",
"rimraf": "^3.0.2",
"tslib": "^2.8.1",
"tsx": "4.20.6",
"typescript": "^4.7.2"
}
}
75 changes: 75 additions & 0 deletions integrations/calendly/scripts/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pg from 'pg';

const { Client } = pg;

async function main() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error('DATABASE_URL environment variable is required');
process.exit(1);
}

const clientId = process.env.CALENDLY_CLIENT_ID;
const clientSecret = process.env.CALENDLY_CLIENT_SECRET;

if (!clientId || !clientSecret) {
console.error(
'CALENDLY_CLIENT_ID and CALENDLY_CLIENT_SECRET environment variables are required',
);
process.exit(1);
}

const client = new Client({ connectionString });

const spec = {
name: 'Calendly',
key: 'calendly',
description:
'Connect your Calendly account to view and manage event types, scheduled meetings, invitees, availability, routing forms, and webhook subscriptions in CORE.',
icon: 'calendly',
auth: {
OAuth2: {
authorization_url: 'https://auth.calendly.com/oauth/authorize',
token_url: 'https://auth.calendly.com/oauth/token',
scopes: ['default'],
scope_separator: ' ',
token_request_auth_method: 'basic',
},
},
};

try {
await client.connect();

await client.query(
`
INSERT INTO core."IntegrationDefinitionV2" ("id", "name", "slug", "description", "icon", "spec", "config", "version", "url", "updatedAt", "createdAt")
VALUES (gen_random_uuid(), 'Calendly', 'calendly', $4, 'calendly', $1, $2, '0.1.0', $3, NOW(), NOW())
ON CONFLICT (name) DO UPDATE SET
"slug" = EXCLUDED."slug",
"description" = EXCLUDED."description",
"icon" = EXCLUDED."icon",
"spec" = EXCLUDED."spec",
"config" = EXCLUDED."config",
"version" = EXCLUDED."version",
"url" = EXCLUDED."url",
"updatedAt" = NOW()
RETURNING *;
`,
[
JSON.stringify(spec),
JSON.stringify({ clientId, clientSecret }),
'../../integrations/calendly/dist/index.js',
spec.description,
],
);

console.log('Calendly integration registered successfully in the database.');
} catch (error) {
console.error('Error registering Calendly integration:', error);
} finally {
await client.end();
}
}

main().catch(console.error);
45 changes: 45 additions & 0 deletions integrations/calendly/src/account-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getCurrentUser } from './utils';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function integrationCreate(data: any) {
const { oauthResponse } = data;

let userUri: string | null = null;
let orgUri: string | null = null;
let email: string | null = null;
let name: string | null = null;

try {
const user = await getCurrentUser(oauthResponse.access_token);
userUri = user.uri;
orgUri = user.current_organization;
email = user.email;
name = user.name;
} catch (error) {
console.error('Error fetching Calendly user profile:', error);
}

return [
{
type: 'account',
data: {
settings: {
userUri,
orgUri,
name,
},
accountId: email || userUri || 'calendly',
config: {
access_token: oauthResponse.access_token,
refresh_token: oauthResponse.refresh_token,
expires_in: oauthResponse.expires_in,
expires_at: oauthResponse.expires_at,
userUri,
orgUri,
email,
name,
},
},
},
];
}
85 changes: 85 additions & 0 deletions integrations/calendly/src/create-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
interface CalendlyWebhookEvent {
event: string;
payload: {
event_type?: { name?: string };
invitee?: {
name?: string;
email?: string;
cancel_url?: string;
reschedule_url?: string;
};
scheduled_event?: {
name?: string;
start_time?: string;
end_time?: string;
uri?: string;
};
form_submission?: {
questions_and_answers?: Array<{ question: string; answer: string }>;
};
routing_form?: { name?: string };
cancel_url?: string;
reschedule_url?: string;
};
}

function formatTime(iso: string | undefined): string {
if (!iso) return '';
return new Date(iso).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short',
});
}

function describeEvent(webhookEvent: CalendlyWebhookEvent): string | null {
const { event, payload } = webhookEvent;
const invitee = payload.invitee;
const scheduled = payload.scheduled_event;
const eventTypeName = payload.event_type?.name || scheduled?.name || 'meeting';
const inviteeName = invitee?.name || invitee?.email || 'someone';
const startTime = scheduled?.start_time;

switch (event) {
case 'invitee.created':
return `${inviteeName} booked "${eventTypeName}"${startTime ? ` on ${formatTime(startTime)}` : ''}`;

case 'invitee.canceled':
return `${inviteeName} canceled "${eventTypeName}"${startTime ? ` (was ${formatTime(startTime)})` : ''}`;

case 'routing_form_submission.created': {
const formName = payload.routing_form?.name || 'routing form';
return `New submission on "${formName}"`;
}

default:
return null;
}
}

function getSourceUrl(webhookEvent: CalendlyWebhookEvent): string {
const { payload } = webhookEvent;
const uri = payload.scheduled_event?.uri;
if (uri) {
// Extract UUID from URI like https://api.calendly.com/scheduled_events/<uuid>
const uuid = uri.split('/').pop();
return `https://calendly.com/scheduled_events/${uuid}`;
}
return 'https://calendly.com/app/scheduled_events';
}

export function createActivity(webhookEvent: CalendlyWebhookEvent) {
const text = describeEvent(webhookEvent);
if (!text) return null;

return {
type: 'activity',
data: {
text,
sourceURL: getSourceUrl(webhookEvent),
},
};
}
84 changes: 84 additions & 0 deletions integrations/calendly/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { fileURLToPath } from 'url';

import {
IntegrationCLI,
IntegrationEventPayload,
IntegrationEventType,
Spec,
} from '@redplanethq/sdk';

import { integrationCreate } from './account-create';
import { createActivity } from './create-activity';
import { callTool, getTools } from './mcp';

export async function run(eventPayload: IntegrationEventPayload) {
switch (eventPayload.event) {
case IntegrationEventType.SETUP:
return await integrationCreate(eventPayload.eventBody);

case IntegrationEventType.PROCESS: {
const event = eventPayload.eventBody?.eventData;
if (!event) return [];
const activity = createActivity(event);
return activity ? [activity] : [];
}

case IntegrationEventType.GET_TOOLS: {
try {
return getTools();
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
return { message: `Error ${message}` };
}
}

case IntegrationEventType.CALL_TOOL: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const config = eventPayload.config as any;
const { name, arguments: args } = eventPayload.eventBody;
return await callTool(name, args, config);
}

default:
return { message: `The event payload type is ${eventPayload.event}` };
}
}

class CalendlyCLI extends IntegrationCLI {
constructor() {
super('calendly', '1.0.0');
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
return await run(eventPayload);
}

protected async getSpec(): Promise<Spec> {
return {
name: 'Calendly',
key: 'calendly',
description:
'Connect your Calendly account to view and manage event types, scheduled meetings, invitees, availability, routing forms, and webhook subscriptions in CORE.',
icon: 'calendly',
auth: {
OAuth2: {
authorization_url: 'https://auth.calendly.com/oauth/authorize',
token_url: 'https://auth.calendly.com/oauth/token',
scopes: ['default'],
scope_separator: ' ',
token_request_auth_method: 'basic',
},
},
};
}
}

function main() {
const calendlyCLI = new CalendlyCLI();
calendlyCLI.parse();
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}
Loading