@plosson/agentio 0.3.2 → 0.4.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/package.json +1 -1
- package/src/auth/oauth.ts +24 -2
- package/src/auth/token-manager.ts +10 -8
- package/src/commands/discourse.ts +3 -3
- package/src/commands/gchat.ts +4 -4
- package/src/commands/gdocs.ts +186 -0
- package/src/commands/gdrive.ts +285 -0
- package/src/commands/github.ts +2 -2
- package/src/commands/gmail.ts +10 -10
- package/src/commands/jira.ts +16 -14
- package/src/commands/slack.ts +1 -1
- package/src/commands/sql.ts +3 -2
- package/src/commands/status.ts +14 -0
- package/src/commands/telegram.ts +1 -1
- package/src/config/config-manager.ts +35 -1
- package/src/index.ts +4 -0
- package/src/services/gdocs/client.ts +165 -0
- package/src/services/gdrive/client.ts +452 -0
- package/src/types/config.ts +3 -1
- package/src/types/gdocs.ts +28 -0
- package/src/types/gdrive.ts +81 -0
- package/src/utils/client-factory.ts +12 -9
- package/src/utils/output.ts +109 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { readFile, writeFile, stat } from 'fs/promises';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import type { drive_v3 } from 'googleapis';
|
|
5
|
+
import { CliError, httpStatusToErrorCode, type ErrorCode } from '../../utils/errors';
|
|
6
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
7
|
+
import { GOOGLE_OAUTH_CONFIG } from '../../config/credentials';
|
|
8
|
+
import type {
|
|
9
|
+
GDriveCredentials,
|
|
10
|
+
GDriveFile,
|
|
11
|
+
GDriveListOptions,
|
|
12
|
+
GDriveSearchOptions,
|
|
13
|
+
GDriveFolderListOptions,
|
|
14
|
+
GDriveDownloadOptions,
|
|
15
|
+
GDriveDownloadResult,
|
|
16
|
+
GDriveUploadResult,
|
|
17
|
+
GDriveUploadOptions,
|
|
18
|
+
GDriveExportFormat,
|
|
19
|
+
} from '../../types/gdrive';
|
|
20
|
+
|
|
21
|
+
// Export MIME types for Google Workspace files
|
|
22
|
+
const EXPORT_MIME_TYPES: Record<string, Record<GDriveExportFormat, string | undefined>> = {
|
|
23
|
+
'application/vnd.google-apps.document': {
|
|
24
|
+
pdf: 'application/pdf',
|
|
25
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
26
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
27
|
+
txt: 'text/plain',
|
|
28
|
+
html: 'text/html',
|
|
29
|
+
rtf: 'application/rtf',
|
|
30
|
+
xlsx: undefined, csv: undefined, pptx: undefined, ods: undefined, odp: undefined, tsv: undefined, png: undefined, jpeg: undefined, svg: undefined,
|
|
31
|
+
},
|
|
32
|
+
'application/vnd.google-apps.spreadsheet': {
|
|
33
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
34
|
+
csv: 'text/csv',
|
|
35
|
+
pdf: 'application/pdf',
|
|
36
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
37
|
+
tsv: 'text/tab-separated-values',
|
|
38
|
+
docx: undefined, odt: undefined, txt: undefined, html: undefined, rtf: undefined, pptx: undefined, odp: undefined, png: undefined, jpeg: undefined, svg: undefined,
|
|
39
|
+
},
|
|
40
|
+
'application/vnd.google-apps.presentation': {
|
|
41
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
42
|
+
pdf: 'application/pdf',
|
|
43
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
44
|
+
txt: 'text/plain',
|
|
45
|
+
docx: undefined, odt: undefined, html: undefined, rtf: undefined, xlsx: undefined, csv: undefined, ods: undefined, tsv: undefined, png: undefined, jpeg: undefined, svg: undefined,
|
|
46
|
+
},
|
|
47
|
+
'application/vnd.google-apps.drawing': {
|
|
48
|
+
pdf: 'application/pdf',
|
|
49
|
+
png: 'image/png',
|
|
50
|
+
jpeg: 'image/jpeg',
|
|
51
|
+
svg: 'image/svg+xml',
|
|
52
|
+
docx: undefined, odt: undefined, txt: undefined, html: undefined, rtf: undefined, xlsx: undefined, csv: undefined, pptx: undefined, ods: undefined, odp: undefined, tsv: undefined,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// MIME types that trigger conversion to Google Workspace format
|
|
57
|
+
const CONVERTIBLE_MIME_TYPES: Record<string, string> = {
|
|
58
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'application/vnd.google-apps.document',
|
|
59
|
+
'application/msword': 'application/vnd.google-apps.document',
|
|
60
|
+
'application/vnd.oasis.opendocument.text': 'application/vnd.google-apps.document',
|
|
61
|
+
'text/plain': 'application/vnd.google-apps.document',
|
|
62
|
+
'text/html': 'application/vnd.google-apps.document',
|
|
63
|
+
'application/rtf': 'application/vnd.google-apps.document',
|
|
64
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'application/vnd.google-apps.spreadsheet',
|
|
65
|
+
'application/vnd.ms-excel': 'application/vnd.google-apps.spreadsheet',
|
|
66
|
+
'application/vnd.oasis.opendocument.spreadsheet': 'application/vnd.google-apps.spreadsheet',
|
|
67
|
+
'text/csv': 'application/vnd.google-apps.spreadsheet',
|
|
68
|
+
'text/tab-separated-values': 'application/vnd.google-apps.spreadsheet',
|
|
69
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'application/vnd.google-apps.presentation',
|
|
70
|
+
'application/vnd.ms-powerpoint': 'application/vnd.google-apps.presentation',
|
|
71
|
+
'application/vnd.oasis.opendocument.presentation': 'application/vnd.google-apps.presentation',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// File extension to MIME type mapping for conversion
|
|
75
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
76
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
77
|
+
'.doc': 'application/msword',
|
|
78
|
+
'.odt': 'application/vnd.oasis.opendocument.text',
|
|
79
|
+
'.txt': 'text/plain',
|
|
80
|
+
'.html': 'text/html',
|
|
81
|
+
'.htm': 'text/html',
|
|
82
|
+
'.rtf': 'application/rtf',
|
|
83
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
84
|
+
'.xls': 'application/vnd.ms-excel',
|
|
85
|
+
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
|
86
|
+
'.csv': 'text/csv',
|
|
87
|
+
'.tsv': 'text/tab-separated-values',
|
|
88
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
89
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
90
|
+
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export class GDriveClient implements ServiceClient {
|
|
94
|
+
private credentials: GDriveCredentials;
|
|
95
|
+
private drive: drive_v3.Drive;
|
|
96
|
+
|
|
97
|
+
constructor(credentials: GDriveCredentials) {
|
|
98
|
+
this.credentials = credentials;
|
|
99
|
+
const auth = this.createOAuthClient();
|
|
100
|
+
this.drive = google.drive({ version: 'v3', auth });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async validate(): Promise<ValidationResult> {
|
|
104
|
+
try {
|
|
105
|
+
await this.drive.files.list({ pageSize: 1 });
|
|
106
|
+
return { valid: true, info: this.credentials.email };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
109
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
110
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
111
|
+
}
|
|
112
|
+
return { valid: false, error: message };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async list(options: GDriveListOptions = {}): Promise<GDriveFile[]> {
|
|
117
|
+
const { limit = 20, folderId, query, orderBy = 'modifiedTime desc', includeTrash = false } = options;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const queryParts: string[] = [];
|
|
121
|
+
|
|
122
|
+
if (!includeTrash) {
|
|
123
|
+
queryParts.push('trashed = false');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (folderId) {
|
|
127
|
+
queryParts.push(`'${folderId}' in parents`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (query) {
|
|
131
|
+
queryParts.push(query);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const q = queryParts.join(' and ') || undefined;
|
|
135
|
+
|
|
136
|
+
const response = await this.drive.files.list({
|
|
137
|
+
pageSize: Math.min(limit, 100),
|
|
138
|
+
q,
|
|
139
|
+
fields: 'files(id,name,mimeType,size,createdTime,modifiedTime,owners,parents,webViewLink,webContentLink,starred,trashed,shared,description)',
|
|
140
|
+
orderBy,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return (response.data.files || []).map(this.parseFile);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
this.throwApiError(err, 'list files');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async listFolders(options: GDriveFolderListOptions = {}): Promise<GDriveFile[]> {
|
|
150
|
+
const { limit = 20, parentId, query } = options;
|
|
151
|
+
|
|
152
|
+
const queryParts: string[] = [
|
|
153
|
+
"mimeType = 'application/vnd.google-apps.folder'",
|
|
154
|
+
'trashed = false',
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
if (parentId) {
|
|
158
|
+
queryParts.push(`'${parentId}' in parents`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (query) {
|
|
162
|
+
queryParts.push(query);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.list({
|
|
166
|
+
limit,
|
|
167
|
+
query: queryParts.join(' and '),
|
|
168
|
+
orderBy: 'name',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async get(fileIdOrUrl: string): Promise<GDriveFile> {
|
|
173
|
+
const fileId = this.extractFileId(fileIdOrUrl);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await this.drive.files.get({
|
|
177
|
+
fileId,
|
|
178
|
+
fields: 'id,name,mimeType,size,createdTime,modifiedTime,owners,parents,webViewLink,webContentLink,starred,trashed,shared,description',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return this.parseFile(response.data);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.throwApiError(err, 'get file');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async search(options: GDriveSearchOptions): Promise<GDriveFile[]> {
|
|
188
|
+
const { query, mimeType, limit = 20, folderId } = options;
|
|
189
|
+
|
|
190
|
+
const queryParts: string[] = [
|
|
191
|
+
'trashed = false',
|
|
192
|
+
`fullText contains '${query.replace(/'/g, "\\'")}'`,
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
if (mimeType) {
|
|
196
|
+
queryParts.push(`mimeType = '${mimeType}'`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (folderId) {
|
|
200
|
+
queryParts.push(`'${folderId}' in parents`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this.list({
|
|
204
|
+
limit,
|
|
205
|
+
query: queryParts.join(' and '),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async download(options: GDriveDownloadOptions): Promise<GDriveDownloadResult> {
|
|
210
|
+
const { fileIdOrUrl, outputPath, exportFormat } = options;
|
|
211
|
+
const fileId = this.extractFileId(fileIdOrUrl);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const file = await this.get(fileId);
|
|
215
|
+
const isWorkspaceFile = file.mimeType.startsWith('application/vnd.google-apps.');
|
|
216
|
+
|
|
217
|
+
// Handle Google Workspace files
|
|
218
|
+
if (isWorkspaceFile) {
|
|
219
|
+
if (!exportFormat) {
|
|
220
|
+
const supportedFormats = this.getSupportedExportFormats(file.mimeType);
|
|
221
|
+
throw new CliError(
|
|
222
|
+
'INVALID_PARAMS',
|
|
223
|
+
`Cannot download Google ${this.getWorkspaceTypeName(file.mimeType)} directly`,
|
|
224
|
+
`Use --export with one of: ${supportedFormats.join(', ')}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const exportMimeType = this.getExportMimeType(file.mimeType, exportFormat);
|
|
229
|
+
if (!exportMimeType) {
|
|
230
|
+
const supportedFormats = this.getSupportedExportFormats(file.mimeType);
|
|
231
|
+
throw new CliError(
|
|
232
|
+
'INVALID_PARAMS',
|
|
233
|
+
`Cannot export Google ${this.getWorkspaceTypeName(file.mimeType)} to ${exportFormat}`,
|
|
234
|
+
`Supported formats: ${supportedFormats.join(', ')}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const response = await this.drive.files.export(
|
|
239
|
+
{ fileId, mimeType: exportMimeType },
|
|
240
|
+
{ responseType: 'arraybuffer' }
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const buffer = Buffer.from(response.data as ArrayBuffer);
|
|
244
|
+
await writeFile(outputPath, buffer);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
filename: file.name,
|
|
248
|
+
path: outputPath,
|
|
249
|
+
size: buffer.length,
|
|
250
|
+
mimeType: exportMimeType,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle regular files
|
|
255
|
+
if (exportFormat) {
|
|
256
|
+
throw new CliError(
|
|
257
|
+
'INVALID_PARAMS',
|
|
258
|
+
'Export format is only for Google Workspace files',
|
|
259
|
+
'Remove --export flag for regular files'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const response = await this.drive.files.get(
|
|
264
|
+
{ fileId, alt: 'media' },
|
|
265
|
+
{ responseType: 'arraybuffer' }
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const buffer = Buffer.from(response.data as ArrayBuffer);
|
|
269
|
+
await writeFile(outputPath, buffer);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
filename: file.name,
|
|
273
|
+
path: outputPath,
|
|
274
|
+
size: buffer.length,
|
|
275
|
+
mimeType: file.mimeType,
|
|
276
|
+
};
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (err instanceof CliError) throw err;
|
|
279
|
+
this.throwApiError(err, 'download file');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async upload(options: GDriveUploadOptions): Promise<GDriveUploadResult> {
|
|
284
|
+
// Treat missing accessLevel as readonly for backward compatibility
|
|
285
|
+
if (!this.credentials.accessLevel || this.credentials.accessLevel === 'readonly') {
|
|
286
|
+
throw new CliError(
|
|
287
|
+
'PERMISSION_DENIED',
|
|
288
|
+
'This profile has read-only access',
|
|
289
|
+
'Create a new profile with full access: agentio gdrive profile add --full'
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { filePath, name, folderId, mimeType, convert } = options;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const { extname } = await import('path');
|
|
297
|
+
const fileStats = await stat(filePath);
|
|
298
|
+
const content = await readFile(filePath);
|
|
299
|
+
const fileName = name || basename(filePath);
|
|
300
|
+
const ext = extname(filePath).toLowerCase();
|
|
301
|
+
|
|
302
|
+
// Determine source MIME type
|
|
303
|
+
const sourceMimeType = mimeType || EXT_TO_MIME[ext] || 'application/octet-stream';
|
|
304
|
+
|
|
305
|
+
// Check if conversion is requested
|
|
306
|
+
let targetMimeType: string | undefined;
|
|
307
|
+
if (convert) {
|
|
308
|
+
targetMimeType = CONVERTIBLE_MIME_TYPES[sourceMimeType];
|
|
309
|
+
if (!targetMimeType) {
|
|
310
|
+
throw new CliError(
|
|
311
|
+
'INVALID_PARAMS',
|
|
312
|
+
`Cannot convert ${ext || 'this file type'} to Google Workspace format`,
|
|
313
|
+
'Supported: docx, doc, odt, txt, html, rtf, xlsx, xls, ods, csv, tsv, pptx, ppt, odp'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const fileMetadata: { name: string; parents?: string[]; mimeType?: string } = {
|
|
319
|
+
name: convert ? fileName.replace(/\.[^.]+$/, '') : fileName, // Remove extension when converting
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (folderId) {
|
|
323
|
+
fileMetadata.parents = [folderId];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (targetMimeType) {
|
|
327
|
+
fileMetadata.mimeType = targetMimeType;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const response = await this.drive.files.create({
|
|
331
|
+
requestBody: fileMetadata,
|
|
332
|
+
media: {
|
|
333
|
+
mimeType: sourceMimeType,
|
|
334
|
+
body: content,
|
|
335
|
+
},
|
|
336
|
+
fields: 'id,name,mimeType,size,webViewLink',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
id: response.data.id!,
|
|
341
|
+
name: response.data.name || fileName,
|
|
342
|
+
mimeType: response.data.mimeType || sourceMimeType,
|
|
343
|
+
size: fileStats.size,
|
|
344
|
+
webViewLink: response.data.webViewLink || undefined,
|
|
345
|
+
};
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (err instanceof CliError) throw err;
|
|
348
|
+
this.throwApiError(err, 'upload file');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private extractFileId(fileIdOrUrl: string): string {
|
|
353
|
+
const patterns = [
|
|
354
|
+
/\/file\/d\/([a-zA-Z0-9_-]+)/,
|
|
355
|
+
/\/folders\/([a-zA-Z0-9_-]+)/,
|
|
356
|
+
/id=([a-zA-Z0-9_-]+)/,
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
for (const pattern of patterns) {
|
|
360
|
+
const match = fileIdOrUrl.match(pattern);
|
|
361
|
+
if (match) return match[1];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return fileIdOrUrl;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private createOAuthClient() {
|
|
368
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
369
|
+
GOOGLE_OAUTH_CONFIG.clientId,
|
|
370
|
+
GOOGLE_OAUTH_CONFIG.clientSecret
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
oauth2Client.setCredentials({
|
|
374
|
+
access_token: this.credentials.accessToken,
|
|
375
|
+
refresh_token: this.credentials.refreshToken,
|
|
376
|
+
expiry_date: this.credentials.expiryDate,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return oauth2Client;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private parseFile(file: drive_v3.Schema$File): GDriveFile {
|
|
383
|
+
return {
|
|
384
|
+
id: file.id!,
|
|
385
|
+
name: file.name || 'Untitled',
|
|
386
|
+
mimeType: file.mimeType || 'application/octet-stream',
|
|
387
|
+
size: file.size ? parseInt(file.size, 10) : undefined,
|
|
388
|
+
createdTime: file.createdTime || undefined,
|
|
389
|
+
modifiedTime: file.modifiedTime || undefined,
|
|
390
|
+
owners: file.owners?.map((o) => o.displayName || o.emailAddress || 'Unknown'),
|
|
391
|
+
parents: file.parents || undefined,
|
|
392
|
+
webViewLink: file.webViewLink || undefined,
|
|
393
|
+
webContentLink: file.webContentLink || undefined,
|
|
394
|
+
starred: file.starred || false,
|
|
395
|
+
trashed: file.trashed || false,
|
|
396
|
+
shared: file.shared || false,
|
|
397
|
+
description: file.description || undefined,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private getWorkspaceTypeName(mimeType: string): string {
|
|
402
|
+
const types: Record<string, string> = {
|
|
403
|
+
'application/vnd.google-apps.document': 'Doc',
|
|
404
|
+
'application/vnd.google-apps.spreadsheet': 'Sheet',
|
|
405
|
+
'application/vnd.google-apps.presentation': 'Slide',
|
|
406
|
+
'application/vnd.google-apps.drawing': 'Drawing',
|
|
407
|
+
'application/vnd.google-apps.form': 'Form',
|
|
408
|
+
};
|
|
409
|
+
return types[mimeType] || 'Workspace file';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private getExportMimeType(sourceMimeType: string, format: GDriveExportFormat): string | undefined {
|
|
413
|
+
const formatMap = EXPORT_MIME_TYPES[sourceMimeType];
|
|
414
|
+
return formatMap?.[format];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private getSupportedExportFormats(mimeType: string): string[] {
|
|
418
|
+
const formatMap = EXPORT_MIME_TYPES[mimeType];
|
|
419
|
+
if (!formatMap) return [];
|
|
420
|
+
return Object.entries(formatMap)
|
|
421
|
+
.filter(([, mime]) => mime !== undefined)
|
|
422
|
+
.map(([format]) => format);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private throwApiError(err: unknown, operation: string): never {
|
|
426
|
+
const code = this.getErrorCode(err);
|
|
427
|
+
const message = this.getErrorMessage(err);
|
|
428
|
+
throw new CliError(code, `Failed to ${operation}: ${message}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private getErrorCode(err: unknown): ErrorCode {
|
|
432
|
+
if (err && typeof err === 'object') {
|
|
433
|
+
const error = err as Record<string, unknown>;
|
|
434
|
+
const code = error.code || error.status;
|
|
435
|
+
if (typeof code === 'number') return httpStatusToErrorCode(code);
|
|
436
|
+
}
|
|
437
|
+
return 'API_ERROR';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private getErrorMessage(err: unknown): string {
|
|
441
|
+
if (err && typeof err === 'object') {
|
|
442
|
+
const error = err as Record<string, unknown>;
|
|
443
|
+
const code = error.code || error.status;
|
|
444
|
+
if (code === 401) return 'OAuth token expired or invalid';
|
|
445
|
+
if (code === 403) return 'Insufficient permissions to access this file';
|
|
446
|
+
if (code === 404) return 'File not found';
|
|
447
|
+
if (code === 429) return 'Rate limit exceeded, please try again later';
|
|
448
|
+
if (error.message && typeof error.message === 'string') return error.message;
|
|
449
|
+
}
|
|
450
|
+
return err instanceof Error ? err.message : String(err);
|
|
451
|
+
}
|
|
452
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface Config {
|
|
2
2
|
profiles: {
|
|
3
|
+
gdocs?: string[];
|
|
4
|
+
gdrive?: string[];
|
|
3
5
|
gmail?: string[];
|
|
4
6
|
gchat?: string[];
|
|
5
7
|
github?: string[];
|
|
@@ -12,4 +14,4 @@ export interface Config {
|
|
|
12
14
|
env?: Record<string, string>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export type ServiceName = 'gmail' | 'gchat' | 'github' | 'jira' | 'slack' | 'telegram' | 'discourse' | 'sql';
|
|
17
|
+
export type ServiceName = 'gdocs' | 'gdrive' | 'gmail' | 'gchat' | 'github' | 'jira' | 'slack' | 'telegram' | 'discourse' | 'sql';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface GDocsCredentials {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
expiryDate?: number;
|
|
5
|
+
tokenType: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
email: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GDocsDocument {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
owner?: string;
|
|
14
|
+
createdTime?: string;
|
|
15
|
+
modifiedTime?: string;
|
|
16
|
+
webViewLink: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GDocsListOptions {
|
|
20
|
+
limit?: number;
|
|
21
|
+
query?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GDocsCreateResult {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
webViewLink: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type GDriveAccessLevel = 'readonly' | 'full';
|
|
2
|
+
|
|
3
|
+
export interface GDriveCredentials {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
refreshToken?: string;
|
|
6
|
+
expiryDate?: number;
|
|
7
|
+
tokenType: string;
|
|
8
|
+
scope?: string;
|
|
9
|
+
email: string;
|
|
10
|
+
accessLevel: GDriveAccessLevel;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GDriveFile {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
createdTime?: string;
|
|
19
|
+
modifiedTime?: string;
|
|
20
|
+
owners?: string[];
|
|
21
|
+
parents?: string[];
|
|
22
|
+
webViewLink?: string;
|
|
23
|
+
webContentLink?: string;
|
|
24
|
+
starred: boolean;
|
|
25
|
+
trashed: boolean;
|
|
26
|
+
shared: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GDriveListOptions {
|
|
31
|
+
limit?: number;
|
|
32
|
+
folderId?: string;
|
|
33
|
+
query?: string;
|
|
34
|
+
orderBy?: string;
|
|
35
|
+
includeTrash?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GDriveSearchOptions {
|
|
39
|
+
query: string;
|
|
40
|
+
mimeType?: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
folderId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GDriveFolderListOptions {
|
|
46
|
+
limit?: number;
|
|
47
|
+
parentId?: string;
|
|
48
|
+
query?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GDriveDownloadOptions {
|
|
52
|
+
fileIdOrUrl: string;
|
|
53
|
+
outputPath: string;
|
|
54
|
+
exportFormat?: GDriveExportFormat;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GDriveDownloadResult {
|
|
58
|
+
filename: string;
|
|
59
|
+
path: string;
|
|
60
|
+
size: number;
|
|
61
|
+
mimeType: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Export formats for Google Workspace files
|
|
65
|
+
export type GDriveExportFormat = 'pdf' | 'docx' | 'xlsx' | 'csv' | 'pptx' | 'txt' | 'html' | 'odt' | 'ods' | 'odp' | 'rtf' | 'tsv' | 'png' | 'jpeg' | 'svg';
|
|
66
|
+
|
|
67
|
+
export interface GDriveUploadOptions {
|
|
68
|
+
filePath: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
folderId?: string;
|
|
71
|
+
mimeType?: string;
|
|
72
|
+
convert?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface GDriveUploadResult {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
mimeType: string;
|
|
79
|
+
size: number;
|
|
80
|
+
webViewLink?: string;
|
|
81
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getCredentials } from '../auth/token-store';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveProfile } from '../config/config-manager';
|
|
3
3
|
import { CliError } from './errors';
|
|
4
4
|
import type { ServiceName } from '../types/config';
|
|
5
5
|
|
|
@@ -10,6 +10,7 @@ export interface ClientFactoryConfig<TCredentials, TClient> {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Creates a type-safe client getter function for a service.
|
|
13
|
+
* Profile is optional - if only one profile exists, it will be used automatically.
|
|
13
14
|
*
|
|
14
15
|
* Usage:
|
|
15
16
|
* ```typescript
|
|
@@ -21,18 +22,20 @@ export interface ClientFactoryConfig<TCredentials, TClient> {
|
|
|
21
22
|
*/
|
|
22
23
|
export function createClientGetter<TCredentials, TClient>(
|
|
23
24
|
config: ClientFactoryConfig<TCredentials, TClient>
|
|
24
|
-
): (profileName
|
|
25
|
+
): (profileName?: string) => Promise<{ client: TClient; profile: string }> {
|
|
25
26
|
const { service, createClient } = config;
|
|
26
27
|
|
|
27
|
-
return async (profileName
|
|
28
|
-
const profile = await
|
|
28
|
+
return async (profileName?: string): Promise<{ client: TClient; profile: string }> => {
|
|
29
|
+
const { profile, error } = await resolveProfile(service, profileName);
|
|
29
30
|
|
|
30
31
|
if (!profile) {
|
|
31
|
-
|
|
32
|
-
'PROFILE_NOT_FOUND',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if (error === 'none') {
|
|
33
|
+
throw new CliError('PROFILE_NOT_FOUND', `No ${service} profile configured`, `Run: agentio ${service} profile add`);
|
|
34
|
+
}
|
|
35
|
+
if (error === 'multiple') {
|
|
36
|
+
throw new CliError('PROFILE_NOT_FOUND', `Multiple ${service} profiles exist`, 'Specify --profile <name>');
|
|
37
|
+
}
|
|
38
|
+
throw new CliError('PROFILE_NOT_FOUND', `Profile "${profileName}" not found for ${service}`, `Run: agentio ${service} profile add`);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const credentials = await getCredentials<TCredentials>(service, profile);
|