@friggframework/ai-agents 2.0.0--canary.522.cbd3d5a.0
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/LICENSE.md +9 -0
- package/jest.config.js +8 -0
- package/package.json +60 -0
- package/src/domain/entities/agent-event.js +55 -0
- package/src/domain/entities/agent-proposal.js +81 -0
- package/src/domain/entities/index.js +9 -0
- package/src/domain/index.js +7 -0
- package/src/domain/interfaces/agent-framework.js +29 -0
- package/src/domain/interfaces/index.js +10 -0
- package/src/domain/interfaces/validation-pipeline.js +61 -0
- package/src/index.js +41 -0
- package/src/infrastructure/adapters/claude-agent-adapter.js +148 -0
- package/src/infrastructure/adapters/index.js +10 -0
- package/src/infrastructure/adapters/vercel-ai-adapter.js +135 -0
- package/src/infrastructure/git/git-checkpoint-service.js +110 -0
- package/src/infrastructure/git/index.js +3 -0
- package/src/infrastructure/mcp/frigg-tools.js +1918 -0
- package/src/infrastructure/mcp/index.js +25 -0
- package/src/infrastructure/streaming/agent-stream-handler.js +117 -0
- package/src/infrastructure/streaming/index.js +3 -0
- package/src/infrastructure/validation/index.js +3 -0
- package/src/infrastructure/validation/validation-pipeline.js +226 -0
- package/tests/integration/agent-workflow.test.js +276 -0
- package/tests/unit/domain/entities/agent-event.test.js +73 -0
- package/tests/unit/domain/entities/agent-proposal.test.js +121 -0
- package/tests/unit/domain/interfaces/agent-framework.test.js +66 -0
- package/tests/unit/domain/interfaces/validation-pipeline.test.js +83 -0
- package/tests/unit/infrastructure/adapters/claude-agent-adapter.test.js +193 -0
- package/tests/unit/infrastructure/adapters/vercel-ai-adapter.test.js +174 -0
- package/tests/unit/infrastructure/git/git-checkpoint-service.test.js +182 -0
- package/tests/unit/infrastructure/mcp/frigg-tools.test.js +810 -0
- package/tests/unit/infrastructure/streaming/agent-stream-handler.test.js +214 -0
- package/tests/unit/infrastructure/validation/validation-pipeline.test.js +288 -0
|
@@ -0,0 +1,1918 @@
|
|
|
1
|
+
const { exec, spawn } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
const INTEGRATION_CATEGORIES = [
|
|
10
|
+
'CRM', 'Marketing', 'Communication', 'ECommerce',
|
|
11
|
+
'Finance', 'Analytics', 'Storage', 'Development',
|
|
12
|
+
'Productivity', 'Social', 'Other'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const INTEGRATION_TYPES = ['api', 'webhook', 'sync', 'transform', 'custom'];
|
|
16
|
+
|
|
17
|
+
const CATEGORY_TEMPLATES = {
|
|
18
|
+
CRM: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
19
|
+
|
|
20
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
21
|
+
static Definition = {
|
|
22
|
+
name: '${name.toLowerCase()}',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
modules: {
|
|
25
|
+
${name.toLowerCase()}: { definition: require('@friggframework/api-module-${name.toLowerCase()}') }
|
|
26
|
+
},
|
|
27
|
+
options: {
|
|
28
|
+
type: 'api',
|
|
29
|
+
hasUserConfig: true,
|
|
30
|
+
display: {
|
|
31
|
+
name: '${capitalize(name)}',
|
|
32
|
+
description: '${capitalize(name)} CRM integration for contacts, deals, and company management',
|
|
33
|
+
category: 'CRM',
|
|
34
|
+
icon: '${name.toLowerCase()}'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
capabilities: {
|
|
38
|
+
auth: ['oauth2'],
|
|
39
|
+
webhooks: ${options.webhooks || false},
|
|
40
|
+
sync: { bidirectional: true, incremental: true }
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async onCreate({ integrationId }) {
|
|
45
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
46
|
+
${options.webhooks ? `// Register webhooks for real-time updates
|
|
47
|
+
// await this.registerWebhooks();` : ''}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async onUpdate(params) {
|
|
51
|
+
await this.validateConfig();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async onDelete(params) {
|
|
55
|
+
${options.webhooks ? `// Cleanup webhooks
|
|
56
|
+
// await this.unregisterWebhooks();` : ''}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getConfigOptions() {
|
|
60
|
+
return {
|
|
61
|
+
jsonSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
syncContacts: { type: 'boolean', title: 'Sync Contacts', default: true },
|
|
65
|
+
syncDeals: { type: 'boolean', title: 'Sync Deals', default: true },
|
|
66
|
+
syncCompanies: { type: 'boolean', title: 'Sync Companies', default: true }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
uiSchema: {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async testAuth() {
|
|
74
|
+
const module = this.getModule('${name.toLowerCase()}');
|
|
75
|
+
return module.testAuth();
|
|
76
|
+
}
|
|
77
|
+
${options.webhooks ? `
|
|
78
|
+
async onWebhookReceived({ req, res }) {
|
|
79
|
+
await this.queueWebhook({
|
|
80
|
+
integrationId: req.params.integrationId,
|
|
81
|
+
body: req.body,
|
|
82
|
+
headers: req.headers
|
|
83
|
+
});
|
|
84
|
+
res.status(200).json({ received: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async onWebhook({ data }) {
|
|
88
|
+
const { body } = data;
|
|
89
|
+
// Process CRM webhook events (contact.created, deal.updated, etc.)
|
|
90
|
+
}
|
|
91
|
+
` : ''}}
|
|
92
|
+
|
|
93
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
94
|
+
`,
|
|
95
|
+
|
|
96
|
+
Finance: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
97
|
+
|
|
98
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
99
|
+
static Definition = {
|
|
100
|
+
name: '${name.toLowerCase()}',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
modules: {
|
|
103
|
+
${name.toLowerCase()}: { definition: require('@friggframework/api-module-${name.toLowerCase()}') }
|
|
104
|
+
},
|
|
105
|
+
options: {
|
|
106
|
+
type: 'api',
|
|
107
|
+
hasUserConfig: true,
|
|
108
|
+
display: {
|
|
109
|
+
name: '${capitalize(name)}',
|
|
110
|
+
description: '${capitalize(name)} finance integration for invoices, payments, and accounting',
|
|
111
|
+
category: 'Finance',
|
|
112
|
+
icon: '${name.toLowerCase()}'
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
capabilities: {
|
|
116
|
+
auth: ['${options.authType || 'oauth2'}'],
|
|
117
|
+
webhooks: ${options.webhooks || false},
|
|
118
|
+
sync: { bidirectional: false, incremental: true }
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
async onCreate({ integrationId }) {
|
|
123
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async onUpdate(params) {
|
|
127
|
+
await this.validateConfig();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async onDelete(params) {}
|
|
131
|
+
|
|
132
|
+
async getConfigOptions() {
|
|
133
|
+
return {
|
|
134
|
+
jsonSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
syncInvoices: { type: 'boolean', title: 'Sync Invoices', default: true },
|
|
138
|
+
syncPayments: { type: 'boolean', title: 'Sync Payments', default: true },
|
|
139
|
+
syncCustomers: { type: 'boolean', title: 'Sync Customers', default: true }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
uiSchema: {}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async testAuth() {
|
|
147
|
+
const module = this.getModule('${name.toLowerCase()}');
|
|
148
|
+
return module.testAuth();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
153
|
+
`,
|
|
154
|
+
|
|
155
|
+
Communication: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
156
|
+
|
|
157
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
158
|
+
static Definition = {
|
|
159
|
+
name: '${name.toLowerCase()}',
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
modules: {
|
|
162
|
+
${name.toLowerCase()}: { definition: require('@friggframework/api-module-${name.toLowerCase()}') }
|
|
163
|
+
},
|
|
164
|
+
options: {
|
|
165
|
+
type: 'api',
|
|
166
|
+
hasUserConfig: true,
|
|
167
|
+
display: {
|
|
168
|
+
name: '${capitalize(name)}',
|
|
169
|
+
description: '${capitalize(name)} communication integration for messaging and notifications',
|
|
170
|
+
category: 'Communication',
|
|
171
|
+
icon: '${name.toLowerCase()}'
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
capabilities: {
|
|
175
|
+
auth: ['oauth2'],
|
|
176
|
+
webhooks: true,
|
|
177
|
+
realtime: true
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
async onCreate({ integrationId }) {
|
|
182
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
183
|
+
// Subscribe to message events
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async onUpdate(params) {
|
|
187
|
+
await this.validateConfig();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async onDelete(params) {
|
|
191
|
+
// Unsubscribe from events
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getConfigOptions() {
|
|
195
|
+
return {
|
|
196
|
+
jsonSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
defaultChannel: { type: 'string', title: 'Default Channel' },
|
|
200
|
+
notifyOnMention: { type: 'boolean', title: 'Notify on Mention', default: true }
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
uiSchema: {}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async testAuth() {
|
|
208
|
+
const module = this.getModule('${name.toLowerCase()}');
|
|
209
|
+
return module.testAuth();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async onWebhookReceived({ req, res }) {
|
|
213
|
+
// Handle Slack/Teams challenge verification
|
|
214
|
+
if (req.body.challenge) {
|
|
215
|
+
return res.status(200).json({ challenge: req.body.challenge });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await this.queueWebhook({
|
|
219
|
+
integrationId: req.params.integrationId,
|
|
220
|
+
body: req.body,
|
|
221
|
+
headers: req.headers
|
|
222
|
+
});
|
|
223
|
+
res.status(200).json({ received: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async onWebhook({ data }) {
|
|
227
|
+
const { body } = data;
|
|
228
|
+
// Process message events
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
233
|
+
`,
|
|
234
|
+
|
|
235
|
+
ECommerce: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
236
|
+
|
|
237
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
238
|
+
static Definition = {
|
|
239
|
+
name: '${name.toLowerCase()}',
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
modules: {
|
|
242
|
+
${name.toLowerCase()}: { definition: require('@friggframework/api-module-${name.toLowerCase()}') }
|
|
243
|
+
},
|
|
244
|
+
options: {
|
|
245
|
+
type: 'api',
|
|
246
|
+
hasUserConfig: true,
|
|
247
|
+
display: {
|
|
248
|
+
name: '${capitalize(name)}',
|
|
249
|
+
description: '${capitalize(name)} e-commerce integration for orders, products, and customers',
|
|
250
|
+
category: 'ECommerce',
|
|
251
|
+
icon: '${name.toLowerCase()}'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
capabilities: {
|
|
255
|
+
auth: ['oauth2'],
|
|
256
|
+
webhooks: true,
|
|
257
|
+
sync: { bidirectional: true, incremental: true, batchSize: 250 }
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
async onCreate({ integrationId }) {
|
|
262
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
263
|
+
// Register order and inventory webhooks
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async onUpdate(params) {
|
|
267
|
+
await this.validateConfig();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async onDelete(params) {
|
|
271
|
+
// Cleanup webhooks
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async getConfigOptions() {
|
|
275
|
+
return {
|
|
276
|
+
jsonSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
syncOrders: { type: 'boolean', title: 'Sync Orders', default: true },
|
|
280
|
+
syncProducts: { type: 'boolean', title: 'Sync Products', default: true },
|
|
281
|
+
syncCustomers: { type: 'boolean', title: 'Sync Customers', default: true },
|
|
282
|
+
syncInventory: { type: 'boolean', title: 'Sync Inventory', default: false }
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
uiSchema: {}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async testAuth() {
|
|
290
|
+
const module = this.getModule('${name.toLowerCase()}');
|
|
291
|
+
return module.testAuth();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async onWebhookReceived({ req, res }) {
|
|
295
|
+
// Verify webhook signature
|
|
296
|
+
await this.queueWebhook({
|
|
297
|
+
integrationId: req.params.integrationId,
|
|
298
|
+
body: req.body,
|
|
299
|
+
headers: req.headers
|
|
300
|
+
});
|
|
301
|
+
res.status(200).json({ received: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async onWebhook({ data }) {
|
|
305
|
+
const { body } = data;
|
|
306
|
+
// Process order/product/inventory events
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
311
|
+
`,
|
|
312
|
+
|
|
313
|
+
Storage: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
314
|
+
|
|
315
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
316
|
+
static Definition = {
|
|
317
|
+
name: '${name.toLowerCase()}',
|
|
318
|
+
version: '1.0.0',
|
|
319
|
+
modules: {
|
|
320
|
+
${name.toLowerCase()}: { definition: require('@friggframework/api-module-${name.toLowerCase()}') }
|
|
321
|
+
},
|
|
322
|
+
options: {
|
|
323
|
+
type: 'api',
|
|
324
|
+
hasUserConfig: true,
|
|
325
|
+
display: {
|
|
326
|
+
name: '${capitalize(name)}',
|
|
327
|
+
description: '${capitalize(name)} storage integration for files and documents',
|
|
328
|
+
category: 'Storage',
|
|
329
|
+
icon: '${name.toLowerCase()}'
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
capabilities: {
|
|
333
|
+
auth: ['oauth2'],
|
|
334
|
+
webhooks: ${options.webhooks || false}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
async onCreate({ integrationId }) {
|
|
339
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async onUpdate(params) {
|
|
343
|
+
await this.validateConfig();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async onDelete(params) {}
|
|
347
|
+
|
|
348
|
+
async getConfigOptions() {
|
|
349
|
+
return {
|
|
350
|
+
jsonSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
rootFolder: { type: 'string', title: 'Root Folder Path' },
|
|
354
|
+
syncSubfolders: { type: 'boolean', title: 'Sync Subfolders', default: true }
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
uiSchema: {}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async testAuth() {
|
|
362
|
+
const module = this.getModule('${name.toLowerCase()}');
|
|
363
|
+
return module.testAuth();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
368
|
+
`,
|
|
369
|
+
|
|
370
|
+
Webhook: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
371
|
+
const crypto = require('crypto');
|
|
372
|
+
|
|
373
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
374
|
+
static Definition = {
|
|
375
|
+
name: '${name.toLowerCase()}',
|
|
376
|
+
version: '1.0.0',
|
|
377
|
+
modules: {},
|
|
378
|
+
options: {
|
|
379
|
+
type: 'webhook',
|
|
380
|
+
hasUserConfig: true,
|
|
381
|
+
display: {
|
|
382
|
+
name: '${capitalize(name)}',
|
|
383
|
+
description: '${capitalize(name)} webhook-only integration for receiving external events',
|
|
384
|
+
category: '${options.category || 'Other'}',
|
|
385
|
+
icon: '${name.toLowerCase()}'
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
capabilities: {
|
|
389
|
+
auth: ['custom'],
|
|
390
|
+
webhooks: true
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
async onCreate({ integrationId }) {
|
|
395
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async onUpdate(params) {}
|
|
399
|
+
|
|
400
|
+
async onDelete(params) {}
|
|
401
|
+
|
|
402
|
+
async getConfigOptions() {
|
|
403
|
+
return {
|
|
404
|
+
jsonSchema: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
required: ['webhookSecret'],
|
|
407
|
+
properties: {
|
|
408
|
+
webhookSecret: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
title: 'Webhook Secret',
|
|
411
|
+
description: 'Secret for validating webhook signatures'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
uiSchema: {
|
|
416
|
+
webhookSecret: { 'ui:widget': 'password' }
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async testAuth() {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
verifySignature(body, signature, secret) {
|
|
426
|
+
const expected = crypto
|
|
427
|
+
.createHmac('sha256', secret)
|
|
428
|
+
.update(JSON.stringify(body))
|
|
429
|
+
.digest('hex');
|
|
430
|
+
return crypto.timingSafeEqual(
|
|
431
|
+
Buffer.from(signature || ''),
|
|
432
|
+
Buffer.from(expected)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async onWebhookReceived({ req, res }) {
|
|
437
|
+
const signature = req.headers['x-webhook-signature'] || req.headers['x-hub-signature-256'];
|
|
438
|
+
const config = this.getConfig();
|
|
439
|
+
|
|
440
|
+
if (config.webhookSecret && !this.verifySignature(req.body, signature, config.webhookSecret)) {
|
|
441
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await this.queueWebhook({
|
|
445
|
+
integrationId: req.params.integrationId,
|
|
446
|
+
body: req.body,
|
|
447
|
+
headers: req.headers
|
|
448
|
+
});
|
|
449
|
+
res.status(200).json({ received: true });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async onWebhook({ data }) {
|
|
453
|
+
const { body } = data;
|
|
454
|
+
// Process webhook event
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
459
|
+
`,
|
|
460
|
+
|
|
461
|
+
Sync: (name, options = {}) => `const { IntegrationBase } = require('@friggframework/core');
|
|
462
|
+
|
|
463
|
+
class ${capitalize(name)}Integration extends IntegrationBase {
|
|
464
|
+
static Definition = {
|
|
465
|
+
name: '${name.toLowerCase()}',
|
|
466
|
+
version: '1.0.0',
|
|
467
|
+
modules: {
|
|
468
|
+
source: { definition: require('@friggframework/api-module-${options.sourceModule || name.toLowerCase()}') },
|
|
469
|
+
target: { definition: require('@friggframework/api-module-${options.targetModule || name.toLowerCase()}') }
|
|
470
|
+
},
|
|
471
|
+
options: {
|
|
472
|
+
type: 'sync',
|
|
473
|
+
hasUserConfig: true,
|
|
474
|
+
display: {
|
|
475
|
+
name: '${capitalize(name)}',
|
|
476
|
+
description: '${capitalize(name)} sync integration for bidirectional data synchronization',
|
|
477
|
+
category: '${options.category || 'Other'}',
|
|
478
|
+
icon: '${name.toLowerCase()}'
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
capabilities: {
|
|
482
|
+
auth: ['oauth2'],
|
|
483
|
+
webhooks: true,
|
|
484
|
+
sync: { bidirectional: true, incremental: true, batchSize: 100 }
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
async onCreate({ integrationId }) {
|
|
489
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
490
|
+
// Initialize sync state
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async onUpdate(params) {
|
|
494
|
+
await this.validateConfig();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async onDelete(params) {
|
|
498
|
+
// Cleanup sync state
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async getConfigOptions() {
|
|
502
|
+
return {
|
|
503
|
+
jsonSchema: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
properties: {
|
|
506
|
+
syncDirection: {
|
|
507
|
+
type: 'string',
|
|
508
|
+
title: 'Sync Direction',
|
|
509
|
+
enum: ['source-to-target', 'target-to-source', 'bidirectional'],
|
|
510
|
+
default: 'bidirectional'
|
|
511
|
+
},
|
|
512
|
+
conflictResolution: {
|
|
513
|
+
type: 'string',
|
|
514
|
+
title: 'Conflict Resolution',
|
|
515
|
+
enum: ['source-wins', 'target-wins', 'newest-wins'],
|
|
516
|
+
default: 'newest-wins'
|
|
517
|
+
},
|
|
518
|
+
syncInterval: {
|
|
519
|
+
type: 'integer',
|
|
520
|
+
title: 'Sync Interval (minutes)',
|
|
521
|
+
default: 15,
|
|
522
|
+
minimum: 5
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
uiSchema: {}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async testAuth() {
|
|
531
|
+
const sourceModule = this.getModule('source');
|
|
532
|
+
const targetModule = this.getModule('target');
|
|
533
|
+
await sourceModule.testAuth();
|
|
534
|
+
await targetModule.testAuth();
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = { ${capitalize(name)}Integration };
|
|
540
|
+
`
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
function capitalize(str) {
|
|
544
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
class NPMRegistryService {
|
|
548
|
+
constructor() {
|
|
549
|
+
this.searchUrl = 'https://registry.npmjs.org/-/v1/search';
|
|
550
|
+
this.packageScope = '@friggframework';
|
|
551
|
+
this.modulePrefix = 'api-module-';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async searchApiModules(options = {}) {
|
|
555
|
+
const { category, limit = 250 } = options;
|
|
556
|
+
const searchQuery = `${this.packageScope}/${this.modulePrefix}`;
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const response = await axios.get(this.searchUrl, {
|
|
560
|
+
params: {
|
|
561
|
+
text: searchQuery,
|
|
562
|
+
size: limit,
|
|
563
|
+
quality: 0.65,
|
|
564
|
+
popularity: 0.98,
|
|
565
|
+
maintenance: 0.5
|
|
566
|
+
},
|
|
567
|
+
timeout: 10000
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
let modules = response.data.objects
|
|
571
|
+
.filter(obj => obj.package.name.startsWith(`${this.packageScope}/${this.modulePrefix}`))
|
|
572
|
+
.map(obj => this.formatPackageInfo(obj.package));
|
|
573
|
+
|
|
574
|
+
if (category && category !== 'all') {
|
|
575
|
+
modules = modules.filter(m => m.category === category);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return modules;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
formatPackageInfo(pkg) {
|
|
585
|
+
const name = pkg.name.replace(`${this.packageScope}/${this.modulePrefix}`, '');
|
|
586
|
+
return {
|
|
587
|
+
name,
|
|
588
|
+
fullName: pkg.name,
|
|
589
|
+
displayName: this.formatDisplayName(name),
|
|
590
|
+
version: pkg.version,
|
|
591
|
+
description: pkg.description || '',
|
|
592
|
+
category: this.categorizeModule(name, pkg.description || ''),
|
|
593
|
+
authType: this.inferAuthType(name, pkg.description || '')
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
formatDisplayName(name) {
|
|
598
|
+
return name
|
|
599
|
+
.split('-')
|
|
600
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
601
|
+
.join(' ');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
categorizeModule(name, description) {
|
|
605
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
606
|
+
const categories = {
|
|
607
|
+
'CRM': ['crm', 'customer', 'salesforce', 'hubspot', 'pipedrive', 'zoho', 'attio', 'copper'],
|
|
608
|
+
'Finance': ['accounting', 'quickbooks', 'xero', 'sage', 'invoice', 'billing', 'stripe', 'payment'],
|
|
609
|
+
'Communication': ['email', 'sms', 'chat', 'messaging', 'slack', 'discord', 'twilio', 'teams', 'intercom'],
|
|
610
|
+
'ECommerce': ['shop', 'commerce', 'shopify', 'woocommerce', 'magento', 'bigcommerce', 'store'],
|
|
611
|
+
'Marketing': ['marketing', 'campaign', 'mailchimp', 'sendgrid', 'marketo', 'constantcontact'],
|
|
612
|
+
'Analytics': ['analytics', 'tracking', 'mixpanel', 'segment', 'amplitude', 'google-analytics'],
|
|
613
|
+
'Storage': ['storage', 'drive', 'dropbox', 'box', 'onedrive', 'file', 'document'],
|
|
614
|
+
'Development': ['github', 'gitlab', 'bitbucket', 'jira', 'linear', 'notion', 'confluence'],
|
|
615
|
+
'Productivity': ['asana', 'monday', 'trello', 'clickup', 'basecamp', 'todoist', 'airtable'],
|
|
616
|
+
'Social': ['social', 'twitter', 'facebook', 'linkedin', 'instagram', 'youtube']
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
for (const [category, keywords] of Object.entries(categories)) {
|
|
620
|
+
if (keywords.some(keyword => text.includes(keyword))) {
|
|
621
|
+
return category;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return 'Other';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
inferAuthType(name, description) {
|
|
628
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
629
|
+
if (text.includes('api key') || text.includes('apikey')) {
|
|
630
|
+
return 'api-key';
|
|
631
|
+
}
|
|
632
|
+
return 'oauth2';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let gitCheckpointServiceInstance = null;
|
|
637
|
+
|
|
638
|
+
function setGitCheckpointService(service) {
|
|
639
|
+
gitCheckpointServiceInstance = service;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function validateSchemaHandler({ schemaType, content }) {
|
|
643
|
+
let parsed;
|
|
644
|
+
try {
|
|
645
|
+
parsed = typeof content === 'string' ? JSON.parse(content) : content;
|
|
646
|
+
} catch (e) {
|
|
647
|
+
return { valid: false, errors: [`Invalid JSON: ${e.message}`] };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const errors = [];
|
|
651
|
+
const warnings = [];
|
|
652
|
+
|
|
653
|
+
if (schemaType === 'integration-definition') {
|
|
654
|
+
if (!parsed.name) {
|
|
655
|
+
errors.push('name is required');
|
|
656
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(parsed.name)) {
|
|
657
|
+
errors.push('name must match pattern ^[a-zA-Z][a-zA-Z0-9_-]*$');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!parsed.version) {
|
|
661
|
+
errors.push('version is required');
|
|
662
|
+
} else if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/.test(parsed.version)) {
|
|
663
|
+
errors.push('version must follow semantic versioning (X.Y.Z or X.Y.Z-prerelease)');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (parsed.options?.type && !INTEGRATION_TYPES.includes(parsed.options.type)) {
|
|
667
|
+
errors.push(`options.type must be one of: ${INTEGRATION_TYPES.join(', ')}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (parsed.options?.display?.category && !INTEGRATION_CATEGORIES.includes(parsed.options.display.category)) {
|
|
671
|
+
errors.push(`options.display.category must be one of: ${INTEGRATION_CATEGORIES.join(', ')}`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (parsed.capabilities?.auth) {
|
|
675
|
+
const validAuth = ['oauth2', 'api-key', 'basic', 'token', 'custom'];
|
|
676
|
+
for (const auth of parsed.capabilities.auth) {
|
|
677
|
+
if (!validAuth.includes(auth)) {
|
|
678
|
+
errors.push(`capabilities.auth contains invalid value: ${auth}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (parsed.model?.status) {
|
|
684
|
+
const validStatus = ['active', 'inactive', 'error', 'pending', 'disabled'];
|
|
685
|
+
if (!validStatus.includes(parsed.model.status)) {
|
|
686
|
+
errors.push(`model.status must be one of: ${validStatus.join(', ')}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!parsed.modules || Object.keys(parsed.modules).length === 0) {
|
|
691
|
+
warnings.push('No modules defined - integration may not connect to external services');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (!parsed.options?.display?.name) {
|
|
695
|
+
warnings.push('Missing display.name - UI will use internal name');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (schemaType === 'api-module-definition') {
|
|
700
|
+
if (!parsed.name && !parsed.moduleName) {
|
|
701
|
+
errors.push('name or moduleName is required');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!parsed.authType && !parsed.requester?.baseUrl) {
|
|
705
|
+
warnings.push('No authType or baseUrl specified');
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (schemaType === 'app-definition') {
|
|
710
|
+
if (!parsed.name) {
|
|
711
|
+
errors.push('name is required');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!parsed.integrations || parsed.integrations.length === 0) {
|
|
715
|
+
warnings.push('No integrations defined in app');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
valid: errors.length === 0,
|
|
721
|
+
errors,
|
|
722
|
+
warnings: warnings.length > 0 ? warnings : undefined
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function getTemplateHandler({ category, integrationName, options = {} }) {
|
|
727
|
+
const templateFn = CATEGORY_TEMPLATES[category];
|
|
728
|
+
|
|
729
|
+
if (!templateFn) {
|
|
730
|
+
const availableCategories = Object.keys(CATEGORY_TEMPLATES);
|
|
731
|
+
return {
|
|
732
|
+
error: `Unknown category: ${category}. Available: ${availableCategories.join(', ')}`,
|
|
733
|
+
availableCategories
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const template = templateFn(integrationName || 'MyIntegration', options);
|
|
738
|
+
return {
|
|
739
|
+
template: template.trim(),
|
|
740
|
+
category,
|
|
741
|
+
integrationName: integrationName || 'MyIntegration',
|
|
742
|
+
suggestedFilename: `${(integrationName || 'my-integration').toLowerCase()}-integration.js`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function checkPatternsHandler({ code, fileType }) {
|
|
747
|
+
const violations = [];
|
|
748
|
+
const suggestions = [];
|
|
749
|
+
|
|
750
|
+
if (fileType === 'integration') {
|
|
751
|
+
if (!code.includes('extends IntegrationBase')) {
|
|
752
|
+
violations.push({
|
|
753
|
+
rule: 'extends-integration-base',
|
|
754
|
+
severity: 'error',
|
|
755
|
+
message: 'Integration must extend IntegrationBase',
|
|
756
|
+
suggestion: 'class YourIntegration extends IntegrationBase { ... }'
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (!code.includes('static Definition')) {
|
|
761
|
+
violations.push({
|
|
762
|
+
rule: 'static-definition',
|
|
763
|
+
severity: 'error',
|
|
764
|
+
message: 'Integration must have static Definition property',
|
|
765
|
+
suggestion: 'Add: static Definition = { name, version, modules, options, capabilities }'
|
|
766
|
+
});
|
|
767
|
+
} else {
|
|
768
|
+
if (!code.includes("name:") && !code.includes('name :')) {
|
|
769
|
+
violations.push({
|
|
770
|
+
rule: 'definition-name',
|
|
771
|
+
severity: 'error',
|
|
772
|
+
message: 'Definition must include name property'
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (!code.includes("version:") && !code.includes('version :')) {
|
|
776
|
+
violations.push({
|
|
777
|
+
rule: 'definition-version',
|
|
778
|
+
severity: 'error',
|
|
779
|
+
message: 'Definition must include version property'
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const lifecycleMethods = ['onCreate', 'onUpdate', 'onDelete', 'getConfigOptions', 'testAuth'];
|
|
785
|
+
const missingMethods = lifecycleMethods.filter(m => !code.includes(`async ${m}(`));
|
|
786
|
+
|
|
787
|
+
if (missingMethods.length > 0) {
|
|
788
|
+
violations.push({
|
|
789
|
+
rule: 'lifecycle-methods',
|
|
790
|
+
severity: 'warning',
|
|
791
|
+
message: `Missing lifecycle methods: ${missingMethods.join(', ')}`,
|
|
792
|
+
suggestion: `Consider implementing: ${missingMethods.map(m => `async ${m}() { }`).join(', ')}`
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (code.includes('webhooks: true') || code.includes("type: 'webhook'")) {
|
|
797
|
+
if (!code.includes('onWebhookReceived') || !code.includes('onWebhook')) {
|
|
798
|
+
violations.push({
|
|
799
|
+
rule: 'webhook-handlers',
|
|
800
|
+
severity: 'warning',
|
|
801
|
+
message: 'Webhooks enabled but handlers not implemented',
|
|
802
|
+
suggestion: 'Implement onWebhookReceived() and onWebhook() methods'
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!code.includes('updateIntegrationStatus')) {
|
|
808
|
+
suggestions.push({
|
|
809
|
+
rule: 'status-updates',
|
|
810
|
+
message: 'Consider calling updateIntegrationStatus in onCreate',
|
|
811
|
+
suggestion: "await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');"
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (fileType === 'api-module') {
|
|
817
|
+
if (!code.includes('class') || (!code.includes('extends') && !code.includes('Api'))) {
|
|
818
|
+
violations.push({
|
|
819
|
+
rule: 'api-class',
|
|
820
|
+
severity: 'warning',
|
|
821
|
+
message: 'API module should define a class (typically extending a base Api class)'
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!code.includes('testAuth')) {
|
|
826
|
+
violations.push({
|
|
827
|
+
rule: 'test-auth',
|
|
828
|
+
severity: 'warning',
|
|
829
|
+
message: 'API module should implement testAuth() method'
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
compliant: violations.filter(v => v.severity === 'error').length === 0,
|
|
836
|
+
violations,
|
|
837
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function listModulesHandler({ category }) {
|
|
842
|
+
const npmService = new NPMRegistryService();
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
const modules = await npmService.searchApiModules({ category });
|
|
846
|
+
return {
|
|
847
|
+
modules,
|
|
848
|
+
total: modules.length,
|
|
849
|
+
source: 'npm-registry'
|
|
850
|
+
};
|
|
851
|
+
} catch (error) {
|
|
852
|
+
return {
|
|
853
|
+
modules: [],
|
|
854
|
+
total: 0,
|
|
855
|
+
error: error.message,
|
|
856
|
+
source: 'npm-registry'
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function runTestsHandler({ testPattern, coverage = false, watch = false }) {
|
|
862
|
+
const args = ['jest'];
|
|
863
|
+
|
|
864
|
+
if (testPattern) {
|
|
865
|
+
args.push(testPattern);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (coverage) {
|
|
869
|
+
args.push('--coverage');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (watch) {
|
|
873
|
+
args.push('--watch');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
args.push('--passWithNoTests');
|
|
877
|
+
|
|
878
|
+
return new Promise((resolve) => {
|
|
879
|
+
const jestProcess = spawn('npx', args, {
|
|
880
|
+
cwd: process.cwd(),
|
|
881
|
+
env: { ...process.env, FORCE_COLOR: '0' }
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
let stdout = '';
|
|
885
|
+
let stderr = '';
|
|
886
|
+
|
|
887
|
+
jestProcess.stdout.on('data', (data) => {
|
|
888
|
+
stdout += data.toString();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
jestProcess.stderr.on('data', (data) => {
|
|
892
|
+
stderr += data.toString();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
jestProcess.on('close', (code) => {
|
|
896
|
+
const lines = (stdout + stderr).split('\n');
|
|
897
|
+
const summaryLine = lines.find(l => l.includes('Tests:') || l.includes('Test Suites:'));
|
|
898
|
+
const coverageLine = lines.find(l => l.includes('Coverage'));
|
|
899
|
+
|
|
900
|
+
let passed = 0;
|
|
901
|
+
let failed = 0;
|
|
902
|
+
let total = 0;
|
|
903
|
+
|
|
904
|
+
const testMatch = (stdout + stderr).match(/Tests:\s+(\d+)\s+passed/);
|
|
905
|
+
const failMatch = (stdout + stderr).match(/(\d+)\s+failed/);
|
|
906
|
+
const totalMatch = (stdout + stderr).match(/(\d+)\s+total/);
|
|
907
|
+
|
|
908
|
+
if (testMatch) passed = parseInt(testMatch[1]);
|
|
909
|
+
if (failMatch) failed = parseInt(failMatch[1]);
|
|
910
|
+
if (totalMatch) total = parseInt(totalMatch[1]);
|
|
911
|
+
|
|
912
|
+
resolve({
|
|
913
|
+
passed,
|
|
914
|
+
failed,
|
|
915
|
+
total,
|
|
916
|
+
exitCode: code,
|
|
917
|
+
success: code === 0,
|
|
918
|
+
summary: summaryLine || 'Tests completed',
|
|
919
|
+
coverage: coverageLine || (coverage ? 'Coverage data in ./coverage' : undefined),
|
|
920
|
+
output: stdout.substring(0, 5000)
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
jestProcess.on('error', (error) => {
|
|
925
|
+
resolve({
|
|
926
|
+
passed: 0,
|
|
927
|
+
failed: 0,
|
|
928
|
+
total: 0,
|
|
929
|
+
exitCode: 1,
|
|
930
|
+
success: false,
|
|
931
|
+
error: error.message
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function securityScanHandler({ code, scanType = 'full' }) {
|
|
938
|
+
const vulnerabilities = [];
|
|
939
|
+
|
|
940
|
+
if (scanType === 'full' || scanType === 'credentials') {
|
|
941
|
+
const credentialPatterns = [
|
|
942
|
+
{ pattern: /api[_-]?key\s*[:=]\s*['"][^'"]{10,}['"]/gi, type: 'API key' },
|
|
943
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]+['"]/gi, type: 'Password' },
|
|
944
|
+
{ pattern: /secret\s*[:=]\s*['"][^'"]{10,}['"]/gi, type: 'Secret' },
|
|
945
|
+
{ pattern: /token\s*[:=]\s*['"][^'"]{20,}['"]/gi, type: 'Token' },
|
|
946
|
+
{ pattern: /bearer\s+[a-zA-Z0-9._-]{20,}/gi, type: 'Bearer token' },
|
|
947
|
+
{ pattern: /aws[_-]?(access[_-]?key|secret)[_-]?id?\s*[:=]\s*['"][^'"]+['"]/gi, type: 'AWS credentials' }
|
|
948
|
+
];
|
|
949
|
+
|
|
950
|
+
for (const { pattern, type } of credentialPatterns) {
|
|
951
|
+
if (pattern.test(code)) {
|
|
952
|
+
vulnerabilities.push({
|
|
953
|
+
type: 'hardcoded-credential',
|
|
954
|
+
credentialType: type,
|
|
955
|
+
severity: 'high',
|
|
956
|
+
description: `Possible hardcoded ${type} detected`,
|
|
957
|
+
fix: 'Use environment variables: process.env.YOUR_SECRET_NAME'
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (scanType === 'full' || scanType === 'injection') {
|
|
964
|
+
if (/eval\s*\(/.test(code)) {
|
|
965
|
+
vulnerabilities.push({
|
|
966
|
+
type: 'code-injection',
|
|
967
|
+
severity: 'critical',
|
|
968
|
+
description: 'Use of eval() detected - potential code injection vulnerability',
|
|
969
|
+
fix: 'Avoid eval(). Use JSON.parse() for JSON, or safer alternatives'
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (/new\s+Function\s*\(/.test(code) && code.includes('req.')) {
|
|
974
|
+
vulnerabilities.push({
|
|
975
|
+
type: 'code-injection',
|
|
976
|
+
severity: 'high',
|
|
977
|
+
description: 'Dynamic Function constructor with user input detected',
|
|
978
|
+
fix: 'Avoid constructing functions from user input'
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (/\$\{.*req\.(body|query|params)/.test(code) && /exec|spawn/.test(code)) {
|
|
983
|
+
vulnerabilities.push({
|
|
984
|
+
type: 'command-injection',
|
|
985
|
+
severity: 'critical',
|
|
986
|
+
description: 'Potential command injection - user input in shell command',
|
|
987
|
+
fix: 'Sanitize input and use parameterized commands'
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (scanType === 'full' || scanType === 'validation') {
|
|
993
|
+
if (code.includes('req.body') && !code.includes('validate') && !code.includes('schema') && !code.includes('joi') && !code.includes('zod')) {
|
|
994
|
+
vulnerabilities.push({
|
|
995
|
+
type: 'missing-validation',
|
|
996
|
+
severity: 'medium',
|
|
997
|
+
description: 'Request body used without apparent validation',
|
|
998
|
+
fix: 'Add input validation using a schema library (Joi, Zod, AJV)'
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (/onWebhookReceived|onWebhook/.test(code) && !code.includes('signature') && !code.includes('verify') && !code.includes('hmac')) {
|
|
1003
|
+
vulnerabilities.push({
|
|
1004
|
+
type: 'missing-webhook-validation',
|
|
1005
|
+
severity: 'medium',
|
|
1006
|
+
description: 'Webhook handler without signature verification',
|
|
1007
|
+
fix: 'Implement HMAC signature verification for webhook security'
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
vulnerabilities,
|
|
1014
|
+
scanned: true,
|
|
1015
|
+
scanType,
|
|
1016
|
+
summary: vulnerabilities.length === 0
|
|
1017
|
+
? 'No vulnerabilities detected'
|
|
1018
|
+
: `Found ${vulnerabilities.length} potential issue(s)`
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function gitCheckpointHandler({ message }) {
|
|
1023
|
+
if (gitCheckpointServiceInstance) {
|
|
1024
|
+
try {
|
|
1025
|
+
const checkpoint = await gitCheckpointServiceInstance.createCheckpoint(message);
|
|
1026
|
+
return {
|
|
1027
|
+
checkpointId: checkpoint.id,
|
|
1028
|
+
hash: checkpoint.hash,
|
|
1029
|
+
message: checkpoint.message,
|
|
1030
|
+
timestamp: checkpoint.timestamp,
|
|
1031
|
+
hasPendingChanges: checkpoint.hasPendingChanges,
|
|
1032
|
+
rollbackCommand: `git reset --mixed ${checkpoint.hash}`
|
|
1033
|
+
};
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
return {
|
|
1036
|
+
error: error.message,
|
|
1037
|
+
fallback: true
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
const { stdout: hash } = await execAsync('git rev-parse HEAD');
|
|
1044
|
+
const { stdout: status } = await execAsync('git status --porcelain');
|
|
1045
|
+
|
|
1046
|
+
const checkpointId = `checkpoint-${Date.now()}-${hash.trim().substring(0, 8)}`;
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
checkpointId,
|
|
1050
|
+
hash: hash.trim(),
|
|
1051
|
+
message,
|
|
1052
|
+
timestamp: new Date().toISOString(),
|
|
1053
|
+
hasPendingChanges: status.trim().length > 0,
|
|
1054
|
+
rollbackCommand: `git reset --mixed ${hash.trim()}`
|
|
1055
|
+
};
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
return {
|
|
1058
|
+
error: `Git error: ${error.message}`,
|
|
1059
|
+
suggestion: 'Ensure you are in a git repository'
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function getExampleHandler({ pattern }) {
|
|
1065
|
+
const examples = {
|
|
1066
|
+
'crm-integration': {
|
|
1067
|
+
description: 'Complete CRM integration with contacts and deals sync',
|
|
1068
|
+
code: CATEGORY_TEMPLATES.CRM('hubspot', { webhooks: true }),
|
|
1069
|
+
relatedFiles: [
|
|
1070
|
+
'packages/core/integrations/integration-base.js',
|
|
1071
|
+
'api-module-library/packages/hubspot/*'
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
'webhook-handler': {
|
|
1075
|
+
description: 'Secure webhook handler with signature verification',
|
|
1076
|
+
code: `async onWebhookReceived({ req, res }) {
|
|
1077
|
+
const signature = req.headers['x-webhook-signature'];
|
|
1078
|
+
const secret = this.getConfig().webhookSecret;
|
|
1079
|
+
|
|
1080
|
+
if (!this.verifySignature(req.body, signature, secret)) {
|
|
1081
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
await this.queueWebhook({
|
|
1085
|
+
integrationId: req.params.integrationId,
|
|
1086
|
+
body: req.body,
|
|
1087
|
+
headers: req.headers
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
res.status(200).json({ received: true });
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
verifySignature(body, signature, secret) {
|
|
1094
|
+
const crypto = require('crypto');
|
|
1095
|
+
const expected = crypto
|
|
1096
|
+
.createHmac('sha256', secret)
|
|
1097
|
+
.update(JSON.stringify(body))
|
|
1098
|
+
.digest('hex');
|
|
1099
|
+
return crypto.timingSafeEqual(
|
|
1100
|
+
Buffer.from(signature || '', 'utf8'),
|
|
1101
|
+
Buffer.from(expected, 'utf8')
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async onWebhook({ data }) {
|
|
1106
|
+
const { body, headers } = data;
|
|
1107
|
+
const eventType = headers['x-event-type'] || body.event;
|
|
1108
|
+
|
|
1109
|
+
switch (eventType) {
|
|
1110
|
+
case 'contact.created':
|
|
1111
|
+
await this.handleContactCreated(body.data);
|
|
1112
|
+
break;
|
|
1113
|
+
case 'deal.updated':
|
|
1114
|
+
await this.handleDealUpdated(body.data);
|
|
1115
|
+
break;
|
|
1116
|
+
default:
|
|
1117
|
+
console.log('Unhandled event:', eventType);
|
|
1118
|
+
}
|
|
1119
|
+
}`
|
|
1120
|
+
},
|
|
1121
|
+
'form-config': {
|
|
1122
|
+
description: 'Dynamic form configuration with JSON Schema',
|
|
1123
|
+
code: `async getConfigOptions() {
|
|
1124
|
+
const module = this.getModule('myModule');
|
|
1125
|
+
const availableWorkspaces = await module.listWorkspaces();
|
|
1126
|
+
|
|
1127
|
+
return {
|
|
1128
|
+
jsonSchema: {
|
|
1129
|
+
type: 'object',
|
|
1130
|
+
required: ['workspace', 'syncDirection'],
|
|
1131
|
+
properties: {
|
|
1132
|
+
workspace: {
|
|
1133
|
+
type: 'string',
|
|
1134
|
+
title: 'Workspace',
|
|
1135
|
+
enum: availableWorkspaces.map(w => w.id),
|
|
1136
|
+
enumNames: availableWorkspaces.map(w => w.name)
|
|
1137
|
+
},
|
|
1138
|
+
syncDirection: {
|
|
1139
|
+
type: 'string',
|
|
1140
|
+
title: 'Sync Direction',
|
|
1141
|
+
enum: ['push', 'pull', 'bidirectional'],
|
|
1142
|
+
default: 'bidirectional'
|
|
1143
|
+
},
|
|
1144
|
+
syncInterval: {
|
|
1145
|
+
type: 'integer',
|
|
1146
|
+
title: 'Sync Interval (minutes)',
|
|
1147
|
+
minimum: 5,
|
|
1148
|
+
maximum: 1440,
|
|
1149
|
+
default: 60
|
|
1150
|
+
},
|
|
1151
|
+
enableNotifications: {
|
|
1152
|
+
type: 'boolean',
|
|
1153
|
+
title: 'Enable Notifications',
|
|
1154
|
+
default: true
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
uiSchema: {
|
|
1159
|
+
workspace: {
|
|
1160
|
+
'ui:placeholder': 'Select a workspace...'
|
|
1161
|
+
},
|
|
1162
|
+
syncInterval: {
|
|
1163
|
+
'ui:widget': 'range'
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async refreshConfigOptions({ configKey, currentConfig }) {
|
|
1170
|
+
if (configKey === 'workspace') {
|
|
1171
|
+
const module = this.getModule('myModule');
|
|
1172
|
+
const workspaces = await module.listWorkspaces();
|
|
1173
|
+
return {
|
|
1174
|
+
options: workspaces.map(w => ({ value: w.id, label: w.name }))
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}`
|
|
1179
|
+
},
|
|
1180
|
+
'oauth2-flow': {
|
|
1181
|
+
description: 'OAuth2 authentication flow implementation',
|
|
1182
|
+
code: `// In your API module (not integration)
|
|
1183
|
+
class MyServiceApi extends OAuth2Requester {
|
|
1184
|
+
constructor(params) {
|
|
1185
|
+
super(params);
|
|
1186
|
+
this.baseUrl = 'https://api.myservice.com';
|
|
1187
|
+
|
|
1188
|
+
this.URLs = {
|
|
1189
|
+
authorization: 'https://myservice.com/oauth/authorize',
|
|
1190
|
+
token: 'https://myservice.com/oauth/token',
|
|
1191
|
+
userInfo: '/api/v1/me'
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
getAuthorizationUri() {
|
|
1196
|
+
return this.authorizationUri({
|
|
1197
|
+
client_id: this.client_id,
|
|
1198
|
+
redirect_uri: this.redirect_uri,
|
|
1199
|
+
scope: 'read write',
|
|
1200
|
+
response_type: 'code'
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async getAccessToken(code) {
|
|
1205
|
+
return this._getAccessToken({
|
|
1206
|
+
code,
|
|
1207
|
+
grant_type: 'authorization_code',
|
|
1208
|
+
client_id: this.client_id,
|
|
1209
|
+
client_secret: this.client_secret,
|
|
1210
|
+
redirect_uri: this.redirect_uri
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async refreshAccessToken() {
|
|
1215
|
+
return this._refreshAccessToken({
|
|
1216
|
+
grant_type: 'refresh_token',
|
|
1217
|
+
refresh_token: this.refresh_token,
|
|
1218
|
+
client_id: this.client_id,
|
|
1219
|
+
client_secret: this.client_secret
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async testAuth() {
|
|
1224
|
+
const response = await this._get(this.URLs.userInfo);
|
|
1225
|
+
return { success: true, user: response };
|
|
1226
|
+
}
|
|
1227
|
+
}`
|
|
1228
|
+
},
|
|
1229
|
+
'sync-pattern': {
|
|
1230
|
+
description: 'Bidirectional data sync pattern',
|
|
1231
|
+
code: CATEGORY_TEMPLATES.Sync('DataSync', { category: 'Productivity' })
|
|
1232
|
+
},
|
|
1233
|
+
'api-module-complete': {
|
|
1234
|
+
description: 'Complete API module structure',
|
|
1235
|
+
code: `const { OAuth2Requester } = require('@friggframework/module-plugin');
|
|
1236
|
+
const { Credential } = require('./models/credential');
|
|
1237
|
+
const { Entity } = require('./models/entity');
|
|
1238
|
+
|
|
1239
|
+
class MyServiceApi extends OAuth2Requester {
|
|
1240
|
+
static Config = {
|
|
1241
|
+
name: 'MyService',
|
|
1242
|
+
authType: 'oauth2',
|
|
1243
|
+
hasTestAuth: true
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
constructor(params) {
|
|
1247
|
+
super(params);
|
|
1248
|
+
this.baseUrl = process.env.MYSERVICE_API_URL || 'https://api.myservice.com';
|
|
1249
|
+
this.tokenUri = 'https://myservice.com/oauth/token';
|
|
1250
|
+
this.authorizationUri = 'https://myservice.com/oauth/authorize';
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Auth methods
|
|
1254
|
+
getAuthorizationUri() { /* ... */ }
|
|
1255
|
+
async getAccessToken(code) { /* ... */ }
|
|
1256
|
+
async testAuth() {
|
|
1257
|
+
const user = await this._get('/api/v1/me');
|
|
1258
|
+
return { success: true, user };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Resource methods
|
|
1262
|
+
async listContacts(params = {}) {
|
|
1263
|
+
return this._get('/api/v1/contacts', { params });
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async getContact(id) {
|
|
1267
|
+
return this._get(\`/api/v1/contacts/\${id}\`);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async createContact(data) {
|
|
1271
|
+
return this._post('/api/v1/contacts', data);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async updateContact(id, data) {
|
|
1275
|
+
return this._patch(\`/api/v1/contacts/\${id}\`, data);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async deleteContact(id) {
|
|
1279
|
+
return this._delete(\`/api/v1/contacts/\${id}\`);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Webhook methods
|
|
1283
|
+
async registerWebhook(config) {
|
|
1284
|
+
return this._post('/api/v1/webhooks', config);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async deleteWebhook(id) {
|
|
1288
|
+
return this._delete(\`/api/v1/webhooks/\${id}\`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const Definition = {
|
|
1293
|
+
moduleName: 'myservice',
|
|
1294
|
+
Api: MyServiceApi,
|
|
1295
|
+
Credential,
|
|
1296
|
+
Entity
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
module.exports = { MyServiceApi, Definition };`
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const example = examples[pattern];
|
|
1304
|
+
|
|
1305
|
+
if (!example) {
|
|
1306
|
+
return {
|
|
1307
|
+
error: `Unknown pattern: ${pattern}`,
|
|
1308
|
+
availablePatterns: Object.keys(examples).map(key => ({
|
|
1309
|
+
name: key,
|
|
1310
|
+
description: examples[key].description
|
|
1311
|
+
}))
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return {
|
|
1316
|
+
pattern,
|
|
1317
|
+
description: example.description,
|
|
1318
|
+
code: example.code.trim(),
|
|
1319
|
+
relatedFiles: example.relatedFiles
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const DOCS_INDEX = {
|
|
1324
|
+
'integration-base': {
|
|
1325
|
+
title: 'IntegrationBase Class',
|
|
1326
|
+
path: 'packages/core/integrations/integration-base.js',
|
|
1327
|
+
topics: ['integration', 'lifecycle', 'modules', 'events', 'webhooks'],
|
|
1328
|
+
summary: 'Base class all integrations must extend. Provides lifecycle methods (onCreate, onUpdate, onDelete), module management, webhook handling, and status updates.'
|
|
1329
|
+
},
|
|
1330
|
+
'api-module': {
|
|
1331
|
+
title: 'API Module Development',
|
|
1332
|
+
path: 'docs/api-module-library/overview.md',
|
|
1333
|
+
topics: ['api-module', 'oauth2', 'api-key', 'requester'],
|
|
1334
|
+
summary: 'Guide to building API modules that connect to external services. Covers authentication types, requester patterns, and module structure.'
|
|
1335
|
+
},
|
|
1336
|
+
'forms-config': {
|
|
1337
|
+
title: 'Form Configuration (JSON Schema)',
|
|
1338
|
+
path: 'packages/core/integrations/options.js',
|
|
1339
|
+
topics: ['forms', 'json-schema', 'ui-schema', 'config-options'],
|
|
1340
|
+
summary: 'Dynamic form configuration using JSON Schema. Used in getConfigOptions() to define user-configurable settings.'
|
|
1341
|
+
},
|
|
1342
|
+
'webhooks': {
|
|
1343
|
+
title: 'Webhook Handling',
|
|
1344
|
+
path: 'packages/core/integrations/integration-base.js',
|
|
1345
|
+
topics: ['webhooks', 'signature', 'queue', 'events'],
|
|
1346
|
+
summary: 'Webhook implementation patterns including signature verification, event queuing, and processing. Methods: onWebhookReceived, onWebhook, queueWebhook.'
|
|
1347
|
+
},
|
|
1348
|
+
'encryption': {
|
|
1349
|
+
title: 'Field-Level Encryption',
|
|
1350
|
+
path: 'packages/core/database/encryption/README.md',
|
|
1351
|
+
topics: ['encryption', 'kms', 'aes', 'credentials', 'security'],
|
|
1352
|
+
summary: 'Transparent field-level encryption for sensitive data. Supports AWS KMS and AES. Automatically encrypts credentials, tokens, and mapping data.'
|
|
1353
|
+
},
|
|
1354
|
+
'repositories': {
|
|
1355
|
+
title: 'Repository Pattern',
|
|
1356
|
+
path: 'packages/core/integrations/repositories/',
|
|
1357
|
+
topics: ['repository', 'database', 'crud', 'prisma', 'mongo'],
|
|
1358
|
+
summary: 'Data access layer following repository pattern. Supports MongoDB, PostgreSQL via Prisma, and DocumentDB. Handles integration and mapping persistence.'
|
|
1359
|
+
},
|
|
1360
|
+
'use-cases': {
|
|
1361
|
+
title: 'Use Case Pattern',
|
|
1362
|
+
path: 'packages/core/integrations/use-cases/',
|
|
1363
|
+
topics: ['use-case', 'business-logic', 'orchestration'],
|
|
1364
|
+
summary: 'Business logic orchestration following hexagonal architecture. Use cases coordinate repositories and domain operations.'
|
|
1365
|
+
},
|
|
1366
|
+
'cli': {
|
|
1367
|
+
title: 'Frigg CLI',
|
|
1368
|
+
path: 'packages/devtools/frigg-cli/',
|
|
1369
|
+
topics: ['cli', 'install', 'deploy', 'start', 'validate'],
|
|
1370
|
+
summary: 'Command-line interface for Frigg development. Commands: install, search, start, deploy, validate. Manages API modules and infrastructure.'
|
|
1371
|
+
},
|
|
1372
|
+
'infrastructure': {
|
|
1373
|
+
title: 'Infrastructure as Code',
|
|
1374
|
+
path: 'packages/devtools/infrastructure/',
|
|
1375
|
+
topics: ['serverless', 'aws', 'lambda', 'vpc', 'deployment'],
|
|
1376
|
+
summary: 'AWS infrastructure generation and deployment. Creates serverless.yml, discovers VPC/KMS resources, manages IAM policies.'
|
|
1377
|
+
},
|
|
1378
|
+
'testing': {
|
|
1379
|
+
title: 'Testing Patterns',
|
|
1380
|
+
path: 'packages/core/integrations/test/',
|
|
1381
|
+
topics: ['testing', 'jest', 'mock', 'integration-test'],
|
|
1382
|
+
summary: 'Testing strategies for Frigg integrations. Includes mock API utilities, test doubles, and integration test patterns.'
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
async function searchDocsHandler({ query, topic, limit = 5 }) {
|
|
1387
|
+
const results = [];
|
|
1388
|
+
const queryLower = query.toLowerCase();
|
|
1389
|
+
const queryWords = queryLower.split(/\s+/);
|
|
1390
|
+
|
|
1391
|
+
for (const [key, doc] of Object.entries(DOCS_INDEX)) {
|
|
1392
|
+
let score = 0;
|
|
1393
|
+
|
|
1394
|
+
if (topic && doc.topics.includes(topic.toLowerCase())) {
|
|
1395
|
+
score += 50;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
for (const word of queryWords) {
|
|
1399
|
+
if (doc.title.toLowerCase().includes(word)) {
|
|
1400
|
+
score += 30;
|
|
1401
|
+
}
|
|
1402
|
+
if (doc.summary.toLowerCase().includes(word)) {
|
|
1403
|
+
score += 20;
|
|
1404
|
+
}
|
|
1405
|
+
if (doc.topics.some(t => t.includes(word))) {
|
|
1406
|
+
score += 25;
|
|
1407
|
+
}
|
|
1408
|
+
if (key.includes(word)) {
|
|
1409
|
+
score += 15;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (score > 0) {
|
|
1414
|
+
results.push({
|
|
1415
|
+
key,
|
|
1416
|
+
...doc,
|
|
1417
|
+
score
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
results.sort((a, b) => b.score - a.score);
|
|
1423
|
+
|
|
1424
|
+
return {
|
|
1425
|
+
query,
|
|
1426
|
+
topic,
|
|
1427
|
+
results: results.slice(0, limit).map(r => ({
|
|
1428
|
+
title: r.title,
|
|
1429
|
+
path: r.path,
|
|
1430
|
+
summary: r.summary,
|
|
1431
|
+
topics: r.topics,
|
|
1432
|
+
relevance: Math.min(100, r.score)
|
|
1433
|
+
})),
|
|
1434
|
+
totalMatches: results.length
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
async function readDocsHandler({ docKey, section }) {
|
|
1439
|
+
const doc = DOCS_INDEX[docKey];
|
|
1440
|
+
|
|
1441
|
+
if (!doc) {
|
|
1442
|
+
return {
|
|
1443
|
+
error: `Unknown documentation key: ${docKey}`,
|
|
1444
|
+
availableDocs: Object.entries(DOCS_INDEX).map(([key, d]) => ({
|
|
1445
|
+
key,
|
|
1446
|
+
title: d.title,
|
|
1447
|
+
topics: d.topics
|
|
1448
|
+
}))
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const content = {
|
|
1453
|
+
key: docKey,
|
|
1454
|
+
title: doc.title,
|
|
1455
|
+
path: doc.path,
|
|
1456
|
+
summary: doc.summary,
|
|
1457
|
+
topics: doc.topics
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
if (docKey === 'integration-base') {
|
|
1461
|
+
content.sections = {
|
|
1462
|
+
'static-definition': {
|
|
1463
|
+
title: 'Static Definition',
|
|
1464
|
+
content: `Integration classes must define a static Definition property:
|
|
1465
|
+
|
|
1466
|
+
\`\`\`javascript
|
|
1467
|
+
static Definition = {
|
|
1468
|
+
name: 'integration-name', // Unique identifier
|
|
1469
|
+
version: '1.0.0', // Semantic version
|
|
1470
|
+
modules: { // API modules used
|
|
1471
|
+
moduleName: { definition: require('@friggframework/api-module-name') }
|
|
1472
|
+
},
|
|
1473
|
+
options: {
|
|
1474
|
+
type: 'api', // api, webhook, sync, transform, custom
|
|
1475
|
+
hasUserConfig: true, // Requires user configuration
|
|
1476
|
+
display: {
|
|
1477
|
+
name: 'Display Name',
|
|
1478
|
+
description: 'Integration description',
|
|
1479
|
+
category: 'CRM', // CRM, Finance, Communication, etc.
|
|
1480
|
+
icon: 'icon-name'
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
capabilities: {
|
|
1484
|
+
auth: ['oauth2'], // oauth2, api-key, basic, token, custom
|
|
1485
|
+
webhooks: true,
|
|
1486
|
+
sync: { bidirectional: true, incremental: true }
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
\`\`\``
|
|
1490
|
+
},
|
|
1491
|
+
'lifecycle-methods': {
|
|
1492
|
+
title: 'Lifecycle Methods',
|
|
1493
|
+
content: `Required lifecycle methods:
|
|
1494
|
+
|
|
1495
|
+
\`\`\`javascript
|
|
1496
|
+
// Called when integration is created
|
|
1497
|
+
async onCreate({ integrationId }) {
|
|
1498
|
+
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Called when integration config is updated
|
|
1502
|
+
async onUpdate(params) {
|
|
1503
|
+
await this.validateConfig();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Called when integration is deleted
|
|
1507
|
+
async onDelete(params) {
|
|
1508
|
+
// Cleanup: unregister webhooks, clear data
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Returns form configuration
|
|
1512
|
+
async getConfigOptions() {
|
|
1513
|
+
return { jsonSchema: {}, uiSchema: {} };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Verify authentication is valid
|
|
1517
|
+
async testAuth() {
|
|
1518
|
+
const module = this.getModule('moduleName');
|
|
1519
|
+
return module.testAuth();
|
|
1520
|
+
}
|
|
1521
|
+
\`\`\``
|
|
1522
|
+
},
|
|
1523
|
+
'webhook-methods': {
|
|
1524
|
+
title: 'Webhook Methods',
|
|
1525
|
+
content: `Webhook handling methods:
|
|
1526
|
+
|
|
1527
|
+
\`\`\`javascript
|
|
1528
|
+
// HTTP handler - no database context
|
|
1529
|
+
async onWebhookReceived({ req, res }) {
|
|
1530
|
+
// Validate signature first
|
|
1531
|
+
const signature = req.headers['x-webhook-signature'];
|
|
1532
|
+
if (!this.verifySignature(req.body, signature)) {
|
|
1533
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Queue for processing
|
|
1537
|
+
await this.queueWebhook({
|
|
1538
|
+
integrationId: req.params.integrationId,
|
|
1539
|
+
body: req.body,
|
|
1540
|
+
headers: req.headers
|
|
1541
|
+
});
|
|
1542
|
+
res.status(200).json({ received: true });
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Queue worker - has database context
|
|
1546
|
+
async onWebhook({ data }) {
|
|
1547
|
+
const { body } = data;
|
|
1548
|
+
// Process webhook event with full integration context
|
|
1549
|
+
}
|
|
1550
|
+
\`\`\``
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (docKey === 'forms-config') {
|
|
1556
|
+
content.sections = {
|
|
1557
|
+
'json-schema': {
|
|
1558
|
+
title: 'JSON Schema',
|
|
1559
|
+
content: `Use JSON Schema to define configuration options:
|
|
1560
|
+
|
|
1561
|
+
\`\`\`javascript
|
|
1562
|
+
async getConfigOptions() {
|
|
1563
|
+
return {
|
|
1564
|
+
jsonSchema: {
|
|
1565
|
+
type: 'object',
|
|
1566
|
+
required: ['workspace'],
|
|
1567
|
+
properties: {
|
|
1568
|
+
workspace: {
|
|
1569
|
+
type: 'string',
|
|
1570
|
+
title: 'Workspace',
|
|
1571
|
+
enum: ['ws1', 'ws2'], // Static options
|
|
1572
|
+
enumNames: ['Workspace 1', 'Workspace 2']
|
|
1573
|
+
},
|
|
1574
|
+
syncEnabled: {
|
|
1575
|
+
type: 'boolean',
|
|
1576
|
+
title: 'Enable Sync',
|
|
1577
|
+
default: true
|
|
1578
|
+
},
|
|
1579
|
+
syncInterval: {
|
|
1580
|
+
type: 'integer',
|
|
1581
|
+
title: 'Sync Interval (minutes)',
|
|
1582
|
+
minimum: 5,
|
|
1583
|
+
maximum: 1440,
|
|
1584
|
+
default: 60
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
uiSchema: {
|
|
1589
|
+
workspace: { 'ui:placeholder': 'Select workspace...' },
|
|
1590
|
+
syncInterval: { 'ui:widget': 'range' }
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
\`\`\``
|
|
1595
|
+
},
|
|
1596
|
+
'dynamic-options': {
|
|
1597
|
+
title: 'Dynamic Options',
|
|
1598
|
+
content: `Fetch options dynamically from the API:
|
|
1599
|
+
|
|
1600
|
+
\`\`\`javascript
|
|
1601
|
+
async getConfigOptions() {
|
|
1602
|
+
const module = this.getModule('myModule');
|
|
1603
|
+
const workspaces = await module.listWorkspaces();
|
|
1604
|
+
|
|
1605
|
+
return {
|
|
1606
|
+
jsonSchema: {
|
|
1607
|
+
type: 'object',
|
|
1608
|
+
properties: {
|
|
1609
|
+
workspace: {
|
|
1610
|
+
type: 'string',
|
|
1611
|
+
title: 'Workspace',
|
|
1612
|
+
enum: workspaces.map(w => w.id),
|
|
1613
|
+
enumNames: workspaces.map(w => w.name)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
uiSchema: {}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Refresh specific option
|
|
1622
|
+
async refreshConfigOptions({ configKey, currentConfig }) {
|
|
1623
|
+
if (configKey === 'workspace') {
|
|
1624
|
+
const workspaces = await this.getModule('myModule').listWorkspaces();
|
|
1625
|
+
return { options: workspaces.map(w => ({ value: w.id, label: w.name })) };
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
\`\`\``
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (docKey === 'webhooks') {
|
|
1635
|
+
content.sections = {
|
|
1636
|
+
'signature-verification': {
|
|
1637
|
+
title: 'Signature Verification',
|
|
1638
|
+
content: `Always verify webhook signatures:
|
|
1639
|
+
|
|
1640
|
+
\`\`\`javascript
|
|
1641
|
+
const crypto = require('crypto');
|
|
1642
|
+
|
|
1643
|
+
verifySignature(body, signature, secret) {
|
|
1644
|
+
const expected = crypto
|
|
1645
|
+
.createHmac('sha256', secret)
|
|
1646
|
+
.update(JSON.stringify(body))
|
|
1647
|
+
.digest('hex');
|
|
1648
|
+
|
|
1649
|
+
// Use timing-safe comparison
|
|
1650
|
+
return crypto.timingSafeEqual(
|
|
1651
|
+
Buffer.from(signature || '', 'utf8'),
|
|
1652
|
+
Buffer.from(expected, 'utf8')
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
\`\`\``
|
|
1656
|
+
},
|
|
1657
|
+
'event-processing': {
|
|
1658
|
+
title: 'Event Processing',
|
|
1659
|
+
content: `Process webhook events in onWebhook:
|
|
1660
|
+
|
|
1661
|
+
\`\`\`javascript
|
|
1662
|
+
async onWebhook({ data }) {
|
|
1663
|
+
const { body, headers } = data;
|
|
1664
|
+
const eventType = headers['x-event-type'] || body.event;
|
|
1665
|
+
|
|
1666
|
+
switch (eventType) {
|
|
1667
|
+
case 'contact.created':
|
|
1668
|
+
await this.handleContactCreated(body.data);
|
|
1669
|
+
break;
|
|
1670
|
+
case 'deal.updated':
|
|
1671
|
+
await this.handleDealUpdated(body.data);
|
|
1672
|
+
break;
|
|
1673
|
+
case 'contact.deleted':
|
|
1674
|
+
await this.handleContactDeleted(body.data);
|
|
1675
|
+
break;
|
|
1676
|
+
default:
|
|
1677
|
+
console.log('Unhandled event:', eventType);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
\`\`\``
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (section && content.sections?.[section]) {
|
|
1686
|
+
return {
|
|
1687
|
+
...content,
|
|
1688
|
+
section: content.sections[section]
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
return content;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function createFriggMcpTools(options = {}) {
|
|
1696
|
+
if (options.gitCheckpointService) {
|
|
1697
|
+
setGitCheckpointService(options.gitCheckpointService);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
return [
|
|
1701
|
+
{
|
|
1702
|
+
name: 'frigg_validate_schema',
|
|
1703
|
+
description: 'Validate integration, API module, or app definitions against Frigg schemas. Checks required fields, valid enums, and structural correctness.',
|
|
1704
|
+
inputSchema: {
|
|
1705
|
+
type: 'object',
|
|
1706
|
+
required: ['schemaType', 'content'],
|
|
1707
|
+
properties: {
|
|
1708
|
+
schemaType: {
|
|
1709
|
+
type: 'string',
|
|
1710
|
+
enum: ['app-definition', 'integration-definition', 'api-module-definition'],
|
|
1711
|
+
description: 'Type of schema to validate against'
|
|
1712
|
+
},
|
|
1713
|
+
content: {
|
|
1714
|
+
type: 'string',
|
|
1715
|
+
description: 'JSON content to validate (as string or object)'
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
},
|
|
1719
|
+
handler: validateSchemaHandler
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
name: 'frigg_get_template',
|
|
1723
|
+
description: 'Get starter templates for Frigg integrations based on category (CRM, Finance, Communication, etc.) rather than auth type.',
|
|
1724
|
+
inputSchema: {
|
|
1725
|
+
type: 'object',
|
|
1726
|
+
required: ['category', 'integrationName'],
|
|
1727
|
+
properties: {
|
|
1728
|
+
category: {
|
|
1729
|
+
type: 'string',
|
|
1730
|
+
enum: ['CRM', 'Finance', 'Communication', 'ECommerce', 'Storage', 'Webhook', 'Sync'],
|
|
1731
|
+
description: 'Integration category determines the template structure'
|
|
1732
|
+
},
|
|
1733
|
+
integrationName: {
|
|
1734
|
+
type: 'string',
|
|
1735
|
+
description: 'Name for the integration (e.g., "hubspot", "stripe")'
|
|
1736
|
+
},
|
|
1737
|
+
options: {
|
|
1738
|
+
type: 'object',
|
|
1739
|
+
properties: {
|
|
1740
|
+
webhooks: { type: 'boolean', description: 'Include webhook handling' },
|
|
1741
|
+
authType: { type: 'string', description: 'Override default auth type' },
|
|
1742
|
+
sourceModule: { type: 'string', description: 'For Sync: source API module' },
|
|
1743
|
+
targetModule: { type: 'string', description: 'For Sync: target API module' }
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
handler: getTemplateHandler
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: 'frigg_check_patterns',
|
|
1752
|
+
description: 'Verify code follows Frigg architectural patterns. Checks for required base classes, lifecycle methods, and proper structure.',
|
|
1753
|
+
inputSchema: {
|
|
1754
|
+
type: 'object',
|
|
1755
|
+
required: ['code', 'fileType'],
|
|
1756
|
+
properties: {
|
|
1757
|
+
code: { type: 'string', description: 'Source code to analyze' },
|
|
1758
|
+
fileType: {
|
|
1759
|
+
type: 'string',
|
|
1760
|
+
enum: ['integration', 'api-module', 'handler', 'use-case', 'repository'],
|
|
1761
|
+
description: 'Type of file being checked'
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
handler: checkPatternsHandler
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
name: 'frigg_list_modules',
|
|
1769
|
+
description: 'List available Frigg API modules from npm registry. Searches @friggframework/api-module-* packages.',
|
|
1770
|
+
inputSchema: {
|
|
1771
|
+
type: 'object',
|
|
1772
|
+
properties: {
|
|
1773
|
+
category: {
|
|
1774
|
+
type: 'string',
|
|
1775
|
+
enum: ['CRM', 'Marketing', 'Communication', 'Finance', 'ECommerce', 'Analytics', 'Storage', 'Development', 'Productivity', 'Social', 'Other', 'all'],
|
|
1776
|
+
description: 'Filter by category'
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
handler: listModulesHandler
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
name: 'frigg_run_tests',
|
|
1784
|
+
description: 'Execute Jest tests for generated code',
|
|
1785
|
+
inputSchema: {
|
|
1786
|
+
type: 'object',
|
|
1787
|
+
properties: {
|
|
1788
|
+
testPattern: {
|
|
1789
|
+
type: 'string',
|
|
1790
|
+
description: 'Test file pattern or path (e.g., "integration.test.js")'
|
|
1791
|
+
},
|
|
1792
|
+
coverage: {
|
|
1793
|
+
type: 'boolean',
|
|
1794
|
+
description: 'Generate coverage report'
|
|
1795
|
+
},
|
|
1796
|
+
watch: {
|
|
1797
|
+
type: 'boolean',
|
|
1798
|
+
description: 'Run in watch mode'
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
handler: runTestsHandler
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
name: 'frigg_security_scan',
|
|
1806
|
+
description: 'Scan code for security vulnerabilities including hardcoded credentials, injection risks, and missing validation',
|
|
1807
|
+
inputSchema: {
|
|
1808
|
+
type: 'object',
|
|
1809
|
+
required: ['code'],
|
|
1810
|
+
properties: {
|
|
1811
|
+
code: { type: 'string', description: 'Code to scan' },
|
|
1812
|
+
scanType: {
|
|
1813
|
+
type: 'string',
|
|
1814
|
+
enum: ['full', 'credentials', 'injection', 'validation'],
|
|
1815
|
+
description: 'Type of security scan'
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
handler: securityScanHandler
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
name: 'frigg_git_checkpoint',
|
|
1823
|
+
description: 'Create git checkpoint before making changes. Records current HEAD for potential rollback.',
|
|
1824
|
+
inputSchema: {
|
|
1825
|
+
type: 'object',
|
|
1826
|
+
required: ['message'],
|
|
1827
|
+
properties: {
|
|
1828
|
+
message: {
|
|
1829
|
+
type: 'string',
|
|
1830
|
+
description: 'Checkpoint description'
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
},
|
|
1834
|
+
handler: gitCheckpointHandler
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
name: 'frigg_get_example',
|
|
1838
|
+
description: 'Get working examples of specific Frigg patterns and implementations',
|
|
1839
|
+
inputSchema: {
|
|
1840
|
+
type: 'object',
|
|
1841
|
+
required: ['pattern'],
|
|
1842
|
+
properties: {
|
|
1843
|
+
pattern: {
|
|
1844
|
+
type: 'string',
|
|
1845
|
+
enum: ['crm-integration', 'webhook-handler', 'form-config', 'oauth2-flow', 'sync-pattern', 'api-module-complete'],
|
|
1846
|
+
description: 'Pattern to get example for'
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
handler: getExampleHandler
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
name: 'frigg_search_docs',
|
|
1854
|
+
description: 'Search Frigg documentation by keyword or topic. Returns relevant documentation sections with relevance scores.',
|
|
1855
|
+
inputSchema: {
|
|
1856
|
+
type: 'object',
|
|
1857
|
+
required: ['query'],
|
|
1858
|
+
properties: {
|
|
1859
|
+
query: {
|
|
1860
|
+
type: 'string',
|
|
1861
|
+
description: 'Search query (e.g., "webhook signature", "json schema forms")'
|
|
1862
|
+
},
|
|
1863
|
+
topic: {
|
|
1864
|
+
type: 'string',
|
|
1865
|
+
enum: ['integration', 'api-module', 'webhooks', 'forms', 'encryption', 'testing', 'cli', 'deployment'],
|
|
1866
|
+
description: 'Filter by topic'
|
|
1867
|
+
},
|
|
1868
|
+
limit: {
|
|
1869
|
+
type: 'integer',
|
|
1870
|
+
description: 'Maximum number of results (default: 5)',
|
|
1871
|
+
default: 5
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
handler: searchDocsHandler
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
name: 'frigg_read_docs',
|
|
1879
|
+
description: 'Read detailed documentation for a specific Frigg topic with code examples.',
|
|
1880
|
+
inputSchema: {
|
|
1881
|
+
type: 'object',
|
|
1882
|
+
required: ['docKey'],
|
|
1883
|
+
properties: {
|
|
1884
|
+
docKey: {
|
|
1885
|
+
type: 'string',
|
|
1886
|
+
enum: ['integration-base', 'api-module', 'forms-config', 'webhooks', 'encryption', 'repositories', 'use-cases', 'cli', 'infrastructure', 'testing'],
|
|
1887
|
+
description: 'Documentation key to read'
|
|
1888
|
+
},
|
|
1889
|
+
section: {
|
|
1890
|
+
type: 'string',
|
|
1891
|
+
description: 'Specific section to read (e.g., "lifecycle-methods", "json-schema")'
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
handler: readDocsHandler
|
|
1896
|
+
}
|
|
1897
|
+
];
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
module.exports = {
|
|
1901
|
+
createFriggMcpTools,
|
|
1902
|
+
validateSchemaHandler,
|
|
1903
|
+
getTemplateHandler,
|
|
1904
|
+
checkPatternsHandler,
|
|
1905
|
+
listModulesHandler,
|
|
1906
|
+
runTestsHandler,
|
|
1907
|
+
securityScanHandler,
|
|
1908
|
+
gitCheckpointHandler,
|
|
1909
|
+
getExampleHandler,
|
|
1910
|
+
searchDocsHandler,
|
|
1911
|
+
readDocsHandler,
|
|
1912
|
+
setGitCheckpointService,
|
|
1913
|
+
NPMRegistryService,
|
|
1914
|
+
INTEGRATION_CATEGORIES,
|
|
1915
|
+
INTEGRATION_TYPES,
|
|
1916
|
+
CATEGORY_TEMPLATES,
|
|
1917
|
+
DOCS_INDEX
|
|
1918
|
+
};
|