tide-commander 1.47.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-BEZSJWBV.js → BossLogsModal-buht3LyW.js} +1 -1
- package/dist/assets/BossSpawnModal-D7M1JocE.js +1 -0
- package/dist/assets/{ControlsModal-BZKmtLz7.js → ControlsModal-C3-j1E6y.js} +1 -1
- package/dist/assets/{DockerLogsModal-DD8gRVu6.js → DockerLogsModal-CA2j24QX.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DMcKiZ5q.js → EmbeddedEditor-CrLtAaIO.js} +1 -1
- package/dist/assets/GmailOAuthSetup-CG2BmCyg.js +303 -0
- package/dist/assets/GoogleOAuthSetup-DlaXAoyq.js +236 -0
- package/dist/assets/{IframeModal-DJn-dNqr.js → IframeModal-CvJ6LEIu.js} +1 -1
- package/dist/assets/IntegrationsPanel-iTByepHj.js +2 -0
- package/dist/assets/{LogViewerModal-BgIfdR-l.js → LogViewerModal-C75Yyse9.js} +1 -1
- package/dist/assets/{MonitoringModal-Dm8wbcHY.js → MonitoringModal-C3NapNrP.js} +1 -1
- package/dist/assets/{PM2LogsModal-CDULU4yk.js → PM2LogsModal-CaacRI13.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DLuiZj6S.js → RestoreArchivedAreaModal-mwQqJ4qp.js} +1 -1
- package/dist/assets/{SaveSnapshotModal-CbfjHVJz.js → SaveSnapshotModal-Didvj3hf.js} +1 -1
- package/dist/assets/{Scene2DCanvas-Dw9o4Fk8.js → Scene2DCanvas-chSGLBlj.js} +1 -1
- package/dist/assets/{SceneManager-KAhHYpUf.js → SceneManager-G0H-cWU4.js} +1 -1
- package/dist/assets/{SkillsPanel-BYy3W8hd.js → SkillsPanel-nKzb0E59.js} +1 -1
- package/dist/assets/{SnapshotManager-LxlHIjaa.js → SnapshotManager-DoQRZCNb.js} +1 -1
- package/dist/assets/SpawnModal-CPTZGYNR.js +1 -0
- package/dist/assets/{SubordinateAssignmentModal-zuSTZAqj.js → SubordinateAssignmentModal-B2XP8wZU.js} +1 -1
- package/dist/assets/{SupervisorPanel-tTfMa-0x.js → SupervisorPanel-LlgMdGCQ.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-BoHdbpqk.js → TriggerManagerPanel-DMiK0_y5.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-Cusd-51G.js → WorkflowEditorPanel-DPwmjV2f.js} +1 -1
- package/dist/assets/{index-BoFICHSl.js → index-BCuYxI5F.js} +2 -2
- package/dist/assets/{index-BPn1NjZ5.js → index-BIt0Cgpm.js} +1 -1
- package/dist/assets/{index-CeMGJMtZ.js → index-BM1s5vTS.js} +3 -3
- package/dist/assets/{index-ChZ1qMl3.js → index-CG-I_YeN.js} +1 -1
- package/dist/assets/{index-ifiZ65Cu.js → index-CxEHgPe3.js} +1 -1
- package/dist/assets/{index-C_3LXJYS.js → index-D33VRpYX.js} +1 -1
- package/dist/assets/{index-D3KwWKEv.js → index-DYwDeYnm.js} +1 -1
- package/dist/assets/{index-Cxx3kFoj.js → index-XXW1ctrV.js} +1 -1
- package/dist/assets/{main-BsOvZp5M.js → main-D-o_EGqJ.js} +35 -35
- package/dist/assets/{web-B5moyGVG.js → web-Blb26Kj7.js} +1 -1
- package/dist/assets/{web-BYzo_b4X.js → web-hHpxvpTK.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/codex/backend.js +5 -1
- package/dist/src/packages/server/data/builtin-skills/report-task-to-boss.js +1 -1
- package/dist/src/packages/server/data/event-queries.js +15 -0
- package/dist/src/packages/server/data/migrations/005_drive_actions.sql +19 -0
- package/dist/src/packages/server/index.js +1 -0
- package/dist/src/packages/server/integrations/gmail/index.js +8 -6
- package/dist/src/packages/server/integrations/google-drive/drive-client.js +605 -0
- package/dist/src/packages/server/integrations/google-drive/drive-config.js +127 -0
- package/dist/src/packages/server/integrations/google-drive/drive-routes.js +365 -0
- package/dist/src/packages/server/integrations/google-drive/drive-skill.js +248 -0
- package/dist/src/packages/server/integrations/google-drive/index.js +60 -0
- package/dist/src/packages/server/integrations/integration-registry.js +2 -0
- package/dist/src/packages/server/routes/agents.js +45 -17
- package/dist/src/packages/server/services/agent-service.js +7 -1
- package/dist/src/packages/server/services/llm-matcher-service.js +3 -1
- package/dist/src/packages/shared/agent-types.js +9 -1
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-BknVJp2f.js +0 -1
- package/dist/assets/GmailOAuthSetup-CO2LqLvU.js +0 -270
- package/dist/assets/GoogleOAuthSetup-OVzavssB.js +0 -203
- package/dist/assets/IntegrationsPanel-C0JSiDOw.js +0 -2
- package/dist/assets/SpawnModal-Ch7yKsdS.js +0 -1
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Drive Client
|
|
3
|
+
* Wraps the Google Drive API via googleapis.
|
|
4
|
+
* Shares OAuth2 credentials with Gmail/Calendar plugins through the secrets system.
|
|
5
|
+
* Supports: list files, read file content, create files, update files, create folders.
|
|
6
|
+
*/
|
|
7
|
+
import { google } from 'googleapis';
|
|
8
|
+
import { loadConfig, updateConfig } from './drive-config.js';
|
|
9
|
+
import { Readable } from 'stream';
|
|
10
|
+
// ─── State ───
|
|
11
|
+
let ctx = null;
|
|
12
|
+
let driveApi = null;
|
|
13
|
+
let docsApi = null;
|
|
14
|
+
// ─── OAuth ───
|
|
15
|
+
// Combined scopes for Gmail, Calendar, and Drive (shared OAuth client)
|
|
16
|
+
const SCOPES = [
|
|
17
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
18
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
19
|
+
'https://www.googleapis.com/auth/gmail.modify',
|
|
20
|
+
'https://www.googleapis.com/auth/calendar',
|
|
21
|
+
'https://www.googleapis.com/auth/drive',
|
|
22
|
+
];
|
|
23
|
+
const REDIRECT_PATH = '/api/drive/auth/callback';
|
|
24
|
+
let oauth2Client = null;
|
|
25
|
+
// ─── Init / Shutdown ───
|
|
26
|
+
export async function init(integrationCtx) {
|
|
27
|
+
ctx = integrationCtx;
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
if (!config.enabled) {
|
|
30
|
+
ctx.log.info('Google Drive integration disabled, skipping init');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const clientId = ctx.secrets.get('GOOGLE_CLIENT_ID');
|
|
34
|
+
const clientSecret = ctx.secrets.get('GOOGLE_CLIENT_SECRET');
|
|
35
|
+
const refreshToken = ctx.secrets.get('GOOGLE_REFRESH_TOKEN');
|
|
36
|
+
if (!clientId || !clientSecret || !refreshToken) {
|
|
37
|
+
ctx.log.info('Google Drive missing OAuth credentials, skipping init');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
|
|
41
|
+
oauth2Client.setCredentials({ refresh_token: refreshToken });
|
|
42
|
+
driveApi = google.drive({ version: 'v3', auth: oauth2Client });
|
|
43
|
+
docsApi = google.docs({ version: 'v1', auth: oauth2Client });
|
|
44
|
+
ctx.log.info('Google Drive initialized');
|
|
45
|
+
}
|
|
46
|
+
export async function shutdown() {
|
|
47
|
+
driveApi = null;
|
|
48
|
+
docsApi = null;
|
|
49
|
+
}
|
|
50
|
+
// ─── Status ───
|
|
51
|
+
export function getStatus() {
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const hasCredentials = !!(ctx?.secrets.get('GOOGLE_CLIENT_ID') &&
|
|
54
|
+
ctx?.secrets.get('GOOGLE_CLIENT_SECRET') &&
|
|
55
|
+
ctx?.secrets.get('GOOGLE_REFRESH_TOKEN'));
|
|
56
|
+
return {
|
|
57
|
+
authenticated: Boolean(driveApi && hasCredentials),
|
|
58
|
+
connected: config.enabled && hasCredentials && driveApi !== null,
|
|
59
|
+
lastChecked: Date.now(),
|
|
60
|
+
error: !hasCredentials && config.enabled ? 'Missing OAuth credentials' : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function isConfigured() {
|
|
64
|
+
return driveApi !== null;
|
|
65
|
+
}
|
|
66
|
+
// ─── OAuth ───
|
|
67
|
+
export function getAuthUrl() {
|
|
68
|
+
if (!oauth2Client) {
|
|
69
|
+
const clientId = ctx?.secrets.get('GOOGLE_CLIENT_ID');
|
|
70
|
+
const clientSecret = ctx?.secrets.get('GOOGLE_CLIENT_SECRET');
|
|
71
|
+
if (!clientId || !clientSecret || !ctx) {
|
|
72
|
+
throw new Error('Google Drive OAuth not configured');
|
|
73
|
+
}
|
|
74
|
+
oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
|
|
75
|
+
}
|
|
76
|
+
return oauth2Client.generateAuthUrl({
|
|
77
|
+
access_type: 'offline',
|
|
78
|
+
scope: SCOPES,
|
|
79
|
+
prompt: 'consent',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function handleAuthCallback(code) {
|
|
83
|
+
if (!oauth2Client || !ctx)
|
|
84
|
+
throw new Error('Google Drive OAuth not initialized');
|
|
85
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
86
|
+
oauth2Client.setCredentials(tokens);
|
|
87
|
+
if (tokens.refresh_token) {
|
|
88
|
+
ctx.secrets.set('GOOGLE_REFRESH_TOKEN', tokens.refresh_token);
|
|
89
|
+
}
|
|
90
|
+
driveApi = google.drive({ version: 'v3', auth: oauth2Client });
|
|
91
|
+
docsApi = google.docs({ version: 'v1', auth: oauth2Client });
|
|
92
|
+
// Auto-enable the integration after successful OAuth
|
|
93
|
+
updateConfig({ enabled: true });
|
|
94
|
+
ctx.log.info('Google Drive OAuth complete. Drive initialized.');
|
|
95
|
+
}
|
|
96
|
+
// ─── Files CRUD ───
|
|
97
|
+
const FILE_FIELDS = 'id, name, mimeType, size, createdTime, modifiedTime, webViewLink, parents, trashed';
|
|
98
|
+
export async function listFiles(params) {
|
|
99
|
+
if (!driveApi)
|
|
100
|
+
throw new Error('Google Drive not configured');
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
const queryParts = [];
|
|
103
|
+
// Folder filter
|
|
104
|
+
const folderId = params.folderId || config.defaultFolderId;
|
|
105
|
+
if (folderId) {
|
|
106
|
+
queryParts.push(`'${folderId}' in parents`);
|
|
107
|
+
}
|
|
108
|
+
// MIME type filter
|
|
109
|
+
if (params.mimeType) {
|
|
110
|
+
queryParts.push(`mimeType = '${params.mimeType}'`);
|
|
111
|
+
}
|
|
112
|
+
// Trashed filter (default: exclude trashed)
|
|
113
|
+
if (params.trashed === true) {
|
|
114
|
+
queryParts.push('trashed = true');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
queryParts.push('trashed = false');
|
|
118
|
+
}
|
|
119
|
+
// Custom query (appended with AND)
|
|
120
|
+
if (params.query) {
|
|
121
|
+
queryParts.push(params.query);
|
|
122
|
+
}
|
|
123
|
+
// Shared Drive scoping: when a driveId is provided we scope results to that
|
|
124
|
+
// Shared Drive (corpora='drive'). When includeItemsFromAllDrives is requested,
|
|
125
|
+
// we broaden the listing across My Drive + all accessible Shared Drives.
|
|
126
|
+
const useSharedDrives = Boolean(params.driveId) || params.includeItemsFromAllDrives === true;
|
|
127
|
+
const result = await driveApi.files.list({
|
|
128
|
+
q: queryParts.join(' and '),
|
|
129
|
+
fields: `nextPageToken, files(${FILE_FIELDS})`,
|
|
130
|
+
pageSize: params.maxResults || 50,
|
|
131
|
+
pageToken: params.pageToken,
|
|
132
|
+
orderBy: params.orderBy || 'modifiedTime desc',
|
|
133
|
+
supportsAllDrives: useSharedDrives || undefined,
|
|
134
|
+
includeItemsFromAllDrives: useSharedDrives || undefined,
|
|
135
|
+
corpora: params.driveId ? 'drive' : undefined,
|
|
136
|
+
driveId: params.driveId,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
files: (result.data.files || []).map(mapDriveFile),
|
|
140
|
+
nextPageToken: result.data.nextPageToken || undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export async function getFile(fileId) {
|
|
144
|
+
if (!driveApi)
|
|
145
|
+
throw new Error('Google Drive not configured');
|
|
146
|
+
const result = await driveApi.files.get({
|
|
147
|
+
fileId,
|
|
148
|
+
fields: FILE_FIELDS,
|
|
149
|
+
supportsAllDrives: true,
|
|
150
|
+
});
|
|
151
|
+
return mapDriveFile(result.data);
|
|
152
|
+
}
|
|
153
|
+
export async function getFileContent(fileId, exportMimeType) {
|
|
154
|
+
if (!driveApi)
|
|
155
|
+
throw new Error('Google Drive not configured');
|
|
156
|
+
// First, get the file metadata to check its type
|
|
157
|
+
const meta = await driveApi.files.get({
|
|
158
|
+
fileId,
|
|
159
|
+
fields: 'mimeType, name',
|
|
160
|
+
supportsAllDrives: true,
|
|
161
|
+
});
|
|
162
|
+
const fileMimeType = meta.data.mimeType || '';
|
|
163
|
+
// Google Workspace files (Docs, Sheets, Slides) must be exported
|
|
164
|
+
if (fileMimeType.startsWith('application/vnd.google-apps.')) {
|
|
165
|
+
const exportType = exportMimeType || getDefaultExportType(fileMimeType);
|
|
166
|
+
const result = await driveApi.files.export({
|
|
167
|
+
fileId,
|
|
168
|
+
mimeType: exportType,
|
|
169
|
+
}, { responseType: 'text' });
|
|
170
|
+
return {
|
|
171
|
+
content: typeof result.data === 'string' ? result.data : String(result.data),
|
|
172
|
+
mimeType: exportType,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Regular files: download content
|
|
176
|
+
const result = await driveApi.files.get({
|
|
177
|
+
fileId,
|
|
178
|
+
alt: 'media',
|
|
179
|
+
supportsAllDrives: true,
|
|
180
|
+
}, { responseType: 'text' });
|
|
181
|
+
return {
|
|
182
|
+
content: typeof result.data === 'string' ? result.data : String(result.data),
|
|
183
|
+
mimeType: fileMimeType,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export async function createFile(params) {
|
|
187
|
+
if (!driveApi)
|
|
188
|
+
throw new Error('Google Drive not configured');
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
const folderId = params.folderId || config.defaultFolderId;
|
|
191
|
+
const fileMetadata = {
|
|
192
|
+
name: params.name,
|
|
193
|
+
description: params.description,
|
|
194
|
+
parents: folderId ? [folderId] : undefined,
|
|
195
|
+
};
|
|
196
|
+
// Determine if we should create a Google Workspace file
|
|
197
|
+
const mimeType = params.mimeType || 'text/plain';
|
|
198
|
+
const googleDocType = getGoogleDocType(mimeType);
|
|
199
|
+
// For Google Workspace files the target mimeType goes on the metadata so Drive
|
|
200
|
+
// converts the upload into a native Doc/Sheet/Slide, while the media itself must
|
|
201
|
+
// describe the SOURCE format (plain text by default, or HTML if the content
|
|
202
|
+
// looks like HTML so Docs preserves formatting).
|
|
203
|
+
let mediaMimeType = mimeType;
|
|
204
|
+
if (googleDocType) {
|
|
205
|
+
fileMetadata.mimeType = googleDocType;
|
|
206
|
+
const looksLikeHtml = /^\s*<(!doctype|html|body|div|h[1-6]|p|ul|ol|table)\b/i.test(params.content);
|
|
207
|
+
mediaMimeType = looksLikeHtml ? 'text/html' : 'text/plain';
|
|
208
|
+
}
|
|
209
|
+
const media = {
|
|
210
|
+
mimeType: mediaMimeType,
|
|
211
|
+
body: Readable.from(params.content),
|
|
212
|
+
};
|
|
213
|
+
const result = await driveApi.files.create({
|
|
214
|
+
requestBody: fileMetadata,
|
|
215
|
+
media,
|
|
216
|
+
fields: FILE_FIELDS,
|
|
217
|
+
supportsAllDrives: true,
|
|
218
|
+
});
|
|
219
|
+
const file = mapDriveFile(result.data);
|
|
220
|
+
// Log to SQLite
|
|
221
|
+
ctx?.eventDb.logDriveAction({
|
|
222
|
+
fileId: file.fileId,
|
|
223
|
+
action: 'created',
|
|
224
|
+
fileName: params.name,
|
|
225
|
+
mimeType: file.mimeType,
|
|
226
|
+
folderId: folderId || undefined,
|
|
227
|
+
agentId: params.agentId,
|
|
228
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
229
|
+
recordedAt: Date.now(),
|
|
230
|
+
});
|
|
231
|
+
return file;
|
|
232
|
+
}
|
|
233
|
+
export async function copyFile(params) {
|
|
234
|
+
if (!driveApi)
|
|
235
|
+
throw new Error('Google Drive not configured');
|
|
236
|
+
const requestBody = {};
|
|
237
|
+
if (params.name)
|
|
238
|
+
requestBody.name = params.name;
|
|
239
|
+
if (params.description !== undefined)
|
|
240
|
+
requestBody.description = params.description;
|
|
241
|
+
if (params.folderId)
|
|
242
|
+
requestBody.parents = [params.folderId];
|
|
243
|
+
const result = await driveApi.files.copy({
|
|
244
|
+
fileId: params.sourceFileId,
|
|
245
|
+
requestBody,
|
|
246
|
+
fields: FILE_FIELDS,
|
|
247
|
+
supportsAllDrives: true,
|
|
248
|
+
});
|
|
249
|
+
const file = mapDriveFile(result.data);
|
|
250
|
+
ctx?.eventDb.logDriveAction({
|
|
251
|
+
fileId: file.fileId,
|
|
252
|
+
action: 'created',
|
|
253
|
+
fileName: file.name,
|
|
254
|
+
mimeType: file.mimeType,
|
|
255
|
+
folderId: params.folderId,
|
|
256
|
+
agentId: params.agentId,
|
|
257
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
258
|
+
recordedAt: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
return file;
|
|
261
|
+
}
|
|
262
|
+
export async function moveFile(fileId, params) {
|
|
263
|
+
if (!driveApi)
|
|
264
|
+
throw new Error('Google Drive not configured');
|
|
265
|
+
// Figure out which parents to detach
|
|
266
|
+
let removeParents = params.removeFromFolderIds;
|
|
267
|
+
if (!removeParents) {
|
|
268
|
+
const existing = await driveApi.files.get({
|
|
269
|
+
fileId,
|
|
270
|
+
fields: 'parents',
|
|
271
|
+
supportsAllDrives: true,
|
|
272
|
+
});
|
|
273
|
+
removeParents = existing.data.parents || [];
|
|
274
|
+
}
|
|
275
|
+
const result = await driveApi.files.update({
|
|
276
|
+
fileId,
|
|
277
|
+
addParents: params.folderId,
|
|
278
|
+
removeParents: removeParents.join(','),
|
|
279
|
+
fields: FILE_FIELDS,
|
|
280
|
+
supportsAllDrives: true,
|
|
281
|
+
});
|
|
282
|
+
const file = mapDriveFile(result.data);
|
|
283
|
+
ctx?.eventDb.logDriveAction({
|
|
284
|
+
fileId: file.fileId,
|
|
285
|
+
action: 'updated',
|
|
286
|
+
fileName: file.name,
|
|
287
|
+
mimeType: file.mimeType,
|
|
288
|
+
folderId: params.folderId,
|
|
289
|
+
agentId: params.agentId,
|
|
290
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
291
|
+
recordedAt: Date.now(),
|
|
292
|
+
});
|
|
293
|
+
return file;
|
|
294
|
+
}
|
|
295
|
+
export async function replaceTextInDoc(fileId, params) {
|
|
296
|
+
if (!docsApi)
|
|
297
|
+
throw new Error('Google Docs API not configured');
|
|
298
|
+
const requests = params.replacements.map(r => ({
|
|
299
|
+
replaceAllText: {
|
|
300
|
+
containsText: {
|
|
301
|
+
text: r.find,
|
|
302
|
+
matchCase: r.matchCase ?? false,
|
|
303
|
+
},
|
|
304
|
+
replaceText: r.replace,
|
|
305
|
+
},
|
|
306
|
+
}));
|
|
307
|
+
// Append plain text if requested. We read the current end index so the new text
|
|
308
|
+
// goes at the tail of the body, just before the trailing newline that Docs always has.
|
|
309
|
+
let appended = false;
|
|
310
|
+
if (params.appendText && params.appendText.length > 0) {
|
|
311
|
+
const doc = await docsApi.documents.get({ documentId: fileId, fields: 'body(content(endIndex))' });
|
|
312
|
+
const segments = doc.data.body?.content || [];
|
|
313
|
+
const lastEnd = segments.reduce((max, seg) => Math.max(max, seg.endIndex || 0), 1);
|
|
314
|
+
// endIndex points one past the final newline; insert BEFORE it so the body keeps a trailing newline.
|
|
315
|
+
const insertAt = Math.max(1, lastEnd - 1);
|
|
316
|
+
requests.push({
|
|
317
|
+
insertText: {
|
|
318
|
+
location: { index: insertAt },
|
|
319
|
+
text: (insertAt > 1 ? '\n' : '') + params.appendText,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
appended = true;
|
|
323
|
+
}
|
|
324
|
+
const result = await docsApi.documents.batchUpdate({
|
|
325
|
+
documentId: fileId,
|
|
326
|
+
requestBody: { requests },
|
|
327
|
+
});
|
|
328
|
+
const totalOccurrencesChanged = (result.data.replies || []).reduce((sum, reply) => {
|
|
329
|
+
return sum + (reply.replaceAllText?.occurrencesChanged || 0);
|
|
330
|
+
}, 0);
|
|
331
|
+
ctx?.eventDb.logDriveAction({
|
|
332
|
+
fileId,
|
|
333
|
+
action: 'updated',
|
|
334
|
+
fileName: fileId,
|
|
335
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
336
|
+
agentId: params.agentId,
|
|
337
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
338
|
+
recordedAt: Date.now(),
|
|
339
|
+
});
|
|
340
|
+
return { fileId, totalOccurrencesChanged, appended };
|
|
341
|
+
}
|
|
342
|
+
// ─── Google Docs API ───
|
|
343
|
+
// These functions use the Docs API directly (instead of Drive). They operate on
|
|
344
|
+
// Google Doc files only (mimeType application/vnd.google-apps.document).
|
|
345
|
+
/**
|
|
346
|
+
* Create a blank Google Doc with the given title. Returns the same DriveFile
|
|
347
|
+
* shape as createFile for consistency.
|
|
348
|
+
*/
|
|
349
|
+
export async function createDocument(params) {
|
|
350
|
+
if (!docsApi)
|
|
351
|
+
throw new Error('Google Docs API not configured');
|
|
352
|
+
if (!driveApi)
|
|
353
|
+
throw new Error('Google Drive not configured');
|
|
354
|
+
const created = await docsApi.documents.create({
|
|
355
|
+
requestBody: { title: params.title },
|
|
356
|
+
});
|
|
357
|
+
const documentId = created.data.documentId;
|
|
358
|
+
if (!documentId)
|
|
359
|
+
throw new Error('documents.create returned no documentId');
|
|
360
|
+
// Fetch the matching Drive file record so we can return standard DriveFile fields.
|
|
361
|
+
const driveRes = await driveApi.files.get({
|
|
362
|
+
fileId: documentId,
|
|
363
|
+
fields: FILE_FIELDS,
|
|
364
|
+
supportsAllDrives: true,
|
|
365
|
+
});
|
|
366
|
+
const file = mapDriveFile(driveRes.data);
|
|
367
|
+
ctx?.eventDb.logDriveAction({
|
|
368
|
+
fileId: file.fileId,
|
|
369
|
+
action: 'created',
|
|
370
|
+
fileName: file.name,
|
|
371
|
+
mimeType: file.mimeType,
|
|
372
|
+
agentId: params.agentId,
|
|
373
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
374
|
+
recordedAt: Date.now(),
|
|
375
|
+
});
|
|
376
|
+
return file;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get the full structured Google Doc including body, headers, footers, styles,
|
|
380
|
+
* named ranges, inline objects and revision metadata. This is the Docs API's
|
|
381
|
+
* native representation — much richer than the Drive text/plain export.
|
|
382
|
+
*/
|
|
383
|
+
export async function getDocument(documentId) {
|
|
384
|
+
if (!docsApi)
|
|
385
|
+
throw new Error('Google Docs API not configured');
|
|
386
|
+
const result = await docsApi.documents.get({ documentId });
|
|
387
|
+
return result.data;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Generic passthrough to documents.batchUpdate. Accepts the raw requests array
|
|
391
|
+
* so every Docs API mutation type (replaceAllText, insertText, insertTable,
|
|
392
|
+
* insertInlineImage, updateParagraphStyle, updateTextStyle, createParagraphBullets,
|
|
393
|
+
* insertPageBreak, insertSectionBreak, replaceImage, createHeader, createFooter,
|
|
394
|
+
* createFootnote, deleteContentRange, etc.) is supported without any new server
|
|
395
|
+
* code.
|
|
396
|
+
*/
|
|
397
|
+
export async function batchUpdateDocument(documentId, params) {
|
|
398
|
+
if (!docsApi)
|
|
399
|
+
throw new Error('Google Docs API not configured');
|
|
400
|
+
const requestBody = {
|
|
401
|
+
requests: params.requests,
|
|
402
|
+
};
|
|
403
|
+
if (params.writeControl)
|
|
404
|
+
requestBody.writeControl = params.writeControl;
|
|
405
|
+
const result = await docsApi.documents.batchUpdate({
|
|
406
|
+
documentId,
|
|
407
|
+
requestBody,
|
|
408
|
+
});
|
|
409
|
+
ctx?.eventDb.logDriveAction({
|
|
410
|
+
fileId: documentId,
|
|
411
|
+
action: 'updated',
|
|
412
|
+
fileName: documentId,
|
|
413
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
414
|
+
agentId: params.agentId,
|
|
415
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
416
|
+
recordedAt: Date.now(),
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
documentId: result.data.documentId || documentId,
|
|
420
|
+
replies: result.data.replies || [],
|
|
421
|
+
writeControl: result.data.writeControl || undefined,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
export async function updateFile(fileId, params) {
|
|
425
|
+
if (!driveApi)
|
|
426
|
+
throw new Error('Google Drive not configured');
|
|
427
|
+
const requestBody = {};
|
|
428
|
+
if (params.name !== undefined)
|
|
429
|
+
requestBody.name = params.name;
|
|
430
|
+
if (params.description !== undefined)
|
|
431
|
+
requestBody.description = params.description;
|
|
432
|
+
let media;
|
|
433
|
+
if (params.content !== undefined) {
|
|
434
|
+
const mimeType = params.mimeType || 'text/plain';
|
|
435
|
+
media = {
|
|
436
|
+
mimeType,
|
|
437
|
+
body: Readable.from(params.content),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const result = await driveApi.files.update({
|
|
441
|
+
fileId,
|
|
442
|
+
requestBody,
|
|
443
|
+
media,
|
|
444
|
+
fields: FILE_FIELDS,
|
|
445
|
+
supportsAllDrives: true,
|
|
446
|
+
});
|
|
447
|
+
const file = mapDriveFile(result.data);
|
|
448
|
+
ctx?.eventDb.logDriveAction({
|
|
449
|
+
fileId: file.fileId,
|
|
450
|
+
action: 'updated',
|
|
451
|
+
fileName: file.name,
|
|
452
|
+
mimeType: file.mimeType,
|
|
453
|
+
agentId: params.agentId,
|
|
454
|
+
workflowInstanceId: params.workflowInstanceId,
|
|
455
|
+
recordedAt: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
return file;
|
|
458
|
+
}
|
|
459
|
+
export async function deleteFile(fileId, opts) {
|
|
460
|
+
if (!driveApi)
|
|
461
|
+
throw new Error('Google Drive not configured');
|
|
462
|
+
// Get file name before deletion for logging
|
|
463
|
+
let fileName = fileId;
|
|
464
|
+
try {
|
|
465
|
+
const existing = await driveApi.files.get({ fileId, fields: 'name, mimeType', supportsAllDrives: true });
|
|
466
|
+
fileName = existing.data.name || fileId;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// File may already be deleted, proceed
|
|
470
|
+
}
|
|
471
|
+
await driveApi.files.delete({ fileId, supportsAllDrives: true });
|
|
472
|
+
ctx?.eventDb.logDriveAction({
|
|
473
|
+
fileId,
|
|
474
|
+
action: 'deleted',
|
|
475
|
+
fileName,
|
|
476
|
+
mimeType: '',
|
|
477
|
+
agentId: opts?.agentId,
|
|
478
|
+
workflowInstanceId: opts?.workflowInstanceId,
|
|
479
|
+
recordedAt: Date.now(),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
export async function createFolder(name, parentFolderId, opts) {
|
|
483
|
+
if (!driveApi)
|
|
484
|
+
throw new Error('Google Drive not configured');
|
|
485
|
+
const config = loadConfig();
|
|
486
|
+
const parent = parentFolderId || config.defaultFolderId;
|
|
487
|
+
const result = await driveApi.files.create({
|
|
488
|
+
requestBody: {
|
|
489
|
+
name,
|
|
490
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
491
|
+
parents: parent ? [parent] : undefined,
|
|
492
|
+
},
|
|
493
|
+
fields: FILE_FIELDS,
|
|
494
|
+
supportsAllDrives: true,
|
|
495
|
+
});
|
|
496
|
+
const folder = mapDriveFile(result.data);
|
|
497
|
+
ctx?.eventDb.logDriveAction({
|
|
498
|
+
fileId: folder.fileId,
|
|
499
|
+
action: 'created',
|
|
500
|
+
fileName: name,
|
|
501
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
502
|
+
folderId: parent || undefined,
|
|
503
|
+
agentId: opts?.agentId,
|
|
504
|
+
workflowInstanceId: opts?.workflowInstanceId,
|
|
505
|
+
recordedAt: Date.now(),
|
|
506
|
+
});
|
|
507
|
+
return folder;
|
|
508
|
+
}
|
|
509
|
+
export async function searchFiles(queryText, maxResults) {
|
|
510
|
+
if (!driveApi)
|
|
511
|
+
throw new Error('Google Drive not configured');
|
|
512
|
+
const result = await driveApi.files.list({
|
|
513
|
+
q: `fullText contains '${queryText.replace(/'/g, "\\'")}' and trashed = false`,
|
|
514
|
+
fields: `files(${FILE_FIELDS})`,
|
|
515
|
+
pageSize: maxResults || 20,
|
|
516
|
+
orderBy: 'modifiedTime desc',
|
|
517
|
+
supportsAllDrives: true,
|
|
518
|
+
includeItemsFromAllDrives: true,
|
|
519
|
+
});
|
|
520
|
+
return (result.data.files || []).map(mapDriveFile);
|
|
521
|
+
}
|
|
522
|
+
// ─── Shared Drives (Team Drives) ───
|
|
523
|
+
const DRIVE_FIELDS = 'id, name, createdTime, hidden, colorRgb, themeId, backgroundImageLink, capabilities';
|
|
524
|
+
export async function listSharedDrives(params = {}) {
|
|
525
|
+
if (!driveApi)
|
|
526
|
+
throw new Error('Google Drive not configured');
|
|
527
|
+
const result = await driveApi.drives.list({
|
|
528
|
+
pageSize: params.maxResults || 50,
|
|
529
|
+
pageToken: params.pageToken,
|
|
530
|
+
q: params.query,
|
|
531
|
+
useDomainAdminAccess: params.useDomainAdminAccess,
|
|
532
|
+
fields: `nextPageToken, drives(${DRIVE_FIELDS})`,
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
drives: (result.data.drives || []).map(mapSharedDrive),
|
|
536
|
+
nextPageToken: result.data.nextPageToken || undefined,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
export async function getSharedDrive(driveId) {
|
|
540
|
+
if (!driveApi)
|
|
541
|
+
throw new Error('Google Drive not configured');
|
|
542
|
+
const result = await driveApi.drives.get({
|
|
543
|
+
driveId,
|
|
544
|
+
fields: DRIVE_FIELDS,
|
|
545
|
+
});
|
|
546
|
+
return mapSharedDrive(result.data);
|
|
547
|
+
}
|
|
548
|
+
// ─── Helpers ───
|
|
549
|
+
function mapDriveFile(data) {
|
|
550
|
+
return {
|
|
551
|
+
fileId: data.id || '',
|
|
552
|
+
name: data.name || '',
|
|
553
|
+
mimeType: data.mimeType || '',
|
|
554
|
+
size: data.size || undefined,
|
|
555
|
+
createdTime: data.createdTime || undefined,
|
|
556
|
+
modifiedTime: data.modifiedTime || undefined,
|
|
557
|
+
webViewLink: data.webViewLink || undefined,
|
|
558
|
+
parents: data.parents || undefined,
|
|
559
|
+
trashed: data.trashed || false,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function mapSharedDrive(data) {
|
|
563
|
+
return {
|
|
564
|
+
driveId: data.id || '',
|
|
565
|
+
name: data.name || '',
|
|
566
|
+
createdTime: data.createdTime || undefined,
|
|
567
|
+
hidden: data.hidden || false,
|
|
568
|
+
colorRgb: data.colorRgb || undefined,
|
|
569
|
+
themeId: data.themeId || undefined,
|
|
570
|
+
backgroundImageLink: data.backgroundImageLink || undefined,
|
|
571
|
+
capabilities: data.capabilities
|
|
572
|
+
? {
|
|
573
|
+
canEdit: data.capabilities.canEdit || undefined,
|
|
574
|
+
canManageMembers: data.capabilities.canManageMembers || undefined,
|
|
575
|
+
canShare: data.capabilities.canShare || undefined,
|
|
576
|
+
}
|
|
577
|
+
: undefined,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function getDefaultExportType(googleMimeType) {
|
|
581
|
+
switch (googleMimeType) {
|
|
582
|
+
case 'application/vnd.google-apps.document':
|
|
583
|
+
return 'text/plain';
|
|
584
|
+
case 'application/vnd.google-apps.spreadsheet':
|
|
585
|
+
return 'text/csv';
|
|
586
|
+
case 'application/vnd.google-apps.presentation':
|
|
587
|
+
return 'text/plain';
|
|
588
|
+
case 'application/vnd.google-apps.drawing':
|
|
589
|
+
return 'image/png';
|
|
590
|
+
default:
|
|
591
|
+
return 'text/plain';
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function getGoogleDocType(mimeType) {
|
|
595
|
+
switch (mimeType) {
|
|
596
|
+
case 'application/vnd.google-apps.document':
|
|
597
|
+
return 'application/vnd.google-apps.document';
|
|
598
|
+
case 'application/vnd.google-apps.spreadsheet':
|
|
599
|
+
return 'application/vnd.google-apps.spreadsheet';
|
|
600
|
+
case 'application/vnd.google-apps.presentation':
|
|
601
|
+
return 'application/vnd.google-apps.presentation';
|
|
602
|
+
default:
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|