@agenticmail/enterprise 0.5.170 → 0.5.172
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/dist/chunk-LSANOO23.js +898 -0
- package/dist/chunk-TX2QYPQN.js +2195 -0
- package/dist/chunk-XY3B3MYT.js +17682 -0
- package/dist/cli-agent-2XSLVRCT.js +1007 -0
- package/dist/cli-agent-MFI6ZMPW.js +1007 -0
- package/dist/cli-serve-VMOFSJFI.js +34 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/routes-RGIHFP65.js +6992 -0
- package/dist/runtime-OF4ISGPR.js +49 -0
- package/dist/server-UWD5SXCF.js +12 -0
- package/dist/setup-3ASZ4FJ5.js +20 -0
- package/package.json +1 -1
- package/src/agent-tools/tools/google/chat.ts +393 -0
- package/src/agent-tools/tools/google/forms.ts +367 -0
- package/src/agent-tools/tools/google/index.ts +9 -0
- package/src/agent-tools/tools/google/slides.ts +454 -0
- package/src/cli-agent.ts +16 -0
- package/src/engine/agent-routes.ts +6 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Forms API Tools
|
|
3
|
+
*
|
|
4
|
+
* Lets agents create forms, add questions, read responses, and manage form settings.
|
|
5
|
+
* Uses Google Forms API v1: https://forms.googleapis.com
|
|
6
|
+
*
|
|
7
|
+
* Required OAuth scopes:
|
|
8
|
+
* https://www.googleapis.com/auth/forms.body (create/edit forms)
|
|
9
|
+
* https://www.googleapis.com/auth/forms.responses.readonly (read responses)
|
|
10
|
+
* https://www.googleapis.com/auth/drive (for form creation via Drive)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AnyAgentTool, ToolCreationOptions } from '../../types.js';
|
|
14
|
+
import type { GoogleToolsConfig } from './index.js';
|
|
15
|
+
import { jsonResult, errorResult } from '../../common.js';
|
|
16
|
+
|
|
17
|
+
// ─── Helper ─────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
async function formsApi(token: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string> }): Promise<any> {
|
|
20
|
+
const url = new URL(`https://forms.googleapis.com/v1${path}`);
|
|
21
|
+
if (opts?.query) for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
22
|
+
const res = await fetch(url.toString(), {
|
|
23
|
+
method: opts?.method || 'GET',
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
...(opts?.body ? { 'Content-Type': 'application/json' } : {}),
|
|
27
|
+
},
|
|
28
|
+
...(opts?.body ? { body: JSON.stringify(opts.body) } : {}),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const errText = await res.text();
|
|
32
|
+
throw new Error(`Google Forms API ${res.status}: ${errText}`);
|
|
33
|
+
}
|
|
34
|
+
if (res.status === 204) return {};
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Tool Definitions ───────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function createGoogleFormsTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
|
|
41
|
+
const tp = config.tokenProvider;
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
// ─── Create Form ────────────────────────────────────
|
|
45
|
+
{
|
|
46
|
+
name: 'google_forms_create',
|
|
47
|
+
description: 'Create a new Google Form with a title and optional description. Returns the form ID and edit URL.',
|
|
48
|
+
category: 'productivity' as const,
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'object' as const,
|
|
51
|
+
properties: {
|
|
52
|
+
title: { type: 'string', description: 'Form title' },
|
|
53
|
+
documentTitle: { type: 'string', description: 'Document title (shown in Drive). Defaults to form title.' },
|
|
54
|
+
},
|
|
55
|
+
required: ['title'],
|
|
56
|
+
},
|
|
57
|
+
async execute(_id: string, input: any) {
|
|
58
|
+
try {
|
|
59
|
+
const token = await tp.getAccessToken();
|
|
60
|
+
const body: any = {
|
|
61
|
+
info: { title: input.title, documentTitle: input.documentTitle || input.title },
|
|
62
|
+
};
|
|
63
|
+
const result = await formsApi(token, '/forms', { method: 'POST', body });
|
|
64
|
+
return jsonResult({
|
|
65
|
+
formId: result.formId,
|
|
66
|
+
title: result.info?.title,
|
|
67
|
+
responderUri: result.responderUri,
|
|
68
|
+
editUrl: `https://docs.google.com/forms/d/${result.formId}/edit`,
|
|
69
|
+
responseUrl: `https://docs.google.com/forms/d/${result.formId}/viewanalytics`,
|
|
70
|
+
});
|
|
71
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// ─── Get Form ───────────────────────────────────────
|
|
76
|
+
{
|
|
77
|
+
name: 'google_forms_get',
|
|
78
|
+
description: 'Get a Google Form — title, description, all questions/items, and settings.',
|
|
79
|
+
category: 'productivity' as const,
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object' as const,
|
|
82
|
+
properties: {
|
|
83
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
84
|
+
},
|
|
85
|
+
required: ['formId'],
|
|
86
|
+
},
|
|
87
|
+
async execute(_id: string, input: any) {
|
|
88
|
+
try {
|
|
89
|
+
const token = await tp.getAccessToken();
|
|
90
|
+
const result = await formsApi(token, `/forms/${input.formId}`);
|
|
91
|
+
const items = (result.items || []).map((item: any) => {
|
|
92
|
+
const q = item.questionItem?.question;
|
|
93
|
+
const qg = item.questionGroupItem;
|
|
94
|
+
return {
|
|
95
|
+
itemId: item.itemId,
|
|
96
|
+
title: item.title,
|
|
97
|
+
description: item.description,
|
|
98
|
+
questionType: q?.choiceQuestion ? 'choice' : q?.textQuestion ? 'text' : q?.scaleQuestion ? 'scale' : q?.dateQuestion ? 'date' : q?.timeQuestion ? 'time' : q?.fileUploadQuestion ? 'fileUpload' : qg ? 'questionGroup' : item.pageBreakItem ? 'pageBreak' : item.textItem ? 'textBlock' : item.imageItem ? 'image' : item.videoItem ? 'video' : 'unknown',
|
|
99
|
+
required: q?.required || false,
|
|
100
|
+
choiceOptions: q?.choiceQuestion?.options?.map((o: any) => o.value) || undefined,
|
|
101
|
+
choiceType: q?.choiceQuestion?.type || undefined,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return jsonResult({
|
|
105
|
+
formId: result.formId,
|
|
106
|
+
title: result.info?.title,
|
|
107
|
+
description: result.info?.description,
|
|
108
|
+
responderUri: result.responderUri,
|
|
109
|
+
itemCount: items.length,
|
|
110
|
+
items,
|
|
111
|
+
});
|
|
112
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ─── Add Questions (batchUpdate) ────────────────────
|
|
117
|
+
{
|
|
118
|
+
name: 'google_forms_add_question',
|
|
119
|
+
description: 'Add a question to an existing Google Form. Supports text, multiple choice, checkbox, dropdown, scale, date, and time questions.',
|
|
120
|
+
category: 'productivity' as const,
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object' as const,
|
|
123
|
+
properties: {
|
|
124
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
125
|
+
title: { type: 'string', description: 'Question title/text' },
|
|
126
|
+
description: { type: 'string', description: 'Question description/help text' },
|
|
127
|
+
type: { type: 'string', description: 'Question type: "text", "paragraph", "multipleChoice", "checkbox", "dropdown", "scale", "date", "time" (default: "text")' },
|
|
128
|
+
required: { type: 'string', description: '"true" to make required' },
|
|
129
|
+
options: { type: 'string', description: 'Comma-separated options for choice questions (e.g. "Yes,No,Maybe")' },
|
|
130
|
+
scaleMin: { type: 'string', description: 'Scale min value (default: 1)' },
|
|
131
|
+
scaleMax: { type: 'string', description: 'Scale max value (default: 5)' },
|
|
132
|
+
scaleMinLabel: { type: 'string', description: 'Label for scale minimum' },
|
|
133
|
+
scaleMaxLabel: { type: 'string', description: 'Label for scale maximum' },
|
|
134
|
+
index: { type: 'string', description: 'Position index (0-based). Omit to append.' },
|
|
135
|
+
},
|
|
136
|
+
required: ['formId', 'title'],
|
|
137
|
+
},
|
|
138
|
+
async execute(_id: string, input: any) {
|
|
139
|
+
try {
|
|
140
|
+
const token = await tp.getAccessToken();
|
|
141
|
+
const qType = (input.type || 'text').toLowerCase();
|
|
142
|
+
|
|
143
|
+
let question: any = { required: input.required === 'true' };
|
|
144
|
+
|
|
145
|
+
if (qType === 'text') {
|
|
146
|
+
question.textQuestion = { paragraph: false };
|
|
147
|
+
} else if (qType === 'paragraph') {
|
|
148
|
+
question.textQuestion = { paragraph: true };
|
|
149
|
+
} else if (qType === 'multiplechoice' || qType === 'checkbox' || qType === 'dropdown') {
|
|
150
|
+
const typeMap: Record<string, string> = { multiplechoice: 'RADIO', checkbox: 'CHECKBOX', dropdown: 'DROP_DOWN' };
|
|
151
|
+
const opts = (input.options || '').split(',').map((o: string) => o.trim()).filter(Boolean);
|
|
152
|
+
question.choiceQuestion = {
|
|
153
|
+
type: typeMap[qType] || 'RADIO',
|
|
154
|
+
options: opts.map((v: string) => ({ value: v })),
|
|
155
|
+
};
|
|
156
|
+
} else if (qType === 'scale') {
|
|
157
|
+
question.scaleQuestion = {
|
|
158
|
+
low: parseInt(input.scaleMin || '1'),
|
|
159
|
+
high: parseInt(input.scaleMax || '5'),
|
|
160
|
+
lowLabel: input.scaleMinLabel || undefined,
|
|
161
|
+
highLabel: input.scaleMaxLabel || undefined,
|
|
162
|
+
};
|
|
163
|
+
} else if (qType === 'date') {
|
|
164
|
+
question.dateQuestion = { includeTime: false, includeYear: true };
|
|
165
|
+
} else if (qType === 'time') {
|
|
166
|
+
question.timeQuestion = { duration: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const item: any = {
|
|
170
|
+
title: input.title,
|
|
171
|
+
questionItem: { question },
|
|
172
|
+
};
|
|
173
|
+
if (input.description) item.description = input.description;
|
|
174
|
+
|
|
175
|
+
const request: any = {
|
|
176
|
+
createItem: {
|
|
177
|
+
item,
|
|
178
|
+
location: { index: input.index !== undefined ? parseInt(input.index) : undefined },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
// If no index, remove location so it appends
|
|
182
|
+
if (input.index === undefined) delete request.createItem.location;
|
|
183
|
+
|
|
184
|
+
const result = await formsApi(token, `/forms/${input.formId}:batchUpdate`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: { requests: [request] },
|
|
187
|
+
});
|
|
188
|
+
return jsonResult({ added: true, itemId: result.replies?.[0]?.createItem?.itemId });
|
|
189
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// ─── Update Form Info ───────────────────────────────
|
|
194
|
+
{
|
|
195
|
+
name: 'google_forms_update_info',
|
|
196
|
+
description: 'Update form title and/or description.',
|
|
197
|
+
category: 'productivity' as const,
|
|
198
|
+
parameters: {
|
|
199
|
+
type: 'object' as const,
|
|
200
|
+
properties: {
|
|
201
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
202
|
+
title: { type: 'string', description: 'New form title' },
|
|
203
|
+
description: { type: 'string', description: 'New form description' },
|
|
204
|
+
},
|
|
205
|
+
required: ['formId'],
|
|
206
|
+
},
|
|
207
|
+
async execute(_id: string, input: any) {
|
|
208
|
+
try {
|
|
209
|
+
const token = await tp.getAccessToken();
|
|
210
|
+
const updateMasks: string[] = [];
|
|
211
|
+
const info: any = {};
|
|
212
|
+
if (input.title) { info.title = input.title; updateMasks.push('info.title'); }
|
|
213
|
+
if (input.description !== undefined) { info.description = input.description; updateMasks.push('info.description'); }
|
|
214
|
+
if (updateMasks.length === 0) return errorResult('Provide title or description to update');
|
|
215
|
+
const result = await formsApi(token, `/forms/${input.formId}:batchUpdate`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: {
|
|
218
|
+
requests: [{
|
|
219
|
+
updateFormInfo: {
|
|
220
|
+
info,
|
|
221
|
+
updateMask: updateMasks.join(','),
|
|
222
|
+
},
|
|
223
|
+
}],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
return jsonResult({ updated: true });
|
|
227
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// ─── Delete Question ────────────────────────────────
|
|
232
|
+
{
|
|
233
|
+
name: 'google_forms_delete_item',
|
|
234
|
+
description: 'Delete a question/item from a form by its index position.',
|
|
235
|
+
category: 'productivity' as const,
|
|
236
|
+
parameters: {
|
|
237
|
+
type: 'object' as const,
|
|
238
|
+
properties: {
|
|
239
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
240
|
+
index: { type: 'string', description: 'Item index (0-based) to delete' },
|
|
241
|
+
},
|
|
242
|
+
required: ['formId', 'index'],
|
|
243
|
+
},
|
|
244
|
+
async execute(_id: string, input: any) {
|
|
245
|
+
try {
|
|
246
|
+
const token = await tp.getAccessToken();
|
|
247
|
+
await formsApi(token, `/forms/${input.formId}:batchUpdate`, {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
body: { requests: [{ deleteItem: { location: { index: parseInt(input.index) } } }] },
|
|
250
|
+
});
|
|
251
|
+
return jsonResult({ deleted: true });
|
|
252
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// ─── List Responses ─────────────────────────────────
|
|
257
|
+
{
|
|
258
|
+
name: 'google_forms_list_responses',
|
|
259
|
+
description: 'List all responses submitted to a Google Form. Returns respondent answers with timestamps.',
|
|
260
|
+
category: 'productivity' as const,
|
|
261
|
+
parameters: {
|
|
262
|
+
type: 'object' as const,
|
|
263
|
+
properties: {
|
|
264
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
265
|
+
pageSize: { type: 'string', description: 'Max responses (default: 50)' },
|
|
266
|
+
filter: { type: 'string', description: 'Filter by timestamp, e.g. "timestamp >= 2024-01-01T00:00:00Z"' },
|
|
267
|
+
},
|
|
268
|
+
required: ['formId'],
|
|
269
|
+
},
|
|
270
|
+
async execute(_id: string, input: any) {
|
|
271
|
+
try {
|
|
272
|
+
const token = await tp.getAccessToken();
|
|
273
|
+
const query: Record<string, string> = {};
|
|
274
|
+
if (input.pageSize) query.pageSize = input.pageSize;
|
|
275
|
+
if (input.filter) query.filter = input.filter;
|
|
276
|
+
const result = await formsApi(token, `/forms/${input.formId}/responses`, { query });
|
|
277
|
+
const responses = (result.responses || []).map((r: any) => {
|
|
278
|
+
const answers: Record<string, any> = {};
|
|
279
|
+
for (const [qId, ans] of Object.entries(r.answers || {})) {
|
|
280
|
+
const a = ans as any;
|
|
281
|
+
answers[qId] = {
|
|
282
|
+
questionId: qId,
|
|
283
|
+
textAnswers: a.textAnswers?.answers?.map((ta: any) => ta.value) || [],
|
|
284
|
+
fileUploadAnswers: a.fileUploadAnswers?.answers?.map((f: any) => ({ fileId: f.fileId, fileName: f.fileName })) || undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
responseId: r.responseId,
|
|
289
|
+
createTime: r.createTime,
|
|
290
|
+
lastSubmittedTime: r.lastSubmittedTime,
|
|
291
|
+
respondentEmail: r.respondentEmail,
|
|
292
|
+
totalScore: r.totalScore,
|
|
293
|
+
answers,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
return jsonResult({ responses, count: responses.length, nextPageToken: result.nextPageToken });
|
|
297
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
// ─── Get Single Response ────────────────────────────
|
|
302
|
+
{
|
|
303
|
+
name: 'google_forms_get_response',
|
|
304
|
+
description: 'Get a single form response by its response ID.',
|
|
305
|
+
category: 'productivity' as const,
|
|
306
|
+
parameters: {
|
|
307
|
+
type: 'object' as const,
|
|
308
|
+
properties: {
|
|
309
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
310
|
+
responseId: { type: 'string', description: 'Response ID' },
|
|
311
|
+
},
|
|
312
|
+
required: ['formId', 'responseId'],
|
|
313
|
+
},
|
|
314
|
+
async execute(_id: string, input: any) {
|
|
315
|
+
try {
|
|
316
|
+
const token = await tp.getAccessToken();
|
|
317
|
+
const r = await formsApi(token, `/forms/${input.formId}/responses/${input.responseId}`);
|
|
318
|
+
const answers: Record<string, any> = {};
|
|
319
|
+
for (const [qId, ans] of Object.entries(r.answers || {})) {
|
|
320
|
+
const a = ans as any;
|
|
321
|
+
answers[qId] = {
|
|
322
|
+
questionId: qId,
|
|
323
|
+
textAnswers: a.textAnswers?.answers?.map((ta: any) => ta.value) || [],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return jsonResult({
|
|
327
|
+
responseId: r.responseId,
|
|
328
|
+
createTime: r.createTime,
|
|
329
|
+
lastSubmittedTime: r.lastSubmittedTime,
|
|
330
|
+
respondentEmail: r.respondentEmail,
|
|
331
|
+
totalScore: r.totalScore,
|
|
332
|
+
answers,
|
|
333
|
+
});
|
|
334
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// ─── Set Publish Settings ───────────────────────────
|
|
339
|
+
{
|
|
340
|
+
name: 'google_forms_publish_settings',
|
|
341
|
+
description: 'Update publish settings of a form — control if it accepts responses, is published, etc.',
|
|
342
|
+
category: 'productivity' as const,
|
|
343
|
+
parameters: {
|
|
344
|
+
type: 'object' as const,
|
|
345
|
+
properties: {
|
|
346
|
+
formId: { type: 'string', description: 'Form ID' },
|
|
347
|
+
isPublished: { type: 'string', description: '"true" to publish, "false" to unpublish' },
|
|
348
|
+
isAcceptingResponses: { type: 'string', description: '"true" to accept responses, "false" to close' },
|
|
349
|
+
},
|
|
350
|
+
required: ['formId'],
|
|
351
|
+
},
|
|
352
|
+
async execute(_id: string, input: any) {
|
|
353
|
+
try {
|
|
354
|
+
const token = await tp.getAccessToken();
|
|
355
|
+
const publishSettings: any = {};
|
|
356
|
+
if (input.isPublished !== undefined) publishSettings.isPublished = input.isPublished === 'true';
|
|
357
|
+
if (input.isAcceptingResponses !== undefined) publishSettings.isAcceptingResponses = input.isAcceptingResponses === 'true';
|
|
358
|
+
const result = await formsApi(token, `/forms/${input.formId}:setPublishSettings`, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
body: { publishSettings },
|
|
361
|
+
});
|
|
362
|
+
return jsonResult({ updated: true });
|
|
363
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
}
|
|
@@ -13,6 +13,9 @@ export { createGoogleDocsTools } from './docs.js';
|
|
|
13
13
|
export { createGoogleContactsTools } from './contacts.js';
|
|
14
14
|
export { createMeetingTools } from './meetings.js';
|
|
15
15
|
export { createGoogleTasksTools } from './tasks.js';
|
|
16
|
+
export { createGoogleChatTools } from './chat.js';
|
|
17
|
+
export { createGoogleSlidesTools } from './slides.js';
|
|
18
|
+
export { createGoogleFormsTools } from './forms.js';
|
|
16
19
|
|
|
17
20
|
import type { AnyAgentTool, ToolCreationOptions } from '../../types.js';
|
|
18
21
|
import type { TokenProvider } from '../oauth-token-provider.js';
|
|
@@ -24,6 +27,9 @@ import { createGoogleDocsTools } from './docs.js';
|
|
|
24
27
|
import { createGoogleContactsTools } from './contacts.js';
|
|
25
28
|
import { createMeetingTools } from './meetings.js';
|
|
26
29
|
import { createGoogleTasksTools } from './tasks.js';
|
|
30
|
+
import { createGoogleChatTools } from './chat.js';
|
|
31
|
+
import { createGoogleSlidesTools } from './slides.js';
|
|
32
|
+
import { createGoogleFormsTools } from './forms.js';
|
|
27
33
|
|
|
28
34
|
export interface GoogleToolsConfig {
|
|
29
35
|
tokenProvider: TokenProvider;
|
|
@@ -47,5 +53,8 @@ export function createAllGoogleTools(config: GoogleToolsConfig, options?: ToolCr
|
|
|
47
53
|
...createGoogleContactsTools(config, options),
|
|
48
54
|
...createMeetingTools(config, options),
|
|
49
55
|
...createGoogleTasksTools(config.tokenProvider),
|
|
56
|
+
...createGoogleChatTools(config, options),
|
|
57
|
+
...createGoogleSlidesTools(config, options),
|
|
58
|
+
...createGoogleFormsTools(config, options),
|
|
50
59
|
];
|
|
51
60
|
}
|