@agenticmail/enterprise 0.5.75 → 0.5.77
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-2HOTPWQ6.js +2191 -0
- package/dist/chunk-7S5VMGP7.js +898 -0
- package/dist/chunk-QZHWUMPS.js +898 -0
- package/dist/chunk-R5JPVOVE.js +2191 -0
- package/dist/chunk-SXSA3OQS.js +14351 -0
- package/dist/chunk-T36SA4K3.js +15035 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +3 -3
- package/dist/runtime-IQ3PYZN5.js +47 -0
- package/dist/runtime-LFPQKGMJ.js +47 -0
- package/dist/server-5DCT62CG.js +12 -0
- package/dist/server-EZOBWT7K.js +12 -0
- package/dist/setup-42DBT5CL.js +20 -0
- package/dist/setup-OLC7UNFL.js +20 -0
- package/package.json +1 -1
- package/src/agent-tools/index.ts +59 -1
- package/src/agent-tools/tools/google/calendar.ts +230 -0
- package/src/agent-tools/tools/google/contacts.ts +209 -0
- package/src/agent-tools/tools/google/docs.ts +162 -0
- package/src/agent-tools/tools/google/drive.ts +262 -0
- package/src/agent-tools/tools/google/gmail.ts +696 -0
- package/src/agent-tools/tools/google/index.ts +45 -0
- package/src/agent-tools/tools/google/sheets.ts +215 -0
- package/src/agent-tools/tools/oauth-token-provider.ts +101 -0
- package/src/runtime/index.ts +23 -7
- package/src/runtime/types.ts +4 -0
- package/src/server.ts +23 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Docs Tools
|
|
3
|
+
*
|
|
4
|
+
* Read and write Google Docs via Google Docs 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 DOCS_BASE = 'https://docs.googleapis.com/v1/documents';
|
|
12
|
+
const DRIVE_BASE = 'https://www.googleapis.com/drive/v3';
|
|
13
|
+
|
|
14
|
+
async function dapi(token: string, path: string, opts?: { method?: string; body?: any; base?: string }): Promise<any> {
|
|
15
|
+
const base = opts?.base || DOCS_BASE;
|
|
16
|
+
const res = await fetch(base + path, {
|
|
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(`Google Docs API ${res.status}: ${err}`); }
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractText(doc: any): string {
|
|
26
|
+
const parts: string[] = [];
|
|
27
|
+
for (const el of doc.body?.content || []) {
|
|
28
|
+
if (el.paragraph) {
|
|
29
|
+
for (const pe of el.paragraph.elements || []) {
|
|
30
|
+
if (pe.textRun?.content) parts.push(pe.textRun.content);
|
|
31
|
+
}
|
|
32
|
+
} else if (el.table) {
|
|
33
|
+
for (const row of el.table.tableRows || []) {
|
|
34
|
+
const cells: string[] = [];
|
|
35
|
+
for (const cell of row.tableCells || []) {
|
|
36
|
+
const cellText: string[] = [];
|
|
37
|
+
for (const cp of cell.content || []) {
|
|
38
|
+
if (cp.paragraph) {
|
|
39
|
+
for (const pe of cp.paragraph.elements || []) {
|
|
40
|
+
if (pe.textRun?.content) cellText.push(pe.textRun.content.trim());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
cells.push(cellText.join(''));
|
|
45
|
+
}
|
|
46
|
+
parts.push(cells.join('\t'));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createGoogleDocsTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
|
|
54
|
+
const tp = config.tokenProvider;
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
name: 'google_docs_read',
|
|
58
|
+
description: 'Read the full text content of a Google Doc.',
|
|
59
|
+
category: 'utility' as const,
|
|
60
|
+
parameters: {
|
|
61
|
+
type: 'object' as const,
|
|
62
|
+
properties: {
|
|
63
|
+
documentId: { type: 'string', description: 'Document ID (required)' },
|
|
64
|
+
},
|
|
65
|
+
required: ['documentId'],
|
|
66
|
+
},
|
|
67
|
+
async execute(_id: string, params: any) {
|
|
68
|
+
try {
|
|
69
|
+
const token = await tp.getAccessToken();
|
|
70
|
+
const doc = await dapi(token, `/${params.documentId}`);
|
|
71
|
+
const text = extractText(doc);
|
|
72
|
+
return jsonResult({
|
|
73
|
+
documentId: doc.documentId, title: doc.title,
|
|
74
|
+
content: text.slice(0, 80000),
|
|
75
|
+
truncated: text.length > 80000,
|
|
76
|
+
characterCount: text.length,
|
|
77
|
+
});
|
|
78
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'google_docs_create',
|
|
83
|
+
description: 'Create a new Google Doc with optional initial content.',
|
|
84
|
+
category: 'utility' as const,
|
|
85
|
+
parameters: {
|
|
86
|
+
type: 'object' as const,
|
|
87
|
+
properties: {
|
|
88
|
+
title: { type: 'string', description: 'Document title (required)' },
|
|
89
|
+
content: { type: 'string', description: 'Initial text content to insert' },
|
|
90
|
+
folderId: { type: 'string', description: 'Parent folder ID in Drive' },
|
|
91
|
+
},
|
|
92
|
+
required: ['title'],
|
|
93
|
+
},
|
|
94
|
+
async execute(_id: string, params: any) {
|
|
95
|
+
try {
|
|
96
|
+
const token = await tp.getAccessToken();
|
|
97
|
+
const doc = await dapi(token, '', { method: 'POST', body: { title: params.title } });
|
|
98
|
+
// Insert content if provided
|
|
99
|
+
if (params.content) {
|
|
100
|
+
await dapi(token, `/${doc.documentId}:batchUpdate`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: { requests: [{ insertText: { location: { index: 1 }, text: params.content } }] },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Move to folder if specified
|
|
106
|
+
if (params.folderId) {
|
|
107
|
+
const file = await fetch(`${DRIVE_BASE}/files/${doc.documentId}?fields=parents`, {
|
|
108
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
109
|
+
}).then(r => r.json()) as any;
|
|
110
|
+
await fetch(`${DRIVE_BASE}/files/${doc.documentId}?addParents=${params.folderId}&removeParents=${(file.parents || []).join(',')}&fields=id`, {
|
|
111
|
+
method: 'PATCH', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return jsonResult({ created: true, documentId: doc.documentId, title: doc.title, url: `https://docs.google.com/document/d/${doc.documentId}/edit` });
|
|
115
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'google_docs_write',
|
|
120
|
+
description: 'Insert, replace, or append text in a Google Doc.',
|
|
121
|
+
category: 'utility' as const,
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object' as const,
|
|
124
|
+
properties: {
|
|
125
|
+
documentId: { type: 'string', description: 'Document ID (required)' },
|
|
126
|
+
action: { type: 'string', description: '"append" (add to end), "insert" (at index), or "replace" (find & replace) (required)' },
|
|
127
|
+
text: { type: 'string', description: 'Text to insert/append (required for append/insert)' },
|
|
128
|
+
index: { type: 'number', description: 'Character index for insert (1-based, required for insert action)' },
|
|
129
|
+
find: { type: 'string', description: 'Text to find (required for replace action)' },
|
|
130
|
+
replaceWith: { type: 'string', description: 'Replacement text (required for replace action)' },
|
|
131
|
+
matchCase: { type: 'string', description: '"true" for case-sensitive replace (default: "false")' },
|
|
132
|
+
},
|
|
133
|
+
required: ['documentId', 'action'],
|
|
134
|
+
},
|
|
135
|
+
async execute(_id: string, params: any) {
|
|
136
|
+
try {
|
|
137
|
+
const token = await tp.getAccessToken();
|
|
138
|
+
const requests: any[] = [];
|
|
139
|
+
if (params.action === 'append') {
|
|
140
|
+
const doc = await dapi(token, `/${params.documentId}`);
|
|
141
|
+
const endIndex = doc.body?.content?.slice(-1)?.[0]?.endIndex || 1;
|
|
142
|
+
requests.push({ insertText: { location: { index: Math.max(endIndex - 1, 1) }, text: params.text } });
|
|
143
|
+
} else if (params.action === 'insert') {
|
|
144
|
+
requests.push({ insertText: { location: { index: params.index || 1 }, text: params.text } });
|
|
145
|
+
} else if (params.action === 'replace') {
|
|
146
|
+
requests.push({
|
|
147
|
+
replaceAllText: {
|
|
148
|
+
containsText: { text: params.find, matchCase: params.matchCase === 'true' },
|
|
149
|
+
replaceText: params.replaceWith || '',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
return errorResult('action must be "append", "insert", or "replace"');
|
|
154
|
+
}
|
|
155
|
+
const result = await dapi(token, `/${params.documentId}:batchUpdate`, { method: 'POST', body: { requests } });
|
|
156
|
+
const replaceCount = result.replies?.[0]?.replaceAllText?.occurrencesChanged;
|
|
157
|
+
return jsonResult({ success: true, action: params.action, ...(replaceCount !== undefined && { replacements: replaceCount }) });
|
|
158
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Drive Tools
|
|
3
|
+
*
|
|
4
|
+
* File management, search, sharing, and content access via Google Drive 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/drive/v3';
|
|
12
|
+
const UPLOAD_BASE = 'https://www.googleapis.com/upload/drive/v3';
|
|
13
|
+
|
|
14
|
+
async function gapi(token: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string>; base?: string; rawBody?: BodyInit; headers?: Record<string, string> }): Promise<any> {
|
|
15
|
+
const method = opts?.method || 'GET';
|
|
16
|
+
const base = opts?.base || BASE;
|
|
17
|
+
const url = new URL(base + path);
|
|
18
|
+
if (opts?.query) for (const [k, v] of Object.entries(opts.query)) { if (v) url.searchParams.set(k, v); }
|
|
19
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${token}`, ...opts?.headers };
|
|
20
|
+
if (!opts?.rawBody) headers['Content-Type'] = 'application/json';
|
|
21
|
+
const res = await fetch(url.toString(), {
|
|
22
|
+
method, headers,
|
|
23
|
+
body: opts?.rawBody || (opts?.body ? JSON.stringify(opts.body) : undefined),
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) { const err = await res.text(); throw new Error(`Google Drive API ${res.status}: ${err}`); }
|
|
26
|
+
if (res.status === 204) return {};
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createGoogleDriveTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
|
|
31
|
+
const tp = config.tokenProvider;
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
name: 'google_drive_list',
|
|
35
|
+
description: 'List files and folders in Google Drive. Supports search queries, folder filtering, and MIME type filtering.',
|
|
36
|
+
category: 'utility' as const,
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object' as const,
|
|
39
|
+
properties: {
|
|
40
|
+
query: { type: 'string', description: 'Search query (Drive query syntax, e.g. "name contains \'report\'" or free text)' },
|
|
41
|
+
folderId: { type: 'string', description: 'List files in a specific folder' },
|
|
42
|
+
mimeType: { type: 'string', description: 'Filter by MIME type (e.g. "application/vnd.google-apps.spreadsheet")' },
|
|
43
|
+
maxResults: { type: 'number', description: 'Max results (default: 25, max: 100)' },
|
|
44
|
+
orderBy: { type: 'string', description: 'Sort order (e.g. "modifiedTime desc", "name")' },
|
|
45
|
+
sharedWithMe: { type: 'string', description: 'If "true", show only files shared with agent' },
|
|
46
|
+
trashed: { type: 'string', description: 'If "true", show trashed files' },
|
|
47
|
+
},
|
|
48
|
+
required: [],
|
|
49
|
+
},
|
|
50
|
+
async execute(_id: string, params: any) {
|
|
51
|
+
try {
|
|
52
|
+
const token = await tp.getAccessToken();
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
if (params.folderId) parts.push(`'${params.folderId}' in parents`);
|
|
55
|
+
if (params.mimeType) parts.push(`mimeType = '${params.mimeType}'`);
|
|
56
|
+
if (params.sharedWithMe === 'true') parts.push('sharedWithMe = true');
|
|
57
|
+
if (params.trashed !== 'true') parts.push('trashed = false');
|
|
58
|
+
if (params.query) {
|
|
59
|
+
// If it looks like Drive query syntax, use as-is; otherwise wrap in fullText
|
|
60
|
+
if (params.query.includes('=') || params.query.includes('in parents') || params.query.includes('contains')) {
|
|
61
|
+
parts.push(params.query);
|
|
62
|
+
} else {
|
|
63
|
+
parts.push(`fullText contains '${params.query.replace(/'/g, "\\'")}'`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const q: Record<string, string> = {
|
|
67
|
+
fields: 'files(id,name,mimeType,size,modifiedTime,createdTime,owners,shared,webViewLink,parents)',
|
|
68
|
+
pageSize: String(Math.min(params.maxResults || 25, 100)),
|
|
69
|
+
};
|
|
70
|
+
if (parts.length) q.q = parts.join(' and ');
|
|
71
|
+
if (params.orderBy) q.orderBy = params.orderBy;
|
|
72
|
+
const data = await gapi(token, '/files', { query: q });
|
|
73
|
+
const files = (data.files || []).map((f: any) => ({
|
|
74
|
+
id: f.id, name: f.name, mimeType: f.mimeType,
|
|
75
|
+
size: f.size ? Number(f.size) : undefined,
|
|
76
|
+
modifiedTime: f.modifiedTime, createdTime: f.createdTime,
|
|
77
|
+
owner: f.owners?.[0]?.emailAddress, shared: f.shared,
|
|
78
|
+
webViewLink: f.webViewLink, parentId: f.parents?.[0],
|
|
79
|
+
isFolder: f.mimeType === 'application/vnd.google-apps.folder',
|
|
80
|
+
}));
|
|
81
|
+
return jsonResult({ files, count: files.length });
|
|
82
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'google_drive_get',
|
|
87
|
+
description: 'Get metadata and content of a file. For Google Docs/Sheets/Slides, exports as text. For other files, returns metadata only.',
|
|
88
|
+
category: 'utility' as const,
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object' as const,
|
|
91
|
+
properties: {
|
|
92
|
+
fileId: { type: 'string', description: 'File ID (required)' },
|
|
93
|
+
exportFormat: { type: 'string', description: 'Export format for Google Docs types: "text", "html", "pdf", "csv" (for Sheets)' },
|
|
94
|
+
},
|
|
95
|
+
required: ['fileId'],
|
|
96
|
+
},
|
|
97
|
+
async execute(_id: string, params: any) {
|
|
98
|
+
try {
|
|
99
|
+
const token = await tp.getAccessToken();
|
|
100
|
+
const meta = await gapi(token, `/files/${params.fileId}`, {
|
|
101
|
+
query: { fields: 'id,name,mimeType,size,modifiedTime,createdTime,description,webViewLink,owners,shared' },
|
|
102
|
+
});
|
|
103
|
+
const result: any = {
|
|
104
|
+
id: meta.id, name: meta.name, mimeType: meta.mimeType,
|
|
105
|
+
size: meta.size ? Number(meta.size) : undefined,
|
|
106
|
+
modifiedTime: meta.modifiedTime, description: meta.description,
|
|
107
|
+
webViewLink: meta.webViewLink, owner: meta.owners?.[0]?.emailAddress,
|
|
108
|
+
};
|
|
109
|
+
// Export Google Docs content
|
|
110
|
+
const exportMap: Record<string, Record<string, string>> = {
|
|
111
|
+
'application/vnd.google-apps.document': { text: 'text/plain', html: 'text/html', pdf: 'application/pdf' },
|
|
112
|
+
'application/vnd.google-apps.spreadsheet': { csv: 'text/csv', text: 'text/csv', html: 'text/html' },
|
|
113
|
+
'application/vnd.google-apps.presentation': { text: 'text/plain', html: 'text/html', pdf: 'application/pdf' },
|
|
114
|
+
};
|
|
115
|
+
const formats = exportMap[meta.mimeType];
|
|
116
|
+
if (formats) {
|
|
117
|
+
const fmt = params.exportFormat || 'text';
|
|
118
|
+
const exportMime = formats[fmt] || formats['text'];
|
|
119
|
+
if (exportMime && (fmt === 'text' || fmt === 'csv' || fmt === 'html')) {
|
|
120
|
+
const exportRes = await fetch(`${BASE}/files/${params.fileId}/export?mimeType=${encodeURIComponent(exportMime)}`, {
|
|
121
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
122
|
+
});
|
|
123
|
+
if (exportRes.ok) {
|
|
124
|
+
const text = await exportRes.text();
|
|
125
|
+
result.content = text.slice(0, 50000); // Truncate large files
|
|
126
|
+
result.truncated = text.length > 50000;
|
|
127
|
+
result.exportFormat = fmt;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return jsonResult(result);
|
|
132
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'google_drive_create',
|
|
137
|
+
description: 'Create a new file or folder in Google Drive. For text files, provide content directly.',
|
|
138
|
+
category: 'utility' as const,
|
|
139
|
+
parameters: {
|
|
140
|
+
type: 'object' as const,
|
|
141
|
+
properties: {
|
|
142
|
+
name: { type: 'string', description: 'File/folder name (required)' },
|
|
143
|
+
mimeType: { type: 'string', description: 'MIME type. Use "application/vnd.google-apps.folder" for folders, "application/vnd.google-apps.document" for Docs, "application/vnd.google-apps.spreadsheet" for Sheets' },
|
|
144
|
+
parentId: { type: 'string', description: 'Parent folder ID' },
|
|
145
|
+
content: { type: 'string', description: 'Text content for the file' },
|
|
146
|
+
description: { type: 'string', description: 'File description' },
|
|
147
|
+
},
|
|
148
|
+
required: ['name'],
|
|
149
|
+
},
|
|
150
|
+
async execute(_id: string, params: any) {
|
|
151
|
+
try {
|
|
152
|
+
const token = await tp.getAccessToken();
|
|
153
|
+
const metadata: any = { name: params.name };
|
|
154
|
+
if (params.mimeType) metadata.mimeType = params.mimeType;
|
|
155
|
+
if (params.parentId) metadata.parents = [params.parentId];
|
|
156
|
+
if (params.description) metadata.description = params.description;
|
|
157
|
+
|
|
158
|
+
if (params.content && !params.mimeType?.startsWith('application/vnd.google-apps.')) {
|
|
159
|
+
// Multipart upload for files with content
|
|
160
|
+
const boundary = '===agenticmail_boundary===';
|
|
161
|
+
const body = `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${JSON.stringify(metadata)}\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n${params.content}\r\n--${boundary}--`;
|
|
162
|
+
const result = await gapi(token, '/files?uploadType=multipart', {
|
|
163
|
+
method: 'POST', base: UPLOAD_BASE,
|
|
164
|
+
rawBody: body,
|
|
165
|
+
headers: { 'Content-Type': `multipart/related; boundary=${boundary}` },
|
|
166
|
+
query: { fields: 'id,name,mimeType,webViewLink' },
|
|
167
|
+
});
|
|
168
|
+
return jsonResult({ created: true, fileId: result.id, name: result.name, webViewLink: result.webViewLink });
|
|
169
|
+
} else {
|
|
170
|
+
// Metadata-only (folders, Google Docs types)
|
|
171
|
+
const result = await gapi(token, '/files', {
|
|
172
|
+
method: 'POST', body: metadata,
|
|
173
|
+
query: { fields: 'id,name,mimeType,webViewLink' },
|
|
174
|
+
});
|
|
175
|
+
return jsonResult({ created: true, fileId: result.id, name: result.name, mimeType: result.mimeType, webViewLink: result.webViewLink });
|
|
176
|
+
}
|
|
177
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'google_drive_delete',
|
|
182
|
+
description: 'Move a file to trash (or permanently delete).',
|
|
183
|
+
category: 'utility' as const,
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object' as const,
|
|
186
|
+
properties: {
|
|
187
|
+
fileId: { type: 'string', description: 'File ID (required)' },
|
|
188
|
+
permanent: { type: 'string', description: 'If "true", permanently delete instead of trashing' },
|
|
189
|
+
},
|
|
190
|
+
required: ['fileId'],
|
|
191
|
+
},
|
|
192
|
+
async execute(_id: string, params: any) {
|
|
193
|
+
try {
|
|
194
|
+
const token = await tp.getAccessToken();
|
|
195
|
+
if (params.permanent === 'true') {
|
|
196
|
+
await gapi(token, `/files/${params.fileId}`, { method: 'DELETE' });
|
|
197
|
+
return jsonResult({ deleted: true, permanent: true, fileId: params.fileId });
|
|
198
|
+
} else {
|
|
199
|
+
await gapi(token, `/files/${params.fileId}`, { method: 'PATCH', body: { trashed: true } });
|
|
200
|
+
return jsonResult({ trashed: true, fileId: params.fileId });
|
|
201
|
+
}
|
|
202
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'google_drive_share',
|
|
207
|
+
description: 'Share a file with a user, group, or make it accessible via link.',
|
|
208
|
+
category: 'utility' as const,
|
|
209
|
+
parameters: {
|
|
210
|
+
type: 'object' as const,
|
|
211
|
+
properties: {
|
|
212
|
+
fileId: { type: 'string', description: 'File ID (required)' },
|
|
213
|
+
email: { type: 'string', description: 'Email to share with' },
|
|
214
|
+
role: { type: 'string', description: 'Permission role: "reader", "writer", "commenter" (default: "reader")' },
|
|
215
|
+
type: { type: 'string', description: 'Permission type: "user", "group", "domain", "anyone" (default: "user")' },
|
|
216
|
+
sendNotification: { type: 'string', description: 'Send email notification? "true" or "false" (default: "true")' },
|
|
217
|
+
},
|
|
218
|
+
required: ['fileId'],
|
|
219
|
+
},
|
|
220
|
+
async execute(_id: string, params: any) {
|
|
221
|
+
try {
|
|
222
|
+
const token = await tp.getAccessToken();
|
|
223
|
+
const permission: any = {
|
|
224
|
+
role: params.role || 'reader',
|
|
225
|
+
type: params.type || (params.email ? 'user' : 'anyone'),
|
|
226
|
+
};
|
|
227
|
+
if (params.email) permission.emailAddress = params.email;
|
|
228
|
+
const query: Record<string, string> = {};
|
|
229
|
+
if (params.sendNotification === 'false') query.sendNotificationEmail = 'false';
|
|
230
|
+
const result = await gapi(token, `/files/${params.fileId}/permissions`, { method: 'POST', body: permission, query });
|
|
231
|
+
return jsonResult({ shared: true, permissionId: result.id, role: result.role, type: result.type });
|
|
232
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'google_drive_move',
|
|
237
|
+
description: 'Move a file to a different folder.',
|
|
238
|
+
category: 'utility' as const,
|
|
239
|
+
parameters: {
|
|
240
|
+
type: 'object' as const,
|
|
241
|
+
properties: {
|
|
242
|
+
fileId: { type: 'string', description: 'File ID to move (required)' },
|
|
243
|
+
destinationFolderId: { type: 'string', description: 'Target folder ID (required)' },
|
|
244
|
+
},
|
|
245
|
+
required: ['fileId', 'destinationFolderId'],
|
|
246
|
+
},
|
|
247
|
+
async execute(_id: string, params: any) {
|
|
248
|
+
try {
|
|
249
|
+
const token = await tp.getAccessToken();
|
|
250
|
+
// Get current parents
|
|
251
|
+
const file = await gapi(token, `/files/${params.fileId}`, { query: { fields: 'parents' } });
|
|
252
|
+
const removeParents = (file.parents || []).join(',');
|
|
253
|
+
const result = await gapi(token, `/files/${params.fileId}`, {
|
|
254
|
+
method: 'PATCH', body: {},
|
|
255
|
+
query: { addParents: params.destinationFolderId, removeParents, fields: 'id,name,parents' },
|
|
256
|
+
});
|
|
257
|
+
return jsonResult({ moved: true, fileId: result.id, name: result.name, newParent: params.destinationFolderId });
|
|
258
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
}
|