@agenticmail/enterprise 0.5.75 → 0.5.76

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/cli.js CHANGED
@@ -48,7 +48,7 @@ Skill Development:
48
48
  break;
49
49
  case "setup":
50
50
  default:
51
- import("./setup-FRTQSRNC.js").then((m) => m.runSetupWizard()).catch(fatal);
51
+ import("./setup-OLC7UNFL.js").then((m) => m.runSetupWizard()).catch(fatal);
52
52
  break;
53
53
  }
54
54
  function fatal(err) {
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  executeTool,
36
36
  runAgentLoop,
37
37
  toolsToDefinitions
38
- } from "./chunk-3ZYTG6YH.js";
38
+ } from "./chunk-SXSA3OQS.js";
39
39
  import "./chunk-TYW5XTOW.js";
40
40
  import {
41
41
  ValidationError,
@@ -50,11 +50,11 @@ import {
50
50
  requireRole,
51
51
  securityHeaders,
52
52
  validate
53
- } from "./chunk-72VXEHUE.js";
53
+ } from "./chunk-R5JPVOVE.js";
54
54
  import {
55
55
  provision,
56
56
  runSetupWizard
57
- } from "./chunk-PVSNZHZK.js";
57
+ } from "./chunk-QZHWUMPS.js";
58
58
  import {
59
59
  ENGINE_TABLES,
60
60
  ENGINE_TABLES_POSTGRES,
@@ -0,0 +1,47 @@
1
+ import {
2
+ AgentRuntime,
3
+ EmailChannel,
4
+ FollowUpScheduler,
5
+ SessionManager,
6
+ SubAgentManager,
7
+ ToolRegistry,
8
+ callLLM,
9
+ createAgentRuntime,
10
+ createNoopHooks,
11
+ createRuntimeHooks,
12
+ estimateMessageTokens,
13
+ estimateTokens,
14
+ executeTool,
15
+ runAgentLoop,
16
+ toolsToDefinitions
17
+ } from "./chunk-SXSA3OQS.js";
18
+ import "./chunk-TYW5XTOW.js";
19
+ import "./chunk-JLSQOQ5L.js";
20
+ import {
21
+ PROVIDER_REGISTRY,
22
+ listAllProviders,
23
+ resolveApiKeyForProvider,
24
+ resolveProvider
25
+ } from "./chunk-67KZYSLU.js";
26
+ import "./chunk-KFQGP6VL.js";
27
+ export {
28
+ AgentRuntime,
29
+ EmailChannel,
30
+ FollowUpScheduler,
31
+ PROVIDER_REGISTRY,
32
+ SessionManager,
33
+ SubAgentManager,
34
+ ToolRegistry,
35
+ callLLM,
36
+ createAgentRuntime,
37
+ createNoopHooks,
38
+ createRuntimeHooks,
39
+ estimateMessageTokens,
40
+ estimateTokens,
41
+ executeTool,
42
+ listAllProviders,
43
+ resolveApiKeyForProvider,
44
+ resolveProvider,
45
+ runAgentLoop,
46
+ toolsToDefinitions
47
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-R5JPVOVE.js";
4
+ import "./chunk-3SMTCIR4.js";
5
+ import "./chunk-JLSQOQ5L.js";
6
+ import "./chunk-RO537U6H.js";
7
+ import "./chunk-DRXMYYKN.js";
8
+ import "./chunk-67KZYSLU.js";
9
+ import "./chunk-KFQGP6VL.js";
10
+ export {
11
+ createServer
12
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-QZHWUMPS.js";
10
+ import "./chunk-QDXUZP7Y.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.75",
3
+ "version": "0.5.76",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -105,6 +105,11 @@ export { createMemoryTool } from './tools/memory.js';
105
105
  export { createAgenticMailTools } from './tools/agenticmail.js';
106
106
  export type { AgenticMailManagerRef, AgenticMailToolsConfig } from './tools/agenticmail.js';
107
107
 
108
+ // --- Tool creators (Google Workspace) ---
109
+ export { createAllGoogleTools, createGoogleCalendarTools, createGoogleDriveTools, createGoogleSheetsTools, createGoogleDocsTools, createGoogleContactsTools } from './tools/google/index.js';
110
+ export { createTokenProvider } from './tools/oauth-token-provider.js';
111
+ export type { TokenProvider, OAuthTokens, TokenProviderConfig } from './tools/oauth-token-provider.js';
112
+
108
113
  // --- Tool creators (enterprise) ---
109
114
  export { createDatabaseTools } from './tools/enterprise-database.js';
110
115
  export { createSpreadsheetTools } from './tools/enterprise-spreadsheet.js';
@@ -166,11 +171,28 @@ import { createDiffTools } from './tools/enterprise-diff.js';
166
171
  import { createVisionTools } from './tools/enterprise-vision.js';
167
172
  import { createAgenticMailTools } from './tools/agenticmail.js';
168
173
  import type { AgenticMailManagerRef } from './tools/agenticmail.js';
174
+ import { createAllGoogleTools } from './tools/google/index.js';
175
+ import { createTokenProvider } from './tools/oauth-token-provider.js';
176
+ import type { OAuthTokens, TokenProvider } from './tools/oauth-token-provider.js';
169
177
 
170
178
  /** Extended options that includes AgenticMail manager */
171
179
  export interface AllToolsOptions extends ToolCreationOptions {
172
180
  /** AgenticMail manager for org email access */
173
181
  agenticmailManager?: AgenticMailManagerRef;
182
+ /** OAuth token provider for Google/Microsoft API tools */
183
+ oauthTokenProvider?: TokenProvider;
184
+ /** Raw email config for auto-creating token provider */
185
+ emailConfig?: {
186
+ oauthProvider?: string;
187
+ oauthAccessToken?: string;
188
+ oauthRefreshToken?: string;
189
+ oauthTokenExpiry?: string;
190
+ oauthClientId?: string;
191
+ oauthClientSecret?: string;
192
+ email?: string;
193
+ };
194
+ /** Callback to persist updated tokens after refresh */
195
+ onTokenRefresh?: (tokens: Partial<OAuthTokens>) => void;
174
196
  }
175
197
 
176
198
  /**
@@ -247,10 +269,46 @@ export function createAllTools(options?: AllToolsOptions): AnyAgentTool[] {
247
269
  );
248
270
  }
249
271
 
272
+ // Google Workspace / Microsoft Graph tools (if OAuth configured)
273
+ var workspaceTools: AnyAgentTool[] = [];
274
+ var tp = options?.oauthTokenProvider;
275
+ if (!tp && options?.emailConfig?.oauthAccessToken) {
276
+ // Auto-create token provider from email config
277
+ var ec = options.emailConfig;
278
+ tp = createTokenProvider({
279
+ getTokens: function() {
280
+ return {
281
+ accessToken: ec.oauthAccessToken!,
282
+ refreshToken: ec.oauthRefreshToken,
283
+ expiresAt: ec.oauthTokenExpiry,
284
+ provider: (ec.oauthProvider === 'microsoft' ? 'microsoft' : 'google') as any,
285
+ clientId: ec.oauthClientId || '',
286
+ clientSecret: ec.oauthClientSecret || '',
287
+ };
288
+ },
289
+ saveTokens: function(newTokens) {
290
+ if (newTokens.accessToken) ec.oauthAccessToken = newTokens.accessToken;
291
+ if (newTokens.refreshToken) ec.oauthRefreshToken = newTokens.refreshToken;
292
+ if (newTokens.expiresAt) ec.oauthTokenExpiry = newTokens.expiresAt;
293
+ if (options?.onTokenRefresh) options.onTokenRefresh(newTokens);
294
+ },
295
+ getEmail: function() { return ec.email; },
296
+ });
297
+ }
298
+ if (tp) {
299
+ var provider = tp.getProvider();
300
+ if (provider === 'google') {
301
+ workspaceTools = createAllGoogleTools({ tokenProvider: tp }, options);
302
+ }
303
+ // TODO: Microsoft Graph tools
304
+ // if (provider === 'microsoft') { workspaceTools = createAllMicrosoftTools({ tokenProvider: tp }, options); }
305
+ }
306
+
250
307
  var enabledTools = rawTools
251
308
  .filter(function(t): t is AnyAgentTool { return t !== null; })
252
309
  .concat(enterpriseTools)
253
- .concat(agenticmailTools);
310
+ .concat(agenticmailTools)
311
+ .concat(workspaceTools);
254
312
 
255
313
  // Wrap with middleware if configured
256
314
  if (options?.middleware) {
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Google Calendar Tools
3
+ *
4
+ * CRUD for events, free/busy lookup, and calendar listing via Google Calendar API v3.
5
+ */
6
+
7
+ import type { AnyAgentTool, ToolCreationOptions } from '../../types.js';
8
+ import { jsonResult, errorResult } from '../../common.js';
9
+ import type { GoogleToolsConfig } from './index.js';
10
+
11
+ const BASE = 'https://www.googleapis.com/calendar/v3';
12
+
13
+ async function gapi(token: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string> }): Promise<any> {
14
+ const method = opts?.method || 'GET';
15
+ const url = new URL(BASE + path);
16
+ if (opts?.query) for (const [k, v] of Object.entries(opts.query)) { if (v) url.searchParams.set(k, v); }
17
+ const res = await fetch(url.toString(), {
18
+ method,
19
+ headers: {
20
+ Authorization: `Bearer ${token}`,
21
+ 'Content-Type': 'application/json',
22
+ },
23
+ body: opts?.body ? JSON.stringify(opts.body) : undefined,
24
+ });
25
+ if (!res.ok) {
26
+ const err = await res.text();
27
+ throw new Error(`Google Calendar API ${res.status}: ${err}`);
28
+ }
29
+ if (res.status === 204) return {};
30
+ return res.json();
31
+ }
32
+
33
+ export function createGoogleCalendarTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
34
+ const tp = config.tokenProvider;
35
+ return [
36
+ {
37
+ name: 'google_calendar_list',
38
+ description: 'List all calendars the agent has access to.',
39
+ category: 'utility' as const,
40
+ parameters: { type: 'object' as const, properties: {}, required: [] },
41
+ async execute(_id: string) {
42
+ try {
43
+ const token = await tp.getAccessToken();
44
+ const data = await gapi(token, '/users/me/calendarList');
45
+ const cals = (data.items || []).map((c: any) => ({
46
+ id: c.id, summary: c.summary, description: c.description,
47
+ primary: c.primary || false, timeZone: c.timeZone, accessRole: c.accessRole,
48
+ }));
49
+ return jsonResult({ calendars: cals, count: cals.length });
50
+ } catch (e: any) { return errorResult(e.message); }
51
+ },
52
+ },
53
+ {
54
+ name: 'google_calendar_events',
55
+ description: 'List upcoming events from a calendar. Defaults to primary calendar.',
56
+ category: 'utility' as const,
57
+ parameters: {
58
+ type: 'object' as const,
59
+ properties: {
60
+ calendarId: { type: 'string', description: 'Calendar ID (default: "primary")' },
61
+ timeMin: { type: 'string', description: 'Start of range (ISO 8601, default: now)' },
62
+ timeMax: { type: 'string', description: 'End of range (ISO 8601)' },
63
+ maxResults: { type: 'number', description: 'Max events to return (default: 25, max: 250)' },
64
+ query: { type: 'string', description: 'Free-text search filter' },
65
+ },
66
+ required: [],
67
+ },
68
+ async execute(_id: string, params: any) {
69
+ try {
70
+ const token = await tp.getAccessToken();
71
+ const calId = params.calendarId || 'primary';
72
+ const query: Record<string, string> = {
73
+ singleEvents: 'true',
74
+ orderBy: 'startTime',
75
+ maxResults: String(Math.min(params.maxResults || 25, 250)),
76
+ timeMin: params.timeMin || new Date().toISOString(),
77
+ };
78
+ if (params.timeMax) query.timeMax = params.timeMax;
79
+ if (params.query) query.q = params.query;
80
+ const data = await gapi(token, `/calendars/${encodeURIComponent(calId)}/events`, { query });
81
+ const events = (data.items || []).map((e: any) => ({
82
+ id: e.id, summary: e.summary, description: e.description,
83
+ start: e.start?.dateTime || e.start?.date,
84
+ end: e.end?.dateTime || e.end?.date,
85
+ location: e.location,
86
+ attendees: (e.attendees || []).map((a: any) => ({ email: a.email, name: a.displayName, status: a.responseStatus })),
87
+ status: e.status, htmlLink: e.htmlLink,
88
+ organizer: e.organizer?.email,
89
+ recurring: !!e.recurringEventId,
90
+ }));
91
+ return jsonResult({ events, count: events.length, calendarId: calId });
92
+ } catch (e: any) { return errorResult(e.message); }
93
+ },
94
+ },
95
+ {
96
+ name: 'google_calendar_create_event',
97
+ description: 'Create a new calendar event. Supports attendees, location, reminders, and recurrence.',
98
+ category: 'utility' as const,
99
+ parameters: {
100
+ type: 'object' as const,
101
+ properties: {
102
+ calendarId: { type: 'string', description: 'Calendar ID (default: "primary")' },
103
+ summary: { type: 'string', description: 'Event title (required)' },
104
+ description: { type: 'string', description: 'Event description/body' },
105
+ start: { type: 'string', description: 'Start time (ISO 8601, required)' },
106
+ end: { type: 'string', description: 'End time (ISO 8601, required)' },
107
+ location: { type: 'string', description: 'Event location' },
108
+ attendees: { type: 'string', description: 'Comma-separated email addresses' },
109
+ timeZone: { type: 'string', description: 'Timezone (e.g. "America/New_York")' },
110
+ allDay: { type: 'string', description: 'If "true", creates all-day event (start/end should be YYYY-MM-DD)' },
111
+ recurrence: { type: 'string', description: 'RRULE string (e.g. "RRULE:FREQ=WEEKLY;COUNT=10")' },
112
+ sendUpdates: { type: 'string', description: '"all" to email attendees, "none" to skip (default: "all")' },
113
+ },
114
+ required: ['summary', 'start', 'end'],
115
+ },
116
+ async execute(_id: string, params: any) {
117
+ try {
118
+ const token = await tp.getAccessToken();
119
+ const calId = params.calendarId || 'primary';
120
+ const isAllDay = params.allDay === 'true';
121
+ const event: any = {
122
+ summary: params.summary,
123
+ description: params.description,
124
+ location: params.location,
125
+ start: isAllDay ? { date: params.start } : { dateTime: params.start, timeZone: params.timeZone },
126
+ end: isAllDay ? { date: params.end } : { dateTime: params.end, timeZone: params.timeZone },
127
+ };
128
+ if (params.attendees) {
129
+ event.attendees = params.attendees.split(',').map((e: string) => ({ email: e.trim() }));
130
+ }
131
+ if (params.recurrence) event.recurrence = [params.recurrence];
132
+ const query: Record<string, string> = {};
133
+ if (params.sendUpdates) query.sendUpdates = params.sendUpdates;
134
+ const result = await gapi(token, `/calendars/${encodeURIComponent(calId)}/events`, { method: 'POST', body: event, query });
135
+ return jsonResult({ created: true, eventId: result.id, htmlLink: result.htmlLink, summary: result.summary, start: result.start?.dateTime || result.start?.date });
136
+ } catch (e: any) { return errorResult(e.message); }
137
+ },
138
+ },
139
+ {
140
+ name: 'google_calendar_update_event',
141
+ description: 'Update an existing calendar event.',
142
+ category: 'utility' as const,
143
+ parameters: {
144
+ type: 'object' as const,
145
+ properties: {
146
+ calendarId: { type: 'string', description: 'Calendar ID (default: "primary")' },
147
+ eventId: { type: 'string', description: 'Event ID to update (required)' },
148
+ summary: { type: 'string', description: 'New title' },
149
+ description: { type: 'string', description: 'New description' },
150
+ start: { type: 'string', description: 'New start time (ISO 8601)' },
151
+ end: { type: 'string', description: 'New end time (ISO 8601)' },
152
+ location: { type: 'string', description: 'New location' },
153
+ attendees: { type: 'string', description: 'Comma-separated email addresses (replaces existing)' },
154
+ sendUpdates: { type: 'string', description: '"all" to email attendees, "none" to skip' },
155
+ },
156
+ required: ['eventId'],
157
+ },
158
+ async execute(_id: string, params: any) {
159
+ try {
160
+ const token = await tp.getAccessToken();
161
+ const calId = params.calendarId || 'primary';
162
+ const patch: any = {};
163
+ if (params.summary) patch.summary = params.summary;
164
+ if (params.description) patch.description = params.description;
165
+ if (params.location) patch.location = params.location;
166
+ if (params.start) patch.start = { dateTime: params.start };
167
+ if (params.end) patch.end = { dateTime: params.end };
168
+ if (params.attendees) patch.attendees = params.attendees.split(',').map((e: string) => ({ email: e.trim() }));
169
+ const query: Record<string, string> = {};
170
+ if (params.sendUpdates) query.sendUpdates = params.sendUpdates;
171
+ const result = await gapi(token, `/calendars/${encodeURIComponent(calId)}/events/${params.eventId}`, { method: 'PATCH', body: patch, query });
172
+ return jsonResult({ updated: true, eventId: result.id, summary: result.summary });
173
+ } catch (e: any) { return errorResult(e.message); }
174
+ },
175
+ },
176
+ {
177
+ name: 'google_calendar_delete_event',
178
+ description: 'Delete a calendar event.',
179
+ category: 'utility' as const,
180
+ parameters: {
181
+ type: 'object' as const,
182
+ properties: {
183
+ calendarId: { type: 'string', description: 'Calendar ID (default: "primary")' },
184
+ eventId: { type: 'string', description: 'Event ID to delete (required)' },
185
+ sendUpdates: { type: 'string', description: '"all" to notify attendees, "none" to skip' },
186
+ },
187
+ required: ['eventId'],
188
+ },
189
+ async execute(_id: string, params: any) {
190
+ try {
191
+ const token = await tp.getAccessToken();
192
+ const calId = params.calendarId || 'primary';
193
+ const query: Record<string, string> = {};
194
+ if (params.sendUpdates) query.sendUpdates = params.sendUpdates;
195
+ await gapi(token, `/calendars/${encodeURIComponent(calId)}/events/${params.eventId}`, { method: 'DELETE', query });
196
+ return jsonResult({ deleted: true, eventId: params.eventId });
197
+ } catch (e: any) { return errorResult(e.message); }
198
+ },
199
+ },
200
+ {
201
+ name: 'google_calendar_freebusy',
202
+ description: 'Check free/busy status for one or more calendars in a time range. Useful for scheduling meetings.',
203
+ category: 'utility' as const,
204
+ parameters: {
205
+ type: 'object' as const,
206
+ properties: {
207
+ timeMin: { type: 'string', description: 'Start of range (ISO 8601, required)' },
208
+ timeMax: { type: 'string', description: 'End of range (ISO 8601, required)' },
209
+ calendars: { type: 'string', description: 'Comma-separated calendar IDs (default: "primary")' },
210
+ },
211
+ required: ['timeMin', 'timeMax'],
212
+ },
213
+ async execute(_id: string, params: any) {
214
+ try {
215
+ const token = await tp.getAccessToken();
216
+ const calIds = (params.calendars || 'primary').split(',').map((c: string) => ({ id: c.trim() }));
217
+ const data = await gapi(token, '/freeBusy', {
218
+ method: 'POST',
219
+ body: { timeMin: params.timeMin, timeMax: params.timeMax, items: calIds },
220
+ });
221
+ const result: Record<string, any> = {};
222
+ for (const [calId, info] of Object.entries(data.calendars || {})) {
223
+ result[calId] = { busy: (info as any).busy || [], errors: (info as any).errors };
224
+ }
225
+ return jsonResult({ freeBusy: result, timeMin: params.timeMin, timeMax: params.timeMax });
226
+ } catch (e: any) { return errorResult(e.message); }
227
+ },
228
+ },
229
+ ];
230
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Google Contacts (People API) Tools
3
+ *
4
+ * Search, list, create, and update contacts via Google People API v1.
5
+ */
6
+
7
+ import type { AnyAgentTool, ToolCreationOptions } from '../../types.js';
8
+ import { jsonResult, errorResult } from '../../common.js';
9
+ import type { GoogleToolsConfig } from './index.js';
10
+
11
+ const BASE = 'https://people.googleapis.com/v1';
12
+
13
+ async function papi(token: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string> }): Promise<any> {
14
+ const url = new URL(BASE + path);
15
+ if (opts?.query) for (const [k, v] of Object.entries(opts.query)) { if (v) url.searchParams.set(k, v); }
16
+ const res = await fetch(url.toString(), {
17
+ method: opts?.method || 'GET',
18
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
19
+ body: opts?.body ? JSON.stringify(opts.body) : undefined,
20
+ });
21
+ if (!res.ok) { const err = await res.text(); throw new Error(`People API ${res.status}: ${err}`); }
22
+ return res.json();
23
+ }
24
+
25
+ function mapPerson(p: any) {
26
+ return {
27
+ resourceName: p.resourceName,
28
+ name: p.names?.[0]?.displayName,
29
+ firstName: p.names?.[0]?.givenName,
30
+ lastName: p.names?.[0]?.familyName,
31
+ emails: (p.emailAddresses || []).map((e: any) => ({ value: e.value, type: e.type })),
32
+ phones: (p.phoneNumbers || []).map((ph: any) => ({ value: ph.value, type: ph.type })),
33
+ organization: p.organizations?.[0]?.name,
34
+ jobTitle: p.organizations?.[0]?.title,
35
+ department: p.organizations?.[0]?.department,
36
+ addresses: (p.addresses || []).map((a: any) => ({ formatted: a.formattedValue, type: a.type })),
37
+ birthday: p.birthdays?.[0]?.date ? `${p.birthdays[0].date.year || '????'}-${String(p.birthdays[0].date.month).padStart(2, '0')}-${String(p.birthdays[0].date.day).padStart(2, '0')}` : undefined,
38
+ notes: p.biographies?.[0]?.value,
39
+ };
40
+ }
41
+
42
+ const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,organizations,addresses,birthdays,biographies';
43
+
44
+ export function createGoogleContactsTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
45
+ const tp = config.tokenProvider;
46
+ return [
47
+ {
48
+ name: 'google_contacts_list',
49
+ description: 'List contacts from the agent\'s Google directory. Returns names, emails, phones, organizations.',
50
+ category: 'utility' as const,
51
+ parameters: {
52
+ type: 'object' as const,
53
+ properties: {
54
+ maxResults: { type: 'number', description: 'Max contacts to return (default: 50, max: 200)' },
55
+ sortOrder: { type: 'string', description: '"FIRST_NAME_ASCENDING" or "LAST_NAME_ASCENDING"' },
56
+ },
57
+ required: [],
58
+ },
59
+ async execute(_id: string, params: any) {
60
+ try {
61
+ const token = await tp.getAccessToken();
62
+ const data = await papi(token, '/people/me/connections', {
63
+ query: {
64
+ personFields: PERSON_FIELDS,
65
+ pageSize: String(Math.min(params.maxResults || 50, 200)),
66
+ sortOrder: params.sortOrder || 'FIRST_NAME_ASCENDING',
67
+ },
68
+ });
69
+ const contacts = (data.connections || []).map(mapPerson);
70
+ return jsonResult({ contacts, count: contacts.length, totalPeople: data.totalPeople });
71
+ } catch (e: any) { return errorResult(e.message); }
72
+ },
73
+ },
74
+ {
75
+ name: 'google_contacts_search',
76
+ description: 'Search contacts by name, email, or phone number.',
77
+ category: 'utility' as const,
78
+ parameters: {
79
+ type: 'object' as const,
80
+ properties: {
81
+ query: { type: 'string', description: 'Search term (required)' },
82
+ maxResults: { type: 'number', description: 'Max results (default: 20)' },
83
+ },
84
+ required: ['query'],
85
+ },
86
+ async execute(_id: string, params: any) {
87
+ try {
88
+ const token = await tp.getAccessToken();
89
+ const data = await papi(token, '/people:searchContacts', {
90
+ query: {
91
+ query: params.query,
92
+ readMask: PERSON_FIELDS,
93
+ pageSize: String(Math.min(params.maxResults || 20, 30)),
94
+ },
95
+ });
96
+ const contacts = (data.results || []).map((r: any) => mapPerson(r.person));
97
+ return jsonResult({ contacts, count: contacts.length, query: params.query });
98
+ } catch (e: any) { return errorResult(e.message); }
99
+ },
100
+ },
101
+ {
102
+ name: 'google_contacts_search_directory',
103
+ description: 'Search the organization\'s Google Workspace directory (all employees). Requires domain-wide access.',
104
+ category: 'utility' as const,
105
+ parameters: {
106
+ type: 'object' as const,
107
+ properties: {
108
+ query: { type: 'string', description: 'Search term (required)' },
109
+ maxResults: { type: 'number', description: 'Max results (default: 20)' },
110
+ },
111
+ required: ['query'],
112
+ },
113
+ async execute(_id: string, params: any) {
114
+ try {
115
+ const token = await tp.getAccessToken();
116
+ const data = await papi(token, '/people:searchDirectoryPeople', {
117
+ query: {
118
+ query: params.query,
119
+ readMask: PERSON_FIELDS,
120
+ pageSize: String(Math.min(params.maxResults || 20, 50)),
121
+ sources: 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE',
122
+ },
123
+ });
124
+ const people = (data.people || []).map(mapPerson);
125
+ return jsonResult({ people, count: people.length, query: params.query });
126
+ } catch (e: any) { return errorResult(e.message); }
127
+ },
128
+ },
129
+ {
130
+ name: 'google_contacts_create',
131
+ description: 'Create a new contact in the agent\'s Google Contacts.',
132
+ category: 'utility' as const,
133
+ parameters: {
134
+ type: 'object' as const,
135
+ properties: {
136
+ firstName: { type: 'string', description: 'First name (required)' },
137
+ lastName: { type: 'string', description: 'Last name' },
138
+ email: { type: 'string', description: 'Email address' },
139
+ phone: { type: 'string', description: 'Phone number' },
140
+ organization: { type: 'string', description: 'Company/organization name' },
141
+ jobTitle: { type: 'string', description: 'Job title' },
142
+ notes: { type: 'string', description: 'Notes about this contact' },
143
+ },
144
+ required: ['firstName'],
145
+ },
146
+ async execute(_id: string, params: any) {
147
+ try {
148
+ const token = await tp.getAccessToken();
149
+ const person: any = { names: [{ givenName: params.firstName, familyName: params.lastName }] };
150
+ if (params.email) person.emailAddresses = [{ value: params.email, type: 'work' }];
151
+ if (params.phone) person.phoneNumbers = [{ value: params.phone, type: 'work' }];
152
+ if (params.organization || params.jobTitle) {
153
+ person.organizations = [{ name: params.organization, title: params.jobTitle }];
154
+ }
155
+ if (params.notes) person.biographies = [{ value: params.notes, contentType: 'TEXT_PLAIN' }];
156
+ const result = await papi(token, '/people:createContact', {
157
+ method: 'POST', body: person,
158
+ query: { personFields: PERSON_FIELDS },
159
+ });
160
+ return jsonResult({ created: true, contact: mapPerson(result) });
161
+ } catch (e: any) { return errorResult(e.message); }
162
+ },
163
+ },
164
+ {
165
+ name: 'google_contacts_update',
166
+ description: 'Update an existing contact.',
167
+ category: 'utility' as const,
168
+ parameters: {
169
+ type: 'object' as const,
170
+ properties: {
171
+ resourceName: { type: 'string', description: 'Contact resource name, e.g. "people/c1234567890" (required)' },
172
+ firstName: { type: 'string', description: 'New first name' },
173
+ lastName: { type: 'string', description: 'New last name' },
174
+ email: { type: 'string', description: 'New email' },
175
+ phone: { type: 'string', description: 'New phone' },
176
+ organization: { type: 'string', description: 'New organization' },
177
+ jobTitle: { type: 'string', description: 'New job title' },
178
+ },
179
+ required: ['resourceName'],
180
+ },
181
+ async execute(_id: string, params: any) {
182
+ try {
183
+ const token = await tp.getAccessToken();
184
+ // Get current etag
185
+ const current = await papi(token, `/${params.resourceName}`, {
186
+ query: { personFields: PERSON_FIELDS },
187
+ });
188
+ const person: any = { etag: current.etag };
189
+ const updateFields: string[] = [];
190
+ if (params.firstName || params.lastName) {
191
+ person.names = [{ givenName: params.firstName || current.names?.[0]?.givenName, familyName: params.lastName || current.names?.[0]?.familyName }];
192
+ updateFields.push('names');
193
+ }
194
+ if (params.email) { person.emailAddresses = [{ value: params.email, type: 'work' }]; updateFields.push('emailAddresses'); }
195
+ if (params.phone) { person.phoneNumbers = [{ value: params.phone, type: 'work' }]; updateFields.push('phoneNumbers'); }
196
+ if (params.organization || params.jobTitle) {
197
+ person.organizations = [{ name: params.organization || current.organizations?.[0]?.name, title: params.jobTitle || current.organizations?.[0]?.title }];
198
+ updateFields.push('organizations');
199
+ }
200
+ const result = await papi(token, `/${params.resourceName}:updateContact`, {
201
+ method: 'PATCH', body: person,
202
+ query: { updatePersonFields: updateFields.join(','), personFields: PERSON_FIELDS },
203
+ });
204
+ return jsonResult({ updated: true, contact: mapPerson(result) });
205
+ } catch (e: any) { return errorResult(e.message); }
206
+ },
207
+ },
208
+ ];
209
+ }