digital-tools 2.0.2 → 2.1.1
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/CHANGELOG.md +17 -0
- package/package.json +3 -4
- package/src/define.js +267 -0
- package/src/entities/advertising.js +999 -0
- package/src/entities/ai.js +756 -0
- package/src/entities/analytics.js +1588 -0
- package/src/entities/automation.js +601 -0
- package/src/entities/communication.js +1150 -0
- package/src/entities/crm.js +1386 -0
- package/src/entities/design.js +546 -0
- package/src/entities/development.js +2212 -0
- package/src/entities/document.js +874 -0
- package/src/entities/ecommerce.js +1429 -0
- package/src/entities/experiment.js +1039 -0
- package/src/entities/finance.js +3478 -0
- package/src/entities/forms.js +1892 -0
- package/src/entities/hr.js +661 -0
- package/src/entities/identity.js +997 -0
- package/src/entities/index.js +282 -0
- package/src/entities/infrastructure.js +1153 -0
- package/src/entities/knowledge.js +1438 -0
- package/src/entities/marketing.js +1610 -0
- package/src/entities/media.js +1634 -0
- package/src/entities/notification.js +1199 -0
- package/src/entities/presentation.js +1274 -0
- package/src/entities/productivity.js +1317 -0
- package/src/entities/project-management.js +1136 -0
- package/src/entities/recruiting.js +736 -0
- package/src/entities/shipping.js +509 -0
- package/src/entities/signature.js +1102 -0
- package/src/entities/site.js +222 -0
- package/src/entities/spreadsheet.js +1341 -0
- package/src/entities/storage.js +1198 -0
- package/src/entities/support.js +1166 -0
- package/src/entities/video-conferencing.js +1750 -0
- package/src/entities/video.js +950 -0
- package/src/entities.js +1663 -0
- package/src/index.js +74 -0
- package/src/providers/analytics/index.js +17 -0
- package/src/providers/analytics/mixpanel.js +255 -0
- package/src/providers/calendar/cal-com.js +303 -0
- package/src/providers/calendar/google-calendar.js +335 -0
- package/src/providers/calendar/index.js +20 -0
- package/src/providers/crm/hubspot.js +566 -0
- package/src/providers/crm/index.js +17 -0
- package/src/providers/development/github.js +472 -0
- package/src/providers/development/index.js +17 -0
- package/src/providers/ecommerce/index.js +17 -0
- package/src/providers/ecommerce/shopify.js +378 -0
- package/src/providers/email/index.js +20 -0
- package/src/providers/email/resend.js +258 -0
- package/src/providers/email/sendgrid.js +161 -0
- package/src/providers/finance/index.js +17 -0
- package/src/providers/finance/stripe.js +549 -0
- package/src/providers/forms/index.js +17 -0
- package/src/providers/forms/typeform.js +500 -0
- package/src/providers/index.js +123 -0
- package/src/providers/knowledge/index.js +17 -0
- package/src/providers/knowledge/notion.js +389 -0
- package/src/providers/marketing/index.js +17 -0
- package/src/providers/marketing/mailchimp.js +443 -0
- package/src/providers/media/cloudinary.js +318 -0
- package/src/providers/media/index.js +17 -0
- package/src/providers/messaging/index.js +20 -0
- package/src/providers/messaging/slack.js +393 -0
- package/src/providers/messaging/twilio-sms.js +249 -0
- package/src/providers/project-management/index.js +17 -0
- package/src/providers/project-management/linear.js +575 -0
- package/src/providers/registry.js +86 -0
- package/src/providers/spreadsheet/google-sheets.js +375 -0
- package/src/providers/spreadsheet/index.js +20 -0
- package/src/providers/spreadsheet/xlsx.js +423 -0
- package/src/providers/storage/index.js +24 -0
- package/src/providers/storage/s3.js +419 -0
- package/src/providers/support/index.js +17 -0
- package/src/providers/support/zendesk.js +373 -0
- package/src/providers/tasks/index.js +17 -0
- package/src/providers/tasks/todoist.js +286 -0
- package/src/providers/types.js +9 -0
- package/src/providers/video-conferencing/google-meet.js +286 -0
- package/src/providers/video-conferencing/index.js +31 -0
- package/src/providers/video-conferencing/jitsi.js +254 -0
- package/src/providers/video-conferencing/teams.js +270 -0
- package/src/providers/video-conferencing/zoom.js +332 -0
- package/src/registry.js +128 -0
- package/src/tools/communication.js +184 -0
- package/src/tools/data.js +205 -0
- package/src/tools/index.js +11 -0
- package/src/tools/web.js +137 -0
- package/src/types.js +10 -0
- package/test/define.test.js +306 -0
- package/test/registry.test.js +357 -0
- package/test/tools.test.js +363 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digital-tools - Tools that can be used by both humans and AI agents
|
|
3
|
+
*
|
|
4
|
+
* This package provides:
|
|
5
|
+
* - Core Tool interface and types
|
|
6
|
+
* - Tool ontology/categories for organization
|
|
7
|
+
* - Tool registry for registration and discovery
|
|
8
|
+
* - Tool definition helpers with type safety
|
|
9
|
+
* - Common pre-built tool implementations
|
|
10
|
+
* - MCP (Model Context Protocol) compatibility
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
// Export entity type definitions (Nouns for digital tools)
|
|
15
|
+
export {
|
|
16
|
+
// Email
|
|
17
|
+
Email, EmailThread,
|
|
18
|
+
// Spreadsheet
|
|
19
|
+
Spreadsheet, Sheet, Cell,
|
|
20
|
+
// Document
|
|
21
|
+
Document,
|
|
22
|
+
// Presentation
|
|
23
|
+
Presentation, Slide,
|
|
24
|
+
// Phone
|
|
25
|
+
PhoneCall, Voicemail,
|
|
26
|
+
// Team Messaging (Slack/Teams/Discord equivalent)
|
|
27
|
+
Workspace, Channel, Message, Thread, DirectMessage, Member, Reaction,
|
|
28
|
+
// Supporting
|
|
29
|
+
Attachment, Contact, Comment, Revision,
|
|
30
|
+
// Collections
|
|
31
|
+
DigitalToolEntities, DigitalToolCategories, } from './entities.js';
|
|
32
|
+
// Export registry
|
|
33
|
+
export { registry, createRegistry, registerTool, getTool, executeTool, toMCP, listMCPTools, } from './registry.js';
|
|
34
|
+
// Export tool definition helpers
|
|
35
|
+
export { defineTool, defineAndRegister, createToolExecutor, toolBuilder, } from './define.js';
|
|
36
|
+
// Export pre-built tools
|
|
37
|
+
export {
|
|
38
|
+
// Web tools
|
|
39
|
+
fetchUrl, parseHtml, readUrl, webTools,
|
|
40
|
+
// Data tools
|
|
41
|
+
parseJson, stringifyJson, parseCsv, transformData, filterData, dataTools,
|
|
42
|
+
// Communication tools
|
|
43
|
+
sendEmail, sendSlackMessage, sendNotification, sendSms, communicationTools, } from './tools/index.js';
|
|
44
|
+
// Export providers (concrete implementations using third-party APIs)
|
|
45
|
+
export {
|
|
46
|
+
// Provider registry
|
|
47
|
+
providerRegistry, createProviderRegistry, registerProvider, getProvider, createProvider, listProviders, defineProvider,
|
|
48
|
+
// Email providers
|
|
49
|
+
sendgridProvider, resendProvider, createSendGridProvider, createResendProvider,
|
|
50
|
+
// Messaging providers
|
|
51
|
+
slackProvider, twilioSmsProvider, createSlackProvider, createTwilioSmsProvider,
|
|
52
|
+
// Spreadsheet providers
|
|
53
|
+
xlsxProvider, googleSheetsProvider, createXlsxProvider, createGoogleSheetsProvider,
|
|
54
|
+
// Registration helpers
|
|
55
|
+
registerAllProviders, registerEmailProviders, registerMessagingProviders, registerSpreadsheetProviders, allProviders, } from './providers/index.js';
|
|
56
|
+
// Convenience function to register all built-in tools
|
|
57
|
+
import { registry } from './registry.js';
|
|
58
|
+
import { webTools } from './tools/web.js';
|
|
59
|
+
import { dataTools } from './tools/data.js';
|
|
60
|
+
import { communicationTools } from './tools/communication.js';
|
|
61
|
+
/**
|
|
62
|
+
* Register all built-in tools in the global registry
|
|
63
|
+
*/
|
|
64
|
+
export function registerBuiltinTools() {
|
|
65
|
+
for (const tool of [...webTools, ...dataTools, ...communicationTools]) {
|
|
66
|
+
registry.register(tool);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get all built-in tools
|
|
71
|
+
*/
|
|
72
|
+
export function getBuiltinTools() {
|
|
73
|
+
return [...webTools, ...dataTools, ...communicationTools];
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Providers
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export { mixpanelInfo, mixpanelProvider, createMixpanelProvider } from './mixpanel.js';
|
|
7
|
+
import { mixpanelProvider } from './mixpanel.js';
|
|
8
|
+
/**
|
|
9
|
+
* Register all analytics providers
|
|
10
|
+
*/
|
|
11
|
+
export function registerAnalyticsProviders() {
|
|
12
|
+
mixpanelProvider.register();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* All analytics providers
|
|
16
|
+
*/
|
|
17
|
+
export const analyticsProviders = [mixpanelProvider];
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixpanel Analytics Provider
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementation of AnalyticsProvider using Mixpanel API.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { defineProvider } from '../registry.js';
|
|
9
|
+
const MIXPANEL_TRACK_URL = 'https://api.mixpanel.com/track';
|
|
10
|
+
const MIXPANEL_ENGAGE_URL = 'https://api.mixpanel.com/engage';
|
|
11
|
+
const MIXPANEL_QUERY_URL = 'https://mixpanel.com/api/2.0';
|
|
12
|
+
/**
|
|
13
|
+
* Mixpanel provider info
|
|
14
|
+
*/
|
|
15
|
+
export const mixpanelInfo = {
|
|
16
|
+
id: 'analytics.mixpanel',
|
|
17
|
+
name: 'Mixpanel',
|
|
18
|
+
description: 'Mixpanel product analytics and user engagement platform',
|
|
19
|
+
category: 'analytics',
|
|
20
|
+
website: 'https://mixpanel.com',
|
|
21
|
+
docsUrl: 'https://developer.mixpanel.com/docs',
|
|
22
|
+
requiredConfig: ['projectToken'],
|
|
23
|
+
optionalConfig: ['apiSecret'],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Create Mixpanel analytics provider
|
|
27
|
+
*/
|
|
28
|
+
export function createMixpanelProvider(config) {
|
|
29
|
+
let projectToken;
|
|
30
|
+
let apiSecret;
|
|
31
|
+
return {
|
|
32
|
+
info: mixpanelInfo,
|
|
33
|
+
async initialize(cfg) {
|
|
34
|
+
projectToken = cfg.projectToken;
|
|
35
|
+
apiSecret = cfg.apiSecret;
|
|
36
|
+
if (!projectToken) {
|
|
37
|
+
throw new Error('Mixpanel project token is required');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async healthCheck() {
|
|
41
|
+
const start = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
// Try to track a test event to verify connectivity
|
|
44
|
+
const testEvent = {
|
|
45
|
+
event: 'health_check',
|
|
46
|
+
properties: {
|
|
47
|
+
token: projectToken,
|
|
48
|
+
time: Math.floor(Date.now() / 1000),
|
|
49
|
+
$insert_id: `health_check_${Date.now()}`,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const response = await fetch(MIXPANEL_TRACK_URL, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify([testEvent]),
|
|
58
|
+
});
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
return {
|
|
61
|
+
healthy: response.ok && result.status === 1,
|
|
62
|
+
latencyMs: Date.now() - start,
|
|
63
|
+
message: response.ok ? 'Connected' : `HTTP ${response.status}`,
|
|
64
|
+
checkedAt: new Date(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
healthy: false,
|
|
70
|
+
latencyMs: Date.now() - start,
|
|
71
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
72
|
+
checkedAt: new Date(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async dispose() {
|
|
77
|
+
// No cleanup needed
|
|
78
|
+
},
|
|
79
|
+
async track(event) {
|
|
80
|
+
try {
|
|
81
|
+
const eventData = {
|
|
82
|
+
event: event.event,
|
|
83
|
+
properties: {
|
|
84
|
+
token: projectToken,
|
|
85
|
+
distinct_id: event.userId || event.anonymousId || 'unknown',
|
|
86
|
+
time: event.timestamp ? Math.floor(event.timestamp.getTime() / 1000) : Math.floor(Date.now() / 1000),
|
|
87
|
+
$insert_id: `${event.event}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
88
|
+
...event.properties,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const response = await fetch(MIXPANEL_TRACK_URL, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify([eventData]),
|
|
97
|
+
});
|
|
98
|
+
const result = await response.json();
|
|
99
|
+
return response.ok && result.status === 1;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error('Mixpanel track error:', error);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
async identify(userId, traits) {
|
|
107
|
+
try {
|
|
108
|
+
const engageData = {
|
|
109
|
+
$token: projectToken,
|
|
110
|
+
$distinct_id: userId,
|
|
111
|
+
$set: traits || {},
|
|
112
|
+
};
|
|
113
|
+
const response = await fetch(MIXPANEL_ENGAGE_URL, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify([engageData]),
|
|
119
|
+
});
|
|
120
|
+
const result = await response.json();
|
|
121
|
+
return response.ok && result.status === 1;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('Mixpanel identify error:', error);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async page(name, properties) {
|
|
129
|
+
return this.track({
|
|
130
|
+
event: 'Page Viewed',
|
|
131
|
+
properties: {
|
|
132
|
+
page_name: name,
|
|
133
|
+
...properties,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
async alias(userId, previousId) {
|
|
138
|
+
try {
|
|
139
|
+
const aliasEvent = {
|
|
140
|
+
event: '$create_alias',
|
|
141
|
+
properties: {
|
|
142
|
+
token: projectToken,
|
|
143
|
+
distinct_id: previousId,
|
|
144
|
+
alias: userId,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const response = await fetch(MIXPANEL_TRACK_URL, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify([aliasEvent]),
|
|
153
|
+
});
|
|
154
|
+
const result = await response.json();
|
|
155
|
+
return response.ok && result.status === 1;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error('Mixpanel alias error:', error);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
async getReport(reportId) {
|
|
163
|
+
if (!apiSecret) {
|
|
164
|
+
throw new Error('Mixpanel API secret is required for querying reports');
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const auth = Buffer.from(`${apiSecret}:`).toString('base64');
|
|
168
|
+
const response = await fetch(`${MIXPANEL_QUERY_URL}/reports/${reportId}`, {
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Basic ${auth}`,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
return {
|
|
178
|
+
id: reportId,
|
|
179
|
+
name: data.name || reportId,
|
|
180
|
+
description: data.description,
|
|
181
|
+
query: data.query || { metrics: [], dateRange: { start: new Date(), end: new Date() } },
|
|
182
|
+
result: data.result,
|
|
183
|
+
createdAt: data.created ? new Date(data.created) : new Date(),
|
|
184
|
+
updatedAt: data.updated ? new Date(data.updated) : new Date(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error('Mixpanel getReport error:', error);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async runQuery(query) {
|
|
193
|
+
if (!apiSecret) {
|
|
194
|
+
throw new Error('Mixpanel API secret is required for running queries');
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const auth = Buffer.from(`${apiSecret}:`).toString('base64');
|
|
198
|
+
// Construct query parameters
|
|
199
|
+
const params = new URLSearchParams({
|
|
200
|
+
from_date: query.dateRange.start.toISOString().split('T')[0],
|
|
201
|
+
to_date: query.dateRange.end.toISOString().split('T')[0],
|
|
202
|
+
});
|
|
203
|
+
if (query.metrics.length > 0) {
|
|
204
|
+
params.append('event', query.metrics.join(','));
|
|
205
|
+
}
|
|
206
|
+
if (query.limit) {
|
|
207
|
+
params.append('limit', query.limit.toString());
|
|
208
|
+
}
|
|
209
|
+
// Use the segmentation endpoint for queries
|
|
210
|
+
const endpoint = query.dimensions && query.dimensions.length > 0
|
|
211
|
+
? 'segmentation'
|
|
212
|
+
: 'events';
|
|
213
|
+
const response = await fetch(`${MIXPANEL_QUERY_URL}/${endpoint}?${params.toString()}`, {
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Basic ${auth}`,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`Query failed: ${response.statusText}`);
|
|
220
|
+
}
|
|
221
|
+
const data = await response.json();
|
|
222
|
+
// Transform Mixpanel response to our format
|
|
223
|
+
const rows = [];
|
|
224
|
+
const totals = {};
|
|
225
|
+
if (data.data) {
|
|
226
|
+
Object.entries(data.data).forEach(([key, values]) => {
|
|
227
|
+
const row = { metric: key };
|
|
228
|
+
if (typeof values === 'object' && values !== null) {
|
|
229
|
+
Object.entries(values).forEach(([date, value]) => {
|
|
230
|
+
row[date] = value;
|
|
231
|
+
if (typeof value === 'number') {
|
|
232
|
+
totals[key] = (totals[key] || 0) + value;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
rows.push(row);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
rows,
|
|
241
|
+
totals,
|
|
242
|
+
rowCount: rows.length,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error('Mixpanel runQuery error:', error);
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Mixpanel provider definition
|
|
254
|
+
*/
|
|
255
|
+
export const mixpanelProvider = defineProvider(mixpanelInfo, async (config) => createMixpanelProvider(config));
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cal.com Provider
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementation of CalendarProvider using Cal.com API v1.
|
|
5
|
+
* Cal.com is an open-source Calendly alternative for scheduling.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { defineProvider } from '../registry.js';
|
|
10
|
+
const DEFAULT_BASE_URL = 'https://api.cal.com/v1';
|
|
11
|
+
/**
|
|
12
|
+
* Cal.com provider info
|
|
13
|
+
*/
|
|
14
|
+
export const calComInfo = {
|
|
15
|
+
id: 'calendar.cal-com',
|
|
16
|
+
name: 'Cal.com',
|
|
17
|
+
description: 'Cal.com API for scheduling and calendar management (open-source Calendly alternative)',
|
|
18
|
+
category: 'calendar',
|
|
19
|
+
website: 'https://cal.com',
|
|
20
|
+
docsUrl: 'https://cal.com/docs/api-reference',
|
|
21
|
+
requiredConfig: ['apiKey'],
|
|
22
|
+
optionalConfig: ['baseUrl'],
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Create Cal.com provider
|
|
26
|
+
*/
|
|
27
|
+
export function createCalComProvider(config) {
|
|
28
|
+
let apiKey;
|
|
29
|
+
let baseUrl;
|
|
30
|
+
/**
|
|
31
|
+
* Helper to make authenticated API requests
|
|
32
|
+
*/
|
|
33
|
+
async function apiRequest(endpoint, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const url = `${baseUrl}${endpoint}`;
|
|
36
|
+
const params = new URLSearchParams();
|
|
37
|
+
// Cal.com uses apiKey query parameter for authentication
|
|
38
|
+
params.append('apiKey', apiKey);
|
|
39
|
+
const fullUrl = `${url}${url.includes('?') ? '&' : '?'}${params}`;
|
|
40
|
+
const response = await fetch(fullUrl, {
|
|
41
|
+
...options,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
...options.headers,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
const data = (await response.json());
|
|
49
|
+
return { ok: true, status: response.status, data };
|
|
50
|
+
}
|
|
51
|
+
const errorData = await response.json().catch(() => ({}));
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
status: response.status,
|
|
55
|
+
error: errorData?.message || response.statusText,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
status: 0,
|
|
62
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
info: calComInfo,
|
|
68
|
+
async initialize(cfg) {
|
|
69
|
+
apiKey = cfg.apiKey;
|
|
70
|
+
baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
|
|
71
|
+
if (!apiKey) {
|
|
72
|
+
throw new Error('Cal.com API key is required');
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async healthCheck() {
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
const result = await apiRequest('/me', { method: 'GET' });
|
|
78
|
+
return {
|
|
79
|
+
healthy: result.ok,
|
|
80
|
+
latencyMs: Date.now() - start,
|
|
81
|
+
message: result.ok ? 'Connected' : result.error || `HTTP ${result.status}`,
|
|
82
|
+
checkedAt: new Date(),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
async dispose() {
|
|
86
|
+
// No cleanup needed
|
|
87
|
+
},
|
|
88
|
+
async listCalendars(options) {
|
|
89
|
+
// Cal.com uses "event types" as calendar configurations
|
|
90
|
+
const params = new URLSearchParams();
|
|
91
|
+
if (options?.limit)
|
|
92
|
+
params.append('take', String(options.limit));
|
|
93
|
+
if (options?.cursor)
|
|
94
|
+
params.append('skip', String(options.cursor));
|
|
95
|
+
const result = await apiRequest(`/event-types?${params}`);
|
|
96
|
+
if (!result.ok || !result.data) {
|
|
97
|
+
return { items: [], hasMore: false };
|
|
98
|
+
}
|
|
99
|
+
const items = result.data.event_types
|
|
100
|
+
.filter((et) => !et.hidden)
|
|
101
|
+
.map((et) => ({
|
|
102
|
+
id: String(et.id),
|
|
103
|
+
name: et.title,
|
|
104
|
+
description: et.description,
|
|
105
|
+
timeZone: 'UTC', // Cal.com handles timezone per user
|
|
106
|
+
primary: et.position === 0,
|
|
107
|
+
accessRole: 'owner',
|
|
108
|
+
}));
|
|
109
|
+
return {
|
|
110
|
+
items,
|
|
111
|
+
hasMore: result.data.event_types.length >= (options?.limit || 10),
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
async getCalendar(calendarId) {
|
|
115
|
+
const result = await apiRequest(`/event-types/${calendarId}`);
|
|
116
|
+
if (!result.ok || !result.data) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const et = result.data.event_type;
|
|
120
|
+
return {
|
|
121
|
+
id: String(et.id),
|
|
122
|
+
name: et.title,
|
|
123
|
+
description: et.description,
|
|
124
|
+
timeZone: 'UTC',
|
|
125
|
+
primary: et.position === 0,
|
|
126
|
+
accessRole: 'owner',
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
async createEvent(calendarId, event) {
|
|
130
|
+
// In Cal.com, creating a booking is done through the booking endpoint
|
|
131
|
+
const body = {
|
|
132
|
+
eventTypeId: parseInt(calendarId, 10),
|
|
133
|
+
start: event.start.toISOString(),
|
|
134
|
+
end: event.end.toISOString(),
|
|
135
|
+
responses: {
|
|
136
|
+
name: event.summary,
|
|
137
|
+
email: event.attendees?.[0] || 'guest@example.com',
|
|
138
|
+
notes: event.description,
|
|
139
|
+
location: event.location,
|
|
140
|
+
},
|
|
141
|
+
timeZone: 'UTC',
|
|
142
|
+
language: 'en',
|
|
143
|
+
metadata: {},
|
|
144
|
+
};
|
|
145
|
+
const result = await apiRequest('/bookings', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
body: JSON.stringify(body),
|
|
148
|
+
});
|
|
149
|
+
if (!result.ok || !result.data) {
|
|
150
|
+
throw new Error(result.error || 'Failed to create booking');
|
|
151
|
+
}
|
|
152
|
+
const booking = result.data.booking;
|
|
153
|
+
return {
|
|
154
|
+
id: booking.uid,
|
|
155
|
+
calendarId,
|
|
156
|
+
summary: booking.title,
|
|
157
|
+
description: booking.description,
|
|
158
|
+
location: booking.location,
|
|
159
|
+
start: new Date(booking.startTime),
|
|
160
|
+
end: new Date(booking.endTime),
|
|
161
|
+
attendees: booking.attendees.map((a) => ({
|
|
162
|
+
email: a.email,
|
|
163
|
+
responseStatus: booking.status === 'ACCEPTED' ? 'accepted' : 'needsAction',
|
|
164
|
+
})),
|
|
165
|
+
status: (booking.status === 'CANCELLED' ? 'cancelled' : 'confirmed'),
|
|
166
|
+
htmlLink: `${baseUrl.replace('/v1', '')}/booking/${booking.uid}`,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
async getEvent(calendarId, eventId) {
|
|
170
|
+
const result = await apiRequest(`/bookings/${eventId}`);
|
|
171
|
+
if (!result.ok || !result.data) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const booking = result.data.booking;
|
|
175
|
+
return {
|
|
176
|
+
id: booking.uid,
|
|
177
|
+
calendarId: String(booking.eventTypeId),
|
|
178
|
+
summary: booking.title,
|
|
179
|
+
description: booking.description,
|
|
180
|
+
location: booking.location,
|
|
181
|
+
start: new Date(booking.startTime),
|
|
182
|
+
end: new Date(booking.endTime),
|
|
183
|
+
attendees: booking.attendees.map((a) => ({
|
|
184
|
+
email: a.email,
|
|
185
|
+
responseStatus: booking.status === 'ACCEPTED' ? 'accepted' : 'needsAction',
|
|
186
|
+
})),
|
|
187
|
+
status: (booking.status === 'CANCELLED' ? 'cancelled' : 'confirmed'),
|
|
188
|
+
htmlLink: `${baseUrl.replace('/v1', '')}/booking/${booking.uid}`,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
async updateEvent(calendarId, eventId, updates) {
|
|
192
|
+
// Cal.com bookings are typically rescheduled rather than updated
|
|
193
|
+
const body = {};
|
|
194
|
+
if (updates.start && updates.end) {
|
|
195
|
+
body.start = updates.start.toISOString();
|
|
196
|
+
body.end = updates.end.toISOString();
|
|
197
|
+
}
|
|
198
|
+
if (updates.summary) {
|
|
199
|
+
body.title = updates.summary;
|
|
200
|
+
}
|
|
201
|
+
if (updates.description) {
|
|
202
|
+
body.description = updates.description;
|
|
203
|
+
}
|
|
204
|
+
const result = await apiRequest(`/bookings/${eventId}`, {
|
|
205
|
+
method: 'PATCH',
|
|
206
|
+
body: JSON.stringify(body),
|
|
207
|
+
});
|
|
208
|
+
if (!result.ok || !result.data) {
|
|
209
|
+
throw new Error(result.error || 'Failed to update booking');
|
|
210
|
+
}
|
|
211
|
+
const booking = result.data.booking;
|
|
212
|
+
return {
|
|
213
|
+
id: booking.uid,
|
|
214
|
+
calendarId: String(booking.eventTypeId),
|
|
215
|
+
summary: booking.title,
|
|
216
|
+
description: booking.description,
|
|
217
|
+
location: booking.location,
|
|
218
|
+
start: new Date(booking.startTime),
|
|
219
|
+
end: new Date(booking.endTime),
|
|
220
|
+
attendees: booking.attendees.map((a) => ({
|
|
221
|
+
email: a.email,
|
|
222
|
+
responseStatus: booking.status === 'ACCEPTED' ? 'accepted' : 'needsAction',
|
|
223
|
+
})),
|
|
224
|
+
status: (booking.status === 'CANCELLED' ? 'cancelled' : 'confirmed'),
|
|
225
|
+
htmlLink: `${baseUrl.replace('/v1', '')}/booking/${booking.uid}`,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
async deleteEvent(calendarId, eventId) {
|
|
229
|
+
// Cancel the booking
|
|
230
|
+
const result = await apiRequest(`/bookings/${eventId}`, {
|
|
231
|
+
method: 'DELETE',
|
|
232
|
+
});
|
|
233
|
+
return result.ok;
|
|
234
|
+
},
|
|
235
|
+
async listEvents(calendarId, options) {
|
|
236
|
+
const params = new URLSearchParams();
|
|
237
|
+
if (options?.limit)
|
|
238
|
+
params.append('take', String(options.limit));
|
|
239
|
+
if (options?.cursor)
|
|
240
|
+
params.append('skip', String(options.cursor));
|
|
241
|
+
// Filter by event type (calendar)
|
|
242
|
+
params.append('eventTypeId', calendarId);
|
|
243
|
+
if (options?.timeMin)
|
|
244
|
+
params.append('afterStart', options.timeMin.toISOString());
|
|
245
|
+
if (options?.timeMax)
|
|
246
|
+
params.append('beforeEnd', options.timeMax.toISOString());
|
|
247
|
+
const result = await apiRequest(`/bookings?${params}`);
|
|
248
|
+
if (!result.ok || !result.data) {
|
|
249
|
+
return { items: [], hasMore: false };
|
|
250
|
+
}
|
|
251
|
+
const items = result.data.bookings.map((booking) => ({
|
|
252
|
+
id: booking.uid,
|
|
253
|
+
calendarId: String(booking.eventTypeId),
|
|
254
|
+
summary: booking.title,
|
|
255
|
+
description: booking.description,
|
|
256
|
+
location: booking.location,
|
|
257
|
+
start: new Date(booking.startTime),
|
|
258
|
+
end: new Date(booking.endTime),
|
|
259
|
+
attendees: booking.attendees.map((a) => ({
|
|
260
|
+
email: a.email,
|
|
261
|
+
responseStatus: booking.status === 'ACCEPTED' ? 'accepted' : 'needsAction',
|
|
262
|
+
})),
|
|
263
|
+
status: (booking.status === 'CANCELLED' ? 'cancelled' : 'confirmed'),
|
|
264
|
+
htmlLink: `${baseUrl.replace('/v1', '')}/booking/${booking.uid}`,
|
|
265
|
+
}));
|
|
266
|
+
return {
|
|
267
|
+
items,
|
|
268
|
+
hasMore: result.data.bookings.length >= (options?.limit || 10),
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
async findAvailability(calendarIds, timeMin, timeMax) {
|
|
272
|
+
// Cal.com's availability endpoint
|
|
273
|
+
const results = [];
|
|
274
|
+
for (const calendarId of calendarIds) {
|
|
275
|
+
const params = new URLSearchParams();
|
|
276
|
+
params.append('eventTypeId', calendarId);
|
|
277
|
+
params.append('dateFrom', timeMin.toISOString());
|
|
278
|
+
params.append('dateTo', timeMax.toISOString());
|
|
279
|
+
const result = await apiRequest(`/availability?${params}`);
|
|
280
|
+
if (result.ok && result.data) {
|
|
281
|
+
results.push({
|
|
282
|
+
calendarId,
|
|
283
|
+
busy: result.data.busy.map((slot) => ({
|
|
284
|
+
start: new Date(slot.start),
|
|
285
|
+
end: new Date(slot.end),
|
|
286
|
+
})),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
results.push({
|
|
291
|
+
calendarId,
|
|
292
|
+
busy: [],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return results;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Cal.com provider definition
|
|
302
|
+
*/
|
|
303
|
+
export const calComProvider = defineProvider(calComInfo, async (config) => createCalComProvider(config));
|