@plosson/agentio 0.1.13 → 0.1.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,12 +1,12 @@
1
1
  import { Command } from 'commander';
2
- import { basename } from 'path';
2
+ import { basename, join } from 'path';
3
3
  import { google } from 'googleapis';
4
4
  import { getValidTokens, createGoogleAuth } from '../auth/token-manager';
5
5
  import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
6
6
  import { setProfile, removeProfile, listProfiles } from '../config/config-manager';
7
7
  import { performOAuthFlow } from '../auth/oauth';
8
8
  import { GmailClient } from '../services/gmail/client';
9
- import { printMessageList, printMessage, printSendResult, printArchived, printMarked, raw } from '../utils/output';
9
+ import { printMessageList, printMessage, printSendResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
10
10
  import { CliError, handleError } from '../utils/errors';
11
11
  import { readStdin, resolveProfileName } from '../utils/stdin';
12
12
  import type { GmailAttachment } from '../types/gmail';
@@ -234,6 +234,49 @@ Query Syntax Examples:
234
234
  }
235
235
  });
236
236
 
237
+ gmail
238
+ .command('attachment <message-id>')
239
+ .description('Download attachments from a message')
240
+ .option('--profile <name>', 'Profile name')
241
+ .option('--name <filename>', 'Download specific attachment by filename (downloads all if not specified)')
242
+ .option('--output <dir>', 'Output directory', '.')
243
+ .action(async (messageId: string, options) => {
244
+ try {
245
+ const { client } = await getGmailClient(options.profile);
246
+ const outputDir = options.output;
247
+
248
+ // Download all attachments
249
+ const results = await client.getAllAttachments(messageId);
250
+
251
+ if (results.length === 0) {
252
+ console.log('No attachments found');
253
+ return;
254
+ }
255
+
256
+ // Filter by filename if specified
257
+ const toDownload = options.name
258
+ ? results.filter(({ attachment }) => attachment.filename === options.name)
259
+ : results;
260
+
261
+ if (toDownload.length === 0) {
262
+ throw new CliError('NOT_FOUND', `Attachment not found: ${options.name}`);
263
+ }
264
+
265
+ if (toDownload.length > 1) {
266
+ console.log(`Downloading ${toDownload.length} attachment(s)...\n`);
267
+ }
268
+
269
+ for (const { data, attachment } of toDownload) {
270
+ const outputPath = join(outputDir, attachment.filename);
271
+ await Bun.write(outputPath, data);
272
+ printAttachmentDownloaded(attachment.filename, outputPath, data.length);
273
+ if (toDownload.length > 1) console.log('');
274
+ }
275
+ } catch (error) {
276
+ handleError(error);
277
+ }
278
+ });
279
+
237
280
  // Profile management
238
281
  const profile = gmail
239
282
  .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 parseMessage(message: gmail_v1.Schema$Message): GmailMessage {
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
- return {
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
  }
@@ -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[];
@@ -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');