@slates/google-people-recipes 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +479 -0
- package/dist/index.d.cts +754 -0
- package/dist/index.d.ts +754 -0
- package/dist/index.module.js +420 -0
- package/package.json +45 -0
- package/src/index.test.ts +78 -0
- package/src/index.ts +559 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { ServiceError, badRequestError } from "@lowerdeck/error";
|
|
3
|
+
import { defineToolRecipe } from "@slates/tool-recipes";
|
|
4
|
+
import { anyOf, createAxios } from "slates";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var GOOGLE_PEOPLE_API_BASE_URL = "https://people.googleapis.com/v1/";
|
|
7
|
+
var peopleAxios = createAxios({
|
|
8
|
+
baseURL: GOOGLE_PEOPLE_API_BASE_URL
|
|
9
|
+
});
|
|
10
|
+
var googlePeopleScopes = {
|
|
11
|
+
contactsReadonly: "https://www.googleapis.com/auth/contacts.readonly"
|
|
12
|
+
};
|
|
13
|
+
var googlePeopleReadonlyScopes = anyOf(googlePeopleScopes.contactsReadonly);
|
|
14
|
+
var DEFAULT_PERSON_FIELDS = "names,emailAddresses,phoneNumbers,addresses,organizations,birthdays,urls,biographies,events,genders,occupations,nicknames,relations,userDefined,memberships";
|
|
15
|
+
var READONLY_PERSON_FIELDS = "names,emailAddresses,phoneNumbers";
|
|
16
|
+
var nameSchema = z.object({
|
|
17
|
+
displayName: z.string().optional().describe("Full display name"),
|
|
18
|
+
givenName: z.string().optional().describe("First/given name"),
|
|
19
|
+
familyName: z.string().optional().describe("Last/family name"),
|
|
20
|
+
middleName: z.string().optional().describe("Middle name"),
|
|
21
|
+
prefix: z.string().optional().describe("Name prefix (e.g., Mr., Dr.)"),
|
|
22
|
+
suffix: z.string().optional().describe("Name suffix (e.g., Jr., III)")
|
|
23
|
+
}).describe("Name of the contact");
|
|
24
|
+
var emailSchema = z.object({
|
|
25
|
+
value: z.string().describe("Email address"),
|
|
26
|
+
type: z.string().optional().describe("Type of email (e.g., home, work, other)")
|
|
27
|
+
}).describe("Email address");
|
|
28
|
+
var phoneSchema = z.object({
|
|
29
|
+
value: z.string().describe("Phone number"),
|
|
30
|
+
type: z.string().optional().describe("Type of phone (e.g., home, work, mobile)")
|
|
31
|
+
}).describe("Phone number");
|
|
32
|
+
var addressSchema = z.object({
|
|
33
|
+
streetAddress: z.string().optional().describe("Street address"),
|
|
34
|
+
city: z.string().optional().describe("City"),
|
|
35
|
+
region: z.string().optional().describe("State or region"),
|
|
36
|
+
postalCode: z.string().optional().describe("Postal/ZIP code"),
|
|
37
|
+
country: z.string().optional().describe("Country"),
|
|
38
|
+
type: z.string().optional().describe("Type of address (e.g., home, work)")
|
|
39
|
+
}).describe("Physical address");
|
|
40
|
+
var organizationSchema = z.object({
|
|
41
|
+
name: z.string().optional().describe("Organization name"),
|
|
42
|
+
title: z.string().optional().describe("Job title"),
|
|
43
|
+
department: z.string().optional().describe("Department name")
|
|
44
|
+
}).describe("Organization/company");
|
|
45
|
+
var dateFieldSchema = z.object({
|
|
46
|
+
year: z.number().optional().describe("Year (omit for recurring events)"),
|
|
47
|
+
month: z.number().optional().describe("Month (1-12)"),
|
|
48
|
+
day: z.number().optional().describe("Day of month (1-31)")
|
|
49
|
+
}).describe("Date");
|
|
50
|
+
var birthdaySchema = z.object({
|
|
51
|
+
date: dateFieldSchema.optional()
|
|
52
|
+
}).describe("Birthday");
|
|
53
|
+
var urlSchema = z.object({
|
|
54
|
+
value: z.string().describe("URL"),
|
|
55
|
+
type: z.string().optional().describe("Type of URL (e.g., homePage, blog, profile)")
|
|
56
|
+
}).describe("URL");
|
|
57
|
+
var biographySchema = z.object({
|
|
58
|
+
value: z.string().describe("Biography or notes text")
|
|
59
|
+
}).describe("Biography/notes");
|
|
60
|
+
var customFieldSchema = z.object({
|
|
61
|
+
key: z.string().describe("Custom field label"),
|
|
62
|
+
value: z.string().describe("Custom field value")
|
|
63
|
+
}).describe("Custom field");
|
|
64
|
+
var nicknameSchema = z.object({
|
|
65
|
+
value: z.string().describe("Nickname")
|
|
66
|
+
}).describe("Nickname");
|
|
67
|
+
var relationSchema = z.object({
|
|
68
|
+
person: z.string().describe("Name of the related person"),
|
|
69
|
+
type: z.string().optional().describe("Relationship type (e.g., spouse, parent, child, friend)")
|
|
70
|
+
}).describe("Relationship");
|
|
71
|
+
var eventSchema = z.object({
|
|
72
|
+
date: dateFieldSchema.optional(),
|
|
73
|
+
type: z.string().optional().describe("Type of event (e.g., anniversary)")
|
|
74
|
+
}).describe("Event/date");
|
|
75
|
+
var occupationSchema = z.object({
|
|
76
|
+
value: z.string().describe("Occupation")
|
|
77
|
+
}).describe("Occupation");
|
|
78
|
+
var membershipSchema = z.object({
|
|
79
|
+
contactGroupResourceName: z.string().optional().describe("Resource name of the contact group")
|
|
80
|
+
}).describe("Group membership");
|
|
81
|
+
var contactInputSchema = z.object({
|
|
82
|
+
names: z.array(nameSchema).optional().describe("Names for the contact"),
|
|
83
|
+
emailAddresses: z.array(emailSchema).optional().describe("Email addresses"),
|
|
84
|
+
phoneNumbers: z.array(phoneSchema).optional().describe("Phone numbers"),
|
|
85
|
+
addresses: z.array(addressSchema).optional().describe("Physical addresses"),
|
|
86
|
+
organizations: z.array(organizationSchema).optional().describe("Organizations/companies"),
|
|
87
|
+
birthdays: z.array(birthdaySchema).optional().describe("Birthdays"),
|
|
88
|
+
urls: z.array(urlSchema).optional().describe("URLs"),
|
|
89
|
+
biographies: z.array(biographySchema).optional().describe("Biographies/notes"),
|
|
90
|
+
userDefined: z.array(customFieldSchema).optional().describe("Custom fields"),
|
|
91
|
+
nicknames: z.array(nicknameSchema).optional().describe("Nicknames"),
|
|
92
|
+
relations: z.array(relationSchema).optional().describe("Relationships"),
|
|
93
|
+
events: z.array(eventSchema).optional().describe("Events/dates"),
|
|
94
|
+
occupations: z.array(occupationSchema).optional().describe("Occupations")
|
|
95
|
+
});
|
|
96
|
+
var contactOutputSchema = z.object({
|
|
97
|
+
resourceName: z.string().describe("Unique resource name (e.g., people/c12345)"),
|
|
98
|
+
etag: z.string().optional().describe("ETag for concurrency control, required when updating"),
|
|
99
|
+
names: z.array(nameSchema).optional(),
|
|
100
|
+
emailAddresses: z.array(emailSchema).optional(),
|
|
101
|
+
phoneNumbers: z.array(phoneSchema).optional(),
|
|
102
|
+
addresses: z.array(addressSchema).optional(),
|
|
103
|
+
organizations: z.array(organizationSchema).optional(),
|
|
104
|
+
birthdays: z.array(birthdaySchema).optional(),
|
|
105
|
+
urls: z.array(urlSchema).optional(),
|
|
106
|
+
biographies: z.array(biographySchema).optional(),
|
|
107
|
+
userDefined: z.array(customFieldSchema).optional(),
|
|
108
|
+
nicknames: z.array(nicknameSchema).optional(),
|
|
109
|
+
relations: z.array(relationSchema).optional(),
|
|
110
|
+
events: z.array(eventSchema).optional(),
|
|
111
|
+
occupations: z.array(occupationSchema).optional(),
|
|
112
|
+
memberships: z.array(membershipSchema).optional()
|
|
113
|
+
});
|
|
114
|
+
var listContactsInputSchema = z.object({
|
|
115
|
+
pageSize: z.number().optional().describe("Number of contacts to return per page (max 1000, default 100)"),
|
|
116
|
+
pageToken: z.string().optional().describe("Token for fetching the next page of results"),
|
|
117
|
+
sortOrder: z.enum([
|
|
118
|
+
"LAST_MODIFIED_ASCENDING",
|
|
119
|
+
"LAST_MODIFIED_DESCENDING",
|
|
120
|
+
"FIRST_NAME_ASCENDING",
|
|
121
|
+
"LAST_NAME_ASCENDING"
|
|
122
|
+
]).optional().describe("Sort order for the results")
|
|
123
|
+
});
|
|
124
|
+
var listContactsOutputSchema = z.object({
|
|
125
|
+
contacts: z.array(contactOutputSchema).describe("List of contacts"),
|
|
126
|
+
nextPageToken: z.string().optional().describe("Token for fetching the next page"),
|
|
127
|
+
totalPeople: z.number().optional().describe("Total number of contacts"),
|
|
128
|
+
totalItems: z.number().optional().describe("Total number of items in response")
|
|
129
|
+
});
|
|
130
|
+
var searchContactsInputSchema = z.object({
|
|
131
|
+
query: z.string().describe(
|
|
132
|
+
"Search query \u2014 matches against names, emails, phone numbers, and other contact fields"
|
|
133
|
+
),
|
|
134
|
+
pageSize: z.number().optional().describe("Maximum number of results to return (default 30)")
|
|
135
|
+
});
|
|
136
|
+
var searchContactsOutputSchema = z.object({
|
|
137
|
+
contacts: z.array(contactOutputSchema).describe("Matching contacts")
|
|
138
|
+
});
|
|
139
|
+
var getContactInputSchema = z.object({
|
|
140
|
+
resourceName: z.string().describe('Resource name of the contact (e.g., "people/c12345" or "people/me")')
|
|
141
|
+
});
|
|
142
|
+
var formatContact = (person) => {
|
|
143
|
+
return {
|
|
144
|
+
resourceName: person.resourceName,
|
|
145
|
+
etag: person.etag,
|
|
146
|
+
names: person.names,
|
|
147
|
+
emailAddresses: person.emailAddresses,
|
|
148
|
+
phoneNumbers: person.phoneNumbers,
|
|
149
|
+
addresses: person.addresses,
|
|
150
|
+
organizations: person.organizations,
|
|
151
|
+
birthdays: person.birthdays,
|
|
152
|
+
urls: person.urls,
|
|
153
|
+
biographies: person.biographies,
|
|
154
|
+
userDefined: person.userDefined,
|
|
155
|
+
nicknames: person.nicknames,
|
|
156
|
+
relations: person.relations,
|
|
157
|
+
events: person.events,
|
|
158
|
+
occupations: person.occupations,
|
|
159
|
+
memberships: person.memberships?.map((membership) => ({
|
|
160
|
+
contactGroupResourceName: membership.contactGroupMembership?.contactGroupResourceName
|
|
161
|
+
}))
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
165
|
+
var addMessage = (messages, value) => {
|
|
166
|
+
if (typeof value !== "string") return;
|
|
167
|
+
let trimmed = value.trim();
|
|
168
|
+
if (trimmed && !messages.includes(trimmed)) {
|
|
169
|
+
messages.push(trimmed);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var collectMessages = (value, messages) => {
|
|
173
|
+
if (!isRecord(value)) {
|
|
174
|
+
addMessage(messages, value);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (let key of ["message", "error_description", "error", "detail", "reason"]) {
|
|
178
|
+
addMessage(messages, value[key]);
|
|
179
|
+
}
|
|
180
|
+
let nestedError = value.error;
|
|
181
|
+
if (isRecord(nestedError)) {
|
|
182
|
+
collectMessages(nestedError, messages);
|
|
183
|
+
}
|
|
184
|
+
let errors = value.errors;
|
|
185
|
+
if (Array.isArray(errors)) {
|
|
186
|
+
for (let item of errors) {
|
|
187
|
+
collectMessages(item, messages);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var extractGooglePeopleMessage = (error) => {
|
|
192
|
+
let response = isRecord(error) ? error.response : void 0;
|
|
193
|
+
let messages = [];
|
|
194
|
+
collectMessages(response?.data, messages);
|
|
195
|
+
if (isRecord(error)) {
|
|
196
|
+
collectMessages(error.data, messages);
|
|
197
|
+
}
|
|
198
|
+
if (messages.length > 0) {
|
|
199
|
+
return messages.join(" - ");
|
|
200
|
+
}
|
|
201
|
+
if (error instanceof Error && error.message) {
|
|
202
|
+
return error.message;
|
|
203
|
+
}
|
|
204
|
+
return "Unknown error";
|
|
205
|
+
};
|
|
206
|
+
var getErrorStatus = (error) => {
|
|
207
|
+
if (!isRecord(error)) return void 0;
|
|
208
|
+
let response = error.response;
|
|
209
|
+
if (typeof response?.status === "number") return response.status;
|
|
210
|
+
if (typeof error.status === "number") return error.status;
|
|
211
|
+
if (isRecord(error.data) && typeof error.data.status === "number") return error.data.status;
|
|
212
|
+
if (isRecord(error.upstream) && typeof error.upstream.status === "number") {
|
|
213
|
+
return error.upstream.status;
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
216
|
+
};
|
|
217
|
+
var getErrorStatusText = (error) => {
|
|
218
|
+
if (!isRecord(error)) return void 0;
|
|
219
|
+
let response = error.response;
|
|
220
|
+
if (typeof response?.statusText === "string") return response.statusText;
|
|
221
|
+
if (isRecord(error.upstream) && typeof error.upstream.statusText === "string") {
|
|
222
|
+
return error.upstream.statusText;
|
|
223
|
+
}
|
|
224
|
+
return void 0;
|
|
225
|
+
};
|
|
226
|
+
var googlePeopleServiceError = (message) => new ServiceError(badRequestError({ message }));
|
|
227
|
+
var googlePeopleApiError = (error, operation = "request") => {
|
|
228
|
+
if (error instanceof ServiceError) {
|
|
229
|
+
return error;
|
|
230
|
+
}
|
|
231
|
+
let status = getErrorStatus(error);
|
|
232
|
+
let statusText = getErrorStatusText(error);
|
|
233
|
+
let statusLabel = status !== void 0 ? `HTTP ${status}${statusText ? ` ${statusText}` : ""}: ` : "";
|
|
234
|
+
let serviceError = googlePeopleServiceError(
|
|
235
|
+
`Google People API ${operation} failed: ${statusLabel}${extractGooglePeopleMessage(error)}`
|
|
236
|
+
);
|
|
237
|
+
serviceError.data.reason = "google_people_api_error";
|
|
238
|
+
serviceError.data.upstreamStatus = status;
|
|
239
|
+
if (error instanceof Error) {
|
|
240
|
+
serviceError.setParent(error);
|
|
241
|
+
}
|
|
242
|
+
return serviceError;
|
|
243
|
+
};
|
|
244
|
+
var GooglePeopleClient = class {
|
|
245
|
+
constructor(config) {
|
|
246
|
+
this.config = config;
|
|
247
|
+
this.api = config.api ?? peopleAxios;
|
|
248
|
+
}
|
|
249
|
+
config;
|
|
250
|
+
api;
|
|
251
|
+
headers() {
|
|
252
|
+
return {
|
|
253
|
+
Authorization: `Bearer ${this.config.token}`
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async getContact(resourceName, personFields) {
|
|
257
|
+
try {
|
|
258
|
+
let response = await this.api.get(resourceName, {
|
|
259
|
+
params: { personFields: personFields || DEFAULT_PERSON_FIELDS },
|
|
260
|
+
headers: this.headers()
|
|
261
|
+
});
|
|
262
|
+
return response.data;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw googlePeopleApiError(error, "get contact");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async listContacts(params) {
|
|
268
|
+
try {
|
|
269
|
+
let response = await this.api.get("people/me/connections", {
|
|
270
|
+
params: {
|
|
271
|
+
personFields: params.personFields || DEFAULT_PERSON_FIELDS,
|
|
272
|
+
pageSize: params.pageSize || 100,
|
|
273
|
+
pageToken: params.pageToken,
|
|
274
|
+
sortOrder: params.sortOrder,
|
|
275
|
+
syncToken: params.syncToken,
|
|
276
|
+
requestSyncToken: params.requestSyncToken
|
|
277
|
+
},
|
|
278
|
+
headers: this.headers()
|
|
279
|
+
});
|
|
280
|
+
return response.data;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw googlePeopleApiError(error, "list contacts");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async searchContacts(query, personFields, pageSize) {
|
|
286
|
+
try {
|
|
287
|
+
let response = await this.api.get("people:searchContacts", {
|
|
288
|
+
params: {
|
|
289
|
+
query,
|
|
290
|
+
readMask: personFields || DEFAULT_PERSON_FIELDS,
|
|
291
|
+
pageSize: pageSize || 30
|
|
292
|
+
},
|
|
293
|
+
headers: this.headers()
|
|
294
|
+
});
|
|
295
|
+
return response.data;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw googlePeopleApiError(error, "search contacts");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var createGooglePeopleClient = (ctx) => new GooglePeopleClient({ token: ctx.auth.token });
|
|
302
|
+
var listContactsRecipe = defineToolRecipe({
|
|
303
|
+
name: "List Contacts",
|
|
304
|
+
key: "list_contacts",
|
|
305
|
+
description: `Lists the authenticated user's contacts with pagination support. Returns contacts sorted by the specified order. Use the \`pageToken\` from a previous response to fetch the next page.`,
|
|
306
|
+
tags: {
|
|
307
|
+
destructive: false,
|
|
308
|
+
readOnly: true
|
|
309
|
+
},
|
|
310
|
+
defaultScopes: googlePeopleReadonlyScopes,
|
|
311
|
+
inputSchema: listContactsInputSchema,
|
|
312
|
+
outputSchema: listContactsOutputSchema,
|
|
313
|
+
handleInvocation: async ({
|
|
314
|
+
ctx,
|
|
315
|
+
dependencies
|
|
316
|
+
}) => {
|
|
317
|
+
let client = dependencies.createClient(ctx);
|
|
318
|
+
let result = await client.listContacts({
|
|
319
|
+
pageSize: ctx.input.pageSize,
|
|
320
|
+
pageToken: ctx.input.pageToken,
|
|
321
|
+
sortOrder: ctx.input.sortOrder
|
|
322
|
+
});
|
|
323
|
+
let contacts = (result.connections || []).map(formatContact);
|
|
324
|
+
return {
|
|
325
|
+
output: {
|
|
326
|
+
contacts,
|
|
327
|
+
nextPageToken: result.nextPageToken,
|
|
328
|
+
totalPeople: result.totalPeople,
|
|
329
|
+
totalItems: result.totalItems
|
|
330
|
+
},
|
|
331
|
+
message: `Listed **${contacts.length}** contacts${result.totalPeople ? ` out of ${result.totalPeople} total` : ""}.${result.nextPageToken ? " More pages available." : ""}`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
var searchContactsRecipe = defineToolRecipe({
|
|
336
|
+
name: "Search Contacts",
|
|
337
|
+
key: "search_contacts",
|
|
338
|
+
description: `Searches the authenticated user's contacts by name, email address, phone number, or other fields. Returns matching contacts ranked by relevance.`,
|
|
339
|
+
tags: {
|
|
340
|
+
destructive: false,
|
|
341
|
+
readOnly: true
|
|
342
|
+
},
|
|
343
|
+
defaultScopes: googlePeopleReadonlyScopes,
|
|
344
|
+
inputSchema: searchContactsInputSchema,
|
|
345
|
+
outputSchema: searchContactsOutputSchema,
|
|
346
|
+
handleInvocation: async ({
|
|
347
|
+
ctx,
|
|
348
|
+
dependencies
|
|
349
|
+
}) => {
|
|
350
|
+
let client = dependencies.createClient(ctx);
|
|
351
|
+
let result = await client.searchContacts(ctx.input.query, void 0, ctx.input.pageSize);
|
|
352
|
+
let contacts = (result.results || []).map((item) => formatContact(item.person));
|
|
353
|
+
return {
|
|
354
|
+
output: { contacts },
|
|
355
|
+
message: `Found **${contacts.length}** contacts matching "${ctx.input.query}".`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
var getContactRecipe = defineToolRecipe({
|
|
360
|
+
name: "Get Contact",
|
|
361
|
+
key: "get_contact",
|
|
362
|
+
description: `Retrieves detailed information about a specific contact by their resource name. Use \`people/me\` to get the authenticated user's profile. Returns all available contact fields.`,
|
|
363
|
+
tags: {
|
|
364
|
+
destructive: false,
|
|
365
|
+
readOnly: true
|
|
366
|
+
},
|
|
367
|
+
defaultScopes: googlePeopleReadonlyScopes,
|
|
368
|
+
inputSchema: getContactInputSchema,
|
|
369
|
+
outputSchema: contactOutputSchema,
|
|
370
|
+
handleInvocation: async ({
|
|
371
|
+
ctx,
|
|
372
|
+
dependencies
|
|
373
|
+
}) => {
|
|
374
|
+
let client = dependencies.createClient(ctx);
|
|
375
|
+
let result = await client.getContact(ctx.input.resourceName);
|
|
376
|
+
let contact = formatContact(result);
|
|
377
|
+
let displayName = contact.names?.[0]?.displayName || contact.emailAddresses?.[0]?.value || ctx.input.resourceName;
|
|
378
|
+
return {
|
|
379
|
+
output: contact,
|
|
380
|
+
message: `Retrieved contact **${displayName}**.`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
export {
|
|
385
|
+
DEFAULT_PERSON_FIELDS,
|
|
386
|
+
GOOGLE_PEOPLE_API_BASE_URL,
|
|
387
|
+
GooglePeopleClient,
|
|
388
|
+
READONLY_PERSON_FIELDS,
|
|
389
|
+
addressSchema,
|
|
390
|
+
biographySchema,
|
|
391
|
+
birthdaySchema,
|
|
392
|
+
contactInputSchema,
|
|
393
|
+
contactOutputSchema,
|
|
394
|
+
createGooglePeopleClient,
|
|
395
|
+
customFieldSchema,
|
|
396
|
+
dateFieldSchema,
|
|
397
|
+
emailSchema,
|
|
398
|
+
eventSchema,
|
|
399
|
+
formatContact,
|
|
400
|
+
getContactInputSchema,
|
|
401
|
+
getContactRecipe,
|
|
402
|
+
googlePeopleApiError,
|
|
403
|
+
googlePeopleReadonlyScopes,
|
|
404
|
+
googlePeopleScopes,
|
|
405
|
+
googlePeopleServiceError,
|
|
406
|
+
listContactsInputSchema,
|
|
407
|
+
listContactsOutputSchema,
|
|
408
|
+
listContactsRecipe,
|
|
409
|
+
membershipSchema,
|
|
410
|
+
nameSchema,
|
|
411
|
+
nicknameSchema,
|
|
412
|
+
occupationSchema,
|
|
413
|
+
organizationSchema,
|
|
414
|
+
phoneSchema,
|
|
415
|
+
relationSchema,
|
|
416
|
+
searchContactsInputSchema,
|
|
417
|
+
searchContactsOutputSchema,
|
|
418
|
+
searchContactsRecipe,
|
|
419
|
+
urlSchema
|
|
420
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slates/google-people-recipes",
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"author": "Ivan Siianytsia",
|
|
8
|
+
"license": "FSL 1.1",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"source": "src/index.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"src/**",
|
|
13
|
+
"dist/**",
|
|
14
|
+
"README.md",
|
|
15
|
+
"package.json"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
"bun": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"require": "./dist/index.cjs",
|
|
21
|
+
"import": "./dist/index.module.js",
|
|
22
|
+
"default": "./dist/index.module.js"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.module.js",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"unpkg": "./dist/index.module.js",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run --config vitest.config.ts --passWithNoTests",
|
|
30
|
+
"lint": "prettier src/**/*.ts --check",
|
|
31
|
+
"build": "tsup --config ../../tsup.packages.config.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@lowerdeck/error": "^1.1.0",
|
|
36
|
+
"@slates/tool-recipes": "1.0.0-rc.1",
|
|
37
|
+
"slates": "1.0.0-rc.10",
|
|
38
|
+
"zod": "^4.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@slates/tsconfig": "1.0.0-rc.1",
|
|
42
|
+
"typescript": "5.8.2",
|
|
43
|
+
"vitest": "^3.1.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ServiceError } from '@lowerdeck/error';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
formatContact,
|
|
6
|
+
getContactRecipe,
|
|
7
|
+
googlePeopleApiError,
|
|
8
|
+
listContactsRecipe,
|
|
9
|
+
searchContactsRecipe
|
|
10
|
+
} from './index';
|
|
11
|
+
|
|
12
|
+
describe('google people recipes', () => {
|
|
13
|
+
it('uses MCP-compatible top-level object input schemas', () => {
|
|
14
|
+
for (let recipe of [listContactsRecipe, searchContactsRecipe, getContactRecipe]) {
|
|
15
|
+
let jsonSchema = z.toJSONSchema(recipe.inputSchema) as Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
expect(jsonSchema.type).toBe('object');
|
|
18
|
+
expect(jsonSchema).not.toHaveProperty('oneOf');
|
|
19
|
+
expect(jsonSchema).not.toHaveProperty('anyOf');
|
|
20
|
+
expect(jsonSchema).not.toHaveProperty('allOf');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('formats People API contact fields for tool output', () => {
|
|
25
|
+
let formatted = formatContact({
|
|
26
|
+
resourceName: 'people/c123',
|
|
27
|
+
etag: 'etag-123',
|
|
28
|
+
names: [{ displayName: 'Ada Lovelace', givenName: 'Ada', familyName: 'Lovelace' }],
|
|
29
|
+
emailAddresses: [{ value: 'ada@example.com', type: 'work' }],
|
|
30
|
+
phoneNumbers: [{ value: '+15551234567', type: 'mobile' }],
|
|
31
|
+
organizations: [{ name: 'Analytical Engines', title: 'Programmer' }],
|
|
32
|
+
birthdays: [{ date: { month: 12, day: 10 } }],
|
|
33
|
+
userDefined: [{ key: 'crmId', value: 'crm-123' }],
|
|
34
|
+
memberships: [
|
|
35
|
+
{
|
|
36
|
+
contactGroupMembership: {
|
|
37
|
+
contactGroupResourceName: 'contactGroups/friends'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(formatted).toMatchObject({
|
|
44
|
+
resourceName: 'people/c123',
|
|
45
|
+
etag: 'etag-123',
|
|
46
|
+
names: [{ displayName: 'Ada Lovelace', givenName: 'Ada', familyName: 'Lovelace' }],
|
|
47
|
+
emailAddresses: [{ value: 'ada@example.com', type: 'work' }],
|
|
48
|
+
phoneNumbers: [{ value: '+15551234567', type: 'mobile' }],
|
|
49
|
+
organizations: [{ name: 'Analytical Engines', title: 'Programmer' }],
|
|
50
|
+
birthdays: [{ date: { month: 12, day: 10 } }],
|
|
51
|
+
userDefined: [{ key: 'crmId', value: 'crm-123' }],
|
|
52
|
+
memberships: [{ contactGroupResourceName: 'contactGroups/friends' }]
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('converts upstream API failures to ServiceError', () => {
|
|
57
|
+
let error = googlePeopleApiError(
|
|
58
|
+
{
|
|
59
|
+
response: {
|
|
60
|
+
status: 403,
|
|
61
|
+
statusText: 'Forbidden',
|
|
62
|
+
data: {
|
|
63
|
+
error: {
|
|
64
|
+
message: 'Request had insufficient authentication scopes.'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
'list contacts'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(error).toBeInstanceOf(ServiceError);
|
|
73
|
+
expect(error.data.reason).toBe('google_people_api_error');
|
|
74
|
+
expect(error.data.upstreamStatus).toBe(403);
|
|
75
|
+
expect(error.data.message).toContain('Google People API list contacts failed');
|
|
76
|
+
expect(error.data.message).toContain('insufficient authentication scopes');
|
|
77
|
+
});
|
|
78
|
+
});
|