@plosson/agentio 0.1.13 → 0.1.15
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 +3 -2
- package/src/commands/gmail.ts +113 -2
- package/src/services/gmail/client.ts +78 -4
- package/src/types/gmail.ts +8 -0
- package/src/utils/output.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plosson/agentio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "CLI for LLM agents to interact with communication and tracking services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^14.0.2",
|
|
50
|
-
"googleapis": "^169.0.0"
|
|
50
|
+
"googleapis": "^169.0.0",
|
|
51
|
+
"playwright-core": "^1.57.0"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/commands/gmail.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { basename } from 'path';
|
|
2
|
+
import { basename, join } from 'path';
|
|
3
3
|
import { google } from 'googleapis';
|
|
4
|
+
import { chromium } from 'playwright-core';
|
|
4
5
|
import { getValidTokens, createGoogleAuth } from '../auth/token-manager';
|
|
5
6
|
import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
|
|
6
7
|
import { setProfile, removeProfile, listProfiles } from '../config/config-manager';
|
|
7
8
|
import { performOAuthFlow } from '../auth/oauth';
|
|
8
9
|
import { GmailClient } from '../services/gmail/client';
|
|
9
|
-
import { printMessageList, printMessage, printSendResult, printArchived, printMarked, raw } from '../utils/output';
|
|
10
|
+
import { printMessageList, printMessage, printSendResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
|
|
10
11
|
import { CliError, handleError } from '../utils/errors';
|
|
11
12
|
import { readStdin, resolveProfileName } from '../utils/stdin';
|
|
12
13
|
import type { GmailAttachment } from '../types/gmail';
|
|
13
14
|
|
|
15
|
+
function escapeHtml(text: string): string {
|
|
16
|
+
return text
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"');
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
|
|
15
24
|
const { tokens, profile } = await getValidTokens('gmail', profileName);
|
|
16
25
|
const auth = createGoogleAuth(tokens);
|
|
@@ -234,6 +243,108 @@ Query Syntax Examples:
|
|
|
234
243
|
}
|
|
235
244
|
});
|
|
236
245
|
|
|
246
|
+
gmail
|
|
247
|
+
.command('attachment <message-id>')
|
|
248
|
+
.description('Download attachments from a message')
|
|
249
|
+
.option('--profile <name>', 'Profile name')
|
|
250
|
+
.option('--name <filename>', 'Download specific attachment by filename (downloads all if not specified)')
|
|
251
|
+
.option('--output <dir>', 'Output directory', '.')
|
|
252
|
+
.action(async (messageId: string, options) => {
|
|
253
|
+
try {
|
|
254
|
+
const { client } = await getGmailClient(options.profile);
|
|
255
|
+
const outputDir = options.output;
|
|
256
|
+
|
|
257
|
+
// Download all attachments
|
|
258
|
+
const results = await client.getAllAttachments(messageId);
|
|
259
|
+
|
|
260
|
+
if (results.length === 0) {
|
|
261
|
+
console.log('No attachments found');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Filter by filename if specified
|
|
266
|
+
const toDownload = options.name
|
|
267
|
+
? results.filter(({ attachment }) => attachment.filename === options.name)
|
|
268
|
+
: results;
|
|
269
|
+
|
|
270
|
+
if (toDownload.length === 0) {
|
|
271
|
+
throw new CliError('NOT_FOUND', `Attachment not found: ${options.name}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (toDownload.length > 1) {
|
|
275
|
+
console.log(`Downloading ${toDownload.length} attachment(s)...\n`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const { data, attachment } of toDownload) {
|
|
279
|
+
const outputPath = join(outputDir, attachment.filename);
|
|
280
|
+
await Bun.write(outputPath, data);
|
|
281
|
+
printAttachmentDownloaded(attachment.filename, outputPath, data.length);
|
|
282
|
+
if (toDownload.length > 1) console.log('');
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
handleError(error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
gmail
|
|
290
|
+
.command('export <message-id>')
|
|
291
|
+
.description('Export a message as PDF')
|
|
292
|
+
.option('--profile <name>', 'Profile name')
|
|
293
|
+
.option('--output <path>', 'Output file path', 'message.pdf')
|
|
294
|
+
.action(async (messageId: string, options) => {
|
|
295
|
+
try {
|
|
296
|
+
const { client } = await getGmailClient(options.profile);
|
|
297
|
+
const message = await client.get(messageId, 'html');
|
|
298
|
+
|
|
299
|
+
// Build HTML document - inject header before body content
|
|
300
|
+
const emailHeader = `
|
|
301
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 16px 20px; margin-bottom: 16px; border-bottom: 1px solid #ddd; background: #f9f9f9;">
|
|
302
|
+
<div style="font-size: 1.3em; font-weight: 600; margin-bottom: 12px;">${escapeHtml(message.subject)}</div>
|
|
303
|
+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>From:</strong> ${escapeHtml(message.from)}</div>
|
|
304
|
+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>To:</strong> ${escapeHtml(message.to.join(', '))}</div>
|
|
305
|
+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>Date:</strong> ${escapeHtml(message.date)}</div>
|
|
306
|
+
</div>`;
|
|
307
|
+
|
|
308
|
+
let html: string;
|
|
309
|
+
const body = message.body || '';
|
|
310
|
+
|
|
311
|
+
// Check if body is already a full HTML document
|
|
312
|
+
if (body.trim().toLowerCase().startsWith('<!doctype') || body.trim().toLowerCase().startsWith('<html')) {
|
|
313
|
+
// Inject header after <body> tag
|
|
314
|
+
html = body.replace(/<body[^>]*>/i, (match) => `${match}${emailHeader}`);
|
|
315
|
+
} else {
|
|
316
|
+
// Wrap fragment in minimal HTML
|
|
317
|
+
html = `<!DOCTYPE html>
|
|
318
|
+
<html>
|
|
319
|
+
<head><meta charset="utf-8"></head>
|
|
320
|
+
<body>
|
|
321
|
+
${emailHeader}
|
|
322
|
+
<div style="padding: 0 20px;">${body}</div>
|
|
323
|
+
</body>
|
|
324
|
+
</html>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Launch browser and generate PDF
|
|
328
|
+
console.error('Launching browser...');
|
|
329
|
+
const browser = await chromium.launch({
|
|
330
|
+
channel: 'chrome', // Use system Chrome
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const page = await browser.newPage();
|
|
334
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
335
|
+
await page.pdf({
|
|
336
|
+
path: options.output,
|
|
337
|
+
format: 'A4',
|
|
338
|
+
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await browser.close();
|
|
342
|
+
console.log(`Exported to ${options.output}`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
handleError(error);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
237
348
|
// Profile management
|
|
238
349
|
const profile = gmail
|
|
239
350
|
.command('profile')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { google, gmail_v1 } from 'googleapis';
|
|
2
2
|
import type { OAuth2Client } from 'google-auth-library';
|
|
3
3
|
import { basename } from 'path';
|
|
4
|
-
import type { GmailMessage, GmailListOptions, GmailSendOptions, GmailReplyOptions, GmailAttachment } from '../../types/gmail';
|
|
4
|
+
import type { GmailMessage, GmailListOptions, GmailSendOptions, GmailReplyOptions, GmailAttachment, GmailAttachmentInfo } from '../../types/gmail';
|
|
5
5
|
import { CliError } from '../../utils/errors';
|
|
6
6
|
|
|
7
7
|
// Common MIME types by extension
|
|
@@ -64,7 +64,31 @@ export class GmailClient {
|
|
|
64
64
|
return result;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
private
|
|
67
|
+
private extractAttachments(payload: gmail_v1.Schema$MessagePart | undefined): GmailAttachmentInfo[] {
|
|
68
|
+
const attachments: GmailAttachmentInfo[] = [];
|
|
69
|
+
|
|
70
|
+
const processpart = (part: gmail_v1.Schema$MessagePart): void => {
|
|
71
|
+
if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
|
|
72
|
+
attachments.push({
|
|
73
|
+
id: part.body.attachmentId,
|
|
74
|
+
filename: part.filename,
|
|
75
|
+
mimeType: part.mimeType || 'application/octet-stream',
|
|
76
|
+
size: part.body.size || 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const child of part.parts || []) {
|
|
80
|
+
processpart(child);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (payload) {
|
|
85
|
+
processpart(payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return attachments;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private parseMessage(message: gmail_v1.Schema$Message, includeAttachments: boolean = false): GmailMessage {
|
|
68
92
|
const headers = this.parseHeaders(message.payload?.headers);
|
|
69
93
|
|
|
70
94
|
const parseAddresses = (value?: string): string[] => {
|
|
@@ -72,7 +96,7 @@ export class GmailClient {
|
|
|
72
96
|
return value.split(',').map((addr) => addr.trim());
|
|
73
97
|
};
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
const result: GmailMessage = {
|
|
76
100
|
id: message.id!,
|
|
77
101
|
threadId: message.threadId!,
|
|
78
102
|
subject: headers['subject'] || '(no subject)',
|
|
@@ -83,6 +107,15 @@ export class GmailClient {
|
|
|
83
107
|
snippet: message.snippet || '',
|
|
84
108
|
labels: message.labelIds || [],
|
|
85
109
|
};
|
|
110
|
+
|
|
111
|
+
if (includeAttachments) {
|
|
112
|
+
const attachments = this.extractAttachments(message.payload);
|
|
113
|
+
if (attachments.length > 0) {
|
|
114
|
+
result.attachments = attachments;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
private getBody(payload: gmail_v1.Schema$MessagePart | undefined, preferHtml: boolean = false): string {
|
|
@@ -151,7 +184,7 @@ export class GmailClient {
|
|
|
151
184
|
format: format === 'raw' ? 'raw' : 'full',
|
|
152
185
|
});
|
|
153
186
|
|
|
154
|
-
const message = this.parseMessage(response.data);
|
|
187
|
+
const message = this.parseMessage(response.data, true);
|
|
155
188
|
let body: string;
|
|
156
189
|
|
|
157
190
|
if (format === 'raw') {
|
|
@@ -169,6 +202,47 @@ export class GmailClient {
|
|
|
169
202
|
}
|
|
170
203
|
}
|
|
171
204
|
|
|
205
|
+
async getAllAttachments(messageId: string): Promise<Array<{ data: Buffer; attachment: GmailAttachmentInfo }>> {
|
|
206
|
+
try {
|
|
207
|
+
const message = await this.gmail.users.messages.get({
|
|
208
|
+
userId: 'me',
|
|
209
|
+
id: messageId,
|
|
210
|
+
format: 'full',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const attachments = this.extractAttachments(message.data.payload);
|
|
214
|
+
|
|
215
|
+
if (attachments.length === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const results: Array<{ data: Buffer; attachment: GmailAttachmentInfo }> = [];
|
|
220
|
+
|
|
221
|
+
for (const attachment of attachments) {
|
|
222
|
+
const response = await this.gmail.users.messages.attachments.get({
|
|
223
|
+
userId: 'me',
|
|
224
|
+
messageId,
|
|
225
|
+
id: attachment.id,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (response.data.data) {
|
|
229
|
+
results.push({
|
|
230
|
+
data: Buffer.from(response.data.data, 'base64'),
|
|
231
|
+
attachment,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return results;
|
|
237
|
+
} catch (error: any) {
|
|
238
|
+
if (error instanceof CliError) throw error;
|
|
239
|
+
if (error.code === 404) {
|
|
240
|
+
throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
|
|
241
|
+
}
|
|
242
|
+
throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
172
246
|
async search(query: string, limit: number = 10): Promise<{ messages: GmailMessage[]; total: number }> {
|
|
173
247
|
return this.list({ query, limit });
|
|
174
248
|
}
|
package/src/types/gmail.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface GmailMessage {
|
|
|
9
9
|
snippet: string;
|
|
10
10
|
labels: string[];
|
|
11
11
|
body?: string;
|
|
12
|
+
attachments?: GmailAttachmentInfo[];
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface GmailListOptions {
|
|
@@ -23,6 +24,13 @@ export interface GmailAttachment {
|
|
|
23
24
|
mimeType?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export interface GmailAttachmentInfo {
|
|
28
|
+
id: string;
|
|
29
|
+
filename: string;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
size: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
export interface GmailSendOptions {
|
|
27
35
|
to: string[];
|
|
28
36
|
cc?: string[];
|
package/src/utils/output.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { GmailMessage } from '../types/gmail';
|
|
1
|
+
import type { GmailMessage, GmailAttachmentInfo } from '../types/gmail';
|
|
2
2
|
import type { GChatMessage } from '../types/gchat';
|
|
3
3
|
import type { JiraProject, JiraIssue, JiraTransition, JiraCommentResult, JiraTransitionResult } from '../types/jira';
|
|
4
4
|
import type { SlackSendResult } from '../types/slack';
|
|
5
5
|
|
|
6
|
+
function formatBytes(bytes: number): string {
|
|
7
|
+
if (bytes === 0) return '0 B';
|
|
8
|
+
const k = 1024;
|
|
9
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
10
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
11
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
// Format a list of Gmail messages
|
|
7
15
|
export function printMessageList(messages: GmailMessage[], total: number): void {
|
|
8
16
|
console.log(`Messages (${messages.length} of ~${total})\n`);
|
|
@@ -30,10 +38,41 @@ export function printMessage(msg: GmailMessage & { body: string }): void {
|
|
|
30
38
|
console.log(`Date: ${msg.date}`);
|
|
31
39
|
console.log(`Subject: ${msg.subject}`);
|
|
32
40
|
if (msg.labels.length) console.log(`Labels: ${msg.labels.join(', ')}`);
|
|
41
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
42
|
+
console.log(`Attachments: ${msg.attachments.length}`);
|
|
43
|
+
for (const att of msg.attachments) {
|
|
44
|
+
console.log(` - ${att.filename} (${formatBytes(att.size)}) [${att.id}]`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
33
47
|
console.log('---');
|
|
34
48
|
console.log(msg.body);
|
|
35
49
|
}
|
|
36
50
|
|
|
51
|
+
// Format attachment list
|
|
52
|
+
export function printAttachmentList(attachments: GmailAttachmentInfo[]): void {
|
|
53
|
+
if (attachments.length === 0) {
|
|
54
|
+
console.log('No attachments');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Attachments (${attachments.length})\n`);
|
|
59
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
60
|
+
const att = attachments[i];
|
|
61
|
+
console.log(`[${i + 1}] ${att.filename}`);
|
|
62
|
+
console.log(` Size: ${formatBytes(att.size)}`);
|
|
63
|
+
console.log(` Type: ${att.mimeType}`);
|
|
64
|
+
console.log(` ID: ${att.id}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Format attachment download result
|
|
70
|
+
export function printAttachmentDownloaded(filename: string, path: string, size: number): void {
|
|
71
|
+
console.log(`Downloaded: ${filename}`);
|
|
72
|
+
console.log(` Path: ${path}`);
|
|
73
|
+
console.log(` Size: ${formatBytes(size)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
// Format send/reply result
|
|
38
77
|
export function printSendResult(result: { id: string; threadId: string }): void {
|
|
39
78
|
console.log('Message sent');
|